mattermost-redux
Version:
Common code (API client, Redux stores, logic, utility functions) for building a Mattermost client
1,210 lines • 51.7 kB
JavaScript
"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