* [Gekidou]: MM-39757 - Recent mentions * Refactor channel_info to a separate component * Fixes schema tests * Fixes channel_info theme color * Removes RECEIVED_MENTIONS table and model Removes RECEIVED_MENTIONS table and model and saves recent_mentions in the SYSTEM table under the recentMentions ID. * Cleanup recent_mentions handler * Adds i18n in recent_mentions screen * Observe changes on the post messages * Addresses review comments * Batches records * Addresses review comments * Addresses review comments * Addresses review comments * Addresses review comments * Fetches channels and users needed for mentions Fetching mentions from all teams might result in missing info like user profiles, and channels missing from the DB. This commit fetches all missing users and channels. * Adds empty state for recent mentions * Prepares all missing models for channels * Addresses review comments * Fixes mention keys for recent mentions User mention keys when asking for mentions should not include general purpose ones, like @channel, @all, @here. Fixes ActivityIndicator color in recent mentions screen. * Removes top margin of mention message * Addresses review comments * Fixes group.name undefined
373 lines
13 KiB
TypeScript
373 lines
13 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import moment from 'moment-timezone';
|
|
|
|
import {Post} from '@constants';
|
|
|
|
import type PostModel from '@typings/database/models/servers/post';
|
|
|
|
const joinLeavePostTypes = [
|
|
Post.POST_TYPES.JOIN_LEAVE,
|
|
Post.POST_TYPES.JOIN_CHANNEL,
|
|
Post.POST_TYPES.LEAVE_CHANNEL,
|
|
Post.POST_TYPES.ADD_REMOVE,
|
|
Post.POST_TYPES.ADD_TO_CHANNEL,
|
|
Post.POST_TYPES.REMOVE_FROM_CHANNEL,
|
|
Post.POST_TYPES.JOIN_TEAM,
|
|
Post.POST_TYPES.LEAVE_TEAM,
|
|
Post.POST_TYPES.ADD_TO_TEAM,
|
|
Post.POST_TYPES.REMOVE_FROM_TEAM,
|
|
Post.POST_TYPES.COMBINED_USER_ACTIVITY,
|
|
];
|
|
|
|
const postTypePriority = {
|
|
[Post.POST_TYPES.JOIN_TEAM]: 0,
|
|
[Post.POST_TYPES.ADD_TO_TEAM]: 1,
|
|
[Post.POST_TYPES.LEAVE_TEAM]: 2,
|
|
[Post.POST_TYPES.REMOVE_FROM_TEAM]: 3,
|
|
[Post.POST_TYPES.JOIN_CHANNEL]: 4,
|
|
[Post.POST_TYPES.ADD_TO_CHANNEL]: 5,
|
|
[Post.POST_TYPES.LEAVE_CHANNEL]: 6,
|
|
[Post.POST_TYPES.REMOVE_FROM_CHANNEL]: 7,
|
|
[Post.POST_TYPES.PURPOSE_CHANGE]: 8,
|
|
[Post.POST_TYPES.HEADER_CHANGE]: 9,
|
|
[Post.POST_TYPES.JOIN_LEAVE]: 10,
|
|
[Post.POST_TYPES.DISPLAYNAME_CHANGE]: 11,
|
|
[Post.POST_TYPES.CONVERT_CHANNEL]: 12,
|
|
[Post.POST_TYPES.CHANNEL_DELETED]: 13,
|
|
[Post.POST_TYPES.CHANNEL_UNARCHIVED]: 14,
|
|
[Post.POST_TYPES.ADD_REMOVE]: 15,
|
|
[Post.POST_TYPES.EPHEMERAL]: 16,
|
|
};
|
|
|
|
export const COMBINED_USER_ACTIVITY = 'user-activity-';
|
|
export const DATE_LINE = 'date-';
|
|
export const START_OF_NEW_MESSAGES = 'start-of-new-messages';
|
|
export const MAX_COMBINED_SYSTEM_POSTS = 100;
|
|
|
|
function combineUserActivityPosts(orderedPosts: Array<PostModel | string>) {
|
|
let lastPostIsUserActivity = false;
|
|
let combinedCount = 0;
|
|
const out: Array<PostModel | string> = [];
|
|
let changed = false;
|
|
|
|
for (let i = 0; i < orderedPosts.length; i++) {
|
|
const post = orderedPosts[i];
|
|
|
|
if (typeof post === 'string') {
|
|
if (post === START_OF_NEW_MESSAGES || post.startsWith(DATE_LINE)) {
|
|
// Not a post, so it won't be combined
|
|
out.push(post);
|
|
|
|
lastPostIsUserActivity = false;
|
|
combinedCount = 0;
|
|
|
|
continue;
|
|
}
|
|
} else {
|
|
const postIsUserActivity = Post.USER_ACTIVITY_POST_TYPES.includes(post.type);
|
|
if (postIsUserActivity && lastPostIsUserActivity && combinedCount < MAX_COMBINED_SYSTEM_POSTS) {
|
|
// Add the ID to the previous combined post
|
|
out[out.length - 1] += '_' + post.id;
|
|
combinedCount += 1;
|
|
changed = true;
|
|
} else if (postIsUserActivity) {
|
|
// Start a new combined post, even if the "combined" post is only a single post
|
|
out.push(COMBINED_USER_ACTIVITY + post.id);
|
|
combinedCount = 1;
|
|
changed = true;
|
|
} else {
|
|
out.push(post);
|
|
combinedCount = 0;
|
|
}
|
|
|
|
lastPostIsUserActivity = postIsUserActivity;
|
|
}
|
|
}
|
|
|
|
if (!changed) {
|
|
return orderedPosts;
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
function comparePostTypes(a: typeof postTypePriority, b: typeof postTypePriority) {
|
|
return postTypePriority[a.postType] - postTypePriority[b.postType];
|
|
}
|
|
|
|
function extractUserActivityData(userActivities: any) {
|
|
const messageData: any[] = [];
|
|
const allUserIds: string[] = [];
|
|
const allUsernames: string[] = [];
|
|
Object.entries(userActivities).forEach(([postType, values]: [string, any]) => {
|
|
if (
|
|
postType === Post.POST_TYPES.ADD_TO_TEAM ||
|
|
postType === Post.POST_TYPES.ADD_TO_CHANNEL ||
|
|
postType === Post.POST_TYPES.REMOVE_FROM_CHANNEL
|
|
) {
|
|
Object.keys(values).map((key) => [key, values[key]]).forEach(([actorId, users]) => {
|
|
if (Array.isArray(users)) {
|
|
throw new Error('Invalid Post activity data');
|
|
}
|
|
const {ids, usernames} = users;
|
|
messageData.push({postType, userIds: [...usernames, ...ids], actorId});
|
|
if (ids.length > 0) {
|
|
allUserIds.push(...ids);
|
|
}
|
|
|
|
if (usernames.length > 0) {
|
|
allUsernames.push(...usernames);
|
|
}
|
|
allUserIds.push(actorId);
|
|
});
|
|
} else {
|
|
if (!Array.isArray(values)) {
|
|
throw new Error('Invalid Post activity data');
|
|
}
|
|
messageData.push({postType, userIds: values});
|
|
allUserIds.push(...values);
|
|
}
|
|
});
|
|
|
|
messageData.sort(comparePostTypes);
|
|
|
|
function reduceUsers(acc: string[], curr: string) {
|
|
if (!acc.includes(curr)) {
|
|
acc.push(curr);
|
|
}
|
|
return acc;
|
|
}
|
|
|
|
return {
|
|
allUserIds: allUserIds.reduce(reduceUsers, []),
|
|
allUsernames: allUsernames.reduce(reduceUsers, []),
|
|
messageData,
|
|
};
|
|
}
|
|
|
|
function isJoinLeavePostForUsername(post: PostModel, currentUsername: string): boolean {
|
|
if (!post.props || !currentUsername) {
|
|
return false;
|
|
}
|
|
|
|
if (post.props.user_activity_posts) {
|
|
for (const childPost of post.props.user_activity_posts as PostModel[]) {
|
|
if (isJoinLeavePostForUsername(childPost, currentUsername)) {
|
|
// If any of the contained posts are for this user, the client will
|
|
// need to figure out how to render the post
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return post.props.username === currentUsername ||
|
|
post.props.addedUsername === currentUsername ||
|
|
post.props.removedUsername === currentUsername;
|
|
}
|
|
|
|
// are we going to do something with selectedPostId as in v1?
|
|
export function selectOrderedPosts(
|
|
posts: PostModel[], lastViewedAt: number, indicateNewMessages: boolean, currentUsername: string, showJoinLeave: boolean,
|
|
timezoneEnabled: boolean, currentTimezone: string | null, isThreadScreen = false) {
|
|
if (posts.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const out: Array<PostModel|string> = [];
|
|
let lastDate;
|
|
let addedNewMessagesIndicator = false;
|
|
|
|
// Iterating through the posts from oldest to newest
|
|
for (let i = posts.length - 1; i >= 0; i--) {
|
|
const post = posts[i];
|
|
|
|
if (
|
|
!post ||
|
|
(post.type === Post.POST_TYPES.EPHEMERAL_ADD_TO_CHANNEL && !isThreadScreen)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
// Filter out join/leave messages if necessary
|
|
if (shouldFilterJoinLeavePost(post, showJoinLeave, currentUsername)) {
|
|
continue;
|
|
}
|
|
|
|
// Push on a date header if the last post was on a different day than the current one
|
|
const postDate = new Date(post.createAt);
|
|
if (timezoneEnabled) {
|
|
const currentOffset = postDate.getTimezoneOffset() * 60 * 1000;
|
|
if (currentTimezone) {
|
|
const zone = moment.tz.zone(currentTimezone);
|
|
if (zone) {
|
|
const timezoneOffset = zone.utcOffset(post.createAt) * 60 * 1000;
|
|
postDate.setTime(post.createAt + (currentOffset - timezoneOffset));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!lastDate || lastDate.toDateString() !== postDate.toDateString()) {
|
|
out.push(DATE_LINE + postDate.getTime());
|
|
|
|
lastDate = postDate;
|
|
}
|
|
|
|
if (
|
|
lastViewedAt &&
|
|
post.createAt > lastViewedAt &&
|
|
!addedNewMessagesIndicator &&
|
|
indicateNewMessages
|
|
) {
|
|
out.push(START_OF_NEW_MESSAGES);
|
|
addedNewMessagesIndicator = true;
|
|
}
|
|
|
|
out.push(post);
|
|
}
|
|
|
|
// Flip it back to newest to oldest
|
|
return out.reverse();
|
|
}
|
|
|
|
function combineUserActivitySystemPost(systemPosts: PostModel[]) {
|
|
const userActivities = systemPosts.reduce((acc: any, post: PostModel) => {
|
|
const postType = post.type;
|
|
let userActivityProps = acc;
|
|
const combinedPostType = userActivityProps[postType as string];
|
|
|
|
if (
|
|
postType === Post.POST_TYPES.ADD_TO_TEAM ||
|
|
postType === Post.POST_TYPES.ADD_TO_CHANNEL ||
|
|
postType === Post.POST_TYPES.REMOVE_FROM_CHANNEL
|
|
) {
|
|
const userId = post.props.addedUserId || post.props.removedUserId;
|
|
const username = post.props.addedUsername || post.props.removedUsername;
|
|
if (combinedPostType) {
|
|
if (Array.isArray(combinedPostType[post.userId])) {
|
|
throw new Error('Invalid Post activity data');
|
|
}
|
|
const users = combinedPostType[post.userId] || {ids: [], usernames: []};
|
|
if (userId) {
|
|
if (!users.ids.includes(userId)) {
|
|
users.ids.push(userId);
|
|
}
|
|
} else if (username && !users.usernames.includes(username)) {
|
|
users.usernames.push(username);
|
|
}
|
|
combinedPostType[post.userId] = users;
|
|
} else {
|
|
const users = {
|
|
ids: [] as string[],
|
|
usernames: [] as string[],
|
|
};
|
|
|
|
if (userId) {
|
|
users.ids.push(userId);
|
|
} else if (username) {
|
|
users.usernames.push(username);
|
|
}
|
|
userActivityProps[postType] = {
|
|
[post.userId]: users,
|
|
};
|
|
}
|
|
} else {
|
|
const propsUserId = post.userId;
|
|
|
|
if (combinedPostType) {
|
|
if (!Array.isArray(combinedPostType)) {
|
|
throw new Error('Invalid Post activity data');
|
|
}
|
|
if (!combinedPostType.includes(propsUserId)) {
|
|
userActivityProps[postType] = [...combinedPostType, propsUserId];
|
|
}
|
|
} else {
|
|
userActivityProps = {...userActivityProps, [postType]: [propsUserId]};
|
|
}
|
|
}
|
|
|
|
return userActivityProps;
|
|
}, {});
|
|
|
|
return extractUserActivityData(userActivities);
|
|
}
|
|
|
|
export function generateCombinedPost(combinedId: string, systemPosts: PostModel[]): Post {
|
|
// All posts should be in the same chann
|
|
const channelId = systemPosts[0].channelId;
|
|
|
|
// Assume that the last post is the oldest one
|
|
const createAt = systemPosts[systemPosts.length - 1].createAt;
|
|
|
|
const messages = systemPosts.map((post) => post.message);
|
|
const message = messages.join('\n');
|
|
|
|
return {
|
|
id: combinedId,
|
|
root_id: '',
|
|
original_id: '',
|
|
channel_id: channelId,
|
|
create_at: createAt,
|
|
delete_at: 0,
|
|
edit_at: 0,
|
|
update_at: 0,
|
|
is_pinned: false,
|
|
message,
|
|
hashtags: '',
|
|
pending_post_id: '',
|
|
reply_count: 0,
|
|
props: {
|
|
messages,
|
|
user_activity: combineUserActivitySystemPost(systemPosts),
|
|
user_activity_posts: systemPosts,
|
|
system_post_ids: systemPosts.map((post) => post.id),
|
|
},
|
|
type: Post.POST_TYPES.COMBINED_USER_ACTIVITY as PostType,
|
|
user_id: '',
|
|
metadata: {},
|
|
participants: null,
|
|
};
|
|
}
|
|
|
|
export function getDateForDateLine(item: string) {
|
|
return parseInt(item.substring(DATE_LINE.length), 10);
|
|
}
|
|
|
|
export function getPostIdsForCombinedUserActivityPost(item: string) {
|
|
return item.substring(COMBINED_USER_ACTIVITY.length).split('_');
|
|
}
|
|
|
|
export function isCombinedUserActivityPost(item: string) {
|
|
return (/^user-activity-(?:[^_]+_)*[^_]+$/).test(item);
|
|
}
|
|
|
|
export function isDateLine(item: string) {
|
|
return Boolean(item?.startsWith(DATE_LINE));
|
|
}
|
|
|
|
export function isStartOfNewMessages(item: string) {
|
|
return item === START_OF_NEW_MESSAGES;
|
|
}
|
|
|
|
export function preparePostList(
|
|
posts: PostModel[], lastViewedAt: number, indicateNewMessages: boolean, currentUsername: string, showJoinLeave: boolean,
|
|
timezoneEnabled: boolean, currentTimezone: string | null, isThreadScreen = false) {
|
|
const orderedPosts = selectOrderedPosts(posts, lastViewedAt, indicateNewMessages, currentUsername, showJoinLeave, timezoneEnabled, currentTimezone, isThreadScreen);
|
|
return combineUserActivityPosts(orderedPosts);
|
|
}
|
|
|
|
// Returns true if a post should be hidden when the user has Show Join/Leave Messages disabled
|
|
export function shouldFilterJoinLeavePost(post: PostModel, showJoinLeave: boolean, currentUsername: string): boolean {
|
|
if (showJoinLeave) {
|
|
return false;
|
|
}
|
|
|
|
// Don't filter out non-join/leave messages
|
|
if (joinLeavePostTypes.indexOf(post.type) === -1) {
|
|
return false;
|
|
}
|
|
|
|
// Don't filter out join/leave messages about the current user
|
|
return !isJoinLeavePostForUsername(post, currentUsername);
|
|
}
|