UNPKG

mattermost-redux

Version:

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

1,279 lines (1,099 loc) 51.7 kB
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import {createSelector} from 'reselect'; import {General, Permissions} from '../../constants'; import {CategoryTypes} from 'constants/channel_categories'; import {getCategoryInTeamByType} from 'selectors/entities/channel_categories'; import { getCurrentChannelId, getCurrentUser, getUsers, getMyChannelMemberships, getMyCurrentChannelMembership, } from 'selectors/entities/common'; import {getConfig, getLicense, hasNewPermissions} from 'selectors/entities/general'; import {getLastPostPerChannel} from 'selectors/entities/posts'; import { getFavoritesPreferences, getMyPreferences, getTeammateNameDisplaySetting, getVisibleTeammate, getVisibleGroupIds, } from 'selectors/entities/preferences'; import {haveICurrentChannelPermission, haveIChannelPermission, haveITeamPermission} from 'selectors/entities/roles'; import { getCurrentTeamId, getCurrentTeamMembership, getMyTeams, getTeamMemberships, } from 'selectors/entities/teams'; import {isCurrentUserSystemAdmin, getCurrentUserId, getUserIdsInChannels} from 'selectors/entities/users'; import {Channel, ChannelStats, ChannelMembership, ChannelModeration, ChannelMemberCountsByGroup} from 'types/channels'; import {ClientConfig} from 'types/config'; import {Post} from 'types/posts'; import {PreferenceType} from 'types/preferences'; import {GlobalState} from 'types/store'; import {TeamMembership, Team} from 'types/teams'; import {UsersState, UserProfile} from 'types/users'; import { $ID, IDMappedObjects, NameMappedObjects, RelationOneToMany, RelationOneToOne, UserIDMappedObjects, } from 'types/utilities'; import { canManageMembersOldPermissions, completeDirectChannelInfo, completeDirectChannelDisplayName, getUserIdFromChannelName, getChannelByName as getChannelByNameHelper, isChannelMuted, getDirectChannelName, isAutoClosed, isDirectChannelVisible, isGroupChannelVisible, sortChannelsByDisplayName, isFavoriteChannelOld, isDefault, sortChannelsByRecency, } from 'utils/channel_utils'; import {createIdsSelector} from 'utils/helpers'; export {getCurrentChannelId, getMyChannelMemberships, getMyCurrentChannelMembership}; type SortingType = 'recent' | 'alpha'; export function getAllChannels(state: GlobalState): IDMappedObjects<Channel> { return state.entities.channels.channels; } export function getAllChannelStats(state: GlobalState): RelationOneToOne<Channel, ChannelStats> { return state.entities.channels.stats; } export function getChannelsInTeam(state: GlobalState): RelationOneToMany<Team, Channel> { return state.entities.channels.channelsInTeam; } export const getDirectChannelsSet: (state: GlobalState) => Set<string> = createSelector( getChannelsInTeam, (channelsInTeam: RelationOneToMany<Team, Channel>): Set<string> => { if (!channelsInTeam) { return new Set(); } return new Set(channelsInTeam['']); }, ); export function getChannelMembersInChannels(state: GlobalState): RelationOneToOne<Channel, UserIDMappedObjects<ChannelMembership>> { return state.entities.channels.membersInChannel; } function sortChannelsByRecencyOrAlpha(locale: string, lastPosts: RelationOneToOne<Channel, Post>, sorting: SortingType, a: Channel, b: Channel) { if (sorting === 'recent') { return sortChannelsByRecency(lastPosts, a, b); } return sortChannelsByDisplayName(locale, a, b); } // mapAndSortChannelIds sorts channels, primarily by: // For all sections except unreads: // a. All other unread channels // b. Muted channels // For unreads section: // a. Non-muted channels with mentions // b. Muted channels with mentions // c. Remaining unread channels // And then secondary by alphabetical ("alpha") or chronological ("recency") order export const mapAndSortChannelIds = ( channels: Channel[], currentUser: UserProfile, myMembers: RelationOneToOne<Channel, ChannelMembership>, lastPosts: RelationOneToOne<Channel, Post>, sorting: SortingType, sortMentionsFirst = false, ): string[] => { const locale = currentUser.locale || General.DEFAULT_LOCALE; const mutedChannelIds = channels. filter((channel) => isChannelMuted(myMembers[channel.id])). sort(sortChannelsByRecencyOrAlpha.bind(null, locale, lastPosts, sorting)). map((channel) => channel.id); let hasMentionedChannelIds: string[] = []; if (sortMentionsFirst) { hasMentionedChannelIds = channels. filter((channel) => { const member = myMembers[channel.id]; return member && member.mention_count > 0 && !isChannelMuted(member); }). sort(sortChannelsByRecencyOrAlpha.bind(null, locale, lastPosts, sorting)). map((channel) => channel.id); } const otherChannelIds = channels. filter((channel) => { return !mutedChannelIds.includes(channel.id) && !hasMentionedChannelIds.includes(channel.id); }). sort(sortChannelsByRecencyOrAlpha.bind(null, locale, lastPosts, sorting)). map((channel) => channel.id); return sortMentionsFirst ? hasMentionedChannelIds.concat(mutedChannelIds, otherChannelIds) : otherChannelIds.concat(mutedChannelIds); }; export function filterChannels( unreadIds: string[], favoriteIds: string[], channelIds: string[], unreadsAtTop: boolean, favoritesAtTop: boolean, ): string[] { let channels: string[] = channelIds; if (unreadsAtTop) { channels = channels.filter((id) => { return !unreadIds.includes(id); }); } if (favoritesAtTop) { channels = channels.filter((id) => { return !favoriteIds.includes(id); }); } return channels; } // makeGetChannel returns a selector that returns a channel from the store with the following filled in for DM/GM channels: // - The display_name set to the other user(s) names, following the Teammate Name Display setting // - The teammate_id for DM channels // - The status of the other user in a DM channel export function makeGetChannel(): (state: GlobalState, props: {id: string}) => Channel { return createSelector( getAllChannels, (state: GlobalState, props: {id: string}) => props.id, (state: GlobalState) => state.entities.users, getTeammateNameDisplaySetting, (allChannels, channelId, users, teammateNameDisplay) => { const channel = allChannels[channelId]; if (channel) { return completeDirectChannelInfo(users, teammateNameDisplay!, channel); } return channel; }, ); } // getChannel returns a channel as it exists in the store without filling in any additional details such as the // display_name for DM/GM channels. export function getChannel(state: GlobalState, id: string) { return getAllChannels(state)[id]; } // makeGetChannelsForIds returns a selector that, given an array of channel IDs, returns a list of the corresponding // channels. Channels are returned in the same order as the given IDs with undefined entries replacing any invalid IDs. // Note that memoization will fail if an array literal is passed in. export function makeGetChannelsForIds(): (state: GlobalState, ids: string[]) => Channel[] { return createSelector( getAllChannels, (state: GlobalState, ids: string[]) => ids, (allChannels, ids) => { return ids.map((id) => allChannels[id]); }, ); } export const getCurrentChannel: (state: GlobalState) => Channel = createSelector( getAllChannels, getCurrentChannelId, (state: GlobalState): UsersState => state.entities.users, getTeammateNameDisplaySetting, (allChannels: IDMappedObjects<Channel>, currentChannelId: string, users: UsersState, teammateNameDisplay: string): Channel => { const channel = allChannels[currentChannelId]; if (channel) { return completeDirectChannelInfo(users, teammateNameDisplay, channel); } return channel; }, ); export const getMyChannelMember: (state: GlobalState, channelId: string) => ChannelMembership | undefined | null = createSelector( getMyChannelMemberships, (state: GlobalState, channelId: string): string => channelId, (channelMemberships: RelationOneToOne<Channel, ChannelMembership>, channelId: string): ChannelMembership | undefined | null => { return channelMemberships[channelId] || null; }, ); export const getCurrentChannelStats: (state: GlobalState) => ChannelStats = createSelector( getAllChannelStats, getCurrentChannelId, (allChannelStats: RelationOneToOne<Channel, ChannelStats>, currentChannelId: string): ChannelStats => { return allChannelStats[currentChannelId]; }, ); export function isCurrentChannelFavorite(state: GlobalState): boolean { const currentChannelId = getCurrentChannelId(state); return isFavoriteChannel(state, currentChannelId); } export const isCurrentChannelMuted: (state: GlobalState) => boolean = createSelector( getMyCurrentChannelMembership, (membership?: ChannelMembership | null): boolean => { if (!membership) { return false; } return isChannelMuted(membership); }, ); export const isCurrentChannelArchived: (state: GlobalState) => boolean = createSelector( getCurrentChannel, (channel: Channel): boolean => channel.delete_at !== 0, ); export const isCurrentChannelDefault: (state: GlobalState) => boolean = createSelector( getCurrentChannel, (channel: Channel): boolean => isDefault(channel), ); export function isCurrentChannelReadOnly(state: GlobalState): boolean { return isChannelReadOnly(state, getCurrentChannel(state)); } export function isChannelReadOnlyById(state: GlobalState, channelId: string): boolean { return isChannelReadOnly(state, getChannel(state, channelId)); } export function isChannelReadOnly(state: GlobalState, channel: Channel): boolean { return channel && channel.name === General.DEFAULT_CHANNEL && !isCurrentUserSystemAdmin(state) && getConfig(state).ExperimentalTownSquareIsReadOnly === 'true'; } export function shouldHideDefaultChannel(state: GlobalState, channel: Channel): boolean { return channel && channel.name === General.DEFAULT_CHANNEL && !isCurrentUserSystemAdmin(state) && getConfig(state).ExperimentalHideTownSquareinLHS === 'true'; } export const countCurrentChannelUnreadMessages: (state: GlobalState) => number = createSelector( getCurrentChannel, getMyCurrentChannelMembership, (channel: Channel, membership?: ChannelMembership | null): number => { if (!membership) { return 0; } return channel.total_msg_count - membership.msg_count; }, ); export function getChannelByName(state: GlobalState, channelName: string): Channel | undefined | null { return getChannelByNameHelper(getAllChannels(state), channelName); } export const getChannelSetInCurrentTeam: (state: GlobalState) => string[] = createSelector( getCurrentTeamId, getChannelsInTeam, (currentTeamId: string, channelsInTeam: RelationOneToMany<Team, Channel>): string[] => { return (channelsInTeam && channelsInTeam[currentTeamId]) || []; }, ); function sortAndInjectChannels(channels: IDMappedObjects<Channel>, channelSet: string[], locale: string): Channel[] { const currentChannels: Channel[] = []; if (typeof channelSet === 'undefined') { return currentChannels; } channelSet.forEach((c) => { currentChannels.push(channels[c]); }); return currentChannels.sort(sortChannelsByDisplayName.bind(null, locale)); } export const getChannelsInCurrentTeam: (state: GlobalState) => Channel[] = createSelector( getAllChannels, getChannelSetInCurrentTeam, getCurrentUser, (channels: IDMappedObjects<Channel>, currentTeamChannelSet: string[], currentUser: UserProfile): Channel[] => { let locale = General.DEFAULT_LOCALE; if (currentUser && currentUser.locale) { locale = currentUser.locale; } return sortAndInjectChannels(channels, currentTeamChannelSet, locale); }, ); export const getChannelsNameMapInTeam: (state: GlobalState, teamId: string) => NameMappedObjects<Channel> = createSelector( getAllChannels, getChannelsInTeam, (state: GlobalState, teamId: string): string => teamId, (channels: IDMappedObjects<Channel>, channelsInTeams: RelationOneToMany<Team, Channel>, teamId: string): NameMappedObjects<Channel> => { const channelsInTeam = channelsInTeams[teamId] || []; const channelMap: NameMappedObjects<Channel> = {}; channelsInTeam.forEach((id) => { const channel = channels[id]; channelMap[channel.name] = channel; }); return channelMap; }, ); export const getChannelsNameMapInCurrentTeam: (state: GlobalState) => NameMappedObjects<Channel> = createSelector( getAllChannels, getChannelSetInCurrentTeam, (channels: IDMappedObjects<Channel>, currentTeamChannelSet: string[]): NameMappedObjects<Channel> => { const channelMap: NameMappedObjects<Channel> = {}; currentTeamChannelSet.forEach((id) => { const channel = channels[id]; channelMap[channel.name] = channel; }); return channelMap; }, ); // Returns both DMs and GMs export const getAllDirectChannels: (state: GlobalState) => Channel[] = createSelector( getAllChannels, getDirectChannelsSet, (state: GlobalState): UsersState => state.entities.users, getTeammateNameDisplaySetting, (channels: IDMappedObjects<Channel>, channelSet: Set<string>, users: UsersState, teammateNameDisplay: string): Channel[] => { const dmChannels: Channel[] = []; channelSet.forEach((c) => { dmChannels.push(completeDirectChannelInfo(users, teammateNameDisplay, channels[c])); }); return dmChannels; }, ); export const getAllDirectChannelsNameMapInCurrentTeam: (state: GlobalState) => NameMappedObjects<Channel> = createSelector( getAllChannels, getDirectChannelsSet, (state: GlobalState): UsersState => state.entities.users, getTeammateNameDisplaySetting, (channels: IDMappedObjects<Channel>, channelSet: Set<string>, users: UsersState, teammateNameDisplay: string): NameMappedObjects<Channel> => { const channelMap: NameMappedObjects<Channel> = {}; channelSet.forEach((id) => { const channel = channels[id]; channelMap[channel.name] = completeDirectChannelInfo(users, teammateNameDisplay, channel); }); return channelMap; }, ); // Returns only GMs export const getGroupChannels: (state: GlobalState) => Channel[] = createSelector( getAllChannels, getDirectChannelsSet, (state: GlobalState): UsersState => state.entities.users, getTeammateNameDisplaySetting, (channels: IDMappedObjects<Channel>, channelSet: Set<string>, users: UsersState, teammateNameDisplay: string): Channel[] => { const gmChannels: Channel[] = []; channelSet.forEach((id) => { const channel = channels[id]; if (channel.type === General.GM_CHANNEL) { gmChannels.push(completeDirectChannelInfo(users, teammateNameDisplay, channel)); } }); return gmChannels; }, ); export const getMyChannels: (state: GlobalState) => Channel[] = createSelector( getChannelsInCurrentTeam, getAllDirectChannels, getMyChannelMemberships, (channels: Channel[], directChannels: Channel[], myMembers: RelationOneToOne<Channel, ChannelMembership>): Channel[] => { return [...channels, ...directChannels].filter((c) => myMembers.hasOwnProperty(c.id)); }, ); export const getOtherChannels: (state: GlobalState, archived?: boolean | null) => Channel[] = createSelector( getChannelsInCurrentTeam, getMyChannelMemberships, (state: GlobalState, archived: boolean | undefined | null = true) => archived, (channels: Channel[], myMembers: RelationOneToOne<Channel, ChannelMembership>, archived?: boolean | null): Channel[] => { return channels.filter((c) => !myMembers.hasOwnProperty(c.id) && c.type === General.OPEN_CHANNEL && (archived ? true : c.delete_at === 0)); }, ); export const getDefaultChannel: (state: GlobalState) => Channel | undefined | null = createSelector( getAllChannels, getCurrentTeamId, (channels: IDMappedObjects<Channel>, teamId: string): Channel | undefined | null => { return Object.keys(channels).map((key) => channels[key]).find((c) => c && c.team_id === teamId && c.name === General.DEFAULT_CHANNEL); }, ); export const getMembersInCurrentChannel: (state: GlobalState) => UserIDMappedObjects<ChannelMembership> = createSelector( getCurrentChannelId, getChannelMembersInChannels, (currentChannelId: string, members: RelationOneToOne<Channel, UserIDMappedObjects<ChannelMembership>>): UserIDMappedObjects<ChannelMembership> => { return members[currentChannelId]; }, ); export const getUnreads: (state: GlobalState) => { messageCount: number; mentionCount: number; } = createSelector( getAllChannels, getMyChannelMemberships, getUsers, getCurrentUserId, getCurrentTeamId, getMyTeams, getTeamMemberships, (channels: IDMappedObjects<Channel>, myMembers: RelationOneToOne<Channel, ChannelMembership>, users: IDMappedObjects<UserProfile>, currentUserId: string, currentTeamId: string, myTeams: Team[], myTeamMemberships: RelationOneToOne<Team, TeamMembership>): { messageCount: number; mentionCount: number; } => { let messageCountForCurrentTeam = 0; // Includes message count from channels of current team plus all GM'S and all DM's across teams let mentionCountForCurrentTeam = 0; // Includes mention count from channels of current team plus all GM'S and all DM's across teams Object.keys(myMembers).forEach((channelId) => { const channel = channels[channelId]; const m = myMembers[channelId]; if (!channel || !m) { return; } if (channel.team_id !== currentTeamId && channel.type !== General.DM_CHANNEL && channel.type !== General.GM_CHANNEL) { return; } let otherUserId = ''; if (channel.type === General.DM_CHANNEL) { otherUserId = getUserIdFromChannelName(currentUserId, channel.name); if (users[otherUserId] && users[otherUserId].delete_at === 0) { mentionCountForCurrentTeam += m.mention_count; } } else if (m.mention_count > 0 && channel.delete_at === 0) { mentionCountForCurrentTeam += m.mention_count; } if (m.notify_props && m.notify_props.mark_unread !== 'mention' && channel.total_msg_count - m.msg_count > 0) { if (channel.type === General.DM_CHANNEL) { // otherUserId is guaranteed to have been set above if (users[otherUserId] && users[otherUserId].delete_at === 0) { messageCountForCurrentTeam += 1; } } else if (channel.delete_at === 0) { messageCountForCurrentTeam += 1; } } }); // Includes mention count and message count from teams other than the current team // This count does not include GM's and DM's const otherTeamsUnreadCountForChannels = myTeams.reduce((acc, team) => { if (currentTeamId !== team.id) { const member = myTeamMemberships[team.id]; acc.messageCount += member.msg_count; acc.mentionCount += member.mention_count; } return acc; }, { messageCount: 0, mentionCount: 0, }); // messageCount is the number of unread channels, mention count is the total number of mentions return { messageCount: messageCountForCurrentTeam + otherTeamsUnreadCountForChannels.messageCount, mentionCount: mentionCountForCurrentTeam + otherTeamsUnreadCountForChannels.mentionCount, }; }, ); export const getUnreadsInCurrentTeam: (a: GlobalState) => { messageCount: number; mentionCount: number; } = createSelector(getCurrentChannelId, getMyChannels, getMyChannelMemberships, getUsers, getCurrentUserId, (currentChannelId: string, channels: Channel[], myMembers: RelationOneToOne<Channel, ChannelMembership>, users: IDMappedObjects<UserProfile>, currentUserId: string): { messageCount: number; mentionCount: number; } => { let messageCount = 0; let mentionCount = 0; channels.forEach((channel) => { const m = myMembers[channel.id]; if (m && channel.id !== currentChannelId) { let otherUserId = ''; if (channel.type === 'D') { otherUserId = getUserIdFromChannelName(currentUserId, channel.name); if (users[otherUserId] && users[otherUserId].delete_at === 0) { mentionCount += channel.total_msg_count - m.msg_count; } } else if (m.mention_count > 0 && channel.delete_at === 0) { mentionCount += m.mention_count; } if (m.notify_props && m.notify_props.mark_unread !== 'mention' && channel.total_msg_count - m.msg_count > 0) { if (channel.type === 'D') { if (users[otherUserId] && users[otherUserId].delete_at === 0) { messageCount += 1; } } else if (channel.delete_at === 0) { messageCount += 1; } } } }); return { messageCount, mentionCount, }; }); export const canManageChannelMembers: (state: GlobalState) => boolean = createSelector( getCurrentChannel, getCurrentUser, getCurrentTeamMembership, getMyCurrentChannelMembership, getConfig, getLicense, hasNewPermissions, (state: GlobalState): boolean => haveICurrentChannelPermission(state, { permission: Permissions.MANAGE_PRIVATE_CHANNEL_MEMBERS, }), (state: GlobalState): boolean => haveICurrentChannelPermission(state, { permission: Permissions.MANAGE_PUBLIC_CHANNEL_MEMBERS, }), ( channel: Channel, user: UserProfile, teamMembership: TeamMembership, channelMembership: ChannelMembership | undefined | null, config: ClientConfig, license: any, newPermissions: boolean, managePrivateMembers: boolean, managePublicMembers: boolean, ): boolean => { if (!channel) { return false; } if (channel.delete_at !== 0) { return false; } if (channel.type === General.DM_CHANNEL || channel.type === General.GM_CHANNEL || channel.name === General.DEFAULT_CHANNEL) { return false; } if (newPermissions) { if (channel.type === General.OPEN_CHANNEL) { return managePublicMembers; } else if (channel.type === General.PRIVATE_CHANNEL) { return managePrivateMembers; } return true; } if (!channelMembership) { return false; } return canManageMembersOldPermissions(channel, user, teamMembership, channelMembership, config, license); }, ); // Determine if the user has permissions to manage members in at least one channel of the current team export const canManageAnyChannelMembersInCurrentTeam: (state: GlobalState) => boolean = createSelector( getMyChannelMemberships, getCurrentTeamId, (state: GlobalState): GlobalState => state, (members: RelationOneToOne<Channel, ChannelMembership>, currentTeamId: string, state: GlobalState): boolean => { for (const channelId of Object.keys(members)) { const channel = getChannel(state, channelId); if (!channel || channel.team_id !== currentTeamId) { continue; } if (channel.type === General.OPEN_CHANNEL && haveIChannelPermission(state, { permission: Permissions.MANAGE_PUBLIC_CHANNEL_MEMBERS, channel: channelId, team: currentTeamId, })) { return true; } else if (channel.type === General.PRIVATE_CHANNEL && haveIChannelPermission(state, { permission: Permissions.MANAGE_PRIVATE_CHANNEL_MEMBERS, channel: channelId, team: currentTeamId, })) { return true; } } return false; }, ); export const getAllDirectChannelIds: (state: GlobalState) => string[] = createIdsSelector( getDirectChannelsSet, (directIds: Set<string>): string[] => { return Array.from(directIds); }, ); export const getChannelIdsInCurrentTeam: (state: GlobalState) => string[] = createIdsSelector( getCurrentTeamId, getChannelsInTeam, (currentTeamId: string, channelsInTeam: RelationOneToMany<Team, Channel>): string[] => { return Array.from(channelsInTeam[currentTeamId] || []); }, ); export const getChannelIdsForCurrentTeam: (state: GlobalState) => string[] = createIdsSelector( getChannelIdsInCurrentTeam, getAllDirectChannelIds, (channels, direct) => { return [...channels, ...direct]; }, ); export const getUnreadChannelIds: (state: GlobalState, lastUnreadChannel?: Channel | null) => string[] = createIdsSelector( getAllChannels, getMyChannelMemberships, getChannelIdsForCurrentTeam, (state: GlobalState, lastUnreadChannel: Channel | undefined | null = null): Channel | undefined | null => lastUnreadChannel, (channels: IDMappedObjects<Channel>, members: RelationOneToOne<Channel, ChannelMembership>, teamChannelIds: string[], lastUnreadChannel?: Channel | null): string[] => { const unreadIds = teamChannelIds.filter((id) => { const c = channels[id]; const m = members[id]; if (c && m) { const chHasUnread = c.total_msg_count - m.msg_count > 0; const chHasMention = m.mention_count > 0; if ((m.notify_props && m.notify_props.mark_unread !== 'mention' && chHasUnread) || chHasMention) { return true; } } return false; }); if (lastUnreadChannel && !unreadIds.includes(lastUnreadChannel.id)) { unreadIds.push(lastUnreadChannel.id); } return unreadIds; }, ); export const getUnreadChannels: (state: GlobalState, lastUnreadChannel?: Channel | null) => Channel[] = createIdsSelector( getCurrentUser, getUsers, getUserIdsInChannels, getAllChannels, getUnreadChannelIds, getTeammateNameDisplaySetting, (currentUser, profiles, userIdsInChannels: any, channels, unreadIds, settings) => { // If we receive an unread for a channel and then a mention the channel // won't be sorted correctly until we receive a message in another channel if (!currentUser) { return []; } const allUnreadChannels = unreadIds.filter((id) => channels[id] && channels[id].delete_at === 0).map((id) => { const c = channels[id]; if (c.type === General.DM_CHANNEL || c.type === General.GM_CHANNEL) { return completeDirectChannelDisplayName(currentUser.id, profiles, userIdsInChannels[id], settings!, c); } return c; }); return allUnreadChannels; }, ); export const getMapAndSortedUnreadChannelIds: (state: GlobalState, lastUnreadChannel: Channel, sorting: SortingType) => string[] = createIdsSelector( getUnreadChannels, getCurrentUser, getMyChannelMemberships, getLastPostPerChannel, (state: GlobalState, lastUnreadChannel: Channel, sorting: SortingType = 'alpha') => sorting, (channels, currentUser, myMembers, lastPosts: RelationOneToOne<Channel, Post>, sorting: SortingType) => { return mapAndSortChannelIds(channels, currentUser, myMembers, lastPosts, sorting, true); }, ); export const getSortedUnreadChannelIds: (state: GlobalState, lastUnreadChannel: Channel|null, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType) => string[] = createIdsSelector( getUnreadChannelIds, (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType = 'alpha') => { return getMapAndSortedUnreadChannelIds(state, lastUnreadChannel, sorting); }, (unreadChannelIds, mappedAndSortedUnreadChannelIds) => mappedAndSortedUnreadChannelIds, ); //recent channels export const getAllRecentChannels: (state: GlobalState) => Channel[] = createSelector( getUsers, getCurrentUser, getAllChannels, getUserIdsInChannels, getLastPostPerChannel, getMyChannelMemberships, getChannelIdsForCurrentTeam, getTeammateNameDisplaySetting, (profiles, currentUser: UserProfile, channels: IDMappedObjects<Channel>, userIdsInChannels: any, lastPosts: RelationOneToOne<Channel, Post>, members: RelationOneToOne<Channel, ChannelMembership>, teamChannelIds: string[], settings, ): Channel[] => { const sorting = 'recent'; const recentIds = teamChannelIds.filter((id) => { const c = channels[id]; const m = members[id]; return Boolean(c && m); }); if (!currentUser) { return []; } const Channels = recentIds.filter((id) => channels[id] && channels[id].delete_at === 0).map((id) => { const c = channels[id]; if (c.type === General.DM_CHANNEL || c.type === General.GM_CHANNEL) { return completeDirectChannelDisplayName(currentUser.id, profiles, userIdsInChannels[id], settings!, c); } return c; }); const locale = currentUser.locale || General.DEFAULT_LOCALE; const recentChannels = Channels. sort(sortChannelsByRecencyOrAlpha.bind(null, locale, lastPosts, sorting)); return recentChannels; }, ); // Favorites export const getFavoriteChannels: (state: GlobalState) => Channel[] = createIdsSelector( getCurrentUser, getUsers, getUserIdsInChannels, getAllChannels, getMyChannelMemberships, getFavoritesPreferences, getChannelIdsForCurrentTeam, getTeammateNameDisplaySetting, getConfig, getMyPreferences, getCurrentChannelId, ( currentUser: UserProfile, profiles: IDMappedObjects<UserProfile>, userIdsInChannels: any, channels: IDMappedObjects<Channel>, myMembers: RelationOneToOne<Channel, ChannelMembership>, favoriteIds: string[], teamChannelIds: string[], settings: string, config: ClientConfig, prefs: { [x: string]: PreferenceType; }, currentChannelId: string, ): Channel[] => { if (!currentUser) { return []; } const favoriteChannel = favoriteIds.filter((id) => { if (!myMembers[id] || !channels[id]) { return false; } const channel = channels[id]; const otherUserId = getUserIdFromChannelName(currentUser.id, channel.name); if (channel.delete_at !== 0 && channel.id !== currentChannelId) { return false; } // Deleted users from CLI will not have a profiles entry if (channel.type === General.DM_CHANNEL && !profiles[otherUserId]) { return false; } if (channel.type === General.DM_CHANNEL && !isDirectChannelVisible(profiles[otherUserId] || otherUserId, config, prefs, channel, null, false, currentChannelId)) { return false; } else if (channel.type === General.GM_CHANNEL && !isGroupChannelVisible(config, prefs, channel)) { return false; } return teamChannelIds.includes(id); }).map((id) => { const c = channels[id]; if (c.type === General.DM_CHANNEL || c.type === General.GM_CHANNEL) { return completeDirectChannelDisplayName(currentUser.id, profiles, userIdsInChannels[id], settings, c); } return c; }); return favoriteChannel; }, ); export const getFavoriteChannelIds: (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType) => string[] = createIdsSelector( getFavoriteChannels, getCurrentUser, getMyChannelMemberships, getLastPostPerChannel, (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType = 'alpha') => sorting, mapAndSortChannelIds, ); export const getSortedFavoriteChannelIds: (state: GlobalState, lastUnreadChannel: Channel | null, favoritesAtTop: boolean, unreadsAtTop: boolean, sorting: SortingType) => string[] = createIdsSelector( getUnreadChannelIds, getFavoritesPreferences, (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType) => getFavoriteChannelIds(state, lastUnreadChannel, unreadsAtTop, favoritesAtTop, sorting), (state, lastUnreadChannel, unreadsAtTop = true) => unreadsAtTop, (unreadChannelIds, favoritePreferences, favoriteChannelIds, unreadsAtTop) => { return filterChannels(unreadChannelIds, favoritePreferences, favoriteChannelIds, unreadsAtTop, false); }, ); // Public Channels export const getPublicChannels: (state: GlobalState) => Channel[] = createSelector( getCurrentUser, getAllChannels, getMyChannelMemberships, getChannelIdsForCurrentTeam, (currentUser, channels, myMembers, teamChannelIds) => { if (!currentUser) { return []; } const publicChannels = teamChannelIds.filter((id) => { if (!myMembers[id]) { return false; } const channel = channels[id]; return teamChannelIds.includes(id) && channel.type === General.OPEN_CHANNEL; }).map((id) => channels[id]); return publicChannels; }, ); export const getPublicChannelIds: (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType) => string[] = createIdsSelector( getPublicChannels, getCurrentUser, getMyChannelMemberships, getLastPostPerChannel, (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType = 'alpha') => sorting, mapAndSortChannelIds, ); export const getSortedPublicChannelIds: (state: GlobalState, lastUnreadChannel: Channel | null, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType) => string[] = createIdsSelector( getUnreadChannelIds, getFavoritesPreferences, (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType = 'alpha') => getPublicChannelIds(state, lastUnreadChannel, unreadsAtTop, favoritesAtTop, sorting), (state, lastUnreadChannel, unreadsAtTop = true) => unreadsAtTop, (state, lastUnreadChannel, unreadsAtTop, favoritesAtTop = true) => favoritesAtTop, filterChannels, ); // Private Channels export const getPrivateChannels: (a: GlobalState) => Channel[] = createSelector( getCurrentUser, getAllChannels, getMyChannelMemberships, getChannelIdsForCurrentTeam, (currentUser, channels, myMembers, teamChannelIds) => { if (!currentUser) { return []; } const privateChannels = teamChannelIds.filter((id) => { if (!myMembers[id]) { return false; } const channel = channels[id]; return teamChannelIds.includes(id) && channel.type === General.PRIVATE_CHANNEL; }).map((id) => channels[id]); return privateChannels; }, ); export const getPrivateChannelIds: (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType) => string[] = createIdsSelector( getPrivateChannels, getCurrentUser, getMyChannelMemberships, getLastPostPerChannel, (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType = 'alpha') => sorting, mapAndSortChannelIds, ); export const getSortedPrivateChannelIds: (state: GlobalState, lastUnreadChannel: Channel | null, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType) => string[] = createIdsSelector( getUnreadChannelIds, getFavoritesPreferences, (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType = 'alpha') => getPrivateChannelIds(state, lastUnreadChannel, unreadsAtTop, favoritesAtTop, sorting), (state, lastUnreadChannel, unreadsAtTop = true) => unreadsAtTop, (state, lastUnreadChannel, unreadsAtTop, favoritesAtTop = true) => favoritesAtTop, filterChannels, ); // Direct Messages export const getDirectChannels: (state: GlobalState) => Channel[] = createSelector( getCurrentUser, getUsers, getUserIdsInChannels, getAllChannels, getVisibleTeammate, getVisibleGroupIds, getTeammateNameDisplaySetting, getConfig, getMyPreferences, getLastPostPerChannel, getCurrentChannelId, ( currentUser: UserProfile, profiles: IDMappedObjects<UserProfile>, userIdsInChannels: any, channels: IDMappedObjects<Channel>, teammates: string[], groupIds: string[], settings, config, preferences: { [x: string]: PreferenceType; }, lastPosts: RelationOneToOne<Channel, Post>, currentChannelId: string, ): Channel[] => { if (!currentUser) { return []; } const channelValues = Object.keys(channels).map((key) => channels[key]); const directChannelsIds: string[] = []; teammates.reduce((result, teammateId) => { const name = getDirectChannelName(currentUser.id, teammateId); const channel = channelValues.find((c: Channel) => c && c.name === name); //eslint-disable-line max-nested-callbacks if (channel) { const lastPost = lastPosts[channel.id]; const otherUser = profiles[getUserIdFromChannelName(currentUser.id, channel.name)]; if (!isAutoClosed(config, preferences, channel, lastPost ? lastPost.create_at : 0, otherUser ? otherUser.delete_at : 0, currentChannelId)) { result.push(channel.id); } } return result; }, directChannelsIds); const directChannels = groupIds.filter((id) => { const channel = channels[id]; if (channel && (channel.type === General.DM_CHANNEL || channel.type === General.GM_CHANNEL)) { const lastPost = lastPosts[channel.id]; return !isAutoClosed(config, preferences, channels[id], lastPost ? lastPost.create_at : 0, 0, currentChannelId); } return false; }).concat(directChannelsIds).map((id) => { const channel = channels[id]; return completeDirectChannelDisplayName(currentUser.id, profiles, userIdsInChannels[id], settings!, channel); }); return directChannels; }, ); // getDirectAndGroupChannels returns all direct and group channels, even if they have been manually // or automatically closed. // // This is similar to the getDirectChannels above (which actually also returns group channels, // but suppresses manually closed group channels but not manually closed direct channels.) This // method does away with all the suppression, since the webapp client downstream uses this for // the channel switcher and puts such suppressed channels in a separate category. export const getDirectAndGroupChannels: (a: GlobalState) => Channel[] = createSelector( getCurrentUser, getUsers, getUserIdsInChannels, getAllChannels, getTeammateNameDisplaySetting, (currentUser: UserProfile, profiles: IDMappedObjects<UserProfile>, userIdsInChannels: any, channels: IDMappedObjects<Channel>, settings): Channel[] => { if (!currentUser) { return []; } return Object.keys(channels). map((key) => channels[key]). filter((channel: Channel): boolean => Boolean(channel)). filter((channel: Channel): boolean => channel.type === General.DM_CHANNEL || channel.type === General.GM_CHANNEL). map((channel: Channel): Channel => completeDirectChannelDisplayName(currentUser.id, profiles, userIdsInChannels[channel.id], settings!, channel)); }, ); export const getDirectChannelIds: (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType) => string[] = createIdsSelector( getDirectChannels, getCurrentUser, getMyChannelMemberships, getLastPostPerChannel, (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType = 'alpha') => sorting, (directChannels, currentUser, myChannelMemberships, lastPostPerChannel, sorting) => { return mapAndSortChannelIds(directChannels, currentUser, myChannelMemberships, lastPostPerChannel, sorting); }, ); export const getSortedDirectChannelIds: (state: GlobalState, lastUnreadChannel: Channel | null, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType) => Array<$ID<Channel>> = createIdsSelector( getUnreadChannelIds, getFavoritesPreferences, (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType = 'alpha') => getDirectChannelIds(state, lastUnreadChannel, unreadsAtTop, favoritesAtTop, sorting), (state, lastUnreadChannel, unreadsAtTop = true) => unreadsAtTop, (state, lastUnreadChannel, unreadsAtTop, favoritesAtTop = true) => favoritesAtTop, (unreadChannelIds, favoritesPreferences, directChannelIds, unreadsAtTop, favoritesAtTop) => { return filterChannels(unreadChannelIds, favoritesPreferences, directChannelIds, unreadsAtTop, favoritesAtTop); }, ); const getProfiles = (currentUserId: string, usersIdsInChannel: string[], users: IDMappedObjects<UserProfile>): UserProfile[] => { const profiles: UserProfile[] = []; usersIdsInChannel.forEach((userId) => { if (userId !== currentUserId) { profiles.push(users[userId]); } }); return profiles; }; export const getChannelsWithUserProfiles: (state: GlobalState) => Array<{ profiles: UserProfile[]; } & Channel> = createSelector( getUserIdsInChannels, getUsers, getGroupChannels, getCurrentUserId, (channelUserMap: RelationOneToMany<Channel, UserProfile>, users: IDMappedObjects<UserProfile>, channels: Channel[], currentUserId: string) => { return channels.map((channel: Channel): { profiles: UserProfile[]; } & Channel => { const profiles = getProfiles(currentUserId, channelUserMap[channel.id] || [], users); return { ...channel, profiles, }; }); }, ); const getAllActiveChannels = createSelector( getPublicChannels, getPrivateChannels, getDirectChannels, (publicChannels, privateChannels, directChannels) => { const allChannels = [...publicChannels, ...privateChannels, ...directChannels]; return allChannels; }, ); export const getAllChannelIds: (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType) => string[] = createIdsSelector( getAllActiveChannels, getCurrentUser, getMyChannelMemberships, getLastPostPerChannel, (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType = 'alpha') => sorting, mapAndSortChannelIds, ); export const getAllSortedChannelIds: (state: GlobalState, lastUnreadChannel: Channel | null, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType) => string[] = createIdsSelector( getUnreadChannelIds, getFavoritesPreferences, (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType = 'alpha') => getAllChannelIds(state, lastUnreadChannel, unreadsAtTop, favoritesAtTop, sorting), (state, lastUnreadChannel, unreadsAtTop = true) => unreadsAtTop, (state, lastUnreadChannel, unreadsAtTop, favoritesAtTop = true) => favoritesAtTop, filterChannels, ); type ChannelsByCategory = { type: string; name: string; items: string[]; }; let lastChannels: ChannelsByCategory[]; const haveChannelsChanged = (channels: ChannelsByCategory[]) => { if (!lastChannels || lastChannels.length !== channels.length) { return true; } for (let i = 0; i < channels.length; i++) { if (channels[i].type !== lastChannels[i].type || channels[i].items !== lastChannels[i].items) { return true; } } return false; }; export const getOrderedChannelIds = (state: GlobalState, lastUnreadChannel: Channel|null, grouping: 'by_type' | 'none', sorting: SortingType, unreadsAtTop: boolean, favoritesAtTop: boolean) => { const channels: ChannelsByCategory[] = []; if (grouping === 'by_type') { channels.push({ type: 'public', name: 'PUBLIC CHANNELS', items: getSortedPublicChannelIds(state, lastUnreadChannel, unreadsAtTop, favoritesAtTop, sorting), }); channels.push({ type: 'private', name: 'PRIVATE CHANNELS', items: getSortedPrivateChannelIds(state, lastUnreadChannel, unreadsAtTop, favoritesAtTop, sorting), }); channels.push({ type: 'direct', name: 'DIRECT MESSAGES', items: getSortedDirectChannelIds(state, lastUnreadChannel, unreadsAtTop, favoritesAtTop, sorting), }); } else { // Combine all channel types let type = 'alpha'; let name = 'CHANNELS'; if (sorting === 'recent') { type = 'recent'; name = 'RECENT ACTIVITY'; } channels.push({ type, name, items: getAllSortedChannelIds(state, lastUnreadChannel, unreadsAtTop, favoritesAtTop, sorting), }); } if (favoritesAtTop) { channels.unshift({ type: 'favorite', name: 'FAVORITE CHANNELS', items: getSortedFavoriteChannelIds(state, lastUnreadChannel, unreadsAtTop, favoritesAtTop, sorting), }); } if (unreadsAtTop) { channels.unshift({ type: 'unreads', name: 'UNREADS', items: getSortedUnreadChannelIds(state, lastUnreadChannel, unreadsAtTop, favoritesAtTop, sorting), }); } if (haveChannelsChanged(channels)) { lastChannels = channels; } return lastChannels; }; export const getDefaultChannelForTeams: (state: GlobalState) => RelationOneToOne<Team, Channel> = createSelector( getAllChannels, (channels: IDMappedObjects<Channel>): RelationOneToOne<Team, Channel> => { const result: RelationOneToOne<Team, Channel> = {}; for (const channel of Object.keys(channels).map((key) => channels[key])) { if (channel && channel.name === General.DEFAULT_CHANNEL) { result[channel.team_id] = channel; } } return result; }, ); export const getMyFirstChannelForTeams: (state: GlobalState) => RelationOneToOne<Team, Channel> = createSelector( getAllChannels, getMyChannelMemberships, getMyTeams, getCurrentUser, (allChannels: IDMappedObjects<Channel>, myChannelMemberships: RelationOneToOne<Channel, ChannelMembership>, myTeams: Team[], currentUser: UserProfile): RelationOneToOne<Team, Channel> => { const locale = currentUser.locale || General.DEFAULT_LOCALE; const result: RelationOneToOne<Team, Channel> = {}; for (const team of myTeams) { // Get a sorted array of all channels in the team that the current user is a member of const teamChannels = Object.values(allChannels).filter((channel: Channel) => channel && channel.team_id === team.id && Boolean(myChannelMemberships[channel.id])).sort(sortChannelsByDisplayName.bind(null, locale)); if (teamChannels.length === 0) { continue; } result[team.id] = teamChannels[0]; } return result; }, ); export const getRedirectChannelNameForTeam = (state: GlobalState, teamId: string): string => { const defaultChannelForTeam = getDefaultChannelForTeams(state)[teamId]; const myFirstChannelForTeam = getMyFirstChannelForTeams(state)[teamId]; const canIJoinPublicChannelsInTeam = !hasNewPermissions(state) || haveITeamPermission(state, {