Filter to show only unread channels (#6248)

This commit is contained in:
Elias Nahum 2022-05-09 11:29:27 -04:00 committed by GitHub
parent a5d85890dd
commit 597b5b03f1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 278 additions and 42 deletions

View file

@ -6,6 +6,7 @@ import {DeviceEventEmitter} from 'react-native';
import {General, Navigation as NavigationConstants, Preferences, Screens} from '@constants';
import {CHANNELS_CATEGORY, DMS_CATEGORY} from '@constants/categories';
import {SYSTEM_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager';
import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
import {extractChannelDisplayName} from '@helpers/database';
@ -467,3 +468,18 @@ export async function addChannelToDefaultCategory(serverUrl: string, channel: Ch
return {models};
}
export async function showUnreadChannelsOnly(serverUrl: string, onlyUnreads: boolean) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
return operator.handleSystem({
systems: [{
id: SYSTEM_IDENTIFIERS.ONLY_UNREADS,
value: JSON.stringify(onlyUnreads),
}],
prepareRecordsOnly: false,
});
}

View file

@ -54,6 +54,7 @@ export const SYSTEM_IDENTIFIERS = {
DATA_RETENTION_POLICIES: 'dataRetentionPolicies',
EXPANDED_LINKS: 'expandedLinks',
LICENSE: 'license',
ONLY_UNREADS: 'onlyUnreads',
PUSH_VERIFICATION_STATUS: 'pushVerificationStatus',
RECENT_CUSTOM_STATUS: 'recentCustomStatus',
RECENT_MENTIONS: 'recentMentions',

View file

@ -398,3 +398,10 @@ export const getLastUnreadChannelId = async (serverDatabase: Database): Promise<
return '';
}
};
export const observeOnlyUnreads = (database: Database) => {
return querySystemValue(database, SYSTEM_IDENTIFIERS.ONLY_UNREADS).observeWithColumns(['value']).pipe(
switchMap((result) => (result.length ? result[0].observe() : of$({value: false}))),
switchMap((model) => of$(model.value as boolean)),
);
};

View file

@ -415,53 +415,112 @@ exports[`components/categories_list should render team error 1`] = `
</View>
</View>
<View
accessible={true}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"backgroundColor": "rgba(255,255,255,0.12)",
"borderRadius": 8,
"flexDirection": "row",
"height": 40,
"justifyContent": "flex-start",
"marginVertical": 20,
"padding": 8,
"width": "100%",
}
}
>
<Icon
name="magnify"
style={
<View
animatedStyle={
Object {
"color": "rgba(255,255,255,0.72)",
"fontSize": 24,
"width": 24,
"value": Object {
"marginRight": 8,
"opacity": 1,
"width": 40,
},
}
}
/>
<Text
collapsable={false}
style={
Object {
"color": "rgba(255,255,255,0.72)",
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
"marginLeft": 5,
"marginTop": 1,
"marginRight": 8,
"opacity": 1,
"width": 40,
}
}
>
Find channels...
</Text>
<View
accessible={true}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Array [
Object {
"alignItems": "center",
"backgroundColor": "rgba(255,255,255,0.12)",
"borderRadius": 8,
"height": 40,
"justifyContent": "center",
"marginVertical": 20,
"width": 40,
},
false,
]
}
>
<Icon
color="rgba(255,255,255,0.56)"
name="filter-variant"
size={24}
/>
</View>
</View>
<View
accessible={true}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"backgroundColor": "rgba(255,255,255,0.12)",
"borderRadius": 8,
"flex": 1,
"flexDirection": "row",
"height": 40,
"justifyContent": "flex-start",
"marginVertical": 20,
"padding": 8,
}
}
>
<Icon
name="magnify"
style={
Object {
"color": "rgba(255,255,255,0.72)",
"fontSize": 24,
"width": 24,
}
}
/>
<Text
style={
Object {
"color": "rgba(255,255,255,0.72)",
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
"marginLeft": 5,
"marginTop": 1,
}
}
>
Find channels...
</Text>
</View>
</View>
<View
style={

View file

@ -19,6 +19,7 @@ import type CategoryModel from '@typings/database/models/servers/category';
type Props = {
categories: CategoryModel[];
currentTeamId: string;
onlyUnreads: boolean;
unreadsOnTop: boolean;
}
@ -32,7 +33,7 @@ const styles = StyleSheet.create({
const extractKey = (item: CategoryModel | 'UNREADS') => (item === 'UNREADS' ? 'UNREADS' : item.id);
const Categories = ({categories, currentTeamId, unreadsOnTop}: Props) => {
const Categories = ({categories, currentTeamId, onlyUnreads, unreadsOnTop}: Props) => {
const intl = useIntl();
const listRef = useRef<FlatList>(null);
const serverUrl = useServerUrl();
@ -49,6 +50,7 @@ const Categories = ({categories, currentTeamId, unreadsOnTop}: Props) => {
currentTeamId={currentTeamId}
isTablet={isTablet}
onChannelSwitch={onChannelSwitch}
onlyUnreads={onlyUnreads}
/>
);
}
@ -63,20 +65,23 @@ const Categories = ({categories, currentTeamId, unreadsOnTop}: Props) => {
/>
</>
);
}, [currentTeamId, intl.locale, isTablet, onChannelSwitch]);
}, [currentTeamId, intl.locale, isTablet, onChannelSwitch, onlyUnreads]);
useEffect(() => {
listRef.current?.scrollToOffset({animated: false, offset: 0});
}, [currentTeamId]);
const categoriesToShow = useMemo(() => {
if (onlyUnreads && !unreadsOnTop) {
return ['UNREADS' as const];
}
const orderedCategories = [...categories];
orderedCategories.sort((a, b) => a.sortOrder - b.sortOrder);
if (unreadsOnTop) {
return ['UNREADS' as const, ...orderedCategories];
}
return orderedCategories;
}, [categories, unreadsOnTop]);
}, [categories, onlyUnreads, unreadsOnTop]);
if (!categories.length) {
return <LoadCategoriesError/>;

View file

@ -10,6 +10,7 @@ import {Preferences} from '@constants';
import {getPreferenceAsBool} from '@helpers/api/preference';
import {queryCategoriesByTeamIds} from '@queries/servers/categories';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {observeOnlyUnreads} from '@queries/servers/system';
import Categories from './categories';
@ -31,6 +32,7 @@ const enhanced = withObservables(
return {
categories,
onlyUnreads: observeOnlyUnreads(database),
unreadsOnTop,
};
});

View file

@ -24,6 +24,7 @@ import type PreferenceModel from '@typings/database/models/servers/preference';
type WithDatabaseProps = WithDatabaseArgs & {
currentTeamId: string;
isTablet: boolean;
onlyUnreads: boolean;
}
type CA = [
@ -89,7 +90,7 @@ const filterAndSortMyChannels = ([myChannels, notifyProps]: [MyChannelModel[], N
return [...mentions, ...unreads, ...mutedMentions];
};
const enhanced = withObservables(['currentTeamId', 'isTablet'], ({currentTeamId, isTablet, database}: WithDatabaseProps) => {
const enhanced = withObservables(['currentTeamId', 'isTablet', 'onlyUnreads'], ({currentTeamId, isTablet, database, onlyUnreads}: WithDatabaseProps) => {
const unreadsOnTop = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_GROUP_UNREADS).
observeWithColumns(['value']).
pipe(
@ -99,7 +100,7 @@ const enhanced = withObservables(['currentTeamId', 'isTablet'], ({currentTeamId,
const getC = (lastUnreadChannelId: string) => getChannelById(database, lastUnreadChannelId);
const unreadChannels = unreadsOnTop.pipe(switchMap((gU) => {
if (gU) {
if (gU || onlyUnreads) {
const lastUnread = isTablet ? observeLastUnreadChannelId(database).pipe(
switchMap(getC),
) : of$('');

View file

@ -12,7 +12,7 @@ import Categories from './categories';
import ChannelListHeader from './header';
import LoadChannelsError from './load_channels_error';
import LoadTeamsError from './load_teams_error';
import SearchField from './search';
import SubHeader from './subheader';
import ThreadsButton from './threads_button';
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
@ -68,7 +68,7 @@ const CategoriesList = ({channelsCount, currentTeamId, iconPad, isCRTEnabled, is
} else {
content = (
<>
<SearchField/>
<SubHeader/>
{isCRTEnabled && <ThreadsButton/>}
<Categories
currentTeamId={currentTeamId}

View file

@ -0,0 +1,30 @@
// 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$} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {Preferences} from '@constants';
import {getPreferenceAsBool} from '@helpers/api/preference';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import SubHeader from './subheader';
import type {WithDatabaseArgs} from '@typings/database/database';
import type PreferenceModel from '@typings/database/models/servers/preference';
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
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))),
);
return {
unreadsOnTop,
};
});
export default withDatabase(enhanced(SubHeader));

View file

@ -15,12 +15,12 @@ exports[`Search Field should match snapshot 1`] = `
Object {
"backgroundColor": "rgba(255,255,255,0.12)",
"borderRadius": 8,
"flex": 1,
"flexDirection": "row",
"height": 40,
"justifyContent": "flex-start",
"marginVertical": 20,
"padding": 8,
"width": "100%",
}
}
>

View file

@ -18,7 +18,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
container: {
flexDirection: 'row',
justifyContent: 'flex-start',
width: '100%',
flex: 1,
backgroundColor: changeOpacity(theme.sidebarText, 0.12),
borderRadius: 8,
padding: 8,

View file

@ -0,0 +1,44 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useEffect} from 'react';
import {StyleSheet, View} from 'react-native';
import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
import SearchField from './search_field';
import UnreadFilter from './unread_filter';
type Props = {
unreadsOnTop: boolean;
}
const style = StyleSheet.create({
container: {
flexDirection: 'row',
},
});
const SubHeader = ({unreadsOnTop}: Props) => {
const showFilter = useSharedValue(!unreadsOnTop);
const animatedStyle = useAnimatedStyle(() => ({
marginRight: withTiming(showFilter.value ? 8 : 0, {duration: 300}),
width: withTiming(showFilter.value ? 40 : 0, {duration: 300}),
opacity: withTiming(showFilter.value ? 1 : 0, {duration: 300}),
}));
useEffect(() => {
showFilter.value = !unreadsOnTop;
}, [unreadsOnTop]);
return (
<View style={style.container}>
<Animated.View style={animatedStyle}>
<UnreadFilter/>
</Animated.View>
<SearchField/>
</View>
);
};
export default SubHeader;

View file

@ -0,0 +1,17 @@
// 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 {observeOnlyUnreads} from '@queries/servers/system';
import UnreadFilter from './unread_filter';
import type {WithDatabaseArgs} from '@typings/database/database';
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({
onlyUnreads: observeOnlyUnreads(database),
}));
export default withDatabase(enhanced(UnreadFilter));

View file

@ -0,0 +1,54 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {TouchableWithoutFeedback, View} from 'react-native';
import {showUnreadChannelsOnly} from '@actions/local/channel';
import CompassIcon from '@components/compass_icon';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
type Props = {
onlyUnreads: boolean;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
container: {
alignItems: 'center',
backgroundColor: changeOpacity(theme.sidebarText, 0.12),
borderRadius: 8,
height: 40,
justifyContent: 'center',
marginVertical: 20,
width: 40,
},
filtered: {
backgroundColor: theme.sidebarText,
},
}));
const UnreadFilter = ({onlyUnreads}: Props) => {
const theme = useTheme();
const serverUrl = useServerUrl();
const styles = getStyleSheet(theme);
const onPress = () => {
showUnreadChannelsOnly(serverUrl, !onlyUnreads);
};
return (
<TouchableWithoutFeedback onPress={onPress}>
<View style={[styles.container, onlyUnreads && styles.filtered]}>
<CompassIcon
color={changeOpacity(onlyUnreads ? theme.sidebarBg : theme.sidebarText, 0.56)}
name='filter-variant'
size={24}
/>
</View>
</TouchableWithoutFeedback>
);
};
export default UnreadFilter;