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
8 changes: 0 additions & 8 deletions desktop/src/app/AppShell.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ export type AppView =
| "home"
| "channel"
| "agents"
| "reminders"
| "workflows"
| "pulse"
| "projects";
Expand Down Expand Up @@ -86,13 +85,6 @@ export function deriveShellRoute(pathname: string): {
};
}

if (pathname === "/reminders") {
return {
selectedChannelId: null,
selectedView: "reminders",
};
}

return {
selectedChannelId: null,
selectedView: "home",
Expand Down
9 changes: 6 additions & 3 deletions desktop/src/app/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import {
} from "@/features/settings/ui/SettingsPanels";
import { HuddleBar, HuddleProvider } from "@/features/huddle";
import { RemindMeLaterProvider } from "@/features/reminders/ui/RemindMeLaterProvider";
import { useReminderNotifications } from "@/features/reminders/useReminderNotifications";
import { AppSidebar } from "@/features/sidebar/ui/AppSidebar";
import { useChannelMutes } from "@/features/sidebar/lib/useChannelMutes";
import { useChannelStars } from "@/features/sidebar/lib/useChannelStars";
Expand Down Expand Up @@ -115,7 +116,6 @@ export function AppShell() {
goChannel,
goHome,
goProjects,
goReminders,
goPulse,
goSettings,
goWorkflows,
Expand Down Expand Up @@ -163,6 +163,10 @@ export function AppShell() {
const setUserStatusMutation = useSetUserStatusMutation(deferredPubkey);
const { feedProfilesQuery, homeFeedQuery, notificationSettings } =
useHomeFeedNotifications(identityQuery.data?.pubkey);
useReminderNotifications(
identityQuery.data?.pubkey,
notificationSettings.settings,
);
const refetchHomeFeedOnLiveMention = React.useEffectEvent(() => {
void homeFeedQuery.refetch();
});
Expand Down Expand Up @@ -714,7 +718,7 @@ export function AppShell() {
}}
>
<HuddleProvider>
<RemindMeLaterProvider>
<RemindMeLaterProvider pubkey={identityQuery.data?.pubkey}>
<div
className="buzz-huddle-shell relative h-dvh overflow-hidden overscroll-none"
data-huddle-open={isHuddleDrawerOpen}
Expand Down Expand Up @@ -879,7 +883,6 @@ export function AppShell() {
onSelectHome={() => void goHome()}
onSelectProjects={() => void goProjects()}
onSelectPulse={() => void goPulse()}
onSelectReminders={() => void goReminders()}
onSelectSettings={handleOpenSettings}
onSelectWorkflows={() => void goWorkflows()}
onSetPresenceStatus={(status) =>
Expand Down
12 changes: 0 additions & 12 deletions desktop/src/app/navigation/useAppNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,6 @@ export function useAppNavigation() {
[commitNavigation],
);

const goReminders = React.useCallback(
(behavior?: NavigationBehavior) =>
commitNavigation(
{
to: "/reminders",
},
behavior,
),
[commitNavigation],
);

const goProject = React.useCallback(
(projectId: string, behavior?: NavigationBehavior) =>
commitNavigation(
Expand Down Expand Up @@ -272,7 +261,6 @@ export function useAppNavigation() {
goProject,
goProjects,
goPulse,
goReminders,
goSettings,
goWorkflow,
goWorkflows,
Expand Down
24 changes: 8 additions & 16 deletions desktop/src/app/routes/reminders.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
import * as React from "react";
import { createFileRoute } from "@tanstack/react-router";

const RemindersScreen = React.lazy(async () => {
const module = await import("@/features/reminders/ui/RemindersScreen");
return { default: module.RemindersScreen };
});
import { createFileRoute, redirect } from "@tanstack/react-router";

// Reminders is now a filter option inside the inbox dropdown, selected via
// local state rather than the URL. This redirect preserves existing history
// entries and bookmarks pointing at `/reminders` so they land in the inbox
// instead of dead-ending; the user re-selects Reminders from the filter.
export const Route = createFileRoute("/reminders")({
component: RemindersRouteComponent,
beforeLoad: () => {
throw redirect({ to: "/" });
},
});

function RemindersRouteComponent() {
return (
<React.Suspense fallback={null}>
<RemindersScreen />
</React.Suspense>
);
}
3 changes: 2 additions & 1 deletion desktop/src/features/home/lib/inbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export type InboxFilter =
| "mention"
| "needs_action"
| "activity"
| "agent_activity";
| "agent_activity"
| "reminders";

export type InboxItem = {
avatarUrl: string | null;
Expand Down
30 changes: 28 additions & 2 deletions desktop/src/features/home/ui/HomeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ import {
import { splitOutgoingTags } from "@/features/messages/lib/imetaMediaMarkdown";
import { useUsersBatchQuery } from "@/features/profile/hooks";
import { resolveUserLabel } from "@/features/profile/lib/identity";
import {
countDueReminders,
useRemindersQuery,
} from "@/features/reminders/hooks";
import { deleteMessage, sendChannelMessage } from "@/shared/api/tauri";
import type { HomeFeedResponse } from "@/shared/api/types";
import { KIND_REACTION } from "@/shared/constants/kinds";
Expand Down Expand Up @@ -81,7 +85,14 @@ export function HomeView({
// background data loads must never trigger navigations.
const { applyPatch: applyInboxSearchPatch, values: inboxSearchValues } =
useHistorySearchState(INBOX_SEARCH_KEYS);
const urlSelectedItemId = inboxSearchValues.item;
const isReminders = filter === "reminders";
const isMessagesMode = !isReminders;
const remindersQuery = useRemindersQuery(currentPubkey);
const dueReminderCount = countDueReminders(remindersQuery.data ?? []);
// `?item=` is Messages-mode-only machinery: a reminder never enters the
// FeedItem selection model, so reload while in Reminders mode keeps a stale
// `?item=` unconsumed and does not snap back to a feed-item detail view.
const urlSelectedItemId = isMessagesMode ? inboxSearchValues.item : null;
const [selectedItemId, setSelectedItemId] = React.useState<string | null>(
urlSelectedItemId,
);
Expand Down Expand Up @@ -251,6 +262,14 @@ export function HomeView({
return localReplies.filter((reply) => !contextIds.has(reply.id));
}, [contextMessages, localRepliesByItemId, selectedItem]);
React.useEffect(() => {
// Auto-selection is Messages-mode-only: in Reminders mode no FeedItem is
// ever selected, so default-selecting one behind the reminders list would
// be wasted work and could drive narrow-viewport detail off a stale feed
// selection.
if (!isMessagesMode) {
return;
}

// While the feed is loading (e.g. a reload restoring `?item=` from the
// URL) the selected item simply hasn't arrived yet — don't clobber it.
if (isLoading || !feed) {
Expand Down Expand Up @@ -278,6 +297,7 @@ export function HomeView({
filteredItems,
homeInboxWidthPx,
isLoading,
isMessagesMode,
isNarrowHomeViewport,
selectedItemId,
]);
Expand Down Expand Up @@ -347,8 +367,12 @@ export function HomeView({
selectedItem.item.pubkey.trim().toLowerCase();
const isSinglePanelDetailView =
isNarrowHomeViewport && selectedItemId !== null;
// Reminders mode is single-pane: the reminders list renders inline row
// actions and never drives the FeedItem detail pane, so the detail column is
// not rendered at all (no empty pane on wide viewports).
const showListPane = !isSinglePanelDetailView;
const showDetailPane = !isNarrowHomeViewport || isSinglePanelDetailView;
const showDetailPane =
isMessagesMode && (!isNarrowHomeViewport || isSinglePanelDetailView);
const maxEffectiveInboxListWidthPx =
homeInboxWidthPx > 0
? Math.max(
Expand Down Expand Up @@ -384,13 +408,15 @@ export function HomeView({
{showListPane ? (
<InboxListPane
doneSet={effectiveDoneSet}
dueReminderCount={dueReminderCount}
filter={filter}
items={filteredItems}
onFilterChange={setFilter}
onSelect={(itemId) => {
handleUserSelectItem(itemId);
markItemRead(itemId);
}}
reminderPubkey={currentPubkey}
selectedId={selectedItemId}
showRightDivider={showListPane && showDetailPane}
/>
Expand Down
102 changes: 68 additions & 34 deletions desktop/src/features/home/ui/InboxListPane.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { ChevronDown, Inbox } from "lucide-react";
import { ChevronDown } from "lucide-react";
import * as React from "react";

import {
formatInboxTypeLabel,
type InboxFilter,
type InboxItem,
} from "@/features/home/lib/inbox";
import { RemindersPanel } from "@/features/reminders/ui/RemindersPanel";
import { topChromeInset } from "@/shared/layout/chromeLayout";
import { TopChromeInsetHeader } from "@/shared/layout/TopChromeInsetHeader";
import { cn } from "@/shared/lib/cn";
Expand All @@ -27,6 +28,7 @@ const FILTER_OPTIONS: Array<{ label: string; value: InboxFilter }> = [
{ value: "needs_action", label: "Needs Action" },
{ value: "activity", label: "Activity" },
{ value: "agent_activity", label: "Agents" },
{ value: "reminders", label: "Reminders" },
];

type InboxListPaneProps = {
Expand All @@ -37,6 +39,8 @@ type InboxListPaneProps = {
onSelect: (itemId: string) => void;
selectedId: string | null;
showRightDivider?: boolean;
dueReminderCount: number;
reminderPubkey?: string;
};

export function InboxListPane({
Expand All @@ -47,8 +51,11 @@ export function InboxListPane({
onSelect,
selectedId,
showRightDivider = false,
dueReminderCount,
reminderPubkey,
}: InboxListPaneProps) {
const activeFilter = FILTER_OPTIONS.find((option) => option.value === filter);
const isReminders = filter === "reminders";
const scrollRef = React.useRef<HTMLDivElement>(null);

const renderItem = (item: InboxItem) => {
Expand Down Expand Up @@ -144,22 +151,27 @@ export function InboxListPane({
>
<TopChromeInsetHeader>
<div className="px-5 py-1">
<div className="flex min-w-0 items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-[6px]">
<Inbox className="h-4 w-4 shrink-0 text-muted-foreground" />
<h2 className="translate-y-px truncate text-sm font-semibold leading-5 tracking-tight">
Inbox
</h2>
</div>
{/* Cap to the list-column width so the right-aligned dropdown stays
put when the pane goes full-width in reminders mode. */}
<div className="flex min-w-0 max-w-[var(--home-inbox-list-width)] items-center justify-end gap-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
className="inline-flex shrink-0 items-center gap-1.5 rounded-full border-border/70 bg-background/70 px-2.5 text-2xs font-medium leading-none text-muted-foreground shadow-xs backdrop-blur-sm hover:bg-muted/60 hover:text-foreground"
data-testid="inbox-filter-trigger"
size="sm"
type="button"
variant="outline"
>
<span>{activeFilter?.label ?? "All"}</span>
{dueReminderCount > 0 ? (
<span
className="inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 text-2xs font-semibold leading-none text-primary-foreground"
data-testid="inbox-reminder-badge"
>
{dueReminderCount}
</span>
) : null}
<ChevronDown className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
Expand All @@ -175,7 +187,18 @@ export function InboxListPane({
key={option.value}
value={option.value}
>
{option.label}
<span className="flex flex-1 items-center justify-between gap-2">
{option.label}
{option.value === "reminders" &&
dueReminderCount > 0 ? (
<span
className="inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 text-2xs font-semibold leading-none text-primary-foreground"
data-testid="inbox-reminder-badge-option"
>
{dueReminderCount}
</span>
) : null}
</span>
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
Expand All @@ -185,32 +208,43 @@ export function InboxListPane({
</div>
</TopChromeInsetHeader>

<div
className="min-h-0 flex-1 overflow-y-auto overscroll-contain"
data-testid="home-inbox-list"
ref={scrollRef}
>
{items.length === 0 ? (
<div className="flex h-full min-h-64 items-center justify-center px-6 text-center">
<div>
<p className="text-sm font-medium text-foreground">
No messages found
</p>
<p className="mt-1 text-sm text-muted-foreground">
Switch back to all mail to see more messages.
</p>
{isReminders ? (
<div
className="flex min-h-0 flex-1 flex-col overflow-hidden"
data-testid="home-inbox-reminders"
>
{reminderPubkey ? (
<RemindersPanel includeDone pubkey={reminderPubkey} />
) : null}
</div>
) : (
<div
className="min-h-0 flex-1 overflow-y-auto overscroll-contain"
data-testid="home-inbox-list"
ref={scrollRef}
>
{items.length === 0 ? (
<div className="flex h-full min-h-64 items-center justify-center px-6 text-center">
<div>
<p className="text-sm font-medium text-foreground">
No messages found
</p>
<p className="mt-1 text-sm text-muted-foreground">
Switch back to all mail to see more messages.
</p>
</div>
</div>
</div>
) : (
<VirtualizedList
estimateSize={76}
getItemKey={(item) => item.id}
items={items}
renderItem={renderItem}
scrollRef={scrollRef}
/>
)}
</div>
) : (
<VirtualizedList
estimateSize={76}
getItemKey={(item) => item.id}
items={items}
renderItem={renderItem}
scrollRef={scrollRef}
/>
)}
</div>
)}
</section>
);
}
4 changes: 3 additions & 1 deletion desktop/src/features/messages/ui/MessageRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ export const MessageRow = React.memo(
errorMessage: reactionErrorMessage,
select: handleReactionSelect,
} = useReactionHandler(message, onToggleReaction);
const { openReminder } = useRemindLater();
const { openReminder, activeReminderEventIds } = useRemindLater();
const hasActiveReminder = activeReminderEventIds.has(message.id);
const mentionNames = React.useMemo(
() => resolveMentionNames(message.tags, profiles),
[profiles, message.tags],
Expand Down Expand Up @@ -392,6 +393,7 @@ export const MessageRow = React.memo(
? "mx-1 px-2 hover:bg-muted/50 focus-within:bg-muted/50"
: "px-2",
"flex items-start gap-2.5",
hasActiveReminder ? "bg-blue-500/10" : "",
highlighted
? "-mx-4 rounded-none px-6 before:absolute before:-inset-y-1.5 before:inset-x-0 before:animate-[route-target-highlight-fade_2s_ease-out_forwards] before:bg-primary/10 before:content-[''] motion-reduce:before:animate-none sm:-mx-6 sm:px-8"
: "",
Expand Down
Loading