Skip to content

Commit

Permalink
Unreads On Top (#6098)
Browse files Browse the repository at this point in the history
* Unreads on top

* Feedback addressed

* update sorted channels if locale changes

* Extract localized strings

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
  • Loading branch information
shaz-r and enahum authored Apr 12, 2022
1 parent a0ff404 commit 9feb344
Show file tree
Hide file tree
Showing 9 changed files with 168 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ describe('components/channel_list/categories/body', () => {
locale={DEFAULT_LOCALE}
currentChannelId={''}
currentUserId={''}
unreadChannelIds={new Set()}
/>,
{database},
);
Expand Down
21 changes: 15 additions & 6 deletions app/components/channel_list/categories/body/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
);
Expand All @@ -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(),
);
Expand All @@ -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);
Expand Down
43 changes: 26 additions & 17 deletions app/components/channel_list/categories/categories.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
Expand All @@ -39,6 +44,7 @@ const Categories = ({categories, currentChannelId, currentUserId, currentTeamId}
currentChannelId={currentChannelId}
currentUserId={currentUserId}
locale={intl.locale}
unreadChannelIds={unreadChannelIds}
/>
</>
);
Expand All @@ -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}
/>
</>
);
};

Expand Down
29 changes: 27 additions & 2 deletions app/components/channel_list/categories/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand All @@ -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,
};
});

Expand Down
28 changes: 28 additions & 0 deletions app/components/channel_list/categories/unreads.test.tsx
Original file line number Diff line number Diff line change
@@ -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();
});
});
55 changes: 55 additions & 0 deletions app/components/channel_list/categories/unreads.tsx
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions app/constants/preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
14 changes: 14 additions & 0 deletions app/queries/servers/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand Down
1 change: 1 addition & 0 deletions assets/base/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 9feb344

Please sign in to comment.