mattermost-redux
Version:
Common code (API client, Redux stores, logic, utility functions) for building a Mattermost client
436 lines (435 loc) • 19.2 kB
JavaScript
;
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MAX_COMBINED_SYSTEM_POSTS = exports.START_OF_NEW_MESSAGES = exports.DATE_LINE = exports.CREATE_COMMENT = exports.COMBINED_USER_ACTIVITY = void 0;
exports.makePreparePostIdsForPostList = makePreparePostIdsForPostList;
exports.makeFilterPostsAndAddSeparators = makeFilterPostsAndAddSeparators;
exports.makeAddDateSeparatorsForSearchResults = makeAddDateSeparatorsForSearchResults;
exports.makeCombineUserActivityPosts = makeCombineUserActivityPosts;
exports.isStartOfNewMessages = isStartOfNewMessages;
exports.getTimestampForStartOfNewMessages = getTimestampForStartOfNewMessages;
exports.getNewMessagesIndex = getNewMessagesIndex;
exports.isCreateComment = isCreateComment;
exports.isDateLine = isDateLine;
exports.getDateForDateLine = getDateForDateLine;
exports.isCombinedUserActivityPost = isCombinedUserActivityPost;
exports.getPostIdsForCombinedUserActivityPost = getPostIdsForCombinedUserActivityPost;
exports.getFirstPostId = getFirstPostId;
exports.getLastPostId = getLastPostId;
exports.getLastPostIndex = getLastPostIndex;
exports.makeGenerateCombinedPost = makeGenerateCombinedPost;
exports.extractUserActivityData = extractUserActivityData;
exports.combineUserActivitySystemPost = combineUserActivitySystemPost;
exports.isUserActivityProp = isUserActivityProp;
const moment_timezone_1 = __importDefault(require("moment-timezone"));
const utilities_1 = require("@mattermost/types/utilities");
const constants_1 = require("mattermost-redux/constants");
const create_selector_1 = require("mattermost-redux/selectors/create_selector");
const posts_1 = require("mattermost-redux/selectors/entities/posts");
const preferences_1 = require("mattermost-redux/selectors/entities/preferences");
const users_1 = require("mattermost-redux/selectors/entities/users");
const helpers_1 = require("mattermost-redux/utils/helpers");
const post_utils_1 = require("mattermost-redux/utils/post_utils");
const timezone_utils_1 = require("mattermost-redux/utils/timezone_utils");
exports.COMBINED_USER_ACTIVITY = 'user-activity-';
exports.CREATE_COMMENT = 'create-comment';
exports.DATE_LINE = 'date-';
exports.START_OF_NEW_MESSAGES = 'start-of-new-messages-';
exports.MAX_COMBINED_SYSTEM_POSTS = 100;
function makePreparePostIdsForPostList() {
const filterPostsAndAddSeparators = makeFilterPostsAndAddSeparators();
const combineUserActivityPosts = makeCombineUserActivityPosts();
return (state, options) => {
let postIds = filterPostsAndAddSeparators(state, options);
postIds = combineUserActivityPosts(state, postIds);
return postIds;
};
}
// Returns a selector that, given the state and an object containing an array of postIds and an optional
// timestamp of when the channel was last read, returns a memoized array of postIds interspersed with
// day indicators and an optional new message indicator.
function makeFilterPostsAndAddSeparators() {
const getPostsForIds = (0, posts_1.makeGetPostsForIds)();
return (0, helpers_1.createIdsSelector)('makeFilterPostsAndAddSeparators', (state, { postIds }) => getPostsForIds(state, postIds), (state, { lastViewedAt }) => lastViewedAt, (state, { indicateNewMessages }) => indicateNewMessages, () => '', // This previously returned state.entities.posts.selectedPostId which stopped being set at some point
users_1.getCurrentUser, preferences_1.shouldShowJoinLeaveMessages, (posts, lastViewedAt, indicateNewMessages, selectedPostId, currentUser, showJoinLeave) => {
if (posts.length === 0 || !currentUser) {
return [];
}
const out = [];
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 === constants_1.Posts.POST_TYPES.EPHEMERAL_ADD_TO_CHANNEL && !selectedPostId)) {
continue;
}
// Filter out join/leave messages if necessary
if ((0, post_utils_1.shouldFilterJoinLeavePost)(post, showJoinLeave, currentUser.username)) {
continue;
}
// Filter out expired burn-on-read posts
// Note: BoR posts should display regardless of feature flag being enabled/disabled
// The feature flag only controls creation of NEW BoR messages, not display of existing ones
if (post.type === constants_1.Posts.POST_TYPES.BURN_ON_READ) {
// Skip if already expired and deleted
const expireAt = post.metadata?.expire_at;
if (expireAt && typeof expireAt === 'number' && expireAt <= Date.now()) {
continue;
}
}
lastDate = pushPostDateIfNeeded(post, currentUser, out, lastDate);
if (lastViewedAt &&
post.create_at > lastViewedAt &&
(post.user_id !== currentUser.id || (0, post_utils_1.isFromWebhook)(post)) &&
!addedNewMessagesIndicator &&
indicateNewMessages) {
out.push(exports.START_OF_NEW_MESSAGES + lastViewedAt);
addedNewMessagesIndicator = true;
}
out.push(post.id);
}
// Flip it back to newest to oldest
return out.reverse();
});
}
function pushPostDateIfNeeded(post, currentUser, out, lastDate) {
// Push on a date header if the last post was on a different day than the current one
const postDate = new Date(post.create_at);
const currentOffset = postDate.getTimezoneOffset() * 60 * 1000;
const timezone = (0, timezone_utils_1.getUserCurrentTimezone)(currentUser.timezone);
if (timezone) {
const zone = moment_timezone_1.default.tz.zone(timezone);
if (zone) {
const timezoneOffset = zone.utcOffset(postDate.getTime()) * 60 * 1000;
postDate.setTime(postDate.getTime() + (currentOffset - timezoneOffset));
}
}
if (!lastDate || lastDate.toDateString() !== postDate.toDateString()) {
out.push(exports.DATE_LINE + postDate.getTime());
return postDate;
}
return lastDate;
}
function makeAddDateSeparatorsForSearchResults() {
return (0, helpers_1.createIdsSelector)('makeAddDateSeparatorsForSearchResults', (state, posts) => posts, users_1.getCurrentUser, (posts, currentUser) => {
if (posts.length === 0 || !currentUser) {
return [];
}
const out = [];
let lastDate;
for (const post of posts) {
lastDate = pushPostDateIfNeeded(post, currentUser, out, lastDate);
out.push(post);
}
return out;
});
}
function makeCombineUserActivityPosts() {
const getPostsForIds = (0, posts_1.makeGetPostsForIds)();
return (0, helpers_1.createIdsSelector)('makeCombineUserActivityPosts', (state, postIds) => postIds, (state, postIds) => getPostsForIds(state, postIds), (postIds, posts) => {
let lastPostIsUserActivity = false;
let combinedCount = 0;
const out = [];
let changed = false;
for (let i = 0; i < postIds.length; i++) {
const postId = postIds[i];
if (isStartOfNewMessages(postId) || isDateLine(postId) || isCreateComment(postId)) {
// Not a post, so it won't be combined
out.push(postId);
lastPostIsUserActivity = false;
combinedCount = 0;
continue;
}
const post = posts[i];
const postIsUserActivity = (0, post_utils_1.isUserActivityPost)(post.type);
if (postIsUserActivity && lastPostIsUserActivity && combinedCount < exports.MAX_COMBINED_SYSTEM_POSTS) {
// Add the ID to the previous combined post
out[out.length - 1] += '_' + postId;
combinedCount += 1;
changed = true;
}
else if (postIsUserActivity) {
// Start a new combined post, even if the "combined" post is only a single post
out.push(exports.COMBINED_USER_ACTIVITY + postId);
combinedCount = 1;
changed = true;
}
else {
out.push(postId);
combinedCount = 0;
}
lastPostIsUserActivity = postIsUserActivity;
}
if (!changed) {
// Nothing was combined, so return the original array
return postIds;
}
return out;
});
}
function isStartOfNewMessages(item) {
return item.startsWith(exports.START_OF_NEW_MESSAGES);
}
function getTimestampForStartOfNewMessages(item) {
return parseInt(item.substring(exports.START_OF_NEW_MESSAGES.length), 10);
}
function getNewMessagesIndex(postListIds) {
return postListIds.findIndex(isStartOfNewMessages);
}
function isCreateComment(item) {
return item === exports.CREATE_COMMENT;
}
function isDateLine(item) {
return item.startsWith(exports.DATE_LINE);
}
function getDateForDateLine(item) {
return parseInt(item.substring(exports.DATE_LINE.length), 10);
}
function isCombinedUserActivityPost(item) {
return (/^user-activity-(?:[^_]+_)*[^_]+$/).test(item);
}
function getPostIdsForCombinedUserActivityPost(item) {
return item.substring(exports.COMBINED_USER_ACTIVITY.length).split('_');
}
function getFirstPostId(items) {
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (isStartOfNewMessages(item) || isDateLine(item) || isCreateComment(item)) {
// This is not a post at all
continue;
}
if (isCombinedUserActivityPost(item)) {
// This is a combined post, so find the first post ID from it
const combinedIds = getPostIdsForCombinedUserActivityPost(item);
return combinedIds[0];
}
// This is a post ID
return item;
}
return '';
}
function getLastPostId(items) {
for (let i = items.length - 1; i >= 0; i--) {
const item = items[i];
if (isStartOfNewMessages(item) || isDateLine(item) || isCreateComment(item)) {
// This is not a post at all
continue;
}
if (isCombinedUserActivityPost(item)) {
// This is a combined post, so find the first post ID from it
const combinedIds = getPostIdsForCombinedUserActivityPost(item);
return combinedIds[combinedIds.length - 1];
}
// This is a post ID
return item;
}
return '';
}
function getLastPostIndex(postIds) {
let index = 0;
for (let i = postIds.length - 1; i > 0; i--) {
const item = postIds[i];
if (!isStartOfNewMessages(item) && !isDateLine(item) && !isCreateComment(item)) {
index = i;
break;
}
}
return index;
}
function makeGenerateCombinedPost() {
const getPostsForIds = (0, posts_1.makeGetPostsForIds)();
const getPostIds = (0, helpers_1.memoizeResult)(getPostIdsForCombinedUserActivityPost);
return (0, create_selector_1.createSelector)('makeGenerateCombinedPost', (state, combinedId) => combinedId, (state, combinedId) => getPostsForIds(state, getPostIds(combinedId)), (combinedId, posts) => {
// All posts should be in the same channel
const channelId = posts[0].channel_id;
// Assume that the last post is the oldest one
const createAt = posts[posts.length - 1].create_at;
const messages = posts.map((post) => post.message);
return {
id: combinedId,
create_at: createAt,
update_at: 0,
edit_at: 0,
delete_at: 0,
is_pinned: false,
user_id: '',
channel_id: channelId,
root_id: '',
parent_id: '',
original_id: '',
message: messages.join('\n'),
type: constants_1.Posts.POST_TYPES.COMBINED_USER_ACTIVITY,
props: {
messages,
user_activity: combineUserActivitySystemPost(posts),
},
hashtags: '',
pending_post_id: '',
reply_count: 0,
metadata: {
embeds: [],
emojis: [],
files: [],
images: {},
reactions: [],
},
system_post_ids: posts.map((post) => post.id),
user_activity_posts: posts,
};
});
}
function extractUserActivityData(userActivities) {
const messageData = [];
const allUserIds = [];
const allUsernames = [];
userActivities.forEach((activity) => {
if (isUsersRelatedPost(activity.postType)) {
const { postType, actorId, userIds, usernames } = activity;
if (usernames && userIds) {
messageData.push({ postType, userIds: [...userIds], actorId: actorId[0] });
if (userIds.length > 0) {
allUserIds.push(...userIds.filter((userId) => userId));
}
if (usernames.length > 0) {
allUsernames.push(...usernames.filter((username) => username));
}
allUserIds.push(actorId[0]);
}
}
else {
const { postType, actorId } = activity;
const userIds = actorId;
messageData.push({ postType, userIds });
allUserIds.push(...userIds);
}
});
function reduceUsers(acc, curr) {
if (!acc.includes(curr)) {
acc.push(curr);
}
return acc;
}
return {
allUserIds: allUserIds.reduce(reduceUsers, []),
allUsernames: allUsernames.reduce(reduceUsers, []),
messageData,
};
}
function isUsersRelatedPost(postType) {
return (postType === constants_1.Posts.POST_TYPES.ADD_TO_TEAM ||
postType === constants_1.Posts.POST_TYPES.ADD_TO_CHANNEL ||
postType === constants_1.Posts.POST_TYPES.REMOVE_FROM_CHANNEL);
}
function mergeLastSimilarPosts(userActivities) {
const prevPost = userActivities[userActivities.length - 1];
const prePrevPost = userActivities[userActivities.length - 2];
const prevPostType = prevPost && prevPost.postType;
const prePrevPostType = prePrevPost && prePrevPost.postType;
if (prevPostType === prePrevPostType) {
userActivities.pop();
prePrevPost.actorId.push(...prevPost.actorId);
}
}
function isSameActorsInUserActivities(prevActivity, curActivity) {
const prevPostActorsSet = new Set(prevActivity.actorId);
const currentPostActorsSet = new Set(curActivity.actorId);
if (prevPostActorsSet.size !== currentPostActorsSet.size) {
return false;
}
let hasAllActors = true;
currentPostActorsSet.forEach((actor) => {
if (!prevPostActorsSet.has(actor)) {
hasAllActors = false;
}
});
return hasAllActors;
}
function combineUserActivitySystemPost(systemPosts = []) {
if (systemPosts.length === 0) {
return undefined;
}
const userActivities = [];
systemPosts.reverse().forEach((post) => {
const postType = post.type;
const actorId = post.user_id;
// When combining removed posts, the actorId does not need to be the same for each post.
// All removed posts will be combined regardless of their respective actorIds.
const isRemovedPost = post.type === constants_1.Posts.POST_TYPES.REMOVE_FROM_CHANNEL;
const addedUserId = (0, post_utils_1.ensureString)(post.props?.addedUserId);
const removedUserId = (0, post_utils_1.ensureString)(post.props?.removedUserId);
const addedUsername = (0, post_utils_1.ensureString)(post.props?.addedUsername);
const removedUsername = (0, post_utils_1.ensureString)(post.props?.removedUsername);
const userId = isUsersRelatedPost(postType) ? addedUserId || removedUserId : '';
const username = isUsersRelatedPost(postType) ? addedUsername || removedUsername : '';
const prevPost = userActivities[userActivities.length - 1];
const isSamePostType = prevPost && prevPost.postType === post.type;
const isSameActor = prevPost && prevPost.actorId[0] === post.user_id;
const isJoinedPrevPost = prevPost && prevPost.postType === constants_1.Posts.POST_TYPES.JOIN_CHANNEL;
const isLeftCurrentPost = post.type === constants_1.Posts.POST_TYPES.LEAVE_CHANNEL;
const prePrevPost = userActivities[userActivities.length - 2];
const isJoinedPrePrevPost = prePrevPost && prePrevPost.postType === constants_1.Posts.POST_TYPES.JOIN_CHANNEL;
const isLeftPrevPost = prevPost && prevPost.postType === constants_1.Posts.POST_TYPES.LEAVE_CHANNEL;
if (prevPost && isSamePostType && (isSameActor || isRemovedPost)) {
prevPost.userIds.push(userId);
prevPost.usernames.push(username);
}
else if (isSamePostType && !isSameActor && !isUsersRelatedPost(postType)) {
prevPost.actorId.push(actorId);
const isSameActors = (prePrevPost && isSameActorsInUserActivities(prePrevPost, prevPost));
if (isJoinedPrePrevPost && isLeftPrevPost && isSameActors) {
userActivities.pop();
prePrevPost.postType = constants_1.Posts.POST_TYPES.JOIN_LEAVE_CHANNEL;
mergeLastSimilarPosts(userActivities);
}
}
else if (isJoinedPrevPost && isLeftCurrentPost && prevPost.actorId.length === 1 && isSameActor) {
prevPost.postType = constants_1.Posts.POST_TYPES.JOIN_LEAVE_CHANNEL;
mergeLastSimilarPosts(userActivities);
}
else {
userActivities.push({
actorId: [actorId],
userIds: [userId],
usernames: [username],
postType,
});
}
});
return extractUserActivityData(userActivities);
}
function isMessageData(v) {
if (typeof v !== 'object' || !v) {
return false;
}
if ('actorId' in v && typeof v.actorId !== 'string') {
return false;
}
if (!('postType' in v) || typeof v.postType !== 'string') {
return false;
}
if (!('userIds' in v) || !(0, utilities_1.isStringArray)(v.userIds)) {
return false;
}
return true;
}
function isUserActivityProp(v) {
if (typeof v !== 'object' || !v) {
return false;
}
if (!('allUserIds' in v) || !(0, utilities_1.isStringArray)(v.allUserIds)) {
return false;
}
if (!('allUsernames' in v) || !(0, utilities_1.isStringArray)(v.allUsernames)) {
return false;
}
if (!('messageData' in v) || !(0, utilities_1.isArrayOf)(v.messageData, isMessageData)) {
return false;
}
return true;
}