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:
parent
5b44676985
commit
23ff6e8a09
9 changed files with 155 additions and 85 deletions
|
|
@ -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},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
Loading…
Reference in a new issue