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
2 changes: 1 addition & 1 deletion desktop/src/app/navigation/useAppNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export function useAppNavigation() {
},
{
replace: options?.replace,
resetScroll: options?.messageId ? false : undefined,
resetScroll: options?.messageId ? true : undefined,
},
),
[commitNavigation],
Expand Down
19 changes: 15 additions & 4 deletions desktop/src/features/channels/ui/ChannelScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import {
} from "./useChannelActivityTyping";
import { useChannelAgentSessions } from "./useChannelAgentSessions";
import { useChannelProfilePanel } from "./useChannelProfilePanel";
import { useChannelRouteTarget } from "./useChannelRouteTarget";
type ChannelScreenProps = {
activeChannel: Channel | null;
currentIdentity?: Identity;
Expand Down Expand Up @@ -359,8 +360,6 @@ export function ChannelScreen({
setProfilePanelPubkey,
setThreadReplyTargetId,
setThreadScrollTargetId,
targetMessageId,
timelineMessages,
});

const { handleOpenProfilePanel, handleCloseProfilePanel, handleOpenDm } =
Expand Down Expand Up @@ -393,10 +392,22 @@ export function ChannelScreen({
const handleThreadScrollTargetResolved = React.useCallback(() => {
setThreadScrollTargetId(null);
}, []);

React.useEffect(() => {
resetComposerTargets(activeChannelId);
}, [activeChannelId, resetComposerTargets]);
const mainTimelineTargetMessageId = useChannelRouteTarget({
activeChannel,
activeChannelId,
closeAgentSession: handleCloseAgentSession,
setEditTargetId,
setExpandedThreadReplyIds,
setOpenThreadHeadId,
setProfilePanelPubkey,
setThreadReplyTargetId,
setThreadScrollTargetId,
targetMessageId,
timelineMessages,
});
React.useEffect(() => {
if (openThreadHeadId && !openThreadHeadMessage) {
setOpenThreadHeadId(null);
Expand Down Expand Up @@ -507,7 +518,7 @@ export function ChannelScreen({
profilePanelPubkey={profilePanelPubkey}
personaLookup={personaLookup}
profiles={messageProfiles}
targetMessageId={targetMessageId}
targetMessageId={mainTimelineTargetMessageId}
threadHeadMessage={openThreadHeadMessage}
threadMessages={threadMessages}
threadTypingPubkeys={threadTypingPubkeys}
Expand Down
77 changes: 0 additions & 77 deletions desktop/src/features/channels/ui/useChannelAgentSessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ type UseChannelAgentSessionsOptions = {
setProfilePanelPubkey: (value: string | null) => void;
setThreadReplyTargetId: (value: string | null) => void;
setThreadScrollTargetId: (value: string | null) => void;
targetMessageId: string | null;
timelineMessages: TimelineMessage[];
};

function relayStatusToManagedStatus(
Expand Down Expand Up @@ -160,13 +158,10 @@ export function useChannelAgentSessions({
setProfilePanelPubkey,
setThreadReplyTargetId,
setThreadScrollTargetId,
targetMessageId,
timelineMessages,
}: UseChannelAgentSessionsOptions) {
const [openAgentSessionPubkey, setOpenAgentSessionPubkey] = React.useState<
string | null
>(null);
const handledThreadTargetIdRef = React.useRef<string | null>(null);

const channelAgentSessionAgents = React.useMemo(
() =>
Expand Down Expand Up @@ -214,78 +209,6 @@ export function useChannelAgentSessions({
[handleOpenThread, setProfilePanelPubkey],
);

React.useEffect(() => {
if (!targetMessageId) {
handledThreadTargetIdRef.current = null;
return;
}

const targetKey = `${activeChannelId ?? "none"}:${targetMessageId}`;
if (
handledThreadTargetIdRef.current !== null &&
handledThreadTargetIdRef.current !== targetKey
) {
handledThreadTargetIdRef.current = null;
}

if (
handledThreadTargetIdRef.current === targetKey ||
!activeChannel ||
activeChannel.channelType === "forum"
) {
return;
}

const targetMessage =
timelineMessages.find((message) => message.id === targetMessageId) ??
null;

if (!targetMessage?.parentId) {
return;
}

const threadHeadId = targetMessage.rootId ?? targetMessage.parentId;
const messageById = new Map(
timelineMessages.map((message) => [message.id, message]),
);

if (!messageById.has(threadHeadId)) {
return;
}

const expandedReplyIds = new Set<string>();
let ancestorId: string | null = targetMessage.parentId;
let guard = 0;

while (
ancestorId &&
ancestorId !== threadHeadId &&
guard < timelineMessages.length
) {
expandedReplyIds.add(ancestorId);
ancestorId = messageById.get(ancestorId)?.parentId ?? null;
guard += 1;
}

setOpenAgentSessionPubkey(null);
setProfilePanelPubkey(null);
setOpenThreadHeadId(threadHeadId);
setThreadReplyTargetId(threadHeadId);
setThreadScrollTargetId(targetMessageId);
setExpandedThreadReplyIds(expandedReplyIds);
handledThreadTargetIdRef.current = targetKey;
}, [
activeChannel,
activeChannelId,
setExpandedThreadReplyIds,
setOpenThreadHeadId,
setProfilePanelPubkey,
setThreadReplyTargetId,
setThreadScrollTargetId,
targetMessageId,
timelineMessages,
]);

React.useEffect(() => {
if (
openAgentSessionPubkey &&
Expand Down
153 changes: 153 additions & 0 deletions desktop/src/features/channels/ui/useChannelRouteTarget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import * as React from "react";

import type { TimelineMessage } from "@/features/messages/types";
import type { Channel } from "@/shared/api/types";

function isBroadcastReply(message: TimelineMessage): boolean {
return (
message.tags?.some((tag) => tag[0] === "broadcast" && tag[1] === "1") ??
false
);
}

function getThreadRouteTarget(
targetMessage: TimelineMessage,
messageById: ReadonlyMap<string, TimelineMessage>,
): { expandedReplyIds: Set<string>; threadHeadId: string } | null {
const threadHeadId = targetMessage.rootId ?? targetMessage.parentId ?? null;
if (!threadHeadId || !messageById.has(threadHeadId)) {
return null;
}

const expandedReplyIds = new Set<string>();
let ancestorId = targetMessage.parentId ?? null;
let guard = 0;
const maxHops = messageById.size + 1;

while (ancestorId && ancestorId !== threadHeadId && guard < maxHops) {
const ancestor = messageById.get(ancestorId);
if (!ancestor) {
return null;
}

expandedReplyIds.add(ancestor.id);
ancestorId = ancestor.parentId ?? null;
guard += 1;
}

if (ancestorId !== threadHeadId) {
return null;
}

return { expandedReplyIds, threadHeadId };
}

function getRouteMainTimelineTargetId(
targetMessageId: string | null,
targetMessage: TimelineMessage | null,
): string | null {
if (!targetMessageId) {
return null;
}

if (!targetMessage?.parentId || isBroadcastReply(targetMessage)) {
return targetMessageId;
}

return targetMessage.rootId ?? targetMessage.parentId;
}

export function useChannelRouteTarget({
activeChannel,
activeChannelId,
closeAgentSession,
setEditTargetId,
setExpandedThreadReplyIds,
setOpenThreadHeadId,
setProfilePanelPubkey,
setThreadReplyTargetId,
setThreadScrollTargetId,
targetMessageId,
timelineMessages,
}: {
activeChannel: Channel | null;
activeChannelId: string | null;
closeAgentSession: () => void;
setEditTargetId: React.Dispatch<React.SetStateAction<string | null>>;
setExpandedThreadReplyIds: React.Dispatch<React.SetStateAction<Set<string>>>;
setOpenThreadHeadId: React.Dispatch<React.SetStateAction<string | null>>;
setProfilePanelPubkey: React.Dispatch<React.SetStateAction<string | null>>;
setThreadReplyTargetId: React.Dispatch<React.SetStateAction<string | null>>;
setThreadScrollTargetId: React.Dispatch<React.SetStateAction<string | null>>;
targetMessageId: string | null;
timelineMessages: TimelineMessage[];
}) {
const timelineMessageById = React.useMemo(
() => new Map(timelineMessages.map((message) => [message.id, message])),
[timelineMessages],
);
const targetTimelineMessage = targetMessageId
? (timelineMessageById.get(targetMessageId) ?? null)
: null;
const mainTimelineTargetMessageId = getRouteMainTimelineTargetId(
targetMessageId,
targetTimelineMessage,
);
const handledThreadRouteTargetRef = React.useRef<string | null>(null);

React.useEffect(() => {
if (!targetMessageId) {
handledThreadRouteTargetRef.current = null;
return;
}

const targetKey = `${activeChannelId ?? "none"}:${targetMessageId}`;
if (handledThreadRouteTargetRef.current !== targetKey) {
handledThreadRouteTargetRef.current = null;
}

if (
handledThreadRouteTargetRef.current === targetKey ||
!activeChannel ||
activeChannel.channelType === "forum"
) {
return;
}

const targetMessage = timelineMessageById.get(targetMessageId) ?? null;
if (!targetMessage?.parentId || isBroadcastReply(targetMessage)) {
return;
}

const routeTarget = getThreadRouteTarget(
targetMessage,
timelineMessageById,
);
if (!routeTarget) {
return;
}

closeAgentSession();
setProfilePanelPubkey(null);
setEditTargetId(null);
setOpenThreadHeadId(routeTarget.threadHeadId);
setThreadReplyTargetId(routeTarget.threadHeadId);
setThreadScrollTargetId(targetMessageId);
setExpandedThreadReplyIds(routeTarget.expandedReplyIds);
handledThreadRouteTargetRef.current = targetKey;
}, [
activeChannel,
activeChannelId,
closeAgentSession,
setEditTargetId,
setExpandedThreadReplyIds,
setOpenThreadHeadId,
setProfilePanelPubkey,
setThreadReplyTargetId,
setThreadScrollTargetId,
targetMessageId,
timelineMessageById,
]);

return mainTimelineTargetMessageId;
}
25 changes: 12 additions & 13 deletions desktop/src/features/home/ui/InboxMessageRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,18 @@ export function InboxMessageRow({
} = useReactionHandler(timelineMessage, onToggleReaction);

return (
<div className="px-6 py-2">
<div className="relative px-6 py-2">
{message.isSelected ? (
<div
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-x-0 inset-y-1 transition-opacity duration-1000",
isFocusHighlightVisible
? "bg-primary/[0.07] opacity-100"
: "bg-primary/[0.07] opacity-0",
)}
/>
) : null}
<article
className={cn(
"group/message relative flex items-start gap-2.5 px-2 py-1",
Expand All @@ -72,18 +83,6 @@ export function InboxMessageRow({
: "home-inbox-context-message"
}
>
{message.isSelected ? (
<div
aria-hidden="true"
className={cn(
"pointer-events-none absolute -inset-x-2 -inset-y-1 rounded-xl transition-opacity duration-1000",
isFocusHighlightVisible
? "bg-primary/[0.07] opacity-100"
: "bg-primary/[0.07] opacity-0",
)}
/>
) : null}

{canReply || canToggleReactions ? (
<div className="absolute right-2 top-1 z-10">
<MessageActionBar
Expand Down
4 changes: 3 additions & 1 deletion desktop/src/features/messages/ui/MessageRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,9 @@ export const MessageRow = React.memo(
className={cn(
"group/message relative rounded-2xl px-2 py-1 transition-colors",
"flex items-start gap-2.5",
highlighted ? "bg-primary/10 ring-1 ring-primary/30" : "",
highlighted
? "-mx-4 rounded-none px-6 before:absolute before:-inset-y-1.5 before:inset-x-0 before:animate-[route-target-highlight-fade_2s_ease-out_forwards] before:bg-primary/10 before:content-[''] motion-reduce:before:animate-none sm:-mx-6 sm:px-8"
: "",
)}
data-message-id={message.id}
data-testid="message-row"
Expand Down
5 changes: 4 additions & 1 deletion desktop/src/features/messages/ui/MessageTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ export const MessageTimeline = React.memo(function MessageTimeline({
}: MessageTimelineProps) {
const scrollContainerRef = React.useRef<HTMLDivElement>(null);
const topSentinelRef = React.useRef<HTMLDivElement>(null);
const scrollRestorationId = targetMessageId
? `message-timeline:${channelId ?? "none"}:target:${targetMessageId}`
: `message-timeline:${channelId ?? "none"}`;

const {
bottomAnchorRef,
Expand Down Expand Up @@ -129,7 +132,7 @@ export const MessageTimeline = React.memo(function MessageTimeline({
<div className="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
<div
className="absolute inset-0 overflow-y-auto overflow-x-hidden overscroll-contain px-4 pb-24 pt-1 [overflow-anchor:none] sm:px-6"
data-scroll-restoration-id="message-timeline"
data-scroll-restoration-id={scrollRestorationId}
data-testid="message-timeline"
onScroll={syncScrollState}
ref={scrollContainerRef}
Expand Down
Loading
Loading