Skip to content

Commit 491eaff

Browse files
committed
refactor: enhance announcement dialog logic and improve load more functionality
1 parent d589bbf commit 491eaff

9 files changed

Lines changed: 80 additions & 49 deletions

File tree

frontend/features/announcements/components/announcement-dialog-host.tsx

Lines changed: 61 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ import { closeAnnouncement, dismissAnnouncementToday, listAnnouncements } from "
2020
import type { AnnouncementDTO } from "@/shared/api/announcements.types";
2121
import { useAuthSession } from "@/shared/auth/auth-session-context";
2222
import { dispatchAnnouncementUnreadChanged, subscribeOpenAnnouncements } from "@/shared/events/announcement-events";
23+
import { useDialogSnapshot } from "@/shared/hooks/use-dialog-snapshot";
2324
import { cn } from "@/lib/utils";
2425

2526
type AnnouncementSortMode = "default" | "type" | "time";
2627
type AnnouncementDialogMode = "auto" | "manual";
28+
type AnnouncementType = "critical" | "warning" | "info" | "normal" | "general";
2729

2830
function isSkippedPath(pathname: string | null): boolean {
2931
if (!pathname) {
@@ -56,7 +58,11 @@ function formatAnnouncementTime(value: string, locale: string): string {
5658
}).format(date);
5759
}
5860

59-
function normalizeAnnouncementType(value: string): "critical" | "warning" | "info" | "normal" | "general" {
61+
function isAnnouncementSortMode(value: string): value is AnnouncementSortMode {
62+
return value === "default" || value === "type" || value === "time";
63+
}
64+
65+
function normalizeAnnouncementType(value: string): AnnouncementType {
6066
switch (value) {
6167
case "critical":
6268
case "warning":
@@ -112,6 +118,14 @@ function compareReadState(a: AnnouncementDTO, b: AnnouncementDTO): number {
112118
return Number(isAnnouncementRead(a)) - Number(isAnnouncementRead(b));
113119
}
114120

121+
function compareAnnouncementByTime(a: AnnouncementDTO, b: AnnouncementDTO): number {
122+
return announcementTime(b.updatedAt) - announcementTime(a.updatedAt) || b.id - a.id;
123+
}
124+
125+
function compareAnnouncementByType(a: AnnouncementDTO, b: AnnouncementDTO): number {
126+
return announcementTypeRank(b.type) - announcementTypeRank(a.type) || compareAnnouncementByTime(a, b);
127+
}
128+
115129
export function AnnouncementDialogHost() {
116130
const t = useTranslations("announcements");
117131
const locale = useLocale();
@@ -215,10 +229,10 @@ export function AnnouncementDialogHost() {
215229
const queue = dialogMode === "manual" ? manualQueue : autoQueue;
216230
const sortedQueue = React.useMemo(() => {
217231
if (sortMode === "time") {
218-
return [...queue].sort((a, b) => compareReadState(a, b) || announcementTime(b.updatedAt) - announcementTime(a.updatedAt) || b.id - a.id);
232+
return [...queue].sort((a, b) => compareReadState(a, b) || compareAnnouncementByTime(a, b));
219233
}
220234
if (sortMode === "type") {
221-
return [...queue].sort((a, b) => compareReadState(a, b) || announcementTypeRank(b.type) - announcementTypeRank(a.type) || announcementTime(b.updatedAt) - announcementTime(a.updatedAt) || b.id - a.id);
235+
return [...queue].sort((a, b) => compareReadState(a, b) || compareAnnouncementByType(a, b));
222236
}
223237
return queue;
224238
}, [queue, sortMode]);
@@ -233,7 +247,11 @@ export function AnnouncementDialogHost() {
233247
}, [hasUnread]);
234248

235249
const open = manualOpen || autoOpen;
236-
const active = sortedQueue[Math.min(activeIndex, Math.max(sortedQueue.length - 1, 0))] ?? null;
250+
const renderMode = useDialogSnapshot(open ? dialogMode : null) ?? dialogMode;
251+
const renderQueue = useDialogSnapshot(open ? sortedQueue : null) ?? sortedQueue;
252+
const renderActiveIndex = useDialogSnapshot(open ? activeIndex : null) ?? activeIndex;
253+
const renderManualLoading = useDialogSnapshot(open ? manualLoading : null) ?? manualLoading;
254+
const active = renderQueue[Math.min(renderActiveIndex, Math.max(renderQueue.length - 1, 0))] ?? null;
237255
const unreadQueue = React.useMemo(() => queue.filter((item) => !isAnnouncementRead(item)), [queue]);
238256

239257
const closeDialog = React.useCallback(() => {
@@ -255,6 +273,23 @@ export function AnnouncementDialogHost() {
255273
setActiveIndex(0);
256274
}, []);
257275

276+
const handleOpenChange = React.useCallback((nextOpen: boolean) => {
277+
if (nextOpen) {
278+
return;
279+
}
280+
if (manualOpen) {
281+
closeManualDialog();
282+
return;
283+
}
284+
hideAutoDialog();
285+
}, [closeManualDialog, hideAutoDialog, manualOpen]);
286+
287+
const handleSortModeChange = React.useCallback((value: string) => {
288+
if (isAnnouncementSortMode(value)) {
289+
setSortMode(value);
290+
}
291+
}, []);
292+
258293
const dismissAllToday = React.useCallback(async () => {
259294
if (!accessToken || stateSaving) {
260295
return;
@@ -286,40 +321,32 @@ export function AnnouncementDialogHost() {
286321
}, [accessToken, closeDialog, stateSaving, t, unreadQueue]);
287322

288323
return (
289-
<Dialog open={open} onOpenChange={(nextOpen) => {
290-
if (!nextOpen) {
291-
if (manualOpen) {
292-
closeManualDialog();
293-
} else {
294-
hideAutoDialog();
295-
}
296-
}
297-
}}>
298-
<DialogContent className="flex max-h-[min(84svh,720px)] flex-col overflow-hidden sm:max-w-[760px]">
324+
<Dialog open={open} onOpenChange={handleOpenChange}>
325+
<DialogContent className="flex max-h-[min(84svh,720px)] min-w-0 flex-col overflow-hidden p-4 sm:max-w-[760px] sm:p-5">
299326
<DialogHeader className="shrink-0">
300327
<div className="min-w-0">
301328
<DialogTitle className="truncate">{t("title")}</DialogTitle>
302329
</div>
303330
</DialogHeader>
304-
<div className="grid h-[27rem] max-h-[calc(100svh-11rem)] min-h-0 grid-rows-[auto_minmax(0,1fr)] gap-0 overflow-hidden md:grid-cols-[13rem_minmax(0,1fr)] md:grid-rows-1">
305-
<div className="flex min-h-0 flex-col border-b border-border/60 md:border-b-0 md:border-r">
306-
<Tabs value={sortMode} onValueChange={(value) => setSortMode(value as AnnouncementSortMode)} className="shrink-0 px-2 pt-2 pb-1">
331+
<div className="grid h-[27rem] max-h-[calc(100svh-11rem)] min-h-0 min-w-0 grid-rows-[auto_minmax(0,1fr)] gap-0 overflow-hidden md:grid-cols-[13rem_minmax(0,1fr)] md:grid-rows-1">
332+
<div className="flex min-h-0 min-w-0 flex-col border-b border-border/60 md:border-b-0 md:border-r">
333+
<Tabs value={sortMode} onValueChange={handleSortModeChange} className="min-w-0 shrink-0 px-2 pt-2 pb-1">
307334
<TabsList className="grid h-7 w-full grid-cols-3">
308335
<TabsTrigger value="default" className="px-1.5">{t("sort.default")}</TabsTrigger>
309336
<TabsTrigger value="type" className="px-1.5">{t("sort.type")}</TabsTrigger>
310337
<TabsTrigger value="time" className="px-1.5">{t("sort.time")}</TabsTrigger>
311338
</TabsList>
312339
</Tabs>
313-
<div className="flex gap-2 overflow-x-auto px-2 py-2 md:block md:min-h-0 md:flex-1 md:space-y-0.5 md:overflow-y-auto">
314-
{sortedQueue.length > 0 ? sortedQueue.map((item, index) => (
340+
<div className="flex min-w-0 gap-2 overflow-x-auto px-2 py-2 md:block md:min-h-0 md:flex-1 md:space-y-0.5 md:overflow-y-auto">
341+
{renderQueue.length > 0 ? renderQueue.map((item, index) => (
315342
<button
316343
key={`${item.id}:${item.updatedAt}`}
317344
type="button"
318345
className={cn(
319346
"relative min-w-36 rounded-md py-1 pl-3.5 pr-8 text-left text-xs transition-colors before:absolute before:left-1.5 before:top-2 before:bottom-2 before:w-0.5 before:rounded-full md:h-[3.125rem] md:w-full",
320347
announcementTypeAccentClassName(item.type),
321348
isAnnouncementRead(item) && "opacity-55",
322-
index === activeIndex
349+
index === renderActiveIndex
323350
? "bg-muted text-foreground"
324351
: "text-muted-foreground hover:bg-muted/60 hover:text-foreground",
325352
)}
@@ -336,29 +363,38 @@ export function AnnouncementDialogHost() {
336363
</button>
337364
)) : (
338365
<div className="flex h-full min-h-24 items-center justify-center px-3 py-6 text-center text-xs text-muted-foreground">
339-
{manualLoading ? t("loading") : t("empty")}
366+
{renderManualLoading ? t("loading") : t("empty")}
340367
</div>
341368
)}
342369
</div>
343370
</div>
344-
<div className="min-h-0 overflow-y-auto px-4 py-3">
371+
<div className="min-h-0 min-w-0 overflow-y-auto overflow-x-hidden px-3 py-3 sm:px-4">
345372
{active ? (
346373
<>
347374
<div className="mb-2 flex min-w-0 items-center justify-between gap-3 text-xs text-muted-foreground">
348375
<span className="min-w-0 truncate">{active.title}</span>
349376
<span className="shrink-0 tabular-nums">{formatAnnouncementTime(active.updatedAt, locale)}</span>
350377
</div>
351-
<StreamdownRender content={active.contentMarkdown} className="text-sm" />
378+
<StreamdownRender
379+
content={active.contentMarkdown}
380+
className={cn(
381+
"max-w-full text-sm leading-7",
382+
"[&_h1]:text-base [&_h1]:leading-7",
383+
"[&_h2]:text-base [&_h2]:leading-7",
384+
"[&_h3]:text-sm [&_h3]:leading-6",
385+
"[&_li]:leading-7",
386+
)}
387+
/>
352388
</>
353389
) : (
354390
<div className="flex min-h-full items-center justify-center text-center text-sm text-muted-foreground">
355-
{manualLoading ? t("loading") : t("empty")}
391+
{renderManualLoading ? t("loading") : t("empty")}
356392
</div>
357393
)}
358394
</div>
359395
</div>
360396
<DialogFooter className="shrink-0">
361-
{dialogMode === "manual" ? (
397+
{renderMode === "manual" ? (
362398
<Button type="button" onClick={() => unreadQueue.length > 0 ? void closeAll() : closeManualDialog()} disabled={stateSaving}>
363399
{t("close")}
364400
</Button>

frontend/features/files/components/sections/sidebar/sidebar-list.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,14 +218,12 @@ export function SidebarList({
218218
}: SidebarListProps) {
219219
const t = useTranslations("files");
220220
const scrollAreaRef = React.useRef<HTMLDivElement | null>(null);
221-
const loadMoreRef = React.useRef<HTMLDivElement | null>(null);
222221
const selectedFileIDSet = React.useMemo(() => new Set(selectedFileIDs), [selectedFileIDs]);
223222

224-
useLoadMoreSentinel({
223+
const loadMoreRef = useLoadMoreSentinel<HTMLDivElement>({
225224
enabled: hasMore && !loading && !loadingMore,
226225
rootMargin: "0px 0px 200px 0px",
227226
rootRef: scrollAreaRef,
228-
targetRef: loadMoreRef,
229227
onLoadMore,
230228
});
231229

frontend/features/layouts/components/navigation/nav-main-item.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export function NavMainItem({
6666
const itemClassName = cn(
6767
"group/item h-8 gap-0 rounded-md px-0 text-left text-sm font-normal text-sidebar-foreground transition-colors focus-visible:ring-2 focus-visible:ring-sidebar-ring",
6868
isCollapsed
69-
? "w-8 justify-center hover:bg-transparent hover:text-current"
69+
? "w-8 justify-center hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
7070
: "w-full justify-start hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
7171
);
7272

frontend/features/layouts/components/navigation/nav-recents.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ export function NavRecents() {
8888
const [renameValue, setRenameValue] = React.useState("");
8989
const [autoRenamingPublicID, setAutoRenamingPublicID] = React.useState<string | null>(null);
9090
const [recentsOpen, setRecentsOpen] = useStoredBoolean(RECENTS_OPEN_STORAGE_KEY, true);
91-
const loadMoreRef = React.useRef<HTMLLIElement | null>(null);
9291
const listContainerRef = React.useRef<HTMLDivElement | null>(null);
9392
const deleteFilesID = React.useId();
9493
const stableDeleteTarget = useDialogSnapshot(deleteTarget);
@@ -99,9 +98,8 @@ export function NavRecents() {
9998
failureMessage: t("exportFailed"),
10099
});
101100

102-
useLoadMoreSentinel({
101+
const loadMoreRef = useLoadMoreSentinel<HTMLLIElement>({
103102
enabled: recentsOpen && hasMore && !loadingInitial && !loadingMore && !loadMoreFailed,
104-
targetRef: loadMoreRef,
105103
onLoadMore: loadMore,
106104
});
107105

frontend/features/layouts/components/sections/project-layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export function ProjectLayout({
8686
<AnnouncementDialogHost />
8787
<SidebarProvider className="h-svh overflow-hidden" defaultOpen={defaultSidebarOpen}>
8888
<SidebarConversationsProvider
89-
bulkPendingTitle={tRecent("labelMenu.bulk.pending")}
89+
bulkPendingTitle={tRecent("dialogs.bulk.pending")}
9090
newConversationTitle={tRecent("newChat")}
9191
>
9292
<ChatSessionProvider>

frontend/features/prompts/hooks/use-skills-prompt-page.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,6 @@ export function useSkillsPromptPage() {
157157
const [deleteTarget, setDeleteTarget] = React.useState<PromptPresetDTO | null>(null);
158158
const [query, setQuery] = React.useState("");
159159
const [debouncedQuery, setDebouncedQuery] = React.useState("");
160-
const loadMoreRef = React.useRef<HTMLDivElement | null>(null);
161160
const loadingMoreRef = React.useRef(false);
162161
const loadMoreFailedRef = React.useRef(false);
163162

@@ -269,9 +268,8 @@ export function useSkillsPromptPage() {
269268
}
270269
}, [fetchPage, hasMore, loading, pagination, resolveErrorMessage, t]);
271270

272-
useLoadMoreSentinel({
271+
const loadMoreRef = useLoadMoreSentinel<HTMLDivElement>({
273272
enabled: hasMore && !loading && !loadingMore && !loadMoreFailed,
274-
targetRef: loadMoreRef,
275273
rootMargin: "160px",
276274
onLoadMore: loadMore,
277275
});

frontend/features/recent/components/sections/recent-list.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ type RecentListProps = {
320320
shareFilter: ConversationShareFilter;
321321
rowStates: RecentRowState[];
322322
isSelectionMode: boolean;
323-
loadMoreRef: React.RefObject<HTMLDivElement | null>;
323+
loadMoreRef: React.RefCallback<HTMLDivElement>;
324324
hasMore: boolean;
325325
loadMoreFailed: boolean;
326326
loadingMore: boolean;

frontend/features/recent/hooks/use-recent-page.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,6 @@ export function useRecentPage() {
135135
const { deleteFilesByDefault } = useSettingsChatPreferences();
136136
const [shareTarget, setShareTarget] = React.useState<ConversationDTO | null>(null);
137137
const [exportingAll, setExportingAll] = React.useState(false);
138-
const loadMoreRef = React.useRef<HTMLDivElement | null>(null);
139138
const pageRef = React.useRef(1);
140139
const requestVersionRef = React.useRef(0);
141140
const loadingMoreRef = React.useRef(false);
@@ -299,9 +298,8 @@ export function useRecentPage() {
299298
}
300299
}, [hasMore, loadPage, loadingInitial, resolveErrorMessage, t]);
301300

302-
useLoadMoreSentinel({
301+
const loadMoreRef = useLoadMoreSentinel<HTMLDivElement>({
303302
enabled: hasMore && !loadingInitial && !loadingMore && !loadMoreFailed,
304-
targetRef: loadMoreRef,
305303
rootMargin: "160px",
306304
onLoadMore: loadMore,
307305
});
@@ -546,7 +544,7 @@ export function useRecentPage() {
546544
await runBulkActionInChunks({
547545
chunkSize: 10,
548546
items: deleteTarget.ids,
549-
title: t("labelMenu.bulk.pending"),
547+
title: t("dialogs.bulk.pending"),
550548
runChunk: async (ids) => {
551549
for (const id of ids) {
552550
await deleteByPublicID(id, deleteFiles ? { deleteFiles: true } : undefined);
@@ -618,7 +616,7 @@ export function useRecentPage() {
618616
const updates = (await runBulkActionInChunks({
619617
chunkSize: 10,
620618
items: targets,
621-
title: t("labelMenu.bulk.pending"),
619+
title: t("dialogs.bulk.pending"),
622620
runChunk: async (chunk) => {
623621
const updatedItems: Array<ConversationDTO | null> = [];
624622
for (const item of chunk) {
@@ -661,7 +659,7 @@ export function useRecentPage() {
661659
const ids = selectedSharedItems.map((item) => item.publicID);
662660
await runBulkActionInChunks({
663661
items: ids,
664-
title: t("labelMenu.bulk.pending"),
662+
title: t("dialogs.bulk.pending"),
665663
runChunk: (conversationPublicIDs) => revokeConversationShares(token, { conversationPublicIDs }),
666664
});
667665
const patch: Partial<ConversationDTO> = {

frontend/shared/hooks/use-load-more-sentinel.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ type UseLoadMoreSentinelOptions = {
66
enabled: boolean;
77
rootMargin?: string;
88
rootRef?: React.RefObject<HTMLElement | null>;
9-
targetRef: React.RefObject<Element | null>;
109
onLoadMore: () => void | Promise<void>;
1110
};
1211

@@ -20,14 +19,17 @@ function rootMarginToPixels(rootMargin: string): number {
2019
return Number(match[1]) || 0;
2120
}
2221

23-
export function useLoadMoreSentinel({
22+
export function useLoadMoreSentinel<T extends Element = HTMLElement>({
2423
enabled,
2524
rootMargin = "120px",
2625
rootRef,
27-
targetRef,
2826
onLoadMore,
29-
}: UseLoadMoreSentinelOptions) {
27+
}: UseLoadMoreSentinelOptions): React.RefCallback<T> {
28+
const [target, setTarget] = React.useState<T | null>(null);
3029
const onLoadMoreRef = React.useRef(onLoadMore);
30+
const targetRef = React.useCallback((node: T | null) => {
31+
setTarget(node);
32+
}, []);
3133

3234
React.useEffect(() => {
3335
onLoadMoreRef.current = onLoadMore;
@@ -38,7 +40,6 @@ export function useLoadMoreSentinel({
3840
return;
3941
}
4042

41-
const target = targetRef.current;
4243
if (!target) {
4344
return;
4445
}
@@ -103,5 +104,7 @@ export function useLoadMoreSentinel({
103104
window.cancelAnimationFrame(animationFrame);
104105
}
105106
};
106-
}, [enabled, rootMargin, rootRef, targetRef]);
107+
}, [enabled, rootMargin, rootRef, target]);
108+
109+
return targetRef;
107110
}

0 commit comments

Comments
 (0)