From aa595c45d894dcd820600a8d3df27aab5bfabfc3 Mon Sep 17 00:00:00 2001 From: Myx Date: Thu, 19 Mar 2026 21:38:47 +0100 Subject: [PATCH 01/16] Fix autostart on linux --- electron/app/auto-start.ts | 77 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/electron/app/auto-start.ts b/electron/app/auto-start.ts index e29bcdb..6de1b3a 100644 --- a/electron/app/auto-start.ts +++ b/electron/app/auto-start.ts @@ -1,8 +1,13 @@ import { app } from 'electron'; import AutoLaunch from 'auto-launch'; +import * as fsp from 'fs/promises'; +import * as path from 'path'; import { readDesktopSettings } from '../desktop-settings'; let autoLauncher: AutoLaunch | null = null; +let autoLaunchPath = ''; + +const LINUX_AUTO_START_ARGUMENTS = ['--no-sandbox', '%U']; function resolveLaunchPath(): string { // AppImage runs from a temporary mount; APPIMAGE points to the real file path. @@ -13,15 +18,77 @@ function resolveLaunchPath(): string { return appImagePath || process.execPath; } +function escapeDesktopEntryExecArgument(argument: string): string { + const escapedArgument = argument.replace(/(["\\$`])/g, '\\$1'); + + return /[\s"]/u.test(argument) + ? `"${escapedArgument}"` + : escapedArgument; +} + +function getLinuxAutoStartDesktopEntryPath(launchPath: string): string { + return path.join(app.getPath('home'), '.config', 'autostart', `${path.basename(launchPath)}.desktop`); +} + +function buildLinuxAutoStartExecLine(launchPath: string): string { + return `Exec=${[escapeDesktopEntryExecArgument(launchPath), ...LINUX_AUTO_START_ARGUMENTS].join(' ')}`; +} + +function buildLinuxAutoStartDesktopEntry(launchPath: string): string { + const appName = path.basename(launchPath); + + return [ + '[Desktop Entry]', + 'Type=Application', + 'Version=1.0', + `Name=${appName}`, + `Comment=${appName}startup script`, + buildLinuxAutoStartExecLine(launchPath), + 'StartupNotify=false', + 'Terminal=false' + ].join('\n'); +} + +async function synchronizeLinuxAutoStartDesktopEntry(launchPath: string): Promise { + if (process.platform !== 'linux') { + return; + } + + const desktopEntryPath = getLinuxAutoStartDesktopEntryPath(launchPath); + const execLine = buildLinuxAutoStartExecLine(launchPath); + + let currentDesktopEntry = ''; + + try { + currentDesktopEntry = await fsp.readFile(desktopEntryPath, 'utf8'); + } catch { + // Create the desktop entry if auto-launch did not leave one behind. + } + + const nextDesktopEntry = currentDesktopEntry + ? /^Exec=.*$/m.test(currentDesktopEntry) + ? currentDesktopEntry.replace(/^Exec=.*$/m, execLine) + : `${currentDesktopEntry.trimEnd()}\n${execLine}\n` + : buildLinuxAutoStartDesktopEntry(launchPath); + + if (nextDesktopEntry === currentDesktopEntry) { + return; + } + + await fsp.mkdir(path.dirname(desktopEntryPath), { recursive: true }); + await fsp.writeFile(desktopEntryPath, nextDesktopEntry, 'utf8'); +} + function getAutoLauncher(): AutoLaunch | null { if (!app.isPackaged) { return null; } if (!autoLauncher) { + autoLaunchPath = resolveLaunchPath(); autoLauncher = new AutoLaunch({ name: app.getName(), - path: resolveLaunchPath() + path: autoLaunchPath }); } @@ -37,12 +104,16 @@ async function setAutoStartEnabled(enabled: boolean): Promise { const currentlyEnabled = await launcher.isEnabled(); - if (currentlyEnabled === enabled) { + if (!enabled && currentlyEnabled === enabled) { return; } if (enabled) { - await launcher.enable(); + if (!currentlyEnabled) { + await launcher.enable(); + } + + await synchronizeLinuxAutoStartDesktopEntry(autoLaunchPath); return; } From b5d676fb78f62f3182903c3b93d145ecf92ca69f Mon Sep 17 00:00:00 2001 From: Myx Date: Thu, 19 Mar 2026 21:39:20 +0100 Subject: [PATCH 02/16] New attempt to fix windows screenshare --- .../browser-screen-share.capture.ts | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/src/app/core/services/webrtc/screen-share-platforms/browser-screen-share.capture.ts b/src/app/core/services/webrtc/screen-share-platforms/browser-screen-share.capture.ts index 7722dcf..c71c860 100644 --- a/src/app/core/services/webrtc/screen-share-platforms/browser-screen-share.capture.ts +++ b/src/app/core/services/webrtc/screen-share-platforms/browser-screen-share.capture.ts @@ -1,5 +1,6 @@ import { ScreenShareQualityPreset, ScreenShareStartOptions } from '../screen-share.config'; import { WebRTCLogger } from '../webrtc-logger'; +import { ScreenShareWindow } from './shared'; export class BrowserScreenShareCapture { constructor(private readonly logger: WebRTCLogger) {} @@ -16,7 +17,11 @@ export class BrowserScreenShareCapture { throw new Error('navigator.mediaDevices.getDisplayMedia is not available.'); } - return await navigator.mediaDevices.getDisplayMedia(displayConstraints); + const stream = await navigator.mediaDevices.getDisplayMedia(displayConstraints); + + this.logAudioTrackSettings(stream); + + return stream; } private buildDisplayMediaConstraints( @@ -24,6 +29,7 @@ export class BrowserScreenShareCapture { preset: ScreenShareQualityPreset ): DisplayMediaStreamOptions { const supportedConstraints = navigator.mediaDevices?.getSupportedConstraints?.() as Record | undefined; + const isWindowsElectron = this.isWindowsElectron(); const audioConstraints: Record | false = options.includeSystemAudio ? { echoCancellation: false, @@ -37,7 +43,10 @@ export class BrowserScreenShareCapture { } if (audioConstraints && supportedConstraints?.['suppressLocalAudioPlayback']) { - audioConstraints['suppressLocalAudioPlayback'] = true; + // Windows Electron should keep voice playback audible to the sharer. + // Use own-audio restriction to keep the app's playback out of the + // captured stream instead of muting local playback. + audioConstraints['suppressLocalAudioPlayback'] = !isWindowsElectron; } return { @@ -53,4 +62,31 @@ export class BrowserScreenShareCapture { systemAudio: options.includeSystemAudio ? 'include' : 'exclude' } as DisplayMediaStreamOptions; } + + private logAudioTrackSettings(stream: MediaStream): void { + const audioTrack = stream.getAudioTracks()[0]; + + if (!audioTrack || typeof audioTrack.getSettings !== 'function') { + return; + } + + const settings = audioTrack.getSettings() as MediaTrackSettings & { + restrictOwnAudio?: boolean; + suppressLocalAudioPlayback?: boolean; + }; + + this.logger.info('getDisplayMedia audio track settings', { + restrictOwnAudio: settings.restrictOwnAudio ?? null, + suppressLocalAudioPlayback: settings.suppressLocalAudioPlayback ?? null + }); + } + + private isWindowsElectron(): boolean { + if (typeof window === 'undefined' || typeof navigator === 'undefined') { + return false; + } + + return !!(window as ScreenShareWindow).electronAPI + && /win/i.test(`${navigator.userAgent} ${navigator.platform}`); + } } From 429bb9d8fff4c33cac6b7c820c1f55fc81cfafb9 Mon Sep 17 00:00:00 2001 From: Myx Date: Thu, 19 Mar 2026 21:39:47 +0100 Subject: [PATCH 03/16] Chat message placeholder adjustment --- .../chat-message-composer.component.html | 5 +++-- .../chat-message-composer.component.scss | 16 ++++++++++++++-- .../chat-message-composer.component.ts | 17 ++++++++++++++++- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/app/features/chat/chat-messages/components/message-composer/chat-message-composer.component.html b/src/app/features/chat/chat-messages/components/message-composer/chat-message-composer.component.html index 718e660..e298628 100644 --- a/src/app/features/chat/chat-messages/components/message-composer/chat-message-composer.component.html +++ b/src/app/features/chat/chat-messages/components/message-composer/chat-message-composer.component.html @@ -132,7 +132,7 @@ (dragleave)="onDragLeave($event)" (drop)="onDrop($event)" > -
+
@if (klipy.isEnabled()) { + + + } @@ -261,13 +265,13 @@ In voice

} - @if (currentUser()?.screenShareState?.isSharing || (currentUser()?.id && isUserSharing(currentUser()!.id))) { + @if (currentUser() && isUserStreaming(currentUser()!.oderId || currentUser()!.id)) {
- @if (!item().isLocal) { + @if (!item().isLocal && item().hasAudio) {
@@ -128,10 +134,10 @@ [class.tracking-[0.24em]]="!compact()" > - Live + {{ streamBadgeLabel() }}

@@ -156,7 +162,7 @@ /> - @if (!item().isLocal) { + @if (!item().isLocal && item().hasAudio) { } @@ -407,6 +413,14 @@ > Resync Messages + @if (contextChannel()?.type === 'text') { + + } @if (canManageChannels()) {
} @@ -46,6 +54,14 @@ (closed)="closeMenu()" [width]="'w-44'" > + +
+ + + } diff --git a/toju-app/src/app/features/servers/servers-rail.component.ts b/toju-app/src/app/features/servers/servers-rail.component.ts index 6ee1c5d..1136471 100644 --- a/toju-app/src/app/features/servers/servers-rail.component.ts +++ b/toju-app/src/app/features/servers/servers-rail.component.ts @@ -233,6 +233,10 @@ export class ServersRailComponent { return count > 99 ? '99+' : String(count); } + isSelectedRoom(room: Room): boolean { + return this.currentRoom()?.id === room.id; + } + private async refreshBannedLookup(rooms: Room[], currentUser: User | null): Promise { const requestVersion = ++this.banLookupRequestVersion;