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
1 change: 1 addition & 0 deletions desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"emoji-mart": "^5.6.0",
"jdenticon": "^3.3.0",
"lucide-react": "^1.0.0",
"mdast-util-from-markdown": "^2.0.3",
Comment thread
klopez4212 marked this conversation as resolved.
Comment thread
klopez4212 marked this conversation as resolved.
Comment thread
klopez4212 marked this conversation as resolved.
"motion": "^12.38.0",
"qrcode": "^1.5.4",
"qrcode.react": "^4.2.0",
Expand Down
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
152 changes: 119 additions & 33 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,10 @@ 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,
hasRenderedVideoAttachment,
} 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 +53,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 All @@ -64,6 +72,9 @@ const MembersSidebar = React.lazy(async () => {
return { default: module.MembersSidebar };
});

const EMPTY_CONTEXT_MESSAGES: InboxContextMessage[] = [];
const EMPTY_REPLIES: InboxReply[] = [];

type InboxDetailPaneProps = {
agentPubkeys?: ReadonlySet<string>;
canDelete: boolean;
Expand Down Expand Up @@ -142,7 +153,11 @@ export function InboxDetailPane(props: InboxDetailPaneProps) {
);
}

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

function InboxMessageDetailPane({
Expand All @@ -159,9 +174,9 @@ function InboxMessageDetailPane({
hasThreadContextLoadError = false,
isThreadContextLoading = false,
item,
messages = [],
messages = EMPTY_CONTEXT_MESSAGES,
profiles,
replies = [],
replies = EMPTY_REPLIES,
channel,
contextChannelName = null,
currentPubkey,
Expand Down Expand Up @@ -197,7 +212,6 @@ function InboxMessageDetailPane({
// Build the plain, non-virtualized timeline the shared hook anchors against.
// Live arrivals rerun its layout compensation without changing the target.

const selectedMessage = messages.find((message) => message.isSelected);
// A latest reply can represent an Inbox conversation. Resolve the actual
// root from loaded context or the complete feed group; never treat an
// unresolved root/profile lookup as an authoritative empty audience.
Expand Down Expand Up @@ -232,34 +246,100 @@ function InboxMessageDetailPane({
)
: []
: undefined;
const pendingReplyMessages: InboxDisplayMessage[] = replies.map((reply) => ({
...reply,
depth: reply.depth ?? (selectedMessage?.depth ?? 0) + 1,
isSelected: false,
mentionNames: [],
}));
const displayMessages: InboxDisplayMessage[] =
messages.length > 0
? [...messages, ...pendingReplyMessages]
: item
? [
{
authorLabel: item.senderLabel,
authorPubkey: item.item.pubkey,
avatarUrl: item.avatarUrl,
content: item.preview,
createdAt: item.item.createdAt,
depth: 0,
fullTimestampLabel: item.fullTimestampLabel,
id: item.id,
isSelected: true,
mentionNames: item.mentionNames,
mentionPubkeysByName: item.mentionPubkeysByName,
timeLabel: formatTime(item.item.createdAt),
},
...pendingReplyMessages,
]
: pendingReplyMessages;
const displayMessages = React.useMemo<InboxDisplayMessage[]>(() => {
const selectedMessage = messages.find((message) => message.isSelected);
const pendingReplyMessages: InboxDisplayMessage[] = replies.map(
(reply) => ({
...reply,
depth: reply.depth ?? (selectedMessage?.depth ?? 0) + 1,
isSelected: false,
mentionNames: [],
}),
);

if (messages.length > 0) {
return [...messages, ...pendingReplyMessages];
}
if (!item) return pendingReplyMessages;

const threadReference = getThreadReference(item.item.tags);
return [
{
authorLabel: item.senderLabel,
authorPubkey: item.item.pubkey,
avatarUrl: item.avatarUrl,
content: item.preview,
createdAt: item.item.createdAt,
depth: 0,
fullTimestampLabel: item.fullTimestampLabel,
id: item.id,
isSelected: true,
mentionNames: item.mentionNames,
mentionPubkeysByName: item.mentionPubkeysByName,
kind: item.item.kind,
parentId: threadReference.parentId,
rootId: threadReference.rootId,
tags: item.item.tags,
timeLabel: formatTime(item.item.createdAt),
},
...pendingReplyMessages,
];
}, [item, messages, replies]);
const videoReviewMessages = React.useMemo(
() => displayMessages.map(toTimelineMessage),
[displayMessages],
);
const videoReviewChannelType =
item?.item.channelType === "dm" ||
item?.item.channelType === "stream" ||
item?.item.channelType === "forum"
? item.item.channelType
: null;
const handleSendVideoReviewComment = React.useCallback(
(
message: TimelineMessage,
content: string,
mentionPubkeys: string[],
mediaTags?: string[][],
parentEventId?: string,
) =>
onSendReply({
content,
mediaTags,
mentionPubkeys,
parentEventId: parentEventId ?? message.id,
}),
[onSendReply],
);
const videoReviewPresentation = React.useMemo(
() =>
buildVideoReviewPresentationByMessageId(
{
channelId: item?.item.channelId,
channelName: contextChannelName ?? item?.channelLabel ?? undefined,
channelType: videoReviewChannelType,
isSendingVideoReviewComment: isSendingReply,
messages: videoReviewMessages,
onSendVideoReviewComment: canReply
? handleSendVideoReviewComment
: undefined,
onToggleReaction,
profiles,
},
hasRenderedVideoAttachment,
),
[
canReply,
contextChannelName,
handleSendVideoReviewComment,
isSendingReply,
item,
onToggleReaction,
profiles,
videoReviewChannelType,
videoReviewMessages,
],
);
const { onScroll } = useAnchoredScroll({
channelId: conversationId,
contentRef,
Expand Down Expand Up @@ -667,6 +747,12 @@ function InboxMessageDetailPane({
onSelectReplyTarget={handleSelectReplyTarget}
onToggleReaction={onToggleReaction}
showUnreadBoundary={hasUnreadBoundary}
videoReviewCommentRootId={videoReviewPresentation.commentRootIdsByMessageId.get(
message.id,
)}
videoReviewContext={videoReviewPresentation.contextsByMessageId.get(
message.id,
)}
/>
);
})}
Expand Down
36 changes: 34 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 { hasRenderedVideoAttachment } 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,7 @@ import {
ContextMenuSeparator,
ContextMenuTrigger,
} from "@/shared/ui/context-menu";
import { Markdown } from "@/shared/ui/markdown";
import { VideoReviewCommentMarkdown } from "@/shared/ui/VideoReviewCommentMarkdown";
import {
MENTION_CHIP_BASE_CLASSES,
MESSAGE_MARKDOWN_CLASS,
Expand Down Expand Up @@ -121,6 +123,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) =>
hasRenderedVideoAttachment({
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 undefined;
}

function PersonalItemRow({
id,
location,
Expand Down Expand Up @@ -274,6 +304,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 +439,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
17 changes: 15 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,11 @@ 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";
import { parseImetaTags } from "@/shared/ui/markdown/parseImeta";

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

export function InboxMessageRow({
Expand All @@ -54,11 +58,17 @@ export function InboxMessageRow({
onSelectReplyTarget,
onToggleReaction,
showUnreadBoundary = false,
videoReviewCommentRootId,
videoReviewContext,
}: InboxMessageRowProps) {
const timelineMessage = React.useMemo(
() => toTimelineMessage(message),
[message],
);
const imetaByUrl = React.useMemo(
() => (message.tags ? parseImetaTags(message.tags) : undefined),
[message.tags],
);
const { customEmoji, emojiOnly } = useMessageEmoji(
message.content,
message.tags,
Expand Down Expand Up @@ -201,7 +211,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 @@ -221,8 +231,11 @@ export function InboxMessageRow({
timelineMessage.tags,
)}
customEmoji={customEmoji}
imetaByUrl={imetaByUrl}
mentionNames={message.mentionNames}
mentionPubkeysByName={message.mentionPubkeysByName}
videoReviewCommentRootId={videoReviewCommentRootId}
videoReviewContext={videoReviewContext}
Comment thread
klopez4212 marked this conversation as resolved.
/>
<MessageReactions
canToggle={canToggleReactions}
Expand Down
Loading