From 6def5d9610633226e17fa277e1317b3d50f7f5b0 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Wed, 15 Feb 2023 17:08:19 +0200 Subject: [PATCH] Process notifications when the app is in the background and other perf improvements (#7129) --- app/actions/remote/channel.ts | 8 +- app/actions/remote/entry/common.ts | 6 +- app/actions/remote/notifications.ts | 105 ++++++++++++++---- app/actions/remote/thread.ts | 4 +- app/actions/websocket/posts.ts | 9 +- .../announcement_banner.tsx | 1 + .../connection_banner/connection_banner.tsx | 1 + .../server_data_operator/handlers/channel.ts | 4 +- .../server_data_operator/handlers/group.ts | 16 +-- .../handlers/thread.test.ts | 4 +- .../server_data_operator/handlers/thread.ts | 6 +- app/screens/channel/channel.tsx | 7 +- .../channel_post_list/channel_post_list.tsx | 14 ++- .../channel/channel_post_list/index.ts | 3 +- app/screens/channel/header/header.tsx | 5 +- app/screens/channel/header/index.ts | 3 +- app/screens/global_threads/global_threads.tsx | 4 +- .../global_threads/threads_list/index.ts | 4 +- .../threads_list/thread/thread.tsx | 2 +- .../additional_tablet_view.tsx | 2 +- .../channel_list/categories_list/index.tsx | 14 +-- .../home/channel_list/channel_list.tsx | 5 +- app/screens/thread/thread.tsx | 3 - app/screens/thread/thread_post_list/index.ts | 4 +- .../thread_post_list/thread_post_list.tsx | 9 +- app/utils/notification/index.ts | 2 + types/global/push_notifications.d.ts | 14 +++ 27 files changed, 177 insertions(+), 82 deletions(-) diff --git a/app/actions/remote/channel.ts b/app/actions/remote/channel.ts index b52ae1d9b..5d5488ed9 100644 --- a/app/actions/remote/channel.ts +++ b/app/actions/remote/channel.ts @@ -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}; diff --git a/app/actions/remote/entry/common.ts b/app/actions/remote/entry/common.ts index 86fb98cb3..12b04a26a 100644 --- a/app/actions/remote/entry/common.ts +++ b/app/actions/remote/entry/common.ts @@ -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); diff --git a/app/actions/remote/notifications.ts b/app/actions/remote/notifications.ts index 231121770..ebf61eda0 100644 --- a/app/actions/remote/notifications.ts +++ b/app/actions/remote/notifications.ts @@ -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); } }; diff --git a/app/actions/remote/thread.ts b/app/actions/remote/thread.ts index 1de2d241a..ca3dfb6e3 100644 --- a/app/actions/remote/thread.ts +++ b/app/actions/remote/thread.ts @@ -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, }); diff --git a/app/actions/websocket/posts.ts b/app/actions/websocket/posts.ts index 6c16447cc..6a3773b33 100644 --- a/app/actions/websocket/posts.ts +++ b/app/actions/websocket/posts.ts @@ -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) { diff --git a/app/components/announcement_banner/announcement_banner.tsx b/app/components/announcement_banner/announcement_banner.tsx index 08f4bcd17..70fbd87bc 100644 --- a/app/components/announcement_banner/announcement_banner.tsx +++ b/app/components/announcement_banner/announcement_banner.tsx @@ -37,6 +37,7 @@ type Props = { const getStyle = makeStyleSheetFromTheme((theme: Theme) => ({ background: { backgroundColor: theme.sidebarBg, + zIndex: 1, }, bannerContainer: { flex: 1, diff --git a/app/components/connection_banner/connection_banner.tsx b/app/components/connection_banner/connection_banner.tsx index 7b958a45e..e99061c7d 100644 --- a/app/components/connection_banner/connection_banner.tsx +++ b/app/components/connection_banner/connection_banner.tsx @@ -36,6 +36,7 @@ const getStyle = makeStyleSheetFromTheme((theme: Theme) => { return { background: { backgroundColor: theme.sidebarBg, + zIndex: 1, }, bannerContainerNotConnected: { ...bannerContainer, diff --git a/app/database/operator/server_data_operator/handlers/channel.ts b/app/database/operator/server_data_operator/handlers/channel.ts index 942e84820..aad93db12 100644 --- a/app/database/operator/server_data_operator/handlers/channel.ts +++ b/app/database/operator/server_data_operator/handlers/channel.ts @@ -318,12 +318,12 @@ const ChannelHandler = >(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(CHANNEL_MEMBERSHIP).query( Q.where('id', Q.oneOf(ids)), ).fetch(); - const membershipMap = new Map(existing.map((member) => [member.id, member])); + const membershipMap = new Map(existing.map((member) => [member.channelId, member])); const createOrUpdateRawValues = uniqueRaws.reduce((res: ChannelMember[], cm) => { const e = membershipMap.get(cm.channel_id); if (!e) { diff --git a/app/database/operator/server_data_operator/handlers/group.ts b/app/database/operator/server_data_operator/handlers/group.ts index 73050bd12..d7e59769d 100644 --- a/app/database/operator/server_data_operator/handlers/group.ts +++ b/app/database/operator/server_data_operator/handlers/group.ts @@ -148,13 +148,15 @@ const GroupHandler = >(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) { diff --git a/app/database/operator/server_data_operator/handlers/thread.test.ts b/app/database/operator/server_data_operator/handlers/thread.test.ts index e7ce781ef..b7a5ea2ab 100644 --- a/app/database/operator/server_data_operator/handlers/thread.test.ts +++ b/app/database/operator/server_data_operator/handlers/thread.test.ts @@ -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', diff --git a/app/database/operator/server_data_operator/handlers/thread.ts b/app/database/operator/server_data_operator/handlers/thread.ts index 735ae2b20..47cb7ad58 100644 --- a/app/database/operator/server_data_operator/handlers/thread.ts +++ b/app/database/operator/server_data_operator/handlers/thread.ts @@ -124,7 +124,9 @@ const ThreadHandler = >(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 = >(superc const threadIds = threadsMap[teamId].map((thread) => thread.id); const chunks = await (this.database as Database).get(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, chunk) => { result[chunk.threadId] = chunk; diff --git a/app/screens/channel/channel.tsx b/app/screens/channel/channel.tsx index 2a22fcf9f..f792c280f 100644 --- a/app/screens/channel/channel.tsx +++ b/app/screens/channel/channel.tsx @@ -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 && <> { + const appState = useAppState(); const isTablet = useIsTablet(); const serverUrl = useServerUrl(); const canLoadPosts = useRef(true); const fetchingPosts = useRef(false); + const oldPostsCount = useRef(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; diff --git a/app/screens/channel/channel_post_list/index.ts b/app/screens/channel/channel_post_list/index.ts index 3babb5144..c135087a0 100644 --- a/app/screens/channel/channel_post_list/index.ts +++ b/app/screens/channel/channel_post_list/index.ts @@ -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']); diff --git a/app/screens/channel/header/header.tsx b/app/screens/channel/header/header.tsx index e98dc06e8..230b1d8bb 100644 --- a/app/screens/channel/header/header.tsx +++ b/app/screens/channel/header/header.tsx @@ -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} diff --git a/app/screens/channel/header/index.ts b/app/screens/channel/header/index.ts index b6b10b00c..7c0de9c59 100644 --- a/app/screens/channel/header/index.ts +++ b/app/screens/channel/header/index.ts @@ -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))); diff --git a/app/screens/global_threads/global_threads.tsx b/app/screens/global_threads/global_threads.tsx index a2cc13252..b7876b117 100644 --- a/app/screens/global_threads/global_threads.tsx +++ b/app/screens/global_threads/global_threads.tsx @@ -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 && ({ 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']); diff --git a/app/screens/global_threads/threads_list/thread/thread.tsx b/app/screens/global_threads/threads_list/thread/thread.tsx index 9b8731016..c01d10b07 100644 --- a/app/screens/global_threads/threads_list/thread/thread.tsx +++ b/app/screens/global_threads/threads_list/thread/thread.tsx @@ -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 /> ); diff --git a/app/screens/home/channel_list/additional_tablet_view/additional_tablet_view.tsx b/app/screens/home/channel_list/additional_tablet_view/additional_tablet_view.tsx index c361f7ef3..95e4cc9a5 100644 --- a/app/screens/home/channel_list/additional_tablet_view/additional_tablet_view.tsx +++ b/app/screens/home/channel_list/additional_tablet_view/additional_tablet_view.tsx @@ -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; diff --git a/app/screens/home/channel_list/categories_list/index.tsx b/app/screens/home/channel_list/categories_list/index.tsx index d2eb97ca9..d406c82a4 100644 --- a/app/screens/home/channel_list/categories_list/index.tsx +++ b/app/screens/home/channel_list/categories_list/index.tsx @@ -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 (); + } - if (channelsCount < 1) { - content = (); - } else { - content = ( + return ( <> {isCRTEnabled && } ); - } + }, [isCRTEnabled]); return ( diff --git a/app/screens/home/channel_list/channel_list.tsx b/app/screens/home/channel_list/channel_list.tsx index 0cf61900c..f9c3e0184 100644 --- a/app/screens/home/channel_list/channel_list.tsx +++ b/app/screens/home/channel_list/channel_list.tsx @@ -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 ( - + <> { - + ); }; diff --git a/app/screens/thread/thread.tsx b/app/screens/thread/thread.tsx index 4f9122073..3cfcbbe0e 100644 --- a/app/screens/thread/thread.tsx +++ b/app/screens/thread/thread.tsx @@ -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(null); const [containerHeight, setContainerHeight] = useState(0); const rootId = rootPost?.id || ''; @@ -81,7 +79,6 @@ const Thread = ({componentId, rootPost, isInACall}: ThreadProps) => { <> diff --git a/app/screens/thread/thread_post_list/index.ts b/app/screens/thread/thread_post_list/index.ts index 81ecda79f..d307e3702 100644 --- a/app/screens/thread/thread_post_list/index.ts +++ b/app/screens/thread/thread_post_list/index.ts @@ -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( diff --git a/app/screens/thread/thread_post_list/thread_post_list.tsx b/app/screens/thread/thread_post_list/thread_post_list.tsx index e30b11adb..4048cf8fd 100644 --- a/app/screens/thread/thread_post_list/thread_post_list.tsx +++ b/app/screens/thread/thread_post_list/thread_post_list.tsx @@ -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(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; diff --git a/app/utils/notification/index.ts b/app/utils/notification/index.ts index ff5bf7aa6..f72e5d8db 100644 --- a/app/utils/notification/index.ts +++ b/app/utils/notification/index.ts @@ -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, diff --git a/types/global/push_notifications.d.ts b/types/global/push_notifications.d.ts index 024d46aa6..a8c5d5258 100644 --- a/types/global/push_notifications.d.ts +++ b/types/global/push_notifications.d.ts @@ -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 {