fix(ui): clickable unread dot in inbox with fade-out, remove empty circle for read issues
In the My Recent Issues section, the blue unread dot is now a button that marks the issue as read on click with a smooth opacity fade-out. Already-read issues show empty space instead of a hollow circle. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -498,6 +498,31 @@ export function Inbox() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [fadingOutIssues, setFadingOutIssues] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
const markReadMutation = useMutation({
|
||||||
|
mutationFn: (id: string) => issuesApi.markRead(id),
|
||||||
|
onMutate: (id) => {
|
||||||
|
setFadingOutIssues((prev) => new Set(prev).add(id));
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
if (selectedCompanyId) {
|
||||||
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.listTouchedByMe(selectedCompanyId) });
|
||||||
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.listUnreadTouchedByMe(selectedCompanyId) });
|
||||||
|
queryClient.invalidateQueries({ queryKey: queryKeys.sidebarBadges(selectedCompanyId) });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSettled: (_data, _error, id) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setFadingOutIssues((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.delete(id);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, 300);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (!selectedCompanyId) {
|
if (!selectedCompanyId) {
|
||||||
return <EmptyState icon={InboxIcon} message="Select a company to view inbox." />;
|
return <EmptyState icon={InboxIcon} message="Select a company to view inbox." />;
|
||||||
}
|
}
|
||||||
@@ -867,35 +892,53 @@ export function Inbox() {
|
|||||||
My Recent Issues
|
My Recent Issues
|
||||||
</h3>
|
</h3>
|
||||||
<div className="divide-y divide-border border border-border">
|
<div className="divide-y divide-border border border-border">
|
||||||
{touchedIssues.map((issue) => (
|
{touchedIssues.map((issue) => {
|
||||||
<Link
|
const isUnread = issue.isUnreadForMe && !fadingOutIssues.has(issue.id);
|
||||||
key={issue.id}
|
const isFading = fadingOutIssues.has(issue.id);
|
||||||
to={`/issues/${issue.identifier ?? issue.id}`}
|
return (
|
||||||
className="flex cursor-pointer items-center gap-3 px-4 py-3 transition-colors hover:bg-accent/50 no-underline text-inherit"
|
<div
|
||||||
>
|
key={issue.id}
|
||||||
<span className="flex w-4 shrink-0 justify-center">
|
className="flex items-center gap-3 px-4 py-3 transition-colors hover:bg-accent/50"
|
||||||
<span
|
>
|
||||||
className={`h-2.5 w-2.5 rounded-full ${
|
<span className="flex w-4 shrink-0 justify-center">
|
||||||
issue.isUnreadForMe
|
{(isUnread || isFading) && (
|
||||||
? "bg-blue-600 dark:bg-blue-400"
|
<button
|
||||||
: "border border-muted-foreground/40 bg-transparent"
|
type="button"
|
||||||
}`}
|
onClick={(e) => {
|
||||||
aria-label={issue.isUnreadForMe ? "Unread" : "Read"}
|
e.preventDefault();
|
||||||
/>
|
e.stopPropagation();
|
||||||
</span>
|
markReadMutation.mutate(issue.id);
|
||||||
<PriorityIcon priority={issue.priority} />
|
}}
|
||||||
<StatusIcon status={issue.status} />
|
className="group/dot flex h-4 w-4 items-center justify-center rounded-full transition-colors hover:bg-blue-500/20"
|
||||||
<span className="text-xs font-mono text-muted-foreground">
|
aria-label="Mark as read"
|
||||||
{issue.identifier ?? issue.id.slice(0, 8)}
|
>
|
||||||
</span>
|
<span
|
||||||
<span className="flex-1 truncate text-sm">{issue.title}</span>
|
className={`h-2.5 w-2.5 rounded-full bg-blue-600 dark:bg-blue-400 transition-opacity duration-300 ${
|
||||||
<span className="shrink-0 text-xs text-muted-foreground">
|
isFading ? "opacity-0" : "opacity-100"
|
||||||
{issue.lastExternalCommentAt
|
}`}
|
||||||
? `commented ${timeAgo(issue.lastExternalCommentAt)}`
|
/>
|
||||||
: `updated ${timeAgo(issue.updatedAt)}`}
|
</button>
|
||||||
</span>
|
)}
|
||||||
</Link>
|
</span>
|
||||||
))}
|
<Link
|
||||||
|
to={`/issues/${issue.identifier ?? issue.id}`}
|
||||||
|
className="flex flex-1 cursor-pointer items-center gap-3 no-underline text-inherit"
|
||||||
|
>
|
||||||
|
<PriorityIcon priority={issue.priority} />
|
||||||
|
<StatusIcon status={issue.status} />
|
||||||
|
<span className="text-xs font-mono text-muted-foreground">
|
||||||
|
{issue.identifier ?? issue.id.slice(0, 8)}
|
||||||
|
</span>
|
||||||
|
<span className="flex-1 truncate text-sm">{issue.title}</span>
|
||||||
|
<span className="shrink-0 text-xs text-muted-foreground">
|
||||||
|
{issue.lastExternalCommentAt
|
||||||
|
? `commented ${timeAgo(issue.lastExternalCommentAt)}`
|
||||||
|
: `updated ${timeAgo(issue.updatedAt)}`}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user