Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
a32ad2f
feat: NethVoice chat
edospadoni Sep 30, 2026
f718402
chat: drag the chat window from the dock and the header
edospadoni Sep 30, 2026
65e0dd7
chat: keep the chat window on screen while dragging; island 0.1.4
edospadoni Sep 30, 2026
bf819cb
chat: retry the chat check, resend its state when NethLink reloads
edospadoni Sep 30, 2026
a46ffec
command bar: chat with a colleague
edospadoni Sep 30, 2026
2efbbb2
chat: richer desktop notification; island 0.1.5
edospadoni Sep 30, 2026
2e1ba8b
search: chat with a colleague from the results
edospadoni Sep 30, 2026
3bdd3c7
command bar: only Call up top, chat stays on colleague rows
edospadoni Sep 30, 2026
1eb498c
chat: delete or leave from the list, translations, group names; islan…
edospadoni Oct 1, 2026
e05567b
chat: Messages list like the last calls, one chat icon, chat after la…
edospadoni Oct 1, 2026
a28cf6a
chat: island 0.1.8 (replies)
edospadoni Oct 1, 2026
10d2f19
chat: shared contacts mapping and short dates; island 0.1.9
edospadoni Oct 1, 2026
1457344
chat: operator instead of colleague; island 0.1.10
edospadoni Oct 1, 2026
7074d1f
chat: operators no longer active labelled; island 0.1.11
edospadoni Oct 1, 2026
615ecd1
chat: island 0.1.12
edospadoni Oct 1, 2026
55ffbe8
chat: island 0.1.13 (chat.internal)
edospadoni Oct 1, 2026
962e891
chat: island 0.1.14
edospadoni Oct 1, 2026
2238357
chat: island 0.1.15; shadows end inside the window
edospadoni Oct 1, 2026
1d48332
chat: island 0.1.16; the island's rings survive the shorter shadow
edospadoni Oct 2, 2026
03ae1fa
chat: island 0.1.17 (delivered and read ticks)
edospadoni Oct 2, 2026
0892b4a
chat: island 0.1.18; the expanded chat is not capped by the window
edospadoni Oct 5, 2026
16a625f
chat: island 0.1.19
edospadoni Oct 5, 2026
1c161b5
chat: island 0.1.20
edospadoni Oct 5, 2026
646540d
chat: review fixes, island 0.1.23
edospadoni Oct 6, 2026
960cd75
chat: island 0.1.26
edospadoni Oct 7, 2026
8d26aad
chat: island 0.1.27
edospadoni Oct 7, 2026
4d1173d
chat: island 0.1.28, native tooltips on the heads (the window is size…
edospadoni Oct 7, 2026
e03d426
chat: island 0.1.28 (with nativeTooltips); drop a tsc build file
edospadoni Oct 7, 2026
06bba8e
chat: island 0.1.29
edospadoni Oct 7, 2026
9b4d8c2
chat: island 0.1.30; the list shows a group's size and "No messages y…
edospadoni Oct 7, 2026
df7db75
chat: island 0.1.31; group looks as in the CTI
edospadoni Oct 8, 2026
cb6a183
chat: island 0.1.32
edospadoni Oct 9, 2026
cad0927
chat: island 0.1.33
edospadoni Oct 14, 2026
a3552aa
chat: message icon in the menu, as in the CTI
edospadoni Oct 14, 2026
89b4e41
chat: island 0.1.34; the same message icons as the CTI
edospadoni Oct 14, 2026
36c0ea5
chat: island 0.1.35
edospadoni Oct 14, 2026
fae4e23
chat: island 0.1.36
edospadoni Oct 14, 2026
668b45f
chat: island 0.1.37; the window grows from the corner nearest the dock
edospadoni Oct 15, 2026
92a9ce9
chat: island 0.1.38
edospadoni Oct 15, 2026
edb62e7
chat: island 0.1.39
edospadoni Oct 15, 2026
5900d84
chat-island 0.1.40
edospadoni Oct 15, 2026
51792d1
chat: blue accent, registered devices for the island, chat-island 0.1.41
edospadoni Oct 15, 2026
36d99f3
chat-island 0.1.42
edospadoni Oct 15, 2026
5fb0eab
chat-island 0.1.43
edospadoni Oct 15, 2026
628c0db
chat: island in NethLink's language, chat-island 0.1.44
edospadoni Oct 15, 2026
1705200
lockfile on main
edospadoni Oct 15, 2026
c713b41
chat: the dock's tooltips in the page, the window widens to show them…
edospadoni Oct 16, 2026
b57c231
chat: chat-island 0.1.45
edospadoni Oct 16, 2026
fe2bc92
chat: chat-island 0.1.46
edospadoni Oct 16, 2026
32debaa
chat: chat-island 0.1.47
edospadoni Oct 16, 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,078 changes: 733 additions & 345 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^2.2.8",
"@hookform/resolvers": "^3.3.4",
"@nethesis/chat-island": "^0.1.47",
"@nethesis/nethesis-solid-svg-icons": "github:nethesis/Font-Awesome#ns-solid",
"@nethesis/phone-island": "^1.0.15",
"@tailwindcss/forms": "^0.5.7",
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
133 changes: 133 additions & 0 deletions src/main/classes/controllers/ChatIslandController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
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 }

// Between the window's edge and the dock: the island's right-4/left-4 and bottom-8/top-8.
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: { w: number; h: number; dock?: Rect; corner?: Corner } = {
w: 0,
h: 0,
}

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: { w: number; h: number; dock?: Rect; corner?: Corner }) {
try {
this.size = size
// 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 w = Math.min(Math.ceil(size.w), width)
const h = Math.min(Math.ceil(size.h), height)
const left = this.corner.endsWith('left')
? x + this.anchor.x
: x + width - this.anchor.x - w
const top = this.corner.startsWith('top')
? y + this.anchor.y
: y + height - this.anchor.y - h
window.setBounds({
x: Math.min(Math.max(x, left), x + width - w),
y: Math.min(Math.max(y, top), y + height - h),
width: w,
height: h,
})
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)
}
}

private workArea() {
const window = this.window.getWindow()
if (window?.isVisible())
return screen.getDisplayMatching(window.getBounds()).workArea
// Hidden: back on the display it was dragged to, while it is still connected.
const d = screen.getAllDisplays().find((d) => d.id === this.displayId)
return (d ?? screen.getPrimaryDisplay()).workArea
}

// 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()
this.displayId = screen.getDisplayMatching(b).id
const { x, y, width, height } = this.workArea()
// 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,
}
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,
),
}
if (corner !== this.corner) {
this.corner = corner
this.window.emit(IPC_EVENTS.CHAT_TO_ISLAND, 'chat-island-corner', corner)
this.resize(this.size)
}
}

// 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'
63 changes: 63 additions & 0 deletions src/main/lib/ipcEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AccountController } from '@/classes/controllers'
import { LoginController } from '@/classes/controllers/LoginController'
import { PhoneIslandController } from '@/classes/controllers/PhoneIslandController'
import { CommandBarController } from '@/classes/controllers/CommandBarController'
import { ChatIslandController } from '@/classes/controllers/ChatIslandController'
import { IPC_EVENTS } from '@shared/constants'
import { Account, OnDraggingWindow, PAGES } from '@shared/types'
import {
Expand Down Expand Up @@ -236,6 +237,13 @@ export function registerIpcEvents() {
const cursorPosition = screen.getCursorScreenPoint()
const deltaX = cursorPosition.x - draggingWindow.startMousePosition.x
const deltaY = cursorPosition.y - draggingWindow.startMousePosition.y
// A click on a head is not a move: the window stays where it grew from.
if (
window.title === PAGES.CHATISLAND &&
(Math.abs(deltaX) > 3 || Math.abs(deltaY) > 3)
) {
ChatIslandController.instance?.keepPosition()
}
if (window.title === PAGES.PHONEISLAND) {
const bounds = window.getBounds()
Log.info(
Expand Down Expand Up @@ -275,6 +283,19 @@ export function registerIpcEvents() {
},
false,
)
} else if (window.title === PAGES.CHATISLAND) {
// Kept inside the screen under the cursor, like the phone island.
const { width, height } = window.getBounds()
const wa = screen.getDisplayNearestPoint(cursorPosition).workArea
window.setBounds(
{
x: Math.min(Math.max(newX, wa.x), wa.x + wa.width - width),
y: Math.min(Math.max(newY, wa.y), wa.y + wa.height - height),
width,
height,
},
false,
)
} else {
const [w, h] = window.getContentSize()
window.setBounds(
Expand Down Expand Up @@ -345,6 +366,48 @@ export function registerIpcEvents() {
clipboard.writeText(text)
})

// Chat island window: size, and island events between it and NethLink.
ipcMain.on(IPC_EVENTS.CHAT_ISLAND_RESIZE, (_, size) => {
ChatIslandController.instance?.resize(size)
})
ipcMain.on(IPC_EVENTS.CHAT_TO_ISLAND, (_, name, detail) => {
try {
ChatIslandController.instance?.window.emit(
IPC_EVENTS.CHAT_TO_ISLAND,
name,
detail,
)
if (name === 'chat-island-open' || name === 'chat-island-new') {
ChatIslandController.instance?.focus()
}
} catch (e) {
Log.warning('chat island unreachable:', e)
}
})
ipcMain.on(IPC_EVENTS.CHAT_FROM_ISLAND, (_, name, detail) => {
try {
NethLinkController.instance?.window.emit(
IPC_EVENTS.CHAT_FROM_ISLAND,
name,
detail,
)
} catch (e) {
Log.warning('nethlink unreachable:', e)
}
// The command bar only needs to know whether the chat is there.
if (name === 'chat-island-status') {
try {
CommandBarController.instance?.window.emit(
IPC_EVENTS.CHAT_FROM_ISLAND,
name,
detail,
)
} catch (e) {
Log.warning('command bar unreachable:', e)
}
}
})

ipcMain.on(IPC_EVENTS.PHONE_ISLAND_RESIZE, (_, size) => {
PhoneIslandController.instance.resize(size)
})
Expand Down
Loading
Loading