From 23ff6e8a091947129319d9d0e8b5ed373be1854e Mon Sep 17 00:00:00 2001 From: Shaz MJ Date: Fri, 11 Mar 2022 08:19:49 +1100 Subject: [PATCH] Sorting categories body in the observable (#6041) * Sorting categories body in the observable * feedback review * Fix category queries to return only channels that have been loaded * Do not display archived channels unless is the active one Co-authored-by: Elias Nahum --- .../categories/body/category_body.test.tsx | 28 +----- .../categories/body/category_body.tsx | 41 +++------ .../body/channel/channel_list_item.test.tsx | 3 +- .../body/channel/channel_list_item.tsx | 11 ++- .../categories/body/channel/index.ts | 1 + .../channel_list/categories/body/index.ts | 91 +++++++++++++++++-- .../channel_list/categories/categories.tsx | 37 +++++--- .../channel_list/categories/index.ts | 12 ++- app/database/models/server/category.ts | 16 ++-- 9 files changed, 155 insertions(+), 85 deletions(-) diff --git a/app/components/channel_list/categories/body/category_body.test.tsx b/app/components/channel_list/categories/body/category_body.test.tsx index 7a0027400..220ac79ae 100644 --- a/app/components/channel_list/categories/body/category_body.test.tsx +++ b/app/components/channel_list/categories/body/category_body.test.tsx @@ -5,24 +5,19 @@ import {Database, Q} from '@nozbe/watermelondb'; import React from 'react'; import {MM_TABLES} from '@constants/database'; +import {DEFAULT_LOCALE} from '@i18n'; import {renderWithEverything} from '@test/intl-test-helper'; import TestHelper from '@test/test_helper'; -import CategoryBody from './category_body'; +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'; -import type MyChannelModel from '@typings/database/models/servers/my_channel'; -const {SERVER: {CATEGORY, CATEGORY_CHANNEL, CHANNEL, MY_CHANNEL}} = MM_TABLES; +const {SERVER: {CATEGORY}} = MM_TABLES; describe('components/channel_list/categories/body', () => { let database: Database; let category: CategoryModel; - let categoryChannels: CategoryChannelModel[]; - let myChannels: MyChannelModel[]; - let channels: ChannelModel[]; beforeAll(async () => { const server = await TestHelper.setupServerDatabase(); @@ -33,27 +28,14 @@ describe('components/channel_list/categories/body', () => { ).fetch(); category = categories[0]; - - categoryChannels = await database.get(CATEGORY_CHANNEL).query( - Q.where('category_id', category.id), - ).fetch(); - - const channelIds = await database.get(CHANNEL).query( - Q.on(CATEGORY_CHANNEL, 'category_id', category.id), - ).fetchIds(); - - myChannels = await database.get(MY_CHANNEL).query( - Q.where('id', Q.oneOf(channelIds)), - ).fetch(); }); it('should match snapshot', () => { const wrapper = renderWithEverything( , {database}, ); diff --git a/app/components/channel_list/categories/body/category_body.tsx b/app/components/channel_list/categories/body/category_body.tsx index 8d0173978..ea8405d3e 100644 --- a/app/components/channel_list/categories/body/category_body.tsx +++ b/app/components/channel_list/categories/body/category_body.tsx @@ -1,27 +1,14 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useMemo} from 'react'; +import React, {useCallback} from 'react'; import {FlatList} from 'react-native'; import ChannelListItem from './channel'; -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'; -import type MyChannelModel from '@typings/database/models/servers/my_channel'; - type Props = { - category: CategoryModel; - channels: ChannelModel[]; - myChannels: MyChannelModel[]; - categoryChannels: CategoryChannelModel[]; -}; - -const ChannelItem = ({item}: {item: string}) => { - return ( - - ); + currentChannelId: string; + sortedIds: string[]; }; const extractKey = (item: any) => item; @@ -29,21 +16,19 @@ const itemLayout = (d: any, index: number) => ( {length: 40, offset: 40 * index, index} ); -const CategoryBody = ({category, categoryChannels, channels, myChannels}: Props) => { - const data: string[] = useMemo(() => { - switch (category.sorting) { - case 'alpha': - return channels.map((c) => c.id); - case 'manual': - return categoryChannels.map((cc) => cc.channelId); - default: - return myChannels.map((m) => m.id); - } - }, [category.sorting, categoryChannels, channels, myChannels]); +const CategoryBody = ({currentChannelId, sortedIds}: Props) => { + const ChannelItem = useCallback(({item}: {item: string}) => { + return ( + + ); + }, [currentChannelId]); return ( { it('should match snapshot', () => { const wrapper = renderWithIntlAndTheme( , diff --git a/app/components/channel_list/categories/body/channel/channel_list_item.tsx b/app/components/channel_list/categories/body/channel/channel_list_item.tsx index e8af22910..0259377aa 100644 --- a/app/components/channel_list/categories/body/channel/channel_list_item.tsx +++ b/app/components/channel_list/categories/body/channel/channel_list_item.tsx @@ -46,12 +46,13 @@ const textStyle = StyleSheet.create({ }); type Props = { - channel: Pick; + channel: Pick; + isActive: boolean; isOwnDirectMessage: boolean; myChannel: MyChannelModel; } -const ChannelListItem = ({channel, isOwnDirectMessage, myChannel}: Props) => { +const ChannelListItem = ({channel, isActive, isOwnDirectMessage, myChannel}: Props) => { const {formatMessage} = useIntl(); const theme = useTheme(); const styles = getStyleSheet(theme); @@ -80,10 +81,16 @@ const ChannelListItem = ({channel, isOwnDirectMessage, myChannel}: Props) => { displayName = formatMessage({id: 'channel_header.directchannel.you', defaultMessage: '{displayName} (you)'}, {displayName}); } + if (channel.deleteAt > 0 && !isActive) { + return null; + } + return ( 0} membersCount={membersCount} name={channel.name} shared={channel.shared} diff --git a/app/components/channel_list/categories/body/channel/index.ts b/app/components/channel_list/categories/body/channel/index.ts index d0cdfc494..94b16a82b 100644 --- a/app/components/channel_list/categories/body/channel/index.ts +++ b/app/components/channel_list/categories/body/channel/index.ts @@ -42,6 +42,7 @@ const enhance = withObservables(['channelId'], ({channelId, database}: {channelI myChannel, channel: channel.pipe( switchMap((c: ChannelModel) => of$({ + deleteAt: c.deleteAt, displayName: c.displayName, name: c.name, shared: c.shared, diff --git a/app/components/channel_list/categories/body/index.ts b/app/components/channel_list/categories/body/index.ts index 241cb4a35..d720c35cd 100644 --- a/app/components/channel_list/categories/body/index.ts +++ b/app/components/channel_list/categories/body/index.ts @@ -1,17 +1,94 @@ // 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 {switchMap} from 'rxjs/operators'; + +import {MM_TABLES} from '@app/constants/database'; +import {General} from '@constants'; +import {WithDatabaseArgs} from '@typings/database/database'; import CategoryBody from './category_body'; import type CategoryModel from '@typings/database/models/servers/category'; +import type ChannelModel from '@typings/database/models/servers/channel'; +import type MyChannelSettingsModel from '@typings/database/models/servers/my_channel_settings'; -const withCategory = withObservables(['category'], ({category}: {category: CategoryModel}) => ({ - category, - categoryChannels: category.categoryChannelsBySortOrder.observeWithColumns(['sort_order']), - myChannels: category.myChannels.observeWithColumns(['last_post_at']), - channels: category.channels.observeWithColumns(['display_name']), -})); +type ChannelData = Pick & { + isMuted: boolean; +}; -export default withCategory(CategoryBody); +const {SERVER: {MY_CHANNEL_SETTINGS}} = MM_TABLES; + +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 buildAlphaData = (channels: ChannelModel[], settings: MyChannelSettingsModel[], locale: string) => { + const combined = channels.map((c) => { + const s = settings.find((setting) => setting.id === c.id); + return { + id: c.id, + displayName: c.displayName, + isMuted: s?.notifyProps?.mark_unread === General.MENTION, + }; + }); + + combined.sort(sortAlpha.bind(null, locale)); + + return of$(combined.map((c) => c.id)); +}; + +const querySettings = (database: Database, channels: ChannelModel[]) => { + const ids = channels.map((c) => c.id); + return database.get(MY_CHANNEL_SETTINGS). + query( + Q.where('id', Q.oneOf(ids)), + ).observeWithColumns(['notify_props']); +}; + +const getSortedIds = (database: Database, category: CategoryModel, locale: string) => { + switch (category.sorting) { + case 'alpha': { + const channels = category.channels.observeWithColumns(['display_name']); + const settings = channels.pipe( + switchMap((cs) => querySettings(database, cs)), + ); + return combineLatest([channels, settings]).pipe( + switchMap(([cs, st]) => buildAlphaData(cs, st, locale)), + ); + } + case 'manual': { + return category.categoryChannelsBySortOrder.observeWithColumns(['sort_order']).pipe( + // eslint-disable-next-line max-nested-callbacks + switchMap((cc) => of$(cc.map((c) => c.channelId))), + ); + } + default: + return category.myChannels.observeWithColumns(['last_post_at']).pipe( + // eslint-disable-next-line max-nested-callbacks + switchMap((mc) => of$(mc.map((m) => m.id))), + ); + } +}; + +const enhance = withObservables(['category'], ({category, locale, database}: {category: CategoryModel; locale: string} & WithDatabaseArgs) => { + const sortedIds = category.observe().pipe( + switchMap((c) => getSortedIds(database, c, locale)), + ); + + return { + sortedIds, + }; +}); + +export default withDatabase(enhance(CategoryBody)); diff --git a/app/components/channel_list/categories/categories.tsx b/app/components/channel_list/categories/categories.tsx index eeaab72f9..406b12294 100644 --- a/app/components/channel_list/categories/categories.tsx +++ b/app/components/channel_list/categories/categories.tsx @@ -1,7 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React from 'react'; +import React, {useCallback} from 'react'; +import {useIntl} from 'react-intl'; import {FlatList, StyleSheet} from 'react-native'; import CategoryBody from './body'; @@ -12,6 +13,7 @@ import type {CategoryModel} from '@database/models/server'; type Props = { categories: CategoryModel[]; + currentChannelId: string; } const styles = StyleSheet.create({ @@ -22,26 +24,31 @@ const styles = StyleSheet.create({ const extractKey = (item: CategoryModel) => item.id; -const renderCategory = (data: {item: CategoryModel}) => { - return ( - <> - - - - ); -}; +const Categories = ({categories, currentChannelId}: Props) => { + const intl = useIntl(); + const renderCategory = useCallback((data: {item: CategoryModel}) => { + return ( + <> + + + + ); + }, [categories, currentChannelId, intl.locale]); -const Categories = (props: Props) => { - if (!props.categories.length) { + // Sort Categories + categories.sort((a, b) => a.sortOrder - b.sortOrder); + + if (!categories.length) { return ; } - // Sort Categories - props.categories.sort((a, b) => a.sortOrder - b.sortOrder); - return ( { + const currentChannelId = database.get(SYSTEM).findAndObserve(CURRENT_CHANNEL_ID).pipe( + switchMap(({value}) => of$(value)), + ); const categories = database.get(CATEGORY).query( Q.where('team_id', currentTeamId), ).observeWithColumns(['sort_order']); return { + currentChannelId, categories, }; }); diff --git a/app/database/models/server/category.ts b/app/database/models/server/category.ts index f79b1e978..8e651b416 100644 --- a/app/database/models/server/category.ts +++ b/app/database/models/server/category.ts @@ -67,18 +67,20 @@ export default class CategoryModel extends Model implements CategoryInterface { @children(CATEGORY_CHANNEL) categoryChannels!: Query; /** categoryChannelsBySortOrder : Retrieves assocated category channels sorted by sort_order */ - @lazy categoryChannelsBySortOrder = this.categoryChannels.collection.query( - Q.where('category_id', this.id), - Q.sortBy('sort_order', Q.asc), - ); + @lazy categoryChannelsBySortOrder = this.categoryChannels.collection. + query( + Q.on(MY_CHANNEL, + Q.where('id', Q.notEq('')), + ), + Q.where('category_id', this.id), + Q.sortBy('sort_order', Q.asc), + ); /** channels : Retrieves all the channels that are part of this category */ @lazy channels = this.collections. get(CHANNEL). query( Q.on(CATEGORY_CHANNEL, 'category_id', this.id), - Q.where('delete_at', Q.eq(0)), - Q.sortBy('display_name'), ); /** myChannels : Retrieves all myChannels that are part of this category */ @@ -88,7 +90,7 @@ export default class CategoryModel extends Model implements CategoryInterface { Q.experimentalJoinTables([CHANNEL, CATEGORY_CHANNEL]), Q.on(CATEGORY_CHANNEL, Q.and( - Q.on(CHANNEL, Q.where('delete_at', Q.eq(0))), + Q.on(CHANNEL, Q.where('create_at', Q.gte(0))), Q.where('category_id', this.id), ), ),