Repository navigation
feat: NethVoice chat #229
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
feat: NethVoice chat #229
Changes from all commits
Commits
Show all changes
56 commits
Select commit
Hold shift + click to select a range
af76223
feat: NethVoice chat
edospadoni bd5604e
chat: drag the chat window from the dock and the header
edospadoni 3f5cb09
chat: keep the chat window on screen while dragging; island 0.1.4
edospadoni 1d7a001
chat: retry the chat check, resend its state when NethLink reloads
edospadoni 31fbb72
command bar: chat with a colleague
edospadoni 26fb808
chat: richer desktop notification; island 0.1.5
edospadoni df24523
search: chat with a colleague from the results
edospadoni 7204c70
command bar: only Call up top, chat stays on colleague rows
edospadoni 84093fd
chat: delete or leave from the list, translations, group names; islan…
edospadoni f0ac616
chat: Messages list like the last calls, one chat icon, chat after la…
edospadoni 5c6a6bf
chat: island 0.1.8 (replies)
edospadoni 8640672
chat: shared contacts mapping and short dates; island 0.1.9
edospadoni b0299aa
chat: operator instead of colleague; island 0.1.10
edospadoni 9e3bec6
chat: operators no longer active labelled; island 0.1.11
edospadoni 435bc3f
chat: island 0.1.12
edospadoni 134d04d
chat: island 0.1.13 (chat.internal)
edospadoni 6f68dd1
chat: island 0.1.14
edospadoni dee1fd2
chat: island 0.1.15; shadows end inside the window
edospadoni b95838c
chat: island 0.1.16; the island's rings survive the shorter shadow
edospadoni fd7a299
chat: island 0.1.17 (delivered and read ticks)
edospadoni 510a0c6
chat: island 0.1.18; the expanded chat is not capped by the window
edospadoni 1fc0094
chat: island 0.1.19
edospadoni 2f82411
chat: island 0.1.20
edospadoni f96d815
chat: review fixes, island 0.1.23
edospadoni b82f7a4
chat: island 0.1.26
edospadoni 4c743a8
chat: island 0.1.27
edospadoni 48f58c8
chat: island 0.1.28, native tooltips on the heads (the window is size…
edospadoni 432f413
chat: island 0.1.28 (with nativeTooltips); drop a tsc build file
edospadoni 390c2fe
chat: island 0.1.29
edospadoni 7ac1c4a
chat: island 0.1.30; the list shows a group's size and "No messages y…
edospadoni 223d24e
chat: island 0.1.31; group looks as in the CTI
edospadoni e490054
chat: island 0.1.32
edospadoni 0f1b766
chat: island 0.1.33
edospadoni 56a0f59
chat: message icon in the menu, as in the CTI
edospadoni ec5a843
chat: island 0.1.34; the same message icons as the CTI
edospadoni cc9a856
chat: island 0.1.35
edospadoni 61bf461
chat: island 0.1.36
edospadoni 9b19db3
chat: island 0.1.37; the window grows from the corner nearest the dock
edospadoni 4f7bbff
chat: island 0.1.38
edospadoni 8d5f7fd
chat: island 0.1.39
edospadoni efb2525
chat-island 0.1.40
edospadoni bcfdeef
chat: blue accent, registered devices for the island, chat-island 0.1.41
edospadoni d100e8a
chat-island 0.1.42
edospadoni b62ad4b
chat-island 0.1.43
edospadoni a95a5c9
chat: island in NethLink's language, chat-island 0.1.44
edospadoni 183ac96
lockfile on main
edospadoni 6735458
chat: the dock's tooltips in the page, the window widens to show them…
edospadoni 74e3af4
chat: chat-island 0.1.45
edospadoni 45bb4b6
chat: chat-island 0.1.46
edospadoni b7c9c93
chat: chat-island 0.1.47
edospadoni f26b458
chat: chat-island 0.1.48
edospadoni 1422f96
chat: chat-island 0.1.49
edospadoni 900a788
chat-island 0.1.50
edospadoni d3a3bec
chat: no jitter on hover and no jump on drop for the chat window
edospadoni 31f683d
chat-island 0.1.51
edospadoni dbf58e2
chore(deps): phone-island 1.0.16
edospadoni File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,201 @@ | ||
| import { ChatIslandWindow } from '../windows' | ||
| import { Log } from '@shared/utils/logger' | ||
| import { IPC_EVENTS } from '@shared/constants' | ||
| import { screen } from 'electron' | ||
|
|
||
| type Corner = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' | ||
| type Rect = { x: number; y: number; w: number; h: number } | ||
| type Size = { | ||
| w: number | ||
| h: number | ||
| dock?: Rect | ||
| corner?: Corner | ||
| side?: number | ||
| } | ||
|
|
||
| // Between the window's edge and the dock: the island's right-4/left-4 and bottom-8/top-8. | ||
| // The page may ask for more room at the sides (`side`): that part may stick out of the screen. | ||
| const SIDE = 16 | ||
| const END = 32 | ||
|
|
||
| export class ChatIslandController { | ||
| static instance: ChatIslandController | undefined | ||
| window: ChatIslandWindow | ||
| private wantFocus = false | ||
| private focusTimer: ReturnType<typeof setTimeout> | undefined | ||
| // The work area's corner nearest the dock, and the window's distance from its two edges, kept after a drag. | ||
| private corner: Corner = 'bottom-right' | ||
| private anchor = { x: 0, y: 0 } | ||
| private displayId: number | undefined | ||
| private size: Size = { w: 0, h: 0 } | ||
| // While dragged the window keeps its size and only follows the cursor: resizes wait for the drop. | ||
| private drag: { w: number; h: number; x?: number; y?: number } | undefined | ||
| private through = false | ||
| // A new corner moves the dock to the window's other side: hidden until the island has drawn it there. | ||
| private flip: ReturnType<typeof setTimeout> | undefined | ||
|
|
||
| constructor() { | ||
| ChatIslandController.instance = this | ||
| this.window = new ChatIslandWindow() | ||
| } | ||
|
|
||
| // Sized to the island, grown from the corner nearest the dock; hidden when there is nothing to show. | ||
| resize(size: Size) { | ||
| try { | ||
| this.size = size | ||
| if (this.drag || (this.flip && size.corner !== this.corner)) return | ||
| // A reloaded island starts from the default corner: tell it the current one. | ||
| if (size.corner && size.corner !== this.corner) | ||
| this.window.emit( | ||
| IPC_EVENTS.CHAT_TO_ISLAND, | ||
| 'chat-island-corner', | ||
| this.corner, | ||
| ) | ||
| const window = this.window.getWindow() | ||
| if (!window) return | ||
| if (size.w < 8 || size.h < 8) { | ||
| window.hide() | ||
| return | ||
| } | ||
| const { x, y, width, height } = this.workArea() | ||
| const slack = (size.side ?? SIDE) - SIDE | ||
| const w = Math.min(Math.ceil(size.w), width + 2 * slack) | ||
| const h = Math.min(Math.ceil(size.h), height) | ||
| const left = this.corner.endsWith('left') | ||
| ? x + this.anchor.x - slack | ||
| : x + width - this.anchor.x + slack - w | ||
| const top = this.corner.startsWith('top') | ||
| ? y + this.anchor.y | ||
| : y + height - this.anchor.y - h | ||
| const was = window.getBounds() | ||
| window.setBounds({ | ||
| x: Math.min(Math.max(x - slack, left), x + width + slack - w), | ||
| y: Math.min(Math.max(y, top), y + height - h), | ||
| width: w, | ||
| height: h, | ||
| }) | ||
| // Same size: the page is already drawn for the new corner, only the window moved. | ||
| if (this.flip) this.reveal(was.width === w && was.height === h ? 0 : 32) | ||
| if (this.wantFocus) { | ||
| this.wantFocus = false | ||
| window.show() | ||
| window.focus() | ||
| } else if (!window.isVisible()) window.showInactive() | ||
| } catch (e) { | ||
| Log.warning('error during resizing ChatIslandWindow:', e) | ||
| } | ||
| } | ||
|
|
||
| // The display the dock was dropped on, while it is still connected; not the one most of the transparent window is on. | ||
| private workArea() { | ||
| const d = screen.getAllDisplays().find((d) => d.id === this.displayId) | ||
| return (d ?? screen.getPrimaryDisplay()).workArea | ||
| } | ||
|
|
||
| startDrag() { | ||
| const b = this.window.getWindow()?.getBounds() | ||
| if (!b) return | ||
| this.clickThrough(false) | ||
| this.drag = { w: b.width, h: b.height } | ||
| } | ||
|
|
||
| dragTo(x: number, y: number) { | ||
| const window = this.window.getWindow() | ||
| if (!window || !this.drag || (x === this.drag.x && y === this.drag.y)) | ||
| return | ||
| this.drag.x = x | ||
| this.drag.y = y | ||
| window.setBounds({ x, y, width: this.drag.w, height: this.drag.h }, false) | ||
| } | ||
|
|
||
| stopDrag(moved: boolean) { | ||
| if (!this.drag) return | ||
| this.drag = undefined | ||
| if (moved) this.keepPosition() | ||
| else this.resize(this.size) | ||
| } | ||
|
|
||
| // Transparent parts let the mouse through; hover is still reported, so the page can take it back. | ||
| clickThrough(through: boolean) { | ||
| if (through === this.through || (through && this.drag)) return | ||
| this.through = through | ||
| this.window.getWindow()?.setIgnoreMouseEvents(through, { forward: true }) | ||
| } | ||
|
|
||
| // After a drag: the corner nearest the dock is where the island grows from, as in the CTI. | ||
| keepPosition() { | ||
| const window = this.window.getWindow() | ||
| if (!window) return | ||
| const b = window.getBounds() | ||
| const side = this.size.side ?? SIDE | ||
| // The dock sits in the window's corner: its place follows from the bounds, only its size is measured. | ||
| const w = this.size.dock?.w ?? b.width - 2 * side | ||
| const h = this.size.dock?.h ?? b.height - 2 * END | ||
| const dock = { | ||
| x: this.corner.endsWith('left') ? b.x + side : b.x + b.width - side - w, | ||
| y: this.corner.startsWith('top') ? b.y + END : b.y + b.height - END - h, | ||
| w, | ||
| h, | ||
| } | ||
| this.displayId = screen.getDisplayNearestPoint({ | ||
| x: Math.round(dock.x + w / 2), | ||
| y: Math.round(dock.y + h / 2), | ||
| }).id | ||
| const { x, y, width, height } = this.workArea() | ||
| const left = dock.x + dock.w / 2 < x + width / 2 | ||
| const top = dock.y + dock.h / 2 < y + height / 2 | ||
| const corner: Corner = `${top ? 'top' : 'bottom'}-${left ? 'left' : 'right'}` | ||
| this.anchor = { | ||
| x: Math.max( | ||
| 0, | ||
| left ? dock.x - SIDE - x : x + width - dock.x - dock.w - SIDE, | ||
| ), | ||
| y: Math.max( | ||
| 0, | ||
| top ? dock.y - END - y : y + height - dock.y - dock.h - END, | ||
| ), | ||
| } | ||
| // The dock alone in a symmetric window (as large on both sides) stays put when the corner changes. | ||
| const alone = | ||
| side > SIDE && b.width - 2 * side - w < 2 && b.height - 2 * END - h < 2 | ||
| if (corner !== this.corner && alone) { | ||
| this.corner = corner | ||
| this.window.emit(IPC_EVENTS.CHAT_TO_ISLAND, 'chat-island-corner', corner) | ||
| } else if (corner !== this.corner) { | ||
| this.corner = corner | ||
| window.setOpacity(0) | ||
| // Placed by the island's next size, which carries the new corner; shown anyway if it never comes. | ||
| this.reveal(500, true) | ||
| this.window.emit(IPC_EVENTS.CHAT_TO_ISLAND, 'chat-island-corner', corner) | ||
| return | ||
| } | ||
| // Back inside the dock's screen, grown from its corner. | ||
| this.resize(this.size) | ||
| } | ||
|
|
||
| private reveal(ms: number, place = false) { | ||
| clearTimeout(this.flip) | ||
| const show = () => { | ||
| this.flip = undefined | ||
| if (place) this.resize(this.size) | ||
| this.window.getWindow()?.setOpacity(1) | ||
| } | ||
| if (ms) this.flip = setTimeout(show, ms) | ||
| else show() | ||
| } | ||
|
|
||
| // Focus once the island has sized the window for what it opens. | ||
| focus() { | ||
| const window = this.window.getWindow() | ||
| if (window?.isVisible()) window.focus() | ||
| // Only for the resize this open causes: a later one (a message growing the dock) must not steal focus. | ||
| this.wantFocus = true | ||
| clearTimeout(this.focusTimer) | ||
| this.focusTimer = setTimeout(() => (this.wantFocus = false), 1000) | ||
| } | ||
|
|
||
| async safeQuit() { | ||
| ChatIslandController.instance = undefined | ||
| await this.window.quit(true) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import { PAGES } from '@shared/types' | ||
| import { BaseWindow } from './BaseWindow' | ||
|
|
||
| // Transparent, always on top: the chat stays in view with NethLink closed. | ||
| export class ChatIslandWindow extends BaseWindow { | ||
| constructor() { | ||
| super(PAGES.CHATISLAND, { | ||
| width: 440, | ||
| height: 600, | ||
| show: false, | ||
| focusable: true, | ||
| acceptFirstMouse: true, | ||
| fullscreenable: false, | ||
| autoHideMenuBar: true, | ||
| closable: false, | ||
| alwaysOnTop: true, | ||
| minimizable: false, | ||
| maximizable: false, | ||
| movable: false, | ||
| resizable: false, | ||
| skipTaskbar: true, | ||
| roundedCorners: false, | ||
| parent: undefined, | ||
| transparent: true, | ||
| hiddenInMissionControl: true, | ||
| hasShadow: false, | ||
| fullscreen: false, | ||
| enableLargerThanScreen: false, | ||
| frame: false, | ||
| thickFrame: false, | ||
| trafficLightPosition: { x: 0, y: 0 }, | ||
| webPreferences: { | ||
| nodeIntegration: true, | ||
| backgroundThrottling: false, | ||
| }, | ||
| }) | ||
|
|
||
| this.addOnBuildListener(() => { | ||
| this.getWindow()?.setAlwaysOnTop(true, 'screen-saver') | ||
| }) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.