diff --git a/app/actions/local/channel.ts b/app/actions/local/channel.ts index 70304c9029..f5b1425d6c 100644 --- a/app/actions/local/channel.ts +++ b/app/actions/local/channel.ts @@ -8,7 +8,7 @@ import {General, Navigation as NavigationConstants, Preferences, Screens} from ' import {MM_TABLES} from '@constants/database'; import DatabaseManager from '@database/manager'; import {getTeammateNameDisplaySetting} from '@helpers/api/preference'; -import {prepareDeleteChannel, queryAllMyChannelIds, queryChannelsById, queryMyChannel} from '@queries/servers/channel'; +import {prepareDeleteChannel, prepareMyChannelsForTeam, queryAllMyChannelIds, queryChannelsById, queryMyChannel} from '@queries/servers/channel'; import {queryPreferencesByCategoryAndName} from '@queries/servers/preference'; import {prepareCommonSystemValues, PrepareCommonSystemValuesArgs, queryCommonSystemValues, queryCurrentTeamId, setCurrentChannelId} from '@queries/servers/system'; import {addChannelToTeamHistory, addTeamToTeamHistory, removeChannelFromTeamHistory} from '@queries/servers/team'; @@ -61,8 +61,8 @@ export const switchToChannel = async (serverUrl: string, channelId: string, team models.push(...history); } - const viewedAt = await markChannelAsViewed(serverUrl, channelId, true); - if (viewedAt instanceof Model) { + const {member: viewedAt} = await markChannelAsViewed(serverUrl, channelId, true); + if (viewedAt) { models.push(viewedAt); } @@ -155,12 +155,12 @@ export const selectAllMyChannelIds = async (serverUrl: string) => { }; export const markChannelAsViewed = async (serverUrl: string, channelId: string, prepareRecordsOnly = false) => { - const database = DatabaseManager.serverDatabases[serverUrl]?.database; - if (!database) { + const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; + if (!operator) { return {error: `${serverUrl} database not found`}; } - const member = await queryMyChannel(database, channelId); + const member = await queryMyChannel(operator.database, channelId); if (!member) { return {error: 'not a member'}; } @@ -170,15 +170,45 @@ export const markChannelAsViewed = async (serverUrl: string, channelId: string, m.mentionsCount = 0; m.manuallyUnread = false; m.viewedAt = member.lastViewedAt; + m.lastViewedAt = Date.now(); }); try { if (!prepareRecordsOnly) { - const {operator} = DatabaseManager.serverDatabases[serverUrl]; await operator.batchRecords([member]); } - return member; + return {member}; + } catch (error) { + return {error}; + } +}; + +export const markChannelAsUnread = async (serverUrl: string, channelId: string, messageCount: number, mentionsCount: number, manuallyUnread: boolean, lastViewed: number, prepareRecordsOnly = false) => { + const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; + if (!operator) { + return {error: `${serverUrl} database not found`}; + } + + const member = await queryMyChannel(operator.database, channelId); + if (!member) { + return {error: 'not a member'}; + } + + member.prepareUpdate((m) => { + m.viewedAt = lastViewed; + m.lastViewedAt = lastViewed; + m.messageCount = messageCount; + m.mentionsCount = mentionsCount; + m.manuallyUnread = manuallyUnread; + }); + + try { + if (!prepareRecordsOnly) { + await operator.batchRecords([member]); + } + + return {member}; } catch (error) { return {error}; } @@ -207,6 +237,67 @@ export const resetMessageCount = async (serverUrl: string, channelId: string) => } }; +export const storeMyChannelsForTeam = async (serverUrl: string, teamId: string, channels: Channel[], memberships: ChannelMembership[], prepareRecordsOnly = false) => { + const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; + if (!operator) { + return {error: `${serverUrl} database not found`}; + } + const modelPromises: Array> = []; + const prepare = await prepareMyChannelsForTeam(operator, teamId, channels, memberships); + if (prepare) { + modelPromises.push(...prepare); + } + + const models = await Promise.all(modelPromises); + const flattenedModels = models.flat() as Model[]; + + if (prepareRecordsOnly) { + return {models: flattenedModels}; + } + + if (flattenedModels?.length > 0) { + try { + await operator.batchRecords(flattenedModels); + } catch (error) { + // eslint-disable-next-line no-console + console.log('FAILED TO BATCH CHANNELS'); + return {error}; + } + } + + return {models: flattenedModels}; +}; + +export const updateLastPostAt = async (serverUrl: string, channelId: string, lastPostAt: number, prepareRecordsOnly = false) => { + const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; + if (!operator) { + return {error: `${serverUrl} database not found`}; + } + + const member = await queryMyChannel(operator.database, channelId); + if (!member) { + return {error: 'not a member'}; + } + + const models: Model[] = []; + if (lastPostAt > member.lastPostAt) { + member.prepareUpdate((m) => { + m.lastPostAt = lastPostAt; + }); + models.push(); + } + + try { + if (!prepareRecordsOnly && models.length) { + await operator.batchRecords([member]); + } + } catch (error) { + return {error}; + } + + return {models}; +}; + export async function updateChannelsDisplayName(serverUrl: string, channels: ChannelModel[], user: UserProfile, prepareRecordsOnly = false) { const database = DatabaseManager.serverDatabases[serverUrl]; if (!database) { diff --git a/app/actions/local/post.ts b/app/actions/local/post.ts index 33a7d5f20a..02bff3f189 100644 --- a/app/actions/local/post.ts +++ b/app/actions/local/post.ts @@ -100,7 +100,7 @@ export const sendEphemeralPost = async (serverUrl: string, message: string, chan return {post}; }; -export const removePost = async (serverUrl: string, post: PostModel) => { +export const removePost = async (serverUrl: string, post: PostModel | Post) => { const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; if (!operator) { return {error: `${serverUrl} database not found`}; @@ -132,6 +132,29 @@ export const selectAttachmentMenuAction = (serverUrl: string, postId: string, ac return postActionWithCookie(serverUrl, postId, actionId, '', selectedOption); }; +export const markPostAsDeleted = async (serverUrl: string, post: Post) => { + const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; + if (!operator) { + return {error: `${serverUrl} database not found`}; + } + + const dbPost = await queryPostById(operator.database, post.id); + if (!dbPost) { + return {}; + } + + dbPost.prepareUpdate((p) => { + p.deleteAt = Date.now(); + p.message = ''; + p.metadata = null; + p.props = undefined; + }); + + operator.batchRecords([dbPost]); + + return {post: dbPost}; +}; + export const processPostsFetched = async (serverUrl: string, actionType: string, data: PostResponse, fetchOnly = false) => { const order = data.order; const posts = Object.values(data.posts) as Post[]; diff --git a/app/actions/remote/channel.ts b/app/actions/remote/channel.ts index 37b074907f..cc918ab6e7 100644 --- a/app/actions/remote/channel.ts +++ b/app/actions/remote/channel.ts @@ -4,7 +4,7 @@ import {Model} from '@nozbe/watermelondb'; import {IntlShape} from 'react-intl'; -import {switchToChannel} from '@actions/local/channel'; +import {storeMyChannelsForTeam, switchToChannel} from '@actions/local/channel'; import {General} from '@constants'; import DatabaseManager from '@database/manager'; import {privateChannelJoinPrompt} from '@helpers/api/channel'; @@ -211,26 +211,7 @@ export const fetchMyChannelsForTeam = async (serverUrl: string, teamId: string, }, []); if (!fetchOnly) { - const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; - const modelPromises: Array> = []; - if (operator) { - const prepare = await prepareMyChannelsForTeam(operator, teamId, channels, memberships); - if (prepare) { - modelPromises.push(...prepare); - } - if (modelPromises.length) { - const models = await Promise.all(modelPromises); - const flattenedModels = models.flat() as Model[]; - if (flattenedModels?.length > 0) { - try { - await operator.batchRecords(flattenedModels); - } catch { - // eslint-disable-next-line no-console - console.log('FAILED TO BATCH CHANNELS'); - } - } - } - } + storeMyChannelsForTeam(serverUrl, teamId, channels, memberships); } return {channels, memberships}; @@ -255,26 +236,7 @@ export const fetchMyChannel = async (serverUrl: string, teamId: string, channelI ]); if (!fetchOnly) { - const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; - const modelPromises: Array> = []; - if (operator) { - const prepare = await prepareMyChannelsForTeam(operator, teamId, [channel], [member]); - if (prepare) { - modelPromises.push(...prepare); - } - if (modelPromises.length) { - const models = await Promise.all(modelPromises); - const flattenedModels = models.flat() as Model[]; - if (flattenedModels?.length > 0) { - try { - await operator.batchRecords(flattenedModels); - } catch { - // eslint-disable-next-line no-console - console.log('FAILED TO BATCH CHANNELS'); - } - } - } - } + storeMyChannelsForTeam(serverUrl, channel.team_id || teamId, [channel], [member]); } return { diff --git a/app/actions/remote/post.ts b/app/actions/remote/post.ts index b421cb4084..f20e390ec8 100644 --- a/app/actions/remote/post.ts +++ b/app/actions/remote/post.ts @@ -4,6 +4,7 @@ import {DeviceEventEmitter} from 'react-native'; +import {updateLastPostAt} from '@actions/local/channel'; import {processPostsFetched} from '@actions/local/post'; import {ActionType, Events, General} from '@constants'; import {SYSTEM_IDENTIFIERS} from '@constants/database'; @@ -94,8 +95,17 @@ export const fetchPostsForChannel = async (serverUrl: string, channelId: string, } } + let lastPostAt = 0; + for (const post of data.posts) { + lastPostAt = post.create_at > lastPostAt ? post.create_at : lastPostAt; + } + const {models: memberModels} = await updateLastPostAt(serverUrl, channelId, lastPostAt, true); + if (memberModels?.length) { + models.push(...memberModels); + } + if (models.length) { - operator.batchRecords(models); + await operator.batchRecords(models); } } } @@ -372,3 +382,29 @@ export async function getMissingChannelsFromPosts(serverUrl: string, posts: Post channelMemberships, }; } + +export const fetchPostById = async (serverUrl: string, postId: string, fetchOnly = false) => { + const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; + if (!operator) { + return {error: `${serverUrl} database not found`}; + } + + let client: Client; + try { + client = NetworkManager.getClient(serverUrl); + } catch (error) { + return {error}; + } + + try { + const post = await client.getPost(postId); + if (!fetchOnly) { + operator.handlePosts({actionType: ActionType.POSTS.RECEIVED_NEW, order: [post.id], posts: [post]}); + } + + return {post}; + } catch (error) { + forceLogoutIfNecessary(serverUrl, error as ClientErrorProps); + return {error}; + } +}; diff --git a/app/actions/websocket/channel.ts b/app/actions/websocket/channel.ts index 958d56e732..9f64b517b6 100644 --- a/app/actions/websocket/channel.ts +++ b/app/actions/websocket/channel.ts @@ -18,6 +18,8 @@ import {queryCurrentUser, queryUserById} from '@queries/servers/user'; import {dismissAllModals, popToRoot} from '@screens/navigation'; import {isTablet} from '@utils/helpers'; +import type {WebSocketMessage} from '@typings/api/websocket'; + export async function handleUserAddedToChannelEvent(serverUrl: string, msg: any) { const database = DatabaseManager.serverDatabases[serverUrl]; if (!database) { @@ -147,7 +149,7 @@ export async function handleUserRemovedFromChannelEvent(serverUrl: string, msg: database.operator.batchRecords(models); } -export async function handleChannelDeletedEvent(serverUrl: string, msg: any) { +export async function handleChannelDeletedEvent(serverUrl: string, msg: WebSocketMessage) { const database = DatabaseManager.serverDatabases[serverUrl]; if (!database) { return; diff --git a/app/actions/websocket/index.ts b/app/actions/websocket/index.ts index dd74c5208d..aecc0bc15e 100644 --- a/app/actions/websocket/index.ts +++ b/app/actions/websocket/index.ts @@ -20,11 +20,13 @@ import {queryCommonSystemValues, queryConfig, queryWebSocketLastDisconnected} fr import {queryCurrentUser} from '@queries/servers/user'; import {handleChannelDeletedEvent, handleUserAddedToChannelEvent, handleUserRemovedFromChannelEvent} from './channel'; +import {handleNewPostEvent, handlePostDeleted, handlePostEdited, handlePostUnread} from './posts'; import {handlePreferenceChangedEvent, handlePreferencesChangedEvent, handlePreferencesDeletedEvent} from './preferences'; import {handleLeaveTeamEvent} from './teams'; import {handleUserUpdatedEvent} from './users'; import type {Model} from '@nozbe/watermelondb'; +import type {WebSocketMessage} from '@typings/api/websocket'; export async function handleFirstConnect(serverUrl: string) { const database = DatabaseManager.serverDatabases[serverUrl]?.database; @@ -127,7 +129,7 @@ async function doReconnect(serverUrl: string) { } else if (!channelStillExist || (!viewArchivedChannels && channelStillExist.delete_at !== 0) ) { - handleChannelDeletedEvent(serverUrl, {data: {user_id: currentUserId, channel_id: currentChannelId}}); + handleChannelDeletedEvent(serverUrl, {data: {user_id: currentUserId, channel_id: currentChannelId}} as WebSocketMessage); } else { // TODO Differentiate between post and thread, to fetch the thread posts fetchPostsSince(serverUrl, currentChannelId, lastDisconnectedAt); @@ -136,7 +138,7 @@ async function doReconnect(serverUrl: string) { // TODO Consider global thread screen to update global threads } else if (!teamMembershipsError) { - handleLeaveTeamEvent(serverUrl, {data: {user_id: currentUserId, team_id: currentTeamId}}); + handleLeaveTeamEvent(serverUrl, {data: {user_id: currentUserId, team_id: currentTeamId}} as WebSocketMessage); } fetchRoles(serverUrl, teamMemberships, channelMemberships); @@ -147,25 +149,25 @@ async function doReconnect(serverUrl: string) { updateAllUsersSinceLastDisconnect(serverUrl); } -export async function handleEvent(serverUrl: string, msg: any) { +export async function handleEvent(serverUrl: string, msg: WebSocketMessage) { switch (msg.event) { case WebsocketEvents.POSTED: case WebsocketEvents.EPHEMERAL_MESSAGE: + handleNewPostEvent(serverUrl, msg); break; - //return dispatch(handleNewPostEvent(msg)); case WebsocketEvents.POST_EDITED: + handlePostEdited(serverUrl, msg); break; - //return dispatch(handlePostEdited(msg)); case WebsocketEvents.POST_DELETED: + handlePostDeleted(serverUrl, msg); break; - // return dispatch(handlePostDeleted(msg)); case WebsocketEvents.POST_UNREAD: + handlePostUnread(serverUrl, msg); break; - // return dispatch(handlePostUnread(msg)); case WebsocketEvents.LEAVE_TEAM: handleLeaveTeamEvent(serverUrl, msg); break; diff --git a/app/actions/websocket/posts.ts b/app/actions/websocket/posts.ts new file mode 100644 index 0000000000..008be00c58 --- /dev/null +++ b/app/actions/websocket/posts.ts @@ -0,0 +1,209 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Model} from '@nozbe/watermelondb'; +import {DeviceEventEmitter} from 'react-native'; + +import {storeMyChannelsForTeam, markChannelAsUnread, markChannelAsViewed, updateLastPostAt} from '@actions/local/channel'; +import {markPostAsDeleted} from '@actions/local/post'; +import {fetchMyChannel, markChannelAsRead} from '@actions/remote/channel'; +import {fetchPostAuthors, fetchPostById} from '@actions/remote/post'; +import {ActionType, Events} from '@constants'; +import DatabaseManager from '@database/manager'; +import {queryMyChannel} from '@queries/servers/channel'; +import {queryPostById} from '@queries/servers/post'; +import {queryCurrentChannelId, queryCurrentUserId} from '@queries/servers/system'; +import {isFromWebhook, isSystemMessage, shouldIgnorePost} from '@utils/post'; + +import type {WebSocketMessage} from '@typings/api/websocket'; + +export async function handleNewPostEvent(serverUrl: string, msg: WebSocketMessage) { + const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; + if (!operator) { + return; + } + + let post: Post; + try { + post = JSON.parse(msg.data.post); + } catch { + return; + } + const currentUserId = await queryCurrentUserId(operator.database); + + const existing = await queryPostById(operator.database, post.pending_post_id); + + if (existing) { + return; + } + + const models: Model[] = []; + + const postModels = await operator.handlePosts({ + actionType: ActionType.POSTS.RECEIVED_NEW, + order: [post.id], + posts: [post], + prepareRecordsOnly: true, + }); + + if (postModels?.length) { + models.push(...postModels); + } + + // Ensure the channel membership + let myChannel = await queryMyChannel(operator.database, post.channel_id); + if (myChannel) { + // Do not batch lastPostAt update since we may be modifying the member later for unreads + await updateLastPostAt(serverUrl, post.channel_id, post.create_at, false); + } else { + const myChannelRequest = await fetchMyChannel(serverUrl, '', post.channel_id, true); + if (myChannelRequest.error) { + return; + } + + // We want to have this on the database so we can make any needed update later + const myChannelModels = await storeMyChannelsForTeam(serverUrl, '', myChannelRequest.channels!, myChannelRequest.memberships!, false); + if (myChannelModels.error) { + return; + } + + myChannel = await queryMyChannel(operator.database, post.channel_id); + if (!myChannel) { + return; + } + } + + // If we don't have the root post for this post, fetch it from the server + if (post.root_id) { + const rootPost = await queryPostById(operator.database, post.root_id); + + if (!rootPost) { + fetchPostById(serverUrl, post.root_id); + } + } + + const currentChannelId = await queryCurrentChannelId(operator.database); + + if (post.channel_id === currentChannelId) { + const data = { + channelId: post.channel_id, + rootId: post.root_id, + userId: post.user_id, + now: Date.now(), + }; + DeviceEventEmitter.emit(Events.USER_STOP_TYPING, data); + } + + const {authors} = await fetchPostAuthors(serverUrl, [post], true); + if (authors?.length) { + const authorsModels = await operator.handleUsers({users: authors, prepareRecordsOnly: true}); + if (authorsModels.length) { + models.push(...authorsModels); + } + } + + // TODO Thread related functionality: https://mattermost.atlassian.net/browse/MM-41084 + //const viewingGlobalThreads = getViewingGlobalThreads(state); + // const collapsedThreadsEnabled = isCollapsedThreadsEnabled(state); + // actions.push(receivedNewPost(post, collapsedThreadsEnabled)); + if (!shouldIgnorePost(post)) { + let markAsViewed = false; + let markAsRead = false; + + if (!myChannel.manuallyUnread) { + if ( + post.user_id === currentUserId && + !isSystemMessage(post) && + !isFromWebhook(post) + ) { + markAsViewed = true; + markAsRead = false; + } else if ((post.channel_id === currentChannelId)) { // TODO: THREADS && !viewingGlobalThreads) { + // Don't mark as read if we're in global threads screen + // the currentChannelId still refers to previously viewed channel + markAsViewed = false; + markAsRead = true; + } + } + + if (markAsRead) { + markChannelAsRead(serverUrl, post.channel_id); + } else if (markAsViewed) { + const {member: viewedAt} = await markChannelAsViewed(serverUrl, post.channel_id, true); + if (viewedAt) { + models.push(viewedAt); + } + } else { + const hasMentions = msg.data.mentions.includes(currentUserId); + const {member: unreadAt} = await markChannelAsUnread( + serverUrl, + post.channel_id, + myChannel.messageCount + 1, + myChannel.mentionsCount + (hasMentions ? 1 : 0), + false, + myChannel.lastViewedAt, + true, + ); + if (unreadAt) { + models.push(unreadAt); + } + } + } + + operator.batchRecords(models); +} + +export async function handlePostEdited(serverUrl: string, msg: WebSocketMessage) { + const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; + if (!operator) { + return; + } + + let post: Post; + try { + post = JSON.parse(msg.data.post); + } catch { + return; + } + + const models: Model[] = []; + + const {authors} = await fetchPostAuthors(serverUrl, [post], true); + if (authors?.length) { + const authorsModels = await operator.handleUsers({users: authors, prepareRecordsOnly: true}); + if (authorsModels.length) { + models.push(...authorsModels); + } + } + + const postModels = await operator.handlePosts({ + actionType: ActionType.POSTS.RECEIVED_NEW, + order: [post.id], + posts: [post], + prepareRecordsOnly: true, + }); + if (postModels.length) { + models.push(...postModels); + } + + if (models.length) { + operator.batchRecords(models); + } +} + +export function handlePostDeleted(serverUrl: string, msg: WebSocketMessage) { + try { + const data: Post = JSON.parse(msg.data.post); + markPostAsDeleted(serverUrl, data); + } catch { + // Do nothing + } +} + +export async function handlePostUnread(serverUrl: string, msg: WebSocketMessage) { + const {channels} = await fetchMyChannel(serverUrl, msg.broadcast.team_id, msg.broadcast.channel_id, true); + const channel = channels?.[0]; + const postNumber = channel?.total_msg_count; + const delta = postNumber ? postNumber - msg.data.msg_count : msg.data.msg_count; + markChannelAsUnread(serverUrl, msg.broadcast.channel_id, delta, msg.data.mention_count, true, msg.data.last_viewed_at); +} diff --git a/app/actions/websocket/teams.ts b/app/actions/websocket/teams.ts index c6956e0345..0c55a3b69f 100644 --- a/app/actions/websocket/teams.ts +++ b/app/actions/websocket/teams.ts @@ -14,7 +14,9 @@ import {queryLastTeam} from '@queries/servers/team'; import {queryCurrentUser} from '@queries/servers/user'; import {dismissAllModals, popToRoot} from '@screens/navigation'; -export async function handleLeaveTeamEvent(serverUrl: string, msg: any) { +import type {WebSocketMessage} from '@typings/api/websocket'; + +export async function handleLeaveTeamEvent(serverUrl: string, msg: WebSocketMessage) { const database = DatabaseManager.serverDatabases[serverUrl]; if (!database) { return; diff --git a/app/components/post_list/post/avatar/index.tsx b/app/components/post_list/post/avatar/index.tsx index f3fcd20e23..148b2f0566 100644 --- a/app/components/post_list/post/avatar/index.tsx +++ b/app/components/post_list/post/avatar/index.tsx @@ -163,4 +163,4 @@ const withPost = withObservables(['post'], ({database, post}: {post: PostModel} }; }); -export default withDatabase(withPost(React.memo(Avatar))); +export default withDatabase(withPost(Avatar)); diff --git a/app/components/post_list/post/body/add_members/index.tsx b/app/components/post_list/post/body/add_members/index.tsx index 3b237140c0..01005edc75 100644 --- a/app/components/post_list/post/body/add_members/index.tsx +++ b/app/components/post_list/post/body/add_members/index.tsx @@ -234,4 +234,4 @@ const withChannelType = withObservables(['post'], ({database, post}: WithDatabas ), })); -export default withDatabase(withChannelType(React.memo(AddMembers))); +export default withDatabase(withChannelType(AddMembers)); diff --git a/app/components/post_list/post/body/content/embedded_bindings/button_binding/index.tsx b/app/components/post_list/post/body/content/embedded_bindings/button_binding/index.tsx index 1e178f25a2..550e07b9ae 100644 --- a/app/components/post_list/post/body/content/embedded_bindings/button_binding/index.tsx +++ b/app/components/post_list/post/body/content/embedded_bindings/button_binding/index.tsx @@ -141,4 +141,4 @@ const withTeamId = withObservables(['post'], ({post}: {post: PostModel}) => ({ ), })); -export default withTeamId(React.memo(ButtonBinding)); +export default withTeamId(ButtonBinding); diff --git a/app/components/post_list/post/body/content/embedded_bindings/menu_binding/index.tsx b/app/components/post_list/post/body/content/embedded_bindings/menu_binding/index.tsx index 710d7ef398..ceb8d57b7a 100644 --- a/app/components/post_list/post/body/content/embedded_bindings/menu_binding/index.tsx +++ b/app/components/post_list/post/body/content/embedded_bindings/menu_binding/index.tsx @@ -114,4 +114,4 @@ const withTeamId = withObservables(['post'], ({post}: {post: PostModel}) => ({ ), })); -export default withTeamId(React.memo(MenuBinding)); +export default withTeamId(MenuBinding); diff --git a/app/components/post_list/post/body/message/message.tsx b/app/components/post_list/post/body/message/message.tsx index 726743acd4..014ae67db6 100644 --- a/app/components/post_list/post/body/message/message.tsx +++ b/app/components/post_list/post/body/message/message.tsx @@ -110,4 +110,4 @@ const Message = ({currentUser, groupsForPosts, highlight, isEdited, isPendingOrF ); }; -export default React.memo(Message); +export default Message; diff --git a/app/components/post_list/post/header/header.tsx b/app/components/post_list/post/header/header.tsx index cf17f78695..4346f4e4bf 100644 --- a/app/components/post_list/post/header/header.tsx +++ b/app/components/post_list/post/header/header.tsx @@ -145,4 +145,4 @@ const Header = (props: HeaderProps) => { ); }; -export default React.memo(Header); +export default Header; diff --git a/app/components/post_list/post/post.tsx b/app/components/post_list/post/post.tsx index 9f10259a73..7de035ad38 100644 --- a/app/components/post_list/post/post.tsx +++ b/app/components/post_list/post/post.tsx @@ -299,4 +299,4 @@ const Post = ({ ); }; -export default React.memo(Post); +export default Post; diff --git a/app/constants/events.ts b/app/constants/events.ts index cd46ecc3c0..1aeaf8646e 100644 --- a/app/constants/events.ts +++ b/app/constants/events.ts @@ -14,4 +14,6 @@ export default keyMirror({ SERVER_LOGOUT: null, SERVER_VERSION_CHANGED: null, TEAM_LOAD_ERROR: null, + USER_TYPING: null, + USER_STOP_TYPING: null, }); diff --git a/app/constants/post.ts b/app/constants/post.ts index df2f0bba41..ff8eb08f27 100644 --- a/app/constants/post.ts +++ b/app/constants/post.ts @@ -46,4 +46,18 @@ export default { PostTypes.LEAVE_TEAM, PostTypes.REMOVE_FROM_TEAM, ], + IGNORE_POST_TYPES: [ + PostTypes.ADD_REMOVE, + PostTypes.ADD_TO_CHANNEL, + PostTypes.CHANNEL_DELETED, + PostTypes.CHANNEL_UNARCHIVED, + PostTypes.JOIN_LEAVE, + PostTypes.JOIN_CHANNEL, + PostTypes.LEAVE_CHANNEL, + PostTypes.REMOVE_FROM_CHANNEL, + PostTypes.JOIN_TEAM, + PostTypes.LEAVE_TEAM, + PostTypes.ADD_TO_TEAM, + PostTypes.REMOVE_FROM_TEAM, + ], }; diff --git a/app/queries/servers/channel.ts b/app/queries/servers/channel.ts index 05a4f1ca91..57b1452d7c 100644 --- a/app/queries/servers/channel.ts +++ b/app/queries/servers/channel.ts @@ -65,6 +65,11 @@ export const prepareMyChannelsForTeam = async (operator: ServerDataOperator, tea pinned_post_count = storedInfo.pinnedPostCount; } + const member = memberships.find((m) => m.channel_id === c.id); + if (member) { + member.last_post_at = c.last_post_at; + } + channelInfos.push({ id: c.id, header: c.header, diff --git a/app/screens/channel/channel_post_list/index.ts b/app/screens/channel/channel_post_list/index.ts index 33b0de79ef..a94dd6da48 100644 --- a/app/screens/channel/channel_post_list/index.ts +++ b/app/screens/channel/channel_post_list/index.ts @@ -51,7 +51,6 @@ const enhanced = withObservables(['channelId', 'forceQueryAfterAppState'], ({dat const {earliest, latest} = postsInChannel[0]; return database.get(POST).query( Q.and( - Q.where('delete_at', 0), Q.where('channel_id', channelId), Q.where('create_at', Q.between(earliest, latest)), ), diff --git a/app/utils/post/index.ts b/app/utils/post/index.ts index 80cba055cb..7c11a13ab8 100644 --- a/app/utils/post/index.ts +++ b/app/utils/post/index.ts @@ -28,7 +28,7 @@ export function areConsecutivePosts(post: PostModel, previousPost: PostModel) { return consecutive; } -export function isFromWebhook(post: PostModel): boolean { +export function isFromWebhook(post: PostModel | Post): boolean { return post.props && post.props.from_webhook === 'true'; } @@ -41,10 +41,10 @@ export function isPostEphemeral(post: PostModel): boolean { } export function isPostPendingOrFailed(post: PostModel): boolean { - return post.pendingPostId === post.id || post.props.failed; + return post.pendingPostId === post.id || post.props?.failed; } -export function isSystemMessage(post: PostModel): boolean { +export function isSystemMessage(post: PostModel | Post): boolean { return Boolean(post.type && post.type?.startsWith(Post.POST_TYPES.SYSTEM_MESSAGE_PREFIX)); } @@ -74,6 +74,10 @@ export const getMentionKeysForPost = (user: UserModel, post: PostModel, groups: return keys; }; +export function shouldIgnorePost(post: Post): boolean { + return Post.IGNORE_POST_TYPES.includes(post.type); +} + export const sortPostsByNewest = (posts: PostModel[]) => { return posts.sort((a, b) => { if (a.createAt > b.createAt) { diff --git a/types/api/posts.d.ts b/types/api/posts.d.ts index d180c88fd1..044e7fe1b2 100644 --- a/types/api/posts.d.ts +++ b/types/api/posts.d.ts @@ -64,7 +64,6 @@ type Post = { failed?: boolean; user_activity_posts?: Post[]; state?: 'DELETED'; - ownPost?: boolean; prev_post_id?: string; participants: null|string[]; };