UNPKG

mattermost-redux

Version:

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

389 lines (388 loc) 16 kB
"use strict"; // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.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 }; }; }