Performance improvements in the area of home and channel switch. (#6174)

* Performance improvements

* Revert hidden DM/GMs changes

* Minor fixes related to the revert

* Add observeWithColumns missed in merge
This commit is contained in:
Daniel Espino García 2022-04-15 00:56:48 +02:00 committed by GitHub
parent 667716fd0f
commit 7a90f2d4a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 173 additions and 132 deletions

View file

@ -7,7 +7,6 @@ import {IntlShape} from 'react-intl';
import {storeCategories} from '@actions/local/category';
import {addChannelToDefaultCategory, storeMyChannelsForTeam, switchToChannel} from '@actions/local/channel';
import EphemeralStore from '@app/store/ephemeral_store';
import {General, Preferences} from '@constants';
import DatabaseManager from '@database/manager';
import {privateChannelJoinPrompt} from '@helpers/api/channel';
@ -18,6 +17,7 @@ import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {getCommonSystemValues, getCurrentTeamId, getCurrentUserId} from '@queries/servers/system';
import {prepareMyTeams, getNthLastChannelFromTeam, getMyTeamById, getTeamById, getTeamByName} from '@queries/servers/team';
import {getCurrentUser} from '@queries/servers/user';
import EphemeralStore from '@store/ephemeral_store';
import {generateChannelNameFromDisplayName, getDirectChannelName, isDMorGM} from '@utils/channel';
import {PERMALINK_GENERIC_TEAM_NAME_REDIRECT} from '@utils/url';
import {displayGroupMessageName, displayUsername} from '@utils/user';

View file

@ -17,7 +17,6 @@ import {fetchMissingSidebarInfo, fetchMyChannel, fetchChannelStats, fetchChannel
import {fetchPostsForChannel} from '@actions/remote/post';
import {fetchRolesIfNeeded} from '@actions/remote/role';
import {fetchUsersByIds, updateUsersNoLongerVisible} from '@actions/remote/user';
import EphemeralStore from '@app/store/ephemeral_store';
import Events from '@constants/events';
import DatabaseManager from '@database/manager';
import {queryActiveServer} from '@queries/app/servers';
@ -26,6 +25,7 @@ import {prepareCommonSystemValues, getConfig, setCurrentChannelId, getCurrentCha
import {getNthLastChannelFromTeam} from '@queries/servers/team';
import {getCurrentUser, getTeammateNameDisplay, getUserById} from '@queries/servers/user';
import {dismissAllModals, popToRoot} from '@screens/navigation';
import EphemeralStore from '@store/ephemeral_store';
import {isTablet} from '@utils/helpers';
// Received when current user created a channel in a different client

View file

@ -35,9 +35,6 @@ describe('components/channel_list/categories/body', () => {
<CategoryBody
category={category}
locale={DEFAULT_LOCALE}
currentChannelId={''}
currentUserId={''}
unreadChannelIds={new Set()}
/>,
{database},
);

View file

@ -12,7 +12,6 @@ import ChannelListItem from './channel';
import type CategoryModel from '@typings/database/models/servers/category';
type Props = {
currentChannelId: string;
sortedChannels: ChannelModel[];
hiddenChannelIds: Set<string>;
category: CategoryModel;
@ -21,7 +20,7 @@ type Props = {
const extractKey = (item: ChannelModel) => item.id;
const CategoryBody = ({currentChannelId, sortedChannels, category, hiddenChannelIds, limit}: Props) => {
const CategoryBody = ({sortedChannels, category, hiddenChannelIds, limit}: Props) => {
const ids = useMemo(() => {
let filteredChannels = sortedChannels;
@ -36,26 +35,28 @@ const CategoryBody = ({currentChannelId, sortedChannels, category, hiddenChannel
return filteredChannels;
}, [category.type, limit, hiddenChannelIds, sortedChannels]);
const ChannelItem = useCallback(({item}: {item: ChannelModel}) => {
const renderItem = useCallback(({item}: {item: ChannelModel}) => {
return (
<ChannelListItem
channel={item}
isActive={item.id === currentChannelId}
collapsed={category.collapsed}
testID={`category.${category.displayName.replace(/ /g, '_').toLocaleLowerCase()}.channel_list_item`}
/>
);
}, [currentChannelId, category.collapsed]);
}, [category.collapsed]);
return (
<FlatList
data={ids}
renderItem={ChannelItem}
renderItem={renderItem}
keyExtractor={extractKey}
removeClippedSubviews={true}
initialNumToRender={20}
windowSize={15}
updateCellsBatchingPeriod={10}
// @ts-expect-error strictMode not exposed on the types
strictMode={true}
/>
);
};

View file

@ -38,6 +38,7 @@ describe('components/channel_list/categories/body/channel/item', () => {
collapsed={false}
currentUserId={'id'}
testID='channel_list_item'
isVisible={true}
/>,
);

View file

@ -77,10 +77,11 @@ type Props = {
myChannel?: MyChannelModel;
collapsed: boolean;
currentUserId: string;
isVisible: boolean;
testID?: string;
}
const ChannelListItem = ({channel, isActive, currentUserId, isMuted, myChannel, collapsed, testID}: Props) => {
const ChannelListItem = ({channel, isActive, currentUserId, isMuted, isVisible, myChannel, collapsed, testID}: Props) => {
const {formatMessage} = useIntl();
const theme = useTheme();
const styles = getStyleSheet(theme);
@ -132,7 +133,7 @@ const ChannelListItem = ({channel, isActive, currentUserId, isMuted, myChannel,
displayName = formatMessage({id: 'channel_header.directchannel.you', defaultMessage: '{displayName} (you)'}, {displayName});
}
if ((channel.deleteAt > 0 && !isActive) || !myChannel) {
if ((channel.deleteAt > 0 && !isActive) || !myChannel || !isVisible) {
return null;
}

View file

@ -3,41 +3,66 @@
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import React from 'react';
import {of as of$, combineLatest} from 'rxjs';
import {switchMap, distinctUntilChanged} from 'rxjs/operators';
import {Preferences} from '@constants';
import {getPreferenceAsBool} from '@helpers/api/preference';
import {observeMyChannel} from '@queries/servers/channel';
import {observeCurrentUserId} from '@queries/servers/system';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {observeCurrentChannelId, observeCurrentUserId} from '@queries/servers/system';
import ChannelModel from '@typings/database/models/servers/channel';
import MyChannelModel from '@typings/database/models/servers/my_channel';
import ChannelListItem from './channel_list_item';
import type {WithDatabaseArgs} from '@typings/database/database';
import type PreferenceModel from '@typings/database/models/servers/preference';
const enhance = withObservables(['channel'], ({channel, database}: {channel: ChannelModel} & WithDatabaseArgs) => {
const observeIsMutedSetting = (mc: MyChannelModel) => mc.settings.observe().pipe(switchMap((s) => of$(s?.notifyProps?.mark_unread === 'mention')));
const enhance = withObservables(['channel', 'isUnreads'], ({channel, isUnreads, database}: {channel: ChannelModel; isUnreads?: boolean} & WithDatabaseArgs) => {
const currentUserId = observeCurrentUserId(database);
const myChannel = observeMyChannel(database, channel.id);
let isMuted = of$(false);
if (myChannel) {
const settings = myChannel.pipe(
switchMap((mc) => {
return mc ? mc.settings.observe() : of$(undefined);
}),
const isActive = observeCurrentChannelId(database).pipe(switchMap((id) => of$(id ? id === channel.id : false)), distinctUntilChanged());
const unreadsOnTop = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_GROUP_UNREADS).
observeWithColumns(['value']).
pipe(
switchMap((prefs: PreferenceModel[]) => of$(getPreferenceAsBool(prefs, Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_GROUP_UNREADS, false))),
);
isMuted = settings?.pipe(
switchMap((s) => of$(s?.notifyProps?.mark_unread === 'mention')),
);
}
const isVisible = combineLatest([myChannel, unreadsOnTop]).pipe(
switchMap(([mc, u]) => {
if (!mc) {
return of$(false);
}
if (isUnreads) {
return of$(u);
}
return u ? of$(!mc.isUnread) : of$(true);
}),
);
const isMuted = myChannel.pipe(
switchMap((mc) => {
if (!mc) {
return of$(false);
}
return observeIsMutedSetting(mc);
}),
);
return {
currentUserId,
isMuted,
isActive,
isVisible,
myChannel,
channel: channel.observe(),
};
});
export default withDatabase(enhance(ChannelListItem));
export default React.memo(withDatabase(enhance(ChannelListItem)));

View file

@ -11,6 +11,7 @@ import {General, Preferences} from '@constants';
import {DMS_CATEGORY} from '@constants/categories';
import {queryChannelsByNames, queryMyChannelSettingsByIds} from '@queries/servers/channel';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {observeCurrentUserId} from '@queries/servers/system';
import {WithDatabaseArgs} from '@typings/database/database';
import {getDirectChannelName} from '@utils/channel';
@ -70,12 +71,10 @@ export const getChannelsFromRelation = async (relations: CategoryChannelModel[]
return Promise.all(relations.map((r) => r.channel?.fetch()));
};
const getSortedChannels = (database: Database, category: CategoryModel, unreadChannelIds: Set<string>, locale: string) => {
const getSortedChannels = (database: Database, category: CategoryModel, locale: string) => {
switch (category.sorting) {
case 'alpha': {
const channels = category.channels.observeWithColumns(['display_name']).pipe(
map((cs) => cs.filter((c) => !unreadChannelIds.has(c.id))),
);
const channels = category.channels.observeWithColumns(['display_name']);
const settings = channels.pipe(
switchMap((cs) => observeSettings(database, cs)),
);
@ -85,14 +84,12 @@ const getSortedChannels = (database: Database, category: CategoryModel, unreadCh
}
case 'manual': {
return category.categoryChannelsBySortOrder.observeWithColumns(['sort_order']).pipe(
map((cc) => cc.filter((c) => !unreadChannelIds.has(c.channelId))),
map(getChannelsFromRelation),
concatAll(),
);
}
default:
return category.myChannels.observeWithColumns(['last_post_at']).pipe(
map((myCs) => myCs.filter((myC) => !unreadChannelIds.has(myC.id))),
map(getChannelsFromRelation),
concatAll(),
);
@ -107,13 +104,14 @@ type EnhanceProps = {
category: CategoryModel;
locale: string;
currentUserId: string;
unreadChannelIds: Set<string>;
} & WithDatabaseArgs
const enhance = withObservables(['category', 'locale', 'unreadChannelIds'], ({category, locale, database, currentUserId, unreadChannelIds}: EnhanceProps) => {
const withUserId = withObservables([], ({database}: WithDatabaseArgs) => ({currentUserId: observeCurrentUserId(database)}));
const enhance = withObservables(['category', 'locale'], ({category, locale, database, currentUserId}: EnhanceProps) => {
const observedCategory = category.observe();
const sortedChannels = observedCategory.pipe(
switchMap((c) => getSortedChannels(database, c, unreadChannelIds, locale)),
switchMap((c) => getSortedChannels(database, c, locale)),
);
const dmMap = (p: PreferenceModel) => getDirectChannelName(p.name, currentUserId);
@ -154,4 +152,4 @@ const enhance = withObservables(['category', 'locale', 'unreadChannelIds'], ({ca
};
});
export default withDatabase(enhance(CategoryBody));
export default withDatabase(withUserId(enhance(CategoryBody)));

View file

@ -11,14 +11,11 @@ import CategoryHeader from './header';
import UnreadCategories from './unreads';
import type CategoryModel from '@typings/database/models/servers/category';
import type ChannelModel from '@typings/database/models/servers/channel';
type Props = {
categories: CategoryModel[];
unreadChannels: ChannelModel[];
currentChannelId: string;
currentUserId: string;
currentTeamId: string;
unreadsOnTop: boolean;
}
const styles = StyleSheet.create({
@ -29,51 +26,39 @@ const styles = StyleSheet.create({
},
});
const extractKey = (item: CategoryModel) => (Array.isArray(item) ? 'UNREADS' : item.id);
const extractKey = (item: CategoryModel | 'UNREADS') => (item === 'UNREADS' ? 'UNREADS' : item.id);
const Categories = ({categories, currentChannelId, currentUserId, currentTeamId, unreadChannels}: Props) => {
const Categories = ({categories, currentTeamId, unreadsOnTop}: Props) => {
const intl = useIntl();
const listRef = useRef<FlatList>(null);
const unreadChannelIds = useMemo(() => new Set(unreadChannels.map((myC) => myC.id)), [unreadChannels]);
const categoriesToDisplay: Array<CategoryModel|string[]> = useMemo(() => {
if (unreadChannelIds.size) {
return [Array.from(unreadChannelIds), ...categories];
const renderCategory = useCallback((data: {item: CategoryModel | 'UNREADS'}) => {
if (data.item === 'UNREADS') {
return <UnreadCategories currentTeamId={currentTeamId}/>;
}
return categories;
}, [categories, unreadChannelIds]);
const renderCategory = useCallback((data: {item: CategoryModel | string[]}) => {
if (Array.isArray(data.item)) {
return (
<UnreadCategories
currentChannelId={currentChannelId}
unreadChannels={unreadChannels}
/>
);
}
return (
<>
<CategoryHeader category={data.item}/>
<CategoryBody
category={data.item}
currentChannelId={currentChannelId}
currentUserId={currentUserId}
locale={intl.locale}
unreadChannelIds={unreadChannelIds}
/>
</>
);
}, [categories, currentChannelId, intl.locale, unreadChannels]);
}, [intl.locale]);
useEffect(() => {
listRef.current?.scrollToOffset({animated: false, offset: 0});
}, [currentTeamId]);
// Sort Categories
categories.sort((a, b) => a.sortOrder - b.sortOrder);
const categoriesToShow = useMemo(() => {
const orderedCategories = [...categories];
orderedCategories.sort((a, b) => a.sortOrder - b.sortOrder);
if (unreadsOnTop) {
return ['UNREADS' as const, ...orderedCategories];
}
return orderedCategories;
}, [categories, unreadsOnTop]);
if (!categories.length) {
return <LoadCategoriesError/>;
@ -81,7 +66,7 @@ const Categories = ({categories, currentChannelId, currentUserId, currentTeamId,
return (
<FlatList
data={categoriesToDisplay}
data={categoriesToShow}
ref={listRef}
renderItem={renderCategory}
style={styles.mainList}
@ -93,6 +78,9 @@ const Categories = ({categories, currentChannelId, currentUserId, currentTeamId,
windowSize={15}
updateCellsBatchingPeriod={10}
maxToRenderPerBatch={5}
// @ts-expect-error strictMode not included in the types
strictMode={true}
/>
);
};

View file

@ -3,38 +3,24 @@
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {combineLatest, of as of$} from 'rxjs';
import {concatAll, map, switchMap} from 'rxjs/operators';
import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {Preferences} from '@app/constants';
import {getPreferenceAsBool} from '@app/helpers/api/preference';
import {getChannelById, queryMyChannelUnreads} from '@app/queries/servers/channel';
import {queryPreferencesByCategoryAndName} from '@app/queries/servers/preference';
import {Preferences} from '@constants';
import {getPreferenceAsBool} from '@helpers/api/preference';
import {queryCategoriesByTeamIds} from '@queries/servers/categories';
import {observeCurrentChannelId, observeCurrentUserId, observeLastUnreadChannelId} from '@queries/servers/system';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {getChannelsFromRelation} from './body';
import Categories from './categories';
import type {WithDatabaseArgs} from '@typings/database/database';
import type ChannelModel from '@typings/database/models/servers/channel';
import type PreferenceModel from '@typings/database/models/servers/preference';
type CA = [
a: Array<ChannelModel | null>,
b: ChannelModel | undefined,
]
const concatenateChannelsArray = ([a, b]: CA) => {
return of$(b ? a.filter((c) => c && c.id !== b.id).concat(b) : a);
};
type WithDatabaseProps = { currentTeamId: string } & WithDatabaseArgs
const enhanced = withObservables(
['currentTeamId'],
({currentTeamId, database}: WithDatabaseProps) => {
const currentChannelId = observeCurrentChannelId(database);
const currentUserId = observeCurrentUserId(database);
const categories = queryCategoriesByTeamIds(database, [currentTeamId]).observeWithColumns(['sort_order']);
const unreadsOnTop = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_GROUP_UNREADS).
@ -43,33 +29,9 @@ const enhanced = withObservables(
switchMap((prefs: PreferenceModel[]) => of$(getPreferenceAsBool(prefs, Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_GROUP_UNREADS, false))),
);
const getC = (lastUnreadChannelId: string) => getChannelById(database, lastUnreadChannelId);
const unreadChannels = unreadsOnTop.pipe(switchMap((gU) => {
if (gU) {
const lastUnread = observeLastUnreadChannelId(database).pipe(
switchMap(getC),
);
const unreads = queryMyChannelUnreads(database, currentTeamId).observe().pipe(
map(getChannelsFromRelation),
concatAll(),
);
const combined = combineLatest([unreads, lastUnread]).pipe(
switchMap(concatenateChannelsArray),
);
return combined;
}
return of$([]);
}));
return {
unreadChannels,
categories,
currentUserId,
currentChannelId,
unreadsOnTop,
};
});

View file

@ -0,0 +1,67 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {of as of$, combineLatest} from 'rxjs';
import {concatAll, map, switchMap} from 'rxjs/operators';
import {Preferences} from '@constants';
import {getPreferenceAsBool} from '@helpers/api/preference';
import {getChannelById, queryMyChannelUnreads} from '@queries/servers/channel';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {observeLastUnreadChannelId} from '@queries/servers/system';
import {getChannelsFromRelation} from '../body';
import UnreadCategories from './unreads';
import type {WithDatabaseArgs} from '@typings/database/database';
import type ChannelModel from '@typings/database/models/servers/channel';
import type PreferenceModel from '@typings/database/models/servers/preference';
type WithDatabaseProps = { currentTeamId: string } & WithDatabaseArgs
type CA = [
a: Array<ChannelModel | null>,
b: ChannelModel | undefined,
]
const concatenateChannelsArray = ([a, b]: CA) => {
return of$(b ? a.filter((c) => c && c.id !== b.id).concat(b) : a);
};
const enhanced = withObservables(['currentTeamId'], ({currentTeamId, database}: WithDatabaseProps) => {
const unreadsOnTop = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_GROUP_UNREADS).
observeWithColumns(['value']).
pipe(
switchMap((prefs: PreferenceModel[]) => of$(getPreferenceAsBool(prefs, Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_GROUP_UNREADS, false))),
);
const getC = (lastUnreadChannelId: string) => getChannelById(database, lastUnreadChannelId);
const unreadChannels = unreadsOnTop.pipe(switchMap((gU) => {
if (gU) {
const lastUnread = observeLastUnreadChannelId(database).pipe(
switchMap(getC),
);
const unreads = queryMyChannelUnreads(database, currentTeamId).observe().pipe(
map(getChannelsFromRelation),
concatAll(),
);
const combined = combineLatest([unreads, lastUnread]).pipe(
switchMap(concatenateChannelsArray),
);
return combined;
}
return of$([]);
}));
return {
unreadChannels,
};
});
export default withDatabase(enhanced(UnreadCategories));

View file

@ -17,15 +17,14 @@ describe('components/channel_list/categories/body', () => {
database = server.database;
});
it('render without error', () => {
it('do not render when there are no unread channels', () => {
const wrapper = renderWithEverything(
<UnreadsCategory
currentChannelId={''}
unreadChannels={[]}
/>,
{database},
);
expect(wrapper.toJSON()).toBeTruthy();
expect(wrapper.toJSON()).toBeNull();
});
});

View file

@ -1,15 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import React from 'react';
import {useIntl} from 'react-intl';
import {FlatList, Text} from 'react-native';
import {changeOpacity, makeStyleSheetFromTheme} from '@app/utils/theme';
import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import ChannelListItem from './body/channel';
import ChannelListItem from '../body/channel';
import type ChannelModel from '@typings/database/models/servers/channel';
@ -23,26 +23,27 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
},
}));
const renderItem = ({item}: {item: ChannelModel}) => {
return (
<ChannelListItem
channel={item}
collapsed={false}
isUnreads={true}
/>
);
};
type UnreadCategoriesProps = {
unreadChannels: ChannelModel[];
currentChannelId: string;
}
const UnreadCategories = ({unreadChannels, currentChannelId}: UnreadCategoriesProps) => {
const UnreadCategories = ({unreadChannels}: UnreadCategoriesProps) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
const intl = useIntl();
const renderItem = useCallback(({item}: {item: ChannelModel}) => {
return (
<ChannelListItem
channel={item}
isActive={item.id === currentChannelId}
collapsed={false}
/>
);
}, [currentChannelId]);
if (!unreadChannels.length) {
return null;
}
return (
<>
<Text

View file

@ -17,6 +17,7 @@ import WebsocketManager from './app/init/websocket_manager';
import {registerScreens} from './app/screens';
import EphemeralStore from './app/store/ephemeral_store';
import setFontFamily from './app/utils/font_family';
import './app/utils/emoji'; // Imported to ensure it is loaded when used
declare const global: { HermesInternal: null | {} };