Skip to content
Merged
3 changes: 3 additions & 0 deletions desktop/src/features/home/lib/inboxViewHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ export function toInboxContextMessage(
export function toTimelineMessage(
message: InboxContextMessage,
): TimelineMessage {
const threadReference = getThreadReference(message.tags ?? []);
return {
id: message.id,
author: message.authorLabel,
Expand All @@ -239,8 +240,10 @@ export function toTimelineMessage(
createdAt: message.createdAt,
depth: message.depth,
kind: message.kind,
parentId: message.parentId ?? threadReference.parentId,
pubkey: message.authorPubkey,
reactions: message.reactions ?? [],
rootId: message.rootId ?? threadReference.rootId,
signerPubkey: message.signerPubkey,
tags: message.tags,
time: message.timeLabel ?? message.fullTimestampLabel,
Expand Down
47 changes: 45 additions & 2 deletions desktop/src/features/home/ui/InboxDetailPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ import { ProjectInboxDetail } from "@/features/home/ui/ProjectInboxDetail";
import { ChannelMembersBar } from "@/features/channels/ui/ChannelMembersBar";
import { useCommunities } from "@/features/communities/useCommunities";
import { formatInboxTypeLabel } from "@/features/home/lib/inbox";
import { hasInboxThreadContext } from "@/features/home/lib/inboxViewHelpers";
import {
hasInboxThreadContext,
toTimelineMessage,
} from "@/features/home/lib/inboxViewHelpers";
import {
type InboxDisplayMessage,
InboxMessageRow,
Expand All @@ -35,6 +38,7 @@ import { orderMentionPubkeysByText } from "@/features/messages/lib/orderMentionP
import { canManageMessageForCurrentUser } from "@/features/messages/lib/canManageMessage";
import { buildEditMentionState } from "@/features/messages/lib/draftMentionRefs";
import { imetaMediaFromTags } from "@/features/messages/lib/imetaMediaMarkdown";
import { buildVideoReviewPresentationByMessageId } from "@/features/messages/lib/videoReviewContext";
import { getThreadReference } from "@/features/messages/lib/threading";
import { normalizePubkey } from "@/shared/lib/pubkey";
import { MessageComposer } from "@/features/messages/ui/MessageComposer";
Expand All @@ -46,6 +50,7 @@ import { resolveMentionProps } from "@/shared/lib/resolveMentionNames";
import { TopChromeInsetHeader } from "@/shared/layout/TopChromeInsetHeader";
import { cn } from "@/shared/lib/cn";
import { Button } from "@/shared/ui/button";
import { VideoReviewNavigationProvider } from "@/shared/ui/VideoReviewNavigation";
import {
DropdownMenu,
DropdownMenuContent,
Expand Down Expand Up @@ -142,7 +147,11 @@ export function InboxDetailPane(props: InboxDetailPaneProps) {
);
}

return <InboxMessageDetailPane {...props} />;
return (
<VideoReviewNavigationProvider>
<InboxMessageDetailPane {...props} />
</VideoReviewNavigationProvider>
);
}

function InboxMessageDetailPane({
Expand Down Expand Up @@ -255,11 +264,39 @@ function InboxMessageDetailPane({
isSelected: true,
mentionNames: item.mentionNames,
mentionPubkeysByName: item.mentionPubkeysByName,
kind: item.item.kind,
parentId: getThreadReference(item.item.tags).parentId,
rootId: getThreadReference(item.item.tags).rootId,
tags: item.item.tags,
timeLabel: formatTime(item.item.createdAt),
},
...pendingReplyMessages,
]
: pendingReplyMessages;
const videoReviewChannelType =
item?.item.channelType === "dm" ||
item?.item.channelType === "stream" ||
item?.item.channelType === "forum"
? item.item.channelType
: null;
const videoReviewPresentation = buildVideoReviewPresentationByMessageId({
channelId: item?.item.channelId,
channelName: contextChannelName ?? item?.channelLabel ?? undefined,
channelType: videoReviewChannelType,
isSendingVideoReviewComment: isSendingReply,
messages: displayMessages.map(toTimelineMessage),
Comment thread
klopez4212 marked this conversation as resolved.
Outdated
onSendVideoReviewComment: canReply
? (message, content, mentionPubkeys, mediaTags, parentEventId) =>
onSendReply({
content,
mediaTags,
mentionPubkeys,
parentEventId: parentEventId ?? message.id,
})
: undefined,
onToggleReaction,
profiles,
});
const { onScroll } = useAnchoredScroll({
channelId: conversationId,
contentRef,
Expand Down Expand Up @@ -667,6 +704,12 @@ function InboxMessageDetailPane({
onSelectReplyTarget={handleSelectReplyTarget}
onToggleReaction={onToggleReaction}
showUnreadBoundary={hasUnreadBoundary}
videoReviewCommentRootId={videoReviewPresentation.commentRootIdsByMessageId.get(
message.id,
)}
videoReviewContext={videoReviewPresentation.contextsByMessageId.get(
message.id,
)}
/>
);
})}
Expand Down
37 changes: 35 additions & 2 deletions desktop/src/features/home/ui/InboxListPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
type InboxTypeLabel,
} from "@/features/home/lib/inbox";
import { buildInboxListRows } from "@/features/home/lib/inboxListRows";
import { hasVideoAttachment } from "@/features/messages/lib/videoReviewContext";
import { getThreadReference } from "@/features/messages/lib/threading";
import { InboxFilterMenu } from "@/features/home/ui/InboxFilterMenu";
import {
DraftsPanel,
Expand All @@ -30,7 +32,8 @@ import {
ContextMenuSeparator,
ContextMenuTrigger,
} from "@/shared/ui/context-menu";
import { Markdown } from "@/shared/ui/markdown";
import { VideoReviewCommentMarkdown } from "@/shared/ui/VideoReviewCommentMarkdown";
import { parseVideoReviewTimecode } from "@/shared/ui/videoReviewTimecode";
import {
MENTION_CHIP_BASE_CLASSES,
MESSAGE_MARKDOWN_CLASS,
Expand Down Expand Up @@ -121,6 +124,34 @@ function formatReminderStatus(notBefore: number | undefined) {
return `Reminder in ${Math.floor(secondsUntil / 86_400)}d`;
}

function getInboxVideoReviewCommentRootId(item: InboxItem) {
const feedItems = [item.item, ...item.groupItems];
const feedItemById = new Map(
feedItems.map((feedItem) => [feedItem.id, feedItem]),
);
const videoMessageIds = new Set(
feedItems
.filter((feedItem) =>
hasVideoAttachment({ body: feedItem.content, tags: feedItem.tags }),
)
Comment thread
klopez4212 marked this conversation as resolved.
Comment thread
klopez4212 marked this conversation as resolved.
Comment thread
klopez4212 marked this conversation as resolved.
.map((feedItem) => feedItem.id),
);
const visited = new Set<string>();
let ancestorId = getThreadReference(item.item.tags).parentId;

while (ancestorId && !visited.has(ancestorId)) {
if (videoMessageIds.has(ancestorId)) return ancestorId;
visited.add(ancestorId);
const ancestor = feedItemById.get(ancestorId);
ancestorId = ancestor ? getThreadReference(ancestor.tags).parentId : null;
}

return parseVideoReviewTimecode(item.preview) &&
getThreadReference(item.item.tags).parentId
? item.conversationId
: undefined;
Comment thread
klopez4212 marked this conversation as resolved.
Outdated
}

function PersonalItemRow({
id,
location,
Expand Down Expand Up @@ -274,6 +305,7 @@ export function InboxListPane({
);
const hasChannelTarget = Boolean(item.item.channelId);
const typeLabel = getInboxTypeLabel(item);
const videoReviewCommentRootId = getInboxVideoReviewCommentRootId(item);
const isSenderAgent =
agentPubkeys?.has(normalizePubkey(item.item.pubkey)) === true;
const profileRole = isSenderAgent ? "bot" : undefined;
Expand Down Expand Up @@ -408,11 +440,12 @@ export function InboxListPane({
: "font-semibold text-foreground",
)}
>
<Markdown
<VideoReviewCommentMarkdown
className="inbox-preview-markdown text-inherit leading-5"
content={item.preview}
interactive={false}
mentionNames={item.mentionNames}
videoReviewCommentRootId={videoReviewCommentRootId}
/>
</div>
</div>
Expand Down
11 changes: 9 additions & 2 deletions desktop/src/features/home/ui/InboxMessageRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ import { useMessageEmoji } from "@/features/messages/lib/useMessageEmoji";
import { UserProfilePopover } from "@/features/profile/ui/UserProfilePopover";
import { cn } from "@/shared/lib/cn";
import { normalizePubkey } from "@/shared/lib/pubkey";
import { Markdown } from "@/shared/ui/markdown";
import { hasLinkPreviewSuppression } from "@/features/messages/lib/formatTimelineMessages";
import { UserAvatar } from "@/shared/ui/UserAvatar";
import type { VideoReviewContext } from "@/shared/ui/VideoPlayer";
import { VideoReviewCommentMarkdown } from "@/shared/ui/VideoReviewCommentMarkdown";

export type InboxDisplayMessage = InboxContextMessage & {
depth: number;
Expand All @@ -40,6 +41,8 @@ type InboxMessageRowProps = {
remove: boolean,
) => Promise<void>;
showUnreadBoundary?: boolean;
videoReviewCommentRootId?: string;
videoReviewContext?: VideoReviewContext;
};

export function InboxMessageRow({
Expand All @@ -54,6 +57,8 @@ export function InboxMessageRow({
onSelectReplyTarget,
onToggleReaction,
showUnreadBoundary = false,
videoReviewCommentRootId,
videoReviewContext,
}: InboxMessageRowProps) {
const timelineMessage = React.useMemo(
() => toTimelineMessage(message),
Expand Down Expand Up @@ -201,7 +206,7 @@ export function InboxMessageRow({
)}

<div className={isContinuation ? "mt-0" : "mt-0.5"}>
<Markdown
<VideoReviewCommentMarkdown
className={cn(
"max-w-full text-left text-sm text-foreground",
emojiOnly &&
Expand All @@ -223,6 +228,8 @@ export function InboxMessageRow({
customEmoji={customEmoji}
mentionNames={message.mentionNames}
mentionPubkeysByName={message.mentionPubkeysByName}
videoReviewCommentRootId={videoReviewCommentRootId}
videoReviewContext={videoReviewContext}
Comment thread
klopez4212 marked this conversation as resolved.
/>
<MessageReactions
canToggle={canToggleReactions}
Expand Down
4 changes: 3 additions & 1 deletion desktop/src/features/messages/lib/videoReviewContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ type ToggleMessageReaction = (
remove: boolean,
) => Promise<void>;

export function hasVideoAttachment(message: TimelineMessage): boolean {
export function hasVideoAttachment(
message: Pick<TimelineMessage, "body" | "tags">,
): boolean {
if (message.body.includes("![video](")) return true;

return (
Expand Down
34 changes: 5 additions & 29 deletions desktop/src/features/messages/ui/MessageRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,8 @@ import { useMessageEmoji } from "@/features/messages/lib/useMessageEmoji";
import { parseWaveMessageContent } from "@/features/messages/lib/waveMessage";
import { resolveSnapshotSharedBy } from "@/features/messages/lib/snapshotSharedBy";
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 { VideoReviewCommentMarkdown } from "@/shared/ui/VideoReviewCommentMarkdown";
import { MessageActionBar } from "./MessageActionBar";
import { editMessage } from "@/shared/api/tauri";
import { hasLinkPreviewSuppression } from "@/features/messages/lib/formatTimelineMessages";
Expand Down Expand Up @@ -297,7 +294,6 @@ 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 @@ -407,12 +403,8 @@ export const MessageRow = React.memo(
);
}

const reviewRootEventId = videoReviewCommentRootId;
const reviewTimecode = reviewRootEventId
? parseVideoReviewTimecode(message.body)
: null;
const markdown = (
<Markdown
return (
<VideoReviewCommentMarkdown
channelNames={channelNames}
className={cn(
"max-w-full text-sm",
Expand All @@ -427,7 +419,7 @@ export const MessageRow = React.memo(
message,
isKnownAgentPubkey,
)}
content={reviewTimecode?.text ?? message.body}
content={message.body}
messageId={message.id}
linkPreviewsSuppressed={linkPreviewsSuppressed}
linkPreviewTags={message.tags}
Expand All @@ -439,26 +431,10 @@ export const MessageRow = React.memo(
mentionPubkeysByName={mentionPubkeysByName}
searchQuery={searchQuery}
snapshotSharedBy={snapshotSharedBy}
videoReviewCommentRootId={videoReviewCommentRootId}
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
72 changes: 72 additions & 0 deletions desktop/src/shared/ui/VideoReviewCommentMarkdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import * as React from "react";

import { Markdown } from "@/shared/ui/markdown";
import type { MarkdownProps } from "@/shared/ui/markdown/types";
import { useOpenVideoReviewAt } from "@/shared/ui/VideoReviewNavigation";
import { parseVideoReviewTimecode } from "@/shared/ui/videoReviewTimecode";
import {
VideoReviewTimecodeButton,
VideoReviewTimecodeChip,
} from "@/shared/ui/VideoReviewTimecodeButton";

type VideoReviewCommentMarkdownProps = Omit<
MarkdownProps,
"leadingInlineContent"
> & {
videoReviewCommentRootId?: string;
};

/** Renders a video-review timecode inside the comment's first Markdown line. */
export function VideoReviewCommentMarkdown({
content,
interactive = true,
videoReviewCommentRootId,
...markdownProps
}: VideoReviewCommentMarkdownProps) {
const openVideoReviewAt = useOpenVideoReviewAt();
const reviewTimecode = videoReviewCommentRootId
? parseVideoReviewTimecode(content)
: null;
const handleTimecodeClick = React.useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
if (reviewTimecode && videoReviewCommentRootId) {
openVideoReviewAt?.(videoReviewCommentRootId, reviewTimecode.seconds);
}
},
[openVideoReviewAt, reviewTimecode, videoReviewCommentRootId],
);

if (!reviewTimecode) {
return (
<Markdown
{...markdownProps}
content={content}
interactive={interactive}
/>
);
}

const timecode =
interactive && openVideoReviewAt ? (
<VideoReviewTimecodeButton
surface="message"
timecode={reviewTimecode.timecode}
onClick={handleTimecodeClick}
/>
) : (
<VideoReviewTimecodeChip
surface="message"
timecode={reviewTimecode.timecode}
/>
);

return (
<Markdown
{...markdownProps}
content={reviewTimecode.text || "\u200B"}
interactive={interactive}
leadingInlineContent={<>{timecode} </>}
Comment thread
klopez4212 marked this conversation as resolved.
Outdated
/>
);
}
Loading
Loading