Moves collapse animation to FlatList, updates timings (#6220)

* Moves collapse animation to FlatList, updates timings

* dev review

* filters unreads from other categories & removes duplicate

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
This commit is contained in:
Shaz MJ
2022-05-05 11:17:33 +10:00
committed by GitHub
parent 14abe4d2ac
commit 362db9d98d
13 changed files with 293 additions and 506 deletions

View File

@@ -2,129 +2,109 @@
exports[`components/channel_list/categories/body/channel_item should match snapshot 1`] = ` exports[`components/channel_list/categories/body/channel_item should match snapshot 1`] = `
<View <View
animatedStyle={ accessible={true}
Object {
"value": Object {
"height": 40,
"marginVertical": 2,
"opacity": 1,
},
}
}
collapsable={false} collapsable={false}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={ style={
Object { Object {
"height": 40,
"marginVertical": 2,
"opacity": 1, "opacity": 1,
} }
} }
> >
<View <View
accessible={true}
collapsable={false}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={ style={
Object { Array [
"opacity": 1, Object {
} "alignItems": "center",
"flexDirection": "row",
"minHeight": 40,
"paddingHorizontal": 20,
},
false,
undefined,
Object {
"minHeight": 40,
},
]
} }
testID="channel_list_item.hello.collapsed.true"
> >
<View <View
style={ style={
Array [ Object {
Object { "flex": 1,
"alignItems": "center", "flexDirection": "row",
"flexDirection": "row", }
"minHeight": 40,
"paddingHorizontal": 20,
},
false,
undefined,
Object {
"minHeight": 40,
},
]
} }
testID="channel_list_item.hello.collapsed.false"
> >
<View <View
style={ style={
Object { Array [
"flex": 1, Object {
"flexDirection": "row", "alignItems": "center",
} "justifyContent": "center",
},
Object {
"height": 24,
"width": 24,
},
undefined,
undefined,
]
} }
> >
<View <Icon
name="globe"
style={ style={
Array [ Array [
Object { Object {
"alignItems": "center", "color": "rgba(255,255,255,0.4)",
"justifyContent": "center",
}, },
undefined,
undefined,
Object { Object {
"height": 24, "fontSize": 24,
"width": 24, "left": 1,
}, },
undefined,
undefined,
] ]
} }
testID="undefined.public"
/>
</View>
<View>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
Array [
Object {
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
},
Object {
"color": "rgba(255,255,255,0.72)",
"marginTop": -1,
"paddingLeft": 12,
},
false,
false,
null,
null,
]
}
testID="channel_list_item.hello.display_name"
> >
<Icon Hello!
name="globe" </Text>
style={
Array [
Object {
"color": "rgba(255,255,255,0.4)",
},
undefined,
undefined,
Object {
"fontSize": 24,
"left": 1,
},
]
}
testID="undefined.public"
/>
</View>
<View>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
Array [
Object {
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
},
Object {
"color": "rgba(255,255,255,0.72)",
"marginTop": -1,
"paddingLeft": 12,
},
false,
false,
null,
null,
]
}
testID="channel_list_item.hello.display_name"
>
Hello!
</Text>
</View>
</View> </View>
</View> </View>
</View> </View>
@@ -133,129 +113,109 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
exports[`components/channel_list/categories/body/channel_item should match snapshot when it has a draft 1`] = ` exports[`components/channel_list/categories/body/channel_item should match snapshot when it has a draft 1`] = `
<View <View
animatedStyle={ accessible={true}
Object {
"value": Object {
"height": 40,
"marginVertical": 2,
"opacity": 1,
},
}
}
collapsable={false} collapsable={false}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={ style={
Object { Object {
"height": 40,
"marginVertical": 2,
"opacity": 1, "opacity": 1,
} }
} }
> >
<View <View
accessible={true}
collapsable={false}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={ style={
Object { Array [
"opacity": 1, Object {
} "alignItems": "center",
"flexDirection": "row",
"minHeight": 40,
"paddingHorizontal": 20,
},
false,
undefined,
Object {
"minHeight": 40,
},
]
} }
testID="channel_list_item.hello.collapsed.true"
> >
<View <View
style={ style={
Array [ Object {
Object { "flex": 1,
"alignItems": "center", "flexDirection": "row",
"flexDirection": "row", }
"minHeight": 40,
"paddingHorizontal": 20,
},
false,
undefined,
Object {
"minHeight": 40,
},
]
} }
testID="channel_list_item.hello.collapsed.false"
> >
<View <View
style={ style={
Object { Array [
"flex": 1, Object {
"flexDirection": "row", "alignItems": "center",
} "justifyContent": "center",
},
Object {
"height": 24,
"width": 24,
},
undefined,
undefined,
]
} }
> >
<View <Icon
name="pencil-outline"
style={ style={
Array [ Array [
Object { Object {
"alignItems": "center", "color": "rgba(255,255,255,0.4)",
"justifyContent": "center",
}, },
undefined,
undefined,
Object { Object {
"height": 24, "fontSize": 24,
"width": 24, "left": 2,
}, },
undefined,
undefined,
] ]
} }
testID="undefined.draft"
/>
</View>
<View>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
Array [
Object {
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
},
Object {
"color": "rgba(255,255,255,0.72)",
"marginTop": -1,
"paddingLeft": 12,
},
false,
false,
null,
null,
]
}
testID="channel_list_item.hello.display_name"
> >
<Icon Hello!
name="pencil-outline" </Text>
style={
Array [
Object {
"color": "rgba(255,255,255,0.4)",
},
undefined,
undefined,
Object {
"fontSize": 24,
"left": 2,
},
]
}
testID="undefined.draft"
/>
</View>
<View>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
Array [
Object {
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
},
Object {
"color": "rgba(255,255,255,0.72)",
"marginTop": -1,
"paddingLeft": 12,
},
false,
false,
null,
null,
]
}
testID="channel_list_item.hello.display_name"
>
Hello!
</Text>
</View>
</View> </View>
</View> </View>
</View> </View>

View File

@@ -37,10 +37,8 @@ describe('components/channel_list/categories/body/channel_item', () => {
membersCount={0} membersCount={0}
myChannel={myChannel} myChannel={myChannel}
isMuted={false} isMuted={false}
collapsed={false}
currentUserId={'id'} currentUserId={'id'}
testID='channel_list_item' testID='channel_list_item'
isVisible={true}
onPress={() => undefined} onPress={() => undefined}
/>, />,
); );
@@ -57,10 +55,8 @@ describe('components/channel_list/categories/body/channel_item', () => {
membersCount={3} membersCount={3}
myChannel={myChannel} myChannel={myChannel}
isMuted={false} isMuted={false}
collapsed={false}
currentUserId={'id'} currentUserId={'id'}
testID='channel_list_item' testID='channel_list_item'
isVisible={true}
onPress={() => undefined} onPress={() => undefined}
/>, />,
); );

View File

@@ -1,10 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React, {useCallback, useEffect, useMemo} from 'react'; import React, {useCallback, useMemo} from 'react';
import {useIntl} from 'react-intl'; import {useIntl} from 'react-intl';
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'; import {StyleSheet, Text, TouchableOpacity, View} from 'react-native';
import Animated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
import Badge from '@components/badge'; import Badge from '@components/badge';
import ChannelIcon from '@components/channel_icon'; import ChannelIcon from '@components/channel_icon';
@@ -22,13 +21,11 @@ import type MyChannelModel from '@typings/database/models/servers/my_channel';
type Props = { type Props = {
channel: ChannelModel; channel: ChannelModel;
collapsed: boolean;
currentUserId: string; currentUserId: string;
hasDraft: boolean; hasDraft: boolean;
isActive: boolean; isActive: boolean;
isInfo?: boolean; isInfo?: boolean;
isMuted: boolean; isMuted: boolean;
isVisible: boolean;
membersCount: number; membersCount: number;
myChannel?: MyChannelModel; myChannel?: MyChannelModel;
onPress: (channelId: string) => void; onPress: (channelId: string) => void;
@@ -115,8 +112,8 @@ export const textStyle = StyleSheet.create({
}); });
const ChannelListItem = ({ const ChannelListItem = ({
channel, collapsed, currentUserId, hasDraft, channel, currentUserId, hasDraft,
isActive, isInfo, isMuted, isVisible, membersCount, isActive, isInfo, isMuted, membersCount,
myChannel, onPress, teamDisplayName, testID}: Props) => { myChannel, onPress, teamDisplayName, testID}: Props) => {
const {formatMessage} = useIntl(); const {formatMessage} = useIntl();
const theme = useTheme(); const theme = useTheme();
@@ -126,8 +123,6 @@ const ChannelListItem = ({
// Make it brighter if it's not muted, and highlighted or has unreads // Make it brighter if it's not muted, and highlighted or has unreads
const isBright = !isMuted && (myChannel && (myChannel.isUnread || myChannel.mentionsCount > 0)); const isBright = !isMuted && (myChannel && (myChannel.isUnread || myChannel.mentionsCount > 0));
const shouldCollapse = (collapsed && !isBright) && !isActive;
const sharedValue = useSharedValue(shouldCollapse);
const height = useMemo(() => { const height = useMemo(() => {
let h = 40; let h = 40;
if (isInfo) { if (isInfo) {
@@ -136,18 +131,6 @@ const ChannelListItem = ({
return h; return h;
}, [teamDisplayName, isInfo, isTablet]); }, [teamDisplayName, isInfo, isTablet]);
useEffect(() => {
sharedValue.value = shouldCollapse;
}, [shouldCollapse]);
const animatedStyle = useAnimatedStyle(() => {
return {
marginVertical: withTiming(sharedValue.value ? 0 : 2, {duration: 500}),
height: withTiming(sharedValue.value ? 0 : height, {duration: 500}),
opacity: withTiming(sharedValue.value ? 0 : 1, {duration: 500, easing: Easing.inOut(Easing.exp)}),
};
}, [height]);
const handleOnPress = useCallback(() => { const handleOnPress = useCallback(() => {
onPress(myChannel?.id || channel.id); onPress(myChannel?.id || channel.id);
}, [channel.id, myChannel?.id]); }, [channel.id, myChannel?.id]);
@@ -169,7 +152,7 @@ const ChannelListItem = ({
], ],
[height, isActive, isInfo, styles]); [height, isActive, isInfo, styles]);
if ((!isInfo && (channel.deleteAt > 0 && !isActive)) || !myChannel || !isVisible) { if (!myChannel) {
return null; return null;
} }
@@ -182,37 +165,36 @@ const ChannelListItem = ({
} }
return ( return (
<Animated.View style={animatedStyle}> <TouchableOpacity onPress={handleOnPress}>
<TouchableOpacity onPress={handleOnPress}> <>
<> <View
<View style={containerStyle}
style={containerStyle} testID={`${testID}.${channel.name}.collapsed.${!isActive}`}
testID={`${testID}.${channel.name}.collapsed.${collapsed && !isActive}`} >
> <View style={styles.wrapper}>
<View style={styles.wrapper}> <ChannelIcon
<ChannelIcon hasDraft={hasDraft}
hasDraft={hasDraft} isActive={isInfo ? false : isActive}
isActive={isInfo ? false : isActive} isInfo={isInfo}
isInfo={isInfo} isUnread={isBright}
isUnread={isBright} isArchived={channel.deleteAt > 0}
isArchived={channel.deleteAt > 0} membersCount={membersCount}
membersCount={membersCount} name={channel.name}
name={channel.name} shared={channel.shared}
shared={channel.shared} size={24}
size={24} type={channel.type}
type={channel.type} isMuted={isMuted}
isMuted={isMuted} />
/> <View>
<View> <Text
<Text ellipsizeMode='tail'
ellipsizeMode='tail' numberOfLines={1}
numberOfLines={1} style={textStyles}
style={textStyles} testID={`${testID}.${channel.name}.display_name`}
testID={`${testID}.${channel.name}.display_name`} >
> {displayName}
{displayName} </Text>
</Text> {isInfo && Boolean(teamDisplayName) && !isTablet &&
{isInfo && Boolean(teamDisplayName) && !isTablet &&
<Text <Text
ellipsizeMode='tail' ellipsizeMode='tail'
numberOfLines={1} numberOfLines={1}
@@ -221,34 +203,33 @@ const ChannelListItem = ({
> >
{teamDisplayName} {teamDisplayName}
</Text> </Text>
}
</View>
{Boolean(teammateId) &&
<CustomStatus
isInfo={isInfo}
userId={teammateId!}
/>
}
{isInfo && Boolean(teamDisplayName) && isTablet &&
<Text
ellipsizeMode='tail'
numberOfLines={1}
testID={`${testID}.${teamDisplayName}.display_name`}
style={[styles.teamName, styles.teamNameTablet]}
>
{teamDisplayName}
</Text>
} }
</View> </View>
<Badge {Boolean(teammateId) &&
visible={myChannel.mentionsCount > 0} <CustomStatus
value={myChannel.mentionsCount} isInfo={isInfo}
style={[styles.badge, isMuted && styles.mutedBadge, isInfo && styles.infoBadge]} userId={teammateId!}
/> />
}
{isInfo && Boolean(teamDisplayName) && isTablet &&
<Text
ellipsizeMode='tail'
numberOfLines={1}
testID={`${testID}.${teamDisplayName}.display_name`}
style={[styles.teamName, styles.teamNameTablet]}
>
{teamDisplayName}
</Text>
}
</View> </View>
</> <Badge
</TouchableOpacity> visible={myChannel.mentionsCount > 0}
</Animated.View> value={myChannel.mentionsCount}
style={[styles.badge, isMuted && styles.mutedBadge, isInfo && styles.infoBadge]}
/>
</View>
</>
</TouchableOpacity>
); );
}; };

View File

@@ -4,14 +4,12 @@
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables'; import withObservables from '@nozbe/with-observables';
import React from 'react'; import React from 'react';
import {of as of$, combineLatest} from 'rxjs'; import {of as of$} from 'rxjs';
import {switchMap, distinctUntilChanged} from 'rxjs/operators'; import {switchMap, distinctUntilChanged} from 'rxjs/operators';
import {General, Preferences} from '@constants'; import {General} from '@constants';
import {getPreferenceAsBool} from '@helpers/api/preference';
import {observeMyChannel} from '@queries/servers/channel'; import {observeMyChannel} from '@queries/servers/channel';
import {queryDraft} from '@queries/servers/drafts'; import {queryDraft} from '@queries/servers/drafts';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {observeCurrentChannelId, observeCurrentUserId} from '@queries/servers/system'; import {observeCurrentChannelId, observeCurrentUserId} from '@queries/servers/system';
import ChannelModel from '@typings/database/models/servers/channel'; import ChannelModel from '@typings/database/models/servers/channel';
import MyChannelModel from '@typings/database/models/servers/my_channel'; import MyChannelModel from '@typings/database/models/servers/my_channel';
@@ -19,18 +17,15 @@ import MyChannelModel from '@typings/database/models/servers/my_channel';
import ChannelItem from './channel_item'; import ChannelItem from './channel_item';
import type {WithDatabaseArgs} from '@typings/database/database'; import type {WithDatabaseArgs} from '@typings/database/database';
import type PreferenceModel from '@typings/database/models/servers/preference';
type EnhanceProps = WithDatabaseArgs & { type EnhanceProps = WithDatabaseArgs & {
channel: ChannelModel; channel: ChannelModel;
isInfo?: boolean;
isUnreads?: boolean;
showTeamName?: boolean; showTeamName?: boolean;
} }
const observeIsMutedSetting = (mc: MyChannelModel) => mc.settings.observe().pipe(switchMap((s) => of$(s?.notifyProps?.mark_unread === 'mention'))); const observeIsMutedSetting = (mc: MyChannelModel) => mc.settings.observe().pipe(switchMap((s) => of$(s?.notifyProps?.mark_unread === 'mention')));
const enhance = withObservables(['channel', 'isUnreads', 'showTeamName'], ({channel, database, isInfo, isUnreads, showTeamName}: EnhanceProps) => { const enhance = withObservables(['channel', 'showTeamName'], ({channel, database, showTeamName}: EnhanceProps) => {
const currentUserId = observeCurrentUserId(database); const currentUserId = observeCurrentUserId(database);
const myChannel = observeMyChannel(database, channel.id); const myChannel = observeMyChannel(database, channel.id);
@@ -39,29 +34,6 @@ const enhance = withObservables(['channel', 'isUnreads', 'showTeamName'], ({chan
); );
const isActive = observeCurrentChannelId(database).pipe(switchMap((id) => of$(id ? id === channel.id : false)), distinctUntilChanged()); const isActive = observeCurrentChannelId(database).pipe(switchMap((id) => of$(id ? id === channel.id : false)), distinctUntilChanged());
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))),
);
const isVisible = combineLatest([myChannel, unreadsOnTop]).pipe(
switchMap(([mc, u]) => {
if (!mc) {
return of$(false);
}
if (isInfo) {
return of$(true);
}
if (isUnreads) {
return of$(u);
}
return u ? of$(!mc.isUnread || !mc.mentionsCount) : of$(true);
}),
);
const isMuted = myChannel.pipe( const isMuted = myChannel.pipe(
switchMap((mc) => { switchMap((mc) => {
@@ -90,7 +62,6 @@ const enhance = withObservables(['channel', 'isUnreads', 'showTeamName'], ({chan
hasDraft, hasDraft,
isActive, isActive,
isMuted, isMuted,
isVisible,
membersCount, membersCount,
myChannel, myChannel,
teamDisplayName, teamDisplayName,

View File

@@ -170,7 +170,6 @@ const FilteredList = ({
}, {displayName}), }, {displayName}),
); );
return; return;
return;
} }
await close(); await close();
@@ -209,7 +208,6 @@ const FilteredList = ({
return ( return (
<ChannelItem <ChannelItem
channel={item} channel={item}
collapsed={false}
isInfo={true} isInfo={true}
onPress={onSwitchToChannel} onPress={onSwitchToChannel}
showTeamName={showTeamName} showTeamName={showTeamName}

View File

@@ -76,7 +76,6 @@ const UnfilteredList = ({close, keyboardHeight, recentChannels, showTeamName, un
return ( return (
<ChannelItem <ChannelItem
channel={item} channel={item}
collapsed={false}
isInfo={true} isInfo={true}
onPress={onPress} onPress={onPress}
showTeamName={showTeamName} showTeamName={showTeamName}

View File

@@ -1,167 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/channel_list/categories/body should match snapshot 1`] = `
Object {
"children": Array [
<View>
<View
onLayout={[Function]}
style={null}
>
<View
animatedStyle={
Object {
"value": Object {
"height": 40,
"marginVertical": 2,
"opacity": 1,
},
}
}
collapsable={false}
style={
Object {
"height": 40,
"marginVertical": 2,
"opacity": 1,
}
}
>
<View
accessible={true}
collapsable={false}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"opacity": 1,
}
}
>
<View
style={
Array [
Object {
"alignItems": "center",
"flexDirection": "row",
"minHeight": 40,
"paddingHorizontal": 20,
},
false,
undefined,
Object {
"minHeight": 40,
},
]
}
testID="category.test_category.channel_list_item.channel.collapsed.false"
>
<View
style={
Object {
"flex": 1,
"flexDirection": "row",
}
}
>
<View
style={
Array [
Object {
"alignItems": "center",
"justifyContent": "center",
},
Object {
"height": 24,
"width": 24,
},
undefined,
undefined,
]
}
>
<Icon
name="globe"
style={
Array [
Object {
"color": "rgba(255,255,255,0.4)",
},
undefined,
undefined,
Object {
"fontSize": 24,
"left": 1,
},
]
}
testID="undefined.public"
/>
</View>
<View>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
Array [
Object {
"fontFamily": "OpenSans",
"fontSize": 16,
"fontWeight": "400",
"lineHeight": 24,
},
Object {
"color": "rgba(255,255,255,0.72)",
"marginTop": -1,
"paddingLeft": 12,
},
false,
false,
null,
null,
]
}
testID="category.test_category.channel_list_item.channel.display_name"
>
Channel
</Text>
</View>
</View>
</View>
</View>
</View>
</View>
</View>,
],
"props": Object {
"data": Anything,
"getItem": [Function],
"getItemCount": [Function],
"initialNumToRender": 20,
"invertStickyHeaders": undefined,
"keyExtractor": [Function],
"onContentSizeChange": [Function],
"onLayout": [Function],
"onMomentumScrollBegin": [Function],
"onMomentumScrollEnd": [Function],
"onScroll": [Function],
"onScrollBeginDrag": [Function],
"onScrollEndDrag": [Function],
"removeClippedSubviews": true,
"renderItem": [Function],
"scrollEventThrottle": 50,
"stickyHeaderIndices": Array [],
"style": undefined,
"updateCellsBatchingPeriod": 10,
"viewabilityConfigCallbackPairs": Array [],
"windowSize": 15,
},
"type": "RCTScrollView",
}
`;

View File

@@ -35,15 +35,14 @@ describe('components/channel_list/categories/body', () => {
<CategoryBody <CategoryBody
category={category} category={category}
locale={DEFAULT_LOCALE} locale={DEFAULT_LOCALE}
isTablet={false}
onChannelSwitch={() => undefined} onChannelSwitch={() => undefined}
/>, />,
{database}, {database},
); );
setTimeout(() => { setTimeout(() => {
expect(wrapper.toJSON()).toMatchSnapshot({ expect(wrapper.toJSON()).toBeTruthy();
props: {data: expect.anything()},
});
done(); done();
}); });
}); });

View File

@@ -1,8 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React, {useCallback, useMemo} from 'react'; import React, {useCallback, useEffect, useMemo} from 'react';
import {FlatList} from 'react-native'; import {FlatList} from 'react-native';
import Animated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
import ChannelItem from '@components/channel_item'; import ChannelItem from '@components/channel_item';
import {DMS_CATEGORY} from '@constants/categories'; import {DMS_CATEGORY} from '@constants/categories';
@@ -39,26 +40,40 @@ const CategoryBody = ({sortedChannels, category, hiddenChannelIds, limit, onChan
return ( return (
<ChannelItem <ChannelItem
channel={item} channel={item}
collapsed={category.collapsed}
testID={`category.${category.displayName.replace(/ /g, '_').toLocaleLowerCase()}.channel_list_item`} testID={`category.${category.displayName.replace(/ /g, '_').toLocaleLowerCase()}.channel_list_item`}
onPress={onChannelSwitch} onPress={onChannelSwitch}
/> />
); );
}, [category.collapsed, onChannelSwitch]); }, [onChannelSwitch]);
const sharedValue = useSharedValue(category.collapsed);
useEffect(() => {
sharedValue.value = category.collapsed;
}, [category.collapsed]);
const height = ids.length ? ids.length * 40 : 0;
const animatedStyle = useAnimatedStyle(() => {
return {
height: withTiming(sharedValue.value ? 1 : height, {duration: 300}),
opacity: withTiming(sharedValue.value ? 0 : 1, {duration: sharedValue.value ? 200 : 300, easing: Easing.inOut(Easing.exp)}),
};
}, [height]);
return ( return (
<FlatList <Animated.View
data={ids} style={animatedStyle}
renderItem={renderItem} >
keyExtractor={extractKey} <FlatList
removeClippedSubviews={true} data={ids}
initialNumToRender={20} renderItem={renderItem}
windowSize={15} keyExtractor={extractKey}
updateCellsBatchingPeriod={10}
// @ts-expect-error strictMode not exposed on the types // @ts-expect-error strictMode not exposed on the types
strictMode={true} strictMode={true}
/> />
</Animated.View>
); );
}; };

View File

@@ -5,13 +5,14 @@ import {Database} from '@nozbe/watermelondb';
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables'; import withObservables from '@nozbe/with-observables';
import {combineLatest, of as of$} from 'rxjs'; import {combineLatest, of as of$} from 'rxjs';
import {map, switchMap, concatAll} from 'rxjs/operators'; import {map, switchMap, concatAll, combineLatestWith} from 'rxjs/operators';
import {General, Preferences} from '@constants'; import {General, Preferences} from '@constants';
import {DMS_CATEGORY} from '@constants/categories'; import {DMS_CATEGORY} from '@constants/categories';
import {queryChannelsByNames, queryMyChannelSettingsByIds} from '@queries/servers/channel'; import {getPreferenceAsBool} from '@helpers/api/preference';
import {observeAllMyChannelNotifyProps, queryChannelsByNames, queryMyChannelSettingsByIds} from '@queries/servers/channel';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference'; import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {observeCurrentUserId} from '@queries/servers/system'; import {observeCurrentChannelId, observeCurrentUserId, observeLastUnreadChannelId} from '@queries/servers/system';
import {WithDatabaseArgs} from '@typings/database/database'; import {WithDatabaseArgs} from '@typings/database/database';
import {getDirectChannelName} from '@utils/channel'; import {getDirectChannelName} from '@utils/channel';
@@ -104,11 +105,12 @@ type EnhanceProps = {
category: CategoryModel; category: CategoryModel;
locale: string; locale: string;
currentUserId: string; currentUserId: string;
isTablet: boolean;
} & WithDatabaseArgs } & WithDatabaseArgs
const withUserId = withObservables([], ({database}: WithDatabaseArgs) => ({currentUserId: observeCurrentUserId(database)})); const withUserId = withObservables([], ({database}: WithDatabaseArgs) => ({currentUserId: observeCurrentUserId(database)}));
const enhance = withObservables(['category', 'locale'], ({category, locale, database, currentUserId}: EnhanceProps) => { const enhance = withObservables(['category', 'isTablet', 'locale'], ({category, locale, isTablet, database, currentUserId}: EnhanceProps) => {
const observedCategory = category.observe(); const observedCategory = category.observe();
const sortedChannels = observedCategory.pipe( const sortedChannels = observedCategory.pipe(
switchMap((c) => getSortedChannels(database, c, locale)), switchMap((c) => getSortedChannels(database, c, locale)),
@@ -145,10 +147,42 @@ const enhance = withObservables(['category', 'locale'], ({category, locale, data
([a, b]) => of$(new Set(a.concat(b))), ([a, b]) => of$(new Set(a.concat(b))),
)); ));
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))),
);
const notifyProps = observeAllMyChannelNotifyProps(database);
const lastUnreadId = isTablet ? observeLastUnreadChannelId(database) : of$(undefined);
const unreadChannelIds = category.myChannels.observeWithColumns(['mentions_count', 'is_unread']).pipe(
combineLatestWith(unreadsOnTop, notifyProps, lastUnreadId),
map(([my, unreadTop, settings, lastUnread]) => {
if (!unreadTop) {
return new Set();
}
return my.reduce<Set<string>>((set, m) => {
const isMuted = settings[m.id]?.mark_unread === 'mention';
if ((isMuted && m.mentionsCount) || (!isMuted && m.isUnread) || m.id === lastUnread) {
set.add(m.id);
}
return set;
}, new Set());
}),
);
const currentChannelId = observeCurrentChannelId(database);
const filtered = sortedChannels.pipe(
combineLatestWith(currentChannelId, unreadChannelIds),
map(([channels, ccId, unreadIds]) => {
return channels.filter((c) => c && ((c.deleteAt > 0 && c.id === ccId) || !c.deleteAt) && !unreadIds.has(c.id));
}),
);
return { return {
limit, limit,
hiddenChannelIds, hiddenChannelIds,
sortedChannels, sortedChannels: filtered,
category: observedCategory, category: observedCategory,
}; };
}); });

View File

@@ -7,6 +7,7 @@ import {FlatList, StyleSheet} from 'react-native';
import {switchToChannelById} from '@actions/remote/channel'; import {switchToChannelById} from '@actions/remote/channel';
import {useServerUrl} from '@context/server'; import {useServerUrl} from '@context/server';
import {useIsTablet} from '@hooks/device';
import CategoryBody from './body'; import CategoryBody from './body';
import LoadCategoriesError from './error'; import LoadCategoriesError from './error';
@@ -35,6 +36,7 @@ const Categories = ({categories, currentTeamId, unreadsOnTop}: Props) => {
const intl = useIntl(); const intl = useIntl();
const listRef = useRef<FlatList>(null); const listRef = useRef<FlatList>(null);
const serverUrl = useServerUrl(); const serverUrl = useServerUrl();
const isTablet = useIsTablet();
const onChannelSwitch = useCallback(async (channelId: string) => { const onChannelSwitch = useCallback(async (channelId: string) => {
switchToChannelById(serverUrl, channelId); switchToChannelById(serverUrl, channelId);
@@ -45,6 +47,7 @@ const Categories = ({categories, currentTeamId, unreadsOnTop}: Props) => {
return ( return (
<UnreadCategories <UnreadCategories
currentTeamId={currentTeamId} currentTeamId={currentTeamId}
isTablet={isTablet}
onChannelSwitch={onChannelSwitch} onChannelSwitch={onChannelSwitch}
/> />
); );
@@ -54,12 +57,13 @@ const Categories = ({categories, currentTeamId, unreadsOnTop}: Props) => {
<CategoryHeader category={data.item}/> <CategoryHeader category={data.item}/>
<CategoryBody <CategoryBody
category={data.item} category={data.item}
isTablet={isTablet}
locale={intl.locale} locale={intl.locale}
onChannelSwitch={onChannelSwitch} onChannelSwitch={onChannelSwitch}
/> />
</> </>
); );
}, [currentTeamId, intl.locale, onChannelSwitch]); }, [currentTeamId, intl.locale, isTablet, onChannelSwitch]);
useEffect(() => { useEffect(() => {
listRef.current?.scrollToOffset({animated: false, offset: 0}); listRef.current?.scrollToOffset({animated: false, offset: 0});
@@ -87,11 +91,7 @@ const Categories = ({categories, currentTeamId, unreadsOnTop}: Props) => {
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
keyExtractor={extractKey} keyExtractor={extractKey}
removeClippedSubviews={true} initialNumToRender={categoriesToShow.length}
initialNumToRender={5}
windowSize={15}
updateCellsBatchingPeriod={10}
maxToRenderPerBatch={5}
// @ts-expect-error strictMode not included in the types // @ts-expect-error strictMode not included in the types
strictMode={true} strictMode={true}

View File

@@ -21,7 +21,10 @@ import type {WithDatabaseArgs} from '@typings/database/database';
import type ChannelModel from '@typings/database/models/servers/channel'; import type ChannelModel from '@typings/database/models/servers/channel';
import type PreferenceModel from '@typings/database/models/servers/preference'; import type PreferenceModel from '@typings/database/models/servers/preference';
type WithDatabaseProps = { currentTeamId: string } & WithDatabaseArgs type WithDatabaseProps = WithDatabaseArgs & {
currentTeamId: string;
isTablet: boolean;
}
type CA = [ type CA = [
a: Array<ChannelModel | null>, a: Array<ChannelModel | null>,
@@ -42,7 +45,7 @@ const filterMutedFromMyChannels = ([myChannels, notifyProps]: [MyChannelModel[],
); );
}; };
const enhanced = withObservables(['currentTeamId'], ({currentTeamId, database}: WithDatabaseProps) => { const enhanced = withObservables(['currentTeamId', 'isTablet'], ({currentTeamId, isTablet, database}: WithDatabaseProps) => {
const unreadsOnTop = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_GROUP_UNREADS). const unreadsOnTop = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_GROUP_UNREADS).
observeWithColumns(['value']). observeWithColumns(['value']).
pipe( pipe(
@@ -53,9 +56,9 @@ const enhanced = withObservables(['currentTeamId'], ({currentTeamId, database}:
const unreadChannels = unreadsOnTop.pipe(switchMap((gU) => { const unreadChannels = unreadsOnTop.pipe(switchMap((gU) => {
if (gU) { if (gU) {
const lastUnread = observeLastUnreadChannelId(database).pipe( const lastUnread = isTablet ? observeLastUnreadChannelId(database).pipe(
switchMap(getC), switchMap(getC),
); ) : of$('');
const notifyProps = observeAllMyChannelNotifyProps(database); const notifyProps = observeAllMyChannelNotifyProps(database);
const unreads = queryMyChannelUnreads(database, currentTeamId).observe().pipe( const unreads = queryMyChannelUnreads(database, currentTeamId).observe().pipe(

View File

@@ -36,8 +36,6 @@ const UnreadCategories = ({onChannelSwitch, unreadChannels}: UnreadCategoriesPro
return ( return (
<ChannelItem <ChannelItem
channel={item} channel={item}
collapsed={false}
isUnreads={true}
onPress={onChannelSwitch} onPress={onChannelSwitch}
/> />
); );