UNPKG

mattermost-redux

Version:

Common code (API client, Redux stores, logic, utility functions) for building a Mattermost client

436 lines (435 loc) 19.2 kB
"use strict"; // 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; }