Fixes, rewrites, and stable Win notifications

(finally)
This commit is contained in:
Aziz Hasanain 2021-04-06 13:01:56 +03:00
parent 1ca699727b
commit 518baba5ea
23 changed files with 793 additions and 161 deletions

View File

@ -23,7 +23,6 @@
"@fortawesome/free-solid-svg-icons": "^5.15.2",
"@fortawesome/vue-fontawesome": "^3.0.0-3",
"@techassi/vue-lazy-image": "^1.0.0",
"@trevoreyre/autocomplete-vue": "^2.2.0",
"auto-launch": "^5.0.5",
"axios": "^0.21.1",
"bplist-parser": "^0.3.0",
@ -40,18 +39,15 @@
"jquery": "^3.6.0",
"moment": "^2.29.1",
"node-notifier": "^9.0.1",
"powertoast": "^1.2.3",
"rangy": "^1.3.0",
"simplebar-vue": "^1.6.0",
"usbmux": "^0.1.0",
"vue": "^3.0.0",
"vue-avatar": "^2.3.3",
"vue-class-component": "^8.0.0-0",
"vue-confirm-dialog": "^1.0.2",
"vue-feather": "^2.0.0-beta",
"vue-js-popover": "^1.2.1",
"vue-linkify": "^1.0.1",
"vue-long-click": "^0.0.4",
"vue-native-websocket": "^2.0.14",
"vue-native-websocket-vue3": "^3.1.0",
"vue-router": "^4.0.0-0",
"vuex": "^4.0.0-0"
@ -61,6 +57,7 @@
"@types/electron-devtools-installer": "^2.2.0",
"@types/electron-localshortcut": "^3.1.0",
"@types/jquery": "^3.5.5",
"@types/node-notifier": "^8.0.0",
"@types/rangy": "^0.0.33",
"@typescript-eslint/eslint-plugin": "^2.33.0",
"@typescript-eslint/parser": "^2.33.0",

View File

@ -182,6 +182,8 @@ function showWin() {
else {
win.setSkipTaskbar(false)
win.show()
win.setAlwaysOnTop(true)
win.setAlwaysOnTop(false) // weird work around but it works
}
if (app.dock) app.dock.show()
@ -267,12 +269,6 @@ ipcMain.on('loaded', () => {
registerShortcuts()
})
// ipcMain.on('notification', (event, opts) => {
// Notifier.notify("hello??")
// Notifier.notify(opts)
// console.log("notifying with", opts)
// })
ipcMain.on('rightClickMessage', (event, args) => {
rightClickedMessage = args
})
@ -292,7 +288,7 @@ ipcMain.on('startup_check', () => {
autoLauncher
.isEnabled()
.then(function(isEnabled) {
const optionEnabled = persistentStore.get('startup', false)
const optionEnabled = persistentStore.get('launchOnStartup', false)
if (optionEnabled && !isEnabled) {
autoLauncher.enable()
@ -317,11 +313,15 @@ ipcMain.on('quit_app', () => {
app.quit()
})
ipcMain.on('minimizeToTray', () => {
function minimizeToTray () {
if (!win) return
win.setSkipTaskbar(true)
win.hide()
if (app.dock) app.dock.hide()
}
ipcMain.on('minimizeToTray', () => {
minimizeToTray()
})
ipcMain.on('show_win', () => {
@ -351,7 +351,16 @@ function registerLocalFileProtocols() {
}
})
// protocol.registerHttpProtocol('WebMessage', (request, callback) => {})
app.removeAsDefaultProtocolClient('webmessage');
// If we are running a non-packaged version of the app && on windows
if(isDevelopment && process.platform === 'win32') {
// Set the path of electron.exe and your app.
// These two additional parameters are only available on windows.
app.setAsDefaultProtocolClient('webmessage', process.execPath, [path.resolve(process.argv[1])])
} else {
app.setAsDefaultProtocolClient('webmessage');
}
}
const gotTheLock = app.requestSingleInstanceLock()
@ -359,8 +368,12 @@ const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
app.quit()
} else {
app.on('second-instance', () => {
app.on('second-instance', (event, args) => {
showWin()
let uriLocation = args.pop() as string
let personId = uriLocation.split(':').pop()
if (!personId || personId.trim() == '') return
if (win) win.webContents.send('navigateChat', personId)
})
app.whenReady().then(() => {

View File

@ -0,0 +1,186 @@
import closest from './util/closest.js'
import isPromise from './util/isPromise.js'
class AutocompleteCore {
value = ''
searchCounter = 0
results = []
selectedIndex = -1
constructor({
search,
autoSelect = false,
setValue = () => {},
setAttribute = () => {},
onUpdate = () => {},
onSubmit = () => {},
onShow = () => {},
onHide = () => {},
onLoading = () => {},
onLoaded = () => {},
} = {}) {
this.search = isPromise(search) ? search : value => Promise.resolve(search(value))
this.autoSelect = autoSelect
this.setValue = setValue
this.setAttribute = setAttribute
this.onUpdate = onUpdate
this.onSubmit = onSubmit
this.onShow = onShow
this.onHide = onHide
this.onLoading = onLoading
this.onLoaded = onLoaded
}
destroy = () => {
this.search = null
this.setValue = null
this.setAttribute = null
this.onUpdate = null
this.onSubmit = null
this.onShow = null
this.onHide = null
this.onLoading = null
this.onLoaded = null
}
handleInput = event => {
const { value } = event.target
this.updateResults(value)
this.value = value
}
handleKeyDown = event => {
const { key } = event
switch (key) {
case 'Up': // IE/Edge
case 'Down': // IE/Edge
case 'ArrowUp':
case 'ArrowDown': {
const selectedIndex = key === 'ArrowUp' || key === 'Up' ? this.selectedIndex - 1 : this.selectedIndex + 1
event.preventDefault()
this.handleArrows(selectedIndex)
break
}
case 'Tab': {
this.selectResult()
break
}
case 'Enter': {
const selectedResult = this.results[this.selectedIndex]
this.selectResult()
this.onSubmit(selectedResult)
break
}
case 'Esc': // IE/Edge
case 'Escape': {
this.hideResults()
this.setValue()
break
}
default:
return
}
}
handleFocus = event => {
const { value } = event.target
this.updateResults(value)
this.value = value
}
handleBlur = () => {
this.hideResults()
}
// The mousedown event fires before the blur event. Calling preventDefault() when
// the results list is clicked will prevent it from taking focus, firing the
// blur event on the input element, and closing the results list before click fires.
handleResultMouseDown = event => {
event.preventDefault()
}
handleResultClick = event => {
const { target } = event
const result = closest(target, '[data-result-index]')
if (result) {
this.selectedIndex = parseInt(result.dataset.resultIndex, 10)
const selectedResult = this.results[this.selectedIndex]
this.selectResult()
this.onSubmit(selectedResult)
}
}
handleArrows = selectedIndex => {
// Loop selectedIndex back to first or last result if out of bounds
const resultsCount = this.results.length
this.selectedIndex = ((selectedIndex % resultsCount) + resultsCount) % resultsCount
// Update results and aria attributes
this.onUpdate(this.results, this.selectedIndex)
}
selectResult = () => {
const selectedResult = this.results[this.selectedIndex]
if (selectedResult) {
this.setValue(selectedResult)
}
this.hideResults()
}
updateResults = value => {
const currentSearch = ++this.searchCounter
this.onLoading()
this.search(value).then(results => {
if (currentSearch !== this.searchCounter) {
return
}
this.results = results
this.onLoaded()
if (this.results.length === 0) {
this.hideResults()
return
}
this.selectedIndex = this.autoSelect ? 0 : -1
this.onUpdate(this.results, this.selectedIndex)
this.showResults()
})
}
showResults = () => {
this.setAttribute('aria-expanded', true)
this.onShow()
}
hideResults = () => {
this.selectedIndex = -1
this.results = []
this.setAttribute('aria-expanded', false)
this.setAttribute('aria-activedescendant', '')
this.onUpdate(this.results, this.selectedIndex)
this.onHide()
}
// Make sure selected result isn't scrolled out of view
checkSelectedResultVisible = resultsElement => {
const selectedResultElement = resultsElement.querySelector(`[data-result-index="${this.selectedIndex}"]`)
if (!selectedResultElement) {
return
}
const resultsPosition = resultsElement.getBoundingClientRect()
const selectedPosition = selectedResultElement.getBoundingClientRect()
if (selectedPosition.top < resultsPosition.top) {
// Element is above viewable area
resultsElement.scrollTop -= resultsPosition.top - selectedPosition.top
} else if (selectedPosition.bottom > resultsPosition.bottom) {
// Element is below viewable area
resultsElement.scrollTop += selectedPosition.bottom - resultsPosition.bottom
}
}
}
export default AutocompleteCore

View File

@ -0,0 +1,337 @@
// Forked from trevoreyre/autocomplete
<template>
<div ref="root">
<slot
:rootProps="rootProps"
:inputProps="inputProps"
:inputListeners="inputListeners"
:resultListProps="resultListProps"
:resultListListeners="resultListListeners"
:results="results"
:resultProps="resultProps"
>
<div v-bind="rootProps">
<input
ref="input"
v-bind="inputProps"
@input="handleInput"
@keydown="core.handleKeyDown"
@focus="core.handleFocus"
@blur="core.handleBlur"
v-on="$attrs"
/>
<ul ref="resultList" v-bind="resultListProps" v-on="resultListListeners">
<template v-for="(result, index) in results">
<slot name="result" :result="result" :props="resultProps[index]">
<li :key="resultProps[index].id" v-bind="resultProps[index]">
{{ getResultValue(result) }}
</li>
</slot>
</template>
</ul>
</div>
</slot>
</div>
</template>
<script>
import AutocompleteCore from './AutocompleteCore.js'
import uniqueId from './util/uniqueId.js'
import getRelativePosition from './util/getRelativePosition.js'
import debounce from './util/debounce.js'
export default {
name: 'Autocomplete',
inheritAttrs: false,
props: {
search: {
type: Function,
required: true,
},
baseClass: {
type: String,
default: 'autocomplete',
},
autoSelect: {
type: Boolean,
default: false,
},
getResultValue: {
type: Function,
default: result => result,
},
defaultValue: {
type: String,
default: '',
},
debounceTime: {
type: Number,
default: 0,
},
},
data() {
const core = new AutocompleteCore({
search: this.search,
autoSelect: this.autoSelect,
setValue: this.setValue,
onUpdate: this.handleUpdate,
onSubmit: this.handleSubmit,
onShow: this.handleShow,
onHide: this.handleHide,
onLoading: this.handleLoading,
onLoaded: this.handleLoaded,
})
if (this.debounceTime > 0) {
core.handleInput = debounce(core.handleInput, this.debounceTime)
}
return {
core,
value: this.defaultValue,
resultListId: uniqueId(`${this.baseClass}-result-list-`),
results: [],
selectedIndex: -1,
expanded: false,
loading: false,
position: 'below',
resetPosition: true,
}
},
computed: {
rootProps() {
return {
class: this.baseClass,
style: { position: 'relative' },
'data-expanded': this.expanded,
'data-loading': this.loading,
'data-position': this.position,
}
},
inputProps() {
return {
class: `${this.baseClass}-input`,
value: this.value,
role: 'combobox',
autocomplete: 'off',
autocapitalize: 'off',
autocorrect: 'off',
spellcheck: 'false',
'aria-autocomplete': 'list',
'aria-haspopup': 'listbox',
'aria-owns': this.resultListId,
'aria-expanded': this.expanded ? 'true' : 'false',
'aria-activedescendant': this.selectedIndex > -1 ? this.resultProps[this.selectedIndex].id : '',
...this.$attrs,
}
},
inputListeners() {
return {
input: this.handleInput,
keydown: this.core.handleKeyDown,
focus: this.core.handleFocus,
blur: this.core.handleBlur,
}
},
resultListProps() {
const yPosition = this.position === 'below' ? 'top' : 'bottom'
return {
id: this.resultListId,
class: `${this.baseClass}-result-list`,
role: 'listbox',
style: {
position: 'absolute',
zIndex: 1,
width: '100%',
visibility: this.expanded ? 'visible' : 'hidden',
pointerEvents: this.expanded ? 'auto' : 'none',
[yPosition]: '100%',
},
}
},
resultListListeners() {
return {
mousedown: this.core.handleResultMouseDown,
click: this.core.handleResultClick,
}
},
resultProps() {
return this.results.map((result, index) => ({
id: `${this.baseClass}-result-${index}`,
class: `${this.baseClass}-result`,
'data-result-index': index,
role: 'option',
...(this.selectedIndex === index ? { 'aria-selected': 'true' } : {}),
}))
},
},
mounted() {
document.body.addEventListener('click', this.handleDocumentClick)
},
beforeUnmount() {
document.body.removeEventListener('click', this.handleDocumentClick)
},
updated() {
if (!this.$refs.input || !this.$refs.resultList) {
return
}
if (this.resetPosition && this.results.length > 0) {
this.resetPosition = false
this.position = getRelativePosition(this.$refs.input, this.$refs.resultList)
}
this.core.checkSelectedResultVisible(this.$refs.resultList)
},
methods: {
setValue(result) {
this.value = result ? this.getResultValue(result) : ''
},
handleUpdate(results, selectedIndex) {
this.results = results
this.selectedIndex = selectedIndex
this.$emit('update', results, selectedIndex)
},
handleShow() {
this.expanded = true
},
handleHide() {
this.expanded = false
this.resetPosition = true
},
handleLoading() {
this.loading = true
},
handleLoaded() {
this.loading = false
},
handleInput(event) {
this.value = event.target.value
this.core.handleInput(event)
},
handleSubmit(selectedResult) {
this.$emit('submit', selectedResult)
},
handleDocumentClick(event) {
if (this.$refs.root.contains(event.target)) {
return
}
this.core.hideResults()
},
},
}
</script>
<style>
.autocomplete-input {
border: 1px solid #eee;
border-radius: 8px;
width: 100%;
padding: 12px 12px 12px 48px;
box-sizing: border-box;
position: relative;
font-size: 16px;
line-height: 1.5;
flex: 1;
background-color: #eee;
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNjY2IiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PGNpcmNsZSBjeD0iMTEiIGN5PSIxMSIgcj0iOCIvPjxwYXRoIGQ9Ik0yMSAyMWwtNC00Ii8+PC9zdmc+');
background-repeat: no-repeat;
background-position: 12px center;
}
.autocomplete-input:focus,
.autocomplete-input[aria-expanded='true'] {
border-color: rgba(0, 0, 0, 0.12);
background-color: #fff;
outline: none;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.16);
}
[data-position='below'] .autocomplete-input[aria-expanded='true'] {
border-bottom-color: transparent;
border-radius: 8px 8px 0 0;
}
[data-position='above'] .autocomplete-input[aria-expanded='true'] {
border-top-color: transparent;
border-radius: 0 0 8px 8px;
z-index: 2;
}
/* Loading spinner */
.autocomplete[data-loading='true']::after {
content: '';
border: 3px solid rgba(0, 0, 0, 0.12);
border-right: 3px solid rgba(0, 0, 0, 0.48);
border-radius: 100%;
width: 20px;
height: 20px;
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
animation: rotate 1s infinite linear;
}
.autocomplete-result-list {
margin: 0;
border: 1px solid rgba(0, 0, 0, 0.12);
padding: 0;
box-sizing: border-box;
max-height: 296px;
overflow-y: auto;
background: #fff;
list-style: none;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.16);
}
[data-position='below'] .autocomplete-result-list {
margin-top: -1px;
border-top-color: transparent;
border-radius: 0 0 8px 8px;
padding-bottom: 8px;
}
[data-position='above'] .autocomplete-result-list {
margin-bottom: -1px;
border-bottom-color: transparent;
border-radius: 8px 8px 0 0;
padding-top: 8px;
}
/* Single result item */
.autocomplete-result {
cursor: default;
padding: 12px 12px 12px 48px;
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjY2NjIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PGNpcmNsZSBjeD0iMTEiIGN5PSIxMSIgcj0iOCIvPjxwYXRoIGQ9Ik0yMSAyMWwtNC00Ii8+PC9zdmc+');
background-repeat: no-repeat;
background-position: 12px center;
}
.autocomplete-result:hover,
.autocomplete-result[aria-selected='true'] {
background-color: rgba(0, 0, 0, 0.06);
}
@keyframes rotate {
from {
transform: translateY(-50%) rotate(0deg);
}
to {
transform: translateY(-50%) rotate(359deg);
}
}
</style>

View File

@ -0,0 +1,22 @@
// Polyfill for element.closest, to support Internet Explorer. It's a relatively
// simple polyfill, so we'll just include it rather than require the user to
// include the polyfill themselves. Adapted from
// https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
import matches from './matches.js'
const closestPolyfill = (el, selector) => {
let element = el
while (element && element.nodeType === 1) {
if (matches(element, selector)) {
return element
}
element = element.parentNode
}
return null
}
const closest = (element, selector) => {
return element.closest ? element.closest(selector) : closestPolyfill(element, selector)
}
export default closest

View File

@ -0,0 +1,29 @@
// Credit David Walsh (https://davidwalsh.name/javascript-debounce-function)
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
const debounce = (func, wait, immediate) => {
let timeout
return function executedFunction() {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const context = this
// eslint-disable-next-line prefer-rest-params
const args = arguments
const later = function() {
timeout = null
if (!immediate) func.apply(context, args)
}
const callNow = immediate && !timeout
clearTimeout(timeout)
timeout = setTimeout(later, wait)
if (callNow) func.apply(context, args)
}
}
export default debounce

View File

@ -0,0 +1,18 @@
// Calculates whether element2 should be above or below element1. Always
// places element2 below unless all of the following:
// 1. There isn't enough visible viewport below to fit element2
// 2. There is more room above element1 than there is below
// 3. Placing elemen2 above 1 won't overflow window
const getRelativePosition = (element1, element2) => {
const position1 = element1.getBoundingClientRect()
const position2 = element2.getBoundingClientRect()
const positionAbove =
/* 1 */ position1.bottom + position2.height > window.innerHeight &&
/* 2 */ window.innerHeight - position1.bottom < position1.top &&
/* 3 */ window.pageYOffset + position1.top - position2.height > 0
return positionAbove ? 'above' : 'below'
}
export default getRelativePosition

View File

@ -0,0 +1,5 @@
// Returns true if the value has a "then" function. Adapted from
// https://github.com/graphql/graphql-js/blob/499a75939f70c4863d44149371d6a99d57ff7c35/src/jsutils/isPromise.js
const isPromise = value => Boolean(value && typeof value.then === 'function')
export default isPromise

View File

@ -0,0 +1,15 @@
// Polyfill for element.matches, to support Internet Explorer. It's a relatively
// simple polyfill, so we'll just include it rather than require the user to
// include the polyfill themselves. Adapted from
// https://developer.mozilla.org/en-US/docs/Web/API/Element/matches#Polyfill
const matches = (element, selector) => {
return element.matches
? element.matches(selector)
: element.msMatchesSelector
? element.msMatchesSelector(selector)
: element.webkitMatchesSelector
? element.webkitMatchesSelector(selector)
: null
}
export default matches

View File

@ -0,0 +1,6 @@
// Generates a unique ID, with optional prefix. Adapted from
// https://github.com/lodash/lodash/blob/61acdd0c295e4447c9c10da04e287b1ebffe452c/uniqueId.js
let idCounter = 0
const uniqueId = (prefix = '') => `${prefix}${++idCounter}`
export default uniqueId

View File

@ -188,7 +188,7 @@ export default {
align-items: center;
justify-content: center;
>>> .feather {
.feather:deep() {
margin-left: 1px;
}
}

View File

@ -364,6 +364,7 @@ export default {
mounted() {
this.loadValues()
ipcRenderer.send('startup_check')
ipcRenderer.send('app_version')
ipcRenderer.on('app_version', (event, arg) => {
ipcRenderer.removeAllListeners('app_version')

View File

@ -123,7 +123,6 @@ export default createStore({
state['isTypingTimer'][data.chatId] = setTimeout(() => {
state['isTyping'][data.chatId] = false
console.log('closing')
}, 60000)
},
},

View File

@ -6,17 +6,12 @@ declare module 'vue-linkify'
declare module 'vue-confirm-dialog'
declare module 'vue-long-click'
declare module 'vue-js-popover'
declare module 'powertoast'
declare module 'bplist-parser'
declare module '@techassi/vue-lazy-image'
declare module './mixin'
declare module '@trevoreyre/autocomplete-vue'
declare module '@/components/VideoPlayer'
declare module '@/components/ExpandableImage'
declare module '@/components/DownloadAttachment'
declare module '@/components/UploadButton'
declare module '@/components/ReactionMenu'
declare module '@/components/ReactionBubbles'
declare module '@trevoreyre/autocomplete-vue/index.js'
declare module '@/views/Home'
declare module '@/views/Chat'
declare module '@/views/Blank'

View File

@ -281,21 +281,19 @@
</template>
<script lang="ts">
import Autocomplete from '@trevoreyre/autocomplete-vue'
import VideoPlayer from '@/components/VideoPlayer'
import ExpandableImage from '@/components/ExpandableImage'
import DownloadAttachment from '@/components/DownloadAttachment'
import UploadButton from '@/components/UploadButton'
import ReactionMenu from '@/components/ReactionMenu'
import ReactionBubbles from '@/components/ReactionBubbles'
import Autocomplete from '@/components/AutoComplete/index.vue'
import VideoPlayer from '@/components/VideoPlayer.vue'
import ExpandableImage from '@/components/ExpandableImage.vue'
import DownloadAttachment from '@/components/DownloadAttachment.vue'
import UploadButton from '@/components/UploadButton.vue'
import ReactionMenu from '@/components/ReactionMenu.vue'
import ReactionBubbles from '@/components/ReactionBubbles.vue'
import TypingIndicator from '@/components/TypingIndicator.vue'
import PayloadAttachment from '@/components/PayloadAttachment.vue'
import Avatar from '@/components/Avatar.vue'
import AudioPlayer from '@/components/AudioPlayer.vue'
import UnsupportedMessage from '@/components/UnsupportedMessage.vue'
import { Picker } from 'emoji-mart-vue-fast/src/index'
import './messages'
import '@trevoreyre/autocomplete-vue/dist/style.css'
import 'emoji-mart-vue-fast/css/emoji-mart.css'
import functionsComp from './functions'

View File

@ -151,6 +151,8 @@ export default () => {
delete state.messageText[watchRoute.params.id as string]
delete state.subjectText[watchRoute.params.id as string]
state.lastTypingValue = ''
state.lastTypingStamp = 0
const messageInputRef = currentInstance?.refs.messageInput as HTMLElement
const subjectInputRef = currentInstance?.refs.subjectInput as HTMLElement
@ -338,7 +340,7 @@ export default () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const onSubmit = (result: any) => {
if (result && result.personId) {
router?.push('/message/' + result.personId)
router?.push('/chat/' + result.personId)
} else if (result) {
autoCompleteInput(result)
} else {
@ -368,6 +370,7 @@ export default () => {
search,
getResultValue,
onSubmit,
sendText,
state,
}
}

View File

@ -157,7 +157,7 @@ const newMessageHandler = (response: any) => {
if (route.params.id == 'new') {
if (Object.keys(message).length > 0) {
if (message[0].chatId == state.receiver) {
router.push('/message/' + message[0].personId)
router.push('/chat/' + message[0].personId)
}
}
return

View File

@ -169,7 +169,6 @@ const newMessageHandler = (response: any) => {
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')) {
@ -186,7 +185,7 @@ const newMessageHandler = (response: any) => {
icon: null as Nullable<string>,
}
if (document.hasFocus()) return
if (document.hasFocus() && remote.getCurrentWindow().isVisible() && route?.params.id == messageData.personId) return
sendNotifierNotification(notificationOptions, messageData, chatData)
} else if (messageData.sender != 1) {
console.log('Notifications are not supported on this system.')
@ -322,6 +321,9 @@ const onSocketDisconnect = (e: Event | CloseEvent) => {
store?.commit('resetMessages')
router?.push('/').catch(() => {})
state.chats = []
// eslint-disable-next-line @typescript-eslint/no-use-before-define
if (windowState.status == 0) setTimeout(connectWS, 1000)
}
const onSocketConnected = () => {
@ -445,5 +447,6 @@ export default () => {
deleteChat,
composeMessage,
markAsRead,
connectWS,
}
}

View File

@ -1,64 +1,71 @@
<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
id="vueApp"
:class="{
nostyle: !($store.state.macstyle || window.process.platform === 'darwin'),
maximized: (window.state.maximized || !$store.state.acceleration) && window.process.platform !== 'darwin',
}"
>
<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>&ndash;</span></span>
</div>
<div class="zoom" @click="window.maximizeWindow">
<span class="zoombutton"><span>+</span></span>
</div>
</div>
<div class="minimize" @click="window.minimizeWindow">
<span class="minimizebutton"><span>&ndash;</span></span>
<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="zoom" @click="window.maximizeWindow">
<span class="zoombutton"><span>+</span></span>
<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="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 class="searchContainer">
<input type="search" placeholder="Search" class="textinput" v-model="chats.search" />
</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 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 class="searchContainer">
<input type="search" placeholder="Search" class="textinput" v-model="chats.search" />
<div id="content">
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" @markAsRead="chats.markAsRead" />
</transition>
</router-view>
</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>
<script lang="ts">
import Chat from '@/components/Chat.vue'
import Settings from '@/components/Settings.vue'
import Tooltip from '@/components/Tooltip.vue'
@ -221,7 +228,7 @@ body {
border-radius: 10px;
}
#app {
#vueApp {
font-family: 'Roboto', -apple-system, BlinkMacSystemFont, Avenir, Helvetica, Arial, sans-serif;
font-weight: 300;
text-align: center;

View File

@ -1,7 +1,9 @@
import { ipcRenderer } from 'electron'
import { reactive } from 'vue'
import { Router, useRouter } from 'vue-router'
import { RouteLocationNormalizedLoaded, Router, useRoute, useRouter } from 'vue-router'
import { Store, useStore } from 'vuex'
import { remote } from 'electron'
import toast from 'powertoast'
interface NotificationOptions {
appID: string
@ -10,8 +12,13 @@ interface NotificationOptions {
sound: boolean
reply?: boolean
icon?: Nullable<string>
actions?: string[]
cropIcon?: boolean
silent?: boolean
onClick?: string
}
let route: Nullable<RouteLocationNormalizedLoaded> = null
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let store: Nullable<Store<any>> = null
let router: Nullable<Router> = null
@ -20,6 +27,19 @@ const state = reactive({
notifSound: null as Nullable<HTMLAudioElement>,
})
const sendPowertoastNotification = (
options: NotificationOptions,
messageData: { authorDocid: string; personId: string },
chatData: { id: string }
) => {
options.silent = !options.sound
options.cropIcon = true
options.onClick = 'webmessage:' + messageData.personId
toast(options).catch((err: string) => {
console.log(err)
})
}
const sendNotifierNotification = (
options: NotificationOptions,
messageData: { authorDocid: string; personId: string },
@ -28,7 +48,7 @@ const sendNotifierNotification = (
const path = require('path')
const fs = require('fs')
const https = require('https')
const tempDir = path.join(__dirname, '..', 'temp')
const tempDir = path.join(remote.app.getAppPath(), '..', 'temp')
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir)
@ -42,36 +62,44 @@ const sendNotifierNotification = (
agent: new https.Agent({ rejectUnauthorized: false }),
}
console.log('trying to download', dlOptions)
download
.image(dlOptions)
.then(() => {
options.icon = fileDir
if (fs.statSync(fileDir).size == 0) throw Error('Empty image')
})
.catch(() => {
.catch((err: any) => {
console.log(err)
options.icon = __static + '/icon.png'
})
.finally(() => {
console.log('done downloading')
if (!store?.state.systemSound) state.notifSound?.play()
if (document.hasFocus() && remote.getCurrentWindow().isVisible() && route?.params.id != messageData.personId) return
const NotificationCenter = require('node-notifier').NotificationCenter
let Notifier = null
let notifier: any = null
if (process.platform === 'darwin') {
Notifier = new NotificationCenter({
notifier = new NotificationCenter({
withFallback: true,
customPath: path.join(
__dirname,
'../terminal-notifier/vendor/mac.noindex/terminal-notifier.app/Contents/MacOS/terminal-notifier'
),
})
} else if (process.platform == 'win32') {
sendPowertoastNotification(options, messageData, chatData)
return
} else {
Notifier = require('node-notifier')
notifier = require('node-notifier')
}
Notifier.notify(options, (err: string, action: string) => {
notifier.notify(options, (err: any, action: any) => {
if (err) return
if ((action == 'activate' || action == undefined) && chatData && chatData.id) {
if ((action == 'activate' || action == 'click') && chatData && chatData.id) {
ipcRenderer.send('show_win')
router?.push('/chat/' + messageData.personId)
}
@ -83,6 +111,7 @@ export { state, sendNotifierNotification }
export default () => {
store = useStore()
route = useRoute()
router = useRouter()
return { state }

View File

@ -127,8 +127,13 @@ export default () => {
}
})
ipcRenderer.on('navigateChat', (sender, personId) => {
router?.push('/chat/' + personId).catch(() => {})
})
ipcRenderer.on('win_id', (e, id) => {
state.win = remote.BrowserWindow.fromId(id)
// state.win = remote.BrowserWindow.fromId(id)
state.win = remote.getCurrentWindow()
window.addEventListener('resize', () => {
if (state.maximizing) return

View File

@ -21,6 +21,13 @@ module.exports = {
// preload: 'src/preload.js',
builderOptions: {
appId: 'com.sgtaziz.WebMessage',
protocols: [
{
name: 'WebMessage',
role: 'Viewer',
schemes: ['webmessage'],
},
],
productName: 'WebMessage',
publish: ['github'],
snap: {
@ -38,8 +45,8 @@ module.exports = {
to: 'resources/vendor',
},
{
from: 'vendor/',
to: 'resources/terminal-notifier/vendor',
from: 'terminal-notifier/',
to: 'resources/terminal-notifier',
},
],
},

View File

@ -1041,11 +1041,6 @@
dependencies:
vue "^3.0.0"
"@trevoreyre/autocomplete-vue@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@trevoreyre/autocomplete-vue/-/autocomplete-vue-2.2.0.tgz"
integrity sha512-A5j986nM6htTbCpEW9BbwlqCobIMD4+uicAYGCSI8DM1ojK8meCyVI23jK+gAxi+vjhraCBneKI+vbwB8sL0ig==
"@types/anymatch@*":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz"
@ -1169,15 +1164,22 @@
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.1.tgz"
integrity sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg==
"@types/node-notifier@^8.0.0":
version "8.0.0"
resolved "https://registry.yarnpkg.com/@types/node-notifier/-/node-notifier-8.0.0.tgz#51100d67155ed1500a8aaa633987109f59a0637d"
integrity sha512-CseIDQOC/I+yvj/4ItpG4ATcwooQlGPDDJweII8nspjjZg4ZBuvkyHg9P81QkElgU9FpYlb5A27BRggD3idTCQ==
dependencies:
"@types/node" "*"
"@types/node@*":
version "14.14.34"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.34.tgz"
integrity sha512-dBPaxocOK6UVyvhbnpFIj2W+S+1cBTkHQbFQfeeJhoKFbzYcVUGHvddeWPSucKATb3F0+pgDq0i6ghEaZjsugA==
"@types/node@^12.0.12":
version "12.20.5"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.5.tgz"
integrity sha512-5Oy7tYZnu3a4pnJ//d4yVvOImExl4Vtwf0D40iKUlU+XlUsyV9iyFWyCFlwy489b72FMAik/EFwRkNLjjOdSPg==
version "12.20.7"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.7.tgz#1cb61fd0c85cb87e728c43107b5fd82b69bc9ef8"
integrity sha512-gWL8VUkg8VRaCAUgG9WmhefMqHmMblxe2rVpMF86nZY/+ZysU+BkAp+3cz03AixWDSSz0ks5WX59yAhv/cDwFA==
"@types/node@^14.6.2":
version "14.14.35"
@ -2937,11 +2939,6 @@ camelcase@^6.0.0, camelcase@^6.2.0:
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz"
integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==
can-use-dom@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/can-use-dom/-/can-use-dom-0.1.0.tgz"
integrity sha1-IsxKNKCrxDlQ9CxkEQJKP2NmtFo=
caniuse-api@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz"
@ -3528,7 +3525,7 @@ core-js@^2.5.7:
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz"
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
core-js@^3.0.1, core-js@^3.1.3, core-js@^3.6.5:
core-js@^3.1.3, core-js@^3.6.5:
version "3.9.1"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.9.1.tgz"
integrity sha512-gSjRvzkxQc1zjM/5paAmL4idJBFzuJoo+jDjF1tStYFMV2ERfD02HhahhCGXUyHxQRG4yFKVSdO6g62eoRMcDg==
@ -4355,9 +4352,9 @@ electron@*:
extract-zip "^1.0.3"
electron@^11.0.0:
version "11.3.0"
resolved "https://registry.yarnpkg.com/electron/-/electron-11.3.0.tgz"
integrity sha512-MhdS0gok3wZBTscLBbYrOhLaQybCSAfkupazbK1dMP5c+84eVMxJE/QGohiWQkzs0tVFIJsAHyN19YKPbelNrQ==
version "11.4.2"
resolved "https://registry.yarnpkg.com/electron/-/electron-11.4.2.tgz#02005d9f5d77ea6485efeffdb5c5433c769ddda4"
integrity sha512-P0PRLH7cXp8ZdpA9yVPe7jRVM+QeiAtsadqmqS6XY3AYrsH+7bJnVrNuw6p/fcmp+b/UxWaCexobqQpyFJ5Qkw==
dependencies:
"@electron/get" "^1.0.1"
"@types/node" "^12.0.12"
@ -6932,11 +6929,6 @@ lodash.merge@^4.6.1:
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
lodash.throttle@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz"
integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=
lodash.transform@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.transform/-/lodash.transform-4.6.0.tgz"
@ -7527,7 +7519,7 @@ node-libs-browser@^2.2.1:
node-notifier@^9.0.1:
version "9.0.1"
resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-9.0.1.tgz"
resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-9.0.1.tgz#cea837f4c5e733936c7b9005e6545cea825d1af4"
integrity sha512-fPNFIp2hF/Dq7qLDzSg4vZ0J4e9v60gJR+Qx7RbjbWqzPDdEqeVpEx5CFeDAELIl+A/woaaNn1fQ5nEVerMxJg==
dependencies:
growly "^1.3.0"
@ -8647,6 +8639,11 @@ postcss@^8.1.10:
nanoid "^3.1.20"
source-map "^0.6.1"
powertoast@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/powertoast/-/powertoast-1.2.3.tgz#343d993b842eb0f9addb440867135f466d1af943"
integrity sha512-3p5m3ltqeEaQQbjMVNSyA9i4f52OgGVu5MK6IERWtX9L5+oFLwEb9gnQHDoNv+clTPDqhS3As5vJ0fTbZrCo5w==
prelude-ls@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz"
@ -9142,11 +9139,6 @@ requires-port@^1.0.0:
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz"
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
resize-observer-polyfill@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz"
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
resolve-cwd@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz"
@ -9583,26 +9575,6 @@ simple-swizzle@^0.2.2:
dependencies:
is-arrayish "^0.3.1"
simplebar-vue@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/simplebar-vue/-/simplebar-vue-1.6.0.tgz"
integrity sha512-O43JuN0eeI2kfspm+BME8DmfFKLkiOYRaMlGoTOExjiUHVrrGdXnyxRjewCWCSTfBTKy4tACv+mdrYfVxLZdBQ==
dependencies:
core-js "^3.0.1"
simplebar "^5.3.0"
simplebar@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/simplebar/-/simplebar-5.3.0.tgz"
integrity sha512-LgrGdIWpwHLLlI9HqfnGql62H/iZlF0KDZ7w3ZNbd2ZLwh9NKsODLHPzQgUlqQ8aZe7Y6/1xJMXK1PU5e810+w==
dependencies:
can-use-dom "^0.1.0"
core-js "^3.0.1"
lodash.debounce "^4.0.8"
lodash.memoize "^4.1.2"
lodash.throttle "^4.1.1"
resize-observer-polyfill "^1.5.1"
slash@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz"
@ -10896,11 +10868,6 @@ vm-browserify@^1.0.1:
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz"
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"
integrity sha512-Z57ILRTkFIAuCH9JiFBxX74C5zua5ub/jRDM/KZ+QKXNfscvmUOgWBs3kA2+wrpZMowIvfLHIT0gvQu1z+zpLg==
vue-class-component@^8.0.0-0:
version "8.0.0-rc.1"
resolved "https://registry.yarnpkg.com/vue-class-component/-/vue-class-component-8.0.0-rc.1.tgz"
@ -10932,11 +10899,6 @@ vue-cli-plugin-electron-builder@~2.0.0-rc.6:
webpack-merge "^4.2.2"
yargs "^15.3.1"
vue-confirm-dialog@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/vue-confirm-dialog/-/vue-confirm-dialog-1.0.2.tgz"
integrity sha512-gTo1bMDWOLd/6ihmWv8VlPxhc9QaKoE5YqlsKydUOfrrQ3Q3taljF6yI+1TMtAtJLrvZ8DYrePhgBhY1VCJzbQ==
vue-eslint-parser@^7.0.0, vue-eslint-parser@^7.6.0:
version "7.6.0"
resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-7.6.0.tgz"
@ -11004,11 +10966,6 @@ vue-native-websocket-vue3@^3.1.0:
resolved "https://registry.yarnpkg.com/vue-native-websocket-vue3/-/vue-native-websocket-vue3-3.1.0.tgz#e66ce6b88a1991b9f4a0404fb872e39ac3248401"
integrity sha512-pa53/81O7V+rm3asVDx402ruCUgPBE1CKAp+TEachvhN8geUqsIclj8N56+EoUFu/hpsbEI7eIVvVvGRNTn33A==
vue-native-websocket@^2.0.14:
version "2.0.14"
resolved "https://registry.yarnpkg.com/vue-native-websocket/-/vue-native-websocket-2.0.14.tgz"
integrity sha512-oK8+xG1gmqRs4JngHGwEc4zWoRjsdMB20Sz8pemkh4lW2Vr2676/cDRtd30aGnO2xF7Oxf003wS5JO0kypUsCw==
vue-observe-visibility@^0.4.4:
version "0.4.6"
resolved "https://registry.yarnpkg.com/vue-observe-visibility/-/vue-observe-visibility-0.4.6.tgz"