mattermost-redux
Version:
Common code (API client, Redux stores, logic, utility functions) for building a Mattermost client
344 lines (343 loc) • 19.7 kB
JavaScript
;
// 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;
}