mirror of
https://github.com/Cronocide/WebMessage.git
synced 2025-01-22 11:18:25 +00:00
too much to recall, changelogs will show the truth
This commit is contained in:
parent
70028c6218
commit
523423b08c
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
18
src/App.vue
18
src/App.vue
@ -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 |
@ -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() })
|
||||
})
|
||||
|
30
src/components/AudioPlayer.vue
Normal file
30
src/components/AudioPlayer.vue
Normal 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
118
src/components/Avatar.vue
Normal 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>
|
@ -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 {
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
191
src/components/PayloadAttachment.vue
Normal file
191
src/components/PayloadAttachment.vue
Normal 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>
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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) => {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
21
src/components/UnsupportedMessage.vue
Normal file
21
src/components/UnsupportedMessage.vue
Normal 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>
|
@ -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')
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user