[Gekidou] Handle post related websocket events (#5851)

* Handle post related websocket events

* Address feedback and several fixes

* Fix lint

* Address feedback

* LastPostAt as latest post create_at, and lastViewedAt as date.now()

* Address feedback

* Change database for operator and add type imports
This commit is contained in:
Daniel Espino García
2022-01-24 12:43:21 +01:00
committed by GitHub
parent d5228633b2
commit 1e355ee684
21 changed files with 422 additions and 72 deletions

View File

@@ -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<Promise<Model[]>> = [];
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) {

View File

@@ -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[];

View File

@@ -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<Promise<Model[]>> = [];
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<Promise<Model[]>> = [];
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 {

View File

@@ -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};
}
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -163,4 +163,4 @@ const withPost = withObservables(['post'], ({database, post}: {post: PostModel}
};
});
export default withDatabase(withPost(React.memo(Avatar)));
export default withDatabase(withPost(Avatar));

View File

@@ -234,4 +234,4 @@ const withChannelType = withObservables(['post'], ({database, post}: WithDatabas
),
}));
export default withDatabase(withChannelType(React.memo(AddMembers)));
export default withDatabase(withChannelType(AddMembers));

View File

@@ -141,4 +141,4 @@ const withTeamId = withObservables(['post'], ({post}: {post: PostModel}) => ({
),
}));
export default withTeamId(React.memo(ButtonBinding));
export default withTeamId(ButtonBinding);

View File

@@ -114,4 +114,4 @@ const withTeamId = withObservables(['post'], ({post}: {post: PostModel}) => ({
),
}));
export default withTeamId(React.memo(MenuBinding));
export default withTeamId(MenuBinding);

View File

@@ -110,4 +110,4 @@ const Message = ({currentUser, groupsForPosts, highlight, isEdited, isPendingOrF
);
};
export default React.memo(Message);
export default Message;

View File

@@ -145,4 +145,4 @@ const Header = (props: HeaderProps) => {
);
};
export default React.memo(Header);
export default Header;

View File

@@ -299,4 +299,4 @@ const Post = ({
);
};
export default React.memo(Post);
export default Post;

View File

@@ -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,
});

View File

@@ -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,
],
};

View File

@@ -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,

View File

@@ -51,7 +51,6 @@ const enhanced = withObservables(['channelId', 'forceQueryAfterAppState'], ({dat
const {earliest, latest} = postsInChannel[0];
return database.get<PostModel>(POST).query(
Q.and(
Q.where('delete_at', 0),
Q.where('channel_id', channelId),
Q.where('create_at', Q.between(earliest, latest)),
),

View File

@@ -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) {

View File

@@ -64,7 +64,6 @@ type Post = {
failed?: boolean;
user_activity_posts?: Post[];
state?: 'DELETED';
ownPost?: boolean;
prev_post_id?: string;
participants: null|string[];
};