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 <nahumhbl@gmail.com>
This commit is contained in:
Shaz MJ 2022-03-11 08:19:49 +11:00 committed by GitHub
parent 5b44676985
commit 23ff6e8a09
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 155 additions and 85 deletions

View file

@ -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<CategoryChannelModel>(CATEGORY_CHANNEL).query(
Q.where('category_id', category.id),
).fetch();
const channelIds = await database.get<ChannelModel>(CHANNEL).query(
Q.on(CATEGORY_CHANNEL, 'category_id', category.id),
).fetchIds();
myChannels = await database.get<MyChannelModel>(MY_CHANNEL).query(
Q.where('id', Q.oneOf(channelIds)),
).fetch();
});
it('should match snapshot', () => {
const wrapper = renderWithEverything(
<CategoryBody
category={category}
myChannels={myChannels}
categoryChannels={categoryChannels}
channels={channels}
locale={DEFAULT_LOCALE}
currentChannelId={''}
/>,
{database},
);

View file

@ -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 (
<ChannelListItem channelId={item}/>
);
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 (
<ChannelListItem
channelId={item}
isActive={item === currentChannelId}
/>
);
}, [currentChannelId]);
return (
<FlatList
data={data}
data={sortedIds}
renderItem={ChannelItem}
keyExtractor={extractKey}
removeClippedSubviews={true}

View file

@ -30,7 +30,8 @@ describe('components/channel_list/categories/body/channel/item', () => {
it('should match snapshot', () => {
const wrapper = renderWithIntlAndTheme(
<ChannelListItem
channel={{displayName: 'Hello!', type: 'G', shared: false, name: 'hello'}}
channel={{displayName: 'Hello!', type: 'G', shared: false, name: 'hello', deleteAt: 0}}
isActive={false}
isOwnDirectMessage={false}
myChannel={myChannel}
/>,

View file

@ -46,12 +46,13 @@ const textStyle = StyleSheet.create({
});
type Props = {
channel: Pick<ChannelModel, 'displayName' | 'name' | 'shared' | 'type'>;
channel: Pick<ChannelModel, 'deleteAt' | 'displayName' | 'name' | 'shared' | 'type'>;
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 (
<TouchableOpacity onPress={switchChannels}>
<View style={styles.container}>
<ChannelIcon
isActive={isActive}
isArchived={channel.deleteAt > 0}
membersCount={membersCount}
name={channel.name}
shared={channel.shared}

View file

@ -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,

View file

@ -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<ChannelModel, 'id' | 'displayName'> & {
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<MyChannelSettingsModel>(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));

View file

@ -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 (
<>
<CategoryHeader category={data.item}/>
<CategoryBody category={data.item}/>
</>
);
};
const Categories = ({categories, currentChannelId}: Props) => {
const intl = useIntl();
const renderCategory = useCallback((data: {item: CategoryModel}) => {
return (
<>
<CategoryHeader category={data.item}/>
<CategoryBody
category={data.item}
currentChannelId={currentChannelId}
locale={intl.locale}
/>
</>
);
}, [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 <LoadCategoriesError/>;
}
// Sort Categories
props.categories.sort((a, b) => a.sortOrder - b.sortOrder);
return (
<FlatList
data={props.categories}
data={categories}
renderItem={renderCategory}
style={styles.flex}
showsHorizontalScrollIndicator={false}

View file

@ -4,26 +4,34 @@
import {Q} from '@nozbe/watermelondb';
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {MM_TABLES} from '@constants/database';
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
import Categories from './categories';
import type {WithDatabaseArgs} from '@typings/database/database';
import type CategoryModel from '@typings/database/models/servers/category';
import type SystemModel from '@typings/database/models/servers/system';
const {SERVER: {CATEGORY}} = MM_TABLES;
const {SERVER: {CATEGORY, SYSTEM}} = MM_TABLES;
const {CURRENT_CHANNEL_ID} = SYSTEM_IDENTIFIERS;
type WithDatabaseProps = {currentTeamId: string } & WithDatabaseArgs
const enhanced = withObservables(
['currentTeamId'],
({currentTeamId, database}: WithDatabaseProps) => {
const currentChannelId = database.get<SystemModel>(SYSTEM).findAndObserve(CURRENT_CHANNEL_ID).pipe(
switchMap(({value}) => of$(value)),
);
const categories = database.get<CategoryModel>(CATEGORY).query(
Q.where('team_id', currentTeamId),
).observeWithColumns(['sort_order']);
return {
currentChannelId,
categories,
};
});

View file

@ -67,18 +67,20 @@ export default class CategoryModel extends Model implements CategoryInterface {
@children(CATEGORY_CHANNEL) categoryChannels!: Query<CategoryChannelModel>;
/** 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<ChannelModel>(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),
),
),