Refactor category channels to react to setting changes and apply the correct order (#7170)

* Refactor category channels to react to setting changes and apply the correct order

* feedback review
This commit is contained in:
Elias Nahum
2023-03-03 15:53:29 +02:00
committed by GitHub
parent 3c046ae39c
commit fe916ec740
11 changed files with 312 additions and 209 deletions

View File

@@ -11,6 +11,7 @@ import com.mattermost.helpers.database_extension.queryCurrentUserId
import com.nozbe.watermelondb.Database
import java.text.Collator
import java.util.Locale
import kotlin.math.max
suspend fun PushNotificationDataRunnable.Companion.fetchMyChannel(db: Database, serverUrl: String, channelId: String, isCRTEnabled: Boolean): Triple<ReadableMap?, ReadableMap?, ReadableArray?> {
val channel = fetch(serverUrl, "/api/v4/channels/$channelId")

View File

@@ -3,6 +3,8 @@
export const CATEGORIES_TO_KEEP: Record<string, string> = {
ADVANCED_SETTINGS: 'advanced_settings',
CHANNEL_APPROXIMATE_VIEW_TIME: 'channel_approximate_view_time',
CHANNEL_OPEN_TIME: 'channel_open_time',
DIRECT_CHANNEL_SHOW: 'direct_channel_show',
GROUP_CHANNEL_SHOW: 'group_channel_show',
DISPLAY_SETTINGS: 'display_settings',

View File

@@ -246,10 +246,11 @@ const ChannelHandler = <TBase extends Constructor<ServerDataOperatorBase>>(super
const totalMsg = isCRT ? channel.total_msg_count_root! : channel.total_msg_count;
const myMsgCount = isCRT ? my.msg_count_root! : my.msg_count;
const msgCount = Math.max(0, totalMsg - myMsgCount);
const lastPostAt = isCRT ? (channel.last_root_post_at || channel.last_post_at) : channel.last_post_at;
my.msg_count = msgCount;
my.mention_count = isCRT ? my.mention_count_root! : my.mention_count;
my.is_unread = msgCount > 0;
my.last_post_at = (isCRT ? (channel.last_root_post_at || channel.last_post_at) : channel.last_post_at) || 0;
my.last_post_at = lastPostAt;
}
}
@@ -271,7 +272,7 @@ const ChannelHandler = <TBase extends Constructor<ServerDataOperatorBase>>(super
}
const chan = channelMap[my.channel_id];
const lastPostAt = (isCRT ? chan.last_root_post_at : chan.last_post_at) || 0;
const lastPostAt = isCRT ? (chan.last_root_post_at || chan.last_post_at) : chan.last_post_at;
if ((chan && e.lastPostAt < lastPostAt) ||
e.isUnread !== my.is_unread || e.lastViewedAt < my.last_viewed_at ||
e.roles !== my.roles

View File

@@ -11,14 +11,11 @@ import {makeCategoryChannelId} from '@utils/categories';
import {pluckUnique} from '@utils/helpers';
import {logDebug} from '@utils/log';
import {observeChannelsByLastPostAt} from './channel';
import type ServerDataOperator from '@database/operator/server_data_operator';
import type CategoryModel from '@typings/database/models/servers/category';
import type CategoryChannelModel from '@typings/database/models/servers/category_channel';
import type ChannelModel from '@typings/database/models/servers/channel';
const {SERVER: {CATEGORY, CATEGORY_CHANNEL, CHANNEL}} = MM_TABLES;
const {SERVER: {CATEGORY, CATEGORY_CHANNEL}} = MM_TABLES;
export const getCategoryById = async (database: Database, categoryId: string) => {
try {
@@ -144,24 +141,3 @@ export const observeIsChannelFavorited = (database: Database, teamId: string, ch
distinctUntilChanged(),
);
};
export const observeChannelsByCategoryChannelSortOrder = (database: Database, category: CategoryModel, excludeIds?: string[]) => {
return category.categoryChannelsBySortOrder.observeWithColumns(['sort_order']).pipe(
switchMap((categoryChannels) => {
const ids = categoryChannels.map((cc) => cc.channelId);
const idsStr = `'${ids.join("','")}'`;
const exclude = excludeIds?.length ? `AND c.id NOT IN ('${excludeIds.join("','")}')` : '';
return database.get<ChannelModel>(CHANNEL).query(
Q.unsafeSqlQuery(`SELECT DISTINCT c.* FROM ${CHANNEL} c INNER JOIN
${CATEGORY_CHANNEL} cc ON cc.channel_id=c.id AND c.id IN (${idsStr}) ${exclude}
ORDER BY cc.sort_order`),
).observe();
}),
);
};
export const observeChannelsByLastPostAtInCategory = (database: Database, category: CategoryModel, excludeIds?: string[]) => {
return category.myChannels.observeWithColumns(['last_post_at']).pipe(
switchMap((myChannels) => observeChannelsByLastPostAt(database, myChannels, excludeIds)),
);
};

View File

@@ -11,6 +11,7 @@ import {General, Permissions} from '@constants';
import {MM_TABLES} from '@constants/database';
import {sanitizeLikeString} from '@helpers/database';
import {hasPermission} from '@utils/role';
import {getUserIdFromChannelName} from '@utils/user';
import {prepareDeletePost} from './post';
import {queryRoles} from './role';
@@ -437,10 +438,6 @@ export const observeNotifyPropsByChannels = (database: Database, channels: Chann
);
};
export const queryChannelsByNames = (database: Database, names: string[]) => {
return database.get<ChannelModel>(CHANNEL).query(Q.where('name', Q.oneOf(names)));
};
export const queryMyChannelUnreads = (database: Database, currentTeamId: string) => {
return database.get<MyChannelModel>(MY_CHANNEL).query(
Q.on(
@@ -453,40 +450,42 @@ export const queryMyChannelUnreads = (database: Database, currentTeamId: string)
Q.where('delete_at', Q.eq(0)),
),
),
Q.where('is_unread', Q.eq(true)),
Q.or(
Q.where('is_unread', Q.eq(true)),
Q.where('mentions_count', Q.gte(0)),
),
Q.sortBy('last_post_at', Q.desc),
);
};
export const queryEmptyDirectAndGroupChannels = (database: Database) => {
return database.get<MyChannelModel>(MY_CHANNEL).query(
Q.on(
CHANNEL,
Q.where('team_id', Q.eq('')),
),
Q.where('last_post_at', Q.eq(0)),
);
};
export const observeArchivedDirectChannels = (database: Database, currentUserId: string) => {
const deactivatedIds = database.get<UserModel>(USER).query(
const deactivated = database.get<UserModel>(USER).query(
Q.where('delete_at', Q.gt(0)),
).observe().pipe(
switchMap((users) => of$(users.map((u) => u.id))),
);
).observe();
return deactivatedIds.pipe(
switchMap((dIds) => {
return deactivated.pipe(
switchMap((users) => {
const usersMap = new Map(users.map((u) => [u.id, u]));
return database.get<ChannelModel>(CHANNEL).query(
Q.on(
CHANNEL_MEMBERSHIP,
Q.and(
Q.where('user_id', Q.notEq(currentUserId)),
Q.where('user_id', Q.oneOf(dIds)),
Q.where('user_id', Q.oneOf(Array.from(usersMap.keys()))),
),
),
Q.where('type', 'D'),
).observe();
).observe().pipe(
switchMap((channels) => {
// eslint-disable-next-line max-nested-callbacks
return of$(new Map(channels.map((c) => {
const teammateId = getUserIdFromChannelName(currentUserId, c.name);
const user = usersMap.get(teammateId);
return [c.id, user];
})));
}),
);
}),
);
};
@@ -639,13 +638,13 @@ export const observeIsMutedSetting = (database: Database, channelId: string) =>
return observeChannelSettings(database, channelId).pipe(switchMap((s) => of$(s?.notifyProps?.mark_unread === General.MENTION)));
};
export const observeChannelsByLastPostAt = (database: Database, myChannels: MyChannelModel[], excludeIds?: string[]) => {
export const observeChannelsByLastPostAt = (database: Database, myChannels: MyChannelModel[]) => {
const ids = myChannels.map((c) => c.id);
const idsStr = `'${ids.join("','")}'`;
const exclude = excludeIds?.length ? `AND c.id NOT IN ('${excludeIds.join("','")}')` : '';
return database.get<ChannelModel>(CHANNEL).query(
Q.unsafeSqlQuery(`SELECT DISTINCT c.* FROM ${CHANNEL} c INNER JOIN
${MY_CHANNEL} mc ON mc.id=c.id AND c.id IN (${idsStr}) ${exclude}
${MY_CHANNEL} mc ON mc.id=c.id AND c.id IN (${idsStr})
ORDER BY CASE mc.last_post_at WHEN 0 THEN c.create_at ELSE mc.last_post_at END DESC`),
).observe();
};

View File

@@ -12,29 +12,9 @@ import TestHelper from '@test/test_helper';
import CategoryBody from '.';
import type CategoryModel from '@typings/database/models/servers/category';
import type CategoryChannelModel from '@typings/database/models/servers/category_channel';
import type ChannelModel from '@typings/database/models/servers/channel';
const {SERVER: {CATEGORY}} = MM_TABLES;
jest.mock('@queries/servers/categories', () => {
const Queries = jest.requireActual('@queries/servers/categories');
const switchMap = jest.requireActual('rxjs/operators').switchMap;
const mQ = jest.requireActual('@nozbe/watermelondb').Q;
return {
...Queries,
observeChannelsByCategoryChannelSortOrder: (database: Database, category: CategoryModel, excludeIds?: string[]) => {
return category.categoryChannelsBySortOrder.observeWithColumns(['sort_order']).pipe(
switchMap((categoryChannels: CategoryChannelModel[]) => {
const ids = categoryChannels.filter((cc) => excludeIds?.includes(cc.channelId)).map((cc) => cc.channelId);
return database.get<ChannelModel>('Channel').query(mQ.where('id', mQ.oneOf(ids))).observe();
}),
);
},
};
});
describe('components/channel_list/categories/body', () => {
let database: Database;
let category: CategoryModel;

View File

@@ -7,7 +7,6 @@ import Animated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 're
import {fetchDirectChannelsInfo} from '@actions/remote/channel';
import ChannelItem from '@components/channel_item';
import {DMS_CATEGORY} from '@constants/categories';
import {useServerUrl} from '@context/server';
import {isDMorGM} from '@utils/channel';
@@ -17,7 +16,6 @@ import type ChannelModel from '@typings/database/models/servers/channel';
type Props = {
sortedChannels: ChannelModel[];
category: CategoryModel;
limit: number;
onChannelSwitch: (channelId: string) => void;
unreadIds: Set<string>;
unreadsOnTop: boolean;
@@ -25,16 +23,13 @@ type Props = {
const extractKey = (item: ChannelModel) => item.id;
const CategoryBody = ({sortedChannels, unreadIds, unreadsOnTop, category, limit, onChannelSwitch}: Props) => {
const CategoryBody = ({sortedChannels, unreadIds, unreadsOnTop, category, onChannelSwitch}: Props) => {
const serverUrl = useServerUrl();
const ids = useMemo(() => {
const filteredChannels = unreadsOnTop ? sortedChannels.filter((c) => !unreadIds.has(c.id)) : sortedChannels;
if (category.type === DMS_CATEGORY && limit > 0) {
return filteredChannels.slice(0, limit);
}
return filteredChannels;
}, [category.type, limit, sortedChannels, unreadIds, unreadsOnTop]);
}, [category.type, sortedChannels, unreadIds, unreadsOnTop]);
const unreadChannels = useMemo(() => {
return unreadsOnTop ? [] : ids.filter((c) => unreadIds.has(c.id));

View File

@@ -1,33 +1,26 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Database, Q} from '@nozbe/watermelondb';
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {combineLatest, of as of$} from 'rxjs';
import {map, switchMap, combineLatestWith} from 'rxjs/operators';
import {of as of$} from 'rxjs';
import {switchMap, combineLatestWith, distinctUntilChanged} from 'rxjs/operators';
import {General, Preferences} from '@constants';
import {Preferences} from '@constants';
import {DMS_CATEGORY} from '@constants/categories';
import {getSidebarPreferenceAsBool} from '@helpers/api/preference';
import {observeChannelsByCategoryChannelSortOrder, observeChannelsByLastPostAtInCategory} from '@queries/servers/categories';
import {observeArchivedDirectChannels, observeNotifyPropsByChannels, queryChannelsByNames, queryEmptyDirectAndGroupChannels} from '@queries/servers/channel';
import {observeArchivedDirectChannels, observeNotifyPropsByChannels} from '@queries/servers/channel';
import {queryPreferencesByCategoryAndName, querySidebarPreferences} from '@queries/servers/preference';
import {observeCurrentChannelId, observeCurrentUserId, observeLastUnreadChannelId} from '@queries/servers/system';
import {getDirectChannelName} from '@utils/channel';
import {ChannelWithMyChannel, filterArchivedChannels, filterAutoclosedDMs, filterManuallyClosedDms, getUnreadIds, sortChannels} from '@utils/categories';
import CategoryBody from './category_body';
import type {WithDatabaseArgs} from '@typings/database/database';
import type CategoryModel from '@typings/database/models/servers/category';
import type ChannelModel from '@typings/database/models/servers/channel';
import type MyChannelModel from '@typings/database/models/servers/my_channel';
import type PreferenceModel from '@typings/database/models/servers/preference';
type ChannelData = Pick<ChannelModel, 'id' | 'displayName'> & {
isMuted: boolean;
};
type EnhanceProps = {
category: CategoryModel;
locale: string;
@@ -35,87 +28,45 @@ type EnhanceProps = {
isTablet: boolean;
} & WithDatabaseArgs
const sortAlpha = (locale: string, a: ChannelData, b: ChannelData) => {
if (a.isMuted && !b.isMuted) {
return 1;
} else if (!a.isMuted && b.isMuted) {
return -1;
}
return a.displayName.localeCompare(b.displayName, locale, {numeric: true});
};
const filterArchived = (channels: Array<ChannelModel | null>, currentChannelId: string) => {
return channels.filter((c): c is ChannelModel => c != null && ((c.deleteAt > 0 && c.id === currentChannelId) || !c.deleteAt));
};
const buildAlphaData = (channels: ChannelModel[], notifyProps: Record<string, Partial<ChannelNotifyProps>>, locale: string) => {
const chanelsById = channels.reduce((result: Record<string, ChannelModel>, c) => {
result[c.id] = c;
return result;
}, {});
const combined = channels.map((c) => {
const s = notifyProps[c.id];
return {
id: c.id,
displayName: c.displayName,
isMuted: s?.mark_unread === General.MENTION,
};
});
combined.sort(sortAlpha.bind(null, locale));
return of$(combined.map((cdata) => chanelsById[cdata.id]));
};
const observeSortedChannels = (database: Database, category: CategoryModel, excludeIds: string[], locale: string) => {
switch (category.sorting) {
case 'alpha': {
const channels = category.channels.extend(Q.where('id', Q.notIn(excludeIds))).observeWithColumns(['display_name']);
const notifyProps = channels.pipe(switchMap((cs) => observeNotifyPropsByChannels(database, cs)));
return combineLatest([channels, notifyProps]).pipe(
switchMap(([cs, np]) => buildAlphaData(cs, np, locale)),
);
}
case 'manual': {
return observeChannelsByCategoryChannelSortOrder(database, category, excludeIds);
}
default:
return observeChannelsByLastPostAtInCategory(database, category, excludeIds);
}
};
const mapPrefName = (prefs: PreferenceModel[]) => of$(prefs.map((p) => p.name));
const mapChannelIds = (channels: ChannelModel[] | MyChannelModel[]) => of$(channels.map((c) => c.id));
const withUserId = withObservables([], ({database}: WithDatabaseArgs) => ({currentUserId: observeCurrentUserId(database)}));
const enhance = withObservables(['category', 'isTablet', 'locale'], ({category, locale, isTablet, database, currentUserId}: EnhanceProps) => {
const dmMap = (p: PreferenceModel) => getDirectChannelName(p.name, currentUserId);
const observeCategoryChannels = (category: CategoryModel) => {
const myChannels = category.myChannels.observeWithColumns(['last_post_at', 'is_unread']);
const channels = category.channels.observeWithColumns(['create_at', 'display_name']);
const manualSort = category.categoryChannelsBySortOrder.observeWithColumns(['sort_order']);
return myChannels.pipe(
combineLatestWith(channels, manualSort),
switchMap(([my, cs, sorted]) => {
const channelMap = new Map<string, ChannelModel>(cs.map((c) => [c.id, c]));
const categoryChannelMap = new Map<string, number>(sorted.map((s) => [s.channelId, s.sortOrder]));
return of$(my.reduce<ChannelWithMyChannel[]>((result, myChannel) => {
const channel = channelMap.get(myChannel.id);
if (channel) {
const channelWithMyChannel: ChannelWithMyChannel = {
channel,
myChannel,
sortOrder: categoryChannelMap.get(myChannel.id) || 0,
};
result.push(channelWithMyChannel);
}
const currentChannelId = observeCurrentChannelId(database);
return result;
}, []));
}),
);
};
const hiddenDmIds = queryPreferencesByCategoryAndName(database, Preferences.CATEGORIES.DIRECT_CHANNEL_SHOW, undefined, 'false').
observeWithColumns(['value']).pipe(
switchMap((prefs: PreferenceModel[]) => {
const names = prefs.map(dmMap);
const channels = queryChannelsByNames(database, names).observe();
const enhanced = withObservables([], ({category, currentUserId, database, isTablet, locale}: EnhanceProps) => {
const channelsWithMyChannel = observeCategoryChannels(category);
const currentChannelId = isTablet ? observeCurrentChannelId(database) : of$('');
const lastUnreadId = isTablet ? observeLastUnreadChannelId(database) : of$(undefined);
return channels.pipe(
switchMap(mapChannelIds),
);
}),
const unreadsOnTop = querySidebarPreferences(database, Preferences.CHANNEL_SIDEBAR_GROUP_UNREADS).
observeWithColumns(['value']).
pipe(
switchMap((prefs: PreferenceModel[]) => of$(getSidebarPreferenceAsBool(prefs, Preferences.CHANNEL_SIDEBAR_GROUP_UNREADS))),
);
const emptyDmIds = queryEmptyDirectAndGroupChannels(database).observeWithColumns(['last_post_at']).pipe(
switchMap(mapChannelIds),
);
const archivedDmIds = observeArchivedDirectChannels(database, currentUserId).pipe(
switchMap(mapChannelIds),
);
let limit = of$(Preferences.CHANNEL_SIDEBAR_LIMIT_DMS_DEFAULT);
if (category.type === DMS_CATEGORY) {
limit = querySidebarPreferences(database, Preferences.CHANNEL_SIDEBAR_LIMIT_DMS).
@@ -126,54 +77,61 @@ const enhance = withObservables(['category', 'isTablet', 'locale'], ({category,
);
}
const unreadsOnTop = querySidebarPreferences(database, Preferences.CHANNEL_SIDEBAR_GROUP_UNREADS).
observeWithColumns(['value']).
pipe(
switchMap((prefs: PreferenceModel[]) => of$(getSidebarPreferenceAsBool(prefs, Preferences.CHANNEL_SIDEBAR_GROUP_UNREADS))),
);
const lastUnreadId = isTablet ? observeLastUnreadChannelId(database) : of$(undefined);
const hiddenChannelIds = queryPreferencesByCategoryAndName(database, Preferences.CATEGORIES.GROUP_CHANNEL_SHOW, undefined, 'false').
observeWithColumns(['value']).pipe(
switchMap(mapPrefName),
combineLatestWith(hiddenDmIds, emptyDmIds, archivedDmIds, lastUnreadId),
switchMap(([hIds, hDmIds, eDmIds, aDmIds, excludeId]) => {
const hidden = new Set(hIds.concat(hDmIds, eDmIds, aDmIds));
if (excludeId) {
hidden.delete(excludeId);
}
return of$(hidden);
}),
);
const sortedChannels = hiddenChannelIds.pipe(
switchMap((excludeIds) => observeSortedChannels(database, category, Array.from(excludeIds), locale)),
combineLatestWith(currentChannelId),
map(([channels, ccId]) => filterArchived(channels, ccId)),
const notifyPropsPerChannel = channelsWithMyChannel.pipe(
// eslint-disable-next-line max-nested-callbacks
switchMap((cwms) => observeNotifyPropsByChannels(database, cwms.map((c) => c.myChannel))),
);
const unreadChannels = category.myChannels.observeWithColumns(['mentions_count', 'is_unread']);
const notifyProps = unreadChannels.pipe(switchMap((myChannels) => observeNotifyPropsByChannels(database, myChannels)));
const unreadIds = unreadChannels.pipe(
combineLatestWith(notifyProps, lastUnreadId),
map(([my, settings, lastUnread]) => {
return my.reduce<Set<string>>((set, m) => {
const isMuted = settings[m.id]?.mark_unread === 'mention';
if ((isMuted && m.mentionsCount) || (!isMuted && m.isUnread) || m.id === lastUnread) {
set.add(m.id);
}
return set;
}, new Set());
const hiddenDmPrefs = queryPreferencesByCategoryAndName(database, Preferences.CATEGORIES.DIRECT_CHANNEL_SHOW, undefined, 'false').
observeWithColumns(['value']);
const hiddenGmPrefs = queryPreferencesByCategoryAndName(database, Preferences.CATEGORIES.GROUP_CHANNEL_SHOW, undefined, 'false').
observeWithColumns(['value']);
const manuallyClosedPrefs = hiddenDmPrefs.pipe(
combineLatestWith(hiddenGmPrefs),
switchMap(([dms, gms]) => of$(dms.concat(gms))),
);
const approxViewTimePrefs = queryPreferencesByCategoryAndName(database, Preferences.CATEGORIES.CHANNEL_APPROXIMATE_VIEW_TIME, undefined).
observeWithColumns(['value']);
const openTimePrefs = queryPreferencesByCategoryAndName(database, Preferences.CATEGORIES.CHANNEL_OPEN_TIME, undefined).
observeWithColumns(['value']);
const autoclosePrefs = approxViewTimePrefs.pipe(
combineLatestWith(openTimePrefs),
switchMap(([viewTimes, openTimes]) => of$(viewTimes.concat(openTimes))),
);
const categorySorting = category.observe().pipe(
switchMap((c) => of$(c.sorting)),
distinctUntilChanged(),
);
const deactivated = (category.type === DMS_CATEGORY) ? observeArchivedDirectChannels(database, currentUserId) : of$(undefined);
const sortedChannels = channelsWithMyChannel.pipe(
combineLatestWith(categorySorting, currentChannelId, lastUnreadId, notifyPropsPerChannel, manuallyClosedPrefs, autoclosePrefs, deactivated, limit),
switchMap(([cwms, sorting, channelId, unreadId, notifyProps, manuallyClosedDms, autoclose, deactivatedDMS, maxDms]) => {
let channelsW = cwms;
channelsW = filterArchivedChannels(channelsW, channelId);
channelsW = filterManuallyClosedDms(channelsW, notifyProps, manuallyClosedDms, currentUserId, unreadId);
channelsW = filterAutoclosedDMs(category.type, maxDms, channelId, channelsW, autoclose, notifyProps, deactivatedDMS, unreadId);
return of$(sortChannels(sorting, channelsW, notifyProps, locale));
}),
);
const unreadIds = channelsWithMyChannel.pipe(
combineLatestWith(notifyPropsPerChannel, lastUnreadId),
switchMap(([cwms, notifyProps, unreadId]) => {
return of$(getUnreadIds(cwms, notifyProps, unreadId));
}),
);
return {
limit,
sortedChannels,
unreadsOnTop,
unreadIds,
category,
sortedChannels,
unreadIds,
unreadsOnTop,
};
});
export default withDatabase(withUserId(enhance(CategoryBody)));
export default withDatabase(withUserId(enhanced(CategoryBody)));

View File

@@ -54,7 +54,7 @@ const enhanced = withObservables(['currentTeamId', 'isTablet', 'onlyUnreads'], (
const channels = myUnreadChannels.pipe(switchMap((myChannels) => observeChannelsByLastPostAt(database, myChannels)));
const channelsMap = channels.pipe(switchMap((cs) => of$(makeChannelsMap(cs))));
return queryMyChannelUnreads(database, currentTeamId).observeWithColumns(['last_post_at', 'is_unread']).pipe(
return myUnreadChannels.pipe(
combineLatestWith(channelsMap, notifyProps),
map(filterAndSortMyChannels),
combineLatestWith(lastUnread),

View File

@@ -1,6 +1,197 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {General, Preferences} from '@constants';
import {DMS_CATEGORY} from '@constants/categories';
import {getPreferenceAsBool, getPreferenceValue} from '@helpers/api/preference';
import {isDMorGM} from '@utils/channel';
import {getUserIdFromChannelName} from '@utils/user';
import type ChannelModel from '@typings/database/models/servers/channel';
import type MyChannelModel from '@typings/database/models/servers/my_channel';
import type PreferenceModel from '@typings/database/models/servers/preference';
import type UserModel from '@typings/database/models/servers/user';
export type ChannelWithMyChannel = {
channel: ChannelModel;
myChannel: MyChannelModel;
sortOrder: number;
}
export function makeCategoryChannelId(teamId: string, channelId: string) {
return `${teamId}_${channelId}`;
}
export const isUnreadChannel = (myChannel: MyChannelModel, notifyProps?: Partial<ChannelNotifyProps>, lastUnreadChannelId?: string) => {
const isMuted = notifyProps?.mark_unread === General.MENTION;
return (isMuted && myChannel.mentionsCount) || (!isMuted && myChannel.isUnread) || (myChannel.id === lastUnreadChannelId);
};
export const filterArchivedChannels = (channelsWithMyChannel: ChannelWithMyChannel[], currentChannelId: string) => {
return channelsWithMyChannel.filter((cwm) => cwm.channel.deleteAt === 0 || cwm.channel.id === currentChannelId);
};
export const filterAutoclosedDMs = (
categoryType: CategoryType, limit: number, currentChannelId: string,
channelsWithMyChannel: ChannelWithMyChannel[], preferences: PreferenceModel[],
notifyPropsPerChannel: Record<string, Partial<ChannelNotifyProps>>,
deactivatedDMs?: Map<string, UserModel | undefined >,
lastUnreadChannelId?: string,
) => {
if (categoryType !== DMS_CATEGORY) {
// Only autoclose DMs that haven't been assigned to a category
return channelsWithMyChannel;
}
const getLastViewedAt = (cwm: ChannelWithMyChannel) => {
// 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.
const id = cwm.channel.id;
return Math.max(
cwm.myChannel.lastViewedAt,
getPreferenceValue<number>(preferences, Preferences.CATEGORIES.CHANNEL_APPROXIMATE_VIEW_TIME, id, 0),
getPreferenceValue<number>(preferences, Preferences.CATEGORIES.CHANNEL_OPEN_TIME, id, 0),
);
};
let unreadCount = 0;
let visibleChannels = channelsWithMyChannel.filter((cwm) => {
const {channel, myChannel} = cwm;
if (myChannel.isUnread) {
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 === General.DM_CHANNEL) {
const lastViewedAt = getLastViewedAt(cwm);
const teammate = deactivatedDMs?.get(channel.id);
if (teammate && teammate.deleteAt > lastViewedAt) {
return false;
}
}
return true;
});
visibleChannels.sort((cwmA, cwmB) => {
const channelA = cwmA.channel;
const channelB = cwmB.channel;
const myChannelA = cwmA.myChannel;
const myChannelB = cwmB.myChannel;
// 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
const isUnreadA = isUnreadChannel(myChannelA, notifyPropsPerChannel[myChannelA.id], lastUnreadChannelId);
const isUnreadB = isUnreadChannel(myChannelB, notifyPropsPerChannel[myChannelB.id], lastUnreadChannelId);
if (isUnreadA && !isUnreadB) {
return -1;
} else if (isUnreadB && !isUnreadA) {
return 1;
}
// Third priority is last_viewed_at
const channelAlastViewed = getLastViewedAt(cwmA) || 0;
const channelBlastViewed = getLastViewedAt(cwmB) || 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(limit, unreadCount);
visibleChannels = visibleChannels.slice(0, remaining);
return visibleChannels;
};
export const filterManuallyClosedDms = (
channelsWithMyChannel: ChannelWithMyChannel[],
notifyPropsPerChannel: Record<string, Partial<ChannelNotifyProps>>,
preferences: PreferenceModel[],
currentUserId: string,
lastUnreadChannelId?: string,
) => {
return channelsWithMyChannel.filter((cwm) => {
const {channel, myChannel} = cwm;
if (!isDMorGM(channel)) {
return true;
} else if (!myChannel.lastPostAt) {
// If the direct channel does not have posts we hide it
return false;
}
if (isUnreadChannel(myChannel, notifyPropsPerChannel[myChannel.id], lastUnreadChannelId)) {
// Unread DMs/GMs are always visible
return true;
}
if (channel.type === General.DM_CHANNEL) {
const teammateId = getUserIdFromChannelName(currentUserId, channel.name);
return getPreferenceAsBool(preferences, Preferences.CATEGORIES.DIRECT_CHANNEL_SHOW, teammateId, true);
}
return getPreferenceAsBool(preferences, Preferences.CATEGORIES.GROUP_CHANNEL_SHOW, channel.id, true);
});
};
const sortChannelsByName = (notifyPropsPerChannel: Record<string, Partial<ChannelNotifyProps>>, locale: string) => {
return (a: ChannelWithMyChannel, b: ChannelWithMyChannel) => {
// Sort muted channels last
const aMuted = notifyPropsPerChannel[a.channel.id]?.mark_unread === General.MENTION;
const bMuted = notifyPropsPerChannel[b.channel.id]?.mark_unread === General.MENTION;
if (aMuted && !bMuted) {
return 1;
} else if (!aMuted && bMuted) {
return -1;
}
// And then sort alphabetically
return a.channel.displayName.localeCompare(b.channel.displayName, locale, {numeric: true});
};
};
export const sortChannels = (sorting: CategorySorting, channelsWithMyChannel: ChannelWithMyChannel[], notifyPropsPerChannel: Record<string, Partial<ChannelNotifyProps>>, locale: string) => {
if (sorting === 'recent') {
return channelsWithMyChannel.sort((cwmA, cwmB) => {
return cwmB.myChannel.lastPostAt - cwmA.myChannel.lastPostAt;
}).map((cwm) => cwm.channel);
} else if (sorting === 'manual') {
return channelsWithMyChannel.sort((cwmA, cwmB) => {
return cwmB.sortOrder - cwmA.sortOrder;
}).map((cwm) => cwm.channel);
}
const sortByName = sortChannelsByName(notifyPropsPerChannel, locale);
return channelsWithMyChannel.sort(sortByName).map((cwm) => cwm.channel);
};
export const getUnreadIds = (cwms: ChannelWithMyChannel[], notifyPropsPerChannel: Record<string, Partial<ChannelNotifyProps>>, lastUnreadId?: string) => {
return cwms.reduce<Set<string>>((result, cwm) => {
if (isUnreadChannel(cwm.myChannel, notifyPropsPerChannel, lastUnreadId)) {
result.add(cwm.channel.id);
}
return result;
}, new Set());
};

View File

@@ -25,7 +25,7 @@ declare class CategoryModel extends Model {
displayName: string;
/** type : The type of category */
type: string;
type: CategoryType;
/** sort_order : The sort order for this category */
sortOrder: number;