Unreads On Top (#6098)

* Unreads on top

* Feedback addressed

* update sorted channels if locale changes

* Extract localized strings

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
Shaz MJ
2022-04-13 02:00:43 +10:00
committed by GitHub
parent a0ff404d39
commit 9feb3446a8
9 changed files with 168 additions and 25 deletions

View File

@@ -37,6 +37,7 @@ describe('components/channel_list/categories/body', () => {
locale={DEFAULT_LOCALE}
currentChannelId={''}
currentUserId={''}
unreadChannelIds={new Set()}
/>,
{database},
);

View File

@@ -66,14 +66,16 @@ const observeSettings = (database: Database, channels: ChannelModel[]) => {
return queryMyChannelSettingsByIds(database, ids).observeWithColumns(['notify_props']);
};
const getChannelsFromRelation = async (relations: CategoryChannelModel[] | MyChannelModel[]) => {
export const getChannelsFromRelation = async (relations: CategoryChannelModel[] | MyChannelModel[]) => {
return Promise.all(relations.map((r) => r.channel?.fetch()));
};
const getSortedChannels = (database: Database, category: CategoryModel, locale: string) => {
const getSortedChannels = (database: Database, category: CategoryModel, unreadChannelIds: Set<string>, locale: string) => {
switch (category.sorting) {
case 'alpha': {
const channels = category.channels.observeWithColumns(['display_name']);
const channels = category.channels.observeWithColumns(['display_name']).pipe(
map((cs) => cs.filter((c) => !unreadChannelIds.has(c.id))),
);
const settings = channels.pipe(
switchMap((cs) => observeSettings(database, cs)),
);
@@ -83,12 +85,14 @@ const getSortedChannels = (database: Database, category: CategoryModel, locale:
}
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(),
);
@@ -99,12 +103,17 @@ const mapPrefName = (prefs: PreferenceModel[]) => of$(prefs.map((p) => p.name));
const mapChannelIds = (channels: ChannelModel[]) => of$(channels.map((c) => c.id));
type EnhanceProps = {category: CategoryModel; locale: string; currentUserId: string} & WithDatabaseArgs
type EnhanceProps = {
category: CategoryModel;
locale: string;
currentUserId: string;
unreadChannelIds: Set<string>;
} & WithDatabaseArgs
const enhance = withObservables(['category'], ({category, locale, database, currentUserId}: EnhanceProps) => {
const enhance = withObservables(['category', 'locale', 'unreadChannelIds'], ({category, locale, database, currentUserId, unreadChannelIds}: EnhanceProps) => {
const observedCategory = category.observe();
const sortedChannels = observedCategory.pipe(
switchMap((c) => getSortedChannels(database, c, locale)),
switchMap((c) => getSortedChannels(database, c, unreadChannelIds, locale)),
);
const dmMap = (p: PreferenceModel) => getDirectChannelName(p.name, currentUserId);

View File

@@ -1,35 +1,40 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useRef} from 'react';
import React, {useCallback, useEffect, useMemo, useRef} from 'react';
import {useIntl} from 'react-intl';
import {FlatList, StyleSheet} from 'react-native';
import CategoryBody from './body';
import LoadCategoriesError from './error';
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;
}
const styles = StyleSheet.create({
flex: {
mainList: {
flex: 1,
},
});
const extractKey = (item: CategoryModel) => item.id;
const Categories = ({categories, currentChannelId, currentUserId, currentTeamId}: Props) => {
const Categories = ({categories, currentChannelId, currentUserId, currentTeamId, unreadChannels}: Props) => {
const intl = useIntl();
const listRef = useRef<FlatList>(null);
const unreadChannelIds = useMemo(() => new Set(unreadChannels.map((myC) => myC.id)), [unreadChannels]);
const renderCategory = useCallback((data: {item: CategoryModel}) => {
return (
<>
@@ -39,6 +44,7 @@ const Categories = ({categories, currentChannelId, currentUserId, currentTeamId}
currentChannelId={currentChannelId}
currentUserId={currentUserId}
locale={intl.locale}
unreadChannelIds={unreadChannelIds}
/>
</>
);
@@ -56,20 +62,23 @@ const Categories = ({categories, currentChannelId, currentUserId, currentTeamId}
}
return (
<FlatList
data={categories}
ref={listRef}
renderItem={renderCategory}
style={styles.flex}
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
keyExtractor={extractKey}
removeClippedSubviews={true}
initialNumToRender={5}
windowSize={15}
updateCellsBatchingPeriod={10}
maxToRenderPerBatch={5}
/>
<>
{unreadChannels.length > 0 && <UnreadCategories unreadChannels={unreadChannels}/>}
<FlatList
data={categories}
ref={listRef}
renderItem={renderCategory}
style={styles.mainList}
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
keyExtractor={extractKey}
removeClippedSubviews={true}
initialNumToRender={5}
windowSize={15}
updateCellsBatchingPeriod={10}
maxToRenderPerBatch={5}
/>
</>
);
};

View File

@@ -3,15 +3,23 @@
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {of as of$} from 'rxjs';
import {concatAll, map, switchMap} from 'rxjs/operators';
import {Preferences} from '@app/constants';
import {getPreferenceAsBool} from '@app/helpers/api/preference';
import {queryMyChannelUnreads} from '@app/queries/servers/channel';
import {queryPreferencesByCategoryAndName} from '@app/queries/servers/preference';
import {queryCategoriesByTeamIds} from '@queries/servers/categories';
import {observeCurrentChannelId, observeCurrentUserId} from '@queries/servers/system';
import {getChannelsFromRelation} from './body';
import Categories from './categories';
import type {WithDatabaseArgs} from '@typings/database/database';
import type PreferenceModel from '@typings/database/models/servers/preference';
type WithDatabaseProps = {currentTeamId: string } & WithDatabaseArgs
type WithDatabaseProps = { currentTeamId: string } & WithDatabaseArgs
const enhanced = withObservables(
['currentTeamId'],
@@ -20,10 +28,27 @@ const enhanced = withObservables(
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).
observe().
pipe(
switchMap((prefs: PreferenceModel[]) => of$(getPreferenceAsBool(prefs, Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_GROUP_UNREADS, false))),
);
const unreadChannels = unreadsOnTop.pipe(switchMap((gU) => {
if (gU) {
return queryMyChannelUnreads(database, currentTeamId).observe().pipe(
map(getChannelsFromRelation),
concatAll(),
);
}
return of$([]);
}));
return {
currentChannelId,
unreadChannels,
categories,
currentUserId,
currentChannelId,
};
});

View File

@@ -0,0 +1,28 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Database} from '@nozbe/watermelondb';
import React from 'react';
import {renderWithEverything} from '@test/intl-test-helper';
import TestHelper from '@test/test_helper';
import UnreadsCategory from './unreads';
describe('components/channel_list/categories/body', () => {
let database: Database;
beforeAll(async () => {
const server = await TestHelper.setupServerDatabase();
database = server.database;
});
it('render without error', () => {
const wrapper = renderWithEverything(
<UnreadsCategory unreadChannels={[]}/>,
{database},
);
expect(wrapper.toJSON()).toBeTruthy();
});
});

View File

@@ -0,0 +1,55 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
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 {typography} from '@utils/typography';
import ChannelListItem from './body/channel';
import type ChannelModel from '@typings/database/models/servers/channel';
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
heading: {
color: changeOpacity(theme.sidebarText, 0.64),
...typography('Heading', 75),
paddingLeft: 5,
paddingTop: 10,
},
}));
const renderItem = ({item}: {item: ChannelModel}) => {
return (
<ChannelListItem
channel={item}
isActive={true}
collapsed={false}
/>
);
};
const UnreadCategories = ({unreadChannels}: {unreadChannels: ChannelModel[]}) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
const intl = useIntl();
return (
<>
<Text
style={styles.heading}
>
{intl.formatMessage({id: 'mobile.channel_list.unreads', defaultMessage: 'UNREADS'})}
</Text>
<FlatList
data={unreadChannels}
renderItem={renderItem}
/>
</>
);
};
export default UnreadCategories;

View File

@@ -40,6 +40,7 @@ const Preferences: Record<string, any> = {
CHANNEL_SIDEBAR_ORGANIZATION: 'channel_sidebar_organization',
CHANNEL_SIDEBAR_LIMIT_DMS: 'limit_visible_dms_gms',
CHANNEL_SIDEBAR_LIMIT_DMS_DEFAULT: 20,
CHANNEL_SIDEBAR_GROUP_UNREADS: 'show_unread_section',
AUTOCLOSE_DMS_ENABLED: 'after_seven_days',
CATEGORY_ADVANCED_SETTINGS: 'advanced_settings',
ADVANCED_FILTER_JOIN_LEAVE: 'join_leave',

View File

@@ -373,6 +373,20 @@ 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(
CHANNEL,
Q.or(
Q.where('team_id', Q.eq(currentTeamId)),
Q.where('team_id', Q.eq('')),
),
),
Q.where('is_unread', Q.eq(true)),
Q.sortBy('last_post_at', Q.desc),
);
};
export function observeMyChannelMentionCount(database: Database, teamId?: string, columns = ['mentions_count', 'is_unread']): Observable<number> {
const conditions: Q.Condition[] = [
Q.where('delete_at', Q.eq(0)),

View File

@@ -240,6 +240,7 @@
"mobile.camera_photo_permission_denied_title": "{applicationName} would like to access your camera",
"mobile.channel_info.alertNo": "No",
"mobile.channel_info.alertYes": "Yes",
"mobile.channel_list.unreads": "UNREADS",
"mobile.commands.error_title": "Error Executing Command",
"mobile.components.select_server_view.connect": "Connect",
"mobile.components.select_server_view.connecting": "Connecting",