forked from Ivasoft/mattermost-mobile
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:
@@ -37,6 +37,7 @@ describe('components/channel_list/categories/body', () => {
|
||||
locale={DEFAULT_LOCALE}
|
||||
currentChannelId={''}
|
||||
currentUserId={''}
|
||||
unreadChannelIds={new Set()}
|
||||
/>,
|
||||
{database},
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
28
app/components/channel_list/categories/unreads.test.tsx
Normal file
28
app/components/channel_list/categories/unreads.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
55
app/components/channel_list/categories/unreads.tsx
Normal file
55
app/components/channel_list/categories/unreads.tsx
Normal 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;
|
||||
@@ -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',
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user