UNPKG

mattermost-redux

Version:

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

497 lines (493 loc) 22.8 kB
"use strict"; // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. Object.defineProperty(exports, "__esModule", { value: true }); exports.isPersistentNotificationsEnabled = exports.makeIsPostCommentMention = exports.isPostIdSending = exports.isPostsChunkIncludingUnreadsPosts = exports.getMostRecentPostIdInChannel = exports.getSearchResults = exports.getPostsInCurrentChannel = exports.getLatestReplyablePostId = void 0; exports.getAllPosts = getAllPosts; exports.getPost = getPost; exports.isPostFlagged = isPostFlagged; exports.getPostRepliesCount = getPostRepliesCount; exports.getPostsInThread = getPostsInThread; exports.getPostsInThreadOrdered = getPostsInThreadOrdered; exports.getReactionsForPosts = getReactionsForPosts; exports.makeGetReactionsForPost = makeGetReactionsForPost; exports.getHasReactions = getHasReactions; exports.getOpenGraphMetadata = getOpenGraphMetadata; exports.getOpenGraphMetadataForUrl = getOpenGraphMetadataForUrl; exports.getPostIdsInCurrentChannel = getPostIdsInCurrentChannel; exports.makeGetPostIdsForThread = makeGetPostIdsForThread; exports.makeGetPostsChunkAroundPost = makeGetPostsChunkAroundPost; exports.getLatestInteractablePostId = getLatestInteractablePostId; exports.getLatestPostToEdit = getLatestPostToEdit; exports.makeGetPostsForThread = makeGetPostsForThread; exports.makeGetProfilesForThread = makeGetProfilesForThread; exports.makeGetCommentCountForPost = makeGetCommentCountForPost; exports.getSearchMatches = getSearchMatches; exports.makeGetMessageInHistoryItem = makeGetMessageInHistoryItem; exports.makeGetPostsForIds = makeGetPostsForIds; exports.getRecentPostsChunkInChannel = getRecentPostsChunkInChannel; exports.getOldestPostsChunkInChannel = getOldestPostsChunkInChannel; exports.getOldestPostTimeInChannel = getOldestPostTimeInChannel; exports.getPostIdsInChannel = getPostIdsInChannel; exports.getPostsChunkInChannelAroundTime = getPostsChunkInChannelAroundTime; exports.getUnreadPostsChunk = getUnreadPostsChunk; exports.getLimitedViews = getLimitedViews; exports.isPostPriorityEnabled = isPostPriorityEnabled; exports.isPostAcknowledgementsEnabled = isPostAcknowledgementsEnabled; exports.getAllowPersistentNotifications = getAllowPersistentNotifications; exports.getPersistentNotificationMaxRecipients = getPersistentNotificationMaxRecipients; exports.getPersistentNotificationIntervalMinutes = getPersistentNotificationIntervalMinutes; exports.getAllowPersistentNotificationsForGuests = getAllowPersistentNotificationsForGuests; exports.getPostAcknowledgements = getPostAcknowledgements; exports.makeGetPostAcknowledgementsWithProfiles = makeGetPostAcknowledgementsWithProfiles; exports.getTeamIdFromPost = getTeamIdFromPost; const constants_1 = require("mattermost-redux/constants"); const create_selector_1 = require("mattermost-redux/selectors/create_selector"); const channels_1 = require("mattermost-redux/selectors/entities/channels"); const common_1 = require("mattermost-redux/selectors/entities/common"); const general_1 = require("mattermost-redux/selectors/entities/general"); const preferences_1 = require("mattermost-redux/selectors/entities/preferences"); const teams_1 = require("mattermost-redux/selectors/entities/teams"); 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 user_utils_1 = require("mattermost-redux/utils/user_utils"); function getAllPosts(state) { return state.entities.posts.posts; } function getPost(state, postId) { return getAllPosts(state)[postId]; } function isPostFlagged(state, postId) { return (0, preferences_1.getBool)(state, constants_1.Preferences.CATEGORY_FLAGGED_POST, postId); } function getPostRepliesCount(state, postId) { return state.entities.posts.postsReplies[postId] || 0; } function getPostsInThread(state) { return state.entities.posts.postsInThread; } function getPostsInThreadOrdered(state, rootId) { const postIds = getPostsInThread(state)[rootId]; if (!postIds) { return [rootId]; } const allPosts = getAllPosts(state); const threadPosts = postIds.map((v) => allPosts[v]).filter((v) => v); const sortedPosts = threadPosts.sort(post_utils_1.comparePosts); return [...sortedPosts.map((v) => v.id), rootId]; } function getReactionsForPosts(state) { return state.entities.posts.reactions; } function makeGetReactionsForPost() { return (0, create_selector_1.createSelector)('makeGetReactionsForPost', getReactionsForPosts, (state, postId) => postId, (reactions, postId) => { if (reactions[postId]) { return reactions[postId]; } return undefined; }); } function getHasReactions(state, postId) { const reactions = getReactionsForPosts(state)?.[postId] || {}; return Object.keys(reactions).length > 0; } function getOpenGraphMetadata(state) { return state.entities.posts.openGraph; } function getOpenGraphMetadataForUrl(state, postId, url) { const openGraphForPost = state.entities.posts.openGraph[postId]; return openGraphForPost ? openGraphForPost[url] : undefined; } // getPostIdsInCurrentChannel returns the IDs of posts loaded at the bottom of the channel. It does not include older // posts such as those loaded by viewing a thread or a permalink. function getPostIdsInCurrentChannel(state) { return getPostIdsInChannel(state, state.entities.channels.currentChannelId); } function makeGetPostIdsForThread() { const getPostsForThread = makeGetPostsForThread(); return (0, helpers_1.createIdsSelector)('makeGetPostIdsForThread', (state, rootId) => getPostsForThread(state, rootId), (posts) => { return posts.map((post) => post.id); }); } function makeGetPostsChunkAroundPost() { return (0, helpers_1.createIdsSelector)('makeGetPostsChunkAroundPost', (state, postId, channelId) => state.entities.posts.postsInChannel[channelId], (state, postId) => postId, (postsForChannel, postId) => { if (!postsForChannel) { return null; } let postChunk; for (const block of postsForChannel) { const index = block.order.indexOf(postId); if (index === -1) { continue; } postChunk = block; } return postChunk; }); } function isPostInteractable(post) { return post && !post.delete_at && !(0, post_utils_1.isPostEphemeral)(post) && !(0, post_utils_1.isSystemMessage)(post) && !(0, post_utils_1.isPostPendingOrFailed)(post) && post.state !== constants_1.Posts.POST_DELETED; } function getLatestInteractablePostId(state, channelId, rootId = '') { const postsIds = rootId ? getPostsInThreadOrdered(state, rootId) : getPostIdsInChannel(state, channelId); if (!postsIds) { return ''; } const allPosts = getAllPosts(state); for (const postId of postsIds) { if (!isPostInteractable(allPosts[postId])) { continue; } return postId; } return ''; } function getLatestPostToEdit(state, channelId, rootId = '') { const postsIds = rootId ? getPostsInThreadOrdered(state, rootId) : getPostIdsInChannel(state, channelId); if (!postsIds) { return ''; } if (rootId) { postsIds.push(rootId); } const allPosts = getAllPosts(state); const currentUserId = (0, users_1.getCurrentUserId)(state); for (const postId of postsIds) { const post = allPosts[postId]; if (post?.user_id !== currentUserId || !isPostInteractable(post)) { continue; } return post.id; } return ''; } const getLatestReplyablePostId = (state) => getLatestInteractablePostId(state, (0, common_1.getCurrentChannelId)(state)); exports.getLatestReplyablePostId = getLatestReplyablePostId; // getPostsInCurrentChannel returns an array of all recent posts loaded at the bottom of the given channel. // It does not include older posts such as those loaded by viewing a thread or a permalink. exports.getPostsInCurrentChannel = (0, create_selector_1.createSelector)('getPostsInCurrentChannel', getAllPosts, getPostIdsInCurrentChannel, common_1.getCurrentUser, preferences_1.shouldShowJoinLeaveMessages, (allPosts, postIds, currentUser, showJoinLeave) => { if (!postIds) { return null; } const posts = []; for (let i = 0; i < postIds.length; i++) { const post = allPosts[postIds[i]]; if (!post || (0, post_utils_1.shouldFilterJoinLeavePost)(post, showJoinLeave, currentUser ? currentUser.username : '')) { continue; } posts.push(post); } return posts; }); // Returns a function that creates a creates a selector that will get the posts for a given thread. // That selector will take a props object (containing a rootId field) as its // only argument and will be memoized based on that argument. function makeGetPostsForThread() { return (0, helpers_1.createIdsSelector)('makeGetPostsForThread', getAllPosts, common_1.getCurrentUser, (state, rootId) => state.entities.posts.postsInThread[rootId], (state, rootId) => state.entities.posts.posts[rootId], preferences_1.shouldShowJoinLeaveMessages, (posts, currentUser, postsForThread, rootPost, showJoinLeave) => { const thread = []; if (rootPost) { thread.push(rootPost); } if (postsForThread && Array.isArray(postsForThread) && postsForThread.length > 0) { for (const postId of postsForThread) { const post = posts[postId]; if (!post) { continue; } const skip = (0, post_utils_1.shouldFilterJoinLeavePost)(post, showJoinLeave, currentUser ? currentUser.username : ''); if (!skip) { thread.push(post); } } } thread.sort(post_utils_1.comparePosts); return thread; }); } // The selector below filters current user if it exists. Excluding currentUser is just for convenience function makeGetProfilesForThread() { const getPostsForThread = makeGetPostsForThread(); return (0, create_selector_1.createSelector)('makeGetProfilesForThread', users_1.getUsers, users_1.getCurrentUserId, getPostsForThread, users_1.getUserStatuses, (allUsers, currentUserId, posts, userStatuses) => { const profileIds = posts.map((post) => post.user_id).filter(Boolean); const uniqueIds = [...new Set(profileIds)]; return uniqueIds.reduce((acc, id) => { const profile = userStatuses ? { ...allUsers[id], status: userStatuses[id] } : { ...allUsers[id] }; if (profile && Object.keys(profile).length > 0 && currentUserId !== id) { return [ ...acc, profile, ]; } return acc; }, []); }); } function makeGetCommentCountForPost() { return (0, create_selector_1.createSelector)('makeGetCommentCountForPost', getAllPosts, (state, post) => state.entities.posts.postsInThread[post ? post.root_id || post.id : ''] || null, (state, post) => post, (posts, postsForThread, post) => { if (!post) { return 0; } if (!postsForThread) { return post.reply_count; } let count = 0; postsForThread.forEach((id) => { const post = posts[id]; if (post && post.state !== constants_1.Posts.POST_DELETED && !(0, post_utils_1.isPostEphemeral)(post)) { count += 1; } }); return count; }); } exports.getSearchResults = (0, create_selector_1.createSelector)('getSearchResults', getAllPosts, (state) => state.entities.search.results, (posts, postIds) => { if (!postIds) { return []; } return postIds.map((id) => posts[id]); }); // Returns the matched text from the search results, if the server has provided them. // These matches will only be present if the server is running Mattermost 5.1 or higher // with Elasticsearch enabled to search posts. Otherwise, null will be returned. function getSearchMatches(state) { return state.entities.search.matches; } function makeGetMessageInHistoryItem(type) { return (0, create_selector_1.createSelector)('makeGetMessageInHistoryItem', (state) => state.entities.posts.messagesHistory, (messagesHistory) => { const idx = messagesHistory.index[type]; const messages = messagesHistory.messages; if (idx >= 0 && messages && messages.length > idx) { return messages[idx]; } return ''; }); } function makeGetPostsForIds() { return (0, helpers_1.createIdsSelector)('makeGetPostsForIds', getAllPosts, (state, postIds) => postIds, (allPosts, postIds) => { if (!postIds) { return []; } return postIds.map((id) => allPosts[id]); }); } exports.getMostRecentPostIdInChannel = (0, create_selector_1.createSelector)('getMostRecentPostIdInChannel', getAllPosts, (state, channelId) => getPostIdsInChannel(state, channelId), preferences_1.shouldShowJoinLeaveMessages, (posts, postIdsInChannel, allowSystemMessages) => { if (!postIdsInChannel) { return ''; } if (!allowSystemMessages) { // return the most recent non-system message in the channel let postId; for (let i = 0; i < postIdsInChannel.length; i++) { const p = posts[postIdsInChannel[i]]; if (!p.type || !p.type.startsWith(constants_1.Posts.SYSTEM_MESSAGE_PREFIX)) { postId = p.id; break; } } return postId; } // return the most recent message in the channel return postIdsInChannel[0]; }); function getRecentPostsChunkInChannel(state, channelId) { const postsForChannel = state.entities.posts.postsInChannel[channelId]; if (!postsForChannel) { return null; } return postsForChannel.find((block) => block.recent); } function getOldestPostsChunkInChannel(state, channelId) { const postsForChannel = state.entities.posts.postsInChannel[channelId]; if (!postsForChannel) { return null; } return postsForChannel.find((block) => block.oldest); } // returns timestamp of the channel's oldest post. 0 otherwise function getOldestPostTimeInChannel(state, channelId) { const postsForChannel = state.entities.posts.postsInChannel[channelId]; if (!postsForChannel) { return 0; } const allPosts = getAllPosts(state); const oldestPostTime = postsForChannel.reduce((acc, postBlock) => { if (postBlock.order.length > 0) { const oldestPostIdInBlock = postBlock.order[postBlock.order.length - 1]; const blockOldestPostTime = allPosts[oldestPostIdInBlock]?.create_at; if (typeof blockOldestPostTime === 'number' && blockOldestPostTime < acc) { return blockOldestPostTime; } } return acc; }, Number.MAX_SAFE_INTEGER); if (oldestPostTime === Number.MAX_SAFE_INTEGER) { return 0; } return oldestPostTime; } // getPostIdsInChannel returns the IDs of posts loaded at the bottom of the given channel. It does not include older // posts such as those loaded by viewing a thread or a permalink. function getPostIdsInChannel(state, channelId) { const recentBlock = getRecentPostsChunkInChannel(state, channelId); if (!recentBlock) { return null; } return recentBlock.order; } function getPostsChunkInChannelAroundTime(state, channelId, timeStamp) { const postsEntity = state.entities.posts; const postsForChannel = postsEntity.postsInChannel[channelId]; const posts = postsEntity.posts; if (!postsForChannel) { return null; } const blockAroundTimestamp = postsForChannel.find((block) => { const { order } = block; const recentPostInBlock = posts[order[0]]; const oldestPostInBlock = posts[order[order.length - 1]]; if (recentPostInBlock && oldestPostInBlock) { return (recentPostInBlock.create_at >= timeStamp && oldestPostInBlock.create_at <= timeStamp); } return false; }); return blockAroundTimestamp; } function getUnreadPostsChunk(state, channelId, timeStamp) { const postsEntity = state.entities.posts; const posts = postsEntity.posts; const recentChunk = getRecentPostsChunkInChannel(state, channelId); /* 1. lastViewedAt can be greater than the most recent chunk in case of edited posts etc. * return if recent block exists and oldest post is created after the last lastViewedAt timestamp i.e all posts are read and the lastViewedAt is greater than the last post 2. lastViewedAt can be less than the first post in a channel if all the last viewed posts are deleted * return if oldest block exist and oldest post created_at is greater than the last viewed post i.e all posts are unread so the lastViewedAt is lessthan the first post The above two exceptions are added because we cannot select the chunk based on timestamp alone as these cases are out of bounds 3. Normal cases where there are few unreads and few reads in a chunk as that is how unread API returns data * return getPostsChunkInChannelAroundTime */ if (recentChunk) { // This would happen if there are no posts in channel. // If the system messages are deleted by sys admin. // Experimental changes like hiding Join/Leave still will have recent chunk so it follows the default path based on timestamp if (!recentChunk.order.length) { return recentChunk; } const { order } = recentChunk; const oldestPostInBlock = posts[order[order.length - 1]]; // check for only oldest posts because this can be higher than the latest post if the last post is edited if (oldestPostInBlock.create_at <= timeStamp) { return recentChunk; } } const oldestPostsChunk = getOldestPostsChunkInChannel(state, channelId); if (oldestPostsChunk && oldestPostsChunk.order.length) { const { order } = oldestPostsChunk; const oldestPostInBlock = posts[order[order.length - 1]]; if (oldestPostInBlock.create_at >= timeStamp) { return oldestPostsChunk; } } return getPostsChunkInChannelAroundTime(state, channelId, timeStamp); } const isPostsChunkIncludingUnreadsPosts = (state, chunk, timeStamp) => { const postsEntity = state.entities.posts; const posts = postsEntity.posts; if (!chunk || !chunk.order.length) { return false; } const { order } = chunk; const oldestPostInBlock = posts[order[order.length - 1]]; return oldestPostInBlock.create_at <= timeStamp; }; exports.isPostsChunkIncludingUnreadsPosts = isPostsChunkIncludingUnreadsPosts; const isPostIdSending = (state, postId) => { return state.entities.posts.pendingPostIds.some((sendingPostId) => sendingPostId === postId); }; exports.isPostIdSending = isPostIdSending; const makeIsPostCommentMention = () => { return (0, create_selector_1.createSelector)('makeIsPostCommentMention', getAllPosts, getPostsInThread, common_1.getCurrentUser, getPost, (allPosts, postsInThread, currentUser, post) => { if (!post) { return false; } let threadRepliedToByCurrentUser = false; let isCommentMention = false; if (currentUser) { const rootId = post.root_id || post.id; const threadIds = postsInThread[rootId] || []; for (const pid of threadIds) { const p = allPosts[pid]; if (!p) { continue; } if (p.user_id === currentUser.id) { threadRepliedToByCurrentUser = true; } } const rootPost = allPosts[rootId]; isCommentMention = (0, post_utils_1.isPostCommentMention)({ post, currentUser, threadRepliedToByCurrentUser, rootPost }); } return isCommentMention; }); }; exports.makeIsPostCommentMention = makeIsPostCommentMention; function getLimitedViews(state) { return state.entities.posts.limitedViews; } function isPostPriorityEnabled(state) { return (0, general_1.getConfig)(state).PostPriority === 'true'; } function isPostAcknowledgementsEnabled(state) { return (isPostPriorityEnabled(state) && (0, general_1.getConfig)(state).PostAcknowledgements === 'true'); } function getAllowPersistentNotifications(state) { return (isPostPriorityEnabled(state) && (0, general_1.getConfig)(state).AllowPersistentNotifications === 'true'); } function getPersistentNotificationMaxRecipients(state) { return (0, general_1.getConfig)(state).PersistentNotificationMaxRecipients; } function getPersistentNotificationIntervalMinutes(state) { return (0, general_1.getConfig)(state).PersistentNotificationIntervalMinutes; } function getAllowPersistentNotificationsForGuests(state) { return (isPostPriorityEnabled(state) && (0, general_1.getConfig)(state).AllowPersistentNotificationsForGuests === 'true'); } function getPostAcknowledgements(state, postId) { return state.entities.posts.acknowledgements[postId]; } exports.isPersistentNotificationsEnabled = (0, create_selector_1.createSelector)('getPersistentNotificationsEnabled', common_1.getCurrentUser, getAllowPersistentNotifications, getAllowPersistentNotificationsForGuests, (user, forAll, forGuests) => ((0, user_utils_1.isGuest)(user.roles) ? (forAll && forGuests) : forAll)); function makeGetPostAcknowledgementsWithProfiles() { return (0, create_selector_1.createSelector)('makeGetPostAcknowledgementsWithProfiles', users_1.getUsers, getPostAcknowledgements, (users, acknowledgements) => { if (!acknowledgements) { return []; } return Object.keys(acknowledgements).flatMap((userId) => { if (!users[userId]) { return []; } return { user: users[userId], acknowledgedAt: acknowledgements[userId], }; }).sort((a, b) => b.acknowledgedAt - a.acknowledgedAt); }); } function getTeamIdFromPost(state, post) { const channel = (0, channels_1.getChannel)(state, post.channel_id); if (!channel) { return undefined; } if (channel.type === constants_1.General.DM_CHANNEL || channel.type === constants_1.General.GM_CHANNEL) { return (0, teams_1.getCurrentTeamId)(state); } return channel.team_id; }