Process notifications when the app is in the background and other perf improvements (#7129)
This commit is contained in:
parent
153c2f7c8d
commit
6def5d9610
27 changed files with 177 additions and 82 deletions
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ type Props = {
|
|||
const getStyle = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
background: {
|
||||
backgroundColor: theme.sidebarBg,
|
||||
zIndex: 1,
|
||||
},
|
||||
bannerContainer: {
|
||||
flex: 1,
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ const getStyle = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
return {
|
||||
background: {
|
||||
backgroundColor: theme.sidebarBg,
|
||||
zIndex: 1,
|
||||
},
|
||||
bannerContainerNotConnected: {
|
||||
...bannerContainer,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
|
|
|
|||
|
|
@ -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'}
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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]}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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!}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
14
types/global/push_notifications.d.ts
vendored
14
types/global/push_notifications.d.ts
vendored
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue