@@ -20,10 +20,12 @@ import { closeAnnouncement, dismissAnnouncementToday, listAnnouncements } from "
2020import type { AnnouncementDTO } from "@/shared/api/announcements.types" ;
2121import { useAuthSession } from "@/shared/auth/auth-session-context" ;
2222import { dispatchAnnouncementUnreadChanged , subscribeOpenAnnouncements } from "@/shared/events/announcement-events" ;
23+ import { useDialogSnapshot } from "@/shared/hooks/use-dialog-snapshot" ;
2324import { cn } from "@/lib/utils" ;
2425
2526type AnnouncementSortMode = "default" | "type" | "time" ;
2627type AnnouncementDialogMode = "auto" | "manual" ;
28+ type AnnouncementType = "critical" | "warning" | "info" | "normal" | "general" ;
2729
2830function 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+
115129export 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 >
0 commit comments