Process notifications when the app is in the background and other perf improvements (#7129)

This commit is contained in:
Elias Nahum 2023-02-15 17:08:19 +02:00 committed by GitHub
parent 153c2f7c8d
commit 6def5d9610
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 177 additions and 82 deletions

View file

@ -5,7 +5,7 @@
import {DeviceEventEmitter} from 'react-native';
import {addChannelToDefaultCategory, storeCategories} from '@actions/local/category';
import {removeCurrentUserFromChannel, setChannelDeleteAt, storeMyChannelsForTeam, switchToChannel} from '@actions/local/channel';
import {markChannelAsViewed, removeCurrentUserFromChannel, setChannelDeleteAt, storeMyChannelsForTeam, switchToChannel} from '@actions/local/channel';
import {switchToGlobalThreads} from '@actions/local/thread';
import {updateLocalUser} from '@actions/local/user';
import {loadCallForChannel} from '@calls/actions/calls';
@ -726,11 +726,15 @@ export async function joinChannelIfNeeded(serverUrl: string, channelId: string)
}
}
export async function markChannelAsRead(serverUrl: string, channelId: string) {
export async function markChannelAsRead(serverUrl: string, channelId: string, updateLocal = false) {
try {
const client = NetworkManager.getClient(serverUrl);
await client.viewMyChannel(channelId);
if (updateLocal) {
await markChannelAsViewed(serverUrl, channelId, true);
}
return {};
} catch (error) {
return {error};

View file

@ -1,8 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {markChannelAsViewed} from '@actions/local/channel';
import {dataRetentionCleanup} from '@actions/local/systems';
import {fetchMissingDirectChannelsInfo, fetchMyChannelsForTeam, handleKickFromChannel, MyChannelsRequest} from '@actions/remote/channel';
import {fetchMissingDirectChannelsInfo, fetchMyChannelsForTeam, handleKickFromChannel, markChannelAsRead, MyChannelsRequest} from '@actions/remote/channel';
import {fetchGroupsForMember} from '@actions/remote/groups';
import {fetchPostsForUnreadChannels} from '@actions/remote/post';
import {MyPreferencesRequest, fetchMyPreferences} from '@actions/remote/preference';
@ -560,6 +561,9 @@ export async function handleEntryAfterLoadNavigation(
} else {
await setCurrentTeamAndChannelId(operator, initialTeamId, initialChannelId);
}
} else if (tabletDevice && initialChannelId === currentChannelId) {
await markChannelAsRead(serverUrl, initialChannelId);
markChannelAsViewed(serverUrl, initialChannelId);
}
} catch (error) {
logDebug('could not manage the entry after load navigation', error);

View file

@ -3,19 +3,26 @@
import {Platform} from 'react-native';
// import {updatePostSinceCache, updatePostsInThreadsSinceCache} from '@actions/local/notification';
import {addChannelToDefaultCategory, storeCategories} from '@actions/local/category';
import {storeMyChannelsForTeam} from '@actions/local/channel';
import {storePostsForChannel} from '@actions/local/post';
import {fetchDirectChannelsInfo, fetchMyChannel, switchToChannelById} from '@actions/remote/channel';
import {fetchPostsForChannel, fetchPostThread} from '@actions/remote/post';
import {forceLogoutIfNecessary} from '@actions/remote/session';
import {fetchMyTeam} from '@actions/remote/team';
import {fetchAndSwitchToThread} from '@actions/remote/thread';
import {ActionType} from '@constants';
import DatabaseManager from '@database/manager';
import {getMyChannel, getChannelById} from '@queries/servers/channel';
import {getCurrentTeamId, getWebSocketLastDisconnected} from '@queries/servers/system';
import {getMyTeamById} from '@queries/servers/team';
import {getCurrentTeamId} from '@queries/servers/system';
import {getMyTeamById, prepareMyTeams} from '@queries/servers/team';
import {getIsCRTEnabled} from '@queries/servers/thread';
import EphemeralStore from '@store/ephemeral_store';
import {logWarning} from '@utils/log';
import {emitNotificationError} from '@utils/notification';
import {processPostsFetched} from '@utils/post';
import type {Model} from '@nozbe/watermelondb';
const fetchNotificationData = async (serverUrl: string, notification: NotificationWithData, skipEvents = false) => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
@ -95,24 +102,84 @@ const fetchNotificationData = async (serverUrl: string, notification: Notificati
};
export const backgroundNotification = async (serverUrl: string, notification: NotificationWithData) => {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return;
}
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const channelId = notification.payload?.channel_id;
let teamId = notification.payload?.team_id;
if (!channelId) {
throw new Error('No chanel Id was specified');
}
const lastDisconnectedAt = await getWebSocketLastDisconnected(database);
if (lastDisconnectedAt) {
// if (Platform.OS === 'ios') {
// const isCRTEnabled = await getIsCRTEnabled(database);
// const isThreadNotification = isCRTEnabled && Boolean(notification.payload?.root_id);
// if (isThreadNotification) {
// updatePostsInThreadsSinceCache(serverUrl, notification);
// } else {
// updatePostSinceCache(serverUrl, notification);
// }
// }
if (!teamId) {
// If the notification payload does not have a teamId we assume is a DM/GM
const currentTeamId = await getCurrentTeamId(database);
teamId = currentTeamId;
}
if (notification.payload?.data) {
const {data, isCRTEnabled} = notification.payload;
const {channel, myChannel, team, myTeam, posts, users, threads} = data;
const models: Model[] = [];
await fetchNotificationData(serverUrl, notification, true);
if (posts) {
const postsData = processPostsFetched(posts);
const isThreadNotification = isCRTEnabled && Boolean(notification.payload.root_id);
const actionType = isThreadNotification ? ActionType.POSTS.RECEIVED_IN_THREAD : ActionType.POSTS.RECEIVED_IN_CHANNEL;
if (team || myTeam) {
const teamPromises = prepareMyTeams(operator, team ? [team] : [], myTeam ? [myTeam] : []);
if (teamPromises.length) {
const teamModels = await Promise.all(teamPromises);
models.push(...teamModels.flat());
}
}
await storeMyChannelsForTeam(
serverUrl, teamId,
channel ? [channel] : [],
myChannel ? [myChannel] : [],
true, isCRTEnabled,
);
if (data.categoryChannels?.length && channel) {
const {models: categoryModels} = await addChannelToDefaultCategory(serverUrl, channel, true);
if (categoryModels?.length) {
models.push(...categoryModels);
}
} else if (data.categories?.categories) {
const {models: categoryModels} = await storeCategories(serverUrl, data.categories.categories, false, true);
if (categoryModels?.length) {
models.push(...categoryModels);
}
}
await storePostsForChannel(
serverUrl, channelId,
postsData.posts, postsData.order, postsData.previousPostId ?? '',
actionType, users || [],
);
if (isThreadNotification && threads?.length) {
const threadModels = await operator.handleThreads({
threads: threads.map((t) => ({
...t,
lastFetchedAt: Math.max(t.post.create_at, t.post.update_at, t.post.delete_at),
})),
teamId,
prepareRecordsOnly: true,
});
if (threadModels.length) {
models.push(...threadModels);
}
}
}
if (models.length) {
await operator.batchRecords(models, 'backgroundNotification');
}
}
} catch (error) {
logWarning('backgroundNotification', error);
}
};

View file

@ -115,7 +115,7 @@ export const updateTeamThreadsAsRead = async (serverUrl: string, teamId: string)
}
};
export const markThreadAsRead = async (serverUrl: string, teamId: string | undefined, threadId: string) => {
export const markThreadAsRead = async (serverUrl: string, teamId: string | undefined, threadId: string, updateLastViewed = true) => {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
@ -141,7 +141,7 @@ export const markThreadAsRead = async (serverUrl: string, teamId: string | undef
// Update locally
await updateThread(serverUrl, threadId, {
last_viewed_at: timestamp,
last_viewed_at: updateLastViewed ? timestamp : undefined,
unread_replies: 0,
unread_mentions: 0,
});

View file

@ -6,7 +6,7 @@ import {DeviceEventEmitter} from 'react-native';
import {storeMyChannelsForTeam, markChannelAsUnread, markChannelAsViewed, updateLastPostAt} from '@actions/local/channel';
import {markPostAsDeleted} from '@actions/local/post';
import {createThreadFromNewPost, updateThread} from '@actions/local/thread';
import {fetchChannelStats, fetchMyChannel, markChannelAsRead} from '@actions/remote/channel';
import {fetchChannelStats, fetchMyChannel} from '@actions/remote/channel';
import {fetchPostAuthors, fetchPostById} from '@actions/remote/post';
import {fetchThread} from '@actions/remote/thread';
import {ActionType, Events, Screens} from '@constants';
@ -116,7 +116,6 @@ export async function handleNewPostEvent(serverUrl: string, msg: WebSocketMessag
if (!shouldIgnorePost(post)) {
let markAsViewed = false;
let markAsRead = false;
if (!myChannel.manuallyUnread) {
if (
@ -125,21 +124,17 @@ export async function handleNewPostEvent(serverUrl: string, msg: WebSocketMessag
!isFromWebhook(post)
) {
markAsViewed = true;
markAsRead = false;
} else if ((post.channel_id === currentChannelId)) {
const isChannelScreenMounted = NavigationStore.getScreensInStack().includes(Screens.CHANNEL);
const isTabletDevice = await isTablet();
if (isChannelScreenMounted || isTabletDevice) {
markAsViewed = false;
markAsRead = true;
}
}
}
if (markAsRead) {
markChannelAsRead(serverUrl, post.channel_id);
} else if (markAsViewed) {
if (markAsViewed) {
preparedMyChannelHack(myChannel);
const {member: viewedAt} = await markChannelAsViewed(serverUrl, post.channel_id, false, true);
if (viewedAt) {

View file

@ -37,6 +37,7 @@ type Props = {
const getStyle = makeStyleSheetFromTheme((theme: Theme) => ({
background: {
backgroundColor: theme.sidebarBg,
zIndex: 1,
},
bannerContainer: {
flex: 1,

View file

@ -36,6 +36,7 @@ const getStyle = makeStyleSheetFromTheme((theme: Theme) => {
return {
background: {
backgroundColor: theme.sidebarBg,
zIndex: 1,
},
bannerContainerNotConnected: {
...bannerContainer,

View file

@ -318,12 +318,12 @@ const ChannelHandler = <TBase extends Constructor<ServerDataOperatorBase>>(super
}));
const uniqueRaws = getUniqueRawsBy({raws: memberships, key: 'id'}) as ChannelMember[];
const ids = uniqueRaws.map((cm: ChannelMember) => cm.channel_id);
const ids = uniqueRaws.map((cm: ChannelMember) => `${cm.channel_id}-${cm.user_id}`);
const db: Database = this.database;
const existing = await db.get<ChannelMembershipModel>(CHANNEL_MEMBERSHIP).query(
Q.where('id', Q.oneOf(ids)),
).fetch();
const membershipMap = new Map<string, ChannelMembershipModel>(existing.map((member) => [member.id, member]));
const membershipMap = new Map<string, ChannelMembershipModel>(existing.map((member) => [member.channelId, member]));
const createOrUpdateRawValues = uniqueRaws.reduce((res: ChannelMember[], cm) => {
const e = membershipMap.get(cm.channel_id);
if (!e) {

View file

@ -148,13 +148,15 @@ const GroupHandler = <TBase extends Constructor<ServerDataOperatorBase>>(supercl
rawValues.push(...Object.values(groupsSet));
}
records.push(...(await this.handleRecords({
fieldName: 'id',
transformer: transformGroupMembershipRecord,
createOrUpdateRawValues: rawValues,
tableName: GROUP_MEMBERSHIP,
prepareRecordsOnly: true,
}, 'handleGroupMembershipsForMember')));
if (rawValues.length) {
records.push(...(await this.handleRecords({
fieldName: 'id',
transformer: transformGroupMembershipRecord,
createOrUpdateRawValues: rawValues,
tableName: GROUP_MEMBERSHIP,
prepareRecordsOnly: true,
}, 'handleGroupMembershipsForMember')));
}
// Batch update if there are records
if (records.length && !prepareRecordsOnly) {

View file

@ -164,11 +164,11 @@ describe('*** Operator: Thread Handlers tests ***', () => {
expect(spyOnPrepareRecords).toHaveBeenCalledWith({
createRaws: [{
raw: {team_id: 'team_id_1', thread_id: 'thread-1'},
}, {
raw: {team_id: 'team_id_1', thread_id: 'thread-2'},
record: undefined,
}, {
raw: {team_id: 'team_id_2', thread_id: 'thread-2'},
record: undefined,
}],
transformer: transformThreadInTeamRecord,
tableName: 'ThreadsInTeam',

View file

@ -124,7 +124,9 @@ const ThreadHandler = <TBase extends Constructor<ServerDataOperatorBase>>(superc
threadsMap: {[teamId]: threads},
prepareRecordsOnly: true,
}) as ThreadInTeamModel[];
batch.push(...threadsInTeam);
if (threadsInTeam.length) {
batch.push(...threadsInTeam);
}
}
if (batch.length && !prepareRecordsOnly) {
@ -199,7 +201,7 @@ const ThreadHandler = <TBase extends Constructor<ServerDataOperatorBase>>(superc
const threadIds = threadsMap[teamId].map((thread) => thread.id);
const chunks = await (this.database as Database).get<ThreadInTeamModel>(THREADS_IN_TEAM).query(
Q.where('team_id', teamId),
Q.where('id', Q.oneOf(threadIds)),
Q.where('thread_id', Q.oneOf(threadIds)),
).fetch();
const chunksMap = chunks.reduce((result: Record<string, ThreadInTeamModel>, chunk) => {
result[chunk.threadId] = chunk;

View file

@ -14,7 +14,7 @@ import {Screens} from '@constants';
import {ACCESSORIES_CONTAINER_NATIVE_ID} from '@constants/post_draft';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {useChannelSwitch} from '@hooks/channel_switch';
import {useAppState, useIsTablet} from '@hooks/device';
import {useIsTablet} from '@hooks/device';
import {useDefaultHeaderHeight} from '@hooks/header';
import {useKeyboardTrackingPaused} from '@hooks/keyboard_tracking';
import {useTeamSwitch} from '@hooks/team_switch';
@ -35,6 +35,7 @@ type ChannelProps = {
isInACall: boolean;
isInCurrentChannelCall: boolean;
isCallsEnabledInChannel: boolean;
isTabletView?: boolean;
};
const edges: Edge[] = ['left', 'right'];
@ -54,8 +55,8 @@ const Channel = ({
isInACall,
isInCurrentChannelCall,
isCallsEnabledInChannel,
isTabletView,
}: ChannelProps) => {
const appState = useAppState();
const isTablet = useIsTablet();
const insets = useSafeAreaInsets();
const [shouldRenderPosts, setShouldRenderPosts] = useState(false);
@ -125,13 +126,13 @@ const Channel = ({
channelId={channelId}
componentId={componentId}
callsEnabledInChannel={isCallsEnabledInChannel}
isTabletView={isTabletView}
/>
{shouldRender &&
<>
<View style={[styles.flex, {marginTop}]}>
<ChannelPostList
channelId={channelId}
forceQueryAfterAppState={appState}
nativeID={channelId}
currentCallBarVisible={isInACall}
joinCallBannerVisible={showJoinCallBanner}

View file

@ -1,16 +1,17 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useRef} from 'react';
import React, {useCallback, useEffect, useRef} from 'react';
import {StyleProp, StyleSheet, ViewStyle} from 'react-native';
import {Edge, SafeAreaView} from 'react-native-safe-area-context';
import {markChannelAsRead} from '@actions/remote/channel';
import {fetchPostsBefore} from '@actions/remote/post';
import PostList from '@components/post_list';
import {Screens} from '@constants';
import {useServerUrl} from '@context/server';
import {debounce} from '@helpers/api/general';
import {useIsTablet} from '@hooks/device';
import {useAppState, useIsTablet} from '@hooks/device';
import Intro from './intro';
@ -39,11 +40,20 @@ const ChannelPostList = ({
lastViewedAt, nativeID, posts, shouldShowJoinLeaveMessages,
currentCallBarVisible, joinCallBannerVisible,
}: Props) => {
const appState = useAppState();
const isTablet = useIsTablet();
const serverUrl = useServerUrl();
const canLoadPosts = useRef(true);
const fetchingPosts = useRef(false);
const oldPostsCount = useRef<number>(posts.length);
useEffect(() => {
if (oldPostsCount.current < posts.length && appState === 'active') {
oldPostsCount.current = posts.length;
markChannelAsRead(serverUrl, channelId, true);
}
}, [isCRTEnabled, posts, channelId, serverUrl, appState === 'active']);
const onEndReached = useCallback(debounce(async () => {
if (!fetchingPosts.current && canLoadPosts.current && posts.length) {
fetchingPosts.current = true;

View file

@ -18,9 +18,8 @@ import {observeIsCRTEnabled} from '@queries/servers/thread';
import ChannelPostList from './channel_post_list';
import type {WithDatabaseArgs} from '@typings/database/database';
import type {AppStateStatus} from 'react-native';
const enhanced = withObservables(['channelId', 'forceQueryAfterAppState'], ({database, channelId}: {channelId: string; forceQueryAfterAppState: AppStateStatus} & WithDatabaseArgs) => {
const enhanced = withObservables(['channelId'], ({database, channelId}: {channelId: string} & WithDatabaseArgs) => {
const isCRTEnabledObserver = observeIsCRTEnabled(database);
const postsInChannelObserver = queryPostsInChannel(database, channelId).observeWithColumns(['earliest', 'latest']);

View file

@ -42,6 +42,7 @@ type ChannelProps = {
searchTerm: string;
teamId: string;
callsEnabledInChannel: boolean;
isTabletView?: boolean;
};
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
@ -69,7 +70,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
const ChannelHeader = ({
channelId, channelType, componentId, customStatus, displayName,
isCustomStatusEnabled, isCustomStatusExpired, isOwnDirectMessage, memberCount,
searchTerm, teamId, callsEnabledInChannel,
searchTerm, teamId, callsEnabledInChannel, isTabletView,
}: ChannelProps) => {
const intl = useIntl();
const isTablet = useIsTablet();
@ -233,7 +234,7 @@ const ChannelHeader = ({
onBackPress={onBackPress}
onTitlePress={onTitlePress}
rightButtons={rightButtons}
showBackButton={!isTablet}
showBackButton={!isTablet || !isTabletView}
subtitle={subtitle}
subtitleCompanion={subtitleCompanion}
title={title}

View file

@ -3,6 +3,7 @@
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import React from 'react';
import {of as of$} from 'rxjs';
import {combineLatestWith, switchMap} from 'rxjs/operators';
@ -91,4 +92,4 @@ const enhanced = withObservables(['channelId'], ({channelId, database}: OwnProps
};
});
export default withDatabase(enhanced(ChannelHeader));
export default withDatabase(enhanced(React.memo(ChannelHeader)));

View file

@ -11,7 +11,7 @@ import NavigationHeader from '@components/navigation_header';
import RoundedHeaderContext from '@components/rounded_header_context';
import {useServerUrl} from '@context/server';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {useAppState, useIsTablet} from '@hooks/device';
import {useIsTablet} from '@hooks/device';
import {useDefaultHeaderHeight} from '@hooks/header';
import {useTeamSwitch} from '@hooks/team_switch';
import {popTopScreen} from '@screens/navigation';
@ -34,7 +34,6 @@ const styles = StyleSheet.create({
});
const GlobalThreads = ({componentId, globalThreadsTab}: Props) => {
const appState = useAppState();
const serverUrl = useServerUrl();
const intl = useIntl();
const switchingTeam = useTeamSwitch();
@ -93,7 +92,6 @@ const GlobalThreads = ({componentId, globalThreadsTab}: Props) => {
{!switchingTeam &&
<View style={containerStyle}>
<ThreadsList
forceQueryAfterAppState={appState}
setTab={setTab}
tab={tab}
testID={'global_threads.threads_list'}

View file

@ -12,19 +12,17 @@ import {observeTeammateNameDisplay} from '@queries/servers/user';
import ThreadsList from './threads_list';
import type {WithDatabaseArgs} from '@typings/database/database';
import type {AppStateStatus} from 'react-native';
type Props = {
tab: GlobalThreadsTab;
teamId: string;
forceQueryAfterAppState: AppStateStatus;
} & WithDatabaseArgs;
const withTeamId = withObservables([], ({database}: WithDatabaseArgs) => ({
teamId: observeCurrentTeamId(database),
}));
const enhanced = withObservables(['tab', 'teamId', 'forceQueryAfterAppState'], ({database, tab, teamId}: Props) => {
const enhanced = withObservables(['tab', 'teamId'], ({database, tab, teamId}: Props) => {
const getOnlyUnreads = tab !== 'all';
const teamThreadsSyncObserver = queryTeamThreadsSync(database, teamId).observeWithColumns(['earliest']);

View file

@ -205,7 +205,7 @@ const Thread = ({author, channel, location, post, teammateNameDisplay, testID, t
enableSoftBreak={true}
textStyle={textStyles}
baseStyle={styles.message}
value={post.message}
value={post.message.substring(0, 100)} // This substring helps to avoid ANR's
/>
</Text>
);

View file

@ -57,7 +57,7 @@ const AdditionalTabletView = ({onTeam, currentChannelId, isCRTEnabled}: Props) =
return null;
}
return React.createElement(selected.Component, {componentId: selected.id, isTablet: true});
return React.createElement(selected.Component, {componentId: selected.id, isTabletView: true});
};
export default AdditionalTabletView;

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useEffect} from 'react';
import React, {useEffect, useMemo} from 'react';
import {useWindowDimensions} from 'react-native';
import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
@ -60,19 +60,19 @@ const CategoriesList = ({channelsCount, iconPad, isCRTEnabled, teamsCount}: Chan
return {maxWidth: withTiming(tabletWidth.value, {duration: 350})};
}, [isTablet, width]);
let content;
const content = useMemo(() => {
if (channelsCount < 1) {
return (<LoadChannelsError/>);
}
if (channelsCount < 1) {
content = (<LoadChannelsError/>);
} else {
content = (
return (
<>
<SubHeader/>
{isCRTEnabled && <ThreadsButton/>}
<Categories/>
</>
);
}
}, [isCRTEnabled]);
return (
<Animated.View style={[styles.container, tabletStyle]}>

View file

@ -11,7 +11,6 @@ import {Edge, SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-cont
import AnnouncementBanner from '@components/announcement_banner';
import ConnectionBanner from '@components/connection_banner';
import FreezeScreen from '@components/freeze_screen';
import TeamSidebar from '@components/team_sidebar';
import {Navigation as NavigationConstants, Screens} from '@constants';
import {useServerUrl} from '@context/server';
@ -160,7 +159,7 @@ const ChannelListScreen = (props: ChannelProps) => {
}, []);
return (
<FreezeScreen freeze={!isFocused}>
<>
<Animated.View style={top}/>
<SafeAreaView
style={styles.flex}
@ -192,7 +191,7 @@ const ChannelListScreen = (props: ChannelProps) => {
</Animated.View>
</View>
</SafeAreaView>
</FreezeScreen>
</>
);
};

View file

@ -13,7 +13,6 @@ import RoundedHeaderContext from '@components/rounded_header_context';
import {Screens} from '@constants';
import {THREAD_ACCESSORIES_CONTAINER_NATIVE_ID} from '@constants/post_draft';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {useAppState} from '@hooks/device';
import useDidUpdate from '@hooks/did_update';
import {useKeyboardTrackingPaused} from '@hooks/keyboard_tracking';
import {popTopScreen} from '@screens/navigation';
@ -39,7 +38,6 @@ const styles = StyleSheet.create({
});
const Thread = ({componentId, rootPost, isInACall}: ThreadProps) => {
const appState = useAppState();
const postDraftRef = useRef<KeyboardTrackingViewRef>(null);
const [containerHeight, setContainerHeight] = useState(0);
const rootId = rootPost?.id || '';
@ -81,7 +79,6 @@ const Thread = ({componentId, rootPost, isInACall}: ThreadProps) => {
<>
<View style={styles.flex}>
<ThreadPostList
forceQueryAfterAppState={appState}
nativeID={rootPost!.id}
rootPost={rootPost!}
/>

View file

@ -15,14 +15,12 @@ import ThreadPostList from './thread_post_list';
import type {WithDatabaseArgs} from '@typings/database/database';
import type PostModel from '@typings/database/models/servers/post';
import type {AppStateStatus} from 'react-native';
type Props = WithDatabaseArgs & {
forceQueryAfterAppState: AppStateStatus;
rootPost: PostModel;
};
const enhanced = withObservables(['forceQueryAfterAppState', 'rootPost'], ({database, rootPost}: Props) => {
const enhanced = withObservables(['rootPost'], ({database, rootPost}: Props) => {
return {
isCRTEnabled: observeIsCRTEnabled(database),
channelLastViewedAt: observeMyChannel(database, rootPost.channelId).pipe(

View file

@ -13,7 +13,7 @@ import {Screens} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {debounce} from '@helpers/api/general';
import {useIsTablet} from '@hooks/device';
import {useAppState, useIsTablet} from '@hooks/device';
import {useFetchingThreadState} from '@hooks/fetching_thread';
import {isMinimumServerVersion} from '@utils/helpers';
@ -43,6 +43,7 @@ const ThreadPostList = ({
channelLastViewedAt, isCRTEnabled,
nativeID, posts, rootPost, teamId, thread, version,
}: Props) => {
const appState = useAppState();
const isTablet = useIsTablet();
const serverUrl = useServerUrl();
const theme = useTheme();
@ -82,11 +83,11 @@ const ThreadPostList = ({
// If CRT is enabled, When new post arrives and thread modal is open, mark thread as read.
const oldPostsCount = useRef<number>(posts.length);
useEffect(() => {
if (isCRTEnabled && thread?.isFollowing && oldPostsCount.current < posts.length) {
if (isCRTEnabled && thread?.isFollowing && oldPostsCount.current < posts.length && appState === 'active') {
oldPostsCount.current = posts.length;
markThreadAsRead(serverUrl, teamId, rootPost.id);
markThreadAsRead(serverUrl, teamId, rootPost.id, false);
}
}, [isCRTEnabled, posts, rootPost, serverUrl, teamId, thread]);
}, [isCRTEnabled, posts, rootPost, serverUrl, teamId, thread, appState === 'active']);
const lastViewedAt = isCRTEnabled ? (thread?.viewedAt ?? 0) : channelLastViewedAt;

View file

@ -37,6 +37,8 @@ export const convertToNotificationData = (notification: Notification, tapped = t
type: payload.type,
use_user_icon: payload.use_user_icon,
version: payload.version,
isCRTEnabled: typeof payload.is_crt_enabled === 'string' ? payload.is_crt_enabled === 'true' : Boolean(payload.is_crt_enabled),
data: payload.data,
},
userInteraction: tapped,
foreground: false,

View file

@ -39,6 +39,20 @@ interface NotificationData {
use_user_icon?: string;
userInfo?: NotificationUserInfo;
version: string;
isCRTEnabled: boolean;
data?: NotificationExtraData;
}
interface NotificationExtraData {
channel?: Channel;
myChannel?: ChannelMembership;
categories?: CategoriesWithOrder;
categoryChannels?: CategoryChannel[];
team?: Team;
myTeam?: TeamMembership;
users?: UserProfile[];
posts?: PostResponse;
threads?: Thread[];
}
interface NotificationWithData extends Notification {