diff --git a/package.json b/package.json index 0e755c1..f73aef1 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/background.ts b/src/background.ts index 62be015..fa99c3f 100644 --- a/src/background.ts +++ b/src/background.ts @@ -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(() => { diff --git a/src/components/AutoComplete/AutocompleteCore.js b/src/components/AutoComplete/AutocompleteCore.js new file mode 100644 index 0000000..e32ff0c --- /dev/null +++ b/src/components/AutoComplete/AutocompleteCore.js @@ -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 diff --git a/src/components/AutoComplete/index.vue b/src/components/AutoComplete/index.vue new file mode 100644 index 0000000..61db2c4 --- /dev/null +++ b/src/components/AutoComplete/index.vue @@ -0,0 +1,337 @@ +// Forked from trevoreyre/autocomplete + + + + + + diff --git a/src/components/AutoComplete/util/closest.js b/src/components/AutoComplete/util/closest.js new file mode 100644 index 0000000..5f2a038 --- /dev/null +++ b/src/components/AutoComplete/util/closest.js @@ -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 diff --git a/src/components/AutoComplete/util/debounce.js b/src/components/AutoComplete/util/debounce.js new file mode 100644 index 0000000..7769a36 --- /dev/null +++ b/src/components/AutoComplete/util/debounce.js @@ -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 diff --git a/src/components/AutoComplete/util/getRelativePosition.js b/src/components/AutoComplete/util/getRelativePosition.js new file mode 100644 index 0000000..6b00d74 --- /dev/null +++ b/src/components/AutoComplete/util/getRelativePosition.js @@ -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 diff --git a/src/components/AutoComplete/util/isPromise.js b/src/components/AutoComplete/util/isPromise.js new file mode 100644 index 0000000..b5a21e1 --- /dev/null +++ b/src/components/AutoComplete/util/isPromise.js @@ -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 diff --git a/src/components/AutoComplete/util/matches.js b/src/components/AutoComplete/util/matches.js new file mode 100644 index 0000000..ddf187b --- /dev/null +++ b/src/components/AutoComplete/util/matches.js @@ -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 diff --git a/src/components/AutoComplete/util/uniqueId.js b/src/components/AutoComplete/util/uniqueId.js new file mode 100644 index 0000000..f6f183d --- /dev/null +++ b/src/components/AutoComplete/util/uniqueId.js @@ -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 diff --git a/src/components/PayloadAttachment.vue b/src/components/PayloadAttachment.vue index e91a96f..72c7277 100644 --- a/src/components/PayloadAttachment.vue +++ b/src/components/PayloadAttachment.vue @@ -188,7 +188,7 @@ export default { align-items: center; justify-content: center; - >>> .feather { + .feather:deep() { margin-left: 1px; } } diff --git a/src/components/Settings.vue b/src/components/Settings.vue index 7e83732..2ae53c2 100644 --- a/src/components/Settings.vue +++ b/src/components/Settings.vue @@ -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') diff --git a/src/store/index.ts b/src/store/index.ts index b47ff14..c67e5b9 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -123,7 +123,6 @@ export default createStore({ state['isTypingTimer'][data.chatId] = setTimeout(() => { state['isTyping'][data.chatId] = false - console.log('closing') }, 60000) }, }, diff --git a/src/types/modules.d.ts b/src/types/modules.d.ts index dba5133..939851e 100644 --- a/src/types/modules.d.ts +++ b/src/types/modules.d.ts @@ -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' diff --git a/src/views/Chat/index.vue b/src/views/Chat/index.vue index 68e945f..368312e 100644 --- a/src/views/Chat/index.vue +++ b/src/views/Chat/index.vue @@ -281,21 +281,19 @@