Skip to content

Commit 5b59439

Browse files
committed
feat: add reminders implementation
1 parent 19b994c commit 5b59439

31 files changed

+1008
-60
lines changed

examples/SampleApp/App.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import {
1010
OverlayProvider,
1111
setupCommandUIMiddlewares,
1212
SqliteClient,
13+
Streami18n,
1314
ThemeProvider,
1415
useOverlayContext,
16+
enTranslations,
1517
} from 'stream-chat-react-native';
1618
import { getMessaging } from '@react-native-firebase/messaging';
1719
import notifee, { EventType } from '@notifee/react-native';
@@ -201,16 +203,28 @@ const DrawerNavigatorWrapper: React.FC<{
201203
chatClient: StreamChat;
202204
}> = ({ chatClient }) => {
203205
const streamChatTheme = useStreamChatTheme();
206+
const streami18n = new Streami18n();
207+
208+
streami18n.registerTranslation('en', {
209+
...enTranslations,
210+
'Due since {{ dueSince }}': 'Due since {{ dueSince }}',
211+
'Due {{ timeLeft }}': 'Due {{ timeLeft }}',
212+
'duration/Message reminder': '{{ milliseconds | durationFormatter(withSuffix: true) }}',
213+
'duration/Remind Me': '{{ milliseconds | durationFormatter(withSuffix: true) }}',
214+
'timestamp/Remind me': '{{ milliseconds | durationFormatter(withSuffix: true) }}',
215+
'timestamp/ReminderNotification': '{{ timestamp | timestampFormatter(calendar: true) }}',
216+
});
204217

205218
return (
206219
<GestureHandlerRootView style={{ flex: 1 }}>
207-
<OverlayProvider value={{ style: streamChatTheme }}>
220+
<OverlayProvider value={{ style: streamChatTheme }} i18nInstance={streami18n}>
208221
<Chat
209222
client={chatClient}
210223
enableOfflineSupport
211224
// @ts-expect-error - the `ImageComponent` prop is generic, meaning we can expect an error
212225
ImageComponent={FastImage}
213226
isMessageAIGenerated={isMessageAIGenerated}
227+
i18nInstance={streami18n}
214228
>
215229
<AppOverlayProvider>
216230
<UserSearchProvider>

examples/SampleApp/ios/Podfile.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2425,7 +2425,7 @@ PODS:
24252425
- libwebp (~> 1.0)
24262426
- SDWebImage/Core (~> 5.10)
24272427
- SocketRocket (0.7.1)
2428-
- stream-chat-react-native (7.2.0):
2428+
- stream-chat-react-native (8.0.0):
24292429
- DoubleConversion
24302430
- glog
24312431
- hermes-engine
@@ -2872,7 +2872,7 @@ SPEC CHECKSUMS:
28722872
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
28732873
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
28742874
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
2875-
stream-chat-react-native: 2de2866392bfecadf005ec5f0b1f882b613b89ba
2875+
stream-chat-react-native: 14526230f326d59b638dd1e2bd36f105045c7065
28762876
Yoga: b2eaabf17044cd4273a661b14eb83f9fd2c90491
28772877

28782878
PODFILE CHECKSUM: 4f662370295f8f9cee909f1a4c59a614999a209d

examples/SampleApp/src/components/BottomTabs.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { MentionsTab } from '../icons/MentionsTab';
1212
import type { BottomTabBarProps } from '@react-navigation/bottom-tabs';
1313
import type { Route } from '@react-navigation/native';
1414
import { DraftsTab } from '../icons/DraftsTab';
15+
import { RemindersTab } from '../icons/ReminderTab';
1516

1617
const styles = StyleSheet.create({
1718
notification: {
@@ -50,7 +51,6 @@ const getTab = (key: string) => {
5051
icon: <DraftsTab />,
5152
iconActive: <DraftsTab active />,
5253
title: 'Drafts',
53-
notification: <ChannelsUnreadCountBadge />,
5454
};
5555
case 'ThreadsScreen':
5656
return {
@@ -65,6 +65,13 @@ const getTab = (key: string) => {
6565
iconActive: <MentionsTab active />,
6666
title: 'Mentions',
6767
};
68+
case 'RemindersScreen':
69+
return {
70+
icon: <RemindersTab />,
71+
iconActive: <RemindersTab active />,
72+
title: 'Reminders',
73+
};
74+
// Add more cases for other tabs as needed
6875
default:
6976
return null;
7077
}

examples/SampleApp/src/components/DraftsList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ export const DraftsList = () => {
187187
}, [draftsManager]);
188188

189189
const onEndReached = useCallback(() => {
190-
draftsManager.loadNextPage();
190+
draftsManager.loadNextPage();
191191
}, [draftsManager]);
192192

193193
return (
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import {
2+
MessageFooterProps,
3+
Time,
4+
useStateStore,
5+
useTranslationContext,
6+
} from 'stream-chat-react-native';
7+
import { useMessageReminder } from '../../hooks/useMessageReminder';
8+
import { ReminderState } from 'stream-chat';
9+
import { StyleSheet, Text, View } from 'react-native';
10+
11+
const reminderStateSelector = (state: ReminderState) => ({
12+
timeLeftMs: state.timeLeftMs,
13+
});
14+
15+
export const MessageReminderHeader = ({ message }: MessageFooterProps) => {
16+
const messageId = message?.id ?? '';
17+
const reminder = useMessageReminder(messageId);
18+
const { timeLeftMs } = useStateStore(reminder?.state, reminderStateSelector) ?? {};
19+
const { t } = useTranslationContext();
20+
21+
const stopRefreshBoundaryMs = reminder?.timer.stopRefreshBoundaryMs;
22+
const stopRefreshTimeStamp =
23+
reminder?.remindAt && stopRefreshBoundaryMs
24+
? reminder?.remindAt.getTime() + stopRefreshBoundaryMs
25+
: undefined;
26+
27+
const isBehindRefreshBoundary =
28+
!!stopRefreshTimeStamp && new Date().getTime() > stopRefreshTimeStamp;
29+
30+
if (!reminder) {
31+
return null;
32+
}
33+
34+
// This is for "Saved for Later"
35+
if (!reminder.remindAt) {
36+
return (
37+
<View>
38+
<Text style={styles.headerTitle}>🔖 Saved for Later</Text>
39+
</View>
40+
);
41+
}
42+
43+
if (reminder.remindAt && timeLeftMs !== null) {
44+
return (
45+
<View style={styles.headerContainer}>
46+
<Time height={16} width={16} />
47+
<Text style={styles.headerTitle}>
48+
{isBehindRefreshBoundary
49+
? t('Due since {{ dueSince }}', {
50+
dueSince: t('timestamp/ReminderNotification', {
51+
timestamp: reminder.remindAt,
52+
}),
53+
})
54+
: t('Due {{ timeLeft }}', {
55+
timeLeft: t('duration/Message reminder', {
56+
milliseconds: timeLeftMs,
57+
}),
58+
})}
59+
</Text>
60+
</View>
61+
);
62+
}
63+
};
64+
65+
const styles = StyleSheet.create({
66+
headerContainer: {
67+
flexDirection: 'row',
68+
alignItems: 'center',
69+
},
70+
headerTitle: {
71+
fontSize: 14,
72+
fontWeight: '500',
73+
marginLeft: 4,
74+
},
75+
});
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { StyleSheet, Text, View } from 'react-native';
2+
import { ReminderResponse, ReminderState } from 'stream-chat';
3+
import { useTheme, useTranslationContext, useStateStore } from 'stream-chat-react-native';
4+
import { useMessageReminder } from '../../hooks/useMessageReminder';
5+
6+
const reminderStateSelector = (state: ReminderState) => ({
7+
timeLeftMs: state.timeLeftMs,
8+
});
9+
10+
export const ReminderBanner = (item: ReminderResponse) => {
11+
const {
12+
theme: {
13+
colors: { accent_blue, accent_red },
14+
},
15+
} = useTheme();
16+
const { t } = useTranslationContext();
17+
const { message } = item;
18+
const reminder = useMessageReminder(message.id);
19+
const { timeLeftMs } = useStateStore(reminder?.state, reminderStateSelector) ?? {};
20+
const stopRefreshBoundaryMs = reminder?.timer.stopRefreshBoundaryMs;
21+
const stopRefreshTimeStamp =
22+
reminder?.remindAt && stopRefreshBoundaryMs
23+
? reminder?.remindAt.getTime() + stopRefreshBoundaryMs
24+
: undefined;
25+
26+
const isBehindRefreshBoundary =
27+
!!stopRefreshTimeStamp && new Date().getTime() > stopRefreshTimeStamp;
28+
29+
if (!reminder?.remindAt) {
30+
return <Text style={styles.date}>🔖</Text>;
31+
} else if (reminder.remindAt && timeLeftMs) {
32+
return (
33+
<View
34+
style={[
35+
styles.bannerContainer,
36+
{ backgroundColor: timeLeftMs > 0 ? accent_blue : accent_red },
37+
]}
38+
>
39+
<Text style={styles.date}>
40+
{isBehindRefreshBoundary
41+
? t('Due since {{ dueSince }}', {
42+
dueSince: t('timestamp/ReminderNotification', {
43+
timestamp: reminder.remindAt,
44+
}),
45+
})
46+
: t('Due {{ timeLeft }}', {
47+
timeLeft: t('duration/Message reminder', {
48+
milliseconds: timeLeftMs,
49+
}),
50+
})}
51+
</Text>
52+
</View>
53+
);
54+
}
55+
};
56+
57+
const styles = StyleSheet.create({
58+
bannerContainer: {
59+
paddingHorizontal: 8,
60+
borderRadius: 16,
61+
paddingVertical: 4,
62+
},
63+
date: {
64+
color: 'white',
65+
fontWeight: '500',
66+
fontSize: 12,
67+
},
68+
});

0 commit comments

Comments
 (0)