Skip to content
Merged
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
5 changes: 3 additions & 2 deletions desktop/src-tauri/src/unread_catch_up.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ fn classify_batch(
let broadcast = has_exact_tag(&event.tags, "broadcast", "1");
let threaded = reference.parent_id.is_some() && !broadcast;
let high_priority = item.channel.channel_type == "dm"
|| threaded
|| broadcast
|| has_tag_value(&event.tags, "p", &self_pubkey);
max_trigger = max_trigger.max(event.created_at);
Expand Down Expand Up @@ -518,9 +519,9 @@ mod tests {
assert_eq!(
observed_events
.iter()
.map(|event| event.id.as_str())
.map(|event| (event.id.as_str(), event.high_priority))
.collect::<Vec<_>>(),
["external-reply"]
[("external-reply", true)]
);
assert_eq!(discovered.participated, ["root"]);
}
Expand Down
1 change: 1 addition & 0 deletions desktop/src/app/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,7 @@ export function AppShell() {
selectedChannelId={selectedChannelId}
selectedView={selectedView}
unreadChannelIds={unreadChannelIds}
{...{ highPriorityUnreadChannelIds }}
previewActivityChannelIds={unreadThreadChannelIds}
unreadChannelCounts={unreadChannelCounts}
mutedChannelIds={mutedChannelIds}
Expand Down
28 changes: 7 additions & 21 deletions desktop/src/features/channels/useUnreadChannels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
} from "@/features/channels/useLiveChannelUpdates";
import {
countUnreadAppBadgeObservedEvents,
countUnreadBadgeObservedEvents,
countUnreadHighPriorityObservedEvents,
countUnreadObservedEvents,
hasUnreadTopLevelObservedEvent,
Expand Down Expand Up @@ -426,13 +425,14 @@ export function useUnreadChannels(
const handleChannelMessage = React.useCallback(
(channelId: string, event: RelayEvent) => {
const channel = channelsRef.current.find((ch) => ch.id === channelId);
const isThreadedReply =
getThreadReference(event.tags).parentId !== null &&
!isBroadcastReply(event.tags);
const isHighPriority =
channel?.channelType === "dm" ||
isThreadedReply ||
(normalizedPubkey !== null &&
isHighPriorityEventForUser(event, normalizedPubkey));
const isThreadedReply =
getThreadReference(event.tags).parentId !== null &&
!isBroadcastReply(event.tags);
const didRecordUnreadEvent = recordUnreadEvent(
channelId,
makeObservedUnreadEvent({
Expand Down Expand Up @@ -852,38 +852,24 @@ export function useUnreadChannels(
) {
topLevelUnread.add(channel.id);
}
const badgeCount =
nativeProjection?.badgeCount ??
countUnreadBadgeObservedEvents(
observedEvents,
readAtForObservedEvent,
);
const appBadgeCount =
nativeProjection?.appBadgeCount ??
countUnreadAppBadgeObservedEvents(
observedEvents,
readAtForObservedEvent,
);
// Sidebar numerals on non-DM rows count every unread mention and
// broadcast, including threaded ones. The Dock projection
// (appBadgeCount) keeps excluding threaded replies because Home's
// badge subtotal already counts those; reusing it here would hide
// thread mentions from the channel row.
const highPriorityCount =
nativeProjection?.highPriorityCount ??
countUnreadHighPriorityObservedEvents(
observedEvents,
readAtForObservedEvent,
);
counts.set(
channel.id,
channel.channelType === "dm" ? badgeCount : highPriorityCount,
);
counts.set(channel.id, unreadCount);
unreadChannelNotificationCount += appBadgeCount;

// DM channels: any unread DM is high-priority. Non-DM: high-priority
// only if at least one mention/broadcast remains unread in its own
// channel/thread context.
// only if at least one mention, broadcast, or relevant thread reply
// remains unread in its own channel/thread context.
if (channel.channelType === "dm" || highPriorityCount > 0) {
highPriority.add(channel.id);
}
Expand Down

This file was deleted.

This file was deleted.

47 changes: 0 additions & 47 deletions desktop/src/features/sidebar/lib/useSidebarActivityOverflow.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import assert from "node:assert/strict";
import test from "node:test";

import {
hasHighPriorityOverflow,
sidebarOverflowUnreadLabel,
} from "./useSidebarUnreadOverflow.ts";

test("labels the destination total as unread", () => {
assert.equal(sidebarOverflowUnreadLabel(3), "3 unread");
});

test("promotes actionable unread and every offscreen DM", () => {
const actionable = new Set(["mention"]);
const dms = new Set(["dm"]);

assert.equal(hasHighPriorityOverflow(["channel"], actionable, dms), false);
assert.equal(hasHighPriorityOverflow(["mention"], actionable, dms), true);
assert.equal(hasHighPriorityOverflow(["dm"], actionable, dms), true);
assert.equal(
hasHighPriorityOverflow(["channel", "dm"], actionable, dms),
true,
);
});
69 changes: 69 additions & 0 deletions desktop/src/features/sidebar/lib/useSidebarUnreadOverflow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import * as React from "react";

import { useUnreadOverflow } from "@/features/sidebar/lib/useUnreadOverflow";

type ScrollRef = Parameters<typeof useUnreadOverflow>[0]["scrollRef"];

/**
* Returns whether any offscreen destination is a DM or has directed unread
* activity, which should keep the sidebar overflow control emphasized.
*/
export function hasHighPriorityOverflow(
offscreenChannelIds: readonly string[],
highPriorityUnreadChannelIds: ReadonlySet<string>,
dmChannelIds: ReadonlySet<string>,
) {
return offscreenChannelIds.some(
(channelId) =>
dmChannelIds.has(channelId) ||
highPriorityUnreadChannelIds.has(channelId),
);
}

/** Formats the accessible label for a distinct unread destination count. */
export function sidebarOverflowUnreadLabel(count: number) {
return `${count} unread`;
}

/**
* Projects unread message and thread activity into offscreen destination sets.
* Message and preview destinations are unioned and deduplicated; DMs and
* destinations with directed unread activity receive high-priority treatment.
*/
export function useSidebarUnreadOverflow({
dmChannelIds,
highPriorityUnreadChannelIds,
previewActivityChannelIds,
scrollRef,
unreadChannelIds,
}: {
dmChannelIds: ReadonlySet<string>;
highPriorityUnreadChannelIds: ReadonlySet<string>;
previewActivityChannelIds: ReadonlySet<string>;
scrollRef: ScrollRef;
unreadChannelIds: ReadonlySet<string>;
}) {
const messageChannelIds = React.useMemo(
() => new Set([...unreadChannelIds, ...previewActivityChannelIds]),
[previewActivityChannelIds, unreadChannelIds],
);
const messageOverflow = useUnreadOverflow({
scrollRef,
unreadChannelIds: messageChannelIds,
});

return {
...messageOverflow,
unreadMessageBelowChannelIds: messageOverflow.unreadBelowChannelIds,
hasHighPriorityAbove: hasHighPriorityOverflow(
messageOverflow.unreadAboveChannelIds,
highPriorityUnreadChannelIds,
dmChannelIds,
),
hasHighPriorityBelow: hasHighPriorityOverflow(
messageOverflow.unreadBelowChannelIds,
highPriorityUnreadChannelIds,
dmChannelIds,
),
};
}
Loading
Loading