Skip to content

Commit 57c28b4

Browse files
authored
feat: show delivery status and read status on the message and channel preview (#3258)
* fix: add expo PN entitelments on app.json * fix: useChannelPreviewData types * feat: add delivery count to the message status * fix: useMessageDeliveryStatus * fix: refine the schema for offline support * fix: add useMessageReadData optimization * fix: hook * fix: hook * feat: add delivery receipt support inside push notification * feat: add delivery receipt support inside push notification * feat: add delivery receipt support inside push notification * perf: optimization in the hook * fix: change deliveredBy to deliveredToCount * fix: further optimize Message status component * fix: further optimize Message status component
1 parent eff8da9 commit 57c28b4

23 files changed

+492
-210
lines changed

examples/SampleApp/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import './src/utils/bootstrapBackgroundMessageHandler';
12
import 'react-native-gesture-handler';
23
import { AppRegistry } from 'react-native';
34
import { enableScreens } from 'react-native-screens';

examples/SampleApp/src/hooks/useChatClient.ts

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { useEffect, useRef, useState } from 'react';
22
import { StreamChat, PushProvider } from 'stream-chat';
3-
import { getMessaging, AuthorizationStatus } from '@react-native-firebase/messaging';
3+
import {
4+
getMessaging,
5+
AuthorizationStatus,
6+
FirebaseMessagingTypes,
7+
} from '@react-native-firebase/messaging';
48
import notifee from '@notifee/react-native';
59
import { SqliteClient } from 'stream-chat-react-native';
610
import { USER_TOKENS, USERS } from '../ChatUsers';
@@ -11,6 +15,30 @@ import { PermissionsAndroid, Platform } from 'react-native';
1115

1216
const messaging = getMessaging();
1317

18+
const displayNotification = async (
19+
remoteMessage: FirebaseMessagingTypes.RemoteMessage,
20+
channelId: string,
21+
) => {
22+
const { stream, ...rest } = remoteMessage.data ?? {};
23+
const data = {
24+
...rest,
25+
...((stream as unknown as Record<string, string> | undefined) ?? {}), // extract and merge stream object if present
26+
};
27+
if (data.body && data.title) {
28+
await notifee.displayNotification({
29+
android: {
30+
channelId,
31+
pressAction: {
32+
id: 'default',
33+
},
34+
},
35+
body: data.body as string,
36+
title: data.title as string,
37+
data,
38+
});
39+
}
40+
};
41+
1442
// Request Push Notification permission from device.
1543
const requestNotificationPermission = async () => {
1644
const authStatus = await messaging.requestPermission();
@@ -94,34 +122,12 @@ export const useChatClient = () => {
94122
});
95123
// show notifications when on foreground
96124
const unsubscribeForegroundMessageReceive = messaging.onMessage(async (remoteMessage) => {
97-
const { stream, ...rest } = remoteMessage.data ?? {};
98-
const data = {
99-
...rest,
100-
...((stream as unknown as Record<string, string> | undefined) ?? {}), // extract and merge stream object if present
101-
};
102125
const channelId = await notifee.createChannel({
103126
id: 'foreground',
104127
name: 'Foreground Messages',
105128
});
106-
// create the android channel to send the notification to
107-
// display the notification on foreground
108-
const notification = remoteMessage.notification ?? {};
109-
const body = (data.body ?? notification.body) as string;
110-
const title = (data.title ?? notification.title) as string;
111-
112-
if (body && title) {
113-
await notifee.displayNotification({
114-
android: {
115-
channelId,
116-
pressAction: {
117-
id: 'default',
118-
},
119-
},
120-
body,
121-
title,
122-
data,
123-
});
124-
}
129+
130+
await displayNotification(remoteMessage, channelId);
125131
});
126132

127133
unsubscribePushListenersRef.current = () => {
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { LoginConfig } from '../types';
2+
import AsyncStore from './AsyncStore';
3+
import {
4+
FirebaseMessagingTypes,
5+
setBackgroundMessageHandler,
6+
} from '@react-native-firebase/messaging';
7+
import { DeliveredMessageConfirmation, StreamChat } from 'stream-chat';
8+
import notifee from '@notifee/react-native';
9+
import { getMessaging } from '@react-native-firebase/messaging';
10+
11+
const messaging = getMessaging();
12+
13+
const displayNotification = async (
14+
remoteMessage: FirebaseMessagingTypes.RemoteMessage,
15+
channelId: string,
16+
) => {
17+
const { stream, ...rest } = remoteMessage.data ?? {};
18+
const data = {
19+
...rest,
20+
...((stream as unknown as Record<string, string> | undefined) ?? {}), // extract and merge stream object if present
21+
};
22+
if (data.body && data.title) {
23+
await notifee.displayNotification({
24+
android: {
25+
channelId,
26+
pressAction: {
27+
id: 'default',
28+
},
29+
},
30+
body: data.body as string,
31+
title: data.title as string,
32+
data,
33+
});
34+
}
35+
};
36+
37+
setBackgroundMessageHandler(messaging, async (remoteMessage) => {
38+
try {
39+
const loginConfig = await AsyncStore.getItem<LoginConfig>(
40+
'@stream-rn-sampleapp-login-config',
41+
null,
42+
);
43+
if (!loginConfig) {
44+
return;
45+
}
46+
const chatClient = StreamChat.getInstance(loginConfig.apiKey);
47+
await chatClient._setToken({ id: loginConfig.userId }, loginConfig.userToken);
48+
49+
const notification = remoteMessage.data;
50+
51+
const deliverMessageConfirmation = [
52+
{
53+
cid: notification?.cid,
54+
id: notification?.id,
55+
},
56+
];
57+
58+
await chatClient?.markChannelsDelivered({
59+
latest_delivered_messages: deliverMessageConfirmation as DeliveredMessageConfirmation[],
60+
});
61+
// create the android channel to send the notification to
62+
const channelId = await notifee.createChannel({
63+
id: 'chat-messages',
64+
name: 'Chat Messages',
65+
});
66+
// display the notification
67+
await displayNotification(remoteMessage, channelId);
68+
} catch (error) {
69+
console.error(error);
70+
}
71+
});

package/src/components/ChannelPreview/ChannelPreviewStatus.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { StyleSheet, Text, View } from 'react-native';
33

44
import { ChannelPreviewProps } from './ChannelPreview';
55
import type { ChannelPreviewMessengerPropsWithContext } from './ChannelPreviewMessenger';
6-
import { MessageReadStatus } from './hooks/useLatestMessagePreview';
6+
7+
import { MessageDeliveryStatus } from './hooks/useMessageDeliveryStatus';
78

89
import { useTheme } from '../../contexts/themeContext/ThemeContext';
910
import { useTranslationContext } from '../../contexts/translationContext/TranslationContext';
@@ -38,7 +39,7 @@ export const ChannelPreviewStatus = (props: ChannelPreviewStatusProps) => {
3839
},
3940
} = useTheme();
4041

41-
const created_at = latestMessagePreview.messageObject?.created_at;
42+
const created_at = latestMessagePreview?.created_at;
4243
const latestMessageDate = created_at ? new Date(created_at) : new Date();
4344

4445
const formattedDate = useMemo(
@@ -55,9 +56,11 @@ export const ChannelPreviewStatus = (props: ChannelPreviewStatusProps) => {
5556

5657
return (
5758
<View style={styles.flexRow}>
58-
{status === MessageReadStatus.READ ? (
59+
{status === MessageDeliveryStatus.READ ? (
5960
<CheckAll pathFill={accent_blue} {...checkAllIcon} />
60-
) : status === MessageReadStatus.UNREAD ? (
61+
) : status === MessageDeliveryStatus.DELIVERED ? (
62+
<CheckAll pathFill={grey} {...checkAllIcon} />
63+
) : status === MessageDeliveryStatus.SENT ? (
6164
<Check pathFill={grey} {...checkIcon} />
6265
) : null}
6366
<Text style={[styles.date, { color: grey }, date]}>

package/src/components/ChannelPreview/hooks/useChannelPreviewData.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { type SetStateAction, useEffect, useMemo, useState } from 'react';
22

33
import throttle from 'lodash/throttle';
4-
import type { Channel, ChannelState, Event, MessageResponse, StreamChat } from 'stream-chat';
4+
import type { Channel, Event, LocalMessage, MessageResponse, StreamChat } from 'stream-chat';
55

66
import { useIsChannelMuted } from './useIsChannelMuted';
77

@@ -16,7 +16,7 @@ const setLastMessageThrottleOptions = { leading: true, trailing: true };
1616
const refreshUnreadCountThrottleTimeout = 400;
1717
const refreshUnreadCountThrottleOptions = setLastMessageThrottleOptions;
1818

19-
type LastMessageType = ReturnType<ChannelState['formatMessage']> | MessageResponse;
19+
type LastMessageType = LocalMessage | MessageResponse;
2020

2121
export const useChannelPreviewData = (
2222
channel: Channel,
@@ -172,7 +172,11 @@ export const useChannelPreviewData = (
172172
return () => listeners.forEach((l) => l.unsubscribe());
173173
}, [channel, refreshUnreadCount, forceUpdate, channelListForceUpdate, setLastMessage]);
174174

175-
const latestMessagePreview = useLatestMessagePreview(channel, forceUpdate, lastMessage);
175+
const latestMessagePreview = useLatestMessagePreview(
176+
channel,
177+
forceUpdate,
178+
lastMessage as LocalMessage,
179+
);
176180

177181
return { latestMessagePreview, muted, unread };
178182
};

0 commit comments

Comments
 (0)