Skip to content

Commit f37116c

Browse files
committed
feat: redesign ReminderNotification
1 parent a51b47f commit f37116c

20 files changed

Lines changed: 193 additions & 62 deletions

src/components/Icons/icons.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,13 @@ export const IconAtSolid = createIcon(
7373

7474
export const IconBellNotification = createIcon(
7575
'IconBellNotification',
76-
<path d='M12.8926 10.7972L12.0674 9.19757C11.9412 8.95287 11.8684 8.68417 11.8545 8.40948L11.7314 5.97003C11.632 3.99113 9.99271 2.43292 8 2.43292C6.00726 2.43292 4.368 3.99015 4.26855 5.96906L4.14453 8.40948C4.13061 8.68431 4.0587 8.95302 3.93262 9.19757L3.10742 10.7972C3.1024 10.807 3.09961 10.8182 3.09961 10.8294C3.09962 10.8684 3.1319 10.8997 3.1709 10.8997H12.8291C12.8681 10.8997 12.9004 10.8684 12.9004 10.8294C12.9004 10.8183 12.8977 10.8071 12.8926 10.7972ZM6.02246 12.0999C6.2796 12.9486 7.06733 13.5667 8 13.5667C8.93265 13.5667 9.72039 12.9486 9.97754 12.0999H6.02246ZM14.0996 10.8294C14.0996 11.5311 13.5308 12.0999 12.8291 12.0999H11.2109C10.9292 13.6174 9.59912 14.7669 8 14.7669C6.40085 14.7669 5.07082 13.6174 4.78906 12.0999H3.1709C2.46916 12.0999 1.90041 11.5311 1.90039 10.8294C1.90039 10.627 1.94825 10.4274 2.04102 10.2474L2.86621 8.64777C2.91399 8.5551 2.94099 8.45314 2.94629 8.34894L3.06934 5.90948C3.20084 3.28834 5.37132 1.2337 8 1.2337C10.6286 1.2337 12.7992 3.28834 12.9307 5.90948L13.0537 8.34894C13.059 8.45327 13.0861 8.55526 13.1338 8.64777L13.959 10.2474C14.0517 10.4273 14.0996 10.6269 14.0996 10.8294Z' />,
76+
<path
77+
d='M10.6667 11.4997C10.6667 12.9724 9.47273 14.1663 8 14.1663C6.52724 14.1663 5.33333 12.9724 5.33333 11.4997M13.5 10.8291C13.5 11.1994 13.1997 11.4997 12.8294 11.4997H3.17062C2.80025 11.4997 2.5 11.1994 2.5 10.8291C2.5 10.7221 2.52557 10.6167 2.57457 10.5217L3.39922 8.92241C3.48623 8.75367 3.53621 8.56827 3.54578 8.37861L3.66901 5.93899C3.7844 3.63889 5.68924 1.83301 8 1.83301C10.3107 1.83301 12.2156 3.63889 12.331 5.93899L12.4542 8.37861C12.4638 8.56827 12.5137 8.75367 12.6008 8.92241L13.4254 10.5217C13.4744 10.6167 13.5 10.7221 13.5 10.8291Z'
78+
fill='none'
79+
stroke='currentColor'
80+
strokeLinecap='round'
81+
strokeLinejoin='round'
82+
/>,
7783
);
7884

7985
export const IconBellOff = createIcon(
@@ -90,7 +96,13 @@ export const IconBellOff = createIcon(
9096

9197
export const IconBookmark = createIcon(
9298
'IconBookmark',
93-
<path d='M6.92056 11.5033C7.57321 11.0641 8.42712 11.0641 9.07974 11.5033L12.1295 13.556C12.145 13.5664 12.1554 13.5685 12.1628 13.5687C12.1722 13.569 12.1849 13.5668 12.1979 13.5599C12.2109 13.553 12.2202 13.5436 12.2253 13.5355C12.2291 13.5293 12.233 13.5196 12.2331 13.5013V3.16638C12.2329 2.76152 11.9046 2.43298 11.4997 2.43298H4.49966C4.09491 2.43316 3.76644 2.76163 3.76627 3.16638V13.5013C3.76631 13.5198 3.77121 13.5293 3.77505 13.5355C3.7801 13.5436 3.7894 13.553 3.8024 13.5599C3.81508 13.5666 3.82721 13.569 3.83658 13.5687C3.84393 13.5685 3.85513 13.5666 3.87076 13.556L6.92056 11.5033ZM13.4333 13.5013C13.433 14.5152 12.3009 15.1181 11.4596 14.5521L8.40982 12.4994C8.19302 12.3535 7.91754 12.3351 7.68619 12.4447L7.59048 12.4994L4.54068 14.5521C3.69947 15.1182 2.56727 14.5154 2.56705 13.5013V3.16638C2.56722 2.09889 3.43217 1.23394 4.49966 1.23376H11.4997C12.5673 1.23376 13.4331 2.09879 13.4333 3.16638V13.5013Z' />,
99+
<path
100+
d='M12.8333 13.501V3.16666C12.8333 2.43028 12.2364 1.83333 11.5 1.83333H4.49999C3.76361 1.83333 3.16666 2.43028 3.16666 3.16666V13.501C3.16666 14.0348 3.76275 14.3521 4.20558 14.054L7.25546 12.0011C7.70559 11.6982 8.29439 11.6982 8.74452 12.0011L11.7944 14.054C12.2373 14.3521 12.8333 14.0348 12.8333 13.501Z'
101+
fill='none'
102+
stroke='black'
103+
strokeLinecap='round'
104+
strokeLinejoin='round'
105+
/>,
94106
);
95107

96108
export const IconBookmarkRemove = createIcon(

src/components/Message/ReminderNotification.tsx

Lines changed: 74 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react';
22
import { useTranslationContext } from '../../context';
33
import { useStateStore } from '../../store';
44
import type { Reminder, ReminderState } from 'stream-chat';
5+
import { IconBellNotification, IconBookmark } from '../Icons';
56

67
export type ReminderNotificationProps = {
78
reminder?: Reminder;
@@ -11,40 +12,92 @@ const reminderStateSelector = (state: ReminderState) => ({
1112
timeLeftMs: state.timeLeftMs,
1213
});
1314

14-
export const ReminderNotification = ({ reminder }: ReminderNotificationProps) => {
15+
function SavedForLaterContent() {
16+
const { t } = useTranslationContext();
17+
return (
18+
<p className='str-chat__message-saved-for-later'>
19+
<IconBookmark />
20+
<span>{t('Saved for later')}</span>
21+
</p>
22+
);
23+
}
24+
25+
const THRESHOLD_RELATIVE_MINUTES = 59;
26+
27+
function RemindMeContent({ reminder }: { reminder: Reminder }) {
1528
const { t } = useTranslationContext();
1629
const { timeLeftMs } = useStateStore(reminder?.state, reminderStateSelector) ?? {};
1730

1831
const stopRefreshBoundaryMs = reminder?.timer.stopRefreshBoundaryMs;
1932
const stopRefreshTimeStamp =
2033
reminder?.remindAt && stopRefreshBoundaryMs
21-
? reminder?.remindAt.getTime() + stopRefreshBoundaryMs
34+
? reminder.remindAt.getTime() + stopRefreshBoundaryMs
2235
: undefined;
2336

2437
const isBehindRefreshBoundary =
2538
!!stopRefreshTimeStamp && new Date().getTime() > stopRefreshTimeStamp;
2639

40+
if (timeLeftMs === null || !reminder.remindAt) return null;
41+
42+
const nowMs = Date.now();
43+
const remindAtMs = reminder.remindAt.getTime();
44+
const diffMs = remindAtMs - nowMs;
45+
const diffMinutes = Math.abs(diffMs) / (60 * 1000);
46+
const useAbsoluteFormat = diffMinutes > THRESHOLD_RELATIVE_MINUTES;
47+
48+
const renderTime = () => {
49+
if (isBehindRefreshBoundary) {
50+
// Past: reminder time has passed
51+
if (useAbsoluteFormat) {
52+
// > 59 min ago: calendar + time (same as DateSeparator + HH:mm)
53+
// e.g. "Due since Today at 15:00", "Due since Yesterday at 09:30"
54+
return t('Due since {{ dueSince }}', {
55+
dueSince: t('timestamp/ReminderNotification', {
56+
timestamp: reminder.remindAt,
57+
}),
58+
});
59+
}
60+
// Within 59 min ago: relative
61+
// e.g. "Due since 5 minutes ago", "Due since a minute ago"
62+
return t('Due since {{ dueSince }}', {
63+
dueSince: t('duration/Message reminder', {
64+
milliseconds: diffMs,
65+
}),
66+
});
67+
}
68+
// Future: reminder not yet due
69+
if (useAbsoluteFormat) {
70+
// > 59 min from now: calendar + time (no "Due" prefix)
71+
// e.g. "Today at 15:00", "Tomorrow at 09:30"
72+
return t('timestamp/ReminderNotification', {
73+
timestamp: reminder.remindAt,
74+
});
75+
}
76+
// Within 59 min from now: relative
77+
// e.g. "Due in 30 minutes", "Due in a minute"
78+
return t('Due {{ timeLeft }}', {
79+
timeLeft: t('duration/Message reminder', {
80+
milliseconds: timeLeftMs,
81+
}),
82+
});
83+
};
84+
2785
return (
2886
<p className='str-chat__message-reminder'>
29-
<span>{t('Saved for later')}</span>
30-
{reminder?.remindAt && timeLeftMs !== null && (
31-
<>
32-
<span> | </span>
33-
<span>
34-
{isBehindRefreshBoundary
35-
? t('Due since {{ dueSince }}', {
36-
dueSince: t('timestamp/ReminderNotification', {
37-
timestamp: reminder.remindAt,
38-
}),
39-
})
40-
: t('Due {{ timeLeft }}', {
41-
timeLeft: t('duration/Message reminder', {
42-
milliseconds: timeLeftMs,
43-
}),
44-
})}
45-
</span>
46-
</>
47-
)}
87+
<IconBellNotification />
88+
<span>{t('Reminder set')}</span>
89+
<span> · </span>
90+
<span className='str-chat__message-reminder__time-left'>{renderTime()}</span>
4891
</p>
4992
);
93+
}
94+
95+
export const ReminderNotification = ({ reminder }: ReminderNotificationProps) => {
96+
if (!reminder) return null;
97+
98+
if (!reminder.remindAt) {
99+
return <SavedForLaterContent />;
100+
}
101+
102+
return <RemindMeContent reminder={reminder} />;
50103
};

src/components/Message/__tests__/ReminderNotification.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ const renderComponent = async ({ reminder }) => {
2121
};
2222

2323
describe('ReminderNotification', () => {
24-
it('displays text for bookmark notifications', async () => {
24+
it('displays text for bookmark notifications (saved for later)', async () => {
2525
const reminder = new Reminder({ data: generateReminderResponse() });
2626
const { container } = await renderComponent({ reminder });
2727
expect(container).toMatchSnapshot();
2828
});
29-
it('displays text for time due in case of timed reminders', async () => {
29+
it('displays text for time due in case of timed reminders (remind me)', async () => {
3030
const reminder = new Reminder({
3131
data: generateReminderResponse({
3232
scheduleOffsetMs: 60 * 1000,

src/components/Message/__tests__/__snapshots__/ReminderNotification.test.js.snap

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`ReminderNotification displays text for bookmark notifications 1`] = `
3+
exports[`ReminderNotification displays text for bookmark notifications (saved for later) 1`] = `
44
<div>
55
<p
6-
class="str-chat__message-reminder"
6+
class="str-chat__message-saved-for-later"
77
>
88
<span>
99
Saved for later
@@ -18,30 +18,34 @@ exports[`ReminderNotification displays text for reminder deadline if trespassed
1818
class="str-chat__message-reminder"
1919
>
2020
<span>
21-
Saved for later
21+
Reminder set
2222
</span>
2323
<span>
24-
|
24+
·
2525
</span>
26-
<span>
26+
<span
27+
class="str-chat__message-reminder__time-left"
28+
>
2729
Due since 01/01/1970
2830
</span>
2931
</p>
3032
</div>
3133
`;
3234

33-
exports[`ReminderNotification displays text for time due in case of timed reminders 1`] = `
35+
exports[`ReminderNotification displays text for time due in case of timed reminders (remind me) 1`] = `
3436
<div>
3537
<p
3638
class="str-chat__message-reminder"
3739
>
3840
<span>
39-
Saved for later
41+
Reminder set
4042
</span>
4143
<span>
42-
|
44+
·
4345
</span>
44-
<span>
46+
<span
47+
class="str-chat__message-reminder__time-left"
48+
>
4549
Due in a minute
4650
</span>
4751
</p>

src/components/Message/styling/Message.scss

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,6 @@
6767
--str-chat__tertiary-surface-color
6868
);
6969

70-
--str-chat__message-reminder-color: var(--str-chat__primary-color);
71-
--str-chat__message-reminder-background-color: transparent;
72-
--str-chat__message-reminder-border-block-start: none;
73-
--str-chat__message-reminder-border-block-end: none;
74-
--str-chat__message-reminder-border-inline-start: none;
75-
--str-chat__message-reminder-border-inline-end: none;
76-
--str-chat__message-reminder-box-shadow: none;
77-
--str-chat__message-reminder-border-radius: 0;
7870
--str-chat__message-reactions-host-offset-x: calc(var(--spacing-xs) * -1);
7971

8072
/* Background color for pinned messages (Figma: background/core/highlight) */
@@ -232,16 +224,9 @@
232224
hyphens: auto;
233225
overflow-wrap: break-word;
234226

235-
.str-chat__message-reminder {
236-
grid-area: message-reminder;
237-
padding-block: var(--spacing-xxs);
238-
margin: 0;
239-
@include utils.component-layer-overrides('message-reminder');
240-
font: var(--str-chat__caption-medium-text);
241-
}
242-
243227
@mixin message-grid-no-avatar {
244228
grid-template-areas:
229+
'message-saved-for-later'
245230
'pin-indicator'
246231
'message-reminder'
247232
'message'
@@ -253,6 +238,7 @@
253238

254239
@mixin message-grid-other-with-avatar {
255240
grid-template-areas:
241+
'. message-saved-for-later'
256242
'. pin-indicator'
257243
'. message-reminder'
258244
'avatar message'
@@ -264,6 +250,7 @@
264250

265251
@mixin message-grid-me-with-avatar {
266252
grid-template-areas:
253+
'message-saved-for-later .'
267254
'pin-indicator .'
268255
'message-reminder .'
269256
'message avatar'
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
@use '../../../styling/utils';
2+
3+
.str-chat {
4+
--str-chat__message-saved-for-later-color: var(--str-chat__primary-color);
5+
--str-chat__message-saved-for-later-background-color: transparent;
6+
--str-chat__message-saved-for-later-border-block-start: none;
7+
--str-chat__message-saved-for-later-border-block-end: none;
8+
--str-chat__message-saved-for-later-border-inline-start: none;
9+
--str-chat__message-saved-for-later-border-inline-end: none;
10+
--str-chat__message-saved-for-later-box-shadow: none;
11+
--str-chat__message-saved-for-later-border-radius: 0;
12+
13+
--str-chat__message-reminder-color: var(--text-primary);
14+
--str-chat__message-reminder-background-color: transparent;
15+
--str-chat__message-reminder-border-block-start: none;
16+
--str-chat__message-reminder-border-block-end: none;
17+
--str-chat__message-reminder-border-inline-start: none;
18+
--str-chat__message-reminder-border-inline-end: none;
19+
--str-chat__message-reminder-box-shadow: none;
20+
--str-chat__message-reminder-border-radius: 0;
21+
}
22+
23+
/* Saved for Later: above pin indicator. Font and spacing aligned with pin indicator. */
24+
.str-chat__message-saved-for-later {
25+
display: flex;
26+
align-items: center;
27+
gap: var(--spacing-xxs);
28+
grid-area: message-saved-for-later;
29+
padding-block: var(--spacing-xxs);
30+
margin: 0;
31+
@include utils.component-layer-overrides('message-saved-for-later');
32+
font: var(--str-chat__metadata-emphasis-text);
33+
34+
svg path {
35+
stroke-width: 1.5px;
36+
stroke: var(--str-chat__message-saved-for-later-color);
37+
}
38+
}
39+
40+
/* Remind Me: below pin indicator. Font and spacing aligned with pin indicator. */
41+
.str-chat__message-reminder {
42+
display: flex;
43+
align-items: center;
44+
gap: var(--spacing-xxs);
45+
grid-area: message-reminder;
46+
padding-block: var(--spacing-xxs);
47+
margin: 0;
48+
@include utils.component-layer-overrides('message-reminder');
49+
font: var(--str-chat__metadata-emphasis-text);
50+
51+
svg path {
52+
stroke-width: 1.5px;
53+
}
54+
55+
.str-chat__message-reminder__time-left {
56+
font: var(--str-chat__metadata-default-text);
57+
}
58+
}

src/components/Message/styling/index.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
@use 'MessageStatus';
44
@use 'MessageSystem';
55
@use 'QuotedMessage';
6+
@use 'ReminderNotification';
67
@use 'UnreadMessageNotification';
78
@use 'UnreadMessagesSeparator';
89
@use 'MessageRepliesCountButton';

src/i18n/de.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@
223223
"Recording format is not supported and cannot be reproduced": "Aufnahmeformat wird nicht unterstützt und kann nicht wiedergegeben werden",
224224
"Remind me": "Erinnern",
225225
"Remind Me": "Erinnern",
226+
"Reminder set": "Erinnerung gesetzt",
226227
"Remove reminder": "Erinnerung entfernen",
227228
"Remove save for later": "„Später ansehen“ entfernen",
228229
"Reply": "Antworten",
@@ -285,7 +286,7 @@
285286
"timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}",
286287
"timestamp/PollVote": "{{ timestamp | timestampFormatter(format: MMM D [at] HH:mm) }}",
287288
"timestamp/PollVoteTooltip": "{{ timestamp | timestampFormatter(calendar: true) }}",
288-
"timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true) }}",
289+
"timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Today] [at] HH:mm\", \"nextDay\": \"[Tomorrow] [at] HH:mm\", \"lastDay\": \"[Yesterday] [at] HH:mm\", \"nextWeek\": \"dddd [at] HH:mm\", \"lastWeek\": \"[Last] dddd [at] HH:mm\", \"sameElse\": \"ddd, D MMM [at] HH:mm\" }) }}",
289290
"timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}",
290291
"To start recording, allow the camera access in your browser": "Um mit der Aufnahme zu beginnen, erlauben Sie den Zugriff auf die Kamera in Ihrem Browser",
291292
"To start recording, allow the microphone access in your browser": "Um mit der Aufnahme zu beginnen, erlauben Sie den Zugriff auf das Mikrofon in Ihrem Browser",

src/i18n/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@
223223
"Recording format is not supported and cannot be reproduced": "Recording format is not supported and cannot be reproduced",
224224
"Remind me": "Remind me",
225225
"Remind Me": "Remind Me",
226+
"Reminder set": "Reminder set",
226227
"Remove reminder": "Remove reminder",
227228
"Remove save for later": "Remove save for later",
228229
"Reply": "Reply",
@@ -285,7 +286,7 @@
285286
"timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}",
286287
"timestamp/PollVote": "{{ timestamp | timestampFormatter(format: MMM D [at] HH:mm) }}",
287288
"timestamp/PollVoteTooltip": "{{ timestamp | timestampFormatter(calendar: true) }}",
288-
"timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true) }}",
289+
"timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Today] [at] HH:mm\", \"nextDay\": \"[Tomorrow] [at] HH:mm\", \"lastDay\": \"[Yesterday] [at] HH:mm\", \"nextWeek\": \"dddd [at] HH:mm\", \"lastWeek\": \"[Last] dddd [at] HH:mm\", \"sameElse\": \"ddd, D MMM [at] HH:mm\" }) }}",
289290
"timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}",
290291
"To start recording, allow the camera access in your browser": "To start recording, allow the camera access in your browser",
291292
"To start recording, allow the microphone access in your browser": "To start recording, allow the microphone access in your browser",

src/i18n/es.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@
228228
"Recording format is not supported and cannot be reproduced": "El formato de grabación no es compatible y no se puede reproducir",
229229
"Remind me": "Recordarme",
230230
"Remind Me": "Recordarme",
231+
"Reminder set": "Recordatorio establecido",
231232
"Remove reminder": "Eliminar recordatorio",
232233
"Remove save for later": "Quitar guardar para después",
233234
"Reply": "Responder",
@@ -294,7 +295,7 @@
294295
"timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}",
295296
"timestamp/PollVote": "{{ timestamp | timestampFormatter(format: MMM D [at] HH:mm) }}",
296297
"timestamp/PollVoteTooltip": "{{ timestamp | timestampFormatter(calendar: true) }}",
297-
"timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true) }}",
298+
"timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Today] [at] HH:mm\", \"nextDay\": \"[Tomorrow] [at] HH:mm\", \"lastDay\": \"[Yesterday] [at] HH:mm\", \"nextWeek\": \"dddd [at] HH:mm\", \"lastWeek\": \"[Last] dddd [at] HH:mm\", \"sameElse\": \"ddd, D MMM [at] HH:mm\" }) }}",
298299
"timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}",
299300
"To start recording, allow the camera access in your browser": "Para comenzar a grabar, permita el acceso a la cámara en su navegador",
300301
"To start recording, allow the microphone access in your browser": "Para comenzar a grabar, permita el acceso al micrófono en su navegador",

0 commit comments

Comments
 (0)