// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import {ChannelCategoryTypes} 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 {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 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); // Add the channel to the new category const categories = [{ ...targetCategory, sorting: (setManualSorting && targetCategory.type !== CategoryTypes.DIRECT_MESSAGES) ? CategorySorting.Manual : 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 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); let updatedOrder; try { updatedOrder = await Client4.updateChannelCategoryOrder(currentUserId, teamId, newOrder); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(logError(error)); return {error}; } // The order will be updated in the state after receiving the corresponding websocket event. return {data: updatedOrder}; }; } export function receivedCategoryOrder(teamId: string, order: string[]) { return { type: ChannelCategoryTypes.RECEIVED_CATEGORY_ORDER, data: { teamId, order, }, }; } export function createCategory(teamId: string, displayName: string, channelIds: $ID[] = []): 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}; }; }