Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
af76223
feat: NethVoice chat
edospadoni Sep 30, 2026
bd5604e
chat: drag the chat window from the dock and the header
edospadoni Sep 30, 2026
3f5cb09
chat: keep the chat window on screen while dragging; island 0.1.4
edospadoni Sep 30, 2026
1d7a001
chat: retry the chat check, resend its state when NethLink reloads
edospadoni Sep 30, 2026
31fbb72
command bar: chat with a colleague
edospadoni Sep 30, 2026
26fb808
chat: richer desktop notification; island 0.1.5
edospadoni Sep 30, 2026
df24523
search: chat with a colleague from the results
edospadoni Sep 30, 2026
7204c70
command bar: only Call up top, chat stays on colleague rows
edospadoni Sep 30, 2026
84093fd
chat: delete or leave from the list, translations, group names; islan…
edospadoni Oct 1, 2026
f0ac616
chat: Messages list like the last calls, one chat icon, chat after la…
edospadoni Oct 1, 2026
5c6a6bf
chat: island 0.1.8 (replies)
edospadoni Oct 1, 2026
8640672
chat: shared contacts mapping and short dates; island 0.1.9
edospadoni Oct 1, 2026
b0299aa
chat: operator instead of colleague; island 0.1.10
edospadoni Oct 1, 2026
9e3bec6
chat: operators no longer active labelled; island 0.1.11
edospadoni Oct 1, 2026
435bc3f
chat: island 0.1.12
edospadoni Oct 1, 2026
134d04d
chat: island 0.1.13 (chat.internal)
edospadoni Oct 1, 2026
6f68dd1
chat: island 0.1.14
edospadoni Oct 1, 2026
dee1fd2
chat: island 0.1.15; shadows end inside the window
edospadoni Oct 1, 2026
b95838c
chat: island 0.1.16; the island's rings survive the shorter shadow
edospadoni Oct 2, 2026
fd7a299
chat: island 0.1.17 (delivered and read ticks)
edospadoni Oct 2, 2026
510a0c6
chat: island 0.1.18; the expanded chat is not capped by the window
edospadoni Oct 5, 2026
1fc0094
chat: island 0.1.19
edospadoni Oct 5, 2026
2f82411
chat: island 0.1.20
edospadoni Oct 5, 2026
f96d815
chat: review fixes, island 0.1.23
edospadoni Oct 6, 2026
b82f7a4
chat: island 0.1.26
edospadoni Oct 7, 2026
4c743a8
chat: island 0.1.27
edospadoni Oct 7, 2026
48f58c8
chat: island 0.1.28, native tooltips on the heads (the window is size…
edospadoni Oct 7, 2026
432f413
chat: island 0.1.28 (with nativeTooltips); drop a tsc build file
edospadoni Oct 7, 2026
390c2fe
chat: island 0.1.29
edospadoni Oct 7, 2026
7ac1c4a
chat: island 0.1.30; the list shows a group's size and "No messages y…
edospadoni Oct 7, 2026
223d24e
chat: island 0.1.31; group looks as in the CTI
edospadoni Oct 8, 2026
e490054
chat: island 0.1.32
edospadoni Oct 9, 2026
0f1b766
chat: island 0.1.33
edospadoni Oct 14, 2026
56a0f59
chat: message icon in the menu, as in the CTI
edospadoni Oct 14, 2026
ec5a843
chat: island 0.1.34; the same message icons as the CTI
edospadoni Oct 14, 2026
cc9a856
chat: island 0.1.35
edospadoni Oct 14, 2026
61bf461
chat: island 0.1.36
edospadoni Oct 14, 2026
9b19db3
chat: island 0.1.37; the window grows from the corner nearest the dock
edospadoni Oct 15, 2026
4f7bbff
chat: island 0.1.38
edospadoni Oct 15, 2026
8d5f7fd
chat: island 0.1.39
edospadoni Oct 15, 2026
efb2525
chat-island 0.1.40
edospadoni Oct 15, 2026
bcfdeef
chat: blue accent, registered devices for the island, chat-island 0.1.41
edospadoni Oct 15, 2026
d100e8a
chat-island 0.1.42
edospadoni Oct 15, 2026
b62ad4b
chat-island 0.1.43
edospadoni Oct 15, 2026
a95a5c9
chat: island in NethLink's language, chat-island 0.1.44
edospadoni Oct 15, 2026
183ac96
lockfile on main
edospadoni Oct 15, 2026
6735458
chat: the dock's tooltips in the page, the window widens to show them…
edospadoni Oct 16, 2026
74e3af4
chat: chat-island 0.1.45
edospadoni Oct 16, 2026
45bb4b6
chat: chat-island 0.1.46
edospadoni Oct 16, 2026
b7c9c93
chat: chat-island 0.1.47
edospadoni Oct 16, 2026
f26b458
chat: chat-island 0.1.48
edospadoni Oct 19, 2026
1422f96
chat: chat-island 0.1.49
edospadoni Oct 19, 2026
900a788
chat-island 0.1.50
edospadoni Oct 20, 2026
d3a3bec
chat: no jitter on hover and no jump on drop for the chat window
edospadoni Oct 20, 2026
31f683d
chat-island 0.1.51
edospadoni Oct 20, 2026
dbf58e2
chore(deps): phone-island 1.0.16
edospadoni Oct 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,712 changes: 807 additions & 905 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^2.2.8",
"@hookform/resolvers": "^3.3.4",
"@nethesis/chat-island": "^0.1.51",
"@nethesis/nethesis-solid-svg-icons": "github:nethesis/Font-Awesome#ns-solid",
"@nethesis/phone-island": "^1.0.15",
"@nethesis/phone-island": "^1.0.16",
"@tailwindcss/forms": "^0.5.7",
"@types/lodash": "^4.14.202",
"@types/node": "^18.19.9",
Expand Down
18 changes: 17 additions & 1 deletion public/locales/en/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,21 @@
"Z-A label": "Z-A",
"EXT label": "Extension"
},
"Chat": {
"No longer active": "No longer active",
"Messages": "Messages",
"Chat": "Chat",
"New chat": "New chat",
"No conversations": "No conversations",
"No messages yet": "No messages yet",
"members": "members",
"You": "You",
"Delete": "Delete",
"Leave": "Leave",
"Delete confirm": "Delete the conversation with \"{{name}}\"? Your copy of the messages is removed.",
"Delete group confirm": "Delete the group \"{{name}}\" for everyone? Its messages are removed for all members.",
"Leave confirm": "Leave the group \"{{name}}\"? You stop receiving its messages and your copy is removed."
},
"LastCalls": {
"Last calls": "Last calls",
"No calls": "No calls",
Expand Down Expand Up @@ -992,7 +1007,8 @@
},
"CommandBar": {
"Placeholder": "Search contact or dial number...",
"Call": "Call"
"Call": "Call",
"Chat hint": "Chat (Ctrl/⌘+Enter)"
},
"Errors": {
"browser_permissions": "Browser permissions error",
Expand Down
18 changes: 17 additions & 1 deletion public/locales/it/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,21 @@
"Z-A label": "Z-A",
"EXT label": "Interno"
},
"Chat": {
"No longer active": "Non più attivo",
"Messages": "Messaggi",
"Chat": "Chat",
"New chat": "Nuova chat",
"No conversations": "Nessuna conversazione",
"No messages yet": "Nessun messaggio",
"members": "membri",
"You": "Tu",
"Delete": "Elimina",
"Leave": "Abbandona",
"Delete confirm": "Eliminare la conversazione con \"{{name}}\"? La tua copia dei messaggi viene rimossa.",
"Delete group confirm": "Eliminare il gruppo \"{{name}}\" per tutti? I suoi messaggi vengono rimossi per tutti i membri.",
"Leave confirm": "Abbandonare il gruppo \"{{name}}\"? Non riceverai più i suoi messaggi e la tua copia viene rimossa."
},
"LastCalls": {
"Last calls": "Ultime chiamate",
"No calls": "Nessuna chiamata",
Expand Down Expand Up @@ -992,7 +1007,8 @@
},
"CommandBar": {
"Placeholder": "Cerca contatto o componi numero...",
"Call": "Chiama"
"Call": "Chiama",
"Chat hint": "Chat (Ctrl/⌘+Invio)"
},
"Errors": {
"browser_permissions": "Errore nei permessi del browser",
Expand Down
6 changes: 6 additions & 0 deletions src/main/classes/controllers/AppController.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { LoginController } from './LoginController'
import { NethLinkController } from './NethLinkController'
import { PhoneIslandController } from './PhoneIslandController'
import { ChatIslandController } from './ChatIslandController'
import { TrayController } from './TrayController'
import { Log } from '@shared/utils/logger'
import { DevToolsController } from './DevToolsController'
Expand Down Expand Up @@ -32,6 +33,11 @@ export class AppController {
Log.warning('unable to correctly close the PhoneIslandController:', e)
}
}
try {
await ChatIslandController.instance?.safeQuit()
} catch (e) {
Log.warning('unable to correctly close the ChatIslandController:', e)
}
if (NethLinkController.instance) {
try {
await NethLinkController.instance.safeQuit()
Expand Down
201 changes: 201 additions & 0 deletions src/main/classes/controllers/ChatIslandController.ts
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
Comment thread
edospadoni marked this conversation as resolved.
clearTimeout(this.focusTimer)
this.focusTimer = setTimeout(() => (this.wantFocus = false), 1000)
}

async safeQuit() {
ChatIslandController.instance = undefined
await this.window.quit(true)
}
}
1 change: 1 addition & 0 deletions src/main/classes/controllers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './PhoneIslandController'
export * from './TrayController'
export * from './DevToolsController'
export * from './CommandBarController'
export * from './ChatIslandController'
42 changes: 42 additions & 0 deletions src/main/classes/windows/ChatIslandWindow.ts
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')
})
}
}
1 change: 1 addition & 0 deletions src/main/classes/windows/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './NethLinkWindow'
export * from './PhoneIslandWindow'
export * from './DevToolsWindow'
export * from './CommandBarWindow'
export * from './ChatIslandWindow'
Loading
Loading