UNPKG

mattermost-redux

Version:

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

344 lines (343 loc) 19.7 kB
"use strict"; // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getAllCategoriesByIds = getAllCategoriesByIds; exports.getCategory = getCategory; exports.getCategoryInTeamByType = getCategoryInTeamByType; exports.getCategoryInTeamWithChannel = getCategoryInTeamWithChannel; exports.getCategoryWhere = getCategoryWhere; exports.getCategoryIdsForTeam = getCategoryIdsForTeam; exports.makeGetCategoriesForTeam = makeGetCategoriesForTeam; exports.makeFilterArchivedChannels = makeFilterArchivedChannels; exports.makeFilterAutoclosedDMs = makeFilterAutoclosedDMs; exports.makeFilterManuallyClosedDMs = makeFilterManuallyClosedDMs; exports.makeCompareChannels = makeCompareChannels; exports.makeSortChannelsByName = makeSortChannelsByName; exports.makeSortChannelsByNameWithDMs = makeSortChannelsByNameWithDMs; exports.makeSortChannelsByRecency = makeSortChannelsByRecency; exports.makeSortChannels = makeSortChannels; exports.makeGetChannelIdsForCategory = makeGetChannelIdsForCategory; exports.makeFilterAndSortChannelsForCategory = makeFilterAndSortChannelsForCategory; exports.makeGetChannelsByCategory = makeGetChannelsByCategory; const shallow_equals_1 = __importDefault(require("shallow-equals")); const channel_categories_1 = require("@mattermost/types/channel_categories"); const constants_1 = require("mattermost-redux/constants"); const channel_categories_2 = require("mattermost-redux/constants/channel_categories"); const create_selector_1 = require("mattermost-redux/selectors/create_selector"); const channels_1 = require("mattermost-redux/selectors/entities/channels"); const i18n_1 = require("mattermost-redux/selectors/entities/i18n"); const preferences_1 = require("mattermost-redux/selectors/entities/preferences"); const users_1 = require("mattermost-redux/selectors/entities/users"); const channel_utils_1 = require("mattermost-redux/utils/channel_utils"); const preference_utils_1 = require("mattermost-redux/utils/preference_utils"); const user_utils_1 = require("mattermost-redux/utils/user_utils"); function getAllCategoriesByIds(state) { return state.entities.channelCategories.byId; } function getCategory(state, categoryId) { return getAllCategoriesByIds(state)[categoryId]; } // getCategoryInTeamByType returns the first category found of the given type on the given team. This is intended for use // with only non-custom types of categories. function getCategoryInTeamByType(state, teamId, categoryType) { return getCategoryWhere(state, (category) => category.type === categoryType && category.team_id === teamId); } // getCategoryInTeamWithChannel returns the category on a given team containing the given channel ID. function getCategoryInTeamWithChannel(state, teamId, channelId) { return getCategoryWhere(state, (category) => category.team_id === teamId && category.channel_ids.includes(channelId)); } // getCategoryWhere returns the first category meeting the given condition. This should not be used with a condition // that matches multiple categories. function getCategoryWhere(state, condition) { const categoriesByIds = getAllCategoriesByIds(state); return Object.values(categoriesByIds).find(condition); } function getCategoryIdsForTeam(state, teamId) { return state.entities.channelCategories.orderByTeam[teamId]; } function makeGetCategoriesForTeam() { return (0, create_selector_1.createSelector)('makeGetCategoriesForTeam', getCategoryIdsForTeam, (state) => state.entities.channelCategories.byId, (categoryIds, categoriesById) => { if (!categoryIds) { return []; } return categoryIds.map((id) => categoriesById[id]); }); } // makeFilterArchivedChannels returns a selector that filters a given list of channels based on whether or not the channel // is archived or is currently being viewed. The selector returns the original array if no channels are filtered out. function makeFilterArchivedChannels() { return (0, create_selector_1.createSelector)('makeFilterArchivedChannels', (state, channels) => channels, channels_1.getCurrentChannelId, (channels, currentChannelId) => { const filtered = channels.filter((channel) => channel && (channel.id === currentChannelId || channel.delete_at === 0)); return filtered.length === channels.length ? channels : filtered; }); } function makeFilterAutoclosedDMs() { return (0, create_selector_1.createSelector)('makeFilterAutoclosedDMs', (state, channels) => channels, (state, channels, categoryType) => categoryType, channels_1.getCurrentChannelId, (state) => state.entities.users.profiles, users_1.getCurrentUserId, channels_1.getMyChannelMemberships, channels_1.getChannelMessageCounts, (state) => (0, preferences_1.getVisibleDmGmLimit)(state), preferences_1.getMyPreferences, preferences_1.isCollapsedThreadsEnabled, (channels, categoryType, currentChannelId, profiles, currentUserId, myMembers, messageCounts, limitPref, myPreferences, collapsedThreads) => { if (categoryType !== channel_categories_2.CategoryTypes.DIRECT_MESSAGES) { // Only autoclose DMs that haven't been assigned to a category return channels; } const getTimestampFromPrefs = (category, name) => { const pref = myPreferences[(0, preference_utils_1.getPreferenceKey)(category, name)]; return parseInt(pref ? pref.value : '0', 10); }; const getLastViewedAt = (channel) => { // The server only ever sets the last_viewed_at to the time of the last post in channel, so we may need // to use the preferences added for the previous version of autoclosing DMs. return Math.max(myMembers[channel.id]?.last_viewed_at, getTimestampFromPrefs(constants_1.Preferences.CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME, channel.id), getTimestampFromPrefs(constants_1.Preferences.CATEGORY_CHANNEL_OPEN_TIME, channel.id)); }; let unreadCount = 0; let visibleChannels = channels.filter((channel) => { if (isUnreadChannel(channel.id, messageCounts, myMembers, collapsedThreads)) { unreadCount++; // Unread DMs/GMs are always visible return true; } if (channel.id === currentChannelId) { return true; } // DMs with deactivated users will be visible if you're currently viewing them and they were opened // since the user was deactivated if (channel.type === constants_1.General.DM_CHANNEL) { const teammateId = (0, channel_utils_1.getUserIdFromChannelName)(currentUserId, channel.name); const teammate = profiles[teammateId]; const lastViewedAt = getLastViewedAt(channel); if (!teammate || teammate.delete_at > lastViewedAt) { return false; } } return true; }); visibleChannels.sort((channelA, channelB) => { // Should always prioritise the current channel if (channelA.id === currentChannelId) { return -1; } else if (channelB.id === currentChannelId) { return 1; } // Second priority is for unread channels if (isUnreadChannel(channelA.id, messageCounts, myMembers, collapsedThreads) && !isUnreadChannel(channelB.id, messageCounts, myMembers, collapsedThreads)) { return -1; } else if (!isUnreadChannel(channelA.id, messageCounts, myMembers, collapsedThreads) && isUnreadChannel(channelB.id, messageCounts, myMembers, collapsedThreads)) { return 1; } // Third priority is last_viewed_at const channelAlastViewed = getLastViewedAt(channelA) || 0; const channelBlastViewed = getLastViewedAt(channelB) || 0; if (channelAlastViewed > channelBlastViewed) { return -1; } else if (channelBlastViewed > channelAlastViewed) { return 1; } return 0; }); // The limit of DMs user specifies to be rendered in the sidebar const remaining = Math.max(limitPref, unreadCount); visibleChannels = visibleChannels.slice(0, remaining); const visibleChannelsSet = new Set(visibleChannels); const filteredChannels = channels.filter((channel) => visibleChannelsSet.has(channel)); return filteredChannels.length === channels.length ? channels : filteredChannels; }); } function makeFilterManuallyClosedDMs() { return (0, create_selector_1.createSelector)('makeFilterManuallyClosedDMs', (state, channels) => channels, preferences_1.getMyPreferences, channels_1.getCurrentChannelId, users_1.getCurrentUserId, channels_1.getMyChannelMemberships, channels_1.getChannelMessageCounts, preferences_1.isCollapsedThreadsEnabled, (channels, myPreferences, currentChannelId, currentUserId, myMembers, messageCounts, collapsedThreads) => { const filtered = channels.filter((channel) => { let preference; if (channel.type !== constants_1.General.DM_CHANNEL && channel.type !== constants_1.General.GM_CHANNEL) { return true; } if (isUnreadChannel(channel.id, messageCounts, myMembers, collapsedThreads)) { // Unread DMs/GMs are always visible return true; } if (currentChannelId === channel.id) { // The current channel is always visible return true; } if (channel.type === constants_1.General.DM_CHANNEL) { const teammateId = (0, channel_utils_1.getUserIdFromChannelName)(currentUserId, channel.name); preference = myPreferences[(0, preference_utils_1.getPreferenceKey)(constants_1.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, teammateId)]; } else { preference = myPreferences[(0, preference_utils_1.getPreferenceKey)(constants_1.Preferences.CATEGORY_GROUP_CHANNEL_SHOW, channel.id)]; } return preference && preference.value !== 'false'; }); // Only return a new array if anything was removed return filtered.length === channels.length ? channels : filtered; }); } function makeCompareChannels(getDisplayName, locale, myMembers) { return (a, b) => { // Sort muted channels last const aMuted = (0, channel_utils_1.isChannelMuted)(myMembers[a.id]); const bMuted = (0, channel_utils_1.isChannelMuted)(myMembers[b.id]); if (aMuted && !bMuted) { return 1; } else if (!aMuted && bMuted) { return -1; } // And then sort alphabetically return getDisplayName(a).localeCompare(getDisplayName(b), locale, { numeric: true }); }; } function makeSortChannelsByName() { return (0, create_selector_1.createSelector)('makeSortChannelsByName', (state, channels) => channels, (state) => (0, i18n_1.getCurrentUserLocale)(state), channels_1.getMyChannelMemberships, (channels, locale, myMembers) => { const getDisplayName = (channel) => channel.display_name; return [...channels].sort(makeCompareChannels(getDisplayName, locale, myMembers)); }); } function makeSortChannelsByNameWithDMs() { return (0, create_selector_1.createSelector)('makeSortChannelsByNameWithDMs', (state, channels) => channels, users_1.getCurrentUserId, (state) => state.entities.users.profiles, preferences_1.getTeammateNameDisplaySetting, (state) => (0, i18n_1.getCurrentUserLocale)(state), channels_1.getMyChannelMemberships, (channels, currentUserId, profiles, teammateNameDisplay, locale, myMembers) => { const cachedNames = {}; const getDisplayName = (channel) => { if (cachedNames[channel.id]) { return cachedNames[channel.id]; } let displayName; // TODO it might be easier to do this by using channel members to find the users if (channel.type === constants_1.General.DM_CHANNEL) { const teammateId = (0, channel_utils_1.getUserIdFromChannelName)(currentUserId, channel.name); const teammate = profiles[teammateId]; displayName = (0, user_utils_1.displayUsername)(teammate, teammateNameDisplay, false); } else if (channel.type === constants_1.General.GM_CHANNEL) { const usernames = channel.display_name.split(', '); const userDisplayNames = []; for (const username of usernames) { const user = Object.values(profiles).find((profile) => profile.username === username); if (!user) { continue; } if (user.id === currentUserId) { continue; } userDisplayNames.push((0, user_utils_1.displayUsername)(user, teammateNameDisplay, false)); } displayName = userDisplayNames.sort((a, b) => a.localeCompare(b, locale, { numeric: true })).join(', '); } else { displayName = channel.display_name; } cachedNames[channel.id] = displayName; return displayName; }; return [...channels].sort(makeCompareChannels(getDisplayName, locale, myMembers)); }); } function makeSortChannelsByRecency() { return (0, create_selector_1.createSelector)('makeSortChannelsByRecency', (_state, channels) => channels, preferences_1.isCollapsedThreadsEnabled, (channels, crtEnabled) => { return [...channels].sort((a, b) => { const aLastPostAt = Math.max(crtEnabled ? (a.last_root_post_at || a.last_post_at) : a.last_post_at, a.create_at); const bLastPostAt = Math.max(crtEnabled ? (b.last_root_post_at || b.last_post_at) : b.last_post_at, b.create_at); return bLastPostAt - aLastPostAt; }); }); } function makeSortChannels() { const sortChannelsByName = makeSortChannelsByName(); const sortChannelsByNameWithDMs = makeSortChannelsByNameWithDMs(); const sortChannelsByRecency = makeSortChannelsByRecency(); return (state, originalChannels, category) => { let channels = originalChannels; // While this function isn't memoized, sortChannelsByX should be since they know what parts of state // will affect sort order. if (category.sorting === channel_categories_1.CategorySorting.Recency) { channels = sortChannelsByRecency(state, channels); } else if (category.sorting === channel_categories_1.CategorySorting.Alphabetical || category.sorting === channel_categories_1.CategorySorting.Default) { if (channels.some((channel) => channel.type === constants_1.General.DM_CHANNEL || channel.type === constants_1.General.GM_CHANNEL)) { channels = sortChannelsByNameWithDMs(state, channels); } else { channels = sortChannelsByName(state, channels); } } return channels; }; } function makeGetChannelIdsForCategory() { const getChannels = (0, channels_1.makeGetChannelsForIds)(); const filterAndSortChannelsForCategory = makeFilterAndSortChannelsForCategory(); let lastChannelIds = []; return (state, category) => { const channels = getChannels(state, category.channel_ids); const filteredChannelIds = filterAndSortChannelsForCategory(state, channels, category).map((channel) => channel.id); if ((0, shallow_equals_1.default)(filteredChannelIds, lastChannelIds)) { return lastChannelIds; } lastChannelIds = filteredChannelIds; return lastChannelIds; }; } // Returns a selector that takes an array of channels and the category they belong to and returns the array sorted and // with inactive DMs/GMs and archived channels filtered out. function makeFilterAndSortChannelsForCategory() { const filterArchivedChannels = makeFilterArchivedChannels(); const filterAutoclosedDMs = makeFilterAutoclosedDMs(); const filterManuallyClosedDMs = makeFilterManuallyClosedDMs(); const sortChannels = makeSortChannels(); return (state, originalChannels, category) => { let channels = originalChannels; channels = filterArchivedChannels(state, channels); channels = filterManuallyClosedDMs(state, channels); channels = filterAutoclosedDMs(state, channels, category.type); channels = sortChannels(state, channels, category); return channels; }; } function makeGetChannelsByCategory() { const getCategoriesForTeam = makeGetCategoriesForTeam(); // Memoize by category. As long as the categories don't change, we can keep using the same selectors for each category. let getChannels; let filterAndSortChannels; let lastCategoryIds = []; let lastChannelsByCategory = {}; return (state, teamId) => { const categoryIds = getCategoryIdsForTeam(state, teamId); // Create an instance of filterAndSortChannels for each category. As long as we don't add or remove new categories, // we can reuse these selectors to memoize the results of each category. This will also create new selectors when // categories are reordered, but that should be rare enough that it won't meaningfully affect performance. if (categoryIds !== lastCategoryIds) { lastCategoryIds = categoryIds; lastChannelsByCategory = {}; getChannels = {}; filterAndSortChannels = {}; if (categoryIds) { for (const categoryId of categoryIds) { getChannels[categoryId] = (0, channels_1.makeGetChannelsForIds)(); filterAndSortChannels[categoryId] = makeFilterAndSortChannelsForCategory(); } } } const categories = getCategoriesForTeam(state, teamId); const channelsByCategory = {}; // TODO: This avoids some rendering, but there is a bigger issue underneath // Every time myPreferences or myChannels change (which can happen for many // unrelated reasons) the whole list of channels gets reordered and re-filtered. let allEquals = categoryIds === lastCategoryIds; for (const category of categories) { const channels = getChannels[category.id](state, category.channel_ids); channelsByCategory[category.id] = filterAndSortChannels[category.id](state, channels, category); allEquals = allEquals && (0, shallow_equals_1.default)(channelsByCategory[category.id], lastChannelsByCategory[category.id]); } // Do a shallow equality check of channelsByCategory to avoid returning a new object containing the same data if (allEquals) { return lastChannelsByCategory; } lastChannelsByCategory = channelsByCategory; return channelsByCategory; }; } function isUnreadChannel(channelId, messageCounts, members, crtEnabled) { const unreadCount = (0, channel_utils_1.calculateUnreadCount)(messageCounts[channelId], members[channelId], crtEnabled); return unreadCount.showUnread; }