Filter to show only unread channels (#6248)
This commit is contained in:
parent
a5d85890dd
commit
597b5b03f1
15 changed files with 278 additions and 42 deletions
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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={
|
||||
|
|
|
|||
|
|
@ -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/>;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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$('');
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
@ -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%",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
|
@ -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,
|
||||
|
|
@ -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;
|
||||
|
|
@ -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));
|
||||
|
|
@ -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;
|
||||
Loading…
Reference in a new issue