mirror of
https://github.com/Cronocide/WebMessage.git
synced 2025-01-22 11:18:25 +00:00
TypeScript + Vue 3 + Elec 11 continuation
This commit is contained in:
parent
58fa723720
commit
1ca699727b
944
src/App.vue
944
src/App.vue
@ -1,952 +1,14 @@
|
||||
<template>
|
||||
<!-- <vue-confirm-dialog class="confirmDialog"></vue-confirm-dialog> -->
|
||||
<settings ref="settingsModal" @saved="connectWS"></settings>
|
||||
<div id="nav" :class="{ notrans: !$store.state.acceleration }">
|
||||
<div class="titlebar">
|
||||
<div class="buttons" v-if="$store.state.macstyle || process.platform === 'darwin'">
|
||||
<div class="close" @click="closeWindow">
|
||||
<span class="closebutton"><span>x</span></span>
|
||||
</div>
|
||||
<div class="minimize" @click="minimizeWindow">
|
||||
<span class="minimizebutton"><span>–</span></span>
|
||||
</div>
|
||||
<div class="zoom" @click="maximizeWindow">
|
||||
<span class="zoombutton"><span>+</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="statusIndicator" style="margin-top: 1px;" v-tooltip:bottom.tooltip="statusText">
|
||||
<feather type="circle" stroke="rgba(25,25,25,0.5)" :fill="statusColor" size="10"></feather>
|
||||
</div>
|
||||
<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>
|
||||
<div class="menuBtn" v-if="updateAvailable">
|
||||
<feather type="download" stroke="rgba(152,255,152,0.65)" size="20" @click="restart"></feather>
|
||||
</div>
|
||||
</div>
|
||||
<div class="searchContainer">
|
||||
<input type="search" placeholder="Search" class="textinput" v-model="search" />
|
||||
</div>
|
||||
<div class="chats scrollable" ref="chats">
|
||||
<chat
|
||||
v-for="chat in filteredChats"
|
||||
:key="chat.id"
|
||||
:chatid="chat.personId"
|
||||
:author="chat.author"
|
||||
:text="chat.text"
|
||||
:date="chat.date"
|
||||
:read="chat.read"
|
||||
:docid="chat.docid"
|
||||
:showNum="chat.showNum"
|
||||
:isGroup="chat.personId.startsWith('chat') && !chat.personId.includes('@') && chat.personId.length >= 20"
|
||||
@deleted="deleteChat(chat)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div id="content">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
<component :is="Component" @markAsRead="markAsRead" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
<home />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Chat from '@/components/Chat.vue'
|
||||
import Settings from '@/components/Settings.vue'
|
||||
import { ipcRenderer, remote } from 'electron'
|
||||
import socketMixin from './mixin/socket'
|
||||
import Tooltip from '@/components/Tooltip.vue'
|
||||
import Home from '@/views/Home'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
Chat,
|
||||
Settings,
|
||||
Tooltip,
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
chats: [],
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
loading: false,
|
||||
notifSound: null,
|
||||
updateAvailable: false,
|
||||
search: '',
|
||||
process: window.process,
|
||||
maximized: false,
|
||||
maximizing: false,
|
||||
win: null,
|
||||
status: 0, // 0 for disconnected, 1 for connecting, 2 for connected,
|
||||
lastNotificationGUID: '',
|
||||
}
|
||||
},
|
||||
mixins: [socketMixin],
|
||||
computed: {
|
||||
filteredChats() {
|
||||
let chats = this.chats.slice()
|
||||
|
||||
if (chats.length > 0) {
|
||||
chats = chats.reduce((r, chat) => {
|
||||
chat.showNum = false
|
||||
|
||||
const duplicateIndex = chats.findIndex(obj => obj.author == chat.author && obj.address != chat.address)
|
||||
if (duplicateIndex > -1 && !chat.address.startsWith('chat') && (chat.address.startsWith('+') || chat.address.includes('@'))) {
|
||||
chat.showNum = true
|
||||
}
|
||||
|
||||
r.push(chat)
|
||||
return r
|
||||
}, [])
|
||||
}
|
||||
|
||||
return chats
|
||||
.filter(chat => {
|
||||
return (
|
||||
chat.author.toLowerCase().includes(this.search.toLowerCase()) || chat.text.toLowerCase().includes(this.search.toLowerCase())
|
||||
)
|
||||
})
|
||||
.sort((a, b) => (b.date - a.date > 0 ? 1 : -1))
|
||||
},
|
||||
statusColor() {
|
||||
if (this.status == 0) {
|
||||
return 'rgba(255,0,0,0.8)'
|
||||
} else if (this.status == 1) {
|
||||
return 'rgba(255,100,0,0.8)'
|
||||
} else if (this.status == 2) {
|
||||
return 'rgba(0,255,0,0.5)'
|
||||
}
|
||||
|
||||
return ''
|
||||
},
|
||||
statusText() {
|
||||
if (this.status == 0) {
|
||||
return 'Device not found'
|
||||
} else if (this.status == 1) {
|
||||
return 'Device found. Retrieving data...'
|
||||
} else if (this.status == 2) {
|
||||
return 'Device connected'
|
||||
}
|
||||
|
||||
return ''
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
markAsRead(val) {
|
||||
const chatIndex = this.chats.findIndex(obj => obj.personId == val)
|
||||
|
||||
if (chatIndex > -1) {
|
||||
const chat = this.chats[chatIndex]
|
||||
|
||||
if (!chat.read) {
|
||||
if (document.hasFocus()) {
|
||||
chat.read = true
|
||||
this.sendSocket({
|
||||
action: 'markAsRead',
|
||||
data: { chatId: this.$route.params.id },
|
||||
})
|
||||
} else {
|
||||
const onFocusHandler = () => {
|
||||
if (!chat.read && this.$route.path == '/chat/' + val) {
|
||||
chat.read = true
|
||||
this.sendSocket({
|
||||
action: 'markAsRead',
|
||||
data: { chatId: this.$route.params.id },
|
||||
})
|
||||
}
|
||||
|
||||
window.removeEventListener('focus', onFocusHandler)
|
||||
}
|
||||
|
||||
window.addEventListener('focus', onFocusHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
closeWindow() {
|
||||
this.win.close()
|
||||
},
|
||||
minimizeWindow() {
|
||||
this.win.minimize()
|
||||
},
|
||||
maximizeWindow() {
|
||||
this.maximizing = true
|
||||
if (this.maximized) {
|
||||
this.win.restore()
|
||||
this.win.setSize(700, 600)
|
||||
this.win.center()
|
||||
if (process.platform !== 'darwin') document.body.style.borderRadius = null
|
||||
} else {
|
||||
this.win.maximize()
|
||||
if (process.platform !== 'darwin') document.body.style.borderRadius = '0'
|
||||
}
|
||||
|
||||
this.maximized = !this.maximized
|
||||
setTimeout(() => {
|
||||
this.maximizing = false
|
||||
}, 50)
|
||||
},
|
||||
restart() {
|
||||
ipcRenderer.send('restart_app')
|
||||
},
|
||||
requestChats() {
|
||||
if (this.$socket && this.$socket.readyState == 1) {
|
||||
if (this.loading) return
|
||||
this.loading = true
|
||||
|
||||
this.sendSocket({
|
||||
action: 'fetchChats',
|
||||
data: { offset: `${this.offset}`, limit: `${this.limit}` },
|
||||
})
|
||||
} else {
|
||||
setTimeout(this.requestChats, 100)
|
||||
}
|
||||
},
|
||||
connectWS() {
|
||||
this.offset = 0
|
||||
this.loading = false
|
||||
this.$disconnect()
|
||||
this.status = 0
|
||||
this.chats = []
|
||||
this.$store.commit('resetMessages')
|
||||
this.$router.push('/').catch(() => {})
|
||||
this.notifSound = new Audio(this.$store.state.notifSound)
|
||||
|
||||
const baseURI = this.$store.getters.baseURI
|
||||
this.$connect(baseURI, {
|
||||
format: 'json',
|
||||
reconnection: true,
|
||||
})
|
||||
},
|
||||
deleteChat(chat) {
|
||||
const chatIndex = this.chats.findIndex(obj => obj.personId == chat.personId)
|
||||
if (chatIndex > -1) {
|
||||
this.chats.splice(chatIndex, 1)
|
||||
}
|
||||
|
||||
if (this.$route.path == '/chat/' + chat.personId) {
|
||||
this.$router.push('/')
|
||||
}
|
||||
|
||||
this.$store.state.messagesCache[chat.personId] = null
|
||||
},
|
||||
composeMessage() {
|
||||
if (this.status == 2) this.$router.push('/chat/new')
|
||||
},
|
||||
onMove(e) {
|
||||
e.preventDefault()
|
||||
if (this.maximizing) return
|
||||
if (this.maximized) {
|
||||
this.win.restore()
|
||||
if (process.platform !== 'darwin') document.body.style.borderRadius = null
|
||||
this.maximized = false
|
||||
}
|
||||
},
|
||||
cacheMessages() {
|
||||
if (!this.$store.state.cacheMessages) return
|
||||
|
||||
if (this.$socket && this.$socket.readyState == 1) {
|
||||
for (let i = 0; i < this.chats.length; i++) {
|
||||
const chat = this.chats[i]
|
||||
this.sendSocket({
|
||||
action: 'fetchMessages',
|
||||
data: {
|
||||
id: chat.personId,
|
||||
offset: `0`,
|
||||
limit: `25`,
|
||||
},
|
||||
})
|
||||
}
|
||||
} else {
|
||||
setTimeout(this.cacheMessages, 1000)
|
||||
}
|
||||
},
|
||||
sendNotifierNotification(options, messageData, chatData) {
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const tempDir = path.join(__dirname, '..', 'temp')
|
||||
|
||||
if (!fs.existsSync(tempDir)) {
|
||||
fs.mkdirSync(tempDir)
|
||||
}
|
||||
|
||||
const download = require('image-downloader')
|
||||
const fileDir = path.join(tempDir, `avatar_${messageData.authorDocid}.jpg`)
|
||||
const dlOptions = {
|
||||
url: `${this.$store.getters.httpURI}/contactimg?docid=${messageData.authorDocid}&auth=${encodeURIComponent(
|
||||
this.$store.state.password
|
||||
)}`,
|
||||
dest: fileDir,
|
||||
agent: new require('https').Agent({ rejectUnauthorized: false }),
|
||||
}
|
||||
|
||||
download
|
||||
.image(dlOptions)
|
||||
.then(() => {
|
||||
options.icon = fileDir
|
||||
if (fs.statSync(fileDir).size == 0) throw Error('Empty image')
|
||||
})
|
||||
.catch(() => {
|
||||
options.icon = __static + '/icon.png'
|
||||
})
|
||||
.finally(() => {
|
||||
if (!this.$store.state.systemSound) this.notifSound.play()
|
||||
|
||||
const NotificationCenter = require('node-notifier').NotificationCenter
|
||||
let Notifier = null
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
Notifier = new NotificationCenter({
|
||||
withFallback: true,
|
||||
customPath: path.join(
|
||||
__dirname,
|
||||
'../terminal-notifier/vendor/mac.noindex/terminal-notifier.app/Contents/MacOS/terminal-notifier'
|
||||
),
|
||||
})
|
||||
} else {
|
||||
Notifier = require('node-notifier')
|
||||
}
|
||||
|
||||
Notifier.notify(options, (err, action) => {
|
||||
if (err) return
|
||||
if ((action == 'activate' || action == undefined) && chatData && chatData.id) {
|
||||
ipcRenderer.send('show_win')
|
||||
this.$router.push('/chat/' + messageData.personId)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
beforeUnmount() {
|
||||
$(window).off('resize')
|
||||
this.win.removeListener('move', this.onMove)
|
||||
ipcRenderer.removeAllListeners('win_id')
|
||||
},
|
||||
mounted() {
|
||||
this.connectWS()
|
||||
|
||||
const container = this.$refs.chats
|
||||
container.addEventListener('scroll', () => {
|
||||
if (container.scrollTop + container.offsetHeight == container.scrollHeight && !this.loading) {
|
||||
this.requestChats()
|
||||
}
|
||||
})
|
||||
|
||||
$(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', () => {
|
||||
ipcRenderer.removeAllListeners('update_available')
|
||||
})
|
||||
|
||||
ipcRenderer.on('update_downloaded', () => {
|
||||
ipcRenderer.removeAllListeners('update_downloaded')
|
||||
this.updateAvailable = true
|
||||
})
|
||||
|
||||
ipcRenderer.on('navigateTo', (sender, id) => {
|
||||
if (isNaN(id)) {
|
||||
this.$router.push('/chat/new').catch(() => {})
|
||||
} else {
|
||||
const arrayId = parseInt(id) - 1
|
||||
if (this.chats[arrayId]) {
|
||||
this.$router.push('/chat/' + this.chats[arrayId].personId).catch(() => {})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ipcRenderer.on('win_id', (e, id) => {
|
||||
this.win = remote.BrowserWindow.fromId(id)
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
if (this.maximizing) return
|
||||
if (this.maximized) {
|
||||
this.win.restore()
|
||||
if (process.platform !== 'darwin') document.body.style.borderRadius = null
|
||||
this.maximized = false
|
||||
}
|
||||
})
|
||||
|
||||
this.win.on('move', this.onMove)
|
||||
})
|
||||
|
||||
this.notifSound = new Audio(this.$store.state.notifSound)
|
||||
|
||||
if (!(this.$store.state.macstyle || process.platform === 'darwin')) {
|
||||
document.body.style.border = 'none'
|
||||
document.body.style.borderRadius = '0'
|
||||
}
|
||||
|
||||
if (!this.$store.state.acceleration) {
|
||||
document.documentElement.style.backgroundColor = 'black'
|
||||
}
|
||||
},
|
||||
socket: {
|
||||
fetchChats(data) {
|
||||
if (this.offset == 0) this.chats = data
|
||||
else this.chats.push(...data)
|
||||
this.offset += this.limit
|
||||
this.loading = false
|
||||
this.cacheMessages()
|
||||
this.status = 2
|
||||
},
|
||||
fetchMessages(data) {
|
||||
if (data && data[0] && !this.$store.state.messagesCache[data[0].personId]) {
|
||||
this.$store.commit('addMessages', { id: data[0].personId, data: data })
|
||||
}
|
||||
},
|
||||
newMessage(data) {
|
||||
const chatData = data.chat[0]
|
||||
|
||||
if (chatData && chatData.personId) {
|
||||
const chatIndex = this.chats.findIndex(obj => obj.personId == chatData.personId)
|
||||
|
||||
if (chatIndex > -1) {
|
||||
this.chats.splice(chatIndex, 1)
|
||||
}
|
||||
|
||||
this.chats.unshift(chatData)
|
||||
}
|
||||
|
||||
const messageData = data.message[0]
|
||||
|
||||
if (messageData) {
|
||||
this.$store.commit('setTyping', {
|
||||
chatId: messageData.personId,
|
||||
isTyping: false,
|
||||
})
|
||||
|
||||
if (this.$store.state.messagesCache[messageData.personId]) {
|
||||
const oldMsgIndex = this.$store.state.messagesCache[messageData.personId].findIndex(obj => obj.guid == messageData.guid)
|
||||
if (oldMsgIndex != -1) {
|
||||
this.$store.state.messagesCache[messageData.personId][oldMsgIndex] = messageData
|
||||
return
|
||||
}
|
||||
this.$store.state.messagesCache[messageData.personId].unshift(messageData)
|
||||
}
|
||||
|
||||
if (messageData.sender != 1 && remote.Notification.isSupported()) {
|
||||
if (this.$store.state.mutedChats.includes(messageData.personId)) return
|
||||
if (this.lastNotificationGUID == messageData.guid) return
|
||||
if (this.$route.params.id == messageData.personId && document.hasFocus()) return
|
||||
|
||||
let body = messageData.text.replace(/\u{fffc}/gu, '')
|
||||
if (messageData.group && messageData.group.startsWith('chat')) {
|
||||
body = `${messageData.author}: ${body}`
|
||||
}
|
||||
this.lastNotificationGUID = messageData.guid
|
||||
|
||||
const notificationOptions = {
|
||||
appID: 'com.sgtaziz.WebMessage',
|
||||
title: messageData.name,
|
||||
message: body == '' ? 'Attachment' : body,
|
||||
sound: this.$store.state.systemSound,
|
||||
reply: true,
|
||||
}
|
||||
|
||||
if (document.hasFocus()) return
|
||||
this.sendNotifierNotification(notificationOptions, messageData, chatData)
|
||||
} else if (messageData.sender != 1) {
|
||||
console.log('Notifications are not supported on this system.')
|
||||
}
|
||||
}
|
||||
},
|
||||
newReaction(data) {
|
||||
const reactions = data.reactions
|
||||
if (reactions && reactions.length > 0 && this.$store.state.messagesCache[reactions[0].personId]) {
|
||||
const msgIndex = this.$store.state.messagesCache[reactions[0].personId].findIndex(obj => obj.guid == reactions[0].forGUID)
|
||||
if (msgIndex > -1) {
|
||||
this.$store.state.messagesCache[reactions[0].personId][msgIndex]['reactions'] = reactions
|
||||
}
|
||||
}
|
||||
|
||||
const chatData = data.chat[0]
|
||||
|
||||
if (chatData && chatData.personId) {
|
||||
const chatIndex = this.chats.findIndex(obj => obj.personId == chatData.personId)
|
||||
|
||||
if (chatIndex > -1) {
|
||||
this.chats.splice(chatIndex, 1)
|
||||
}
|
||||
this.chats.unshift(chatData)
|
||||
}
|
||||
|
||||
if (reactions && reactions.length > 0 && reactions[0].sender != 1 && remote.Notification.isSupported()) {
|
||||
const reaction = reactions[0]
|
||||
if (this.$store.state.mutedChats.includes(reaction.personId)) return
|
||||
if (this.lastNotificationGUID == reaction.guid) return
|
||||
if (this.$route.params.id == reaction.personId && document.hasFocus()) return
|
||||
this.lastNotificationGUID = reaction.guid
|
||||
|
||||
const notificationOptions = {
|
||||
appID: 'com.sgtaziz.WebMessage',
|
||||
title: chatData.author,
|
||||
message: reaction.text.replace(/\u{fffc}/gu, ''),
|
||||
sound: this.$store.state.systemSound,
|
||||
}
|
||||
|
||||
if (document.hasFocus()) return
|
||||
this.sendNotifierNotification(notificationOptions, reaction, chatData)
|
||||
} else if (reactions && reactions.length > 0 && reactions[0].sender != 1) {
|
||||
console.log('Notifications are not supported on this system.')
|
||||
}
|
||||
},
|
||||
setAsRead(data) {
|
||||
if (this.$store.state.messagesCache[data.chatId]) {
|
||||
const messageIndex = this.$store.state.messagesCache[data.chatId].findIndex(obj => obj.guid == data.guid)
|
||||
if (messageIndex > -1) {
|
||||
this.$store.state.messagesCache[data.chatId][messageIndex]['dateRead'] = data.read
|
||||
this.$store.state.messagesCache[data.chatId][messageIndex]['dateDelivered'] = data.delivered
|
||||
}
|
||||
}
|
||||
},
|
||||
setTypingIndicator(data) {
|
||||
if (data && data.chat_id) {
|
||||
const chatId = data.chat_id
|
||||
const typing = data.typing == true
|
||||
|
||||
if (chatId) this.$store.commit('setTyping', { chatId: chatId, isTyping: typing })
|
||||
}
|
||||
},
|
||||
removeChat(data) {
|
||||
if (data.chatId) {
|
||||
const chatId = data.chatId
|
||||
const chatIndex = this.chats.findIndex(obj => obj.address == chatId)
|
||||
|
||||
if (chatIndex > -1) {
|
||||
const chat = this.chats[chatIndex]
|
||||
if (this.$route.path == '/chat/' + chat.personId) {
|
||||
this.$router.push('/')
|
||||
}
|
||||
this.$store.state.messagesCache[chat.personId] = null
|
||||
this.chats.splice(chatIndex, 1)
|
||||
}
|
||||
}
|
||||
},
|
||||
onopen() {
|
||||
this.offset = 0
|
||||
this.requestChats()
|
||||
this.status = 1
|
||||
},
|
||||
onerror() {
|
||||
this.loading = false
|
||||
if (this.$socket && this.$socket.readyState == 1) return
|
||||
this.status = 1
|
||||
this.$store.commit('resetMessages')
|
||||
this.$router.push('/').catch(() => {})
|
||||
this.chats = []
|
||||
},
|
||||
onclose() {
|
||||
this.loading = false
|
||||
if (this.$socket && this.$socket.readyState == 1) return
|
||||
this.status = 0
|
||||
this.$store.commit('resetMessages')
|
||||
this.$router.push('/').catch(() => {})
|
||||
this.chats = []
|
||||
},
|
||||
Home,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.text-emoji {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: -2px;
|
||||
margin-bottom: -3px;
|
||||
// margin-right: 1px;
|
||||
}
|
||||
|
||||
.confirmDialog {
|
||||
.vc-container {
|
||||
background-color: rgba(45, 45, 45, 0.9);
|
||||
border: 1px solid rgba(25, 25, 25, 0.9);
|
||||
.vc-text {
|
||||
color: white;
|
||||
font-weight: 300;
|
||||
font-size: 14px;
|
||||
}
|
||||
.vc-title {
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.vc-btn {
|
||||
background-color: rgba(45, 45, 45, 0.9);
|
||||
border-color: rgba(25, 25, 25, 0.9) !important;
|
||||
color: rgba(255, 0, 0, 0.9);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(45, 45, 45, 0.9);
|
||||
filter: brightness(90%);
|
||||
}
|
||||
|
||||
&.left {
|
||||
color: #4083ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vue-popover {
|
||||
background: rgba(25, 25, 25, 0.8) !important;
|
||||
font-size: 14px;
|
||||
&::before {
|
||||
border-bottom-color: rgba(25, 25, 25, 0.8) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.emoji {
|
||||
position: relative;
|
||||
top: 0.15em;
|
||||
width: 13px;
|
||||
}
|
||||
|
||||
.searchContainer {
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
.menuBtn {
|
||||
-webkit-app-region: no-drag;
|
||||
width: auto;
|
||||
float: right;
|
||||
margin-right: 10px;
|
||||
|
||||
.feather {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
filter: brightness(80%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.statusIndicator {
|
||||
width: auto;
|
||||
float: right;
|
||||
margin-right: 10px;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.textinput {
|
||||
background-color: rgba(200, 200, 200, 0.1);
|
||||
width: calc(100% - 4px);
|
||||
margin: 0 !important;
|
||||
margin-left: -2px !important;
|
||||
border: 0px none;
|
||||
border-color: rgba(87, 87, 87, 0.7) !important;
|
||||
color: #ebecec !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.chats {
|
||||
margin-top: 12px;
|
||||
max-height: calc(100% - 73px);
|
||||
margin-right: 1px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
.simplebar-scrollbar:before {
|
||||
background: #575757;
|
||||
}
|
||||
}
|
||||
|
||||
.scrollable {
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #424241;
|
||||
border-radius: 16px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@import url('https://fonts.googleapis.com/css?family=Roboto:light,regular,medium,thin,italic,mediumitalic,bold');
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
width: 100%;
|
||||
background-color: rgba(29, 29, 29, 0);
|
||||
font-family: 'Roboto', -apple-system, BlinkMacSystemFont, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
height: calc(100% - 2px);
|
||||
max-height: 100%;
|
||||
width: calc(100% - 2px);
|
||||
background-color: rgba(29, 29, 29, 0);
|
||||
overflow: hidden;
|
||||
border: 1px solid rgb(0, 0, 0);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
#app {
|
||||
font-family: 'Roboto', -apple-system, BlinkMacSystemFont, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-weight: 300;
|
||||
text-align: center;
|
||||
color: #ebecec;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
background-color: rgba(29, 29, 29, 0);
|
||||
border: 1px solid #4a4a4a;
|
||||
border-radius: 10px;
|
||||
|
||||
&.maximized {
|
||||
border-radius: 0;
|
||||
|
||||
#nav {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#content {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.nostyle {
|
||||
background-color: rgba(40, 40, 40, 1);
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
#nav {
|
||||
border-radius: 0 !important;
|
||||
margin-left: -1px;
|
||||
margin-top: -1px;
|
||||
width: 312px;
|
||||
}
|
||||
|
||||
#content {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease, backdrop-filter 0.2s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#nav {
|
||||
background-color: rgba(40, 40, 40, 0.93);
|
||||
width: 311px;
|
||||
padding: 9px;
|
||||
padding-right: 0;
|
||||
float: left;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
border-top-left-radius: 10px;
|
||||
border-bottom-left-radius: 10px;
|
||||
|
||||
&.notrans {
|
||||
background-color: rgba(40, 40, 40, 1);
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
|
||||
&.router-link-exact-active {
|
||||
color: #42b983;
|
||||
}
|
||||
}
|
||||
|
||||
input:not([type='range']):not([type='color']):not(.message-input) {
|
||||
height: auto;
|
||||
height: inherit;
|
||||
font-size: 13px;
|
||||
height: 6px;
|
||||
padding: 10px 6px 10px 6px;
|
||||
outline: none;
|
||||
border: 1px solid rgb(213, 213, 213);
|
||||
margin: 5px;
|
||||
cursor: text;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
input:not([type='range']):not([type='color']):not(.message-input):focus {
|
||||
border-radius: 1px;
|
||||
box-shadow: 0px 0px 0px 3.5px rgba(23, 101, 144, 1);
|
||||
animation: showFocus 0.3s;
|
||||
border-color: rgb(122, 167, 221) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.titlebar {
|
||||
-webkit-app-region: drag;
|
||||
width: calc(100% - 10px);
|
||||
height: 30px;
|
||||
padding: 5px;
|
||||
padding-top: 7px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
-webkit-app-region: no-drag;
|
||||
padding-left: 5px;
|
||||
padding-top: 3px;
|
||||
padding-right: 20px;
|
||||
float: left;
|
||||
line-height: 0px;
|
||||
|
||||
div:hover {
|
||||
filter: brightness(75%);
|
||||
}
|
||||
}
|
||||
|
||||
.close {
|
||||
background: #ff5c5c;
|
||||
font-size: 9pt;
|
||||
line-height: 12px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 0px solid #e33e41;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.close:active {
|
||||
background: #c14645;
|
||||
}
|
||||
|
||||
.closebutton {
|
||||
color: #820005;
|
||||
visibility: hidden;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.minimize {
|
||||
background: #ffbd4c;
|
||||
font-size: 9pt;
|
||||
line-height: 12px;
|
||||
margin-left: 8px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 0px solid #e09e3e;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.minimize:active {
|
||||
background: #c08e38;
|
||||
}
|
||||
|
||||
.minimizebutton {
|
||||
color: #9a5518;
|
||||
visibility: hidden;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.zoom {
|
||||
background: #00ca56;
|
||||
font-size: 9pt;
|
||||
line-height: 12px;
|
||||
margin-left: 8px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 0px solid #14ae46;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.zoom:active {
|
||||
background: #029740;
|
||||
}
|
||||
|
||||
.zoombutton {
|
||||
color: #006519;
|
||||
visibility: hidden;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#content {
|
||||
float: left;
|
||||
background-color: rgba(29, 29, 29, 1);
|
||||
position: fixed;
|
||||
top: 1px;
|
||||
left: 321px;
|
||||
right: 1px;
|
||||
bottom: 1px;
|
||||
border-top-right-radius: 10px;
|
||||
border-bottom-right-radius: 10px;
|
||||
border-left: 1px solid #000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@keyframes showFocus {
|
||||
0% {
|
||||
border-color: #dbdbdb;
|
||||
border-radius: -1px;
|
||||
box-shadow: 0px 0px 0px 14px rgba(23, 101, 144, 0);
|
||||
/*outline: 14px solid rgba(159, 204, 250, 0);
|
||||
outline-offset: 0px;*/
|
||||
}
|
||||
100% {
|
||||
border-color: rgb(122, 167, 221) !important;
|
||||
border-radius: 1px;
|
||||
box-shadow: 0px 0px 0px 3.5px rgba(23, 101, 144, 1);
|
||||
/*outline: 4px solid rgba(159, 204, 250, 1);
|
||||
outline-offset: -1px;*/
|
||||
}
|
||||
}
|
||||
|
||||
input[type='search'] {
|
||||
text-indent: 0px;
|
||||
text-align: center;
|
||||
background-image: url('assets/search.svg');
|
||||
background-size: auto 50%, 100% 100%;
|
||||
background-position: 5px 6px, center;
|
||||
background-repeat: no-repeat;
|
||||
text-align: center;
|
||||
text-indent: 18px;
|
||||
height: 28px !important;
|
||||
border-radius: 5px !important;
|
||||
}
|
||||
|
||||
input[type='search']:focus {
|
||||
text-align: left;
|
||||
box-shadow: 0px 0px 0px 4.5px rgb(115, 166, 233);
|
||||
border-bottom-color: #f00 !important;
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
|
||||
input[type='search']::-webkit-input-placeholder {
|
||||
/*text-align: center;*/
|
||||
color: rgb(152, 152, 152);
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
|
||||
import Home from '../views/Home.vue'
|
||||
import Chat from '../views/Chat/index.vue'
|
||||
import Chat from '@/views/Chat'
|
||||
import Blank from '@/views/Blank'
|
||||
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: Home,
|
||||
component: Blank,
|
||||
},
|
||||
{
|
||||
path: '/chat/:id',
|
||||
|
3
src/types/modules.d.ts
vendored
3
src/types/modules.d.ts
vendored
@ -17,6 +17,9 @@ declare module '@/components/DownloadAttachment'
|
||||
declare module '@/components/UploadButton'
|
||||
declare module '@/components/ReactionMenu'
|
||||
declare module '@/components/ReactionBubbles'
|
||||
declare module '@/views/Home'
|
||||
declare module '@/views/Chat'
|
||||
declare module '@/views/Blank'
|
||||
declare const __static: string
|
||||
|
||||
interface EmojiObject {
|
||||
|
3
src/views/Blank.vue
Normal file
3
src/views/Blank.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
@ -1,10 +1,10 @@
|
||||
import { ipcRenderer, remote } from 'electron'
|
||||
import emojiRegex from 'emoji-regex'
|
||||
import { nextTick, onMounted, getCurrentInstance, reactive, ComponentInternalInstance, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { RouteLocationNormalizedLoaded, useRoute } from 'vue-router'
|
||||
import { state as messagesState, fetchMessages } from './messages'
|
||||
|
||||
let route = useRoute()
|
||||
let route: Nullable<RouteLocationNormalizedLoaded> = null
|
||||
let currentInstance: Nullable<ComponentInternalInstance> = null
|
||||
|
||||
const state = reactive({
|
||||
@ -49,7 +49,7 @@ const postLoad = (initial: boolean) => {
|
||||
|
||||
if (container.scrollHeight - (container.scrollTop + container.offsetHeight) <= 20) {
|
||||
state.lastHeight = null
|
||||
currentInstance?.emit('markAsRead', route.params.id)
|
||||
currentInstance?.emit('markAsRead', route?.params.id)
|
||||
} else state.lastHeight = container.scrollHeight - container.scrollTop
|
||||
|
||||
if (container.scrollTop == 0 && !messagesState.loading) {
|
||||
@ -68,7 +68,7 @@ const postLoad = (initial: boolean) => {
|
||||
return
|
||||
}
|
||||
|
||||
currentInstance.emit('markAsRead', route.params.id)
|
||||
currentInstance.emit('markAsRead', route?.params.id)
|
||||
})
|
||||
}
|
||||
|
||||
@ -105,52 +105,8 @@ const rightClickMessage = (args: object) => {
|
||||
ipcRenderer.send('rightClickMessage', args)
|
||||
}
|
||||
|
||||
const handleFiles = (files: FileList, text?: string, target?: HTMLElement) => {
|
||||
if (text && target && text.length > 0) {
|
||||
nextTick(() => {
|
||||
let windowSelection = window.getSelection() as Selection
|
||||
const newText =
|
||||
target.innerHTML.slice(0, windowSelection.anchorOffset - text.length) + target.innerHTML.slice(windowSelection.anchorOffset)
|
||||
target.innerHTML = newText
|
||||
if (newText.length > 0) {
|
||||
windowSelection = window.getSelection() as Selection
|
||||
windowSelection.collapse(target.lastChild, newText.length)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const uploadButtonRef = currentInstance?.refs.uploadButton as any
|
||||
uploadButtonRef.$refs.fileInput.files = files
|
||||
uploadButtonRef.filesChanged()
|
||||
}
|
||||
|
||||
const hookPasteAndDrop = () => {
|
||||
nextTick(() => {
|
||||
const el = currentInstance?.refs.messageInput as HTMLElement
|
||||
if (el) {
|
||||
setTimeout(() => {
|
||||
el.focus()
|
||||
}, 10)
|
||||
el.addEventListener('paste', e => {
|
||||
if (e.clipboardData?.files && e.clipboardData.files.length > 0) {
|
||||
const text = e.clipboardData.getData('Text')
|
||||
const files = e.clipboardData.files
|
||||
handleFiles(files, text, e.target as HTMLElement)
|
||||
}
|
||||
})
|
||||
el.addEventListener('drop', e => {
|
||||
if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
|
||||
const files = e.dataTransfer.files
|
||||
handleFiles(files)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const autoCompleteHooks = () => {
|
||||
if (route.params.id == 'new') {
|
||||
if (route?.params.id == 'new') {
|
||||
nextTick(() => {
|
||||
const input = document.getElementsByClassName('autocomplete-input')[0] as HTMLElement
|
||||
if (input) {
|
||||
@ -168,17 +124,14 @@ const autoCompleteInput = (input: { name: string; phone: string } | string) => {
|
||||
|
||||
if (inputObj && inputObj.name) {
|
||||
messagesState.receiver = inputObj.phone
|
||||
hookPasteAndDrop()
|
||||
} else if (/^\+\d{11,16}/gi.test(input as string)) {
|
||||
messagesState.receiver = input as string
|
||||
hookPasteAndDrop()
|
||||
} else if (/^([a-zA-Z0-9_.+-])+@(([a-zA-Z0-9-])+\.)+([a-zA-Z0-9]{2,6})+$/.test(input as string)) {
|
||||
messagesState.receiver = input as string
|
||||
hookPasteAndDrop()
|
||||
}
|
||||
}
|
||||
|
||||
export { postLoad, scrollToBottom, hookPasteAndDrop, state }
|
||||
export { postLoad, scrollToBottom, autoCompleteHooks, autoCompleteInput, state }
|
||||
|
||||
export default () => {
|
||||
route = useRoute()
|
||||
@ -191,7 +144,7 @@ export default () => {
|
||||
}
|
||||
|
||||
onMounted(init)
|
||||
watch(() => route.params.id, init)
|
||||
watch(() => route?.params.id, init)
|
||||
|
||||
return {
|
||||
postLoad,
|
||||
|
@ -10,7 +10,12 @@
|
||||
v-html="$filters.nativeEmoji(messages.state.messages[0].name)"
|
||||
></span>
|
||||
<span class="contact" v-else-if="$route.params.id == 'new'" style="overflow: visible;">
|
||||
<autocomplete ref="autoComplete" :search="search" :get-result-value="getResultValue" @submit="onSubmit"></autocomplete>
|
||||
<autocomplete
|
||||
ref="autoComplete"
|
||||
:search="input.search"
|
||||
:get-result-value="input.getResultValue"
|
||||
@submit="input.onSubmit"
|
||||
></autocomplete>
|
||||
</span>
|
||||
</div>
|
||||
<div class="closeBtn" @click="$router.push('/')">
|
||||
@ -209,6 +214,9 @@
|
||||
@blur="input.handleBlur"
|
||||
@input="input.messageInputChanged"
|
||||
@paste="input.messageInputPasted"
|
||||
@drop="input.messageInputDropped"
|
||||
@dragenter.prevent
|
||||
@dragover.prevent
|
||||
@keydown.enter.exact="input.enterKey"
|
||||
@keydown.shift.enter="input.shiftEnterKey"
|
||||
contenteditable
|
||||
@ -233,6 +241,9 @@
|
||||
@blur="input.handleBlur"
|
||||
@input="input.messageInputChanged"
|
||||
@paste="input.messageInputPasted"
|
||||
@drop="input.messageInputDropped"
|
||||
@dragenter.prevent
|
||||
@dragover.prevent
|
||||
@keydown.enter.exact="input.enterKey"
|
||||
@keydown.shift.enter="input.shiftEnterKey"
|
||||
contenteditable
|
||||
|
@ -1,15 +1,17 @@
|
||||
import { ComponentInternalInstance, getCurrentInstance, nextTick, onMounted, reactive, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Router, useRoute, useRouter } from 'vue-router'
|
||||
import emojiData from 'emoji-mart-vue-fast/data/all.json'
|
||||
import { EmojiIndex } from 'emoji-mart-vue-fast/src/index'
|
||||
import rangy from 'rangy'
|
||||
import filters from '@/filters'
|
||||
import 'rangy/lib/rangy-selectionsaverestore'
|
||||
import { state as messagesState, sendSocket } from './messages'
|
||||
import { autoCompleteHooks, autoCompleteInput } from './functions'
|
||||
import axios from 'axios'
|
||||
import { useStore } from 'vuex'
|
||||
|
||||
let currentInstance: Nullable<ComponentInternalInstance> = null
|
||||
let router: Nullable<Router> = null
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const $rangy = rangy as any
|
||||
|
||||
@ -30,7 +32,28 @@ const state = reactive({
|
||||
emojiIndex: new EmojiIndex(emojiData),
|
||||
})
|
||||
|
||||
const handleFiles = (files: FileList, text?: string, target?: HTMLElement) => {
|
||||
if (text && target && text.length > 0) {
|
||||
nextTick(() => {
|
||||
let windowSelection = window.getSelection() as Selection
|
||||
const newText =
|
||||
target.innerHTML.slice(0, windowSelection.anchorOffset - text.length) + target.innerHTML.slice(windowSelection.anchorOffset)
|
||||
target.innerHTML = newText
|
||||
if (newText.length > 0) {
|
||||
windowSelection = window.getSelection() as Selection
|
||||
windowSelection.collapse(target.lastChild, newText.length)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const uploadButtonRef = currentInstance?.refs.uploadButton as any
|
||||
uploadButtonRef.$refs.fileInput.files = files
|
||||
uploadButtonRef.filesChanged()
|
||||
}
|
||||
|
||||
export default () => {
|
||||
router = useRouter()
|
||||
const watchRoute = useRoute()
|
||||
const store = useStore()
|
||||
currentInstance = getCurrentInstance()
|
||||
@ -186,11 +209,37 @@ export default () => {
|
||||
const messageInputPasted = (e: ClipboardEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
if (e.clipboardData?.files && e.clipboardData.files.length > 0) {
|
||||
let text = e.clipboardData?.getData('text/plain') as string
|
||||
text = filters.escapeHTML(text)
|
||||
const files = e.clipboardData.files
|
||||
handleFiles(files, text, e.target as HTMLElement)
|
||||
return
|
||||
}
|
||||
|
||||
let paste = e.clipboardData?.getData('text/plain') as string
|
||||
paste = filters.escapeHTML(paste)
|
||||
document.execCommand('insertHTML', false, paste)
|
||||
}
|
||||
|
||||
const messageInputDropped = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
console.log(e)
|
||||
|
||||
if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
|
||||
let text = e.dataTransfer?.getData('text/plain') as string
|
||||
text = filters.escapeHTML(text)
|
||||
const files = e.dataTransfer.files
|
||||
handleFiles(files, text, e.target as HTMLElement)
|
||||
}
|
||||
|
||||
let paste = e.dataTransfer?.getData('text/plain') as string
|
||||
paste = filters.escapeHTML(paste)
|
||||
document.execCommand('insertHTML', false, paste)
|
||||
}
|
||||
|
||||
const messageInputChanged = (e: Event) => {
|
||||
const target = e.target as HTMLElement
|
||||
let content = filters.nativeEmoji(filters.colonEmoji(target.innerHTML))
|
||||
@ -268,6 +317,39 @@ export default () => {
|
||||
state.showEmojiMenu = false
|
||||
}
|
||||
|
||||
const search = (input: string) => {
|
||||
const url = `${store?.getters.httpURI}/search?text=${encodeURIComponent(input)}&auth=${encodeURIComponent(store?.state.password)}`
|
||||
return new Promise(resolve => {
|
||||
if (input.length < 2) {
|
||||
return resolve([])
|
||||
}
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
resolve(data.slice(0, 7))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const getResultValue = (result: { name: string; phone: string }) => {
|
||||
return result.name + ` (${result.phone})`
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const onSubmit = (result: any) => {
|
||||
if (result && result.personId) {
|
||||
router?.push('/message/' + result.personId)
|
||||
} else if (result) {
|
||||
autoCompleteInput(result)
|
||||
} else {
|
||||
const input = currentInstance?.refs.autoComplete as { value: string }
|
||||
if (input) {
|
||||
autoCompleteInput(input.value)
|
||||
}
|
||||
}
|
||||
nextTick(autoCompleteHooks)
|
||||
}
|
||||
|
||||
onMounted(init)
|
||||
watch(() => watchRoute.params.id, init)
|
||||
|
||||
@ -277,11 +359,15 @@ export default () => {
|
||||
previewFiles,
|
||||
removeAttachment,
|
||||
messageInputPasted,
|
||||
messageInputDropped,
|
||||
messageInputChanged,
|
||||
handleBlur,
|
||||
insertEmoji,
|
||||
toggleEmojiMenu,
|
||||
hideEmojiMenu,
|
||||
search,
|
||||
getResultValue,
|
||||
onSubmit,
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { reactive, computed, ComputedRef, watch, ComponentInternalInstance, getCurrentInstance, nextTick } from 'vue'
|
||||
import main from '@/main'
|
||||
import { postLoad, scrollToBottom, hookPasteAndDrop, state as functionsState } from './functions'
|
||||
import { postLoad, scrollToBottom, state as functionsState } from './functions'
|
||||
|
||||
import { parseBuffer } from 'bplist-parser'
|
||||
import moment from 'moment'
|
||||
@ -143,13 +143,10 @@ const fetchMessagesHandler = (response: any) => {
|
||||
if (state.offset == 0) {
|
||||
state.messages = data
|
||||
postLoad(true)
|
||||
// this.hookPasteAndDrop()
|
||||
} else {
|
||||
state.messages.push(...data)
|
||||
postLoad(false)
|
||||
}
|
||||
|
||||
// state.loading = false
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@ -293,13 +290,29 @@ const onSocketConnected = () => {
|
||||
fetchMessages()
|
||||
}
|
||||
|
||||
const hookSocketEvents = () => {
|
||||
const socket: Nullable<WebSocket> = main ? main.config.globalProperties.$socket : null
|
||||
if (socket) {
|
||||
socket.removeEventListener('message', onSocketMessage)
|
||||
socket.removeEventListener('close', onSocketDisconnect)
|
||||
socket.removeEventListener('error', onSocketDisconnect)
|
||||
socket.removeEventListener('open', onSocketConnected)
|
||||
socket.addEventListener('message', onSocketMessage)
|
||||
socket.addEventListener('close', onSocketDisconnect)
|
||||
socket.addEventListener('error', onSocketDisconnect)
|
||||
socket.addEventListener('open', onSocketConnected)
|
||||
} else {
|
||||
setTimeout(hookSocketEvents, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const fetchMessages = () => {
|
||||
const socket: Nullable<WebSocket> = main ? main.config.globalProperties.$socket : null
|
||||
if (route && state.offset == 0 && store && store.state.messagesCache[route.params.id as string]) {
|
||||
state.messages = store.state.messagesCache[route.params.id as string].map((o: object) => ({ ...o }))
|
||||
postLoad(true)
|
||||
hookPasteAndDrop()
|
||||
hookSocketEvents()
|
||||
return
|
||||
}
|
||||
|
||||
@ -316,14 +329,7 @@ const fetchMessages = () => {
|
||||
},
|
||||
})
|
||||
|
||||
socket.removeEventListener('message', onSocketMessage)
|
||||
socket.removeEventListener('close', onSocketDisconnect)
|
||||
socket.removeEventListener('error', onSocketDisconnect)
|
||||
socket.removeEventListener('open', onSocketConnected)
|
||||
socket.addEventListener('message', onSocketMessage)
|
||||
socket.addEventListener('close', onSocketDisconnect)
|
||||
socket.addEventListener('error', onSocketDisconnect)
|
||||
socket.addEventListener('open', onSocketConnected)
|
||||
hookSocketEvents()
|
||||
} else {
|
||||
setTimeout(fetchMessages, 1000)
|
||||
}
|
||||
|
@ -1,9 +0,0 @@
|
||||
<template>
|
||||
<div class="home"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Home',
|
||||
}
|
||||
</script>
|
449
src/views/Home/chats.ts
Normal file
449
src/views/Home/chats.ts
Normal file
@ -0,0 +1,449 @@
|
||||
import { reactive, computed, ComputedRef, onMounted, getCurrentInstance, ComponentInternalInstance } from 'vue'
|
||||
import main from '@/main'
|
||||
|
||||
import { RouteLocationNormalizedLoaded, Router, useRoute, useRouter } from 'vue-router'
|
||||
import { Store, useStore } from 'vuex'
|
||||
import { remote } from 'electron'
|
||||
import { state as windowState } from './window'
|
||||
import { state as notificationsState, sendNotifierNotification } from './notifications'
|
||||
|
||||
interface ChatObject {
|
||||
showNum: boolean
|
||||
address: string
|
||||
text: string
|
||||
author: string
|
||||
date: number
|
||||
personId: string
|
||||
read: boolean
|
||||
}
|
||||
|
||||
let route: Nullable<RouteLocationNormalizedLoaded> = null
|
||||
let router: Nullable<Router> = null
|
||||
let currentInstance: Nullable<ComponentInternalInstance> = null
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let store: Nullable<Store<any>> = null
|
||||
|
||||
const state = reactive({
|
||||
chats: [] as ChatObject[],
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
loading: false,
|
||||
search: '',
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const filteredChats: ComputedRef<any> = computed(() => {
|
||||
let chats = state.chats.slice().sort((a, b) => (b.date - a.date > 0 ? 1 : -1))
|
||||
|
||||
if (chats.length > 0) {
|
||||
chats = chats.reduce((r: ChatObject[], chat) => {
|
||||
chat.showNum = false
|
||||
|
||||
const duplicateIndex = chats.findIndex(obj => obj.author == chat.author && obj.address != chat.address)
|
||||
if (duplicateIndex > -1 && !chat.address.startsWith('chat') && (chat.address.startsWith('+') || chat.address.includes('@'))) {
|
||||
chat.showNum = true
|
||||
}
|
||||
|
||||
r.push(chat)
|
||||
return r
|
||||
}, [])
|
||||
}
|
||||
|
||||
return chats.filter(chat => {
|
||||
return chat.author.toLowerCase().includes(state.search.toLowerCase()) || chat.text.toLowerCase().includes(state.search.toLowerCase())
|
||||
})
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const sendSocket = (obj: { action: string; data: object; password?: string }) => {
|
||||
const socket: Nullable<WebSocket> = main ? main.config.globalProperties.$socket : null
|
||||
if (!socket || !store) return
|
||||
|
||||
obj.password = store.state.password
|
||||
socket.send(JSON.stringify(obj))
|
||||
}
|
||||
|
||||
const markAsRead = (val: string) => {
|
||||
const chatIndex = state.chats.findIndex(obj => obj.personId == val)
|
||||
|
||||
if (chatIndex > -1) {
|
||||
const chat = state.chats[chatIndex]
|
||||
|
||||
if (!chat.read) {
|
||||
if (document.hasFocus()) {
|
||||
chat.read = true
|
||||
sendSocket({
|
||||
action: 'markAsRead',
|
||||
data: { chatId: route?.params.id },
|
||||
})
|
||||
} else {
|
||||
const onFocusHandler = () => {
|
||||
if (!chat.read && route?.path == '/chat/' + val) {
|
||||
chat.read = true
|
||||
sendSocket({
|
||||
action: 'markAsRead',
|
||||
data: { chatId: route.params.id },
|
||||
})
|
||||
}
|
||||
|
||||
window.removeEventListener('focus', onFocusHandler)
|
||||
}
|
||||
|
||||
window.addEventListener('focus', onFocusHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cacheMessages = () => {
|
||||
if (!store?.state.cacheMessages) return
|
||||
const socket: Nullable<WebSocket> = main ? main.config.globalProperties.$socket : null
|
||||
|
||||
if (socket?.readyState == 1) {
|
||||
for (let i = 0; i < state.chats.length; i++) {
|
||||
const chat = state.chats[i]
|
||||
sendSocket({
|
||||
action: 'fetchMessages',
|
||||
data: {
|
||||
id: chat.personId,
|
||||
offset: `0`,
|
||||
limit: `25`,
|
||||
},
|
||||
})
|
||||
}
|
||||
} else {
|
||||
setTimeout(cacheMessages, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const fetchChatsHandler = (response: any) => {
|
||||
const data = response.data
|
||||
if (state.offset == 0) state.chats = data
|
||||
else state.chats.push(...data)
|
||||
state.offset += state.limit
|
||||
state.loading = false
|
||||
cacheMessages()
|
||||
windowState.status = 2
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const fetchMessagesHandler = (response: any) => {
|
||||
const data = response.data
|
||||
if (data && data[0] && !store?.state.messagesCache[data[0].personId]) {
|
||||
store?.commit('addMessages', { id: data[0].personId, data: data })
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const newMessageHandler = (response: any) => {
|
||||
const data = response.data
|
||||
const chatData = data.chat[0]
|
||||
|
||||
if (chatData && chatData.personId) {
|
||||
const chatIndex = state.chats.findIndex(obj => obj.personId == chatData.personId)
|
||||
if (chatIndex > -1) state.chats.splice(chatIndex, 1)
|
||||
|
||||
state.chats.unshift(chatData)
|
||||
}
|
||||
|
||||
const messageData = data.message[0]
|
||||
|
||||
if (messageData) {
|
||||
store?.commit('setTyping', {
|
||||
chatId: messageData.personId,
|
||||
isTyping: false,
|
||||
})
|
||||
|
||||
if (store?.state.messagesCache[messageData.personId]) {
|
||||
const oldMsgIndex = store?.state.messagesCache[messageData.personId].findIndex(
|
||||
(obj: { guid: string }) => obj.guid == messageData.guid
|
||||
)
|
||||
if (oldMsgIndex != -1 && store) {
|
||||
store.state.messagesCache[messageData.personId][oldMsgIndex] = messageData
|
||||
return
|
||||
}
|
||||
store?.state.messagesCache[messageData.personId].unshift(messageData)
|
||||
}
|
||||
|
||||
if (messageData.sender != 1 && remote.Notification.isSupported()) {
|
||||
if (store?.state.mutedChats.includes(messageData.personId)) return
|
||||
// if (this.lastNotificationGUID == messageData.guid) return
|
||||
if (route?.params.id == messageData.personId && document.hasFocus()) return
|
||||
|
||||
let body = messageData.text.replace(/\u{fffc}/gu, '')
|
||||
if (messageData.group && messageData.group.startsWith('chat')) {
|
||||
body = `${messageData.author}: ${body}`
|
||||
}
|
||||
// this.lastNotificationGUID = messageData.guid
|
||||
|
||||
const notificationOptions = {
|
||||
appID: 'com.sgtaziz.WebMessage',
|
||||
title: messageData.name as string,
|
||||
message: (body == '' ? 'Attachment' : body) as string,
|
||||
sound: store?.state.systemSound as boolean,
|
||||
reply: true,
|
||||
icon: null as Nullable<string>,
|
||||
}
|
||||
|
||||
if (document.hasFocus()) return
|
||||
sendNotifierNotification(notificationOptions, messageData, chatData)
|
||||
} else if (messageData.sender != 1) {
|
||||
console.log('Notifications are not supported on this system.')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const newReactionHandler = (response: any) => {
|
||||
const data = response.data
|
||||
const reactions = data.reactions
|
||||
if (reactions && reactions.length > 0 && store?.state.messagesCache[reactions[0].personId]) {
|
||||
const msgIndex = store?.state.messagesCache[reactions[0].personId].findIndex(
|
||||
(obj: { guid: string }) => obj.guid == reactions[0].forGUID
|
||||
)
|
||||
if (msgIndex > -1 && store) {
|
||||
store.state.messagesCache[reactions[0].personId][msgIndex]['reactions'] = reactions
|
||||
}
|
||||
}
|
||||
|
||||
const chatData = data.chat[0]
|
||||
|
||||
if (chatData && chatData.personId) {
|
||||
const chatIndex = state.chats.findIndex(obj => obj.personId == chatData.personId)
|
||||
|
||||
if (chatIndex > -1) state.chats.splice(chatIndex, 1)
|
||||
state.chats.unshift(chatData)
|
||||
}
|
||||
|
||||
if (reactions && reactions.length > 0 && reactions[0].sender != 1 && remote.Notification.isSupported()) {
|
||||
const reaction = reactions[0]
|
||||
if (store?.state.mutedChats.includes(reaction.personId)) return
|
||||
// if (this.lastNotificationGUID == reaction.guid) return
|
||||
if (route?.params.id == reaction.personId && document.hasFocus()) return
|
||||
// this.lastNotificationGUID = reaction.guid
|
||||
|
||||
const notificationOptions = {
|
||||
appID: 'com.sgtaziz.WebMessage',
|
||||
title: chatData.author as string,
|
||||
message: reaction.text.replace(/\u{fffc}/gu, '') as string,
|
||||
sound: store?.state.systemSound as boolean,
|
||||
}
|
||||
|
||||
if (document.hasFocus()) return
|
||||
sendNotifierNotification(notificationOptions, reaction, chatData)
|
||||
} else if (reactions && reactions.length > 0 && reactions[0].sender != 1) {
|
||||
console.log('Notifications are not supported on this system.')
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const setAsReadHandler = (response: any) => {
|
||||
const data = response.data
|
||||
if (store?.state.messagesCache[data.chatId]) {
|
||||
const messageIndex = store.state.messagesCache[data.chatId].findIndex((obj: { guid: string }) => obj.guid == data.guid)
|
||||
if (messageIndex > -1) {
|
||||
store.state.messagesCache[data.chatId][messageIndex]['dateRead'] = data.read
|
||||
store.state.messagesCache[data.chatId][messageIndex]['dateDelivered'] = data.delivered
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const setTypingIndicatorHandler = (response: any) => {
|
||||
const data = response.data
|
||||
if (data && data.chat_id) {
|
||||
const chatId = data.chat_id
|
||||
const typing = data.typing == true
|
||||
|
||||
if (chatId) store?.commit('setTyping', { chatId: chatId, isTyping: typing })
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const removeChatHandler = (response: any) => {
|
||||
const data = response.data
|
||||
if (data.chatId) {
|
||||
const chatId = data.chatId
|
||||
const chatIndex = state.chats.findIndex(obj => obj.address == chatId)
|
||||
|
||||
if (chatIndex > -1) {
|
||||
const chat = state.chats[chatIndex]
|
||||
if (route?.path == '/chat/' + chat.personId) {
|
||||
router?.push('/')
|
||||
}
|
||||
if (store) store.state.messagesCache[chat.personId] = null
|
||||
state.chats.splice(chatIndex, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onSocketMessage = (event: MessageEvent) => {
|
||||
event.data.text().then((responseText: string) => {
|
||||
const response = JSON.parse(responseText)
|
||||
|
||||
if (store && route && response) {
|
||||
switch (response.action) {
|
||||
case 'fetchChats':
|
||||
fetchChatsHandler(response)
|
||||
break
|
||||
case 'fetchMessages':
|
||||
fetchMessagesHandler(response)
|
||||
break
|
||||
case 'newMessage':
|
||||
newMessageHandler(response)
|
||||
break
|
||||
case 'newReaction':
|
||||
newReactionHandler(response)
|
||||
break
|
||||
case 'setAsRead':
|
||||
setAsReadHandler(response)
|
||||
break
|
||||
case 'setTypingIndicator':
|
||||
setTypingIndicatorHandler(response)
|
||||
break
|
||||
case 'removeChat':
|
||||
removeChatHandler(response)
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const onSocketDisconnect = (e: Event | CloseEvent) => {
|
||||
const socket: Nullable<WebSocket> = main ? main.config.globalProperties.$socket : null
|
||||
|
||||
state.loading = false
|
||||
if (socket?.readyState == 1) return
|
||||
windowState.status = e instanceof CloseEvent ? 0 : 1
|
||||
state.offset = 0
|
||||
store?.commit('resetMessages')
|
||||
router?.push('/').catch(() => {})
|
||||
state.chats = []
|
||||
}
|
||||
|
||||
const onSocketConnected = () => {
|
||||
state.chats = []
|
||||
state.offset = 0
|
||||
windowState.status = 1
|
||||
state.loading = false
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
fetchChats()
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const fetchChats = () => {
|
||||
const socket: Nullable<WebSocket> = main ? main.config.globalProperties.$socket : null
|
||||
|
||||
if (socket && socket.readyState == 1) {
|
||||
if (state.loading || !route) return
|
||||
state.loading = true
|
||||
|
||||
sendSocket({
|
||||
action: 'fetchChats',
|
||||
data: {
|
||||
offset: `${state.offset}`,
|
||||
limit: `${state.limit}`,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
setTimeout(fetchChats, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteChat = (chat: ChatObject) => {
|
||||
const chatIndex = state.chats.findIndex(obj => obj.personId == chat.personId)
|
||||
if (chatIndex > -1) {
|
||||
state.chats.splice(chatIndex, 1)
|
||||
}
|
||||
|
||||
if (route?.path == '/chat/' + chat.personId) {
|
||||
router?.push('/')
|
||||
}
|
||||
|
||||
if (store) store.state.messagesCache[chat.personId] = null
|
||||
}
|
||||
|
||||
const composeMessage = () => {
|
||||
if (windowState.status == 2) router?.push('/chat/new')
|
||||
}
|
||||
|
||||
const connectWS = () => {
|
||||
const connect: Nullable<CallableFunction> = main ? main.config.globalProperties.$connect : null
|
||||
const disconnect: Nullable<CallableFunction> = main ? main.config.globalProperties.$disconnect : null
|
||||
if (disconnect) disconnect()
|
||||
|
||||
state.offset = 0
|
||||
state.loading = false
|
||||
windowState.status = 0
|
||||
state.chats = []
|
||||
store?.commit('resetMessages')
|
||||
router?.push('/').catch(() => {})
|
||||
notificationsState.notifSound = new Audio(store?.state.notifSound)
|
||||
|
||||
const baseURI = store?.getters.baseURI
|
||||
if (connect) {
|
||||
connect(baseURI, {
|
||||
format: 'json',
|
||||
reconnection: true,
|
||||
})
|
||||
const socket: Nullable<WebSocket> = main ? main.config.globalProperties.$socket : null
|
||||
|
||||
socket?.removeEventListener('message', onSocketMessage)
|
||||
socket?.removeEventListener('close', onSocketDisconnect)
|
||||
socket?.removeEventListener('error', onSocketDisconnect)
|
||||
socket?.removeEventListener('open', onSocketConnected)
|
||||
socket?.addEventListener('message', onSocketMessage)
|
||||
socket?.addEventListener('close', onSocketDisconnect)
|
||||
socket?.addEventListener('error', onSocketDisconnect)
|
||||
socket?.addEventListener('open', onSocketConnected)
|
||||
} else {
|
||||
setTimeout(connectWS, 1)
|
||||
}
|
||||
}
|
||||
|
||||
export { state }
|
||||
|
||||
export default () => {
|
||||
store = useStore()
|
||||
route = useRoute()
|
||||
router = useRouter()
|
||||
currentInstance = getCurrentInstance()
|
||||
|
||||
const init = () => {
|
||||
state.chats = []
|
||||
state.limit = 50
|
||||
state.offset = 0
|
||||
state.loading = false
|
||||
state.search = ''
|
||||
connectWS()
|
||||
}
|
||||
|
||||
init()
|
||||
|
||||
const mounted = () => {
|
||||
const container = currentInstance?.refs.chats as Nullable<HTMLElement>
|
||||
if (container) {
|
||||
container.addEventListener('scroll', () => {
|
||||
if (container.scrollTop + container.offsetHeight == container.scrollHeight && !state.loading) {
|
||||
fetchChats()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
setTimeout(mounted, 1)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(mounted)
|
||||
|
||||
return {
|
||||
state,
|
||||
filteredChats,
|
||||
fetchChats,
|
||||
deleteChat,
|
||||
composeMessage,
|
||||
markAsRead,
|
||||
}
|
||||
}
|
473
src/views/Home/index.vue
Normal file
473
src/views/Home/index.vue
Normal file
@ -0,0 +1,473 @@
|
||||
<template>
|
||||
<!-- <vue-confirm-dialog class="confirmDialog"></vue-confirm-dialog> -->
|
||||
<settings ref="settingsModal" @saved="chats.connectWS"></settings>
|
||||
<div id="nav" :class="{ notrans: !$store.state.acceleration }">
|
||||
<div class="titlebar">
|
||||
<div class="buttons" v-if="$store.state.macstyle || window.process.platform === 'darwin'">
|
||||
<div class="close" @click="window.closeWindow">
|
||||
<span class="closebutton"><span>x</span></span>
|
||||
</div>
|
||||
<div class="minimize" @click="window.minimizeWindow">
|
||||
<span class="minimizebutton"><span>–</span></span>
|
||||
</div>
|
||||
<div class="zoom" @click="window.maximizeWindow">
|
||||
<span class="zoombutton"><span>+</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="statusIndicator" style="margin-top: 1px;" v-tooltip:bottom.tooltip="window.statusText.value">
|
||||
<feather type="circle" stroke="rgba(25,25,25,0.5)" :fill="window.statusColor.value" size="10"></feather>
|
||||
</div>
|
||||
<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="chats.connectWS"></feather>
|
||||
</div>
|
||||
<div class="menuBtn">
|
||||
<feather type="edit" stroke="rgba(36,132,255,0.65)" size="20" @click="chats.composeMessage"></feather>
|
||||
</div>
|
||||
<div class="menuBtn" v-if="window.updateAvailable">
|
||||
<feather type="download" stroke="rgba(152,255,152,0.65)" size="20" @click="window.restart"></feather>
|
||||
</div>
|
||||
</div>
|
||||
<div class="searchContainer">
|
||||
<input type="search" placeholder="Search" class="textinput" v-model="chats.search" />
|
||||
</div>
|
||||
<div class="chats scrollable" ref="chatsContainer">
|
||||
<chat
|
||||
v-for="chat in chats.filteredChats.value"
|
||||
:key="chat.id"
|
||||
:chatid="chat.personId"
|
||||
:author="chat.author"
|
||||
:text="chat.text"
|
||||
:date="chat.date"
|
||||
:read="chat.read"
|
||||
:docid="chat.docid"
|
||||
:showNum="chat.showNum"
|
||||
:isGroup="chat.personId.startsWith('chat') && !chat.personId.includes('@') && chat.personId.length >= 20"
|
||||
@deleted="deleteChat(chat)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div id="content">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
<component :is="Component" @markAsRead="chats.markAsRead" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Chat from '@/components/Chat.vue'
|
||||
import Settings from '@/components/Settings.vue'
|
||||
import Tooltip from '@/components/Tooltip.vue'
|
||||
import chatsComp from './chats'
|
||||
import windowComp from './window'
|
||||
import notificationsComp from './notifications'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
Chat,
|
||||
Settings,
|
||||
Tooltip,
|
||||
},
|
||||
setup() {
|
||||
const chats = chatsComp()
|
||||
const window = windowComp()
|
||||
const notifications = notificationsComp()
|
||||
|
||||
return { chats, window, notifications }
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.text-emoji {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: -2px;
|
||||
margin-bottom: -3px;
|
||||
// margin-right: 1px;
|
||||
}
|
||||
|
||||
.confirmDialog {
|
||||
.vc-container {
|
||||
background-color: rgba(45, 45, 45, 0.9);
|
||||
border: 1px solid rgba(25, 25, 25, 0.9);
|
||||
.vc-text {
|
||||
color: white;
|
||||
font-weight: 300;
|
||||
font-size: 14px;
|
||||
}
|
||||
.vc-title {
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.vc-btn {
|
||||
background-color: rgba(45, 45, 45, 0.9);
|
||||
border-color: rgba(25, 25, 25, 0.9) !important;
|
||||
color: rgba(255, 0, 0, 0.9);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(45, 45, 45, 0.9);
|
||||
filter: brightness(90%);
|
||||
}
|
||||
|
||||
&.left {
|
||||
color: #4083ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vue-popover {
|
||||
background: rgba(25, 25, 25, 0.8) !important;
|
||||
font-size: 14px;
|
||||
&::before {
|
||||
border-bottom-color: rgba(25, 25, 25, 0.8) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.emoji {
|
||||
position: relative;
|
||||
top: 0.15em;
|
||||
width: 13px;
|
||||
}
|
||||
|
||||
.searchContainer {
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
.menuBtn {
|
||||
-webkit-app-region: no-drag;
|
||||
width: auto;
|
||||
float: right;
|
||||
margin-right: 10px;
|
||||
|
||||
.feather {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
filter: brightness(80%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.statusIndicator {
|
||||
width: auto;
|
||||
float: right;
|
||||
margin-right: 10px;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.textinput {
|
||||
background-color: rgba(200, 200, 200, 0.1);
|
||||
width: calc(100% - 4px);
|
||||
margin: 0 !important;
|
||||
margin-left: -2px !important;
|
||||
border: 0px none;
|
||||
border-color: rgba(87, 87, 87, 0.7) !important;
|
||||
color: #ebecec !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.chats {
|
||||
margin-top: 12px;
|
||||
max-height: calc(100% - 73px);
|
||||
margin-right: 1px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
.simplebar-scrollbar:before {
|
||||
background: #575757;
|
||||
}
|
||||
}
|
||||
|
||||
.scrollable {
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #424241;
|
||||
border-radius: 16px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@import url('https://fonts.googleapis.com/css?family=Roboto:light,regular,medium,thin,italic,mediumitalic,bold');
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
width: 100%;
|
||||
background-color: rgba(29, 29, 29, 0);
|
||||
font-family: 'Roboto', -apple-system, BlinkMacSystemFont, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
height: calc(100% - 2px);
|
||||
max-height: 100%;
|
||||
width: calc(100% - 2px);
|
||||
background-color: rgba(29, 29, 29, 0);
|
||||
overflow: hidden;
|
||||
border: 1px solid rgb(0, 0, 0);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
#app {
|
||||
font-family: 'Roboto', -apple-system, BlinkMacSystemFont, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-weight: 300;
|
||||
text-align: center;
|
||||
color: #ebecec;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
background-color: rgba(29, 29, 29, 0);
|
||||
border: 1px solid #4a4a4a;
|
||||
border-radius: 10px;
|
||||
|
||||
&.maximized {
|
||||
border-radius: 0;
|
||||
|
||||
#nav {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#content {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.nostyle {
|
||||
background-color: rgba(40, 40, 40, 1);
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
#nav {
|
||||
border-radius: 0 !important;
|
||||
margin-left: -1px;
|
||||
margin-top: -1px;
|
||||
width: 312px;
|
||||
}
|
||||
|
||||
#content {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease, backdrop-filter 0.2s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#nav {
|
||||
background-color: rgba(40, 40, 40, 0.93);
|
||||
width: 311px;
|
||||
padding: 9px;
|
||||
padding-right: 0;
|
||||
float: left;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
border-top-left-radius: 10px;
|
||||
border-bottom-left-radius: 10px;
|
||||
|
||||
&.notrans {
|
||||
background-color: rgba(40, 40, 40, 1);
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
|
||||
&.router-link-exact-active {
|
||||
color: #42b983;
|
||||
}
|
||||
}
|
||||
|
||||
input:not([type='range']):not([type='color']):not(.message-input) {
|
||||
height: auto;
|
||||
height: inherit;
|
||||
font-size: 13px;
|
||||
height: 6px;
|
||||
padding: 10px 6px 10px 6px;
|
||||
outline: none;
|
||||
border: 1px solid rgb(213, 213, 213);
|
||||
margin: 5px;
|
||||
cursor: text;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
input:not([type='range']):not([type='color']):not(.message-input):focus {
|
||||
border-radius: 1px;
|
||||
box-shadow: 0px 0px 0px 3.5px rgba(23, 101, 144, 1);
|
||||
animation: showFocus 0.3s;
|
||||
border-color: rgb(122, 167, 221) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.titlebar {
|
||||
-webkit-app-region: drag;
|
||||
width: calc(100% - 10px);
|
||||
height: 30px;
|
||||
padding: 5px;
|
||||
padding-top: 7px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
-webkit-app-region: no-drag;
|
||||
padding-left: 5px;
|
||||
padding-top: 3px;
|
||||
padding-right: 20px;
|
||||
float: left;
|
||||
line-height: 0px;
|
||||
|
||||
div:hover {
|
||||
filter: brightness(75%);
|
||||
}
|
||||
}
|
||||
|
||||
.close {
|
||||
background: #ff5c5c;
|
||||
font-size: 9pt;
|
||||
line-height: 12px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 0px solid #e33e41;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.close:active {
|
||||
background: #c14645;
|
||||
}
|
||||
|
||||
.closebutton {
|
||||
color: #820005;
|
||||
visibility: hidden;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.minimize {
|
||||
background: #ffbd4c;
|
||||
font-size: 9pt;
|
||||
line-height: 12px;
|
||||
margin-left: 8px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 0px solid #e09e3e;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.minimize:active {
|
||||
background: #c08e38;
|
||||
}
|
||||
|
||||
.minimizebutton {
|
||||
color: #9a5518;
|
||||
visibility: hidden;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.zoom {
|
||||
background: #00ca56;
|
||||
font-size: 9pt;
|
||||
line-height: 12px;
|
||||
margin-left: 8px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 0px solid #14ae46;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.zoom:active {
|
||||
background: #029740;
|
||||
}
|
||||
|
||||
.zoombutton {
|
||||
color: #006519;
|
||||
visibility: hidden;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#content {
|
||||
float: left;
|
||||
background-color: rgba(29, 29, 29, 1);
|
||||
position: fixed;
|
||||
top: 1px;
|
||||
left: 321px;
|
||||
right: 1px;
|
||||
bottom: 1px;
|
||||
border-top-right-radius: 10px;
|
||||
border-bottom-right-radius: 10px;
|
||||
border-left: 1px solid #000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@keyframes showFocus {
|
||||
0% {
|
||||
border-color: #dbdbdb;
|
||||
border-radius: -1px;
|
||||
box-shadow: 0px 0px 0px 14px rgba(23, 101, 144, 0);
|
||||
/*outline: 14px solid rgba(159, 204, 250, 0);
|
||||
outline-offset: 0px;*/
|
||||
}
|
||||
100% {
|
||||
border-color: rgb(122, 167, 221) !important;
|
||||
border-radius: 1px;
|
||||
box-shadow: 0px 0px 0px 3.5px rgba(23, 101, 144, 1);
|
||||
/*outline: 4px solid rgba(159, 204, 250, 1);
|
||||
outline-offset: -1px;*/
|
||||
}
|
||||
}
|
||||
|
||||
input[type='search'] {
|
||||
text-indent: 0px;
|
||||
text-align: center;
|
||||
background-image: url('../../assets/search.svg');
|
||||
background-size: auto 50%, 100% 100%;
|
||||
background-position: 5px 6px, center;
|
||||
background-repeat: no-repeat;
|
||||
text-align: center;
|
||||
text-indent: 18px;
|
||||
height: 28px !important;
|
||||
border-radius: 5px !important;
|
||||
}
|
||||
|
||||
input[type='search']:focus {
|
||||
text-align: left;
|
||||
box-shadow: 0px 0px 0px 4.5px rgb(115, 166, 233);
|
||||
border-bottom-color: #f00 !important;
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
|
||||
input[type='search']::-webkit-input-placeholder {
|
||||
/*text-align: center;*/
|
||||
color: rgb(152, 152, 152);
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
</style>
|
89
src/views/Home/notifications.ts
Normal file
89
src/views/Home/notifications.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { ipcRenderer } from 'electron'
|
||||
import { reactive } from 'vue'
|
||||
import { Router, useRouter } from 'vue-router'
|
||||
import { Store, useStore } from 'vuex'
|
||||
|
||||
interface NotificationOptions {
|
||||
appID: string
|
||||
title: string
|
||||
message: string
|
||||
sound: boolean
|
||||
reply?: boolean
|
||||
icon?: Nullable<string>
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let store: Nullable<Store<any>> = null
|
||||
let router: Nullable<Router> = null
|
||||
|
||||
const state = reactive({
|
||||
notifSound: null as Nullable<HTMLAudioElement>,
|
||||
})
|
||||
|
||||
const sendNotifierNotification = (
|
||||
options: NotificationOptions,
|
||||
messageData: { authorDocid: string; personId: string },
|
||||
chatData: { id: string }
|
||||
) => {
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const https = require('https')
|
||||
const tempDir = path.join(__dirname, '..', 'temp')
|
||||
|
||||
if (!fs.existsSync(tempDir)) {
|
||||
fs.mkdirSync(tempDir)
|
||||
}
|
||||
|
||||
const download = require('image-downloader')
|
||||
const fileDir = path.join(tempDir, `avatar_${messageData.authorDocid}.jpg`)
|
||||
const dlOptions = {
|
||||
url: `${store?.getters.httpURI}/contactimg?docid=${messageData.authorDocid}&auth=${encodeURIComponent(store?.state.password)}`,
|
||||
dest: fileDir,
|
||||
agent: new https.Agent({ rejectUnauthorized: false }),
|
||||
}
|
||||
|
||||
download
|
||||
.image(dlOptions)
|
||||
.then(() => {
|
||||
options.icon = fileDir
|
||||
if (fs.statSync(fileDir).size == 0) throw Error('Empty image')
|
||||
})
|
||||
.catch(() => {
|
||||
options.icon = __static + '/icon.png'
|
||||
})
|
||||
.finally(() => {
|
||||
if (!store?.state.systemSound) state.notifSound?.play()
|
||||
|
||||
const NotificationCenter = require('node-notifier').NotificationCenter
|
||||
let Notifier = null
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
Notifier = new NotificationCenter({
|
||||
withFallback: true,
|
||||
customPath: path.join(
|
||||
__dirname,
|
||||
'../terminal-notifier/vendor/mac.noindex/terminal-notifier.app/Contents/MacOS/terminal-notifier'
|
||||
),
|
||||
})
|
||||
} else {
|
||||
Notifier = require('node-notifier')
|
||||
}
|
||||
|
||||
Notifier.notify(options, (err: string, action: string) => {
|
||||
if (err) return
|
||||
if ((action == 'activate' || action == undefined) && chatData && chatData.id) {
|
||||
ipcRenderer.send('show_win')
|
||||
router?.push('/chat/' + messageData.personId)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export { state, sendNotifierNotification }
|
||||
|
||||
export default () => {
|
||||
store = useStore()
|
||||
router = useRouter()
|
||||
|
||||
return { state }
|
||||
}
|
165
src/views/Home/window.ts
Normal file
165
src/views/Home/window.ts
Normal file
@ -0,0 +1,165 @@
|
||||
import { reactive, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
|
||||
import { Router, useRouter } from 'vue-router'
|
||||
import { Store, useStore } from 'vuex'
|
||||
import { BrowserWindow, ipcRenderer, remote } from 'electron'
|
||||
import { state as chatsState } from './chats'
|
||||
|
||||
let router: Nullable<Router> = null
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let store: Nullable<Store<any>> = null
|
||||
|
||||
const state = reactive({
|
||||
updateAvailable: false,
|
||||
maximized: false,
|
||||
maximizing: false,
|
||||
win: null as Nullable<BrowserWindow>,
|
||||
status: 0,
|
||||
})
|
||||
|
||||
const statusColor = computed(() => {
|
||||
if (state.status == 0) {
|
||||
return 'rgba(255,0,0,0.8)'
|
||||
} else if (state.status == 1) {
|
||||
return 'rgba(255,100,0,0.8)'
|
||||
} else if (state.status == 2) {
|
||||
return 'rgba(0,255,0,0.5)'
|
||||
}
|
||||
|
||||
return ''
|
||||
})
|
||||
|
||||
const statusText = computed(() => {
|
||||
if (state.status == 0) {
|
||||
return 'Device not found'
|
||||
} else if (state.status == 1) {
|
||||
return 'Device found. Retrieving data...'
|
||||
} else if (state.status == 2) {
|
||||
return 'Device connected'
|
||||
}
|
||||
|
||||
return ''
|
||||
})
|
||||
|
||||
const closeWindow = () => {
|
||||
state.win?.close()
|
||||
}
|
||||
|
||||
const minimizeWindow = () => {
|
||||
state.win?.minimize()
|
||||
}
|
||||
|
||||
const maximizeWindow = () => {
|
||||
state.maximizing = true
|
||||
|
||||
if (state.maximized) {
|
||||
state.win?.restore()
|
||||
state.win?.setSize(700, 600)
|
||||
state.win?.center()
|
||||
if (process.platform !== 'darwin') document.body.style.borderRadius = ''
|
||||
} else {
|
||||
state.win?.maximize()
|
||||
if (process.platform !== 'darwin') document.body.style.borderRadius = '0'
|
||||
}
|
||||
|
||||
state.maximized = !state.maximized
|
||||
|
||||
setTimeout(() => {
|
||||
state.maximizing = false
|
||||
}, 50)
|
||||
}
|
||||
|
||||
const onMove = (e: Electron.Event) => {
|
||||
e.preventDefault()
|
||||
if (state.maximizing) return
|
||||
if (state.maximized) {
|
||||
state.win?.restore()
|
||||
if (process.platform !== 'darwin') document.body.style.borderRadius = ''
|
||||
state.maximized = false
|
||||
}
|
||||
}
|
||||
|
||||
const restart = () => {
|
||||
ipcRenderer.send('restart_app')
|
||||
}
|
||||
|
||||
export { state }
|
||||
|
||||
export default () => {
|
||||
store = useStore()
|
||||
router = useRouter()
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
$(window).off('resize')
|
||||
state.win?.removeListener('move', onMove)
|
||||
ipcRenderer.removeAllListeners('win_id')
|
||||
})
|
||||
|
||||
const process = window.process
|
||||
|
||||
onMounted(() => {
|
||||
$(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', () => {
|
||||
ipcRenderer.removeAllListeners('update_available')
|
||||
})
|
||||
|
||||
ipcRenderer.on('update_downloaded', () => {
|
||||
ipcRenderer.removeAllListeners('update_downloaded')
|
||||
state.updateAvailable = true
|
||||
})
|
||||
|
||||
ipcRenderer.on('navigateTo', (sender, id) => {
|
||||
if (isNaN(id)) {
|
||||
router?.push('/chat/new').catch(() => {})
|
||||
} else {
|
||||
const arrayId = parseInt(id) - 1
|
||||
if (chatsState.chats[arrayId]) {
|
||||
router?.push('/chat/' + chatsState.chats[arrayId].personId).catch(() => {})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ipcRenderer.on('win_id', (e, id) => {
|
||||
state.win = remote.BrowserWindow.fromId(id)
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
if (state.maximizing) return
|
||||
if (state.maximized) {
|
||||
state.win?.restore()
|
||||
if (process.platform !== 'darwin') document.body.style.borderRadius = ''
|
||||
state.maximized = false
|
||||
}
|
||||
})
|
||||
|
||||
state.win?.on('move', onMove)
|
||||
|
||||
if (!(store?.state.macstyle || process.platform === 'darwin')) {
|
||||
document.body.style.border = 'none'
|
||||
document.body.style.borderRadius = '0'
|
||||
}
|
||||
|
||||
if (!store?.state.acceleration) {
|
||||
document.documentElement.style.backgroundColor = 'black'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
state,
|
||||
statusColor,
|
||||
statusText,
|
||||
process,
|
||||
closeWindow,
|
||||
minimizeWindow,
|
||||
maximizeWindow,
|
||||
restart,
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user