UNPKG

mattermost-redux

Version:

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

470 lines (397 loc) 17.9 kB
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import {ChannelCategoryTypes, ChannelTypes} from 'action_types'; import {Client4} from 'client'; import {unfavoriteChannel, favoriteChannel} from 'actions/channels'; import {logError} from 'actions/errors'; import {forceLogoutIfNecessary} from 'actions/helpers'; import {General} from '../constants'; import {CategoryTypes} from 'constants/channel_categories'; import { getAllCategoriesByIds, getCategory, getCategoryIdsForTeam, getCategoryInTeamByType, getCategoryInTeamWithChannel, } from 'selectors/entities/channel_categories'; import {getCurrentUserId} from 'selectors/entities/users'; import { ActionFunc, batchActions, DispatchFunc, GetStateFunc, } from 'types/actions'; import {CategorySorting, OrderedChannelCategories, ChannelCategory} from 'types/channel_categories'; import {Channel} from 'types/channels'; import {$ID} from 'types/utilities'; import {insertMultipleWithoutDuplicates, insertWithoutDuplicates, removeItem} from 'utils/array_utils'; export function expandCategory(categoryId: string) { return { type: ChannelCategoryTypes.CATEGORY_EXPANDED, data: categoryId, }; } export function collapseCategory(categoryId: string) { return { type: ChannelCategoryTypes.CATEGORY_COLLAPSED, data: categoryId, }; } export function setCategorySorting(categoryId: string, sorting: CategorySorting) { return (dispatch: DispatchFunc, getState: GetStateFunc) => { const state = getState(); const category = getCategory(state, categoryId); return dispatch(updateCategory({ ...category, sorting, })); }; } export function setCategoryMuted(categoryId: string, muted: boolean) { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { const state = getState(); const category = getCategory(state, categoryId); const result = await dispatch(updateCategory({ ...category, muted, })); if ('error' in result) { return result; } const updated = result.data as ChannelCategory; return dispatch(batchActions([ { type: ChannelCategoryTypes.RECEIVED_CATEGORY, data: updated, }, ...(updated.channel_ids.map((channelId) => ({ type: ChannelTypes.SET_CHANNEL_MUTED, data: { channelId, muted, }, }))), ])); }; } export function updateCategory(category: ChannelCategory): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { const state = getState(); const currentUserId = getCurrentUserId(state); let updatedCategory; try { updatedCategory = await Client4.updateChannelCategory(currentUserId, category.team_id, category); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(logError(error)); return {error}; } // The updated category will be added to the state after receiving the corresponding websocket event. return {data: updatedCategory}; }; } export function fetchMyCategories(teamId: string) { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { const currentUserId = getCurrentUserId(getState()); let data: OrderedChannelCategories; try { data = await Client4.getChannelCategories(currentUserId, teamId); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(logError(error)); return {error}; } return dispatch(batchActions([ { type: ChannelCategoryTypes.RECEIVED_CATEGORIES, data: data.categories, }, { type: ChannelCategoryTypes.RECEIVED_CATEGORY_ORDER, data: { teamId, order: data.order, }, }, ])); }; } // addChannelToInitialCategory returns an action that can be dispatched to add a newly-joined or newly-created channel // to its either the Channels or Direct Messages category based on the type of channel. New DM and GM channels are // added to the Direct Messages category on each team. // // Unless setOnServer is true, this only affects the categories on this client. If it is set to true, this updates // categories on the server too. export function addChannelToInitialCategory(channel: Channel, setOnServer = false): ActionFunc { return (dispatch: DispatchFunc, getState: GetStateFunc) => { const state = getState(); const categories = Object.values(getAllCategoriesByIds(state)); if (channel.type === General.DM_CHANNEL || channel.type === General.GM_CHANNEL) { const allDmCategories = categories.filter((category) => category.type === CategoryTypes.DIRECT_MESSAGES); // Get all the categories in which channel exists const channelInCategories = categories.filter((category) => { return category.channel_ids.findIndex((channelId) => channelId === channel.id) !== -1; }); // Skip DM categories where channel already exists in a different category const dmCategories = allDmCategories.filter((dmCategory) => { return channelInCategories.findIndex((category) => dmCategory.team_id === category.team_id) === -1; }); return dispatch({ type: ChannelCategoryTypes.RECEIVED_CATEGORIES, data: dmCategories.map((category) => ({ ...category, channel_ids: insertWithoutDuplicates(category.channel_ids, channel.id, 0), })), }); } // Add the new channel to the Channels category on the channel's team if (categories.some((category) => category.channel_ids.some((channelId) => channelId === channel.id))) { return {data: false}; } const channelsCategory = getCategoryInTeamByType(state, channel.team_id, CategoryTypes.CHANNELS); if (!channelsCategory) { // No categories were found for this team, so the categories for this team haven't been loaded yet. // The channel will have been added to the category by the server, so we'll get it once the categories // are actually loaded. return {data: false}; } if (setOnServer) { return dispatch(addChannelToCategory(channelsCategory.id, channel.id)); } return dispatch({ type: ChannelCategoryTypes.RECEIVED_CATEGORY, data: { ...channelsCategory, channel_ids: insertWithoutDuplicates(channelsCategory.channel_ids, channel.id, 0), }, }); }; } // addChannelToCategory returns an action that can be dispatched to add a channel to a given category without specifying // its order. The channel will be removed from its previous category (if any) on the given category's team and it will be // placed first in its new category. export function addChannelToCategory(categoryId: string, channelId: string): ActionFunc { return moveChannelToCategory(categoryId, channelId, 0, false); } // moveChannelToCategory returns an action that moves a channel into a category and puts it at the given index at the // category. The channel will also be removed from its previous category (if any) on that category's team. The category's // order will also be set to manual by default. export function moveChannelToCategory(categoryId: string, channelId: string, newIndex: number, setManualSorting = true) { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { const state = getState(); const targetCategory = getCategory(state, categoryId); const currentUserId = getCurrentUserId(state); // The default sorting needs to behave like alphabetical sorting until the point that the user rearranges their // channels at which point, it becomes manual. Other than that, we never change the sorting method automatically. let sorting = targetCategory.sorting; if (setManualSorting && targetCategory.type !== CategoryTypes.DIRECT_MESSAGES && targetCategory.sorting === CategorySorting.Default) { sorting = CategorySorting.Manual; } // Add the channel to the new category const categories = [{ ...targetCategory, sorting, channel_ids: insertWithoutDuplicates(targetCategory.channel_ids, channelId, newIndex), }]; // And remove it from the old category const sourceCategory = getCategoryInTeamWithChannel(getState(), targetCategory.team_id, channelId); if (sourceCategory && sourceCategory.id !== targetCategory.id) { categories.push({ ...sourceCategory, channel_ids: removeItem(sourceCategory.channel_ids, channelId), }); } const result = dispatch({ type: ChannelCategoryTypes.RECEIVED_CATEGORIES, data: categories, }); try { await Client4.updateChannelCategories(currentUserId, targetCategory.team_id, categories); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(logError(error)); const originalCategories = [targetCategory]; if (sourceCategory && sourceCategory.id !== targetCategory.id) { originalCategories.push(sourceCategory); } dispatch({ type: ChannelCategoryTypes.RECEIVED_CATEGORIES, data: originalCategories, }); return {error}; } // Update the favorite preferences locally on the client in case we have any logic relying on that if (targetCategory.type === CategoryTypes.FAVORITES) { await dispatch(favoriteChannel(channelId, false)); } else if (sourceCategory && sourceCategory.type === CategoryTypes.FAVORITES) { await dispatch(unfavoriteChannel(channelId, false)); } return result; }; } export function moveChannelsToCategory(categoryId: string, channelIds: string[], newIndex: number, setManualSorting = true) { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { const state = getState(); const targetCategory = getCategory(state, categoryId); const currentUserId = getCurrentUserId(state); // The default sorting needs to behave like alphabetical sorting until the point that the user rearranges their // channels at which point, it becomes manual. Other than that, we never change the sorting method automatically. let sorting = targetCategory.sorting; if (setManualSorting && targetCategory.type !== CategoryTypes.DIRECT_MESSAGES && targetCategory.sorting === CategorySorting.Default) { sorting = CategorySorting.Manual; } // Add the channels to the new category let categories = { [targetCategory.id]: { ...targetCategory, sorting, channel_ids: insertMultipleWithoutDuplicates(targetCategory.channel_ids, channelIds, newIndex), }, }; // Needed if we have to revert categories and for checking for favourites let unmodifiedCategories = {[targetCategory.id]: targetCategory}; let sourceCategories: Record<string, string> = {}; // And remove it from the old categories channelIds.forEach((channelId) => { const sourceCategory = getCategoryInTeamWithChannel(getState(), targetCategory.team_id, channelId); if (sourceCategory && sourceCategory.id !== targetCategory.id) { unmodifiedCategories = { ...unmodifiedCategories, [sourceCategory.id]: sourceCategory, }; sourceCategories = {...sourceCategories, [channelId]: sourceCategory.id}; categories = { ...categories, [sourceCategory.id]: { ...(categories[sourceCategory.id] || sourceCategory), channel_ids: removeItem((categories[sourceCategory.id] || sourceCategory).channel_ids, channelId), }, }; } }); const categoriesArray = Object.values(categories).reduce((allCategories: ChannelCategory[], category) => { allCategories.push(category); return allCategories; }, []); const result = dispatch({ type: ChannelCategoryTypes.RECEIVED_CATEGORIES, data: categoriesArray, }); try { await Client4.updateChannelCategories(currentUserId, targetCategory.team_id, categoriesArray); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(logError(error)); const originalCategories = Object.values(unmodifiedCategories).reduce((allCategories: ChannelCategory[], category) => { allCategories.push(category); return allCategories; }, []); dispatch({ type: ChannelCategoryTypes.RECEIVED_CATEGORIES, data: originalCategories, }); return {error}; } // Update the favorite preferences locally on the client in case we have any logic relying on that await Promise.all(channelIds.map(async (channelId) => { const sourceCategory = unmodifiedCategories[sourceCategories[channelId]]; if (targetCategory.type === CategoryTypes.FAVORITES) { await dispatch(favoriteChannel(channelId, false)); } else if (sourceCategory && sourceCategory.type === CategoryTypes.FAVORITES) { await dispatch(unfavoriteChannel(channelId, false)); } })); return result; }; } export function moveCategory(teamId: string, categoryId: string, newIndex: number) { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { const state = getState(); const order = getCategoryIdsForTeam(state, teamId)!; const currentUserId = getCurrentUserId(state); const newOrder = insertWithoutDuplicates(order, categoryId, newIndex); // Optimistically update the category order const result = dispatch({ type: ChannelCategoryTypes.RECEIVED_CATEGORY_ORDER, data: { teamId, order: newOrder, }, }); try { await Client4.updateChannelCategoryOrder(currentUserId, teamId, newOrder); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(logError(error)); // Restore original order dispatch({ type: ChannelCategoryTypes.RECEIVED_CATEGORY_ORDER, data: { teamId, order, }, }); return {error}; } return result; }; } export function receivedCategoryOrder(teamId: string, order: string[]) { return { type: ChannelCategoryTypes.RECEIVED_CATEGORY_ORDER, data: { teamId, order, }, }; } export function createCategory(teamId: string, displayName: string, channelIds: Array<$ID<Channel>> = []): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { const currentUserId = getCurrentUserId(getState()); let newCategory; try { newCategory = await Client4.createChannelCategory(currentUserId, teamId, { team_id: teamId, user_id: currentUserId, display_name: displayName, channel_ids: channelIds, }); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(logError(error)); return {error}; } // The new category will be added to the state after receiving the corresponding websocket event. return {data: newCategory}; }; } export function renameCategory(categoryId: string, displayName: string): ActionFunc { return (dispatch: DispatchFunc, getState: GetStateFunc) => { const state = getState(); const category = getCategory(state, categoryId); return dispatch(updateCategory({ ...category, display_name: displayName, })); }; } export function deleteCategory(categoryId: string): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { const state = getState(); const category = getCategory(state, categoryId); const currentUserId = getCurrentUserId(state); try { await Client4.deleteChannelCategory(currentUserId, category.team_id, category.id); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(logError(error)); return {error}; } // The category will be deleted from the state after receiving the corresponding websocket event. return {data: true}; }; }