Skip to content

Commit 4f0e3a9

Browse files
authored
feat: Update conversation/messages when app opens from background state (#904)
* feat: update conversation/messages in background state * chore: add delay while fetching the conversaions * chore: remove console logs
1 parent e34631d commit 4f0e3a9

File tree

5 files changed

+84
-7
lines changed

5 files changed

+84
-7
lines changed

app.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export default ({ config }: ConfigContext): ExpoConfig => {
44
return {
55
name: 'Chatwoot',
66
slug: process.env.EXPO_PUBLIC_APP_SLUG || 'chatwoot-mobile',
7-
version: '4.0.10',
7+
version: '4.0.11',
88
orientation: 'portrait',
99
icon: './assets/icon.png',
1010
userInterfaceStyle: 'light',

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@chatwoot/mobile-app",
3-
"version": "4.0.10",
3+
"version": "4.0.11",
44
"main": "expo/AppEntry.js",
55
"scripts": {
66
"start": "expo start",

src/constants/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ export const SCREENS = {
22
LOGIN: 'Login',
33
CONFIG_URL: 'ConfigureURL',
44
CONVERSATION: 'ConversationScreen',
5+
// Deprecated
56
NOTIFICATION: 'NotificationScreen',
67
SETTINGS: 'SettingsScreen',
78
DETAIL: 'Detail',
89
CHAT: 'ChatScreen',
10+
INBOX: 'InboxScreen',
911
};
1012

1113
export const TAB_BAR_HEIGHT = 83;
@@ -305,3 +307,6 @@ export const MESSAGE_VARIANTS = {
305307
EMAIL: 'email',
306308
UNSUPPORTED: 'unsupported',
307309
};
310+
311+
export const LAST_ACTIVE_TIMESTAMP_KEY = 'lastActiveTimestamp';
312+
export const LAST_ACTIVE_TIMESTAMP_THRESHOLD = 1000 * 60 * 1; // 1 minute

src/screens/chat-screen/components/message-list/MessagesListContainer.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import React, { useCallback, useEffect } from 'react';
1+
import React, { useCallback, useEffect, useState } from 'react';
22
import { useAppSelector, useAppDispatch } from '@/hooks';
33
import { useChatWindowContext } from '@/context';
4-
import { Platform } from 'react-native';
4+
import { AppState, Platform } from 'react-native';
55
import { KeyboardGestureArea } from 'react-native-keyboard-controller';
66
import { flatMap } from 'lodash';
77
import useDeepCompareEffect from 'use-deep-compare-effect';
@@ -18,10 +18,11 @@ import { getGroupedMessages, isAnEmailChannel } from '@/utils';
1818
import { MessagesList } from './MessagesList';
1919
import tailwind from 'twrnc';
2020
import { conversationParticipantActions } from '@/store/conversation-participant/conversationParticipantActions';
21-
import { MESSAGE_TYPES } from '@/constants';
21+
import { MESSAGE_TYPES, SCREENS } from '@/constants';
2222
import { Message } from '@/types';
2323
import { selectInboxById } from '@/store/inbox/inboxSelectors';
2424
import { selectUserId } from '@/store/auth/authSelectors';
25+
import { getCurrentRouteName } from '@/utils/navigationUtils';
2526

2627
type DateSeparator = { date: string; type: 'date' };
2728
type MessageOrDate = Message | DateSeparator;
@@ -69,6 +70,7 @@ const PlatformSpecificKeyboardWrapperComponent =
6970
Platform.OS === 'android' ? Animated.View : KeyboardGestureArea;
7071

7172
export const MessagesListContainer = () => {
73+
const [appState, setAppState] = useState(AppState.currentState);
7274
const { conversationId } = useChatWindowContext();
7375
const dispatch = useAppDispatch();
7476
const [isFlashListReady, setFlashListReady] = React.useState(false);
@@ -112,6 +114,26 @@ export const MessagesListContainer = () => {
112114
[conversationId, dispatch, lastMessageId],
113115
);
114116

117+
// Update messages when app comes to foreground from background
118+
useEffect(() => {
119+
const appStateListener = AppState.addEventListener('change', nextAppState => {
120+
if (appState.match(/inactive|background/) && nextAppState === 'active') {
121+
const routeName = getCurrentRouteName();
122+
if (routeName && SCREENS.CHAT === routeName) {
123+
dispatch(
124+
conversationActions.fetchPreviousMessages({
125+
conversationId,
126+
}),
127+
);
128+
}
129+
}
130+
setAppState(nextAppState);
131+
});
132+
return () => {
133+
appStateListener?.remove();
134+
};
135+
}, [appState, conversationId, dispatch]);
136+
115137
const onEndReached = () => {
116138
const shouldFetchMoreMessages = !isAllMessagesFetched && !isLoadingMessages && isFlashListReady;
117139
if (shouldFetchMoreMessages) {

src/screens/conversations/ConversationScreen.tsx

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2-
import { ActivityIndicator, RefreshControl, StatusBar } from 'react-native';
2+
import { ActivityIndicator, AppState, RefreshControl, StatusBar } from 'react-native';
33
import Animated, {
44
LinearTransition,
55
runOnJS,
@@ -22,7 +22,12 @@ import {
2222
import { ActionTabs, BottomSheetBackdrop, BottomSheetWrapper } from '@/components-next';
2323

2424
import { EmptyStateIcon } from '@/svg-icons';
25-
import { TAB_BAR_HEIGHT } from '@/constants';
25+
import {
26+
SCREENS,
27+
TAB_BAR_HEIGHT,
28+
LAST_ACTIVE_TIMESTAMP_KEY,
29+
LAST_ACTIVE_TIMESTAMP_THRESHOLD,
30+
} from '@/constants';
2631
import {
2732
ConversationListStateProvider,
2833
useConversationListStateContext,
@@ -52,6 +57,11 @@ import { clearAssignableAgents } from '@/store/assignable-agent/assignableAgentS
5257

5358
import i18n from '@/i18n';
5459
import ActionBottomSheet from '@/navigation/tabs/ActionBottomSheet';
60+
import { getCurrentRouteName } from '@/utils/navigationUtils';
61+
import AsyncStorage from '@react-native-async-storage/async-storage';
62+
63+
// The screen list thats need to be checked for refreshing the conversations list
64+
const REFRESH_SCREEN_LIST = [SCREENS.CONVERSATION, SCREENS.INBOX, SCREENS.SETTINGS];
5565

5666
const AnimatedFlashList = Animated.createAnimatedComponent(FlashList);
5767

@@ -62,6 +72,7 @@ type FlashListRenderItemType = {
6272

6373
const ConversationList = () => {
6474
const dispatch = useAppDispatch();
75+
const [appState, setAppState] = useState(AppState.currentState);
6576

6677
// This is used to prevent the infinite scrolling before the list is ready
6778
const [isFlashListReady, setFlashListReady] = useState(false);
@@ -93,6 +104,11 @@ const ConversationList = () => {
93104
const filters = useAppSelector(selectFilters);
94105
const previousFilters = useRef(filters);
95106

107+
// Reset last active timestamp when the conversation screen is opened
108+
useEffect(() => {
109+
AsyncStorage.removeItem(LAST_ACTIVE_TIMESTAMP_KEY);
110+
}, []);
111+
96112
useEffect(() => {
97113
if (previousFilters.current !== filters) {
98114
previousFilters.current = filters;
@@ -136,6 +152,40 @@ const ConversationList = () => {
136152
});
137153
}, [clearAndFetchConversations, filters]);
138154

155+
const checkAppStateAndFetchConversations = useCallback(async () => {
156+
const lastActiveTimestamp = await AsyncStorage.getItem(LAST_ACTIVE_TIMESTAMP_KEY);
157+
if (lastActiveTimestamp) {
158+
const currentTimestamp = Date.now();
159+
const difference = currentTimestamp - parseInt(lastActiveTimestamp);
160+
if (difference > LAST_ACTIVE_TIMESTAMP_THRESHOLD) {
161+
clearAndFetchConversations(filters);
162+
}
163+
}
164+
}, [clearAndFetchConversations, filters]);
165+
166+
// Update conversations when app comes to foreground from background
167+
useEffect(() => {
168+
const appStateListener = AppState.addEventListener('change', nextAppState => {
169+
if (appState.match(/inactive|background/) && nextAppState === 'active') {
170+
const routeName = getCurrentRouteName();
171+
if (routeName && REFRESH_SCREEN_LIST.includes(routeName)) {
172+
checkAppStateAndFetchConversations();
173+
}
174+
}
175+
176+
if (appState === 'active' && nextAppState.match(/inactive|background/)) {
177+
// App is going to background
178+
const currentTimestamp = Date.now();
179+
AsyncStorage.setItem(LAST_ACTIVE_TIMESTAMP_KEY, currentTimestamp.toString());
180+
}
181+
182+
setAppState(nextAppState);
183+
});
184+
return () => {
185+
appStateListener?.remove();
186+
};
187+
}, [appState, checkAppStateAndFetchConversations, clearAndFetchConversations, filters]);
188+
139189
const fetchConversations = useCallback(
140190
async (filters: FilterState, page: number = 1) => {
141191
const conversationFilters = {

0 commit comments

Comments
 (0)