mirror of
https://github.com/block/buzz.git
synced 2026-11-05 08:51:10 +00:00
fix(desktop): simplify inbox mention conversation view
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { useAppNavigation } from "@/app/navigation/useAppNavigation";
|
||||
import { useChannelsQuery } from "@/features/channels/hooks";
|
||||
import { useIdentityQuery } from "@/shared/api/hooks";
|
||||
import { HomeScreen } from "@/features/home/ui/HomeScreen";
|
||||
@@ -10,7 +9,6 @@ export const Route = createFileRoute("/")({
|
||||
});
|
||||
|
||||
function HomeRouteComponent() {
|
||||
const { goChannel } = useAppNavigation();
|
||||
const channelsQuery = useChannelsQuery();
|
||||
const identityQuery = useIdentityQuery();
|
||||
const channels = channelsQuery.data ?? [];
|
||||
@@ -20,9 +18,6 @@ function HomeRouteComponent() {
|
||||
<HomeScreen
|
||||
availableChannelIds={availableChannelIds}
|
||||
currentPubkey={identityQuery.data?.pubkey}
|
||||
onOpenChannel={(channelId) => {
|
||||
void goChannel(channelId);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,13 +5,11 @@ import { HomeView } from "@/features/home/ui/HomeView";
|
||||
type HomeScreenProps = {
|
||||
availableChannelIds: ReadonlySet<string>;
|
||||
currentPubkey?: string;
|
||||
onOpenChannel: (channelId: string) => void;
|
||||
};
|
||||
|
||||
export function HomeScreen({
|
||||
availableChannelIds,
|
||||
currentPubkey,
|
||||
onOpenChannel,
|
||||
}: HomeScreenProps) {
|
||||
const homeFeedQuery = useHomeFeedQuery();
|
||||
|
||||
@@ -35,7 +33,6 @@ export function HomeScreen({
|
||||
}
|
||||
feed={homeFeedQuery.data}
|
||||
isLoading={homeFeedQuery.isLoading}
|
||||
onOpenChannel={onOpenChannel}
|
||||
onRefresh={() => {
|
||||
void homeFeedQuery.refetch();
|
||||
}}
|
||||
|
||||
@@ -104,7 +104,6 @@ type HomeViewProps = {
|
||||
errorMessage?: string;
|
||||
currentPubkey?: string;
|
||||
availableChannelIds: ReadonlySet<string>;
|
||||
onOpenChannel: (channelId: string) => void;
|
||||
onRefresh: () => void;
|
||||
};
|
||||
|
||||
@@ -114,7 +113,6 @@ export function HomeView({
|
||||
errorMessage,
|
||||
currentPubkey,
|
||||
availableChannelIds,
|
||||
onOpenChannel,
|
||||
onRefresh,
|
||||
}: HomeViewProps) {
|
||||
const [filter, setFilter] = React.useState<InboxFilter>("all");
|
||||
@@ -371,7 +369,6 @@ export function HomeView({
|
||||
setIsDeletingMessage(false);
|
||||
});
|
||||
}}
|
||||
onOpenChannel={onOpenChannel}
|
||||
onSendReply={async ({
|
||||
content,
|
||||
mediaTags,
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import {
|
||||
ArrowUpRight,
|
||||
CheckCheck,
|
||||
CircleDot,
|
||||
Mail,
|
||||
MailOpen,
|
||||
MoreHorizontal,
|
||||
Reply,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import * as React from "react";
|
||||
@@ -48,7 +45,6 @@ type InboxDetailPaneProps = {
|
||||
messages?: InboxContextMessage[];
|
||||
replies?: InboxReply[];
|
||||
onDelete: () => void;
|
||||
onOpenChannel: (channelId: string) => void;
|
||||
onSendReply: (input: {
|
||||
content: string;
|
||||
mediaTags?: string[][];
|
||||
@@ -93,13 +89,14 @@ export function InboxDetailPane({
|
||||
messages = [],
|
||||
replies = [],
|
||||
onDelete,
|
||||
onOpenChannel,
|
||||
onSendReply,
|
||||
onToggleDone,
|
||||
onToggleReaction,
|
||||
}: InboxDetailPaneProps) {
|
||||
const detailPaneRef = React.useRef<HTMLElement | null>(null);
|
||||
const [replyTargetId, setReplyTargetId] = React.useState<string | null>(null);
|
||||
const [isFocusHighlightVisible, setIsFocusHighlightVisible] =
|
||||
React.useState(true);
|
||||
const selectedItemId = item?.id ?? null;
|
||||
|
||||
const focusComposer = React.useCallback(() => {
|
||||
@@ -117,6 +114,18 @@ export function InboxDetailPane({
|
||||
setReplyTargetId(null);
|
||||
}, [selectedItemId]);
|
||||
|
||||
React.useEffect(() => {
|
||||
void selectedItemId;
|
||||
setIsFocusHighlightVisible(true);
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
setIsFocusHighlightVisible(false);
|
||||
}, 1_200);
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timeoutId);
|
||||
};
|
||||
}, [selectedItemId]);
|
||||
|
||||
if (!item) {
|
||||
return (
|
||||
<section
|
||||
@@ -136,7 +145,6 @@ export function InboxDetailPane({
|
||||
);
|
||||
}
|
||||
|
||||
const channelId = item.item.channelId;
|
||||
const selectedMessage = messages.find((message) => message.isSelected);
|
||||
const pendingReplyMessages: InboxDisplayMessage[] = replies.map((reply) => ({
|
||||
...reply,
|
||||
@@ -160,7 +168,6 @@ export function InboxDetailPane({
|
||||
},
|
||||
...pendingReplyMessages,
|
||||
];
|
||||
const hasConversationContext = displayMessages.length > 1;
|
||||
const replyTarget =
|
||||
displayMessages.find((message) => message.id === replyTargetId) ?? null;
|
||||
const composerParentEventId = replyTarget?.id ?? item.id;
|
||||
@@ -186,129 +193,98 @@ export function InboxDetailPane({
|
||||
data-testid="home-inbox-detail"
|
||||
ref={detailPaneRef}
|
||||
>
|
||||
<div className="px-6 py-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<UserAvatar
|
||||
avatarUrl={item.avatarUrl}
|
||||
className="h-10 w-10 rounded-md"
|
||||
displayName={item.senderLabel}
|
||||
size="md"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<p className="truncate text-base font-semibold">
|
||||
{item.senderLabel}
|
||||
</p>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center text-[10px] font-semibold uppercase tracking-[0.14em]",
|
||||
item.isActionRequired
|
||||
? "text-amber-600 dark:text-amber-300"
|
||||
: "text-primary",
|
||||
)}
|
||||
>
|
||||
{item.categoryLabel}
|
||||
</span>
|
||||
{item.channelLabel ? (
|
||||
<span className="inline-flex items-center text-[11px] font-medium text-muted-foreground">
|
||||
#{item.channelLabel}
|
||||
{!canOpenChannel ? (
|
||||
<div className="px-6 py-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<UserAvatar
|
||||
avatarUrl={item.avatarUrl}
|
||||
className="h-8 w-8 rounded-xl"
|
||||
displayName={item.senderLabel}
|
||||
size="md"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<p className="truncate text-base font-semibold">
|
||||
{item.senderLabel}
|
||||
</p>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center text-[10px] font-semibold uppercase tracking-[0.14em]",
|
||||
item.isActionRequired
|
||||
? "text-amber-600 dark:text-amber-300"
|
||||
: "text-primary",
|
||||
)}
|
||||
>
|
||||
{item.categoryLabel}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{item.channelLabel ? (
|
||||
<span className="inline-flex items-center text-[11px] font-medium text-muted-foreground">
|
||||
#{item.channelLabel}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
|
||||
<span>{item.fullTimestampLabel}</span>
|
||||
{canOpenChannel ? <CircleDot className="h-3.5 w-3.5" /> : null}
|
||||
{canOpenChannel ? (
|
||||
<span>Linked to an active channel</span>
|
||||
) : (
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
|
||||
<span>{item.fullTimestampLabel}</span>
|
||||
<span>Inbox only</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center gap-4">
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<div className="flex items-center gap-4">
|
||||
{canReply ? (
|
||||
<div className="flex shrink-0 items-center gap-4">
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-0.5">
|
||||
<HeaderIconAction
|
||||
label="Reply"
|
||||
onClick={() => {
|
||||
setReplyTargetId(null);
|
||||
focusComposer();
|
||||
}}
|
||||
icon={<Reply className="h-4 w-4" />}
|
||||
label={isDone ? "Mark unread" : "Mark done"}
|
||||
onClick={onToggleDone}
|
||||
icon={
|
||||
isDone ? (
|
||||
<MailOpen className="h-4 w-4" />
|
||||
) : (
|
||||
<CheckCheck className="h-4 w-4" />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex items-center gap-0.5">
|
||||
{canOpenChannel && channelId ? (
|
||||
<HeaderIconAction
|
||||
label="Open channel"
|
||||
onClick={() => onOpenChannel(channelId)}
|
||||
icon={<ArrowUpRight className="h-4 w-4" />}
|
||||
{canDelete ? (
|
||||
<HeaderMoreMenu
|
||||
isDeletingMessage={isDeletingMessage}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
) : null}
|
||||
<HeaderIconAction
|
||||
label={isDone ? "Mark unread" : "Mark done"}
|
||||
onClick={onToggleDone}
|
||||
icon={
|
||||
isDone ? (
|
||||
<MailOpen className="h-4 w-4" />
|
||||
) : (
|
||||
<CheckCheck className="h-4 w-4" />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{canDelete ? (
|
||||
<HeaderMoreMenu
|
||||
isDeletingMessage={isDeletingMessage}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="relative min-h-0 flex-1 overflow-hidden">
|
||||
<div className="absolute inset-0 overflow-y-auto overscroll-contain pb-32 pt-6">
|
||||
<div>
|
||||
<div className="px-6 pb-3 text-[11px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
||||
{hasConversationContext
|
||||
? "Conversation context"
|
||||
: item.categoryLabel}
|
||||
{isThreadContextLoading ? (
|
||||
<span className="ml-2 font-normal normal-case tracking-normal">
|
||||
Loading context...
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{displayMessages.map((message) => (
|
||||
<div className="px-6 py-2" key={message.id}>
|
||||
<div
|
||||
className="relative"
|
||||
style={{
|
||||
marginLeft: `${Math.min(message.depth, 6) * 28}px`,
|
||||
}}
|
||||
>
|
||||
{message.depth > 0 ? (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="absolute bottom-0 top-0 border-l border-border/70"
|
||||
style={{ left: "-14px" }}
|
||||
/>
|
||||
) : null}
|
||||
{isThreadContextLoading ? (
|
||||
<div className="px-6 pb-3 text-[11px] text-muted-foreground">
|
||||
Loading context...
|
||||
</div>
|
||||
) : null}
|
||||
{displayMessages.map((message, index) => (
|
||||
<React.Fragment key={message.id}>
|
||||
{index === 1 ? (
|
||||
<div className="mx-6 my-3 border-t border-border/60" />
|
||||
) : null}
|
||||
<div className="px-6 py-2">
|
||||
<article
|
||||
className={cn(
|
||||
"group/message flex items-start gap-2.5 rounded-xl border-l-2 px-2 py-1 transition-colors",
|
||||
"group/message flex items-start gap-2.5 border-l-2 px-2 py-1 transition-colors duration-1000",
|
||||
message.isSelected
|
||||
? "border-primary/35 bg-muted/25"
|
||||
? cn(
|
||||
"border-primary/35",
|
||||
isFocusHighlightVisible
|
||||
? "bg-primary/[0.07]"
|
||||
: "bg-transparent",
|
||||
)
|
||||
: "border-transparent hover:bg-muted/20",
|
||||
)}
|
||||
data-testid={
|
||||
@@ -383,7 +359,7 @@ export function InboxDetailPane({
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user