Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f41e9ac
feat: NethVoice chat
edospadoni Sep 30, 2026
a809cf3
chat: drag the chat window from the dock and the header
edospadoni Sep 30, 2026
b4c9acf
chat: keep the chat window on screen while dragging; island 0.1.4
edospadoni Sep 30, 2026
c783210
chat: retry the chat check, resend its state when NethLink reloads
edospadoni Sep 30, 2026
1ef0a90
command bar: chat with a colleague
edospadoni Sep 30, 2026
a224eaa
chat: richer desktop notification; island 0.1.5
edospadoni Sep 30, 2026
9835b90
search: chat with a colleague from the results
edospadoni Sep 30, 2026
557bcbf
command bar: only Call up top, chat stays on colleague rows
edospadoni Sep 30, 2026
9de38a8
chat: delete or leave from the list, translations, group names; islan…
edospadoni Oct 1, 2026
0cd3020
chat: Messages list like the last calls, one chat icon, chat after la…
edospadoni Oct 1, 2026
9c0552b
chat: island 0.1.8 (replies)
edospadoni Oct 1, 2026
aed6470
chat: shared contacts mapping and short dates; island 0.1.9
edospadoni Oct 1, 2026
c82c323
chat: operator instead of colleague; island 0.1.10
edospadoni Oct 1, 2026
d0106c0
chat: operators no longer active labelled; island 0.1.11
edospadoni Oct 1, 2026
763934e
chat: island 0.1.12
edospadoni Oct 1, 2026
520764c
chat: island 0.1.13 (chat.internal)
edospadoni Oct 1, 2026
a691b9f
chat: island 0.1.14
edospadoni Oct 1, 2026
904c3d2
chat: island 0.1.15; shadows end inside the window
edospadoni Oct 1, 2026
1119bec
chat: island 0.1.16; the island's rings survive the shorter shadow
edospadoni Oct 2, 2026
0ab037a
chat: island 0.1.17 (delivered and read ticks)
edospadoni Oct 2, 2026
30ed500
chat: island 0.1.18; the expanded chat is not capped by the window
edospadoni Oct 5, 2026
2861b19
chat: island 0.1.19
edospadoni Oct 5, 2026
a54c07f
chat: island 0.1.20
edospadoni Oct 5, 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
463 changes: 463 additions & 0 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 @@ -46,6 +46,7 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^2.2.8",
"@hookform/resolvers": "^3.3.4",
"@nethesis/chat-island": "^0.1.20",
"@nethesis/nethesis-brands-svg-icons": "github:nethesis/Font-Awesome#ns-brands",
"@nethesis/nethesis-light-svg-icons": "github:nethesis/Font-Awesome#ns-light",
"@nethesis/nethesis-solid-svg-icons": "github:nethesis/Font-Awesome#ns-solid",
Expand Down
16 changes: 15 additions & 1 deletion public/locales/en/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,19 @@
"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",
"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 @@ -988,7 +1001,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
16 changes: 15 additions & 1 deletion public/locales/it/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,19 @@
"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",
"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 @@ -988,7 +1001,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
75 changes: 75 additions & 0 deletions src/main/classes/controllers/ChatIslandController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { ChatIslandWindow } from '../windows'
import { Log } from '@shared/utils/logger'
import { screen } from 'electron'

export class ChatIslandController {
static instance: ChatIslandController | undefined
window: ChatIslandWindow
private wantFocus = false
// Distance from the work area's bottom-right corner, kept after a drag.
private anchor = { right: 0, bottom: 0 }

constructor() {
ChatIslandController.instance = this
this.window = new ChatIslandWindow()
}

// Sized to the island, grown from its bottom-right corner; hidden when there is nothing to show.
resize(size: { w: number; h: number }) {
try {
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)
window.setBounds({
x: Math.max(x, x + width - this.anchor.right - w),
y: Math.max(y, y + height - this.anchor.bottom - 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()
return window?.isVisible()
? screen.getDisplayMatching(window.getBounds()).workArea
: screen.getPrimaryDisplay().workArea
}

// After a drag: the new corner is where the island grows from.
keepPosition() {
const window = this.window.getWindow()
if (!window) return
const b = window.getBounds()
const { x, y, width, height } = this.workArea()
this.anchor = {
right: Math.max(0, x + width - b.x - b.width),
bottom: Math.max(0, y + height - b.y - b.height),
}
}

// Focus once the island has sized the window for what it opens.
focus() {
const window = this.window.getWindow()
if (window?.isVisible()) window.focus()
this.wantFocus = true
}

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'
59 changes: 59 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, DevToolsController } 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 @@ -240,6 +241,9 @@ export function registerIpcEvents() {
const cursorPosition = screen.getCursorScreenPoint()
const deltaX = cursorPosition.x - draggingWindow.startMousePosition.x
const deltaY = cursorPosition.y - draggingWindow.startMousePosition.y
if (window.title === PAGES.CHATISLAND) {
ChatIslandController.instance?.keepPosition()
}
if (window.title === PAGES.PHONEISLAND) {
const bounds = window.getBounds()
Log.info(
Expand Down Expand Up @@ -279,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 @@ -349,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
13 changes: 13 additions & 0 deletions src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
import { AccountController } from './classes/controllers'
import { PhoneIslandController } from './classes/controllers/PhoneIslandController'
import { CommandBarController } from './classes/controllers/CommandBarController'
import { ChatIslandController } from './classes/controllers/ChatIslandController'
import {
Account,
AuthAppData,
Expand Down Expand Up @@ -135,6 +136,7 @@ function startup() {
}

await PhoneIslandController.instance.logout()
await ChatIslandController.instance?.safeQuit()
NethLinkController.instance.logout()
AccountController.instance.logout()
await delay(1000)
Expand Down Expand Up @@ -759,6 +761,16 @@ async function attachProtocolListeners() {
}
break
}
case 'chat': {
// The chat opens in its own window, not in NethLink.
ipcMain.emit(
IPC_EVENTS.CHAT_TO_ISLAND,
undefined,
to ? 'chat-island-open' : 'chat-island-new',
to ? { username: to } : undefined,
)
return new Promise((resolve) => resolve)
}
default:
Log.warning('Unknown nethlink action:', action)
}
Expand Down Expand Up @@ -960,6 +972,7 @@ async function createNethLink(show: boolean = true) {
if (show) NethLinkController.instance.show()
await delay(1000)
new PhoneIslandController()
new ChatIslandController()
checkForUpdate()
const account = store.get('account') as Account
if (account) {
Expand Down
16 changes: 15 additions & 1 deletion src/renderer/public/locales/en/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,19 @@
"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",
"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 @@ -989,7 +1002,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
Loading
Loading