too much to recall, changelogs will show the truth

This commit is contained in:
Aziz Hasanain 2021-03-06 21:01:31 +03:00
parent 70028c6218
commit 523423b08c
20 changed files with 823 additions and 214 deletions

View File

@ -41,6 +41,7 @@
"usbmux": "^0.1.0",
"v-lazy-image": "^1.4.0",
"vue": "^2.6.11",
"vue-avatar": "^2.3.3",
"vue-confirm-dialog": "^1.0.2",
"vue-feather": "^1.1.1",
"vue-js-popover": "^1.2.1",
@ -65,4 +66,4 @@
"vue-devtools": "^5.1.4",
"vue-template-compiler": "^2.6.11"
}
}
}

View File

@ -24,6 +24,9 @@
<div class="menuBtn">
<feather type="settings" stroke="rgba(152,152,152,0.5)" size="20" @click="$refs.settingsModal.openModal()"></feather>
</div>
<div class="menuBtn">
<feather type="refresh-cw" stroke="rgba(152,152,152,0.5)" size="19" @click="connectWS"></feather>
</div>
<div class="menuBtn">
<feather type="edit" stroke="rgba(36,132,255,0.65)" size="20" @click="composeMessage"></feather>
</div>
@ -43,6 +46,7 @@
:read="chat.read"
:docid="chat.docid"
:showNum="chat.showNum"
:isGroup="chat.personId.startsWith('chat') && !chat.personId.includes('@') && chat.personId.length >= 20"
@deleted="deleteChat(chat)">
</chat>
</simplebar>
@ -129,6 +133,7 @@ export default {
methods: {
markAsRead (val) {
let chatIndex = this.chats.findIndex(obj => obj.personId == val)
if (chatIndex > -1) {
let chat = this.chats[chatIndex]
@ -265,6 +270,13 @@ export default {
}
})
$(document).mousedown(event => {
if (event.which == 3) {
//this is a right click, so electron-context-menu will be appearing momentarily...
ipcRenderer.send('rightClickMessage', null)
}
})
ipcRenderer.send('loaded')
ipcRenderer.on('update_available', () => {
@ -363,7 +375,7 @@ export default {
const notification = {
title: messageData.name,
body: body,
body: body == '' ? 'Attachment' : body,
silent: !this.$store.state.systemSound
}
@ -409,8 +421,8 @@ export default {
if (reactions && reactions.length > 0 && reactions[0].sender != 1 && remote.Notification.isSupported()) {
let reaction = reactions[0]
if (this.$store.state.mutedChats.includes(reaction.personId)) return
if (this.lastNotificationGUID == messageData.guid) return
this.lastNotificationGUID = messageData.guid
if (this.lastNotificationGUID == reaction.guid) return
this.lastNotificationGUID = reaction.guid
const notification = {
title: chatData.author,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -19,6 +19,7 @@ const autoLauncher = new AutoLaunch({
let tray = null
let win = null
let startMinimized = (process.argv || []).indexOf('--hidden') !== -1;
let rightClickedMessage = null
// Scheme must be registered before the app is ready
protocol.registerSchemesAsPrivileged([
@ -27,6 +28,13 @@ protocol.registerSchemesAsPrivileged([
contextMenu({
prepend: (defaultActions, params, browserWindow) => [
{
label: "Tapback",
visible: rightClickedMessage !== null,
click() {
win.webContents.send('reactToMessage', rightClickedMessage)
}
}
]
})
@ -49,7 +57,8 @@ async function createWindow() {
nodeIntegration: true,
preload: path.join(__dirname, 'preload.js'),
enableRemoteModule: true,
devTools: isDevelopment && !process.env.IS_TEST
devTools: isDevelopment && !process.env.IS_TEST,
spellcheck: true
},
icon: path.join(__static, 'icon.png'),
title: 'WebMessage'
@ -103,6 +112,19 @@ async function createWindow() {
}
win.webContents.send('win_id', win.id)
win.webContents.session.on('will-download', (event, item, webContents) => {
item.on('updated', (event, state) => {
if (state === 'interrupted') {
console.log('Download is interrupted but can be resumed')
} else if (state === 'progressing') {
win.setProgressBar(item.getReceivedBytes()/item.getTotalBytes())
}
})
item.once('done', (event, state) => {
win.setProgressBar(-1)
})
})
}
async function loadURL () {
@ -191,6 +213,10 @@ ipcMain.on('loaded', (event) => {
registerShortcuts()
})
ipcMain.on('rightClickMessage', (event, args) => {
rightClickedMessage = args
})
ipcMain.on('app_version', (event) => {
event.sender.send('app_version', { version: app.getVersion() })
})

View File

@ -0,0 +1,30 @@
<template>
<audio class="audioplayer" controls width="100%" @canplay="handleLoad" @canplaythrough="handleLoad" @loadeddata="handleLoad"
:src="`${$store.getters.httpURI}/attachments?path=${encodeURIComponent(path)}&type=${encodeURIComponent(type)}&auth=${encodeURIComponent($store.state.password)}` + ($store.state.transcode ? '&transcode=1' : '')"
/>
</template>
<script>
export default {
name: "AudioPlayer",
props: {
path: { type: String },
type: { type: String },
loadedData: { type: Function }
},
methods: {
handleLoad() {
this.$nextTick(this.loadedData)
}
},
}
</script>
<style lang="scss" scoped>
.audioplayer {
&:focus {
outline: none;
}
}
</style>

118
src/components/Avatar.vue Normal file
View File

@ -0,0 +1,118 @@
// Forked from eliep/vue-avatar
<template>
<div class="vue-avatar--wrapper" :style="[style, customStyle]" aria-hidden="true">
<!-- this img is not displayed; it is used to detect failure-to-load of div background image -->
<img v-if="this.isImage" style="display: none" :src="this.src" @error="onImgError"/>
<span v-show="!this.isImage">{{ userInitial }}</span>
</div>
</template>
<script>
const getInitials = (username) => {
let parts = username.split(/[ -]/)
let initials = ''
for (var i = 0; i < parts.length; i++) {
initials += parts[i].charAt(0)
}
if (initials.length > 3 && initials.search(/[A-Z]/) !== -1) {
initials = initials.replace(/[a-z]+/g, '')
}
initials = initials.substr(0, 3).toUpperCase()
return initials
}
export default {
name: 'avatar',
props: {
username: {
type: String
},
initials: {
type: String
},
color: {
type: String
},
customStyle: {
type: Object
},
inline: {
type: Boolean
},
size: {
type: Number,
default: 50
},
src: {
type: String
},
rounded: {
type: Boolean,
default: true
}
},
data () {
return {
imgError: false
}
},
mounted () {
if (!this.isImage) {
this.$emit('avatar-initials', this.username, this.userInitial)
}
},
computed: {
isImage () {
return !this.imgError && Boolean(this.src)
},
style () {
const style = {
display: this.inline ? 'inline-flex' : 'flex',
width: `${this.size}px`,
height: `${this.size}px`,
borderRadius: this.rounded ? '50%' : 0,
lineHeight: `${(this.size + Math.floor(this.size / 20)) - 1}px`,
paddingLeft: '0.5px',
fontWeight: '500',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
userSelect: 'none'
}
const imgBackgroundAndFontStyle = {
background: `transparent url('${this.src}') no-repeat scroll 0% 0% / ${this.size}px ${this.size}px content-box border-box`
}
const initialBackgroundAndFontStyle = {
background: 'linear-gradient(#6C6C6C, #474747)'
}
const backgroundAndFontStyle = (this.isImage)
? imgBackgroundAndFontStyle
: initialBackgroundAndFontStyle
Object.assign(style, backgroundAndFontStyle)
return style
},
userInitial () {
if (!this.isImage) {
const regex = /<% RGI_Emoji %>|\p{Emoji_Presentation}|\p{Emoji}\uFE0F|\p{Emoji_Modifier_Base}/gu
const variationSelector = /[\u180B-\u180D\uFE00-\uFE0F]|\uDB40[\uDD00-\uDDEF]/gu
let cleanName = this.username.replace(regex, '').replace(variationSelector, '').trim()
if (cleanName.includes(',')) {
cleanName = cleanName.replace(' ', '')
cleanName = cleanName.replace(',', ' ')
}
return getInitials(cleanName).substring(0, 2)
}
return ''
}
},
methods: {
initial: getInitials,
onImgError (evt) {
this.imgError = true
}
}
}
</script>

View File

@ -3,7 +3,12 @@
<div class="chatWrapper" @mousedown="dragMouseDown" :style="{ left: '-'+slideX+'px' }">
<div class="unread" :style="read ? 'background-color: transparent;' : ''"></div>
<div class="avatarContainer">
<img v-if='(docid && docid != 0)' class="avatar" :src="`${$store.getters.httpURI}/contactimg?docid=${encodeURIComponent(docid)}&auth=${encodeURIComponent($store.state.password)}`" />
<avatar v-if="(docid && docid != 0) || isGroup" class='avatar'
:username="author"
:size="40"
inline
:src="`${$store.getters.httpURI}/contactimg?docid=${encodeURIComponent(isGroup ? chatid : docid)}&auth=${encodeURIComponent($store.state.password)}`"
/>
<img v-else class="avatar" src="../assets/profile.jpg" />
</div>
<div class="chatContent">
@ -31,9 +36,10 @@
<script>
import moment from 'moment'
import TypingIndicator from './TypingIndicator.vue'
import Avatar from './Avatar'
export default {
components: { TypingIndicator },
components: { TypingIndicator, Avatar },
name: 'Chat',
props: {
chatid: { type: String },
@ -42,7 +48,8 @@ export default {
text: { type: String },
date: { type: Number },
read: { type: Boolean },
showNum: { type: Boolean }
showNum: { type: Boolean },
isGroup: { type: Boolean }
},
data () {
return {

View File

@ -34,9 +34,10 @@ export default {
methods: {
download () {
let a = document.createElement('a')
document.body.appendChild(a)
a.download = this.path.split('/').pop()
a.target = '_blank'
a.download = this.type
a.href = this.url
document.body.appendChild(a)
a.click()
a.remove()
}

View File

@ -1,7 +1,7 @@
<template>
<div class="expandable-image" :class="{ expanded: expanded, nostyle: $store.state.macstyle }" ref="this">
<template v-if="loadedImage">
<i v-if="!expanded" class="expand-button" @click="expanded = true">
<i v-if="!expanded" class="expand-button" @click="expandImage">
<feather type="maximize-2" stroke="#fff" size="24"></feather>
</i>
<i v-if="!expanded" class="download-button" @click="download">
@ -22,66 +22,67 @@ export default {
data () {
return {
expanded: false,
loadedImage: false
loadedImage: false,
cloned: null
}
},
computed: {
url() {
return `${this.$store.getters.httpURI}/attachments?path=${encodeURIComponent(this.path)}&type=${encodeURIComponent(this.type)}&auth=${encodeURIComponent(this.$store.state.password)}`
return `${this.$store.getters.httpURI}/attachments?path=${encodeURIComponent(this.path)}&type=${encodeURIComponent(this.type)}&auth=${encodeURIComponent(this.$store.state.password)}` + (this.$store.state.transcode ? '&transcode=1' : '')
}
},
mounted () {
mounted() {
document.addEventListener('keydown', this.closeImage)
},
beforeDestroy () {
document.removeEventListener('keydown', this.closeImage)
},
methods: {
closeImage (event) {
this.expanded = false
event.stopPropagation()
if (!this.cloned) return
this.$nextTick(() => {
this.cloned.style.opacity = 0
this.cloned.removeEventListener('touchmove', this.freezeVp, false)
this.cloned.removeEventListener('click', this.onExpandedImageClick)
setTimeout(() => {
this.cloned.remove()
this.cloned = null
this.expanded = false
}, 250)
})
},
expandImage() {
this.expanded = true
this.$nextTick(() => {
this.cloned = this.$el.cloneNode(true)
document.body.appendChild(this.cloned)
$(this.cloned).addClass('expanded')
this.cloned.addEventListener('touchmove', this.freezeVp, false)
this.cloned.addEventListener('click', this.onExpandedImageClick)
this.$nextTick(() => this.cloned.style.opacity = 1)
})
},
freezeVp (e) {
e.preventDefault()
},
onExpandedImageClick (e) {
e.stopPropagation()
this.expanded = false
this.closeImage(e)
},
download () {
let a = document.createElement('a')
document.body.appendChild(a)
a.download = this.path.split('/').pop()
a.target = '_blank'
a.download = this.type
a.href = this.url
document.body.appendChild(a)
a.click()
a.remove()
},
handleLoad () {
this.$nextTick(() => this.loadedData(true))
$(this.$refs.this).imagesLoaded().done(() => {
this.$nextTick(() => this.loadedData(true))
this.loadedImage = true
})
}
},
watch: {
expanded (status) {
this.$nextTick(() => {
if (status) {
this.cloned = this.$el.cloneNode(true)
document.body.appendChild(this.cloned)
this.cloned.addEventListener('touchmove', this.freezeVp, false);
this.cloned.addEventListener('click', this.onExpandedImageClick)
setTimeout(() => {
this.cloned.style.opacity = 1
}, 0)
} else {
this.cloned.style.opacity = 0
this.cloned.removeEventListener('touchmove', this.freezeVp, false);
this.cloned.removeEventListener('click', this.onExpandedImageClick)
setTimeout(() => {
this.cloned.remove()
this.cloned = null
}, 250)
}
})
this.$nextTick(this.loadedData)
this.loadedImage = true
}
}
}
@ -91,7 +92,6 @@ export default {
.expandable-image {
position: relative;
transition: 0.25s opacity;
/* cursor: zoom-in; */
&.nostyle {
border-radius: 10px;

View File

@ -18,59 +18,64 @@
<img src="@/assets/loading.webp" style="height:18px;" />
</div>
<template v-else-if="$route.params.id != 'new' || this.receiver != ''">
<reactionMenu :target="reactingMessage" :reactions="reactingMessageReactions" :guid="reactingMessageGUID" :part="reactingMessagePart" @close="closeReactionMenu" @sendReaction="sendReaction"></reactionMenu>
<reactionMenu :target="reactingMessage" :reactions="reactingMessageReactions" :guid="reactingMessageGUID" :part="reactingMessagePart" :balloon="reactingToBalloon" @close="closeReactionMenu" @sendReaction="sendReaction"></reactionMenu>
<simplebar class="messages" ref="messages">
<div v-for="(msg, i) in sortedMessages" :key="'msg'+msg.id">
<div class="timegroup" v-html="dateGroup(i-1, i)" v-if="dateGroup(i-1, i) != ''"></div>
<div v-if="msg.group && msg.sender != 1" class="senderName" v-html="$options.filters.twemoji(msg.author)"></div>
<div :ref="'msg'+msg.id" :class="(msg.sender == 1 ? 'send ' : 'receive ') + msg.type + (sortedMessages.length-1 == i ? ' last' : '')" class="messageGroup">
<div v-if="msg.group && msg.sender != 1" class="senderName" v-html="$options.filters.twemoji(msg.author)"></div>
<div class="authorAvatar" v-if="msg.group && msg.sender != 1">
<avatar :username="msg.author" :size="28" inline :customStyle="{ fontSize: '10px', fontWeight: '400' }"
:src="`${$store.getters.httpURI}/contactimg?docid=${msg.authorDocid}&auth=${encodeURIComponent($store.state.password)}`"
/>
</div>
<template v-for="(text, ii) in msg.texts">
<div :key="'wrapper'+ii" :id="'msg'+msg.id+'-text'+ii" class="textWrapper">
<div
v-for="(attachment, index) in text.attachments" :key="`${ii}-${index}`"
class="attachment" :id="'msg'+msg.id+'-text'+ii+'-part'+index"
@mousedown.left="startInterval(msg.id, ii, text.guid, text.reactions, index)"
@mouseup.left="stopInterval" @mouseleave="stopInterval">
<reactions :click="() => openReactionMenu(msg.id, ii, text.guid, text.reactions, index)"
:target="'#msg'+msg.id+'-text'+ii" :part="index" :reactions="text.reactions"
:targetFromMe="msg.sender == 1"></reactions>
<template v-if="attachment[0] != '' && !attachment[0].includes('.pluginPayloadAttachment')">
<expandable-image v-if="isImage(attachment[1])" :loadedData="scrollToBottom" :path="attachment[0]" :type="attachment[1]" />
<video-player v-else-if="isVideo(attachment[1])" :loadedData="scrollToBottom" :path="attachment[0]" :type="attachment[1]" />
<download-attachment v-else :path="attachment[0]" :type="attachment[1]" />
</template>
</div>
<div v-if="$options.filters.twemoji(text.text) != '' || text.undisplayable" :id="'msg'+msg.id+'-text'+ii+'-part'+text.attachments.length" class="bubbleWrapper">
<reactions :click="() => openReactionMenu(msg.id, ii, text.guid, text.reactions, text.attachments.length)"
:target="'#msg'+msg.id+'-text'+ii" :part="text.attachments.length"
:reactions="text.reactions" :targetFromMe="msg.sender == 1"></reactions>
<div class="groupWrapper">
<template v-for="(text, ii) in msg.texts">
<div :key="'wrapper'+ii" :id="'msg'+msg.id+'-text'+ii" class="textWrapper">
<div
class="message"
@mousedown.left="startInterval(msg.id, ii, text.guid, text.reactions, text.attachments.length)" @mouseup.left="stopInterval" @mouseleave="stopInterval"
:key="'msg'+msg.id+'-text'+ii"
:class="(msg.texts.length-1 == ii ? 'last ' : '') + (isEmojis(text.text) ? 'jumbo' : '')"
:style="msg.sender == 1 && text.showStamp && (text.read > 0 || text.delivered > 0) ? 'margin-bottom: 0px;' : ''">
<div class="subject" v-if="text.subject && text.subject != ''" v-html="$options.filters.twemoji(text.subject)"></div>
v-for="(attachment, index) in text.attachments" :key="`${ii}-${index}`"
class="attachment" :id="'msg'+msg.id+'-text'+ii+'-part'+index"
@mousedown.left="startInterval(msg.id, ii, text.guid, text.reactions, index)"
@mouseup.left="stopInterval" @mouseleave="stopInterval" @mousemove="stopIntervalWhen"
@click.right="rightClickMessage({ id: msg.id, ii, guid: text.guid, reactions: text.reactions, part: index })">
<reactions :click="() => openReactionMenu(msg.id, ii, text.guid, text.reactions, index)"
:target="'#msg'+msg.id+'-text'+ii" :part="index" :reactions="text.reactions"
:targetFromMe="msg.sender == 1"></reactions>
<template v-if="attachment[0] != '' && !attachment[0].endsWith('.pluginPayloadAttachment')">
<expandable-image v-if="isImage(attachment[1])" :loadedData="attachmentLoaded" :path="attachment[0]" :type="attachment[1]" />
<video-player v-else-if="isVideo(attachment[1])" :loadedData="attachmentLoaded" :path="attachment[0]" :type="attachment[1]" />
<audio-player v-else-if="isAudio(attachment[0])" :loadedData="attachmentLoaded" :path="attachment[0]" :type="attachment[1]" />
<download-attachment v-else :path="attachment[0]" :type="attachment[1]" />
</template>
</div>
<span style="white-space: pre-wrap;" v-if="!text.undisplayable" v-html="$options.filters.twemoji(text.text)" v-linkified></span>
<div style="white-space: pre-wrap;text-align:center;" v-else>
<div style="font-weight:500;font-size:14px;">Unsupported Type</div>
<div style="font-size:11px;margin-top:4px;">
This message cannot be viewed by WebMessage.
</div>
<feather type="frown" style="height:24px;" stroke="rgb(29,29,29)" size="16"></feather>
<div v-if="$options.filters.twemoji(text.text) != '' || text.undisplayable" :id="'msg'+msg.id+'-text'+ii+'-part'+text.attachments.length" class="bubbleWrapper">
<reactions :click="() => openReactionMenu(msg.id, ii, text.guid, text.reactions, text.attachments.length, text.balloon)"
:target="'#msg'+msg.id+'-text'+ii" :part="text.attachments.length"
:reactions="text.reactions" :targetFromMe="msg.sender == 1" :balloon="text.balloon"></reactions>
<div
class="message"
@mousedown.left="startInterval(msg.id, ii, text.guid, text.reactions, text.attachments.length, text.balloon)" @mouseup.left="stopInterval" @mouseleave="stopInterval"
@mousemove="stopIntervalWhen" @click.right="rightClickMessage({ id: msg.id, ii, guid: text.guid, reactions: text.reactions, part: text.attachments.length, balloon: text.balloon })"
:key="'msg'+msg.id+'-text'+ii"
:class="{ last: msg.texts.length-1 == ii, jumbo: isEmojis(text.text), payload: text.undisplayable || text.payload }"
:style="msg.sender == 1 && text.showStamp && (text.read > 0 || text.delivered > 0) ? 'margin-bottom: 0px;' : ''">
<div class="subject" v-if="text.subject && text.subject != ''" v-html="$options.filters.twemoji(text.subject)"></div>
<span style="white-space: pre-wrap;" v-if="!text.undisplayable && !text.payload" v-html="$options.filters.twemoji(text.text)" v-linkified></span>
<payload-attachment v-else-if="!text.undisplayable && text.payload" :payloadData="text.payload" :loadedData="attachmentLoaded" @refreshRequest="reloadMessage(text.guid)" />
<unsupported-message v-else />
</div>
</div>
<div class="receipt" :key="'msg'+msg.id+'-text'+ii+'-receipt'" v-if="msg.sender == 1 && text.showStamp && (text.read > 0 || text.delivered > 0)">
<span class="type">{{ text.read > 0 ? "Read" : "Delivered" }}</span> {{ humanReadableTimestamp(text.read > 0 ? text.read : text.delivered) }}
</div>
</div>
</div>
<div class="receipt" :key="'msg'+msg.id+'-text'+ii+'-receipt'" v-if="msg.sender == 1 && text.showStamp && (text.read > 0 || text.delivered > 0)">
<span class="type">{{ text.read > 0 ? "Read" : "Delivered" }}</span> {{ humanReadableTimestamp(text.read > 0 ? text.read : text.delivered) }}
</div>
</template>
</template>
</div>
</div>
</div>
@ -91,15 +96,19 @@
</div>
</div>
<div class="msgTextboxWrapper">
<input type="text" v-model="subjectInput" v-if="$store.state.subjectLine" class="subjectLine" ref="subjectLine" placeholder="Subject" @keyup.enter.exact="sendText" :class="{ noTopBorder: hasAttachments }">
<input type="text" v-model="subjectInput" v-if="$store.state.subjectLine" class="subjectLine" id="subjectInput" ref="subjectLine" placeholder="Subject"
@keyup.enter.exact="sendText" @keyup="sendTypingIndicator(subjectInput)" :class="{ noTopBorder: hasAttachments }">
<twemoji-textarea @contentChanged="autoResize" :placeholder="this.messages[0] ? this.messages[0].type.replace('SMS', 'Text Message') : 'Send a message'"
:emojiData="emojiDataAll"
:emojiGroups="emojiGroups"
:recentEmojisFeat="true"
recentEmojisStorage="local"
:searchEmojisFeat="true"
:initialContent="messageText[$route.params.id]"
:class="(hasAttachments || $store.state.subjectLine) ? 'noTopBorder' : ''"
@enterKey="sendText">
<template v-slot:twemoji-picker-button>
<feather type="smile" fill="rgb(152,152,152)" stroke="rgb(29,29,29)" size="26"></feather>
<feather type="smile" fill="rgb(152,152,152)" stroke="rgb(29,29,29)" size="26" style="margin-top: -1px;"></feather>
</template>
</twemoji-textarea>
</div>
@ -133,6 +142,10 @@ import Reactions from './Reactions'
import axios from 'axios'
import { parseBuffer } from 'bplist-parser'
import TypingIndicator from './TypingIndicator.vue'
import PayloadAttachment from './PayloadAttachment.vue'
import Avatar from './Avatar.vue'
import AudioPlayer from './AudioPlayer.vue'
import UnsupportedMessage from './UnsupportedMessage.vue'
export default {
name: 'Message',
@ -146,7 +159,11 @@ export default {
UploadButton,
ReactionMenu,
Reactions,
TypingIndicator
TypingIndicator,
PayloadAttachment,
Avatar,
AudioPlayer,
UnsupportedMessage,
},
data: function () {
return {
@ -162,23 +179,27 @@ export default {
hasAttachments: false,
interval: null,
timeHolding: 0,
initialX: null,
initialY: null,
reactingMessage: null,
reactingMessageGUID: null,
reactingMessageReactions: null,
reactingMessagePart: 0,
subjectInput: ''
reactingToBalloon: false,
subjectInput: '',
lastTypingValue: ''
}
},
computed: {
emojiDataAll() {
let data = EmojiAllData.filter((obj) => {
return obj.group != 2
return true//obj.group != 2
})
return data
},
emojiGroups() {
let groups = EmojiGroups.filter((obj) => {
return obj.description != '🦲'
return true//obj.description != '🦲'
})
return groups
},
@ -197,7 +218,7 @@ export default {
const groupDates = (date1, date2) => (date2 - date1 < 3600000)
const groupAuthor = (author1, author2) => (author1 == author2)
const groupedMessages = messages.reduce((r, { text, subject, dateRead, dateDelivered, guid, reactions, payload, ...rest }, i, arr) => {
const groupedMessages = messages.reduce((r, { text, subject, dateRead, dateDelivered, guid, reactions, balloonReactions, payload, attachments, balloonBundle, ...rest }, i, arr) => {
const prev = arr[i +-1]
let extras = { }
@ -217,10 +238,16 @@ export default {
parsedData[lastClassname].push(object)
})
extras.data = parsedData
extras.payload = parsedData
} catch (error) {
extras.undisplayable = true
}
let supportedFormats = ['', 'com.apple.messages.URLBalloonProvider']
if (!extras.undisplayable && balloonBundle && !supportedFormats.includes(balloonBundle)) {
extras.undisplayable = true
attachments = []
}
}
if (prev && groupAuthor(rest.author, prev.author) && groupAuthor(rest.sender, prev.sender) && groupDates(rest.date, prev.date)) {
@ -228,13 +255,14 @@ export default {
text: text,
subject: subject,
date: rest.date,
attachments: rest.attachments,
attachments: attachments,
read: dateRead,
delivered: dateDelivered,
guid: guid,
reactions:
reactions,
showStamp: rest.sender == 1 && (!lastSentMessageFound || (!lastReadMessageFound && dateRead > 0)),
balloon: balloonBundle != null,
...extras
})
} else {
@ -244,12 +272,13 @@ export default {
text: text,
subject: subject,
date: rest.date,
attachments: rest.attachments,
attachments: attachments,
read: dateRead,
delivered: dateDelivered,
guid: guid,
reactions: reactions,
showStamp: rest.sender == 1 && (!lastSentMessageFound || (!lastReadMessageFound && dateRead > 0)),
balloon: balloonBundle != null,
...extras
}]
})
@ -299,12 +328,20 @@ export default {
isVideo(type) {
return type.includes('video/')
},
startInterval (msgId, textId, guid, reactions, part) {
isAudio(file) {
let ext = file.split('.').pop()
let formats = ['mp3', 'caf', 'wav', 'flac', 'm4a', 'wma', 'aac']
return formats.includes(ext.toLowerCase())
},
rightClickMessage(args) {
ipcRenderer.send('rightClickMessage', args)
},
startInterval (msgId, textId, guid, reactions, part, balloon) {
if (!this.interval) {
this.interval = setInterval(() => {
this.timeHolding++
if (this.timeHolding > 7) { //> 0.7 seconds, will trigger at 0.8 seconds
this.openReactionMenu(msgId, textId, guid, reactions, part)
this.openReactionMenu(msgId, textId, guid, reactions, part, balloon)
clearInterval(this.interval)
this.interval = false
this.timeHolding = 0
@ -316,6 +353,18 @@ export default {
clearInterval(this.interval)
this.interval = false
this.timeHolding = 0
this.initialX = null
this.initialY = null
},
stopIntervalWhen (e) {
if (this.interval) {
if (!this.initialX) this.initialX = e.clientX
if (!this.initialY) this.initialY = e.clientY
if (Math.abs(this.initialX - e.clientX) > 4 || Math.abs(this.initialY - e.clientY) > 4) {
this.stopInterval()
}
}
},
closeReactionMenu () {
this.reactingMessage = null
@ -439,13 +488,19 @@ export default {
setTimeout(this.fetchMessages, 1000)
}
},
reloadMessage(guid) {
this.sendSocket({ action: 'getMessageByGUID', data: {
guid: guid,
}
})
},
autoResize (value) {
var el = document.getElementById('twemoji-textarea')
if (value !== false) {
this.$set(this.messageText, this.$route.params.id, value)
this.sendTypingIndicator(value)
}
var el = document.getElementById('twemoji-textarea')
this.$nextTick(() => {
let scrollHeight = el.scrollHeight
el.style.setProperty('height', '22px', 'important')
@ -455,12 +510,29 @@ export default {
}
})
},
openReactionMenu (msgId, textId, guid, reactions, part) {
sendTypingIndicator(value) {
if (this.lastTypingValue == value) return
this.lastTypingValue = value
if (value != '') {
this.sendSocket({ action: 'setIsLocallyTyping', data: {
chatId: this.messages[0].chatId,
typing: true
}})
} else {
this.sendSocket({ action: 'setIsLocallyTyping', data: {
chatId: this.messages[0].chatId,
typing: false
}})
}
},
openReactionMenu (msgId, textId, guid, reactions, part, balloon) {
let el = $('#msg'+msgId+'-text'+textId+'-part'+part)
this.reactingMessageReactions = reactions
this.reactingMessageGUID = guid
this.reactingMessagePart = part
this.reactingMessage = el
this.reactingToBalloon = balloon
},
sendReaction (reactionId, guid, part) {
if (!this.messages[0]) return
@ -492,36 +564,36 @@ export default {
subject: subjectText
}
document.getElementById("twemoji-textarea").innerHTML = ""
this.messageText[this.$route.params.id] = ""
if (this.$refs.subjectLine) this.$refs.subjectLine.value = ""
this.subjectInput = ''
axios.post(this.$store.getters.httpURI+'/sendText', textObj)
.then(response => {
let focusedEl = $(document.activeElement).attr('id')
if (this.$refs.uploadButton) {
this.$refs.uploadButton.clear()
}
this.$nextTick(() => $('#'+focusedEl).focus())
this.canSend = true
this.autoResize(false)
this.subjectInput = ''
document.getElementById("twemoji-textarea").innerHTML = ""
})
.catch(error => {
alert("There was an error while sending your text.\n" + error)
this.canSend = true
this.autoResize(false)
this.subjectInput = ''
document.getElementById("twemoji-textarea").innerHTML = ""
})
},
scrollToBottom (force) {
scrollToBottom () {
if (this.$refs.messages) {
let container = this.$refs.messages.SimpleBar.getScrollElement()
let scrollTo = this.lastHeight ? (container.scrollHeight - this.lastHeight) : container.scrollHeight
if (force && this.offset <= 25) scrollTo = container.scrollHeight
container.scrollTop = scrollTo
if (document.getElementById('twemoji-textarea') && !this.lastHeight) document.getElementById('twemoji-textarea').focus()
$(document).off('click', '.message a[href^="http"]')
$(document).on('click', '.message a[href^="http"]', function(event) {
event.preventDefault()
@ -529,6 +601,10 @@ export default {
})
}
},
attachmentLoaded() {
this.ignoreNextScroll = true
this.scrollToBottom()
},
autoCompleteHooks () {
if (this.$route.params.id == 'new') {
this.$nextTick(() => {
@ -555,7 +631,7 @@ export default {
}
},
previewFiles () {
this.hasAttachments = this.$refs.uploadButton && this.$refs.uploadButton.attachments != null
this.hasAttachments = this.$refs.uploadButton && this.$refs.uploadButton.attachments != null && this.$refs.uploadButton.attachments.length > 0
this.autoResize(false)
},
removeAttachment (i) {
@ -564,11 +640,11 @@ export default {
},
postLoad () {
if (this.offset == 0) {
setTimeout(() => {
this.$nextTick(() => {
if (this.$refs.messages) {
let container = this.$refs.messages.SimpleBar.getScrollElement()
container.addEventListener('scroll', (e) => {
if (this.ignoreNextScroll) {
if (this.ignoreNextScroll && this.lastHeight == null) {
this.ignoreNextScroll = false
return
}
@ -584,13 +660,15 @@ export default {
}
})
}
}, 100)
})
this.$emit('markAsRead', this.$route.params.id)
}
this.ignoreNextScroll = true
this.$nextTick(this.scrollToBottom)
setTimeout(this.scrollToBottom, 10) //Just in case
let el = document.getElementById('twemoji-textarea')
if (el) el.focus()
this.offset += this.limit
this.loading = false
@ -638,6 +716,10 @@ export default {
mounted () {
this.$nextTick(this.autoCompleteHooks)
this.fetchMessages()
ipcRenderer.on('reactToMessage', (e, args) => {
this.openReactionMenu(args.id, args.ii, args.guid, args.reactions, args.part, args.balloon)
})
},
socket: {
fetchMessages (data) {
@ -661,15 +743,16 @@ export default {
this.$set(this.messages, messageIndex, this.messages[messageIndex]) // Reactivity in Vue is weird
if (this.lastHeight == null) {
this.$nextTick(() => { this.scrollToBottom() })
this.ignoreNextScroll = true
this.$nextTick(this.scrollToBottom)
}
}
}
},
setTypingIndicator (data) {
if (data && data.chat_id == this.$route.params.id && this.lastHeight == null) {
this.ignoreNextScroll = true
this.$nextTick(this.scrollToBottom)
setTimeout(this.scrollToBottom, 10)
}
},
newMessage (data) {
@ -690,6 +773,7 @@ export default {
if (this.messages && this.messages.length > 0 && message[0]['personId'] == this.$route.params.id) {
let oldMsgIndex = this.messages.findIndex(obj => obj.guid == message[0].guid)
if (oldMsgIndex != -1) {
this.messages[oldMsgIndex] = message[0]
this.$set(this.messages, oldMsgIndex, message[0])
return
}
@ -698,12 +782,12 @@ export default {
this.messages.unshift(message[0])
if (this.lastHeight == null) {
// this.ignoreNextScroll = true
this.$nextTick(this.scrollToBottom)
setTimeout(this.scrollToBottom, 10) //Just in case
}
if (this.lastHeight == null) {
this.$emit('markAsRead')
this.$emit('markAsRead', this.$route.params.id)
}
}
}
@ -885,7 +969,7 @@ export default {
padding-right: 10px !important;
background: rgba(29,29,29, 1) !important;
border: 1px solid #545454 !important;
line-height: 18px !important;
line-height: 22px !important;
font-size: 13px !important;
margin-left: 6px !important;
margin-right: 6px !important;
@ -911,7 +995,7 @@ export default {
.emoji-popover-inner {
width: auto !important;
height: 182px !important;
height: 170px !important;
background: none !important;
}
}
@ -957,9 +1041,30 @@ export default {
}
#emoji-container {
#emoji-popover-search {
background-color: rgb(50,50,50) !important;
margin: 0 !important;
#search-header {
border: none !important;
border-radius: 6px !important;
&.is-focused {
background-color: rgb(75,75,75) !important;
}
input {
padding: 5px 5px !important;
color: white;
font-size: 14px !important;
}
}
}
#emoji-popover-header {
padding: 5px !important;
padding-bottom: 20px !important;
padding-top: 0px !important;
padding-bottom: 15px !important;
border-bottom: 1px solid lighten(#555, 30%) !important;
}
@ -990,6 +1095,12 @@ export default {
}
}
.textboxContainer #emoji-container > #emoji-popup #emoji-popover-search > #search-header > span {
width: 2px !important;
padding: 0px 10px;
margin-top: -2px;
}
.emoji-picker__search {
display: flex;
}
@ -1047,7 +1158,8 @@ export default {
width: 22px;
height: 22px;
border-radius: 50%;
background: #2284FF;
background: #1287FF;
margin-left: -20px;
float: right;
display: flex;
justify-content: center;
@ -1170,7 +1282,7 @@ export default {
.senderName {
color: #999999;
font-size: 0.85em;
margin-left: 10px;
margin-left: 54px;
}
}
@ -1179,7 +1291,23 @@ export default {
padding-left: 20px;
padding-right: 20px;
display: flex;
flex-direction: column;
flex-direction: row;
.groupWrapper {
// display: flex;
width: 100%;
display: inline-flex;
flex-direction: column;
}
.authorAvatar {
z-index: 2;
margin-left: -10px;
margin-top: 0px;
margin-right: 8px;
display: inline-flex;
align-self: flex-end;
}
&.last {
margin-bottom: 0px;
@ -1222,15 +1350,18 @@ export default {
}
.message {
border-radius: 18px;
border-radius: 14px;
padding: 6px 10px;
margin-top: 0px;
margin-bottom: 0px;
max-width: 100%;
display: inline-block;
text-align: start;
unicode-bidi: plaintext;
&.payload {
padding: 0;
}
.subject {
white-space: pre-wrap;
font-weight: 500;
@ -1244,9 +1375,16 @@ export default {
.send.iMessage {
.message {
background-color: #2284FF !important;
background-color: #1287FF !important;
&:before {
background: #2284FF !important;
background: #1287FF !important;
}
&.payload {
background-color: #3A3A3C !important;
&:before {
background: #3A3A3C !important;
}
}
}
}
@ -1316,6 +1454,7 @@ export default {
.send {
align-items: flex-end;
justify-content: flex-end;
.textWrapper {
display: flex;

View File

@ -0,0 +1,191 @@
<template>
<div class='URLContainer'>
<div class='bigImage' v-if="large && icon" @click="openURLFromImage">
<img :src="icon" @click="showEmbed = true" @load="handleLoad" />
<iframe v-if="embed && showEmbed" :src="embed" frameborder="0"></iframe>
<video v-else-if="video && showEmbed" controls width="100%" @keypress.esc="escape" ref="videoPlayer">
<source :src="video"
type="video/mp4" />
This video type is not supported.
</video>
<div class='playIcon' v-if="!showEmbed && (embed || video)" @click="showEmbed = true">
<feather type="play" stroke="rgb(200,200,200)" size="24"></feather>
</div>
</div>
<div class='URLInfo' :class="{ large: large }" @click="openURL">
<div class='URLText'>
<div class='URLTitle' v-if="title">
{{ title }}
</div>
<div class='URLSubtitle'>
{{ subtitle }}
</div>
</div>
<div class='URLIcon'>
<img v-if="!large && icon" :src="icon" @load="handleLoad" />
<feather v-else type="chevron-right" stroke="#A7A7A7" size="16"></feather>
</div>
</div>
</div>
</template>
<script>
export default {
name: "PayloadAttachment",
props: {
loadedData: { type: Function },
payloadData: { type: Object }
},
data() {
return {
icon: null,
url: null,
title: null,
subtitle: null,
large: false,
embed: null,
link: '',
showEmbed: false,
vide: null,
}
},
watch: {
payloadData() {
this.populateValues()
}
},
mounted() {
this.populateValues()
},
methods: {
populateValues() {
console.log(this.payloadData)
this.icon = this.payloadData.LPIconMetadata ? this.payloadData.LPIconMetadata[0] : null
this.large = this.payloadData.LPImageMetadata != null || (this.payloadData.LPIconMetadata && this.payloadData.LPIconMetadata[1] != '{0, 0}')
this.title = this.payloadData.NSURL ? this.payloadData.NSURL[1] : null
this.subtitle = this.payloadData.root[0].replace('http://', '').replace('https://', '').replace('www.', '').split('/')[0].toLowerCase()
this.embed = (this.payloadData.LPImageMetadata && this.payloadData.LPVideo && this.payloadData.LPVideo[0] == 'text/html') ? this.payloadData.LPImageMetadata[0] : null
this.link = this.payloadData.root[0]
this.video = this.payloadData.RichLinkVideoAttachmentSubstitute ? this.payloadData.RichLinkVideoAttachmentSubstitute[0] : null
if (this.title && this.title.length > 82) {
this.title = this.title.substring(0, 82).trim() + ' ...'
}
if (!this.title) {
setTimeout(() => {
this.$emit('refreshRequest')
}, 1000)
}
},
openURL() {
shell.openExternal(this.link)
},
openURLFromImage() {
if (this.embed || this.video) return
this.openURL()
},
handleLoad() {
this.$nextTick(this.loadedData)
}
},
}
</script>
<style lang="scss" scoped>
.URLContainer {
border-radius: 14px;
cursor: pointer;
.bigImage {
border-radius: 14px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
overflow: hidden;
width: fit-content;
position: relative;
img {
max-width: 100%;
border-radius: 14px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
iframe, video {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: black;
}
video {
&:focus {
outline: none;
}
}
.playIcon {
position: absolute;
top: 50%;
left: 50%;
transform: translateY(-50%) translateX(-50%);
width: 50px;
height: 50px;
border-radius: 50%;
background: rgba(25,25,25,0.4);
display: flex;
align-items: center;
justify-content: center;
/deep/ .feather {
margin-left: 1px;
}
}
}
.URLInfo {
background: #3B3B3D;
border-radius: 14px;
padding: 8px 15px;
padding-right: 10px;
display: flex;
flex-direction: row;
flex-grow: 1;
&.large {
max-width: fit-content;
}
.URLText {
display: inline-flex;
flex-direction: column;
margin-right: 10px;
.URLTitle {
font-weight: 500;
margin-bottom: 2px;
}
.URLSubtitle {
color: #A7A7A7;
}
}
.URLIcon {
display: inline-flex;
flex: 1;
align-items: center;
justify-content: flex-end;
img {
width: 32px;
}
}
}
}
</style>

View File

@ -34,7 +34,8 @@ export default {
target: { type: Object },
guid: { type: String },
part: { type: Number },
reactions: { type: Array }
reactions: { type: Array },
balloon: { type: Boolean, default: false }
},
watch: {
target(newTarget) {
@ -53,7 +54,7 @@ export default {
if (newTarget == null) return
let target = newTarget.children().last()
let parent = newTarget.parent().parent()
let parent = newTarget.parent().parent().parent()
let p = target.offset()
let w = target.width()
let h = target.height()
@ -93,7 +94,7 @@ export default {
this.position.left = (p.left - 15) + 'px'
}
let activeReactionsList = this.reactions.filter(reaction => reaction.reactionType >= 2000 && reaction.reactionType < 3000 && reaction.sender == 1 && reaction.forPart == this.part)
let activeReactionsList = this.reactions.filter(reaction => reaction.reactionType >= 2000 && reaction.reactionType < 3000 && reaction.sender == 1 && reaction.forPart == (this.balloon ? 'b' : this.part))
this.activeReactions = []
activeReactionsList.forEach((reaction) => {
this.activeReactions.push(reaction.reactionType)
@ -175,7 +176,7 @@ export default {
content: "";
position: absolute;
bottom: -5px;
right: 20px;
right: 11px;
width: 10px;
height: 10px;
border-radius: 50%;
@ -185,8 +186,8 @@ export default {
&:before {
content: "";
position: absolute;
bottom: -12px;
right: 19px;
bottom: -11px;
right: 9px;
width: 6px;
height: 6px;
border-radius: 50%;
@ -195,12 +196,12 @@ export default {
&.left {
&:after {
left: 20px;
left: 11px;
right: unset;
}
&:before {
left: 19px;
left: 9px;
right: unset;
}
}

View File

@ -22,11 +22,13 @@ export default {
targetFromMe: { type: Boolean },
target: { type: String },
part: { type: Number },
click: { type: Function }
click: { type: Function },
balloon: { type: Boolean, default: false },
},
computed: {
reactionList() {
let reactions = this.reactions.filter(reaction => reaction.reactionType >= 2000 && reaction.reactionType < 3000 && reaction.forPart == this.part)
if (!this.reactions) return []
let reactions = this.reactions.filter(reaction => reaction.reactionType >= 2000 && reaction.reactionType < 3000 && reaction.forPart == (this.balloon ? 'b' : this.part))
let reactionFromMe = null
reactions.forEach((reaction) => {

View File

@ -6,67 +6,74 @@
<div class="modal__dialog">
<h3>Settings</h3>
<div class="selectors">
<div class="selector" :class="{ active: activeView == 'tweak'}" @click="activeView = 'tweak'">Tweak</div>
<div class="selector" :class="{ active: activeView == 'client'}" @click="activeView = 'client'">Client</div>
</div>
<div class="settingsWrapper">
<div class="settingsColumn">
<h4>Tweak</h4>
<input type="password" placeholder="Password" class="textinput" v-model="password" />
<input type="text" placeholder="IP Address" class="textinput" v-model="ipAddress" :disabled="this.enableTunnel"/>
<div class="tunnelToggle">
<feather type="circle" size="20" @click="toggleTunnel" :fill="relayColor" v-popover:tunnel.bottom></feather>
</div>
<input ref="portField" type="number" placeholder="Port" class="textinput" min="1" max="65535" @keyup="enforceConstraints" v-model="port" />
<label class="switch">
<input type="checkbox" v-model="ssl">
<i></i>
<div>Enable SSL</div>
</label>
</div>
<div class="settingsColumn">
<h4>Client</h4>
<label class="switch">
<input type="checkbox" v-model="subjectLine">
<i></i>
<div>Enable subject line</div>
</label>
<label class="switch">
<input type="checkbox" v-model="systemSound">
<i></i>
<div>Use system notification sound</div>
</label>
<label class="switch">
<input type="checkbox" v-model="cacheMessages">
<i></i>
<div>Precache messages <span style="color: rgba(255,0,0,0.8);font-size: 12px;">More battery drain</span></div>
</label>
<label class="switch">
<input type="checkbox" v-model="startup">
<i></i>
<div>Launch on startup</div>
</label>
<label class="switch">
<input type="checkbox" v-model="minimize">
<i></i>
<div>Keep in tray</div>
</label>
<label class="switch" v-if="process.platform !== 'darwin'">
<input type="checkbox" v-model="macstyle">
<i></i>
<div>Use macOS style</div>
</label>
<label class="switch">
<input type="checkbox" v-model="acceleration">
<i></i>
<div>Enable hardware acceleration</div>
</label>
<label class="file">
<div>Select custom notification file:</div>
<input type="file" name="soundFile" ref="soundFile" style="display: none;" @change="notifSoundChanged" accept="audio/*">
<div class="fileBtn" @click.prevent="$refs.soundFile.click">
Browse ({{ this.notifSound.includes('wm-audio') ? 'Default' : this.notifSound.split('/').pop().split('\\').pop() }})
<div class="settingsColumn" v-if="activeView == 'tweak'">
<input type="password" placeholder="Password" class="textinput" v-model="password" />
<input type="text" placeholder="IP Address" class="textinput" v-model="ipAddress" :disabled="this.enableTunnel"/>
<div class="tunnelToggle">
<feather type="circle" size="20" @click="toggleTunnel" :fill="relayColor" v-popover:tunnel.bottom></feather>
</div>
<div class="fileBtn" @click.prevent="notifSound = 'wm-audio://receivedText.mp3'" style="margin-left: 8px;">Reset</div>
</label>
</div>
<input ref="portField" type="number" placeholder="Port" class="textinput" min="1" max="65535" @keyup="enforceConstraints" v-model="port" />
<label class="switch">
<input type="checkbox" v-model="ssl">
<i></i>
<div>Enable SSL</div>
</label>
</div>
<div class="settingsColumn" v-if="activeView == 'client'">
<label class="switch">
<input type="checkbox" v-model="subjectLine">
<i></i>
<div>Enable subject line</div>
</label>
<label class="switch">
<input type="checkbox" v-model="transcode">
<i></i>
<div>Convert Apple formats <span style="font-size: 12px;">(mov, heic, caf)</span></div>
</label>
<label class="switch">
<input type="checkbox" v-model="systemSound">
<i></i>
<div>Use system notification sound</div>
</label>
<label class="switch">
<input type="checkbox" v-model="cacheMessages">
<i></i>
<div>Precache messages <span style="color: rgba(255,0,0,0.8);font-size: 12px;">More battery drain</span></div>
</label>
<label class="switch">
<input type="checkbox" v-model="startup">
<i></i>
<div>Launch on startup</div>
</label>
<label class="switch">
<input type="checkbox" v-model="minimize">
<i></i>
<div>Keep in tray</div>
</label>
<label class="switch" v-if="process.platform !== 'darwin'">
<input type="checkbox" v-model="macstyle">
<i></i>
<div>Use macOS style</div>
</label>
<label class="switch">
<input type="checkbox" v-model="acceleration">
<i></i>
<div>Enable hardware acceleration</div>
</label>
<label class="file">
<div>Select custom notification file:</div>
<input type="file" name="soundFile" ref="soundFile" style="display: none;" @change="notifSoundChanged" accept="audio/*">
<div class="fileBtn" @click.prevent="$refs.soundFile.click">
Browse ({{ this.notifSound.includes('wm-audio') ? 'Default' : this.notifSound.split('/').pop().split('\\').pop() }})
</div>
<div class="fileBtn" @click.prevent="notifSound = 'wm-audio://receivedText.mp3'" style="margin-left: 8px;">Reset</div>
</label>
</div>
</div>
<a class="btn" v-on:click="saveModal">Save</a>
@ -93,6 +100,7 @@ export default {
port: 8180,
ssl: false,
subjectLine: false,
transcode: true,
systemSound: false,
launchOnStartup: false,
minimize: true,
@ -106,7 +114,8 @@ export default {
relayColor: 'rgba(152,152,152,0.5)',
enableTunnel: false,
cacheMessages: false,
notifSound: 'wm-audio://receivedText.mp3'
notifSound: 'wm-audio://receivedText.mp3',
activeView: 'tweak'
}
},
beforeDestroy () {
@ -186,6 +195,7 @@ export default {
this.$store.commit('setPort', this.port)
this.$store.commit('setSSL', this.ssl)
this.$store.commit('setSubjectLine', this.subjectLine)
this.$store.commit('setTranscode', this.transcode)
this.$store.commit('setSystemSound', this.systemSound)
this.$store.commit('setStartup', this.startup)
this.$store.commit('setMinimize', this.minimize)
@ -208,6 +218,7 @@ export default {
},
openModal() {
this.show = true
this.activeView = 'tweak'
},
loadValues() {
this.password = this.$store.state.password
@ -215,6 +226,7 @@ export default {
this.port = this.$store.state.port
this.ssl = this.$store.state.ssl
this.subjectLine = this.$store.state.subjectLine
this.transcode = this.$store.state.transcode
this.systemSound = this.$store.state.systemSound
this.startup = this.$store.state.startup
this.minimize = this.$store.state.minimize
@ -320,13 +332,39 @@ export default {
position: relative;
top: 50%;
transform: translateY(-50%);
max-width: 650px;
max-width: 300px;
margin: auto auto;
display: flex;
flex-direction: column;
border-radius: 10px;
z-index: 2;
.selectors {
font-weight: 400;
color: rgb(200,200,200);
.selector {
display: inline-block;
margin-right: 10px;
margin-bottom: 20px;
cursor: pointer;
&:last-of-type {
margin-right: 0px;
}
&:hover {
color: white;
}
&.active {
font-weight: 500;
text-decoration: underline;
color: white;
}
}
}
.settingsWrapper {
display: flex;
justify-content: space-around;

View File

@ -29,9 +29,10 @@ export default {
padding: 10px;
display: table;
margin: 0 auto;
margin-top: 1px;
position: relative;
animation: 2s bulge infinite ease-out;
margin-bottom: 5px;
margin-bottom: 7px;
margin-left: 0;
&::before,
@ -39,7 +40,7 @@ export default {
content: '';
position: absolute;
bottom: -2px;
left: -2px;
left: -1px;
height: 10px;
width: 10px;
border-radius: 50%;
@ -48,8 +49,8 @@ export default {
&::after {
height: 5px;
width: 5px;
left: -5px;
bottom: -5px;
left: -4.5px;
bottom: -6px;
}
span {
height: 7px;

View File

@ -0,0 +1,21 @@
<template>
<div style="white-space: pre-wrap;text-align:center;padding: 6px 10px;margin-top:2px;">
<div style="font-weight:500;font-size:14px;">Unsupported Type</div>
<div style="font-size:11px;margin-top:4px;">
This message cannot be viewed by WebMessage.
</div>
<feather type="frown" style="height:24px;margin-top: 5px;" stroke="rgb(255,255,255)" size="16"></feather>
</div>
</template>
<script>
export default {
name: "UnsupportedMessage",
methods: {
},
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -16,7 +16,7 @@ export default {
},
data() {
return {
attachments: null,
attachments: [],
progress: 0,
isReading: false,
sizeLimit: 100 // In MB. Files larger than this cause the client to crash for some reason.
@ -56,7 +56,7 @@ export default {
},
clear() {
this.isReading = false
this.attachments = null
this.attachments = []
this.$refs.fileInput.value = ''
this.progress = 0
this.$emit('filesChanged')

View File

@ -1,9 +1,11 @@
<template>
<video class="videoplayer" controls width="100%" @canplay="handleLoad" @canplaythrough="handleLoad" @loadeddata="handleLoad">
<source :src="`${$store.getters.httpURI}/attachments?path=${encodeURIComponent(path)}&type=${encodeURIComponent(type)}&auth=${encodeURIComponent($store.state.password)}`"
:type="type.includes('quicktime') ? 'video/mp4' : type" />
<!-- <source :src="`${$store.getters.httpURI}/attachments?path=${encodeURIComponent(path)}&type=${type}&auth=${$store.state.password}`"
type="video/ogg" /> -->
<video class="videoplayer" controls width="100%" @canplay="handleLoad" @canplaythrough="handleLoad" @loadeddata="handleLoad" @keypress.esc="escape" ref="videoPlayer">
<source :src="`${$store.getters.httpURI}/attachments?path=${encodeURIComponent(path)}&type=${encodeURIComponent(type)}&auth=${encodeURIComponent($store.state.password)}` + ($store.state.transcode ? '&transcode=1' : '')"
:type="type" />
<source :src="`${$store.getters.httpURI}/attachments?path=${encodeURIComponent(path)}&type=${encodeURIComponent(type)}&auth=${encodeURIComponent($store.state.password)}` + ($store.state.transcode ? '&transcode=1' : '')"
type="video/mp4" />
<source :src="`${$store.getters.httpURI}/attachments?path=${encodeURIComponent(path)}&type=${encodeURIComponent(type)}&auth=${encodeURIComponent($store.state.password)}` + ($store.state.transcode ? '&transcode=1' : '')"
type="video/ogg" />
This video type is not supported.
</video>
</template>
@ -18,10 +20,19 @@ export default {
loadedData: { type: Function }
},
mounted() {
document.addEventListener('keydown', this.escape)
},
beforeDestroy () {
document.removeEventListener('keydown', this.escape)
},
methods: {
handleLoad() {
this.$nextTick(this.loadedData)
},
escape(e) {
if (e.key == 'Escape' && this.$refs.videoPlayer && document.fullscreenElement !== null) {
document.exitFullscreen()
}
}
},
}

View File

@ -16,6 +16,7 @@ export default new Vuex.Store({
port: persistentStore.get('port', 8180),
ssl: persistentStore.get('ssl', true),
subjectLine: persistentStore.get('subjectLine', false),
transcode: persistentStore.get('transcode', true),
systemSound: persistentStore.get('systemSound', false),
startup: persistentStore.get('startup', false),
minimize: persistentStore.get('minimize', true),
@ -55,6 +56,10 @@ export default new Vuex.Store({
state['subjectLine'] = subjectLine
persistentStore.set('subjectLine', subjectLine)
},
setTranscode(state, transcode) {
state['transcode'] = transcode
persistentStore.set('transcode', transcode)
},
setSystemSound(state, systemSound) {
state['systemSound'] = systemSound
persistentStore.set('systemSound', systemSound)

View File

@ -10519,6 +10519,11 @@ vm-browserify@^1.0.1:
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
vue-avatar@^2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/vue-avatar/-/vue-avatar-2.3.3.tgz#e125bf4f4a6f4f9480da0c522020266a8609d2a8"
integrity sha512-Z57ILRTkFIAuCH9JiFBxX74C5zua5ub/jRDM/KZ+QKXNfscvmUOgWBs3kA2+wrpZMowIvfLHIT0gvQu1z+zpLg==
vue-cli-plugin-electron-builder@~2.0.0-rc.5:
version "2.0.0-rc.5"
resolved "https://registry.yarnpkg.com/vue-cli-plugin-electron-builder/-/vue-cli-plugin-electron-builder-2.0.0-rc.5.tgz#87cd8d09877f5f3ae339abc0bedc47d7d2b733ac"