UNPKG

mattermost-redux

Version:

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

1,210 lines 51.7 kB
"use strict"; // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.receivedPost = receivedPost; exports.receivedNewPost = receivedNewPost; exports.receivedPosts = receivedPosts; exports.receivedPostsAfter = receivedPostsAfter; exports.receivedPostsBefore = receivedPostsBefore; exports.receivedPostsSince = receivedPostsSince; exports.receivedPostsInChannel = receivedPostsInChannel; exports.receivedPostsInThread = receivedPostsInThread; exports.postDeleted = postDeleted; exports.postRemoved = postRemoved; exports.postPinnedChanged = postPinnedChanged; exports.getPost = getPost; exports.createPost = createPost; exports.resetCreatePostRequest = resetCreatePostRequest; exports.deletePost = deletePost; exports.editPost = editPost; exports.setUnreadPost = setUnreadPost; exports.pinPost = pinPost; exports.decrementPinnedPostCount = decrementPinnedPostCount; exports.unpinPost = unpinPost; exports.addReaction = addReaction; exports.removeReaction = removeReaction; exports.getCustomEmojiForReaction = getCustomEmojiForReaction; exports.flagPost = flagPost; exports.getPostThread = getPostThread; exports.getNewestPostThread = getNewestPostThread; exports.getPosts = getPosts; exports.getPostsUnread = getPostsUnread; exports.getPostsSince = getPostsSince; exports.getPostsBefore = getPostsBefore; exports.getPostsAfter = getPostsAfter; exports.getPostsAround = getPostsAround; exports.getPostThreads = getPostThreads; exports.getMentionsAndStatusesForPosts = getMentionsAndStatusesForPosts; exports.getPostsByIds = getPostsByIds; exports.getPostsByIdsBatched = getPostsByIdsBatched; exports.getPostEditHistory = getPostEditHistory; exports.getNeededAtMentionedUsernamesAndGroups = getNeededAtMentionedUsernamesAndGroups; exports.removePost = removePost; exports.moveThread = moveThread; exports.selectFocusedPostId = selectFocusedPostId; exports.unflagPost = unflagPost; exports.addPostReminder = addPostReminder; exports.doPostAction = doPostAction; exports.doPostActionWithCookie = doPostActionWithCookie; exports.addMessageIntoHistory = addMessageIntoHistory; exports.resetHistoryIndex = resetHistoryIndex; exports.moveHistoryIndexBack = moveHistoryIndexBack; exports.moveHistoryIndexForward = moveHistoryIndexForward; exports.resetReloadPostsInTranslatedChannels = resetReloadPostsInTranslatedChannels; exports.resetReloadPostsInChannel = resetReloadPostsInChannel; exports.acknowledgePost = acknowledgePost; exports.unacknowledgePost = unacknowledgePost; exports.restorePostVersion = restorePostVersion; const redux_batched_actions_1 = require("redux-batched-actions"); const message_attachments_1 = require("@mattermost/types/message_attachments"); const action_types_1 = require("mattermost-redux/action_types"); const channels_1 = require("mattermost-redux/actions/channels"); const emojis_1 = require("mattermost-redux/actions/emojis"); const groups_1 = require("mattermost-redux/actions/groups"); const helpers_1 = require("mattermost-redux/actions/helpers"); const preferences_1 = require("mattermost-redux/actions/preferences"); const status_profile_polling_1 = require("mattermost-redux/actions/status_profile_polling"); const threads_1 = require("mattermost-redux/actions/threads"); const users_1 = require("mattermost-redux/actions/users"); const client_1 = require("mattermost-redux/client"); const constants_1 = require("mattermost-redux/constants"); const channels_2 = require("mattermost-redux/selectors/entities/channels"); const common_1 = require("mattermost-redux/selectors/entities/common"); const emojis_2 = require("mattermost-redux/selectors/entities/emojis"); const groups_2 = require("mattermost-redux/selectors/entities/groups"); const PostSelectors = __importStar(require("mattermost-redux/selectors/entities/posts")); const preferences_2 = require("mattermost-redux/selectors/entities/preferences"); const users_2 = require("mattermost-redux/selectors/entities/users"); const data_loader_1 = require("mattermost-redux/utils/data_loader"); const post_list_1 = require("mattermost-redux/utils/post_list"); const errors_1 = require("./errors"); // receivedPost should be dispatched after a single post from the server. This typically happens when an existing post // is updated. function receivedPost(post, crtEnabled) { return { type: action_types_1.PostTypes.RECEIVED_POST, data: post, features: { crtEnabled }, }; } // receivedNewPost should be dispatched when receiving a newly created post or when sending a request to the server // to make a new post. function receivedNewPost(post, crtEnabled) { return { type: action_types_1.PostTypes.RECEIVED_NEW_POST, data: post, features: { crtEnabled }, }; } // receivedPosts should be dispatched when receiving multiple posts from the server that may or may not be ordered. // This will typically be used alongside other actions like receivedPostsAfter which require the posts to be ordered. function receivedPosts(posts) { return { type: action_types_1.PostTypes.RECEIVED_POSTS, data: posts, }; } // receivedPostsAfter should be dispatched when receiving an ordered list of posts that come before a given post. function receivedPostsAfter(posts, channelId, afterPostId, recent = false) { return { type: action_types_1.PostTypes.RECEIVED_POSTS_AFTER, channelId, data: posts, afterPostId, recent, }; } // receivedPostsBefore should be dispatched when receiving an ordered list of posts that come after a given post. function receivedPostsBefore(posts, channelId, beforePostId, oldest = false) { return { type: action_types_1.PostTypes.RECEIVED_POSTS_BEFORE, channelId, data: posts, beforePostId, oldest, }; } // receivedPostsSince should be dispatched when receiving a list of posts that have been updated since a certain time. // Due to how the API endpoint works, some of these posts will be ordered, but others will not, so this needs special // handling from the reducers. function receivedPostsSince(posts, channelId) { return { type: action_types_1.PostTypes.RECEIVED_POSTS_SINCE, channelId, data: posts, }; } // receivedPostsInChannel should be dispatched when receiving a list of ordered posts within a channel when the // the adjacent posts are not known. function receivedPostsInChannel(posts, channelId, recent = false, oldest = false) { return { type: action_types_1.PostTypes.RECEIVED_POSTS_IN_CHANNEL, channelId, data: posts, recent, oldest, }; } // receivedPostsInThread should be dispatched when receiving a list of unordered posts in a thread. function receivedPostsInThread(posts, rootId) { return { type: action_types_1.PostTypes.RECEIVED_POSTS_IN_THREAD, data: posts, rootId, }; } // postDeleted should be dispatched when a post has been deleted and should be replaced with a "message deleted" // placeholder. This typically happens when a post is deleted by another user. function postDeleted(post) { return { type: action_types_1.PostTypes.POST_DELETED, data: post, }; } // postRemoved should be dispatched when a post should be immediately removed. This typically happens when a post is // deleted by the current user. function postRemoved(post) { return { type: action_types_1.PostTypes.POST_REMOVED, data: post, }; } function postPinnedChanged(postId, isPinned, updateAt = Date.now()) { return { type: action_types_1.PostTypes.POST_PINNED_CHANGED, postId, isPinned, updateAt, }; } function getPost(postId, includeDeleted, retainContent) { return async (dispatch, getState) => { let post; const crtEnabled = (0, preferences_2.isCollapsedThreadsEnabled)(getState()); try { post = await client_1.Client4.getPost(postId, includeDeleted, retainContent); } catch (error) { (0, helpers_1.forceLogoutIfNecessary)(error, dispatch, getState); dispatch({ type: action_types_1.PostTypes.GET_POSTS_FAILURE, error }); dispatch((0, errors_1.logError)(error)); return { error }; } dispatch(receivedPost(post, crtEnabled)); return { data: post }; }; } function createPost(post, files = [], afterSubmit) { return async (dispatch, getState) => { const state = getState(); const currentUserId = state.entities.users.currentUserId; const timestamp = Date.now(); const pendingPostId = post.pending_post_id || `${currentUserId}:${timestamp}`; let actions = []; if (PostSelectors.isPostIdSending(state, pendingPostId)) { return { data: { pending: pendingPostId } }; } let newPost = { ...post, pending_post_id: pendingPostId, create_at: timestamp, update_at: timestamp, reply_count: 0, }; if (post.root_id) { newPost.reply_count = PostSelectors.getPostRepliesCount(state, post.root_id) + 1; } // We are retrying a pending post that had files if (newPost.file_ids && !files.length) { // eslint-disable-next-line no-param-reassign files = newPost.file_ids.map((id) => state.entities.files.files[id]); } if (files.length) { const fileIds = files.map((file) => file.id); newPost = { ...newPost, file_ids: fileIds, }; actions.push({ type: action_types_1.FileTypes.RECEIVED_FILES_FOR_POST, postId: pendingPostId, data: files, }, { type: action_types_1.ChannelTypes.INCREMENT_FILE_COUNT, amount: files.length, id: newPost.channel_id, }); } const crtEnabled = (0, preferences_2.isCollapsedThreadsEnabled)(getState()); actions.push({ type: action_types_1.PostTypes.RECEIVED_NEW_POST, data: { ...newPost, id: pendingPostId, }, features: { crtEnabled }, }); dispatch((0, redux_batched_actions_1.batchActions)(actions, 'BATCH_CREATE_POST_INIT')); (async function createPostWrapper() { try { const created = await client_1.Client4.createPost({ ...newPost, create_at: 0 }); actions = [ receivedPost(created, crtEnabled), { type: action_types_1.PostTypes.CREATE_POST_SUCCESS, }, { type: action_types_1.ChannelTypes.INCREMENT_TOTAL_MSG_COUNT, data: { channelId: newPost.channel_id, amount: 1, amountRoot: created.root_id === '' ? 1 : 0, }, }, { type: action_types_1.ChannelTypes.DECREMENT_UNREAD_MSG_COUNT, data: { channelId: newPost.channel_id, amount: 1, amountRoot: created.root_id === '' ? 1 : 0, }, }, ]; if (files) { actions.push({ type: action_types_1.FileTypes.RECEIVED_FILES_FOR_POST, postId: created.id, data: files, }); } dispatch((0, redux_batched_actions_1.batchActions)(actions, 'BATCH_CREATE_POST')); afterSubmit?.({ created }); return { data: { created } }; } catch (error) { const data = { ...newPost, id: pendingPostId, failed: true, update_at: Date.now(), }; actions = [{ type: action_types_1.PostTypes.CREATE_POST_FAILURE, error }]; // If the failure was because: the root post was deleted or // TownSquareIsReadOnly=true then remove the post if (error.server_error_id === 'api.post.create_post.root_id.app_error' || error.server_error_id === 'api.post.create_post.town_square_read_only' || error.server_error_id === 'plugin.message_will_be_posted.dismiss_post') { // RemovePost is a Thunk, and not handled by batchActions dispatch(removePost(data)); } else { actions.push(receivedPost(data, crtEnabled)); } dispatch((0, redux_batched_actions_1.batchActions)(actions, 'BATCH_CREATE_POST_FAILED')); return { error }; } }()); return { data: { created: true } }; }; } function resetCreatePostRequest() { return { type: action_types_1.PostTypes.CREATE_POST_RESET_REQUEST }; } function deletePost(post) { return async (dispatch, getState) => { const state = getState(); const delPost = { ...post }; if (!post.root_id && (0, preferences_2.isCollapsedThreadsEnabled)(state)) { dispatch((0, threads_1.decrementThreadCounts)(post)); } if (delPost.type === constants_1.Posts.POST_TYPES.COMBINED_USER_ACTIVITY && delPost.system_post_ids) { delPost.system_post_ids.forEach((systemPostId) => { const systemPost = PostSelectors.getPost(state, systemPostId); if (systemPost) { dispatch(deletePost(systemPost)); } }); } else { (async function deletePostWrapper() { try { dispatch({ type: action_types_1.PostTypes.POST_DELETED, data: delPost, }); await client_1.Client4.deletePost(post.id); } catch (e) { // Recovering from this state doesn't actually work. The deleteAndRemovePost action // in the webapp needs to get an error in order to not call removePost, but then // the delete modal needs to handle this to show something to the user. Since none // of that ever worked (even with redux-offline in play), leave the behaviour here // unresolved. console.error('failed to delete post', e); // eslint-disable-line no-console } }()); } return { data: true }; }; } function editPost(post) { return (0, helpers_1.bindClientFunc)({ clientFunc: client_1.Client4.patchPost, onRequest: action_types_1.PostTypes.EDIT_POST_REQUEST, onSuccess: [action_types_1.PostTypes.RECEIVED_POST, action_types_1.PostTypes.EDIT_POST_SUCCESS], onFailure: action_types_1.PostTypes.EDIT_POST_FAILURE, params: [ post, ], }); } function getUnreadPostData(unreadChan, state) { const member = (0, channels_2.getMyChannelMember)(state, unreadChan.channel_id); const delta = member ? member.msg_count - unreadChan.msg_count : unreadChan.msg_count; const deltaRoot = member ? member.msg_count_root - unreadChan.msg_count_root : unreadChan.msg_count_root; const data = { teamId: unreadChan.team_id, channelId: unreadChan.channel_id, msgCount: unreadChan.msg_count, mentionCount: unreadChan.mention_count, msgCountRoot: unreadChan.msg_count_root, mentionCountRoot: unreadChan.mention_count_root, urgentMentionCount: unreadChan.urgent_mention_count, lastViewedAt: unreadChan.last_viewed_at, deltaMsgs: delta, deltaMsgsRoot: deltaRoot, }; return data; } function setUnreadPost(userId, postId) { return async (dispatch, getState) => { let state = getState(); const post = PostSelectors.getPost(state, postId); let unreadChan; try { if ((0, post_list_1.isCombinedUserActivityPost)(postId)) { return {}; } dispatch({ type: action_types_1.ChannelTypes.ADD_MANUALLY_UNREAD, data: { channelId: post.channel_id, }, }); unreadChan = await client_1.Client4.markPostAsUnread(userId, postId); } catch (error) { (0, helpers_1.forceLogoutIfNecessary)(error, dispatch, getState); dispatch((0, errors_1.logError)(error)); dispatch({ type: action_types_1.ChannelTypes.REMOVE_MANUALLY_UNREAD, data: { channelId: post.channel_id, }, }); return { error }; } state = getState(); const data = getUnreadPostData(unreadChan, state); dispatch({ type: action_types_1.ChannelTypes.POST_UNREAD_SUCCESS, data, }); return { data }; }; } function pinPost(postId) { return async (dispatch, getState) => { dispatch({ type: action_types_1.PostTypes.EDIT_POST_REQUEST }); let posts; try { posts = await client_1.Client4.pinPost(postId); } catch (error) { (0, helpers_1.forceLogoutIfNecessary)(error, dispatch, getState); dispatch({ type: action_types_1.PostTypes.EDIT_POST_FAILURE, error }); dispatch((0, errors_1.logError)(error)); return { error }; } const actions = [ { type: action_types_1.PostTypes.EDIT_POST_SUCCESS, }, ]; const state = getState(); const post = PostSelectors.getPost(state, postId); if (post) { actions.push(postPinnedChanged(postId, true, Date.now()), { type: action_types_1.ChannelTypes.INCREMENT_PINNED_POST_COUNT, id: post.channel_id, }); } dispatch((0, redux_batched_actions_1.batchActions)(actions)); return { data: posts }; }; } /** * Decrements the pinned post count for a channel by 1 */ function decrementPinnedPostCount(channelId) { return { type: action_types_1.ChannelTypes.DECREMENT_PINNED_POST_COUNT, id: channelId, }; } function unpinPost(postId) { return async (dispatch, getState) => { dispatch({ type: action_types_1.PostTypes.EDIT_POST_REQUEST }); let posts; try { posts = await client_1.Client4.unpinPost(postId); } catch (error) { (0, helpers_1.forceLogoutIfNecessary)(error, dispatch, getState); dispatch({ type: action_types_1.PostTypes.EDIT_POST_FAILURE, error }); dispatch((0, errors_1.logError)(error)); return { error }; } const actions = [ { type: action_types_1.PostTypes.EDIT_POST_SUCCESS, }, ]; const state = getState(); const post = PostSelectors.getPost(state, postId); if (post) { actions.push(postPinnedChanged(postId, false, Date.now()), decrementPinnedPostCount(post.channel_id)); } dispatch((0, redux_batched_actions_1.batchActions)(actions)); return { data: posts }; }; } function addReaction(postId, emojiName) { return async (dispatch, getState) => { const currentUserId = getState().entities.users.currentUserId; let reaction; try { reaction = await client_1.Client4.addReaction(currentUserId, postId, emojiName); } catch (error) { (0, helpers_1.forceLogoutIfNecessary)(error, dispatch, getState); dispatch((0, errors_1.logError)(error)); return { error }; } dispatch({ type: action_types_1.PostTypes.RECEIVED_REACTION, data: reaction, }); return { data: { reaction } }; }; } function removeReaction(postId, emojiName) { return async (dispatch, getState) => { const currentUserId = getState().entities.users.currentUserId; try { await client_1.Client4.removeReaction(currentUserId, postId, emojiName); } catch (error) { (0, helpers_1.forceLogoutIfNecessary)(error, dispatch, getState); dispatch((0, errors_1.logError)(error)); return { error }; } dispatch({ type: action_types_1.PostTypes.REACTION_DELETED, data: { user_id: currentUserId, post_id: postId, emoji_name: emojiName }, }); return { data: { removedReaction: true } }; }; } function getCustomEmojiForReaction(name) { return async (dispatch, getState) => { const nonExistentEmoji = getState().entities.emojis.nonExistentEmoji; const customEmojisByName = (0, emojis_2.getCustomEmojisByName)(getState()); if (emojis_1.systemEmojis.has(name)) { return { data: true }; } if (nonExistentEmoji.has(name)) { return { data: true }; } if (customEmojisByName.has(name)) { return { data: true }; } return dispatch((0, emojis_1.getCustomEmojiByName)(name)); }; } function flagPost(postId) { return async (dispatch, getState) => { const { currentUserId } = getState().entities.users; const preference = { user_id: currentUserId, category: constants_1.Preferences.CATEGORY_FLAGGED_POST, name: postId, value: 'true', }; return dispatch((0, preferences_1.savePreferences)(currentUserId, [preference])); }; } async function getPaginatedPostThread(rootId, options, prevList) { // since there are no complicated things inside (functions, Maps, Sets, etc.) we // can use the JSON approach to deep-copy the object const list = prevList ? JSON.parse(JSON.stringify(prevList)) : { order: [rootId], posts: {}, prev_post_id: '', next_post_id: '', first_inaccessible_post_time: 0, }; const result = await client_1.Client4.getPaginatedPostThread(rootId, options); if (result.first_inaccessible_post_time) { list.first_inaccessible_post_time = list.first_inaccessible_post_time ? Math.min(result.first_inaccessible_post_time, list.first_inaccessible_post_time) : result.first_inaccessible_post_time; } list.order.push(...result.order.slice(1)); list.posts = Object.assign(list.posts, result.posts); if (result.has_next) { const [nextPostId] = list.order.slice(-1); const nextPostPointer = list.posts[nextPostId]; let newOptions; if (options.updatesOnly) { newOptions = { ...options, fromUpdateAt: nextPostPointer.update_at, fromPost: nextPostId, }; } else { newOptions = { ...options, fromCreateAt: nextPostPointer.create_at, fromPost: nextPostId, }; } return getPaginatedPostThread(rootId, newOptions, list); } return list; } function getPostThread(rootId, fetchThreads = true, lastUpdateAt = 0) { return async (dispatch, getState) => { const state = getState(); const collapsedThreadsEnabled = (0, preferences_2.isCollapsedThreadsEnabled)(state); const enabledUserStatuses = (0, common_1.getIsUserStatusesConfigEnabled)(state); let posts; const options = { fetchThreads, collapsedThreads: collapsedThreadsEnabled, }; if (lastUpdateAt !== 0) { options.updatesOnly = true; options.fromUpdateAt = lastUpdateAt; } try { posts = await getPaginatedPostThread(rootId, options); } catch (error) { (0, helpers_1.forceLogoutIfNecessary)(error, dispatch, getState); dispatch((0, errors_1.logError)(error)); return { error }; } dispatch((0, redux_batched_actions_1.batchActions)([ receivedPosts(posts), receivedPostsInThread(posts, rootId), ])); if (enabledUserStatuses) { dispatch((0, status_profile_polling_1.batchFetchStatusesProfilesGroupsFromPosts)(posts.posts)); } return { data: posts }; }; } function getNewestPostThread(rootId) { const getPostsForThread = PostSelectors.makeGetPostsForThread(); return async (dispatch, getState) => { dispatch({ type: action_types_1.PostTypes.GET_POST_THREAD_REQUEST }); const collapsedThreadsEnabled = (0, preferences_2.isCollapsedThreadsEnabled)(getState()); const savedPosts = getPostsForThread(getState(), rootId); const latestReply = savedPosts?.[0]; const options = { fetchThreads: true, collapsedThreads: collapsedThreadsEnabled, direction: 'down', fromCreateAt: latestReply?.create_at, fromPost: latestReply?.id, }; let posts; try { posts = await getPaginatedPostThread(rootId, options); } catch (error) { (0, helpers_1.forceLogoutIfNecessary)(error, dispatch, getState); dispatch({ type: action_types_1.PostTypes.GET_POST_THREAD_FAILURE, error }); dispatch((0, errors_1.logError)(error)); return { error }; } dispatch((0, redux_batched_actions_1.batchActions)([ receivedPosts(posts), receivedPostsInThread(posts, rootId), ])); dispatch((0, status_profile_polling_1.batchFetchStatusesProfilesGroupsFromPosts)(posts.posts)); return { data: posts }; }; } function getPosts(channelId, page = 0, perPage = constants_1.Posts.POST_CHUNK_SIZE, fetchThreads = true, collapsedThreadsExtended = false) { return async (dispatch, getState) => { let posts; const collapsedThreadsEnabled = (0, preferences_2.isCollapsedThreadsEnabled)(getState()); try { posts = await client_1.Client4.getPosts(channelId, page, perPage, fetchThreads, collapsedThreadsEnabled, collapsedThreadsExtended); } catch (error) { (0, helpers_1.forceLogoutIfNecessary)(error, dispatch, getState); dispatch((0, errors_1.logError)(error)); return { error }; } dispatch((0, redux_batched_actions_1.batchActions)([ receivedPosts(posts), receivedPostsInChannel(posts, channelId, page === 0, posts.prev_post_id === ''), ])); dispatch((0, status_profile_polling_1.batchFetchStatusesProfilesGroupsFromPosts)(posts.posts)); return { data: posts }; }; } function getPostsUnread(channelId, fetchThreads = true, collapsedThreadsExtended = false) { return async (dispatch, getState) => { const state = getState(); const shouldLoadRecent = (0, preferences_2.getUnreadScrollPositionPreference)(state) === constants_1.Preferences.UNREAD_SCROLL_POSITION_START_FROM_NEWEST; const collapsedThreadsEnabled = (0, preferences_2.isCollapsedThreadsEnabled)(state); const userId = (0, users_2.getCurrentUserId)(state); let posts; let recentPosts; try { posts = await client_1.Client4.getPostsUnread(channelId, userId, client_1.DEFAULT_LIMIT_BEFORE, client_1.DEFAULT_LIMIT_AFTER, fetchThreads, collapsedThreadsEnabled, collapsedThreadsExtended); if (posts.next_post_id && shouldLoadRecent) { recentPosts = await client_1.Client4.getPosts(channelId, 0, constants_1.Posts.POST_CHUNK_SIZE / 2, fetchThreads, collapsedThreadsEnabled, collapsedThreadsExtended); } } catch (error) { (0, helpers_1.forceLogoutIfNecessary)(error, dispatch, getState); dispatch((0, errors_1.logError)(error)); return { error }; } const actions = [ { type: action_types_1.PostTypes.RECEIVED_POSTS, data: posts, channelId, }, receivedPostsInChannel(posts, channelId, posts.next_post_id === '', posts.prev_post_id === ''), ]; if (recentPosts) { actions.push(receivedPosts(recentPosts), receivedPostsInChannel(recentPosts, channelId, recentPosts.next_post_id === '', recentPosts.prev_post_id === '')); } dispatch((0, redux_batched_actions_1.batchActions)(actions)); dispatch((0, status_profile_polling_1.batchFetchStatusesProfilesGroupsFromPosts)(posts.posts)); return { data: posts }; }; } function getPostsSince(channelId, since, fetchThreads = true, collapsedThreadsExtended = false) { return async (dispatch, getState) => { let posts; try { const collapsedThreadsEnabled = (0, preferences_2.isCollapsedThreadsEnabled)(getState()); posts = await client_1.Client4.getPostsSince(channelId, since, fetchThreads, collapsedThreadsEnabled, collapsedThreadsExtended); } catch (error) { (0, helpers_1.forceLogoutIfNecessary)(error, dispatch, getState); dispatch((0, errors_1.logError)(error)); return { error }; } dispatch((0, redux_batched_actions_1.batchActions)([ receivedPosts(posts), receivedPostsSince(posts, channelId), ])); dispatch((0, status_profile_polling_1.batchFetchStatusesProfilesGroupsFromPosts)(posts.posts)); return { data: posts }; }; } function getPostsBefore(channelId, postId, page = 0, perPage = constants_1.Posts.POST_CHUNK_SIZE, fetchThreads = true, collapsedThreadsExtended = false) { return async (dispatch, getState) => { let posts; try { const collapsedThreadsEnabled = (0, preferences_2.isCollapsedThreadsEnabled)(getState()); posts = await client_1.Client4.getPostsBefore(channelId, postId, page, perPage, fetchThreads, collapsedThreadsEnabled, collapsedThreadsExtended); } catch (error) { (0, helpers_1.forceLogoutIfNecessary)(error, dispatch, getState); dispatch((0, errors_1.logError)(error)); return { error }; } dispatch((0, redux_batched_actions_1.batchActions)([ receivedPosts(posts), receivedPostsBefore(posts, channelId, postId, posts.prev_post_id === ''), ])); dispatch((0, status_profile_polling_1.batchFetchStatusesProfilesGroupsFromPosts)(posts.posts)); return { data: posts }; }; } function getPostsAfter(channelId, postId, page = 0, perPage = constants_1.Posts.POST_CHUNK_SIZE, fetchThreads = true, collapsedThreadsExtended = false) { return async (dispatch, getState) => { let posts; try { const collapsedThreadsEnabled = (0, preferences_2.isCollapsedThreadsEnabled)(getState()); posts = await client_1.Client4.getPostsAfter(channelId, postId, page, perPage, fetchThreads, collapsedThreadsEnabled, collapsedThreadsExtended); } catch (error) { (0, helpers_1.forceLogoutIfNecessary)(error, dispatch, getState); dispatch((0, errors_1.logError)(error)); return { error }; } dispatch((0, redux_batched_actions_1.batchActions)([ receivedPosts(posts), receivedPostsAfter(posts, channelId, postId, posts.next_post_id === ''), ])); dispatch((0, status_profile_polling_1.batchFetchStatusesProfilesGroupsFromPosts)(posts.posts)); return { data: posts }; }; } function getPostsAround(channelId, postId, perPage = constants_1.Posts.POST_CHUNK_SIZE / 2, fetchThreads = true, collapsedThreadsExtended = false) { return async (dispatch, getState) => { let after; let thread; let before; try { const collapsedThreadsEnabled = (0, preferences_2.isCollapsedThreadsEnabled)(getState()); [after, thread, before] = await Promise.all([ client_1.Client4.getPostsAfter(channelId, postId, 0, perPage, fetchThreads, collapsedThreadsEnabled, collapsedThreadsExtended), client_1.Client4.getPostThread(postId, fetchThreads, collapsedThreadsEnabled, collapsedThreadsExtended), client_1.Client4.getPostsBefore(channelId, postId, 0, perPage, fetchThreads, collapsedThreadsEnabled, collapsedThreadsExtended), ]); } catch (error) { (0, helpers_1.forceLogoutIfNecessary)(error, dispatch, getState); dispatch((0, errors_1.logError)(error)); return { error }; } // Dispatch a combined post list so that the order is correct for postsInChannel const posts = { posts: { ...after.posts, ...thread.posts, ...before.posts, }, order: [ ...after.order, postId, ...before.order, ], next_post_id: after.next_post_id, prev_post_id: before.prev_post_id, first_inaccessible_post_time: Math.max(before.first_inaccessible_post_time, after.first_inaccessible_post_time, thread.first_inaccessible_post_time) || 0, }; dispatch((0, redux_batched_actions_1.batchActions)([ receivedPosts(posts), receivedPostsInChannel(posts, channelId, after.next_post_id === '', before.prev_post_id === ''), ])); dispatch((0, status_profile_polling_1.batchFetchStatusesProfilesGroupsFromPosts)(posts.posts)); return { data: posts }; }; } /** * getPostThreads is intended for an array of posts that have been batched * (see the actions/websocket_actions/handleNewPostEvents function in the webapp) * */ function getPostThreads(posts, fetchThreads = true) { return (dispatch, getState) => { if (!Array.isArray(posts) || !posts.length) { return { data: true }; } const state = getState(); const currentChannelId = (0, channels_2.getCurrentChannelId)(state); const getPostThreadPromises = []; const rootPostIds = new Set(); for (const post of posts) { if (!post.root_id) { continue; } const rootPost = PostSelectors.getPost(state, post.root_id); if (rootPost) { continue; } if (rootPostIds.has(post.root_id)) { continue; } // At this point, we know that this post is a thread/reply and its root post is not in the store rootPostIds.add(post.root_id); if (post.channel_id === currentChannelId) { getPostThreadPromises.push(dispatch(getPostThread(post.root_id, fetchThreads))); } } return Promise.all(getPostThreadPromises); }; } // Note that getMentionsAndStatusesForPosts can take either an array of posts or a map of ids to posts async function getMentionsAndStatusesForPosts(postsArrayOrMap, dispatch, getState) { if (!postsArrayOrMap) { // Some API methods return {error} for no results return Promise.resolve(); } const postsArray = Array.isArray(postsArrayOrMap) ? postsArrayOrMap : Object.values(postsArrayOrMap); if (postsArray.length === 0) { return Promise.resolve(); } const postsDictionary = {}; for (let i = 0; i < postsArray.length; i++) { postsDictionary[postsArray[i].id] = postsArray[i]; } const state = getState(); const { currentUserId, profiles, statuses } = state.entities.users; const enabledUserStatuses = (0, common_1.getIsUserStatusesConfigEnabled)(state); // Statuses and profiles of the users who made the posts const userIdsToLoad = new Set(); const statusesToLoad = new Set(); postsArray.forEach((post) => { const userId = post.user_id; if (post.metadata) { if (post.metadata.embeds) { post.metadata.embeds.forEach((embed) => { if (embed.type === 'permalink' && embed.data) { if (embed.data.post?.user_id && !profiles[embed.data.post.user_id] && embed.data.post.user_id !== currentUserId) { userIdsToLoad.add(embed.data.post.user_id); } if (embed.data.post?.user_id && !statuses[embed.data.post.user_id]) { statusesToLoad.add(embed.data.post.user_id); } } }); } if (post.metadata.acknowledgements) { post.metadata.acknowledgements.forEach((ack) => { if (ack.acknowledged_at > 0) { userIdsToLoad.add(ack.user_id); } }); } } if (!statuses[userId]) { statusesToLoad.add(userId); } if (userId === currentUserId) { return; } if (!profiles[userId]) { userIdsToLoad.add(userId); } }); const promises = []; if (userIdsToLoad.size > 0) { promises.push(dispatch((0, users_1.getProfilesByIds)(Array.from(userIdsToLoad)))); } if (statusesToLoad.size > 0 && enabledUserStatuses) { promises.push(dispatch((0, users_1.getStatusesByIds)(Array.from(statusesToLoad)))); } // Profiles of users mentioned in the posts const usernamesAndGroupsToLoad = getNeededAtMentionedUsernamesAndGroups(state, postsArray); if (usernamesAndGroupsToLoad.size > 0) { // We need to load the profiles synchronously to filter them // out of the groups to check const getProfilesPromise = dispatch((0, users_1.getProfilesByUsernames)(Array.from(usernamesAndGroupsToLoad))); promises.push(getProfilesPromise); const { data } = await getProfilesPromise; const loadedProfiles = new Set((data || []).map((p) => p.username)); const groupsToCheck = Array.from(usernamesAndGroupsToLoad).filter((name) => !loadedProfiles.has(name)); if (groupsToCheck.length > 0) { const getGroupsPromise = dispatch((0, groups_1.getGroupsByNames)(groupsToCheck)); promises.push(getGroupsPromise); } } return Promise.all(promises); } function getPostsByIds(ids) { return async (dispatch, getState) => { let posts; try { posts = await client_1.Client4.getPostsByIds(ids); } catch (error) { (0, helpers_1.forceLogoutIfNecessary)(error, dispatch, getState); dispatch((0, errors_1.logError)(error)); return { error }; } dispatch({ type: action_types_1.PostTypes.RECEIVED_POSTS, data: { posts }, }); return { data: { posts } }; }; } function getPostsByIdsBatched(postIds) { const maxBatchSize = 100; const wait = 100; return async (dispatch, getState, { loaders }) => { if (!loaders.postsByIdsLoader) { loaders.postsByIdsLoader = new data_loader_1.DelayedDataLoader({ fetchBatch: (postIds) => dispatch(getPostsByIds(postIds)), maxBatchSize, wait, }); } loaders.postsByIdsLoader.queue(postIds); return { data: true }; }; } function getPostEditHistory(postId) { return (0, helpers_1.bindClientFunc)({ clientFunc: client_1.Client4.getPostEditHistory, onSuccess: action_types_1.PostTypes.RECEIVED_POST_HISTORY, params: [postId], }); } function getNeededAtMentionedUsernamesAndGroups(state, posts) { let usersByUsername; // Populate this lazily since it's relatively expensive let groupsByName; const usernamesAndGroupsToLoad = new Set(); function findNeededUsernamesAndGroups(text) { if (!text || !text.includes('@')) { return; } if (!usersByUsername) { usersByUsername = (0, users_2.getUsersByUsername)(state); } if (!groupsByName) { groupsByName = (0, groups_2.getAllGroupsByName)(state); } const pattern = /\B@(([a-z0-9.\-_:]*[a-z0-9_])[.\-:]*)/gi; let match; while ((match = pattern.exec(text)) !== null) { // match[1] is the matched mention including trailing punctuation // match[2] is the matched mention without trailing punctuation if (constants_1.General.SPECIAL_MENTIONS.indexOf(match[2]) !== -1) { continue; } if (usersByUsername[match[1]] || usersByUsername[match[2]]) { // We have the user, go to the next match continue; } if (groupsByName[match[1]] || groupsByName[match[2]]) { // We have the group, go to the next match continue; } // If there's no trailing punctuation, this will only add 1 item to the set usernamesAndGroupsToLoad.add(match[1]); usernamesAndGroupsToLoad.add(match[2]); } } for (const post of posts) { // These correspond to the fields searched by getMentionsEnabledFields on the server findNeededUsernamesAndGroups(post.message); if ((0, message_attachments_1.isMessageAttachmentArray)(post.props?.attachments)) { for (const attachment of post.props.attachments) { findNeededUsernamesAndGroups(attachment.pretext); findNeededUsernamesAndGroups(attachment.text); if (attachment.fields) { for (const field of attachment.fields) { if (typeof field.value === 'string') { findNeededUsernamesAndGroups(field.value); } } } } } } return usernamesAndGroupsToLoad; } function removePost(post) { return (dispatch, getState) => { if (post.type === constants_1.Posts.POST_TYPES.COMBINED_USER_ACTIVITY && post.system_post_ids) { const state = getState(); for (const systemPostId of post.system_post_ids) { const systemPost = PostSelectors.getPost(state, systemPostId); if (systemPost) { dispatch(removePost(systemPost)); } } } else { dispatch(postRemoved(post)); if (post.is_pinned) { dispatch(decrementPinnedPostCount(post.channel_id)); } } return { data: true }; }; } function moveThread(postId, channelId) { return async (dispatch, getState) => { try { await client_1.Client4.moveThread(postId, channelId); } catch (error) { (0, helpers_1.forceLogoutIfNecessary)(error, dispatch, getState); dispatch({ type: action_types_1.PostTypes.MOVE_POST_FAILURE, error }); dispatch((0, errors_1.logError)(error)); return { error }; } dispatch({ type: action_types_1.PostTypes.MOVE_POST_SUCCESS }); return { data: true }; }; } function selectFocusedPostId(postId) { return { type: action_types_1.PostTypes.RECEIVED_FOCUSED_POST, data: postId, }; } function unflagPost(postId) { return async (dispatch, getState) => { const { currentUserId } = getState().entities.users; const preference = { user_id: currentUserId, category: constants_1.Preferences.CATEGORY_FLAGGED_POST, name: postId, value: 'true', }; return dispatch((0, preferences_1.deletePreferences)(currentUserId, [preference])); }; } function addPostReminder(userId, postId, timestamp) { return async (dispatch, getState) => { try { await client_1.Client4.addPostReminder(userId, postId, timestamp); } catch (error) { (0, helpers_1.forceLogoutIfNecessary)(error, dispatch, getState); dispatch((0, errors_1.logError)(error)); return { error }; } return { data: true }; }; } function doPostAction(postId, actionId, selectedOption = '') { return doPostActionWithCookie(postId, actionId, '', selectedOption); } function doPostActionWithCookie(postId, actionId, actionCookie, selectedOption = '') { return async (dispatch, getState) => { let data; try { data = await client_1.Client4.doPostActionWithCookie(postId, actionId, actionCookie, selectedOption); } catch (error) { (0, helpers_1.forceLogoutIfNecessary)(error, dispatch, getState); dispatch((0, errors_1.logError)(error)); return { error }; } if (data && data.trigger_id) { dispatch({ type: action_types_1.IntegrationTypes.RECEIVED_DIALOG_TRIGGER_ID, data: data.trigger_id, }); const state = getState(); const post = PostSelectors.getPost(state, postId); dispatch({ type: action_types_1.IntegrationTypes.RECEIVED_DIALOG_ARGUMENTS, data: { channel_id: post.channel_id, } }); } return { data }; }; } function addMessageIntoHistory(message) { return { type: action_types_1.PostTypes.ADD_MESSAGE_INTO_HISTORY, data: message, }; } function resetHistoryIndex(index) { return { type: action_types_1.PostTypes.RESET_HISTORY_INDEX, data: index, }; } function moveHistoryIndexBack(index) { return async (dispatch) => { dispatch({ type: action_types_1.PostTypes.MOVE_HISTORY_INDEX_BACK, data: index, }); return { data: true }; }; } function moveHistoryIndexForward(index) { return async (dispatch) => { dispatch({ type: action_types_1.PostTypes.MOVE_HISTORY_INDEX_FORWARD, data: index, }); return { data: true }; }; } function resetReloadPostsInTranslatedChannels() { return async (dispatch, getState) => { const state = getState(); const channels = (0, channels_2.getAllChannels)(state); for (const channel of Object.values(channels)) { if (!channel.autotranslation) { continue; } const myMember = (0, channels_2.getMyChannelMember)(state, channel.id); if (myMember?.autotranslation_disabled) { continue; } dispatch(resetReloadPostsInChannel(channel.id)); } return { data: true }; }; } /** * Ensures thread-replies in channels correctly follow CRT:ON/OFF */ function resetReloadPostsInChannel(channelId) { return async (dispatch, getState) => { dispatch({ type: action_types_1.PostTypes.RESET_POSTS_IN_CHANNEL, channelId, }); const currentChannelId = (0, channels_2.getCurrentChannelId)(getState()); if (currentChannelId && (!channelId || channelId === currentChannelId)) { // wait for channel to be fully deselected; prevent stuck loading screen // full state-change/reconciliation will cause prefetchChannelPosts to reload posts await dispatch((0, channels_1.selectChannel)('')); // do not remove await dispatch((0, channels_1.selectChannel)(currentChannelId)); } return { data: true }; }; } function acknowledgePost(postId) { return async (dispatch, getState) => { const userId = (0, users_2.getCurrentUserId)(getState()); let data; try { data = await client_1.Client4.acknowledgePost(postId, userId); } catch (error) { (0, helpers_1.forceLogoutIfNecessary)(error, dispatch, getState); dispatch((0, errors_1.logError)(error)); return { error }; } dispatch({ type: action