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: 5 additions & 0 deletions desktop/src/app/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
useOpenDmMutation,
} from "@/features/channels/hooks";
import { useUnreadChannels } from "@/features/channels/useUnreadChannels";
import { getThreadReference } from "@/features/messages/lib/threading";
import { useThreadFollows } from "@/features/messages/lib/useThreadFollows";
import {
useHomeFeedNotifications,
Expand Down Expand Up @@ -107,6 +108,7 @@ function toSearchHit(target: DesktopNotificationTarget): SearchHit | null {
channelName: target.channelName ?? null,
createdAt: target.createdAt ?? Math.floor(Date.now() / 1_000),
score: 0,
threadRootId: target.threadRootId ?? null,
};
}

Expand Down Expand Up @@ -228,6 +230,8 @@ export function AppShell() {
: content
: "New message";

const threadRootId = getThreadReference(event.tags).rootId ?? null;

void sendDesktopNotification({
title: channelName,
body,
Expand All @@ -239,6 +243,7 @@ export function AppShell() {
eventId: event.id,
kind: event.kind,
pubkey: event.pubkey,
threadRootId,
},
}).then((didSend) => {
if (!didSend) return;
Expand Down
2 changes: 2 additions & 0 deletions desktop/src/app/navigation/resolveSearchHitDestination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type SearchHitDestination =
kind: "channel";
channelId: string;
messageId?: string;
threadRootId?: string | null;
}
| {
kind: "forum-post";
Expand Down Expand Up @@ -68,5 +69,6 @@ export async function resolveSearchHitDestination(
kind: "channel",
channelId: hit.channelId,
messageId: hit.eventId,
threadRootId: hit.threadRootId ?? null,
};
}
9 changes: 8 additions & 1 deletion desktop/src/app/navigation/useAppNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ export function useAppNavigation() {
options?: {
messageId?: string;
replace?: boolean;
threadRootId?: string | null;
},
) =>
commitNavigation(
Expand All @@ -143,7 +144,12 @@ export function useAppNavigation() {
params: {
channelId,
},
search: options?.messageId ? { messageId: options.messageId } : {},
search: options?.messageId
? {
messageId: options.messageId,
threadRootId: options.threadRootId ?? undefined,
}
: {},
},
{
replace: options?.replace,
Expand Down Expand Up @@ -217,6 +223,7 @@ export function useAppNavigation() {

return goChannel(destination.channelId, {
messageId: destination.messageId,
threadRootId: destination.threadRootId,
});
},
[goChannel, goForumPost],
Expand Down
63 changes: 42 additions & 21 deletions desktop/src/app/routes/ChannelRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ type ChannelRouteScreenProps = {
selectedPostId: string | null;
targetMessageId: string | null;
targetReplyId: string | null;
targetThreadRootId: string | null;
};

export function ChannelRouteScreen({
channelId,
selectedPostId,
targetMessageId,
targetReplyId,
targetThreadRootId,
}: ChannelRouteScreenProps) {
const { closeForumPost, goForumPost } = useAppNavigation();
const channelsQuery = useChannelsQuery();
Expand All @@ -30,42 +32,61 @@ export function ChannelRouteScreen({
const channels = channelsQuery.data ?? [];
const activeChannel =
channels.find((channel) => channel.id === channelId) ?? null;
const [targetMessageEvent, setTargetMessageEvent] =
React.useState<RelayEvent | null>(() =>
getCachedSearchHitEvent(targetMessageId),
);
const [targetMessageEvents, setTargetMessageEvents] = React.useState<
RelayEvent[]
>(() => {
const cachedTarget = getCachedSearchHitEvent(targetMessageId);
return cachedTarget ? [cachedTarget] : [];
});

React.useEffect(() => {
let isCancelled = false;

if (!targetMessageId || selectedPostId) {
setTargetMessageEvent(null);
setTargetMessageEvents([]);
return () => {
isCancelled = true;
};
}

setTargetMessageEvent(getCachedSearchHitEvent(targetMessageId));
void getEventById(targetMessageId)
.then((event) => {
if (!isCancelled) {
setTargetMessageEvent(event);
const cachedTarget = getCachedSearchHitEvent(targetMessageId);
setTargetMessageEvents(cachedTarget ? [cachedTarget] : []);

const eventIds = [
targetMessageId,
targetThreadRootId && targetThreadRootId !== targetMessageId
? targetThreadRootId
: null,
].filter((eventId): eventId is string => eventId !== null);

void Promise.all(
eventIds.map(async (eventId) => {
try {
return await getEventById(eventId);
} catch (error) {
console.error("Failed to load route event", eventId, error);
return null;
}
})
.catch((error) => {
if (!isCancelled) {
console.error(
"Failed to load route target event",
targetMessageId,
error,
}),
).then((events) => {
if (!isCancelled) {
setTargetMessageEvents((currentEvents) => {
const fetchedEvents = events.filter(
(event): event is RelayEvent => event !== null,
);
}
});
const eventsById = new Map<string, RelayEvent>();
for (const event of [...currentEvents, ...fetchedEvents]) {
eventsById.set(event.id, event);
}
return Array.from(eventsById.values());
});
}
});

return () => {
isCancelled = true;
};
}, [selectedPostId, targetMessageId]);
}, [selectedPostId, targetMessageId, targetThreadRootId]);

if (channelsQuery.isPending && !activeChannel) {
return (
Expand All @@ -89,7 +110,7 @@ export function ChannelRouteScreen({
}}
selectedForumPostId={selectedPostId}
targetForumReplyId={targetReplyId}
targetMessageEvent={targetMessageEvent}
targetMessageEvents={targetMessageEvents}
targetMessageId={targetMessageId}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ function ForumPostRouteComponent() {
selectedPostId={postId}
targetMessageId={null}
targetReplyId={search.replyId ?? null}
targetThreadRootId={null}
/>
</React.Suspense>
);
Expand Down
6 changes: 6 additions & 0 deletions desktop/src/app/routes/channels.$channelId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback";

type ChannelRouteSearch = {
messageId?: string;
threadRootId?: string;
};

function validateChannelSearch(
Expand All @@ -15,6 +16,10 @@ function validateChannelSearch(
typeof search.messageId === "string" && search.messageId.length > 0
? search.messageId
: undefined,
threadRootId:
typeof search.threadRootId === "string" && search.threadRootId.length > 0
? search.threadRootId
: undefined,
};
}

Expand All @@ -41,6 +46,7 @@ function ChannelRouteComponent() {
selectedPostId={null}
targetMessageId={search.messageId ?? null}
targetReplyId={null}
targetThreadRootId={search.threadRootId ?? null}
/>
</React.Suspense>
);
Expand Down
12 changes: 7 additions & 5 deletions desktop/src/features/channels/ui/ChannelScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ type ChannelScreenProps = {
onSelectForumPost: (postId: string) => void;
selectedForumPostId: string | null;
targetForumReplyId: string | null;
targetMessageEvent: RelayEvent | null;
targetMessageEvents: RelayEvent[];
targetMessageId: string | null;
};

Expand All @@ -78,7 +78,7 @@ export function ChannelScreen({
onSelectForumPost,
selectedForumPostId,
targetForumReplyId,
targetMessageEvent,
targetMessageEvents,
targetMessageId,
}: ChannelScreenProps) {
const {
Expand Down Expand Up @@ -144,9 +144,11 @@ export function ChannelScreen({

const resolvedMessages = React.useMemo(() => {
const currentMessages = messagesQuery.data ?? [];
if (!activeChannel || !targetMessageEvent) return currentMessages;
return mergeMessages(currentMessages, targetMessageEvent);
}, [activeChannel, messagesQuery.data, targetMessageEvent]);
if (!activeChannel || targetMessageEvents.length === 0) {
return currentMessages;
}
return targetMessageEvents.reduce(mergeMessages, currentMessages);
}, [activeChannel, messagesQuery.data, targetMessageEvents]);
const messageAuthorPubkeys = React.useMemo(
() => collectMessageAuthorPubkeys(resolvedMessages),
[resolvedMessages],
Expand Down
4 changes: 4 additions & 0 deletions desktop/src/features/notifications/lib/desktop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type DesktopNotificationTarget = {
eventId: string | null;
kind: number | null;
pubkey?: string;
threadRootId?: string | null;
};

type DesktopNotificationPayload = {
Expand Down Expand Up @@ -73,6 +74,8 @@ function parseNotificationTarget(
const kind = typeof candidate.kind === "number" ? candidate.kind : null;
const pubkey =
typeof candidate.pubkey === "string" ? candidate.pubkey : undefined;
const threadRootId =
typeof candidate.threadRootId === "string" ? candidate.threadRootId : null;

if (!channelId && !eventId) {
return null;
Expand All @@ -86,6 +89,7 @@ function parseNotificationTarget(
eventId,
kind,
pubkey,
threadRootId,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
truncatePubkey,
type UserProfileLookup,
} from "@/features/profile/lib/identity";
import { getThreadReference } from "@/features/messages/lib/threading";
import type { FeedItem, HomeFeedResponse } from "@/shared/api/types";
import {
collectHomeAlertItems,
Expand Down Expand Up @@ -99,6 +100,7 @@ export function useFeedDesktopNotifications(

const deliverFeedNotification = React.useEffectEvent(
async (item: FeedItem, senderName?: string) => {
const threadRootId = getThreadReference(item.tags).rootId ?? null;
const didSend = await sendDesktopNotification({
body: notificationBody(item),
target: {
Expand All @@ -109,6 +111,7 @@ export function useFeedDesktopNotifications(
eventId: item.id,
kind: item.kind,
pubkey: item.pubkey,
threadRootId,
},
title: notificationTitle(item, senderName),
});
Expand Down
1 change: 1 addition & 0 deletions desktop/src/shared/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ export type SearchHit = {
channelName: string | null;
createdAt: number;
score: number;
threadRootId?: string | null;
};

export type SearchMessagesResponse = {
Expand Down
5 changes: 4 additions & 1 deletion desktop/src/shared/ui/markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,10 @@ function MarkdownInner({
// "the thread root is a forum post" up front would require an
// event lookup we don't currently have synchronously; the brief
// explicitly allows skipping that detection and falling through.
void goChannel(link.channelId, { messageId: link.messageId });
void goChannel(link.channelId, {
messageId: link.messageId,
threadRootId: link.threadRootId,
});
},
imetaByUrl,
mentionPubkeysByName,
Expand Down
5 changes: 4 additions & 1 deletion desktop/src/shared/useMessageDeepLinks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ export function useMessageDeepLinks() {
let cancelled = false;
const unlistenPromise = listenForMessageDeepLinks((payload) => {
if (cancelled) return;
void goChannel(payload.channelId, { messageId: payload.messageId });
void goChannel(payload.channelId, {
messageId: payload.messageId,
threadRootId: payload.threadRootId,
});
});
return () => {
cancelled = true;
Expand Down