Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
28 changes: 28 additions & 0 deletions desktop/src/features/channels/activeChannelSuppression.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import assert from "node:assert/strict";
import test from "node:test";

import { isSuppressedAsActiveChannel } from "./activeChannelSuppression.ts";

test("suppresses the active channel while the app is focused", () => {
assert.equal(isSuppressedAsActiveChannel("ch1", "ch1", true, false), true);
});

test("does not suppress the active channel while the app is unfocused", () => {
// Regression: a DM channel left selected in a minimized/backgrounded
// window must still notify — the user isn't reading it.
assert.equal(isSuppressedAsActiveChannel("ch1", "ch1", false, false), false);
});

test("does not suppress other channels regardless of focus", () => {
assert.equal(isSuppressedAsActiveChannel("ch2", "ch1", true, false), false);
assert.equal(isSuppressedAsActiveChannel("ch2", "ch1", false, false), false);
});

test("does not suppress when no channel is active", () => {
assert.equal(isSuppressedAsActiveChannel("ch1", null, true, false), false);
});

test("notifyForActiveChannel opt-in disables suppression entirely", () => {
assert.equal(isSuppressedAsActiveChannel("ch1", "ch1", true, true), false);
assert.equal(isSuppressedAsActiveChannel("ch1", "ch1", false, true), false);
});
20 changes: 20 additions & 0 deletions desktop/src/features/channels/activeChannelSuppression.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Decides whether a notification for `channelId` is suppressed because the
* user is actively viewing that channel.
*
* "Viewing" requires the app window to be focused: a channel that is merely
* selected in a minimized or backgrounded window is not being read, so
* notifications for it must still fire. `notifyForActiveChannel` opts out of
* suppression entirely.
*/
export function isSuppressedAsActiveChannel(
channelId: string,
activeChannelId: string | null,
appFocused: boolean,
notifyForActiveChannel: boolean,
): boolean {
if (notifyForActiveChannel) {
return false;
}
return channelId === activeChannelId && appFocused;
}
27 changes: 21 additions & 6 deletions desktop/src/features/channels/useLiveChannelUpdates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from "@/features/messages/lib/threading";
import { shouldNotifyForEvent } from "@/features/notifications/lib/shouldNotify";
import { relayClient } from "@/shared/api/relayClient";
import { isAppFocused } from "@/shared/lib/useDocumentVisible";
import {
CHANNEL_EVENT_KINDS,
CHANNEL_MESSAGE_EVENT_KINDS,
Expand All @@ -21,14 +22,15 @@ import {
type TrailingDebounce,
} from "@/shared/lib/trailingDebounce";

import { isSuppressedAsActiveChannel } from "./activeChannelSuppression";
import { isDmNotifiableKind } from "./isDmNotifiableKind";
import { refreshChannelsWhenIdle } from "./refreshChannelsWhenIdle";

export type UseLiveChannelUpdatesOptions = {
currentPubkey?: string;
/**
* When true, DM notifications also fire for the channel the user is
* currently viewing (normally suppressed).
* currently viewing (normally suppressed while the app window is focused).
*/
notifyForActiveChannel?: boolean;
onDmMessage?: (event: RelayEvent, channel: Channel) => void;
Expand All @@ -54,8 +56,8 @@ export type UseLiveChannelUpdatesOptions = {
/**
* Fired for replies in threads the user authored, participated in, or
* follows (non-DM channels only — the DM path owns those). Follows the DM
* active-channel rule: suppressed for the channel being viewed unless
* notifyForActiveChannel opts in.
* active-channel rule: suppressed for the channel being viewed in a focused
* window unless notifyForActiveChannel opts in.
*/
onThreadReplyDesktopNotification?: (
channelId: string,
Expand Down Expand Up @@ -220,8 +222,16 @@ export function useLiveChannelUpdates(
}

// Don't fire a notification for the channel the user is already viewing,
// unless the notify-while-viewing setting opts in.
if (channelId === activeChannelId && !options.notifyForActiveChannel) {
// unless the notify-while-viewing setting opts in. A selected channel in
// an unfocused window doesn't count as viewed.
if (
isSuppressedAsActiveChannel(
channelId,
activeChannelId,
isAppFocused(),
options.notifyForActiveChannel ?? false,
)
) {
return;
}

Expand Down Expand Up @@ -315,7 +325,12 @@ export function useLiveChannelUpdates(
if (shouldNotify && isThreadedReply) {
if (
!dmChannelMap.has(channelId) &&
(channelId !== activeChannelId || options.notifyForActiveChannel)
!isSuppressedAsActiveChannel(
channelId,
activeChannelId,
isAppFocused(),
options.notifyForActiveChannel ?? false,
)
) {
options.onThreadReplyDesktopNotification?.(channelId, event);
}
Expand Down