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
10 changes: 4 additions & 6 deletions desktop/src/features/channels/ui/ChannelPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
getDmHuddleMemberPubkeys,
hasOtherDmParticipant,
} from "@/features/channels/lib/dmHuddleMembers";
import { buildVideoReviewContextsByMessageId } from "@/features/messages/lib/videoReviewContext";
import { buildVideoReviewPresentationByMessageId } from "@/features/messages/lib/videoReviewContext";
import { useComposerHeightPadding } from "@/features/messages/ui/useComposerHeightPadding";
import { UserProfilePanel } from "@/features/profile/ui/UserProfilePanel";
import { ChannelFindBar } from "@/features/search/ui/ChannelFindBar";
Expand Down Expand Up @@ -464,7 +464,7 @@ export const ChannelPane = React.memo(function ChannelPane({
const activeVideoReviewCommentSender = activeChannel?.archivedAt
? undefined
: onSendVideoReviewComment;
const threadVideoReviewContextsByMessageId = React.useMemo(() => {
const threadVideoReviewPresentation = React.useMemo(() => {
const messagesById = new Map(
messages.map((message) => [message.id, message]),
);
Expand All @@ -475,7 +475,7 @@ export const ChannelPane = React.memo(function ChannelPane({
messagesById.set(message.id, message);
}

return buildVideoReviewContextsByMessageId({
return buildVideoReviewPresentationByMessageId({
channelId: activeChannel?.id ?? null,
channelName: activeChannel?.name,
channelType: activeChannel?.channelType ?? null,
Expand Down Expand Up @@ -883,9 +883,7 @@ export const ChannelPane = React.memo(function ChannelPane({
scrollTargetHighlights={!layoutScrollTargetId}
scrollTargetId={layoutScrollTargetId ?? threadScrollTargetId}
threadHead={threadHeadMessage}
videoReviewContextsByMessageId={
threadVideoReviewContextsByMessageId
}
videoReviewPresentation={threadVideoReviewPresentation}
widthPx={threadPanelWidthPx}
threadReplies={threadMessages}
threadRepliesPending={threadMessagesPending}
Expand Down
52 changes: 52 additions & 0 deletions desktop/src/features/messages/lib/videoReviewContext.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import test from "node:test";
import {
buildVideoReviewCommentsByRootId,
buildVideoReviewCommentsForRoot,
buildVideoReviewCommentRootIdsByMessageId,
buildVideoReviewContextForMessage,
buildVideoReviewContextsByMessageId,
hasVideoAttachment,
Expand Down Expand Up @@ -159,6 +160,57 @@ test("buildVideoReviewCommentsForRoot returns descendants for one root", () => {
);
});

test("buildVideoReviewCommentRootIdsByMessageId targets the nearest video ancestor", () => {
const root = message({ id: "root", body: "Review request" });
const firstVideo = message({
id: "first-video",
body: "![video](https://relay/media/a.mp4)",
parentId: root.id,
rootId: root.id,
});
const firstComment = message({
id: "first-comment",
body: "[00:01] tighten this",
parentId: firstVideo.id,
rootId: root.id,
});
const nestedVideo = message({
id: "nested-video",
body: "![video](https://relay/media/b.mp4)",
parentId: firstComment.id,
rootId: root.id,
});
const nestedComment = message({
id: "nested-comment",
body: "[00:02] check this frame",
parentId: nestedVideo.id,
rootId: root.id,
});
const plainReply = message({
id: "plain-reply",
body: "No video ancestor",
parentId: root.id,
rootId: root.id,
});

const rootIds = buildVideoReviewCommentRootIdsByMessageId([
root,
firstVideo,
firstComment,
nestedVideo,
nestedComment,
plainReply,
]);

assert.deepEqual(
[...rootIds.entries()],
[
[firstComment.id, firstVideo.id],
[nestedComment.id, nestedVideo.id],
],
);
});

test("buildVideoReviewContextForMessage posts against the source video", async () => {
const video = message({
id: "video",
Expand Down
42 changes: 42 additions & 0 deletions desktop/src/features/messages/lib/videoReviewContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,33 @@ export function buildVideoReviewCommentsForRoot(
return comments;
}

export function buildVideoReviewCommentRootIdsByMessageId(
messages: TimelineMessage[],
): ReadonlyMap<string, string> {
const messageById = new Map(messages.map((message) => [message.id, message]));
const videoMessageIds = new Set(
messages.filter(hasVideoAttachment).map((message) => message.id),
);
const rootIdsByMessageId = new Map<string, string>();

for (const message of messages) {
if (videoMessageIds.has(message.id)) continue;

let ancestorId = message.parentId ?? null;
const visited = new Set<string>();
while (ancestorId && !visited.has(ancestorId)) {
if (videoMessageIds.has(ancestorId)) {
rootIdsByMessageId.set(message.id, ancestorId);
break;
}
visited.add(ancestorId);
ancestorId = messageById.get(ancestorId)?.parentId ?? null;
}
}

return rootIdsByMessageId;
}

export function buildVideoReviewContextForMessage({
channelId,
channelName,
Expand Down Expand Up @@ -193,3 +220,18 @@ export function buildVideoReviewContextsByMessageId({

return contexts;
}

export function buildVideoReviewPresentationByMessageId(
args: Parameters<typeof buildVideoReviewContextsByMessageId>[0],
) {
return {
commentRootIdsByMessageId: buildVideoReviewCommentRootIdsByMessageId(
args.messages,
),
contextsByMessageId: buildVideoReviewContextsByMessageId(args),
};
}

export type VideoReviewPresentation = ReturnType<
typeof buildVideoReviewPresentationByMessageId
>;
57 changes: 42 additions & 15 deletions desktop/src/features/messages/ui/MessageRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ import { resolveSnapshotSharedBy } from "@/features/messages/lib/snapshotSharedB
import { resolveMentionProps } from "@/shared/lib/resolveMentionNames";
import { Markdown } from "@/shared/ui/markdown";
import type { VideoReviewContext } from "@/shared/ui/VideoPlayer";
import { useOpenVideoReviewAt } from "@/shared/ui/VideoReviewNavigation";
import { parseVideoReviewTimecode } from "@/shared/ui/videoReviewTimecode";
import { VideoReviewTimecodeButton } from "@/shared/ui/VideoReviewTimecodeButton";
import { MessageActionBar } from "./MessageActionBar";
import { MessageAgentOwner } from "./MessageAgentOwner";
import { MessageAuthorText, MessageHeaderRow } from "./MessageHeader";
Expand Down Expand Up @@ -95,6 +98,7 @@ export const MessageRow = React.memo(
profiles,
searchQuery,
showDepthGuides = true,
videoReviewCommentRootId,
videoReviewContext,
}: {
channelId?: string | null;
Expand Down Expand Up @@ -143,6 +147,7 @@ export const MessageRow = React.memo(
profiles?: UserProfileLookup;
searchQuery?: string;
showDepthGuides?: boolean;
videoReviewCommentRootId?: string;
videoReviewContext?: VideoReviewContext;
}) {
// Keep the transient send state with its timestamp rather than collapsing
Expand Down Expand Up @@ -244,6 +249,7 @@ export const MessageRow = React.memo(
const bodyOffsetClass = emojiOnly ? "mt-1" : "-mt-0.5";

const { nonDmChannelNames: channelNames } = useChannelNavigation();
const openVideoReviewAt = useOpenVideoReviewAt();

const indentRem = getThreadReplyIndentRem(message.depth);
const descendantGuideOffsetRem = connectDescendants
Expand Down Expand Up @@ -340,22 +346,24 @@ export const MessageRow = React.memo(
message={message}
/>
);
default:
{
const waveMessage = parseWaveMessageContent(message.body);
if (waveMessage) {
return (
<WaveMessageAttachment
channelId={channelId}
fallbackText={waveMessage.fallbackText}
huddleMemberPubkeys={huddleMemberPubkeys}
huddleMemberPubkeysPending={huddleMemberPubkeysPending}
/>
);
}
default: {
const waveMessage = parseWaveMessageContent(message.body);
if (waveMessage) {
return (
<WaveMessageAttachment
channelId={channelId}
fallbackText={waveMessage.fallbackText}
huddleMemberPubkeys={huddleMemberPubkeys}
huddleMemberPubkeysPending={huddleMemberPubkeysPending}
/>
);
}

return (
const reviewRootEventId = videoReviewCommentRootId;
const reviewTimecode = reviewRootEventId
? parseVideoReviewTimecode(message.body)
: null;
const markdown = (
<Markdown
channelNames={channelNames}
className={cn(
Expand All @@ -371,7 +379,7 @@ export const MessageRow = React.memo(
message,
isKnownAgentPubkey,
)}
content={message.body}
content={reviewTimecode?.text ?? message.body}
customEmoji={customEmoji}
imetaByUrl={imetaByUrl}
agentMentionPubkeysByName={agentMentionPubkeysByName}
Expand All @@ -382,6 +390,24 @@ export const MessageRow = React.memo(
videoReviewContext={videoReviewContext}
/>
);
if (!reviewRootEventId || !reviewTimecode || !openVideoReviewAt) {
return markdown;
}

return (
<div className="flex min-w-0 items-start gap-1.5">
<VideoReviewTimecodeButton
surface="message"
timecode={reviewTimecode.timecode}
onClick={(event) => {
event.stopPropagation();
openVideoReviewAt(reviewRootEventId, reviewTimecode.seconds);
}}
/>
<div className="min-w-0 flex-1">{markdown}</div>
</div>
);
}
}
};

Expand Down Expand Up @@ -893,6 +919,7 @@ export const MessageRow = React.memo(
prev.playEntrance === next.playEntrance &&
prev.profiles === next.profiles &&
prev.searchQuery === next.searchQuery &&
prev.videoReviewCommentRootId === next.videoReviewCommentRootId &&
prev.videoReviewContext === next.videoReviewContext,
);

Expand Down
57 changes: 33 additions & 24 deletions desktop/src/features/messages/ui/MessageThreadPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ import {
import type { ImetaMedia } from "@/features/messages/lib/imetaMediaMarkdown";
import { canManageMessageForCurrentUser } from "@/features/messages/lib/canManageMessage";
import type { TimelineMessage } from "@/features/messages/types";
import type { VideoReviewPresentation } from "@/features/messages/lib/videoReviewContext";
import type { UserProfileLookup } from "@/features/profile/lib/identity";
import type { Channel } from "@/shared/api/types";
import type { ThreadPanelLayoutProps } from "@/features/channels/lib/threadPanelLayout";
import { useEscapeKey } from "@/shared/hooks/useEscapeKey";
import { useIsThreadPanelOverlay } from "@/shared/hooks/use-mobile";
import { VideoReviewNavigationProvider } from "@/shared/ui/VideoReviewNavigation";
import { cn } from "@/shared/lib/cn";
import { AuxiliaryPanel } from "@/shared/layout/AuxiliaryPanel";
import { AuxiliaryPanelBody } from "@/shared/layout/AuxiliaryPanel";
Expand All @@ -38,7 +40,6 @@ import {
} from "@/features/messages/lib/messageThreadPanelLayout";
import { Button } from "@/shared/ui/button";
import { Separator } from "@/shared/ui/separator";
import type { VideoReviewContext } from "@/shared/ui/VideoPlayer";
import { ComposerActivityAccessory } from "./ComposerActivityAccessory";
import { ComposerDockBackdrop } from "./ComposerDockBackdrop";
import { MessageComposer } from "./MessageComposer";
Expand Down Expand Up @@ -111,7 +112,7 @@ type MessageThreadPanelProps = ThreadPanelLayoutProps & {
threadUnreadCount?: number;
threadReplyUnreadCounts?: ReadonlyMap<string, number>;
threadTypingPubkeys: string[];
videoReviewContextsByMessageId?: ReadonlyMap<string, VideoReviewContext>;
videoReviewPresentation?: VideoReviewPresentation;
activityAccessoryContent?: React.ReactNode;
activityAccessoryVisible: boolean;
widthPx: number;
Expand Down Expand Up @@ -225,7 +226,7 @@ export function MessageThreadPanel({
scrollTargetId,
scrollTargetHighlights = true,
threadHead,
videoReviewContextsByMessageId,
videoReviewPresentation,
threadReplies,
threadRepliesPending = false,
threadUnreadCount,
Expand Down Expand Up @@ -617,7 +618,10 @@ export function MessageThreadPanel({
}
profiles={profiles}
showDepthGuides={shouldShowThreadBranchGuides}
videoReviewContext={videoReviewContextsByMessageId?.get(
videoReviewCommentRootId={videoReviewPresentation?.commentRootIdsByMessageId.get(
threadHead.id,
)}
videoReviewContext={videoReviewPresentation?.contextsByMessageId.get(
threadHead.id,
)}
/>
Expand Down Expand Up @@ -776,7 +780,10 @@ export function MessageThreadPanel({
onToggleReaction={onToggleReaction}
profiles={profiles}
showDepthGuides={shouldShowThreadBranchGuides}
videoReviewContext={videoReviewContextsByMessageId?.get(
videoReviewCommentRootId={videoReviewPresentation?.commentRootIdsByMessageId.get(
entry.message.id,
)}
videoReviewContext={videoReviewPresentation?.contextsByMessageId.get(
entry.message.id,
)}
/>
Expand Down Expand Up @@ -955,24 +962,26 @@ export function MessageThreadPanel({
);

return (
<AuxiliaryPanel
className="relative"
// The focus drawer animates itself; a second slide here would compound.
enterMotion={!isFocusMode}
footer={threadFooter}
header={
isHuddleTranscript ? undefined : (
<AuxiliaryPanelHeader>{threadHeaderContent}</AuxiliaryPanelHeader>
)
}
isSinglePanelView={isSinglePanelView}
layout={layout}
onClose={onClose}
testId="message-thread-panel"
transparentChrome={transparentChrome}
widthPx={widthPx}
>
{threadScrollRegion}
</AuxiliaryPanel>
<VideoReviewNavigationProvider>
<AuxiliaryPanel
className="relative"
// The focus drawer animates itself; a second slide here would compound.
enterMotion={!isFocusMode}
footer={threadFooter}
header={
isHuddleTranscript ? undefined : (
<AuxiliaryPanelHeader>{threadHeaderContent}</AuxiliaryPanelHeader>
)
}
isSinglePanelView={isSinglePanelView}
layout={layout}
onClose={onClose}
testId="message-thread-panel"
transparentChrome={transparentChrome}
widthPx={widthPx}
>
{threadScrollRegion}
</AuxiliaryPanel>
</VideoReviewNavigationProvider>
);
}
Loading
Loading