forked from Ivasoft/mattermost-mobile
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:
@@ -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")
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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());
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user