UNPKG

mattermost-redux

Version:

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

612 lines (509 loc) 22.1 kB
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import {General, Preferences, Permissions, Users} from '../constants'; import {MarkUnread} from 'constants/channels'; import {hasNewPermissions} from 'selectors/entities/general'; import {haveITeamPermission, haveIChannelPermission} from 'selectors/entities/roles'; import {Channel, ChannelMembership, ChannelType, ChannelNotifyProps} from 'types/channels'; import {Post} from 'types/posts'; import {UserProfile, UsersState, UserNotifyProps} from 'types/users'; import {GlobalState} from 'types/store'; import {TeamMembership} from 'types/teams'; import {PreferenceType} from 'types/preferences'; import {RelationOneToOne, IDMappedObjects} from 'types/utilities'; import {getPreferenceKey} from './preference_utils'; import {displayUsername} from './user_utils'; const channelTypeOrder = { [General.OPEN_CHANNEL]: 0, [General.PRIVATE_CHANNEL]: 1, [General.DM_CHANNEL]: 2, [General.GM_CHANNEL]: 3, }; export function completeDirectChannelInfo(usersState: UsersState, teammateNameDisplay: string, channel: Channel): Channel { if (isDirectChannel(channel)) { const teammateId = getUserIdFromChannelName(usersState.currentUserId, channel.name); // return empty string instead of `someone` default string for display_name return { ...channel, display_name: displayUsername(usersState.profiles[teammateId], teammateNameDisplay, false), teammate_id: teammateId, status: usersState.statuses[teammateId] || 'offline', }; } else if (isGroupChannel(channel)) { return completeDirectGroupInfo(usersState, teammateNameDisplay, channel); } return channel; } export function completeDirectChannelDisplayName(currentUserId: string, profiles: IDMappedObjects<UserProfile>, userIdsInChannel: Set<string>, teammateNameDisplay: string, channel: Channel): Channel { if (isDirectChannel(channel)) { const dmChannelClone = {...channel}; const teammateId = getUserIdFromChannelName(currentUserId, channel.name); return Object.assign(dmChannelClone, {display_name: displayUsername(profiles[teammateId], teammateNameDisplay)}); } else if (isGroupChannel(channel) && userIdsInChannel && userIdsInChannel.size > 0) { const displayName = getGroupDisplayNameFromUserIds(Array.from(userIdsInChannel), profiles, currentUserId, teammateNameDisplay); return {...channel, display_name: displayName}; } return channel; } export function cleanUpUrlable(input: string): string { let cleaned = input.trim().replace(/-/g, ' ').replace(/[^\w\s]/gi, '').toLowerCase().replace(/\s/g, '-'); cleaned = cleaned.replace(/-{2,}/, '-'); cleaned = cleaned.replace(/^-+/, ''); cleaned = cleaned.replace(/-+$/, ''); return cleaned; } export function getChannelByName(channels: IDMappedObjects<Channel>, name: string): Channel | undefined | null { const channelIds = Object.keys(channels); for (let i = 0; i < channelIds.length; i++) { const id = channelIds[i]; if (channels[id].name === name) { return channels[id]; } } return null; } export function getDirectChannelName(id: string, otherId: string): string { let handle; if (otherId > id) { handle = id + '__' + otherId; } else { handle = otherId + '__' + id; } return handle; } export function getUserIdFromChannelName(userId: string, channelName: string): string { const ids = channelName.split('__'); let otherUserId = ''; if (ids[0] === userId) { otherUserId = ids[1]; } else { otherUserId = ids[0]; } return otherUserId; } export function isAutoClosed( config: any, myPreferences: { [x: string]: PreferenceType; }, channel: Channel, channelActivity: number, channelArchiveTime: number, currentChannelId = '', now = Date.now(), ): boolean { const cutoff = now - (7 * 24 * 60 * 60 * 1000); const viewTimePref = myPreferences[`${Preferences.CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME}--${channel.id}`]; const viewTime = viewTimePref ? parseInt(viewTimePref.value!, 10) : 0; // Note that viewTime is not set correctly at the time of writing if (viewTime > cutoff) { return false; } const openTimePref = myPreferences[`${Preferences.CATEGORY_CHANNEL_OPEN_TIME}--${channel.id}`]; const openTime = openTimePref ? parseInt(openTimePref.value!, 10) : 0; // Only close archived channels when not being viewed if (channel.id !== currentChannelId && channelArchiveTime && channelArchiveTime > openTime) { return true; } if (config.CloseUnusedDirectMessages !== 'true' || isFavoriteChannelOld(myPreferences, channel.id)) { return false; } const autoClose = myPreferences[getPreferenceKey(Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_AUTOCLOSE_DMS)]; if (!autoClose || autoClose.value === Preferences.AUTOCLOSE_DMS_ENABLED) { if (channelActivity && channelActivity > cutoff) { return false; } if (openTime > cutoff) { return false; } const lastActivity = channel.last_post_at; return !lastActivity || lastActivity < cutoff; } return false; } export function isDirectChannel(channel: Channel): boolean { return channel.type === General.DM_CHANNEL; } export function isDirectChannelVisible( otherUserOrOtherUserId: UserProfile | string, config: any, myPreferences: { [x: string]: PreferenceType; }, channel: Channel, lastPost?: Post | null, isUnread?: boolean, currentChannelId = '', now?: number, ): boolean { const otherUser = typeof otherUserOrOtherUserId === 'object' ? otherUserOrOtherUserId : null; const otherUserId = typeof otherUserOrOtherUserId === 'object' ? otherUserOrOtherUserId.id : otherUserOrOtherUserId; const dm = myPreferences[`${Preferences.CATEGORY_DIRECT_CHANNEL_SHOW}--${otherUserId}`]; if (!dm || dm.value !== 'true') { return false; } return isUnread || !isAutoClosed( config, myPreferences, channel, lastPost ? lastPost.create_at : 0, otherUser ? otherUser.delete_at : 0, currentChannelId, now, ); } export function isGroupChannel(channel: Channel): boolean { return channel.type === General.GM_CHANNEL; } export function isGroupChannelVisible( config: any, myPreferences: { [x: string]: PreferenceType; }, channel: Channel, lastPost?: Post, isUnread?: boolean, now?: number, ): boolean { const gm = myPreferences[`${Preferences.CATEGORY_GROUP_CHANNEL_SHOW}--${channel.id}`]; if (!gm || gm.value !== 'true') { return false; } return isUnread || !isAutoClosed( config, myPreferences, channel, lastPost ? lastPost.create_at : 0, 0, '', now, ); } export function isGroupOrDirectChannelVisible( channel: Channel, memberships: RelationOneToOne<Channel, ChannelMembership>, config: any, myPreferences: { [x: string]: PreferenceType; }, currentUserId: string, users: IDMappedObjects<UserProfile>, lastPosts: RelationOneToOne<Channel, Post>, currentChannelId?: string, now?: number, ): boolean { const lastPost = lastPosts[channel.id]; if (isGroupChannel(channel) && isGroupChannelVisible(config, myPreferences, channel, lastPost, isUnreadChannel(memberships, channel), now)) { return true; } if (!isDirectChannel(channel)) { return false; } const otherUserId = getUserIdFromChannelName(currentUserId, channel.name); return isDirectChannelVisible( users[otherUserId] || otherUserId, config, myPreferences, channel, lastPost, isUnreadChannel(memberships, channel), currentChannelId, now, ); } export function showCreateOption(state: GlobalState, config: any, license: any, teamId: string, channelType: ChannelType, isAdmin: boolean, isSystemAdmin: boolean): boolean { if (hasNewPermissions(state)) { if (channelType === General.OPEN_CHANNEL) { return haveITeamPermission(state, {team: teamId, permission: Permissions.CREATE_PUBLIC_CHANNEL}); } else if (channelType === General.PRIVATE_CHANNEL) { return haveITeamPermission(state, {team: teamId, permission: Permissions.CREATE_PRIVATE_CHANNEL}); } return true; } if (license.IsLicensed !== 'true') { return true; } // Backwards compatibility with pre-advanced permissions config settings. if (channelType === General.OPEN_CHANNEL) { if (config.RestrictPublicChannelCreation === General.SYSTEM_ADMIN_ROLE && !isSystemAdmin) { return false; } else if (config.RestrictPublicChannelCreation === General.TEAM_ADMIN_ROLE && !isAdmin) { return false; } } else if (channelType === General.PRIVATE_CHANNEL) { if (config.RestrictPrivateChannelCreation === General.SYSTEM_ADMIN_ROLE && !isSystemAdmin) { return false; } else if (config.RestrictPrivateChannelCreation === General.TEAM_ADMIN_ROLE && !isAdmin) { return false; } } return true; } export function showManagementOptions(state: GlobalState, config: any, license: any, channel: Channel, isAdmin: boolean, isSystemAdmin: boolean, isChannelAdmin: boolean): boolean { if (hasNewPermissions(state)) { if (channel.type === General.OPEN_CHANNEL) { return haveIChannelPermission(state, {channel: channel.id, team: channel.team_id, permission: Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES}); } else if (channel.type === General.PRIVATE_CHANNEL) { return haveIChannelPermission(state, {channel: channel.id, team: channel.team_id, permission: Permissions.MANAGE_PRIVATE_CHANNEL_PROPERTIES}); } return true; } if (license.IsLicensed !== 'true') { return true; } // Backwards compatibility with pre-advanced permissions config settings. if (channel.type === General.OPEN_CHANNEL) { if (config.RestrictPublicChannelManagement === General.SYSTEM_ADMIN_ROLE && !isSystemAdmin) { return false; } if (config.RestrictPublicChannelManagement === General.TEAM_ADMIN_ROLE && !isAdmin) { return false; } if (config.RestrictPublicChannelManagement === General.CHANNEL_ADMIN_ROLE && !isChannelAdmin && !isAdmin) { return false; } } else if (channel.type === General.PRIVATE_CHANNEL) { if (config.RestrictPrivateChannelManagement === General.SYSTEM_ADMIN_ROLE && !isSystemAdmin) { return false; } if (config.RestrictPrivateChannelManagement === General.TEAM_ADMIN_ROLE && !isAdmin) { return false; } if (config.RestrictPrivateChannelManagement === General.CHANNEL_ADMIN_ROLE && !isChannelAdmin && !isAdmin) { return false; } } return true; } export function showDeleteOption(state: GlobalState, config: any, license: any, channel: Channel, isAdmin: boolean, isSystemAdmin: boolean, isChannelAdmin: boolean): boolean { if (hasNewPermissions(state)) { if (channel.type === General.OPEN_CHANNEL) { return haveIChannelPermission(state, {channel: channel.id, team: channel.team_id, permission: Permissions.DELETE_PUBLIC_CHANNEL}); } else if (channel.type === General.PRIVATE_CHANNEL) { return haveIChannelPermission(state, {channel: channel.id, team: channel.team_id, permission: Permissions.DELETE_PRIVATE_CHANNEL}); } return true; } if (license.IsLicensed !== 'true') { return true; } // Backwards compatibility with pre-advanced permissions config settings. if (channel.type === General.OPEN_CHANNEL) { if (config.RestrictPublicChannelDeletion === General.SYSTEM_ADMIN_ROLE && !isSystemAdmin) { return false; } if (config.RestrictPublicChannelDeletion === General.TEAM_ADMIN_ROLE && !isAdmin) { return false; } if (config.RestrictPublicChannelDeletion === General.CHANNEL_ADMIN_ROLE && !isChannelAdmin && !isAdmin) { return false; } } else if (channel.type === General.PRIVATE_CHANNEL) { if (config.RestrictPrivateChannelDeletion === General.SYSTEM_ADMIN_ROLE && !isSystemAdmin) { return false; } if (config.RestrictPrivateChannelDeletion === General.TEAM_ADMIN_ROLE && !isAdmin) { return false; } if (config.RestrictPrivateChannelDeletion === General.CHANNEL_ADMIN_ROLE && !isChannelAdmin && !isAdmin) { return false; } } return true; } // Backwards compatibility with pre-advanced permissions config settings. export function canManageMembersOldPermissions(channel: Channel, user: UserProfile, teamMember: TeamMembership, channelMember: ChannelMembership, config: any, license: any): boolean { if (channel.type === General.DM_CHANNEL || channel.type === General.GM_CHANNEL || channel.name === General.DEFAULT_CHANNEL) { return false; } if (license.IsLicensed !== 'true') { return true; } if (channel.type === General.PRIVATE_CHANNEL) { const isSystemAdmin = user.roles.includes(General.SYSTEM_ADMIN_ROLE); if (config.RestrictPrivateChannelManageMembers === General.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { return false; } const isTeamAdmin = teamMember.roles.includes(General.TEAM_ADMIN_ROLE); if (config.RestrictPrivateChannelManageMembers === General.PERMISSIONS_TEAM_ADMIN && !isTeamAdmin && !isSystemAdmin) { return false; } const isChannelAdmin = channelMember.roles.includes(General.CHANNEL_ADMIN_ROLE); if (config.RestrictPrivateChannelManageMembers === General.PERMISSIONS_CHANNEL_ADMIN && !isChannelAdmin && !isTeamAdmin && !isSystemAdmin) { return false; } } return true; } export function getChannelsIdForTeam(state: GlobalState, teamId: string): string[] { const {channels} = state.entities.channels; return Object.keys(channels).map((key) => channels[key]).reduce((res, channel: Channel) => { if (channel.team_id === teamId) { res.push(channel.id); } return res; }, [] as string[]); } export function getGroupDisplayNameFromUserIds(userIds: string[], profiles: IDMappedObjects<UserProfile>, currentUserId: string, teammateNameDisplay: string): string { const names: string[] = []; userIds.forEach((id) => { if (id !== currentUserId) { names.push(displayUsername(profiles[id], teammateNameDisplay)); } }); function sortUsernames(a: string, b: string) { const locale = getUserLocale(currentUserId, profiles); return a.localeCompare(b, locale, {numeric: true}); } return names.sort(sortUsernames).join(', '); } export function isFavoriteChannelOld(myPreferences: { [x: string]: PreferenceType; }, id: string) { const fav = myPreferences[`${Preferences.CATEGORY_FAVORITE_CHANNEL}--${id}`]; return fav ? fav.value === 'true' : false; } export function isDefault(channel: Channel): boolean { return channel.name === General.DEFAULT_CHANNEL; } function completeDirectGroupInfo(usersState: UsersState, teammateNameDisplay: string, channel: Channel) { const {currentUserId, profiles, profilesInChannel} = usersState; const profilesIds = profilesInChannel[channel.id]; const gm = {...channel}; if (profilesIds) { gm.display_name = getGroupDisplayNameFromUserIds(profilesIds, profiles, currentUserId, teammateNameDisplay); return gm; } const usernames = gm.display_name.split(', '); const users = Object.keys(profiles).map((key) => profiles[key]); const userIds: string[] = []; usernames.forEach((username: string) => { const u = users.find((p): boolean => p.username === username); if (u) { userIds.push(u.id); } }); if (usernames.length === userIds.length) { gm.display_name = getGroupDisplayNameFromUserIds(userIds, profiles, currentUserId, teammateNameDisplay); return gm; } return channel; } export function isUnreadChannel(members: RelationOneToOne<Channel, ChannelMembership>, channel: Channel): boolean { const member = members[channel.id]; if (member) { const msgCount = channel.total_msg_count - member.msg_count; const onlyMentions = member.notify_props && member.notify_props.mark_unread === MarkUnread.MENTION; return (member.mention_count > 0 || (Boolean(msgCount) && !onlyMentions)); } return false; } export function isOpenChannel(channel: Channel): boolean { return channel.type === General.OPEN_CHANNEL; } export function isPrivateChannel(channel: Channel): boolean { return channel.type === General.PRIVATE_CHANNEL; } export function sortChannelsByTypeListAndDisplayName(locale: string, typeList: string[], a: Channel, b: Channel): number { const idxA = typeList.indexOf(a.type); const idxB = typeList.indexOf(b.type); if (idxA === -1 && idxB !== -1) { return 1; } if (idxB === -1 && idxA !== -1) { return -1; } if (idxA !== idxB) { if (idxA < idxB) { return -1; } return 1; } const aDisplayName = filterName(a.display_name); const bDisplayName = filterName(b.display_name); if (aDisplayName !== bDisplayName) { return aDisplayName.toLowerCase().localeCompare(bDisplayName.toLowerCase(), locale, {numeric: true}); } return a.name.toLowerCase().localeCompare(b.name.toLowerCase(), locale, {numeric: true}); } export function sortChannelsByTypeAndDisplayName(locale: string, a: Channel, b: Channel): number { if (channelTypeOrder[a.type] !== channelTypeOrder[b.type]) { if (channelTypeOrder[a.type] < channelTypeOrder[b.type]) { return -1; } return 1; } const aDisplayName = filterName(a.display_name); const bDisplayName = filterName(b.display_name); if (aDisplayName !== bDisplayName) { return aDisplayName.toLowerCase().localeCompare(bDisplayName.toLowerCase(), locale, {numeric: true}); } return a.name.toLowerCase().localeCompare(b.name.toLowerCase(), locale, {numeric: true}); } function filterName(name: string): string { return name.replace(/[.,'"\/#!$%\^&\*;:{}=\-_`~()]/g, ''); // eslint-disable-line no-useless-escape } export function sortChannelsByDisplayName(locale: string, a: Channel, b: Channel): number { // if both channels have the display_name defined if (a.display_name && b.display_name && a.display_name !== b.display_name) { return a.display_name.toLowerCase().localeCompare(b.display_name.toLowerCase(), locale, {numeric: true}); } return a.name.toLowerCase().localeCompare(b.name.toLowerCase(), locale, {numeric: true}); } export function sortChannelsByDisplayNameAndMuted(locale: string, members: RelationOneToOne<Channel, ChannelMembership>, a: Channel, b: Channel): number { const aMember = members[a.id]; const bMember = members[b.id]; if (isChannelMuted(bMember) === isChannelMuted(aMember)) { return sortChannelsByDisplayName(locale, a, b); } if (!isChannelMuted(bMember) && isChannelMuted(aMember)) { return 1; } return -1; } export function sortChannelsByRecency(lastPosts: RelationOneToOne<Channel, Post>, a: Channel, b: Channel): number { let aLastPostAt = a.last_post_at; if (lastPosts[a.id] && lastPosts[a.id].create_at > a.last_post_at) { aLastPostAt = lastPosts[a.id].create_at; } let bLastPostAt = b.last_post_at; if (lastPosts[b.id] && lastPosts[b.id].create_at > b.last_post_at) { bLastPostAt = lastPosts[b.id].create_at; } return bLastPostAt - aLastPostAt; } export function isChannelMuted(member: ChannelMembership): boolean { return member && member.notify_props ? (member.notify_props.mark_unread === MarkUnread.MENTION) : false; } export function areChannelMentionsIgnored(channelMemberNotifyProps: ChannelNotifyProps, currentUserNotifyProps: UserNotifyProps) { let ignoreChannelMentionsDefault = Users.IGNORE_CHANNEL_MENTIONS_OFF; if (currentUserNotifyProps.channel && currentUserNotifyProps.channel === 'false') { ignoreChannelMentionsDefault = Users.IGNORE_CHANNEL_MENTIONS_ON; } let ignoreChannelMentions = channelMemberNotifyProps && channelMemberNotifyProps.ignore_channel_mentions; if (!ignoreChannelMentions || ignoreChannelMentions === Users.IGNORE_CHANNEL_MENTIONS_DEFAULT) { ignoreChannelMentions = ignoreChannelMentionsDefault as any; } return ignoreChannelMentions !== Users.IGNORE_CHANNEL_MENTIONS_OFF; } function getUserLocale(userId: string, profiles: IDMappedObjects<UserProfile>) { let locale = General.DEFAULT_LOCALE; if (profiles && profiles[userId] && profiles[userId].locale) { locale = profiles[userId].locale; } return locale; } export function filterChannelsMatchingTerm(channels: Channel[], term: string): Channel[] { const lowercasedTerm = term.toLowerCase(); return channels.filter((channel: Channel): boolean => { if (!channel) { return false; } const name = (channel.name || '').toLowerCase(); const displayName = (channel.display_name || '').toLowerCase(); return name.startsWith(lowercasedTerm) || displayName.startsWith(lowercasedTerm); }); }