mattermost-mobile/app/mm-redux/selectors/entities/channels.ts

932 lines
48 KiB
TypeScript

// 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 {getCurrentChannelId, getCurrentUser, getUsers, getMyChannelMemberships, getMyCurrentChannelMembership} from '@mm-redux/selectors/entities/common';
import {getConfig, getLicense, hasNewPermissions} from '@mm-redux/selectors/entities/general';
import {getFavoritesPreferences, getMyPreferences, getTeammateNameDisplaySetting, getVisibleTeammate, getVisibleGroupIds} from '@mm-redux/selectors/entities/preferences';
import {getLastPostPerChannel, getAllPosts} from '@mm-redux/selectors/entities/posts';
import {getCurrentTeamId, getCurrentTeamMembership, getMyTeams, getTeamMemberships} from '@mm-redux/selectors/entities/teams';
import {haveICurrentChannelPermission, haveIChannelPermission, haveITeamPermission} from '@mm-redux/selectors/entities/roles';
import {isCurrentUserSystemAdmin, getCurrentUserId} from '@mm-redux/selectors/entities/users';
import {buildDisplayableChannelListWithUnreadSection, canManageMembersOldPermissions, completeDirectChannelInfo, completeDirectChannelDisplayName, getUserIdFromChannelName, getChannelByName as getChannelByNameHelper, isChannelMuted, getDirectChannelName, isAutoClosed, isDirectChannelVisible, isGroupChannelVisible, isGroupOrDirectChannelVisible, sortChannelsByDisplayName, isFavoriteChannel, isDefault, sortChannelsByRecency} from '@mm-redux/utils/channel_utils';
import {createIdsSelector} from '@mm-redux/utils/helpers';
export {getCurrentChannelId, getMyChannelMemberships, getMyCurrentChannelMembership};
import {GlobalState} from '@mm-redux/types/store';
import {Channel, ChannelStats, ChannelMembership, ChannelModeration} from '@mm-redux/types/channels';
import {UsersState, UserProfile} from '@mm-redux/types/users';
import {PreferenceType} from '@mm-redux/types/preferences';
import {Post} from '@mm-redux/types/posts';
import {TeamMembership, Team} from '@mm-redux/types/teams';
import {NameMappedObjects, UserIDMappedObjects, IDMappedObjects, RelationOneToOne, RelationOneToMany} from '@mm-redux/types/utilities';
import {getUserIdsInChannels} from './users';
import {Config} from '@mm-redux/types/config';
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: (a: GlobalState) => Set<string> = createSelector(getChannelsInTeam, (channelsInTeam: RelationOneToMany<Team, Channel>): Set<string> => {
return channelsInTeam && new Set(channelsInTeam['']) || new Set();
});
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: Array<Channel>, currentUser: UserProfile, myMembers: RelationOneToOne<Channel, ChannelMembership>, lastPosts: RelationOneToOne<Channel, Post>, sorting: SortingType, sortMentionsFirst = false): Array<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: Array<string>, favoriteIds: Array<string>, channelIds: Array<string>, unreadsAtTop: boolean, favoritesAtTop: boolean): Array<string> {
let channels: Array<string> = channelIds;
if (unreadsAtTop) {
channels = channels.filter((id) => {
return !unreadIds.includes(id);
});
}
if (favoritesAtTop) {
channels = channels.filter((id) => {
return !favoriteIds.includes(id);
});
}
return channels;
}
export function makeGetChannel(): (b: GlobalState, a: {
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;
});
}
export const getChannel: (b: GlobalState, a: string) => Channel = createSelector(getAllChannels, (state: GlobalState, id: string): string => id, (state: GlobalState): UsersState => state.entities.users, getTeammateNameDisplaySetting, (allChannels: IDMappedObjects<Channel>, channelId: string, users: UsersState, teammateNameDisplay: string): Channel => {
const channel = allChannels[channelId];
if (channel) {
return completeDirectChannelInfo(users, teammateNameDisplay, channel);
}
return channel;
});
export const getCurrentChannel: (a: 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: (b: GlobalState, a: 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: (a: GlobalState) => ChannelStats = createSelector(getAllChannelStats, getCurrentChannelId, (allChannelStats: RelationOneToOne<Channel, ChannelStats>, currentChannelId: string): ChannelStats => {
return allChannelStats[currentChannelId];
});
export const isCurrentChannelFavorite: (a: GlobalState) => boolean = createSelector(getMyPreferences, getCurrentChannelId, (preferences: {
[x: string]: PreferenceType;
}, channelId: string): boolean => isFavoriteChannel(preferences, channelId));
export const isCurrentChannelMuted: (a: GlobalState) => boolean = createSelector(getMyCurrentChannelMembership, (membership?: ChannelMembership | null): boolean => {
if (!membership) {
return false;
}
return isChannelMuted(membership);
});
export const isCurrentChannelArchived: (a: GlobalState) => boolean = createSelector(getCurrentChannel, (channel: Channel): boolean => channel.delete_at !== 0);
export const isCurrentChannelDefault: (a: 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: (a: 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: (a: GlobalState) => Array<string> = createSelector(getCurrentTeamId, getChannelsInTeam, (currentTeamId: string, channelsInTeam: RelationOneToMany<Team, Channel>): Array<string> => {
return channelsInTeam && channelsInTeam[currentTeamId] || [];
});
function sortAndInjectChannels(channels: IDMappedObjects<Channel>, channelSet: Array<string>, locale: string): Array<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: (a: GlobalState) => Array<Channel> = createSelector(
getAllChannels,
getChannelSetInCurrentTeam,
getCurrentUser,
(channels: IDMappedObjects<Channel>, currentTeamChannelSet: Array<string>, currentUser: UserProfile): Array<Channel> => {
let locale = General.DEFAULT_LOCALE;
if (currentUser && currentUser.locale) {
locale = currentUser.locale;
}
return sortAndInjectChannels(channels, currentTeamChannelSet, locale);
}
);
export const getChannelsNameMapInTeam: (b: GlobalState, a: 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: (a: GlobalState) => NameMappedObjects<Channel> = createSelector(getAllChannels, getChannelSetInCurrentTeam, (channels: IDMappedObjects<Channel>, currentTeamChannelSet: Array<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: (a: GlobalState) => Array<Channel> = createSelector(getAllChannels, getDirectChannelsSet, (state: GlobalState): UsersState => state.entities.users, getTeammateNameDisplaySetting, (channels: IDMappedObjects<Channel>, channelSet: Set<string>, users: UsersState, teammateNameDisplay: string): Array<Channel> => {
const dmChannels: Channel[] = [];
channelSet.forEach((c) => {
dmChannels.push(completeDirectChannelInfo(users, teammateNameDisplay, channels[c]));
});
return dmChannels;
});
// Returns only GMs
export const getGroupChannels: (a: GlobalState) => Array<Channel> = createSelector(getAllChannels, getDirectChannelsSet, (state: GlobalState): UsersState => state.entities.users, getTeammateNameDisplaySetting, (channels: IDMappedObjects<Channel>, channelSet: Set<string>, users: UsersState, teammateNameDisplay: string): Array<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: (a: GlobalState) => Array<Channel> = createSelector(
getChannelsInCurrentTeam,
getAllDirectChannels,
getMyChannelMemberships,
(channels: Array<Channel>, directChannels: Array<Channel>, myMembers: RelationOneToOne<Channel, ChannelMembership>): Array<Channel> => {
return [...channels, ...directChannels].filter((c) => myMembers.hasOwnProperty(c.id));
}
);
export const getOtherChannels: (b: GlobalState, a?: boolean | null) => Array<Channel> = createSelector(getChannelsInCurrentTeam, getMyChannelMemberships, (state: GlobalState, archived: boolean | undefined | null = true) => archived, (channels: Array<Channel>, myMembers: RelationOneToOne<Channel, ChannelMembership>, archived?: boolean | null): Array<Channel> => {
return channels.filter((c) => !myMembers.hasOwnProperty(c.id) && c.type === General.OPEN_CHANNEL && (archived ? true : c.delete_at === 0));
});
export const getArchivedChannels: (a: GlobalState) => Array<Channel> = createSelector(getChannelsInCurrentTeam, getMyChannelMemberships, (channels: Array<Channel>, myMembers: RelationOneToOne<Channel, ChannelMembership>): Array<Channel> => {
return channels.filter((c) => myMembers.hasOwnProperty(c.id) && c.delete_at !== 0);
});
export const getChannelsWithUnreadSection: (a: GlobalState) => {
unreadChannels: Array<Channel>;
favoriteChannels: Array<Channel>;
publicChannels: Array<Channel>;
privateChannels: Array<Channel>;
directAndGroupChannels: Array<Channel>;
} = createSelector(
getCurrentChannelId,
getMyChannels,
getMyChannelMemberships,
getConfig,
getMyPreferences,
getTeammateNameDisplaySetting,
(state: GlobalState): UsersState => state.entities.users,
getLastPostPerChannel,
(
currentChannelId: string,
channels: Array<Channel>,
myMembers: RelationOneToOne<Channel, ChannelMembership>,
config: Config,
myPreferences: {
[x: string]: PreferenceType;
},
teammateNameDisplay: string,
usersState: UsersState,
lastPosts: RelationOneToOne<Channel, Post>
) => {
const allChannels = channels.map((c) => {
const channel = {...c,
};
channel.isCurrent = c.id === currentChannelId;
return channel;
});
return buildDisplayableChannelListWithUnreadSection(usersState, allChannels, myMembers, config, myPreferences, teammateNameDisplay, lastPosts);
}
);
export const getDefaultChannel: (a: 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: (a: GlobalState) => UserIDMappedObjects<ChannelMembership> = createSelector(getCurrentChannelId, getChannelMembersInChannels, (currentChannelId: string, members: RelationOneToOne<Channel, UserIDMappedObjects<ChannelMembership>>): UserIDMappedObjects<ChannelMembership> => {
return members[currentChannelId];
});
export const getUnreads: (a: 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: Array<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: Array<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: (a: 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: Config, 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: (a: 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: (a: GlobalState) => Array<string> = createIdsSelector(getDirectChannelsSet, (directIds: Set<string>): Array<string> => {
return Array.from(directIds);
});
export const getChannelIdsInCurrentTeam: (a: GlobalState) => Array<string> = createIdsSelector(getCurrentTeamId, getChannelsInTeam, (currentTeamId: string, channelsInTeam: RelationOneToMany<Team, Channel>): Array<string> => {
return Array.from(channelsInTeam[currentTeamId] || []);
});
export const getChannelIdsForCurrentTeam: (a: GlobalState) => Array<string> = createIdsSelector(getChannelIdsInCurrentTeam, getAllDirectChannelIds, (channels, direct) => {
return [...channels, ...direct];
});
export const getUnreadChannelIds: (b: GlobalState, a?: Channel | null) => Array<string> = createIdsSelector(getAllChannels, getMyChannelMemberships, getChannelIdsForCurrentTeam, (state: GlobalState, lastUnreadChannel: Channel | undefined | null = null): Channel | undefined | null => lastUnreadChannel, (channels: IDMappedObjects<Channel>, members: RelationOneToOne<Channel, ChannelMembership>, teamChannelIds: Array<string>, lastUnreadChannel?: Channel | null): Array<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: (b: GlobalState, a?: Channel | null) => Array<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: (c: GlobalState, b: Channel, a: SortingType) => Array<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: (e: GlobalState, d: Channel|null, c: boolean, b: boolean, a: SortingType) => Array<string> = createIdsSelector(getUnreadChannelIds, (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType = 'alpha') => {
return getMapAndSortedUnreadChannelIds(state, lastUnreadChannel, sorting);
}, (unreadChannelIds, mappedAndSortedUnreadChannelIds) => mappedAndSortedUnreadChannelIds); // Favorites
export const getFavoriteChannels: (a: GlobalState) => Array<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: Array<string>, teamChannelIds: Array<string>, settings: string, config: Config, prefs: {
[x: string]: PreferenceType;
}, currentChannelId: string): Array<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: (e: GlobalState, d: Channel, c: boolean, b: boolean, a: SortingType) => Array<string> = createIdsSelector(getFavoriteChannels, getCurrentUser, getMyChannelMemberships, getLastPostPerChannel, (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType = 'alpha') => sorting, mapAndSortChannelIds);
export const getSortedFavoriteChannelIds: (e: GlobalState, d: Channel | null, c: boolean, b: boolean, a: SortingType) => Array<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: (a: GlobalState) => Array<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: (e: GlobalState, d: Channel, c: boolean, b: boolean, a: SortingType) => Array<string> = createIdsSelector(
getPublicChannels,
getCurrentUser,
getMyChannelMemberships,
getLastPostPerChannel,
(state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType = 'alpha') => sorting,
mapAndSortChannelIds
);
export const getSortedPublicChannelIds: (e: GlobalState, d: Channel | null, c: boolean, b: boolean, a: SortingType) => Array<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) => Array<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: (e: GlobalState, d: Channel, c: boolean, b: boolean, a: SortingType) => Array<string> = createIdsSelector(getPrivateChannels, getCurrentUser, getMyChannelMemberships, getLastPostPerChannel, (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType = 'alpha') => sorting, mapAndSortChannelIds);
export const getSortedPrivateChannelIds: (e: GlobalState, d: Channel | null, c: boolean, b: boolean, a: SortingType) => Array<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: (a: GlobalState) => Array<Channel> = createSelector(getCurrentUser, getUsers, getUserIdsInChannels, getAllChannels, getVisibleTeammate, getVisibleGroupIds, getTeammateNameDisplaySetting, getConfig, getMyPreferences, getLastPostPerChannel, getCurrentChannelId, (currentUser: UserProfile, profiles: IDMappedObjects<UserProfile>, userIdsInChannels: any, channels: IDMappedObjects<Channel>, teammates: Array<string>, groupIds: Array<string>, settings, config, preferences: {
[x: string]: PreferenceType;
}, lastPosts: RelationOneToOne<Channel, Post>, currentChannelId: string): Array<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) => Array<Channel> = createSelector(getCurrentUser, getUsers, getUserIdsInChannels, getAllChannels, getTeammateNameDisplaySetting, (currentUser: UserProfile, profiles: IDMappedObjects<UserProfile>, userIdsInChannels: any, channels: IDMappedObjects<Channel>, settings): Array<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: (e: GlobalState, d: Channel, c: boolean, b: boolean, a: SortingType) => Array<string> = createIdsSelector(getDirectChannels, getCurrentUser, getMyChannelMemberships, getLastPostPerChannel, (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType = 'alpha') => sorting, mapAndSortChannelIds);
export const getSortedDirectChannelIds: (e: GlobalState, d: Channel | null, c: boolean, b: boolean, a: SortingType) => Array<string> = 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, filterChannels);
export function getGroupOrDirectChannelVisibility(state: GlobalState, channelId: string): boolean {
return isGroupOrDirectChannelVisible(getChannel(state, channelId), getMyChannelMemberships(state), getConfig(state), getMyPreferences(state), getCurrentUser(state).id, getUsers(state), getLastPostPerChannel(state));
}
// Filters post IDs by the given condition.
// The condition function receives as parameters the associated channel object and the post object.
export const filterPostIds = (condition: (b: Channel, a: Post) => boolean) => {
if (typeof condition !== 'function') {
throw new TypeError(`${condition} is not a function`);
}
return (createSelector(getAllChannels, getAllPosts, (state: GlobalState, postIds: Array<string>): Array<string> => postIds, (channels: IDMappedObjects<Channel>, posts: IDMappedObjects<Post>, postIds: Array<string>): Array<string> => {
return postIds.filter((postId) => {
const post = posts[postId];
let channel;
if (post) {
channel = channels[post.channel_id];
}
return post && channel && condition(channel, post);
});
}) as (b: GlobalState, a: Array<string>) => Array<string>);
};
const getProfiles = (currentUserId: string, usersIdsInChannel: Array<string>, users: IDMappedObjects<UserProfile>): Array<UserProfile> => {
const profiles: UserProfile[] = [];
usersIdsInChannel.forEach((userId) => {
if (userId !== currentUserId) {
profiles.push(users[userId]);
}
});
return profiles;
};
export const getChannelsWithUserProfiles: (a: GlobalState) => Array<{
profiles: Array<UserProfile>;
} & Channel> = createSelector(getUserIdsInChannels, getUsers, getGroupChannels, getCurrentUserId, (channelUserMap: RelationOneToMany<Channel, UserProfile>, users: IDMappedObjects<UserProfile>, channels: Array<Channel>, currentUserId: string) => {
return channels.map((channel: Channel): {
profiles: Array<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: (e: GlobalState, d: Channel, c: boolean, b: boolean, a: SortingType) => Array<string> = createIdsSelector(getAllActiveChannels, getCurrentUser, getMyChannelMemberships, getLastPostPerChannel, (state: GlobalState, lastUnreadChannel: Channel, unreadsAtTop: boolean, favoritesAtTop: boolean, sorting: SortingType = 'alpha') => sorting, mapAndSortChannelIds);
export const getAllSortedChannelIds: (e: GlobalState, d: Channel | null, c: boolean, b: boolean, a: SortingType) => Array<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);
let lastChannels: {type: string;name: string;items: string[]}[];
const hasChannelsChanged = (channels: {type: string;name: string;items: string[]}[]) => {
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: {type: string;name: string;items: string[]}[] = [];
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 (hasChannelsChanged(channels)) {
lastChannels = channels;
}
return lastChannels;
};
// Added for backwards compatibility
// Can be removed once webapp includes new sidebar preferences
export const getSortedPublicChannelWithUnreadsIds: (e: GlobalState, d: Channel, c: boolean, b: boolean, a: SortingType) => Array<string> = createIdsSelector(getUnreadChannelIds, getFavoritesPreferences, getPublicChannelIds, (state, lastUnreadChannel, unreadsAtTop, favoritesAtTop = true) => favoritesAtTop, (unreadChannelIds, favoritePreferences, publicChannelIds, favoritesAtTop) => {
return filterChannels(unreadChannelIds, favoritePreferences, publicChannelIds, false, favoritesAtTop);
});
export const getSortedPrivateChannelWithUnreadsIds: (e: GlobalState, d: Channel, c: boolean, b: boolean, a: SortingType) => Array<string> = createIdsSelector(getUnreadChannelIds, getFavoritesPreferences, getPrivateChannelIds, (state, lastUnreadChannel, unreadsAtTop, favoritesAtTop = true) => favoritesAtTop, (unreadChannelIds, favoritePreferences, privateChannelId, favoritesAtTop) => {
return filterChannels(unreadChannelIds, favoritePreferences, privateChannelId, false, favoritesAtTop);
});
export const getSortedFavoriteChannelWithUnreadsIds: (e: GlobalState, d: Channel, c: boolean, b: boolean, a: SortingType) => Array<string> = createIdsSelector(getUnreadChannelIds, getFavoriteChannelIds, (unreadChannelIds, favoriteChannelIds) => favoriteChannelIds);
export const getSortedDirectChannelWithUnreadsIds: (e: GlobalState, d: Channel, c: boolean, b: boolean, a: SortingType) => Array<string> = createIdsSelector(getUnreadChannelIds, getFavoritesPreferences, getDirectChannelIds, (state, lastUnreadChannel, unreadsAtTop, favoritesAtTop = true) => favoritesAtTop, (unreadChannelIds, favoritePreferences, directChannelIds, favoritesAtTop) => {
return filterChannels(unreadChannelIds, favoritePreferences, directChannelIds, false, favoritesAtTop);
});
export const getDefaultChannelForTeams: (a: 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: (a: GlobalState) => RelationOneToOne<Team, Channel> = createSelector(getAllChannels, getMyChannelMemberships, getMyTeams, getCurrentUser, (allChannels: IDMappedObjects<Channel>, myChannelMemberships: RelationOneToOne<Channel, ChannelMembership>, myTeams: Array<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, {
team: teamId,
permission: Permissions.JOIN_PUBLIC_CHANNELS,
});
const myChannelMemberships = getMyChannelMemberships(state);
const iAmMemberOfTheTeamDefaultChannel = Boolean(defaultChannelForTeam && myChannelMemberships[defaultChannelForTeam.id]);
if (iAmMemberOfTheTeamDefaultChannel || canIJoinPublicChannelsInTeam) {
return General.DEFAULT_CHANNEL;
}
return myFirstChannelForTeam && myFirstChannelForTeam.name || General.DEFAULT_CHANNEL;
};
// isManually unread looks into state if the provided channelId is marked as unread by the user.
export function isManuallyUnread(state: GlobalState, channelId?: string): boolean {
if (!channelId) {
return false;
}
return Boolean(state.entities.channels.manuallyUnread[channelId]);
}
export function getChannelModerations(state: GlobalState, channelId: string): Array<ChannelModeration> {
return state.entities.channels.channelModerations[channelId];
}