2 Commits

Author SHA1 Message Date
Myx
8b6578da3c fix: Notification audio
All checks were successful
Queue Release Build / prepare (push) Successful in 16s
Deploy Web Apps / deploy (push) Successful in 11m55s
Queue Release Build / build-linux (push) Successful in 30m56s
Queue Release Build / build-windows (push) Successful in 27m50s
Queue Release Build / finalize (push) Successful in 2m0s
2026-03-30 21:14:26 +02:00
Myx
851d6ae759 fix: Prefer cached channels before loaded 2026-03-30 20:37:24 +02:00
8 changed files with 61 additions and 11 deletions

View File

@@ -332,12 +332,15 @@ export function setupSystemHandlers(): void {
const title = typeof payload?.title === 'string' ? payload.title.trim() : '';
const body = typeof payload?.body === 'string' ? payload.body : '';
const mainWindow = getMainWindow();
const suppressSystemNotification = mainWindow?.isVisible() === true
&& !mainWindow.isMinimized()
&& mainWindow.isMaximized();
if (!title) {
return false;
}
if (Notification.isSupported()) {
if (!suppressSystemNotification && Notification.isSupported()) {
try {
const notification = new Notification({
title,

View File

@@ -203,6 +203,7 @@ Additional runtime guards:
The Electron main process handles the actual desktop notification and window-attention behavior.
- `show-desktop-notification` creates a system `Notification` with the window icon when supported.
- `show-desktop-notification` skips the OS toast while the main window is visible and maximized.
- Notification clicks restore, show, and focus the main window.
- `request-window-attention` flashes the taskbar entry directly when Electron is minimized or otherwise backgrounded and actionable unread exists.
- `show-desktop-notification` can still request attention for live toast delivery.
@@ -210,7 +211,7 @@ The Electron main process handles the actual desktop notification and window-att
### Platform-specific policy
- Windows: the facade also plays `AppSound.Notification` before showing the desktop notification.
- Windows: the facade only plays `AppSound.Notification` when the app is not the active selected window.
- Linux: desktop alerts are expected to surface through the system notification center, with window attention requested when the app is backgrounded.
- macOS and browser-only builds use the same desktop notification adapter, but there is no extra renderer-side sound policy in this domain.

View File

@@ -206,7 +206,7 @@ export class NotificationsFacade {
!context.isWindowFocused || !context.isDocumentVisible
);
if (this.platformKind === 'windows') {
if (this.shouldPlayNotificationSound()) {
this.audio.play(AppSound.Notification);
}
@@ -349,6 +349,14 @@ export class NotificationsFacade {
};
}
private shouldPlayNotificationSound(): boolean {
return this.platformKind === 'windows' && !this.isWindowActive();
}
private isWindowActive(): boolean {
return this._windowFocused() && this._documentVisible() && !this._windowMinimized();
}
private ensureRoomTracking(roomId: string, channelId: string, baselineTimestamp: number): void {
const settings = this._settings();

View File

@@ -11,8 +11,8 @@
<div class="min-w-0 flex-1">
<h4 class="text-base font-semibold text-foreground">Delivery</h4>
<p class="mt-1 text-sm text-muted-foreground">
Desktop alerts use the system notification center on Linux and request taskbar attention when the app is not focused. Windows also plays the
configured notification sound.
Desktop alerts use the system notification center on Linux and request taskbar attention when the app is not focused. Maximized app window
suppress system popups, and only play the configured notification sound while the app is in the background.
</p>
</div>
</div>

View File

@@ -139,7 +139,12 @@ export class InviteComponent implements OnInit {
RoomsActions.joinRoom({
roomId: joinResponse.server.id,
serverInfo: {
...invite.server,
...joinResponse.server,
channels:
Array.isArray(joinResponse.server.channels) && joinResponse.server.channels.length > 0
? joinResponse.server.channels
: invite.server.channels,
sourceId: context.endpoint.id,
sourceName: context.endpoint.name,
sourceUrl: context.sourceUrl

View File

@@ -241,6 +241,7 @@ export class ServerSearchComponent implements OnInit {
maxUsers: room.maxUsers ?? 50,
hasPassword: typeof room.hasPassword === 'boolean' ? room.hasPassword : !!room.password,
isPrivate: room.isPrivate,
channels: room.channels,
createdAt: room.createdAt,
ownerId: room.hostId,
sourceId: room.sourceId,
@@ -272,7 +273,14 @@ export class ServerSearchComponent implements OnInit {
sourceId: server.sourceId,
sourceUrl: server.sourceUrl
}));
const resolvedServer = response.server ?? server;
const resolvedServer = {
...server,
...response.server,
channels:
Array.isArray(response.server.channels) && response.server.channels.length > 0
? response.server.channels
: server.channels
};
this.closePasswordDialog();
this.store.dispatch(

View File

@@ -311,7 +311,11 @@ export class ServersRailComponent {
roomId: room.id,
serverInfo: {
...this.toServerInfo(room),
...response.server
...response.server,
channels:
Array.isArray(response.server.channels) && response.server.channels.length > 0
? response.server.channels
: room.channels
}
})
);
@@ -370,6 +374,7 @@ export class ServersRailComponent {
isPrivate: room.isPrivate,
createdAt: room.createdAt,
ownerId: room.hostId,
channels: room.channels,
sourceId: room.sourceId,
sourceName: room.sourceName,
sourceUrl: room.sourceUrl

View File

@@ -117,6 +117,26 @@ function resolveUserDisplayName(user: Pick<User, 'displayName' | 'username'> | n
return user?.username?.trim() || 'User';
}
function hasPersistedChannels(channels: Room['channels'] | undefined): channels is NonNullable<Room['channels']> {
return Array.isArray(channels) && channels.length > 0;
}
/** Keep cached channels until directory metadata provides a concrete replacement. */
function resolveRoomChannels(
cachedChannels: Room['channels'] | undefined,
incomingChannels: Room['channels'] | undefined
): Room['channels'] | undefined {
if (hasPersistedChannels(incomingChannels)) {
return incomingChannels;
}
if (hasPersistedChannels(cachedChannels)) {
return cachedChannels;
}
return undefined;
}
interface RoomPresenceSignalingMessage {
type: string;
reason?: string;
@@ -302,7 +322,7 @@ export class RoomsEffects {
const resolvedRoom: Room = {
...room,
isPrivate: typeof serverInfo?.isPrivate === 'boolean' ? serverInfo.isPrivate : room.isPrivate,
channels: Array.isArray(serverInfo?.channels) ? serverInfo.channels : room.channels,
channels: resolveRoomChannels(room.channels, serverInfo?.channels),
sourceId: serverInfo?.sourceId ?? room.sourceId,
sourceName: serverInfo?.sourceName ?? room.sourceName,
sourceUrl: serverInfo?.sourceUrl ?? room.sourceUrl,
@@ -336,7 +356,7 @@ export class RoomsEffects {
createdAt: Date.now(),
userCount: 1,
maxUsers: 50,
channels: Array.isArray(serverInfo.channels) ? serverInfo.channels : undefined,
channels: resolveRoomChannels(undefined, serverInfo.channels),
sourceId: serverInfo.sourceId,
sourceName: serverInfo.sourceName,
sourceUrl: serverInfo.sourceUrl
@@ -361,7 +381,7 @@ export class RoomsEffects {
createdAt: serverData.createdAt || Date.now(),
userCount: serverData.userCount,
maxUsers: serverData.maxUsers,
channels: Array.isArray(serverData.channels) ? serverData.channels : undefined,
channels: resolveRoomChannels(undefined, serverData.channels),
sourceId: serverData.sourceId,
sourceName: serverData.sourceName,
sourceUrl: serverData.sourceUrl
@@ -428,7 +448,7 @@ export class RoomsEffects {
hasPassword: !!serverData.hasPassword,
isPrivate: serverData.isPrivate,
maxUsers: serverData.maxUsers,
channels: Array.isArray(serverData.channels) ? serverData.channels : room.channels,
channels: resolveRoomChannels(room.channels, serverData.channels),
sourceId: serverData.sourceId ?? room.sourceId,
sourceName: serverData.sourceName ?? room.sourceName,
sourceUrl: serverData.sourceUrl ?? room.sourceUrl