mattermost-redux
Version:
Common code (API client, Redux stores, logic, utility functions) for building a Mattermost client
389 lines (388 loc) • 16 kB
JavaScript
"use strict";
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.fetchThreads = fetchThreads;
exports.getThreadsForCurrentTeam = getThreadsForCurrentTeam;
exports.getThreadCounts = getThreadCounts;
exports.getCountsAndThreadsSince = getCountsAndThreadsSince;
exports.handleThreadArrived = handleThreadArrived;
exports.getThread = getThread;
exports.handleAllMarkedRead = handleAllMarkedRead;
exports.markAllThreadsInTeamRead = markAllThreadsInTeamRead;
exports.markThreadAsUnread = markThreadAsUnread;
exports.markLastPostInThreadAsUnread = markLastPostInThreadAsUnread;
exports.updateThreadRead = updateThreadRead;
exports.handleReadChanged = handleReadChanged;
exports.handleFollowChanged = handleFollowChanged;
exports.setThreadFollow = setThreadFollow;
exports.handleAllThreadsInChannelMarkedRead = handleAllThreadsInChannelMarkedRead;
exports.decrementThreadCounts = decrementThreadCounts;
const uniq_1 = __importDefault(require("lodash/uniq"));
const redux_batched_actions_1 = require("redux-batched-actions");
const action_types_1 = require("mattermost-redux/action_types");
const files_1 = require("mattermost-redux/actions/files");
const users_1 = require("mattermost-redux/actions/users");
const client_1 = require("mattermost-redux/client");
const threads_1 = __importDefault(require("mattermost-redux/constants/threads"));
const channels_1 = require("mattermost-redux/selectors/entities/channels");
const posts_1 = require("mattermost-redux/selectors/entities/posts");
const preferences_1 = require("mattermost-redux/selectors/entities/preferences");
const teams_1 = require("mattermost-redux/selectors/entities/teams");
const threads_2 = require("mattermost-redux/selectors/entities/threads");
const users_2 = require("mattermost-redux/selectors/entities/users");
const errors_1 = require("./errors");
const helpers_1 = require("./helpers");
const posts_2 = require("./posts");
const teams_2 = require("./teams");
function fetchThreads(userId, teamId, { before = '', after = '', perPage = threads_1.default.THREADS_CHUNK_SIZE, unread = false, totalsOnly = false, threadsOnly = false, extended = false, since = 0 } = {}) {
return async (dispatch, getState) => {
let data;
try {
data = await client_1.Client4.getUserThreads(userId, teamId, { before, after, perPage, extended, unread, totalsOnly, threadsOnly, since });
}
catch (error) {
(0, helpers_1.forceLogoutIfNecessary)(error, dispatch, getState);
dispatch((0, errors_1.logError)(error));
return { error };
}
return { data };
};
}
function getThreadsForCurrentTeam({ before = '', after = '', unread = false } = {}) {
return async (dispatch, getState) => {
const userId = (0, users_2.getCurrentUserId)(getState());
const teamId = (0, teams_1.getCurrentTeamId)(getState());
const response = await dispatch(fetchThreads(userId, teamId, {
before,
after,
perPage: threads_1.default.THREADS_PAGE_SIZE,
unread,
totalsOnly: false,
threadsOnly: true,
extended: true,
}));
if (response.error) {
return response;
}
const userThreadList = response?.data;
if (!userThreadList) {
return { error: true };
}
if (userThreadList?.threads?.length) {
await dispatch((0, users_1.getMissingProfilesByIds)(userThreadList.threads.map(({ participants }) => participants.map(({ id }) => id)).flat()));
dispatch({
type: action_types_1.PostTypes.RECEIVED_POSTS,
data: { posts: userThreadList.threads.map(({ post }) => ({ ...post, update_at: 0 })) },
});
dispatch((0, files_1.getMissingFilesByPosts)((0, uniq_1.default)(userThreadList.threads.map(({ post }) => post))));
}
dispatch({
type: unread ? action_types_1.ThreadTypes.RECEIVED_UNREAD_THREADS : action_types_1.ThreadTypes.RECEIVED_THREADS,
data: {
threads: userThreadList?.threads?.map((thread) => ({ ...thread, is_following: true })) ?? [],
team_id: teamId,
},
});
return { data: userThreadList };
};
}
function getThreadCounts(userId, teamId) {
return async (dispatch) => {
const response = await dispatch(fetchThreads(userId, teamId, { totalsOnly: true, threadsOnly: false }));
if (response.error) {
return response;
}
const counts = response?.data;
if (!counts) {
return { error: true };
}
const data = {
total: counts.total,
total_unread_threads: counts.total_unread_threads,
total_unread_mentions: counts.total_unread_mentions,
total_unread_urgent_mentions: counts.total_unread_urgent_mentions,
};
dispatch({
type: action_types_1.ThreadTypes.RECEIVED_THREAD_COUNTS,
data: {
...data,
team_id: teamId,
},
});
return { data };
};
}
function getCountsAndThreadsSince(userId, teamId, since) {
return async (dispatch) => {
const response = await dispatch(fetchThreads(userId, teamId, { since, totalsOnly: false, threadsOnly: false, extended: true }));
if (response.error) {
return response;
}
const userThreadList = response?.data;
if (!userThreadList) {
return { error: true };
}
const actions = [];
if (userThreadList?.threads?.length) {
await dispatch((0, users_1.getMissingProfilesByIds)(userThreadList.threads.map(({ participants }) => participants.map(({ id }) => id)).flat()));
actions.push({
type: action_types_1.PostTypes.RECEIVED_POSTS,
data: { posts: userThreadList.threads.map(({ post }) => ({ ...post, update_at: 0 })) },
});
}
actions.push({
type: action_types_1.ThreadTypes.RECEIVED_THREADS,
data: {
threads: userThreadList?.threads?.map((thread) => ({ ...thread, is_following: true })) ?? [],
team_id: teamId,
},
});
const counts = {
total: userThreadList.total,
total_unread_threads: userThreadList.total_unread_threads,
total_unread_mentions: userThreadList.total_unread_mentions,
total_unread_urgent_mentions: userThreadList.total_unread_urgent_mentions,
};
actions.push({
type: action_types_1.ThreadTypes.RECEIVED_THREAD_COUNTS,
data: {
...counts,
team_id: teamId,
},
});
dispatch((0, redux_batched_actions_1.batchActions)(actions));
return { data: userThreadList };
};
}
function handleThreadArrived(dispatch, getState, threadData, teamId, previousUnreadReplies, previousUnreadMentions) {
const state = getState();
const currentUserId = (0, users_2.getCurrentUserId)(state);
const crtEnabled = (0, preferences_1.isCollapsedThreadsEnabled)(state);
const thread = { ...threadData, is_following: true };
dispatch({
type: action_types_1.UserTypes.RECEIVED_PROFILES_LIST,
data: thread.participants.filter((user) => user.id !== currentUserId),
});
dispatch({
type: action_types_1.PostTypes.RECEIVED_POST,
data: { ...thread.post, update_at: 0 },
features: { crtEnabled },
});
dispatch({
type: action_types_1.ThreadTypes.RECEIVED_THREAD,
data: {
thread,
team_id: teamId,
},
});
const oldThreadData = state.entities.threads.threads[threadData.id];
// update thread read if and only if we have previous unread values
// upon receiving a thread.
// we need that guard to ensure that fetching a thread won't skew the counts
//
// PS: websocket events should always provide the previous unread values
if ((previousUnreadMentions != null && previousUnreadReplies != null) ||
oldThreadData != null) {
dispatch(handleReadChanged(thread.id, teamId, thread.post.channel_id, {
lastViewedAt: thread.last_viewed_at,
prevUnreadMentions: oldThreadData?.unread_mentions ?? previousUnreadMentions,
newUnreadMentions: thread.unread_mentions,
prevUnreadReplies: oldThreadData?.unread_replies ?? previousUnreadReplies,
newUnreadReplies: thread.unread_replies,
}));
}
return thread;
}
function getThread(userId, teamId, threadId, extended = true) {
return async (dispatch, getState) => {
let thread;
try {
thread = await client_1.Client4.getUserThread(userId, teamId, threadId, extended);
}
catch (error) {
(0, helpers_1.forceLogoutIfNecessary)(error, dispatch, getState);
dispatch((0, errors_1.logError)(error));
return { error };
}
if (thread) {
thread = handleThreadArrived(dispatch, getState, thread, teamId);
}
return { data: thread };
};
}
function handleAllMarkedRead(teamId) {
return {
type: action_types_1.ThreadTypes.ALL_TEAM_THREADS_READ,
data: {
team_id: teamId,
},
};
}
function markAllThreadsInTeamRead(userId, teamId) {
return async (dispatch, getState) => {
try {
await client_1.Client4.updateThreadsReadForUser(userId, teamId);
}
catch (error) {
(0, helpers_1.forceLogoutIfNecessary)(error, dispatch, getState);
dispatch((0, errors_1.logError)(error));
return { error };
}
dispatch(handleAllMarkedRead(teamId));
return {};
};
}
function markThreadAsUnread(userId, teamId, threadId, postId) {
return async (dispatch, getState) => {
try {
await client_1.Client4.markThreadAsUnreadForUser(userId, teamId, threadId, postId);
}
catch (error) {
(0, helpers_1.forceLogoutIfNecessary)(error, dispatch, getState);
dispatch((0, errors_1.logError)(error));
return { error };
}
return {};
};
}
function markLastPostInThreadAsUnread(userId, teamId, threadId) {
return async (dispatch, getState) => {
const getPostsForThread = (0, posts_1.makeGetPostsForThread)();
let posts = getPostsForThread(getState(), threadId);
const state = getState();
const thread = (0, threads_2.getThread)(state, threadId);
// load posts in thread if they are not loaded already
if (thread?.reply_count === posts.length - 1) {
dispatch(markThreadAsUnread(userId, teamId, threadId, posts[0].id));
}
else {
dispatch((0, posts_2.getPostThread)(threadId)).then(({ data, error }) => {
if (data) {
posts = getPostsForThread(getState(), threadId);
dispatch(markThreadAsUnread(userId, teamId, threadId, posts[0].id));
}
else if (error) {
return { error };
}
return {};
});
}
return {};
};
}
function updateThreadRead(userId, teamId, threadId, timestamp) {
return async (dispatch, getState) => {
try {
await client_1.Client4.updateThreadReadForUser(userId, teamId, threadId, timestamp);
}
catch (error) {
(0, helpers_1.forceLogoutIfNecessary)(error, dispatch, getState);
dispatch((0, errors_1.logError)(error));
return { error };
}
return {};
};
}
function handleReadChanged(threadId, teamId, channelId, { lastViewedAt, prevUnreadMentions, newUnreadMentions, prevUnreadReplies, newUnreadReplies, }) {
return (dispatch, getState) => {
const state = getState();
const channel = (0, channels_1.getChannel)(state, channelId);
const thread = (0, threads_2.getThread)(state, threadId);
return dispatch({
type: action_types_1.ThreadTypes.READ_CHANGED_THREAD,
data: {
id: threadId,
teamId,
channelId,
lastViewedAt,
prevUnreadMentions,
newUnreadMentions,
prevUnreadReplies,
newUnreadReplies,
channelType: channel?.type,
isUrgent: thread?.is_urgent,
},
});
};
}
function handleFollowChanged(dispatch, threadId, teamId, following) {
dispatch({
type: action_types_1.ThreadTypes.FOLLOW_CHANGED_THREAD,
data: {
id: threadId,
team_id: teamId,
following,
},
});
}
function setThreadFollow(userId, teamId, threadId, newState) {
return async (dispatch, getState) => {
handleFollowChanged(dispatch, threadId, teamId, newState);
try {
await client_1.Client4.updateThreadFollowForUser(userId, teamId, threadId, newState);
}
catch (error) {
(0, helpers_1.forceLogoutIfNecessary)(error, dispatch, getState);
dispatch((0, errors_1.logError)(error));
return { error };
}
// As a short term fix for https://mattermost.atlassian.net/browse/MM-62113, we will fetch
// the users team unreads after following or unfollowing a thread. This will ensure that the unreads are
// updated correctly in the case where the user is following or unfollowing a thread that has unread messages.
dispatch((0, teams_2.getMyTeamUnreads)((0, preferences_1.isCollapsedThreadsEnabled)(getState())));
return {};
};
}
function handleAllThreadsInChannelMarkedRead(channelId, lastViewedAt) {
return (dispatch, getState) => {
const state = getState();
const channel = (0, channels_1.getChannel)(state, channelId);
if (channel == null) {
return { data: false };
}
const teamId = channel.team_id;
const threadsInChannel = (0, threads_2.getThreadsInChannel)(state, channelId);
const actions = [];
for (const thread of threadsInChannel) {
actions.push({
type: action_types_1.ThreadTypes.READ_CHANGED_THREAD,
data: {
id: thread.id,
channelId,
teamId,
lastViewedAt,
newUnreadMentions: 0,
newUnreadReplies: 0,
isUrgent: thread.is_urgent,
},
});
}
dispatch((0, redux_batched_actions_1.batchActions)(actions));
return { data: true };
};
}
function decrementThreadCounts(post) {
return (dispatch, getState) => {
const state = getState();
const thread = (0, threads_2.getThread)(state, post.id);
if (!thread || (!thread.unread_replies && !thread.unread_mentions)) {
return { data: false };
}
const channel = (0, channels_1.getChannel)(state, post.channel_id);
const teamId = channel?.team_id || (0, teams_1.getCurrentTeamId)(state);
if (channel) {
dispatch({
type: action_types_1.ThreadTypes.DECREMENT_THREAD_COUNTS,
teamId,
replies: thread.unread_replies,
mentions: thread.unread_mentions,
channelType: channel.type,
});
}
return { data: true };
};
}