UNPKG

mattermost-redux

Version:

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

1,485 lines (1,264 loc) 53.5 kB
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import {ChannelTypes, PreferenceTypes, UserTypes} from 'action_types'; import {Client4} from 'client'; import {General, Preferences} from '../constants'; import {CategoryTypes} from 'constants/channel_categories'; import {MarkUnread} from 'constants/channels'; import {getCategoryInTeamByType} from 'selectors/entities/channel_categories'; import { getChannel as getChannelSelector, getChannelsNameMapInTeam, getMyChannelMember as getMyChannelMemberSelector, getRedirectChannelNameForTeam, isManuallyUnread, } from 'selectors/entities/channels'; import {getConfig, getServerVersion} from 'selectors/entities/general'; import {getCurrentTeamId} from 'selectors/entities/teams'; import {getCurrentUserId} from 'selectors/entities/users'; import {Action, ActionFunc, batchActions, DispatchFunc, GetStateFunc} from 'types/actions'; import {Channel, ChannelNotifyProps, ChannelMembership, ChannelModerationPatch, ChannelsWithTotalCount, ChannelSearchOpts} from 'types/channels'; import {PreferenceType} from 'types/preferences'; import {getChannelsIdForTeam, getChannelByName} from 'utils/channel_utils'; import {isMinimumServerVersion} from 'utils/helpers'; import {addChannelToInitialCategory, addChannelToCategory} from './channel_categories'; import {logError} from './errors'; import {bindClientFunc, forceLogoutIfNecessary} from './helpers'; import {savePreferences, deletePreferences} from './preferences'; import {loadRolesIfNeeded} from './roles'; import {getMissingProfilesByIds} from './users'; export function selectChannel(channelId: string) { return { type: ChannelTypes.SELECT_CHANNEL, data: channelId, }; } export function createChannel(channel: Channel, userId: string): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { let created; try { created = await Client4.createChannel(channel); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(batchActions([ { type: ChannelTypes.CREATE_CHANNEL_FAILURE, error, }, logError(error), ])); return {error}; } const member = { channel_id: created.id, user_id: userId, roles: `${General.CHANNEL_USER_ROLE} ${General.CHANNEL_ADMIN_ROLE}`, last_viewed_at: 0, msg_count: 0, mention_count: 0, notify_props: {desktop: 'default', mark_unread: 'all'}, last_update_at: created.create_at, }; const actions: Action[] = []; const {channels, myMembers} = getState().entities.channels; if (!channels[created.id]) { actions.push({type: ChannelTypes.RECEIVED_CHANNEL, data: created}); } if (!myMembers[created.id]) { actions.push({type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER, data: member}); dispatch(loadRolesIfNeeded(member.roles.split(' '))); } dispatch(batchActions([ ...actions, { type: ChannelTypes.CREATE_CHANNEL_SUCCESS, }, ])); dispatch(addChannelToInitialCategory(created, true)); return {data: created}; }; } export function createDirectChannel(userId: string, otherUserId: string): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { dispatch({type: ChannelTypes.CREATE_CHANNEL_REQUEST, data: null}); let created; try { created = await Client4.createDirectChannel([userId, otherUserId]); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(batchActions([ {type: ChannelTypes.CREATE_CHANNEL_FAILURE, error}, logError(error), ])); return {error}; } const member = { channel_id: created.id, user_id: userId, roles: `${General.CHANNEL_USER_ROLE}`, last_viewed_at: 0, msg_count: 0, mention_count: 0, notify_props: {desktop: 'default', mark_unread: 'all'}, last_update_at: created.create_at, }; const preferences = [ {user_id: userId, category: Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, name: otherUserId, value: 'true'}, {user_id: userId, category: Preferences.CATEGORY_CHANNEL_OPEN_TIME, name: created.id, value: new Date().getTime().toString()}, ]; savePreferences(userId, preferences)(dispatch); dispatch(batchActions([ { type: ChannelTypes.RECEIVED_CHANNEL, data: created, }, { type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER, data: member, }, { type: PreferenceTypes.RECEIVED_PREFERENCES, data: preferences, }, { type: ChannelTypes.CREATE_CHANNEL_SUCCESS, }, { type: UserTypes.RECEIVED_PROFILES_LIST_IN_CHANNEL, id: created.id, data: [{id: userId}, {id: otherUserId}], }, ])); dispatch(addChannelToInitialCategory(created)); dispatch(loadRolesIfNeeded(member.roles.split(' '))); return {data: created}; }; } export function markGroupChannelOpen(channelId: string): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { const {currentUserId} = getState().entities.users; const preferences: PreferenceType[] = [ {user_id: currentUserId, category: Preferences.CATEGORY_GROUP_CHANNEL_SHOW, name: channelId, value: 'true'}, {user_id: currentUserId, category: Preferences.CATEGORY_CHANNEL_OPEN_TIME, name: channelId, value: new Date().getTime().toString()}, ]; return dispatch(savePreferences(currentUserId, preferences)); }; } export function createGroupChannel(userIds: string[]): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { dispatch({type: ChannelTypes.CREATE_CHANNEL_REQUEST, data: null}); const {currentUserId} = getState().entities.users; let created; try { created = await Client4.createGroupChannel(userIds); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(batchActions([ {type: ChannelTypes.CREATE_CHANNEL_FAILURE, error}, logError(error), ])); return {error}; } let member: Partial<ChannelMembership|undefined> = { channel_id: created.id, user_id: currentUserId, roles: `${General.CHANNEL_USER_ROLE}`, last_viewed_at: 0, msg_count: 0, mention_count: 0, notify_props: {desktop: 'default', mark_unread: 'all'}, last_update_at: created.create_at, }; // Check the channel previous existency: if the channel already have // posts is because it existed before. if (created.total_msg_count > 0) { const storeMember = getMyChannelMemberSelector(getState(), created.id); if (storeMember === null) { try { member = await Client4.getMyChannelMember(created.id); } catch (error) { // Log the error and keep going with the generated membership. dispatch(logError(error)); } } else { member = storeMember; } } dispatch(markGroupChannelOpen(created.id)); const profilesInChannel = userIds.map((id) => ({id})); profilesInChannel.push({id: currentUserId}); // currentUserId is optionally in userIds, but the reducer will get rid of a duplicate dispatch(batchActions([ { type: ChannelTypes.RECEIVED_CHANNEL, data: created, }, { type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER, data: member, }, { type: ChannelTypes.CREATE_CHANNEL_SUCCESS, }, { type: UserTypes.RECEIVED_PROFILES_LIST_IN_CHANNEL, id: created.id, data: profilesInChannel, }, ])); dispatch(addChannelToInitialCategory(created)); dispatch(loadRolesIfNeeded((member && member.roles && member.roles.split(' ')) || [])); return {data: created}; }; } export function patchChannel(channelId: string, patch: Channel): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { dispatch({type: ChannelTypes.UPDATE_CHANNEL_REQUEST, data: null}); let updated; try { updated = await Client4.patchChannel(channelId, patch); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(batchActions([ {type: ChannelTypes.UPDATE_CHANNEL_FAILURE, error}, logError(error), ])); return {error}; } dispatch(batchActions([ { type: ChannelTypes.RECEIVED_CHANNEL, data: updated, }, { type: ChannelTypes.UPDATE_CHANNEL_SUCCESS, }, ])); return {data: updated}; }; } export function updateChannel(channel: Channel): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { dispatch({type: ChannelTypes.UPDATE_CHANNEL_REQUEST, data: null}); let updated; try { updated = await Client4.updateChannel(channel); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(batchActions([ {type: ChannelTypes.UPDATE_CHANNEL_FAILURE, error}, logError(error), ])); return {error}; } dispatch(batchActions([ { type: ChannelTypes.RECEIVED_CHANNEL, data: updated, }, { type: ChannelTypes.UPDATE_CHANNEL_SUCCESS, }, ])); return {data: updated}; }; } export function updateChannelPrivacy(channelId: string, privacy: string): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { dispatch({type: ChannelTypes.UPDATE_CHANNEL_REQUEST, data: null}); let updatedChannel; try { updatedChannel = await Client4.updateChannelPrivacy(channelId, privacy); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(batchActions([ {type: ChannelTypes.UPDATE_CHANNEL_FAILURE, error}, logError(error), ])); return {error}; } dispatch(batchActions([ { type: ChannelTypes.RECEIVED_CHANNEL, data: updatedChannel, }, { type: ChannelTypes.UPDATE_CHANNEL_SUCCESS, }, ])); return {data: updatedChannel}; }; } export function convertChannelToPrivate(channelId: string): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { dispatch({type: ChannelTypes.UPDATE_CHANNEL_REQUEST, data: null}); let convertedChannel; try { convertedChannel = await Client4.convertChannelToPrivate(channelId); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(batchActions([ {type: ChannelTypes.UPDATE_CHANNEL_FAILURE, error}, logError(error), ])); return {error}; } dispatch(batchActions([ { type: ChannelTypes.RECEIVED_CHANNEL, data: convertedChannel, }, { type: ChannelTypes.UPDATE_CHANNEL_SUCCESS, }, ])); return {data: convertedChannel}; }; } export function updateChannelNotifyProps(userId: string, channelId: string, props: ChannelNotifyProps): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { const notifyProps = { user_id: userId, channel_id: channelId, ...props, }; try { await Client4.updateChannelNotifyProps(notifyProps); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(logError(error)); return {error}; } const member = getState().entities.channels.myMembers[channelId] || {}; const currentNotifyProps = member.notify_props || {}; dispatch({ type: ChannelTypes.RECEIVED_CHANNEL_PROPS, data: { channel_id: channelId, notifyProps: {...currentNotifyProps, ...notifyProps}, }, }); return {data: true}; }; } export function getChannelByNameAndTeamName(teamName: string, channelName: string, includeDeleted = false): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { let data; try { data = await Client4.getChannelByNameAndTeamName(teamName, channelName, includeDeleted); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(batchActions([ {type: ChannelTypes.CHANNELS_FAILURE, error}, logError(error), ])); return {error}; } dispatch({ type: ChannelTypes.RECEIVED_CHANNEL, data, }); return {data}; }; } export function getChannel(channelId: string): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { let data; try { data = await Client4.getChannel(channelId); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(batchActions([ {type: ChannelTypes.CHANNELS_FAILURE, error}, logError(error), ])); return {error}; } dispatch({ type: ChannelTypes.RECEIVED_CHANNEL, data, }); return {data}; }; } export function getChannelAndMyMember(channelId: string): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { let channel; let member; try { const channelRequest = Client4.getChannel(channelId); const memberRequest = Client4.getMyChannelMember(channelId); channel = await channelRequest; member = await memberRequest; } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(batchActions([ {type: ChannelTypes.CHANNELS_FAILURE, error}, logError(error), ])); return {error}; } dispatch(batchActions([ { type: ChannelTypes.RECEIVED_CHANNEL, data: channel, }, { type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER, data: member, }, ])); dispatch(loadRolesIfNeeded(member.roles.split(' '))); return {data: {channel, member}}; }; } export function getChannelTimezones(channelId: string): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { let channelTimezones; try { const channelTimezonesRequest = Client4.getChannelTimezones(channelId); channelTimezones = await channelTimezonesRequest; } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(logError(error)); return {error}; } return {data: channelTimezones}; }; } export function fetchMyChannelsAndMembers(teamId: string): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { dispatch({ type: ChannelTypes.CHANNELS_REQUEST, data: null, }); let channels; let channelMembers; const state = getState(); const shouldFetchArchived = isMinimumServerVersion(getServerVersion(state), 5, 21); try { const channelRequest = Client4.getMyChannels(teamId, shouldFetchArchived); const memberRequest = Client4.getMyChannelMembers(teamId); channels = await channelRequest; channelMembers = await memberRequest; } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(batchActions([ {type: ChannelTypes.CHANNELS_FAILURE, error}, logError(error), ])); return {error}; } const {currentUserId} = state.entities.users; const {currentChannelId} = state.entities.channels; dispatch(batchActions([ { type: ChannelTypes.RECEIVED_CHANNELS, teamId, data: channels, currentChannelId, }, { type: ChannelTypes.CHANNELS_SUCCESS, }, { type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBERS, data: channelMembers, sync: !shouldFetchArchived, channels, remove: getChannelsIdForTeam(state, teamId), currentUserId, currentChannelId, }, ])); const roles = new Set<string>(); for (const member of channelMembers) { for (const role of member.roles.split(' ')) { roles.add(role); } } if (roles.size > 0) { dispatch(loadRolesIfNeeded(roles)); } return {data: {channels, members: channelMembers}}; }; } export function getMyChannelMembers(teamId: string): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { let channelMembers; try { const channelMembersRequest = Client4.getMyChannelMembers(teamId); channelMembers = await channelMembersRequest; } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(logError(error)); return {error}; } const state = getState(); const {currentUserId} = state.entities.users; const {currentChannelId} = state.entities.channels; dispatch({ type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBERS, data: channelMembers, remove: getChannelsIdForTeam(getState(), teamId), currentUserId, currentChannelId, }); const roles = new Set<string>(); for (const member of channelMembers) { for (const role of member.roles.split(' ')) { roles.add(role); } } if (roles.size > 0) { dispatch(loadRolesIfNeeded(roles)); } return {data: channelMembers}; }; } export function getChannelMembers(channelId: string, page = 0, perPage: number = General.CHANNELS_CHUNK_SIZE): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { let channelMembers: ChannelMembership[]; try { const channelMembersRequest = Client4.getChannelMembers(channelId, page, perPage); channelMembers = await channelMembersRequest; } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(logError(error)); return {error}; } const userIds = channelMembers.map((cm) => cm.user_id); getMissingProfilesByIds(userIds)(dispatch, getState); dispatch({ type: ChannelTypes.RECEIVED_CHANNEL_MEMBERS, data: channelMembers, }); return {data: channelMembers}; }; } export function leaveChannel(channelId: string): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { const state = getState(); const {currentUserId} = state.entities.users; const {channels, myMembers} = state.entities.channels; const channel = channels[channelId]; const member = myMembers[channelId]; Client4.trackEvent('action', 'action_channels_leave', {channel_id: channelId}); dispatch({ type: ChannelTypes.LEAVE_CHANNEL, data: { id: channelId, user_id: currentUserId, team_id: channel.team_id, type: channel.type, }, meta: { offline: { effect: () => Client4.removeFromChannel(currentUserId, channelId), commit: {type: 'do_nothing'}, // redux-offline always needs to dispatch something on commit rollback: () => { dispatch(batchActions([ { type: ChannelTypes.RECEIVED_CHANNEL, data: channel, }, { type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER, data: member, }, ])); }, }, }, }); return {data: true}; }; } export function joinChannel(userId: string, teamId: string, channelId: string, channelName: string): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { if (!channelId && !channelName) { return {data: null}; } let member: ChannelMembership | undefined | null; let channel: Channel; try { if (channelId) { member = await Client4.addToChannel(userId, channelId); channel = await Client4.getChannel(channelId); } else { channel = await Client4.getChannelByName(teamId, channelName, true); if ((channel.type === General.GM_CHANNEL) || (channel.type === General.DM_CHANNEL)) { member = await Client4.getChannelMember(channel.id, userId); } else { member = await Client4.addToChannel(userId, channel.id); } } } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(logError(error)); return {error}; } Client4.trackEvent('action', 'action_channels_join', {channel_id: channelId}); dispatch(batchActions([ { type: ChannelTypes.RECEIVED_CHANNEL, data: channel, }, { type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER, data: member, }, ])); dispatch(addChannelToInitialCategory(channel)); if (member) { dispatch(loadRolesIfNeeded(member.roles.split(' '))); } return {data: {channel, member}}; }; } export function deleteChannel(channelId: string): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { let state = getState(); const viewArchivedChannels = state.entities.general.config.ExperimentalViewArchivedChannels === 'true'; try { await Client4.deleteChannel(channelId); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(logError(error)); return {error}; } state = getState(); const {currentChannelId} = state.entities.channels; if (channelId === currentChannelId && !viewArchivedChannels) { const teamId = getCurrentTeamId(state); const channelsInTeam = getChannelsNameMapInTeam(state, teamId); const channel = getChannelByName(channelsInTeam, getRedirectChannelNameForTeam(state, teamId)); if (channel && channel.id) { dispatch({type: ChannelTypes.SELECT_CHANNEL, data: channel.id}); } } dispatch({type: ChannelTypes.DELETE_CHANNEL_SUCCESS, data: {id: channelId, viewArchivedChannels}}); return {data: true}; }; } export function unarchiveChannel(channelId: string): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { try { await Client4.unarchiveChannel(channelId); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(logError(error)); return {error}; } const state = getState(); const config = getConfig(state); const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true'; dispatch({type: ChannelTypes.UNARCHIVED_CHANNEL_SUCCESS, data: {id: channelId, viewArchivedChannels}}); return {data: true}; }; } export function viewChannel(channelId: string, prevChannelId = ''): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { const {currentUserId} = getState().entities.users; const {myPreferences} = getState().entities.preferences; const viewTimePref = myPreferences[`${Preferences.CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME}--${channelId}`]; const viewTime = viewTimePref ? parseInt(viewTimePref.value!, 10) : 0; const prevChanManuallyUnread = isManuallyUnread(getState(), prevChannelId); if (viewTime < new Date().getTime() - (3 * 60 * 60 * 1000)) { const preferences = [ {user_id: currentUserId, category: Preferences.CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME, name: channelId, value: new Date().getTime().toString()}, ]; savePreferences(currentUserId, preferences)(dispatch); } try { await Client4.viewMyChannel(channelId, prevChanManuallyUnread ? '' : prevChannelId); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(logError(error)); return {error}; } const actions: Action[] = []; const {myMembers} = getState().entities.channels; const member = myMembers[channelId]; if (member) { if (isManuallyUnread(getState(), channelId)) { actions.push({ type: ChannelTypes.REMOVE_MANUALLY_UNREAD, data: {channelId}, }); } actions.push({ type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER, data: {...member, last_viewed_at: new Date().getTime()}, }); dispatch(loadRolesIfNeeded(member.roles.split(' '))); } const prevMember = myMembers[prevChannelId]; if (!prevChanManuallyUnread && prevMember) { actions.push({ type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER, data: {...prevMember, last_viewed_at: new Date().getTime()}, }); dispatch(loadRolesIfNeeded(prevMember.roles.split(' '))); } dispatch(batchActions(actions)); return {data: true}; }; } export function markChannelAsViewed(channelId: string, prevChannelId = ''): ActionFunc { return (dispatch: DispatchFunc, getState: GetStateFunc) => { const actions: Action[] = []; const {myMembers} = getState().entities.channels; const member = myMembers[channelId]; const state = getState(); if (member) { actions.push({ type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER, data: {...member, last_viewed_at: Date.now()}, }); if (isManuallyUnread(state, channelId)) { actions.push({ type: ChannelTypes.REMOVE_MANUALLY_UNREAD, data: {channelId}, }); } dispatch(loadRolesIfNeeded(member.roles.split(' '))); } const prevMember = myMembers[prevChannelId]; if (prevMember && !isManuallyUnread(getState(), prevChannelId)) { actions.push({ type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER, data: {...prevMember, last_viewed_at: Date.now()}, }); dispatch(loadRolesIfNeeded(prevMember.roles.split(' '))); } if (actions.length) { dispatch(batchActions(actions)); } return {data: true}; }; } export function getChannels(teamId: string, page = 0, perPage: number = General.CHANNELS_CHUNK_SIZE): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { dispatch({type: ChannelTypes.GET_CHANNELS_REQUEST, data: null}); let channels; try { channels = await Client4.getChannels(teamId, page, perPage); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(batchActions([ {type: ChannelTypes.GET_CHANNELS_FAILURE, error}, logError(error), ])); return {error}; } dispatch(batchActions([ { type: ChannelTypes.RECEIVED_CHANNELS, teamId, data: channels, }, { type: ChannelTypes.GET_CHANNELS_SUCCESS, }, ])); return {data: channels}; }; } export function getArchivedChannels(teamId: string, page = 0, perPage: number = General.CHANNELS_CHUNK_SIZE): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { let channels; try { channels = await Client4.getArchivedChannels(teamId, page, perPage); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); return {error}; } dispatch({ type: ChannelTypes.RECEIVED_CHANNELS, teamId, data: channels, }); return {data: channels}; }; } export function getAllChannelsWithCount(page = 0, perPage: number = General.CHANNELS_CHUNK_SIZE, notAssociatedToGroup = '', excludeDefaultChannels = false, includeDeleted = false): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { dispatch({type: ChannelTypes.GET_ALL_CHANNELS_REQUEST, data: null}); let payload; try { payload = await Client4.getAllChannels(page, perPage, notAssociatedToGroup, excludeDefaultChannels, true, includeDeleted) as ChannelsWithTotalCount; } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(batchActions([ {type: ChannelTypes.GET_ALL_CHANNELS_FAILURE, error}, logError(error), ])); return {error}; } dispatch(batchActions([ { type: ChannelTypes.RECEIVED_ALL_CHANNELS, data: payload.channels, }, { type: ChannelTypes.GET_ALL_CHANNELS_SUCCESS, }, { type: ChannelTypes.RECEIVED_TOTAL_CHANNEL_COUNT, data: payload.total_count, }, ])); return {data: payload}; }; } export function getAllChannels(page = 0, perPage: number = General.CHANNELS_CHUNK_SIZE, notAssociatedToGroup = '', excludeDefaultChannels = false): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { dispatch({type: ChannelTypes.GET_ALL_CHANNELS_REQUEST, data: null}); let channels; try { channels = await Client4.getAllChannels(page, perPage, notAssociatedToGroup, excludeDefaultChannels); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(batchActions([ {type: ChannelTypes.GET_ALL_CHANNELS_FAILURE, error}, logError(error), ])); return {error}; } dispatch(batchActions([ { type: ChannelTypes.RECEIVED_ALL_CHANNELS, data: channels, }, { type: ChannelTypes.GET_ALL_CHANNELS_SUCCESS, }, ])); return {data: channels}; }; } export function autocompleteChannels(teamId: string, term: string): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { dispatch({type: ChannelTypes.GET_CHANNELS_REQUEST, data: null}); let channels; try { channels = await Client4.autocompleteChannels(teamId, term); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(batchActions([ {type: ChannelTypes.GET_CHANNELS_FAILURE, error}, logError(error), ])); return {error}; } dispatch(batchActions([ { type: ChannelTypes.RECEIVED_CHANNELS, teamId, data: channels, }, { type: ChannelTypes.GET_CHANNELS_SUCCESS, }, ])); return {data: channels}; }; } export function autocompleteChannelsForSearch(teamId: string, term: string): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { dispatch({type: ChannelTypes.GET_CHANNELS_REQUEST, data: null}); let channels; try { channels = await Client4.autocompleteChannelsForSearch(teamId, term); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(batchActions([ {type: ChannelTypes.GET_CHANNELS_FAILURE, error}, logError(error), ])); return {error}; } dispatch(batchActions([ { type: ChannelTypes.RECEIVED_CHANNELS, teamId, data: channels, }, { type: ChannelTypes.GET_CHANNELS_SUCCESS, }, ])); return {data: channels}; }; } export function searchChannels(teamId: string, term: string, archived?: boolean): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { dispatch({type: ChannelTypes.GET_CHANNELS_REQUEST, data: null}); let channels; try { if (archived) { channels = await Client4.searchArchivedChannels(teamId, term); } else { channels = await Client4.searchChannels(teamId, term); } } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(batchActions([ {type: ChannelTypes.GET_CHANNELS_FAILURE, error}, logError(error), ])); return {error}; } dispatch(batchActions([ { type: ChannelTypes.RECEIVED_CHANNELS, teamId, data: channels, }, { type: ChannelTypes.GET_CHANNELS_SUCCESS, }, ])); return {data: channels}; }; } export function searchAllChannels(term: string, opts: ChannelSearchOpts = {}): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { dispatch({type: ChannelTypes.GET_ALL_CHANNELS_REQUEST, data: null}); let response; try { response = await Client4.searchAllChannels(term, opts) as ChannelsWithTotalCount; } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(batchActions([ {type: ChannelTypes.GET_ALL_CHANNELS_FAILURE, error}, logError(error), ])); return {error}; } const channels = response.channels || response; dispatch(batchActions([ { type: ChannelTypes.RECEIVED_ALL_CHANNELS, data: channels, }, { type: ChannelTypes.GET_ALL_CHANNELS_SUCCESS, }, ])); return {data: response}; }; } export function searchGroupChannels(term: string): ActionFunc { return bindClientFunc({ clientFunc: Client4.searchGroupChannels, params: [term], }); } export function getChannelStats(channelId: string): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { let stat; try { stat = await Client4.getChannelStats(channelId); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(logError(error)); return {error}; } dispatch({ type: ChannelTypes.RECEIVED_CHANNEL_STATS, data: stat, }); return {data: stat}; }; } export function addChannelMember(channelId: string, userId: string, postRootId = ''): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { let member; try { member = await Client4.addToChannel(userId, channelId, postRootId); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(logError(error)); return {error}; } Client4.trackEvent('action', 'action_channels_add_member', {channel_id: channelId}); dispatch(batchActions([ { type: UserTypes.RECEIVED_PROFILE_IN_CHANNEL, data: {id: channelId, user_id: userId}, }, { type: ChannelTypes.RECEIVED_CHANNEL_MEMBER, data: member, }, { type: ChannelTypes.ADD_CHANNEL_MEMBER_SUCCESS, id: channelId, }, ], 'ADD_CHANNEL_MEMBER.BATCH')); return {data: member}; }; } export function removeChannelMember(channelId: string, userId: string): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { try { await Client4.removeFromChannel(userId, channelId); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(logError(error)); return {error}; } Client4.trackEvent('action', 'action_channels_remove_member', {channel_id: channelId}); dispatch(batchActions([ { type: UserTypes.RECEIVED_PROFILE_NOT_IN_CHANNEL, data: {id: channelId, user_id: userId}, }, { type: ChannelTypes.REMOVE_CHANNEL_MEMBER_SUCCESS, id: channelId, }, ], 'REMOVE_CHANNEL_MEMBER.BATCH')); return {data: true}; }; } export function updateChannelMemberRoles(channelId: string, userId: string, roles: string): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { try { await Client4.updateChannelMemberRoles(channelId, userId, roles); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(logError(error)); return {error}; } const membersInChannel = getState().entities.channels.membersInChannel[channelId]; if (membersInChannel && membersInChannel[userId]) { dispatch({ type: ChannelTypes.RECEIVED_CHANNEL_MEMBER, data: {...membersInChannel[userId], roles}, }); } return {data: true}; }; } export function updateChannelHeader(channelId: string, header: string): ActionFunc { return async (dispatch: DispatchFunc) => { Client4.trackEvent('action', 'action_channels_update_header', {channel_id: channelId}); dispatch({ type: ChannelTypes.UPDATE_CHANNEL_HEADER, data: { channelId, header, }, }); return {data: true}; }; } export function updateChannelPurpose(channelId: string, purpose: string): ActionFunc { return async (dispatch: DispatchFunc) => { Client4.trackEvent('action', 'action_channels_update_purpose', {channel_id: channelId}); dispatch({ type: ChannelTypes.UPDATE_CHANNEL_PURPOSE, data: { channelId, purpose, }, }); return {data: true}; }; } export function markChannelAsRead(channelId: string, prevChannelId?: string, updateLastViewedAt = true): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { const prevChanManuallyUnread = isManuallyUnread(getState(), prevChannelId); // Send channel last viewed at to the server if (updateLastViewedAt) { Client4.viewMyChannel(channelId, prevChanManuallyUnread ? '' : prevChannelId).then().catch((error) => { forceLogoutIfNecessary(error, dispatch, getState); dispatch(logError(error)); return {error}; }); } const state = getState(); const {channels, myMembers} = state.entities.channels; // Update channel member objects to set all mentions and posts as viewed const channel = channels[channelId]; const prevChannel = (!prevChanManuallyUnread && prevChannelId) ? channels[prevChannelId] : null; // May be null since prevChannelId is optional // Update team member objects to set mentions and posts in channel as viewed const channelMember = myMembers[channelId]; const prevChannelMember = (!prevChanManuallyUnread && prevChannelId) ? myMembers[prevChannelId] : null; // May also be null const actions: Action[] = []; if (channel && channelMember) { actions.push({ type: ChannelTypes.DECREMENT_UNREAD_MSG_COUNT, data: { teamId: channel.team_id, channelId, amount: channel.total_msg_count - channelMember.msg_count, }, }); actions.push({ type: ChannelTypes.DECREMENT_UNREAD_MENTION_COUNT, data: { teamId: channel.team_id, channelId, amount: channelMember.mention_count, }, }); } if (channel && isManuallyUnread(getState(), channelId)) { actions.push({ type: ChannelTypes.REMOVE_MANUALLY_UNREAD, data: {channelId}, }); } if (prevChannel && prevChannelMember) { actions.push({ type: ChannelTypes.DECREMENT_UNREAD_MSG_COUNT, data: { teamId: prevChannel.team_id, channelId: prevChannelId, amount: prevChannel.total_msg_count - prevChannelMember.msg_count, }, }); actions.push({ type: ChannelTypes.DECREMENT_UNREAD_MENTION_COUNT, data: { teamId: prevChannel.team_id, channelId: prevChannelId, amount: prevChannelMember.mention_count, }, }); } if (actions.length > 0) { dispatch(batchActions(actions)); } return {data: true}; }; } // Increments the number of posts in the channel by 1 and marks it as unread if necessary export function markChannelAsUnread(teamId: string, channelId: string, mentions: string[], fetchedChannelMember = false): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { const state = getState(); const {myMembers} = state.entities.channels; const {currentUserId} = state.entities.users; const actions: Action[] = [{ type: ChannelTypes.INCREMENT_UNREAD_MSG_COUNT, data: { teamId, channelId, amount: 1, onlyMentions: myMembers[channelId] && myMembers[channelId].notify_props && myMembers[channelId].notify_props.mark_unread === MarkUnread.MENTION, fetchedChannelMember, }, }]; if (!fetchedChannelMember) { actions.push({ type: ChannelTypes.INCREMENT_TOTAL_MSG_COUNT, data: { channelId, amount: 1, }, }); } if (mentions && mentions.indexOf(currentUserId) !== -1) { actions.push({ type: ChannelTypes.INCREMENT_UNREAD_MENTION_COUNT, data: { teamId, channelId, amount: 1, fetchedChannelMember, }, }); } dispatch(batchActions(actions)); return {data: true}; }; } export function getChannelMembersByIds(channelId: string, userIds: string[]) { return bindClientFunc({ clientFunc: Client4.getChannelMembersByIds, onSuccess: ChannelTypes.RECEIVED_CHANNEL_MEMBERS, params: [ channelId, userIds, ], }); } export function getChannelMember(channelId: string, userId: string) { return bindClientFunc({ clientFunc: Client4.getChannelMember, onSuccess: ChannelTypes.RECEIVED_CHANNEL_MEMBER, params: [ channelId, userId, ], }); } export function getMyChannelMember(channelId: string) { return bindClientFunc({ clientFunc: Client4.getMyChannelMember, onSuccess: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER, params: [ channelId, ], }); } export function favoriteChannel(channelId: string, updateCategories = true): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { const state = getState(); const config = getConfig(state); const currentUserId = getCurrentUserId(state); const preference: PreferenceType = { user_id: currentUserId, category: Preferences.CATEGORY_FAVORITE_CHANNEL, name: channelId, value: 'true', }; Client4.trackEvent('action', 'action_channels_favorite'); if (config.EnableLegacySidebar === 'true') { // The old sidebar is enabled, so favorite the channel by calling the preferences API return dispatch(savePreferences(currentUserId, [preference])); } // The new sidebar is enabled, so favorite the channel by moving it into the current team's Favorites category if (updateCategories) { const channel = getChannelSelector(state, channelId); const category = getCategoryInTeamByType(state, channel.team_id || getCurrentTeamId(state), CategoryTypes.FAVORITES); if (category) { await dispatch(addChannelToCategory(category.id, channelId)); } } return dispatch({ type: PreferenceTypes.RECEIVED_PREFERENCES, data: [preference], }); }; } export function unfavoriteChannel(channelId: string, updateCategories = true): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { const state = getState(); const config = getConfig(state); const currentUserId = getCurrentUserId(state); const preference: PreferenceType = { user_id: currentUserId, category: Preferences.CATEGORY_FAVORITE_CHANNEL, name: channelId, value: '', }; Client4.trackEvent('action', 'action_channels_unfavorite'); if (config.EnableLegacySidebar === 'true') { // The old sidebar is enabled, so unfavorite the channel by calling the preferences API return dispatch(deletePreferences(currentUserId, [preference])); } // The new sidebar is enabled, so unfavorite the channel by moving it into the current team's Channels/DMs category if (updateCategories) { const channel = getChannelSelector(state, channelId); const category = getCategoryInTeamByType( state, channel.team_id || getCurrentTeamId(state), channel.type === General.DM_CHANNEL || channel.type === General.GM_CHANNEL ? CategoryTypes.DIRECT_MESSAGES : CategoryTypes.CHANNELS, ); if (category) { await dispatch(addChannelToCategory(category.id, channel.id));