Skip to content

Commit 2d79fc7

Browse files
Merge pull request #658 from GetStream/vishal/network-fix
Network recovery fix
2 parents 097d034 + 95c2b54 commit 2d79fc7

File tree

16 files changed

+438
-172
lines changed

16 files changed

+438
-172
lines changed

src/components/Channel/Channel.tsx

Lines changed: 139 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -699,7 +699,7 @@ const ChannelWithContext = <
699699

700700
const connectionChangedHandler = (event: ConnectionChangeEvent) => {
701701
if (event.online) {
702-
reloadChannel();
702+
resyncChannel();
703703
}
704704
};
705705

@@ -746,6 +746,7 @@ const ChannelWithContext = <
746746
try {
747747
await queryCall();
748748
setLastRead(new Date());
749+
setHasMore(true);
749750
copyChannelState();
750751
} catch (err) {
751752
setError(err);
@@ -879,11 +880,86 @@ const ChannelWithContext = <
879880
return;
880881
});
881882

882-
const reloadChannel = () => {
883-
channel?.state.clearMessages();
884-
return loadChannel();
883+
const resyncChannel = async () => {
884+
if (!channel) return;
885+
886+
setError(false);
887+
try {
888+
/**
889+
* Allow a buffer of 30 new messages, so that MessageList won't move its scroll position,
890+
* giving smooth user experience.
891+
*/
892+
const state = await channel.watch({
893+
messages: {
894+
limit: messages.length + 30,
895+
},
896+
});
897+
898+
const oldListTopMessage = messages[0];
899+
const oldListTopMessageId = messages[0]?.id;
900+
const oldListBottomMessage = messages[messages.length - 1];
901+
902+
const newListTopMessage = state.messages[0];
903+
const newListBottomMessage = state.messages[state.messages.length - 1];
904+
905+
if (
906+
!oldListTopMessage || // previous list was empty
907+
!oldListBottomMessage || // previous list was empty
908+
!newListTopMessage || // new list is truncated
909+
!newListBottomMessage // new list is truncated
910+
) {
911+
/** Channel was truncated */
912+
channel.state.clearMessages();
913+
channel.state.setIsUpToDate(true);
914+
channel.state.addMessagesSorted(state.messages);
915+
copyChannelState();
916+
return;
917+
}
918+
919+
const oldListTopMessageCreatedAt = oldListTopMessage.created_at;
920+
const oldListBottomMessageCreatedAt = oldListBottomMessage.created_at;
921+
const newListTopMessageCreatedAt = newListTopMessage.created_at
922+
? new Date(newListTopMessage.created_at)
923+
: new Date();
924+
const newListBottomMessageCreatedAt = newListBottomMessage?.created_at
925+
? new Date(newListBottomMessage.created_at)
926+
: new Date();
927+
928+
let finalMessages = [];
929+
930+
if (
931+
oldListTopMessage &&
932+
oldListTopMessageCreatedAt &&
933+
oldListBottomMessageCreatedAt &&
934+
newListTopMessageCreatedAt < oldListTopMessageCreatedAt &&
935+
newListBottomMessageCreatedAt >= oldListBottomMessageCreatedAt
936+
) {
937+
const index = state.messages.findIndex(
938+
(message) => message.id === oldListTopMessageId,
939+
);
940+
finalMessages = state.messages.slice(index);
941+
} else {
942+
finalMessages = state.messages;
943+
}
944+
945+
channel.state.setIsUpToDate(true);
946+
947+
channel.state.clearMessages();
948+
channel.state.addMessagesSorted(finalMessages);
949+
setHasMore(true);
950+
copyChannelState();
951+
} catch (err) {
952+
setError(err);
953+
setLoading(false);
954+
}
885955
};
886956

957+
const reloadChannel = () =>
958+
channelQueryCall(async () => {
959+
await channel?.watch();
960+
channel?.state.setIsUpToDate(true);
961+
});
962+
887963
/**
888964
* Makes a query to load messages in channel.
889965
*/
@@ -1206,6 +1282,7 @@ const ChannelWithContext = <
12061282
newMessages: ChannelState<At, Ch, Co, Ev, Me, Re, Us>['messages'],
12071283
) => {
12081284
setLoadingMore(false);
1285+
setError(false);
12091286
setHasMore(updatedHasMore);
12101287
setMessages(newMessages);
12111288
},
@@ -1220,7 +1297,9 @@ const ChannelWithContext = <
12201297
Re,
12211298
Us
12221299
>['loadMore'] = async () => {
1223-
if (loadingMore || hasMore === false) return;
1300+
if (loadingMore || hasMore === false) {
1301+
return;
1302+
}
12241303
setLoadingMore(true);
12251304

12261305
if (!messages.length) {
@@ -1247,7 +1326,9 @@ const ChannelWithContext = <
12471326
}
12481327
} catch (err) {
12491328
console.warn('Message pagination request failed with error', err);
1250-
return setLoadingMore(false);
1329+
setError(err);
1330+
setLoadingMore(false);
1331+
throw err;
12511332
}
12521333
};
12531334

@@ -1280,8 +1361,9 @@ const ChannelWithContext = <
12801361
}
12811362
} catch (err) {
12821363
console.warn('Message pagination request failed with error', err);
1364+
setError(err);
12831365
setLoadingMoreRecent(false);
1284-
return;
1366+
throw err;
12851367
}
12861368
};
12871369

@@ -1290,6 +1372,7 @@ const ChannelWithContext = <
12901372
(newMessages: ChannelState<At, Ch, Co, Ev, Me, Re, Us>['messages']) => {
12911373
setLoadingMoreRecent(false);
12921374
setMessages(newMessages);
1375+
setError(false);
12931376
},
12941377
);
12951378

@@ -1426,24 +1509,63 @@ const ChannelWithContext = <
14261509
Re,
14271510
Us
14281511
>['loadMoreThread'] = async () => {
1429-
if (threadLoadingMore || !thread?.id) return;
1512+
if (threadLoadingMore || !thread?.id) {
1513+
return;
1514+
}
14301515
setThreadLoadingMore(true);
14311516

1432-
if (channel) {
1433-
const parentID = thread.id;
1517+
try {
1518+
if (channel) {
1519+
const parentID = thread.id;
14341520

1435-
const oldMessages = channel.state.threads[parentID] || [];
1436-
const oldestMessageID = oldMessages?.[0]?.id;
1521+
/**
1522+
* In the channel is re-initializing, then threads may get wiped out during the process
1523+
* (check `addMessagesSorted` method on channel.state). In those cases, we still want to
1524+
* preserve the messages on active thread, so lets simply copy messages from UI state to
1525+
* `channel.state`.
1526+
*/
1527+
channel.state.threads[parentID] = threadMessages;
1528+
const oldestMessageID = threadMessages?.[0]?.id;
1529+
1530+
const limit = 50;
1531+
const queryResponse = await channel.getReplies(parentID, {
1532+
id_lt: oldestMessageID,
1533+
limit,
1534+
});
1535+
1536+
const updatedHasMore = queryResponse.messages.length === limit;
1537+
const updatedThreadMessages = channel.state.threads[parentID] || [];
1538+
loadMoreThreadFinished(updatedHasMore, updatedThreadMessages);
1539+
}
1540+
} catch (err) {
1541+
console.warn('Message pagination request failed with error', err);
1542+
setError(err);
1543+
setThreadLoadingMore(false);
1544+
throw err;
1545+
}
1546+
};
1547+
1548+
const reloadThread = async () => {
1549+
if (!channel || !thread?.id) return;
1550+
1551+
setThreadLoadingMore(true);
1552+
try {
1553+
const parentID = thread.id;
14371554

14381555
const limit = 50;
1556+
channel.state.threads[parentID] = [];
14391557
const queryResponse = await channel.getReplies(parentID, {
1440-
id_lt: oldestMessageID,
1441-
limit,
1558+
limit: 50,
14421559
});
14431560

14441561
const updatedHasMore = queryResponse.messages.length === limit;
14451562
const updatedThreadMessages = channel.state.threads[parentID] || [];
14461563
loadMoreThreadFinished(updatedHasMore, updatedThreadMessages);
1564+
} catch (err) {
1565+
console.warn('Thread loading request failed with error', err);
1566+
setError(err);
1567+
setThreadLoadingMore(false);
1568+
throw err;
14471569
}
14481570
};
14491571

@@ -1625,6 +1747,7 @@ const ChannelWithContext = <
16251747
closeThread,
16261748
loadMoreThread,
16271749
openThread,
1750+
reloadThread,
16281751
setThreadLoadingMore,
16291752
thread,
16301753
threadHasMore,
@@ -1636,14 +1759,12 @@ const ChannelWithContext = <
16361759
typing,
16371760
});
16381761

1639-
if (!channel || error) {
1762+
if (!channel || (error && messages.length === 0)) {
16401763
return (
16411764
<LoadingErrorIndicator
16421765
error={error}
16431766
listType='message'
1644-
retry={() => {
1645-
loadMore();
1646-
}}
1767+
retry={reloadChannel}
16471768
/>
16481769
);
16491770
}

src/components/Channel/hooks/useCreateThreadContext.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const useCreateThreadContext = <
2525
closeThread,
2626
loadMoreThread,
2727
openThread,
28+
reloadThread,
2829
setThreadLoadingMore,
2930
thread,
3031
threadHasMore,
@@ -50,6 +51,7 @@ export const useCreateThreadContext = <
5051
closeThread,
5152
loadMoreThread,
5253
openThread,
54+
reloadThread,
5355
setThreadLoadingMore,
5456
thread,
5557
threadHasMore,
@@ -58,6 +60,7 @@ export const useCreateThreadContext = <
5860
}),
5961
[
6062
allowThreadMessagesInChannel,
63+
loadMoreThread,
6164
threadHasMore,
6265
threadId,
6366
threadLoadingMore,

src/components/ChannelList/hooks/usePaginatedChannels.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ export const usePaginatedChannels = <
9494
newOptions,
9595
);
9696

97+
channelQueryResponse.forEach((channel) =>
98+
channel.state.setIsUpToDate(true),
99+
);
100+
97101
const newChannels =
98102
queryType === 'reload' || queryType === 'refresh'
99103
? channelQueryResponse

src/components/Chat/Chat.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import React, { PropsWithChildren, useEffect, useState } from 'react';
22
import { Platform } from 'react-native';
33
import Dayjs from 'dayjs';
44

5-
import { useAppStateListener } from './hooks/useAppStateListener';
65
import { useCreateChatContext } from './hooks/useCreateChatContext';
76
import { useIsOnline } from './hooks/useIsOnline';
87

@@ -175,12 +174,7 @@ const ChatWithContext = <
175174
Me,
176175
Re,
177176
Us
178-
>(client);
179-
180-
useAppStateListener<At, Ch, Co, Ev, Me, Re, Us>(
181-
client,
182-
closeConnectionOnBackground,
183-
);
177+
>(client, closeConnectionOnBackground);
184178

185179
useEffect(() => {
186180
if (client.setUserAgent) {
Lines changed: 23 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,32 @@
1-
import { useEffect, useRef } from 'react';
1+
import { useEffect, useState } from 'react';
22
import { AppState, AppStateStatus } from 'react-native';
33

4-
import type { StreamChat } from 'stream-chat';
5-
6-
import type {
7-
DefaultAttachmentType,
8-
DefaultChannelType,
9-
DefaultCommandType,
10-
DefaultEventType,
11-
DefaultMessageType,
12-
DefaultReactionType,
13-
DefaultUserType,
14-
UnknownType,
15-
} from '../../../types/types';
16-
17-
/**
18-
* Disconnect the websocket connection when app goes to background,
19-
* and reconnect when app comes to foreground.
20-
* We do this to make sure, user receives push notifications when app is in the background.
21-
* You can't receive push notification until you have active websocket connection.
22-
*/
23-
export const useAppStateListener = <
24-
At extends UnknownType = DefaultAttachmentType,
25-
Ch extends UnknownType = DefaultChannelType,
26-
Co extends string = DefaultCommandType,
27-
Ev extends UnknownType = DefaultEventType,
28-
Me extends UnknownType = DefaultMessageType,
29-
Re extends UnknownType = DefaultReactionType,
30-
Us extends UnknownType = DefaultUserType
31-
>(
32-
client: StreamChat<At, Ch, Co, Ev, Me, Re, Us>,
33-
closeConnectionOnBackground: boolean,
4+
export const useAppStateListener = (
5+
onForeground?: () => void,
6+
onBackground?: () => void,
347
) => {
35-
const appState = useRef(AppState.currentState);
36-
37-
useEffect(() => {
38-
closeConnectionOnBackground &&
39-
AppState.addEventListener('change', handleAppStateChange);
40-
41-
return () => {
42-
closeConnectionOnBackground &&
43-
AppState.removeEventListener('change', handleAppStateChange);
44-
};
45-
}, [closeConnectionOnBackground]);
8+
const [appState, setAppState] = useState(AppState.currentState);
469

47-
const handleAppStateChange = async (nextAppState: AppStateStatus) => {
48-
if (appState.current === 'background' && nextAppState === 'active') {
49-
await client.openConnection();
10+
const handleAppStateChange = (nextAppState: AppStateStatus) => {
11+
if (
12+
appState === 'background' &&
13+
nextAppState === 'active' &&
14+
onForeground
15+
) {
16+
onForeground();
5017
} else if (
51-
appState.current.match(/active|inactive/) &&
52-
nextAppState === 'background'
18+
appState.match(/active|inactive/) &&
19+
nextAppState === 'background' &&
20+
onBackground
5321
) {
54-
await client.closeConnection();
55-
for (const cid in client.activeChannels) {
56-
const channel = client.activeChannels[cid];
57-
channel.state.setIsUpToDate(false);
58-
}
22+
onBackground();
5923
}
60-
61-
appState.current = nextAppState;
24+
setAppState(nextAppState);
6225
};
26+
27+
useEffect(() => {
28+
AppState.addEventListener('change', handleAppStateChange);
29+
30+
return () => AppState.removeEventListener('change', handleAppStateChange);
31+
}, [handleAppStateChange]);
6332
};

0 commit comments

Comments
 (0)