Skip to content

Commit a4b23c5

Browse files
authored
fix announcement drawer read behavior (#532)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit # Release Notes * **New Features** * Users can now mark individual announcements as read or unread with dedicated toggle buttons * Read announcements are organized in a collapsible "Earlier" section * Visual indicators (border, color, styling) distinguish read from unread announcements * **Bug Fixes** * Removed automatic mark-all-as-read behavior that occurred when opening announcements * **Documentation** * Updated translations for new read/unread state controls across all supported languages <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 62ab30f commit a4b23c5

18 files changed

Lines changed: 403 additions & 129 deletions

File tree

echo/frontend/src/components/announcement/AnnouncementItem.tsx

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { useFormatDate } from "./utils/dateUtils";
2121

2222
type Announcement = {
2323
id: string;
24+
activityIds: string[];
2425
title: string;
2526
message: string;
2627
created_at: string | Date | null | undefined;
@@ -31,13 +32,15 @@ type Announcement = {
3132

3233
interface AnnouncementItemProps {
3334
announcement: Announcement;
35+
onMarkAsRead: (id: string) => void;
36+
onMarkAsUnread: (id: string, activityIds: string[]) => void;
3437
index: number;
3538
}
3639

3740
export const AnnouncementItem = forwardRef<
3841
HTMLDivElement,
3942
AnnouncementItemProps
40-
>(({ announcement, index }, ref) => {
43+
>(({ announcement, onMarkAsRead, onMarkAsUnread, index }, ref) => {
4144
const theme = useMantineTheme();
4245
const [showMore, setShowMore] = useState(false);
4346
const [showReadMoreButton, setShowReadMoreButton] = useState(false);
@@ -52,14 +55,12 @@ export const AnnouncementItem = forwardRef<
5255
}
5356
}, []);
5457

58+
const isRead = !!announcement.read;
59+
5560
return (
5661
<Box
5762
ref={ref}
58-
className={`group border-b border-gray-100 p-4 transition-all duration-200 hover:bg-blue-50 ${index === 0 ? "border-t-0" : ""} ${
59-
!announcement.read
60-
? "border-l-4 border-l-blue-500"
61-
: "border-l-4 border-l-gray-50/50 bg-gray-50/50"
62-
}`}
63+
className={`group border-b border-gray-100 p-4 transition-all duration-200 ${!isRead ? "hover:bg-blue-50" : ""} ${index === 0 ? "border-t-0" : ""} border-l-4 ${isRead ? "border-l-gray-50/50 bg-gray-50/50" : "border-l-blue-500"}`}
6364
{...testId(`announcement-item-${announcement.id}`)}
6465
>
6566
<Stack gap="xs">
@@ -79,7 +80,7 @@ export const AnnouncementItem = forwardRef<
7980
<Stack gap="xs" style={{ flex: 1 }}>
8081
<Group justify="space-between" align="center">
8182
<div style={{ flex: 1 }}>
82-
<Text size="sm" fw={500}>
83+
<Text size="sm" fw={isRead ? 400 : 500} c={isRead ? "dimmed" : undefined}>
8384
{announcement.title}
8485
</Text>
8586
</div>
@@ -89,7 +90,7 @@ export const AnnouncementItem = forwardRef<
8990
{formatDate(announcement.created_at)}
9091
</Text>
9192

92-
{!announcement.read && (
93+
{!isRead && (
9394
<div
9495
style={{
9596
backgroundColor: theme.colors.blue[6],
@@ -110,8 +111,8 @@ export const AnnouncementItem = forwardRef<
110111
/>
111112
</Text>
112113

113-
{showReadMoreButton && (
114-
<Group justify="flex-start">
114+
<Group justify="space-between" align="center">
115+
{showReadMoreButton && (
115116
<Button
116117
variant="transparent"
117118
color="gray"
@@ -133,8 +134,34 @@ export const AnnouncementItem = forwardRef<
133134
</Group>
134135
)}
135136
</Button>
136-
</Group>
137-
)}
137+
)}
138+
139+
{isRead ? (
140+
<Button
141+
variant="transparent"
142+
size="xs"
143+
color="gray"
144+
className="hover:underline"
145+
ml="auto"
146+
onClick={() => onMarkAsUnread(announcement.id, announcement.activityIds)}
147+
{...testId("announcement-mark-as-unread-button")}
148+
>
149+
<Trans>Mark as unread</Trans>
150+
</Button>
151+
) : (
152+
<Button
153+
variant="transparent"
154+
size="xs"
155+
color="gray"
156+
className="hover:underline"
157+
ml="auto"
158+
onClick={() => onMarkAsRead(announcement.id)}
159+
{...testId("announcement-mark-as-read-button")}
160+
>
161+
<Trans>Mark as read</Trans>
162+
</Button>
163+
)}
164+
</Group>
138165
</Stack>
139166
</Group>
140167
</Stack>

echo/frontend/src/components/announcement/Announcements.tsx

Lines changed: 104 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,13 @@ import {
1212
ThemeIcon,
1313
UnstyledButton,
1414
} from "@mantine/core";
15-
import { CaretDown, CaretUp, Sparkle } from "@phosphor-icons/react";
16-
import { useEffect, useRef, useState } from "react";
15+
import {
16+
CaretDown,
17+
CaretUp,
18+
CheckCircle,
19+
Sparkle,
20+
} from "@phosphor-icons/react";
21+
import { useEffect, useMemo, useState } from "react";
1722
import { useInView } from "react-intersection-observer";
1823
import { useAnnouncementDrawer } from "@/components/announcement/hooks";
1924
import {
@@ -33,16 +38,20 @@ import { WhatsNewItem } from "./WhatsNewItem";
3338
import {
3439
useInfiniteAnnouncements,
3540
useMarkAllAsReadMutation,
41+
useMarkAsReadMutation,
42+
useMarkAsUnreadMutation,
3643
useWhatsNewAnnouncements,
3744
} from "./hooks";
3845

3946
export const Announcements = () => {
4047
const { isOpen, close } = useAnnouncementDrawer();
4148
const { language } = useLanguage();
49+
const markAsReadMutation = useMarkAsReadMutation();
50+
const markAsUnreadMutation = useMarkAsUnreadMutation();
4251
const markAllAsReadMutation = useMarkAllAsReadMutation();
4352
const [openedOnce, setOpenedOnce] = useState(false);
53+
const [readExpanded, setReadExpanded] = useState(false);
4454
const [whatsNewExpanded, setWhatsNewExpanded] = useState(false);
45-
const autoReadTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
4655

4756
const { ref: loadMoreRef, inView } = useInView();
4857

@@ -58,23 +67,6 @@ export const Announcements = () => {
5867
}
5968
}, [isOpen, openedOnce]);
6069

61-
// Auto-mark all as read after 1 second when drawer opens
62-
// biome-ignore lint/correctness/useExhaustiveDependencies: only trigger on isOpen changes, mutate ref is stable
63-
useEffect(() => {
64-
if (isOpen) {
65-
autoReadTimerRef.current = setTimeout(() => {
66-
markAllAsReadMutation.mutate();
67-
}, 1000);
68-
}
69-
70-
return () => {
71-
if (autoReadTimerRef.current) {
72-
clearTimeout(autoReadTimerRef.current);
73-
autoReadTimerRef.current = null;
74-
}
75-
};
76-
}, [isOpen]);
77-
7870
const {
7971
data: announcementsData,
8072
fetchNextPage,
@@ -106,22 +98,46 @@ export const Announcements = () => {
10698
language,
10799
);
108100

109-
// Only show unread announcements (read ones are hidden)
110-
const unreadAnnouncements = processedAnnouncements.filter((a) => !a.read);
111-
112-
// Process "What's new" announcements
113-
const whatsNewAnnouncements = useWhatsNewProcessed(
114-
whatsNewData ?? [],
115-
language,
101+
// Split into unread and read
102+
const unreadAnnouncements = useMemo(
103+
() => processedAnnouncements.filter((a) => !a.read),
104+
[processedAnnouncements],
105+
);
106+
const readAnnouncements = useMemo(
107+
() => processedAnnouncements.filter((a) => a.read),
108+
[processedAnnouncements],
116109
);
117110

111+
// Auto-expand read section when there are no unread items
112+
// biome-ignore lint/correctness/useExhaustiveDependencies: only react to unread/read count changes
113+
useEffect(() => {
114+
if (unreadAnnouncements.length === 0 && readAnnouncements.length > 0) {
115+
setReadExpanded(true);
116+
}
117+
}, [unreadAnnouncements.length, readAnnouncements.length]);
118+
119+
// Process "What's new" announcements, excluding those already in the main list
120+
const whatsNewRaw = useWhatsNewProcessed(whatsNewData ?? [], language);
121+
const whatsNewAnnouncements = useMemo(() => {
122+
const mainIds = new Set(processedAnnouncements.map((a) => a.id));
123+
return whatsNewRaw.filter((a) => !mainIds.has(a.id));
124+
}, [whatsNewRaw, processedAnnouncements]);
125+
118126
// Load more announcements when user scrolls to bottom
119127
useEffect(() => {
120128
if (inView && hasNextPage && !isFetchingNextPage) {
121129
fetchNextPage();
122130
}
123131
}, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]);
124132

133+
const handleMarkAsRead = async (id: string) => {
134+
markAsReadMutation.mutate({ announcementId: id });
135+
};
136+
137+
const handleMarkAsUnread = async (id: string, activityIds: string[]) => {
138+
markAsUnreadMutation.mutate({ announcementId: id, activityIds });
139+
};
140+
125141
const handleMarkAllAsRead = async () => {
126142
markAllAsReadMutation.mutate();
127143
};
@@ -166,7 +182,7 @@ export const Announcements = () => {
166182
/>
167183
) : isLoading ? (
168184
<AnnouncementSkeleton />
169-
) : unreadAnnouncements.length === 0 &&
185+
) : processedAnnouncements.length === 0 &&
170186
whatsNewAnnouncements.length === 0 ? (
171187
<Box p="md" {...testId("announcement-empty-state")}>
172188
<Text c="dimmed" ta="center">
@@ -180,12 +196,9 @@ export const Announcements = () => {
180196
<AnnouncementItem
181197
key={announcement.id}
182198
announcement={announcement}
199+
onMarkAsRead={handleMarkAsRead}
200+
onMarkAsUnread={handleMarkAsUnread}
183201
index={index}
184-
ref={
185-
index === unreadAnnouncements.length - 1
186-
? loadMoreRef
187-
: undefined
188-
}
189202
/>
190203
))}
191204

@@ -195,7 +208,61 @@ export const Announcements = () => {
195208
</Center>
196209
)}
197210

198-
{/* Release notes under "View earlier" */}
211+
{/* Earlier (read) section */}
212+
{readAnnouncements.length > 0 && (
213+
<>
214+
<Divider
215+
my="md"
216+
mx="md"
217+
label={
218+
<UnstyledButton
219+
onClick={() =>
220+
setReadExpanded(!readExpanded)
221+
}
222+
>
223+
<Group gap="xs" align="center">
224+
<CheckCircle
225+
size={16}
226+
weight="fill"
227+
color="gray"
228+
/>
229+
<Text
230+
size="sm"
231+
fw={500}
232+
c="dimmed"
233+
>
234+
<Trans>Earlier</Trans>
235+
</Text>
236+
{readExpanded ? (
237+
<CaretUp size={14} color="gray" />
238+
) : (
239+
<CaretDown size={14} color="gray" />
240+
)}
241+
</Group>
242+
</UnstyledButton>
243+
}
244+
labelPosition="left"
245+
/>
246+
247+
<Collapse in={readExpanded}>
248+
<Stack gap="0">
249+
{readAnnouncements.map(
250+
(announcement, index) => (
251+
<AnnouncementItem
252+
key={announcement.id}
253+
announcement={announcement}
254+
onMarkAsRead={handleMarkAsRead}
255+
onMarkAsUnread={handleMarkAsUnread}
256+
index={index}
257+
/>
258+
),
259+
)}
260+
</Stack>
261+
</Collapse>
262+
</>
263+
)}
264+
265+
{/* Release notes */}
199266
{whatsNewAnnouncements.length > 0 && (
200267
<>
201268
<Divider
@@ -243,6 +310,9 @@ export const Announcements = () => {
243310
</Collapse>
244311
</>
245312
)}
313+
314+
{/* Infinite scroll sentinel */}
315+
<div ref={loadMoreRef} />
246316
</>
247317
)}
248318
</Stack>

0 commit comments

Comments
 (0)