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`] = `
<View
animatedStyle={
Object {
"value": Object {
"height": 40,
"marginVertical": 2,
"opacity": 1,
},
}
}
accessible={true}
collapsable={false}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
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,
}
Array [
Object {
"alignItems": "center",
"flexDirection": "row",
"minHeight": 40,
"paddingHorizontal": 20,
},
false,
undefined,
Object {
"minHeight": 40,
},
]
}
testID="channel_list_item.hello.collapsed.true"
>
<View
style={
Array [
Object {
"alignItems": "center",
"flexDirection": "row",
"minHeight": 40,
"paddingHorizontal": 20,
},
false,
undefined,
Object {
"minHeight": 40,
},
]
Object {
"flex": 1,
"flexDirection": "row",
}
}
testID="channel_list_item.hello.collapsed.false"
>
<View
style={
Object {
"flex": 1,
"flexDirection": "row",
}
Array [
Object {
"alignItems": "center",
"justifyContent": "center",
},
Object {
"height": 24,
"width": 24,
},
undefined,
undefined,
]
}
>
<View
<Icon
name="globe"
style={
Array [
Object {
"alignItems": "center",
"justifyContent": "center",
"color": "rgba(255,255,255,0.4)",
},
undefined,
undefined,
Object {
"height": 24,
"width": 24,
"fontSize": 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
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="channel_list_item.hello.display_name"
>
Hello!
</Text>
</View>
Hello!
</Text>
</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`] = `
<View
animatedStyle={
Object {
"value": Object {
"height": 40,
"marginVertical": 2,
"opacity": 1,
},
}
}
accessible={true}
collapsable={false}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
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,
}
Array [
Object {
"alignItems": "center",
"flexDirection": "row",
"minHeight": 40,
"paddingHorizontal": 20,
},
false,
undefined,
Object {
"minHeight": 40,
},
]
}
testID="channel_list_item.hello.collapsed.true"
>
<View
style={
Array [
Object {
"alignItems": "center",
"flexDirection": "row",
"minHeight": 40,
"paddingHorizontal": 20,
},
false,
undefined,
Object {
"minHeight": 40,
},
]
Object {
"flex": 1,
"flexDirection": "row",
}
}
testID="channel_list_item.hello.collapsed.false"
>
<View
style={
Object {
"flex": 1,
"flexDirection": "row",
}
Array [
Object {
"alignItems": "center",
"justifyContent": "center",
},
Object {
"height": 24,
"width": 24,
},
undefined,
undefined,
]
}
>
<View
<Icon
name="pencil-outline"
style={
Array [
Object {
"alignItems": "center",
"justifyContent": "center",
"color": "rgba(255,255,255,0.4)",
},
undefined,
undefined,
Object {
"height": 24,
"width": 24,
"fontSize": 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
name="pencil-outline"
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>
Hello!
</Text>
</View>
</View>
</View>

View File

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

View File

@@ -1,10 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// 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 {StyleSheet, Text, TouchableOpacity, View} from 'react-native';
import Animated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
import Badge from '@components/badge';
import ChannelIcon from '@components/channel_icon';
@@ -22,13 +21,11 @@ import type MyChannelModel from '@typings/database/models/servers/my_channel';
type Props = {
channel: ChannelModel;
collapsed: boolean;
currentUserId: string;
hasDraft: boolean;
isActive: boolean;
isInfo?: boolean;
isMuted: boolean;
isVisible: boolean;
membersCount: number;
myChannel?: MyChannelModel;
onPress: (channelId: string) => void;
@@ -115,8 +112,8 @@ export const textStyle = StyleSheet.create({
});
const ChannelListItem = ({
channel, collapsed, currentUserId, hasDraft,
isActive, isInfo, isMuted, isVisible, membersCount,
channel, currentUserId, hasDraft,
isActive, isInfo, isMuted, membersCount,
myChannel, onPress, teamDisplayName, testID}: Props) => {
const {formatMessage} = useIntl();
const theme = useTheme();
@@ -126,8 +123,6 @@ const ChannelListItem = ({
// Make it brighter if it's not muted, and highlighted or has unreads
const isBright = !isMuted && (myChannel && (myChannel.isUnread || myChannel.mentionsCount > 0));
const shouldCollapse = (collapsed && !isBright) && !isActive;
const sharedValue = useSharedValue(shouldCollapse);
const height = useMemo(() => {
let h = 40;
if (isInfo) {
@@ -136,18 +131,6 @@ const ChannelListItem = ({
return h;
}, [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(() => {
onPress(myChannel?.id || channel.id);
}, [channel.id, myChannel?.id]);
@@ -169,7 +152,7 @@ const ChannelListItem = ({
],
[height, isActive, isInfo, styles]);
if ((!isInfo && (channel.deleteAt > 0 && !isActive)) || !myChannel || !isVisible) {
if (!myChannel) {
return null;
}
@@ -182,37 +165,36 @@ const ChannelListItem = ({
}
return (
<Animated.View style={animatedStyle}>
<TouchableOpacity onPress={handleOnPress}>
<>
<View
style={containerStyle}
testID={`${testID}.${channel.name}.collapsed.${collapsed && !isActive}`}
>
<View style={styles.wrapper}>
<ChannelIcon
hasDraft={hasDraft}
isActive={isInfo ? false : isActive}
isInfo={isInfo}
isUnread={isBright}
isArchived={channel.deleteAt > 0}
membersCount={membersCount}
name={channel.name}
shared={channel.shared}
size={24}
type={channel.type}
isMuted={isMuted}
/>
<View>
<Text
ellipsizeMode='tail'
numberOfLines={1}
style={textStyles}
testID={`${testID}.${channel.name}.display_name`}
>
{displayName}
</Text>
{isInfo && Boolean(teamDisplayName) && !isTablet &&
<TouchableOpacity onPress={handleOnPress}>
<>
<View
style={containerStyle}
testID={`${testID}.${channel.name}.collapsed.${!isActive}`}
>
<View style={styles.wrapper}>
<ChannelIcon
hasDraft={hasDraft}
isActive={isInfo ? false : isActive}
isInfo={isInfo}
isUnread={isBright}
isArchived={channel.deleteAt > 0}
membersCount={membersCount}
name={channel.name}
shared={channel.shared}
size={24}
type={channel.type}
isMuted={isMuted}
/>
<View>
<Text
ellipsizeMode='tail'
numberOfLines={1}
style={textStyles}
testID={`${testID}.${channel.name}.display_name`}
>
{displayName}
</Text>
{isInfo && Boolean(teamDisplayName) && !isTablet &&
<Text
ellipsizeMode='tail'
numberOfLines={1}
@@ -221,34 +203,33 @@ const ChannelListItem = ({
>
{teamDisplayName}
</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>
<Badge
visible={myChannel.mentionsCount > 0}
value={myChannel.mentionsCount}
style={[styles.badge, isMuted && styles.mutedBadge, isInfo && styles.infoBadge]}
{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>
</>
</TouchableOpacity>
</Animated.View>
<Badge
visible={myChannel.mentionsCount > 0}
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 withObservables from '@nozbe/with-observables';
import React from 'react';
import {of as of$, combineLatest} from 'rxjs';
import {of as of$} from 'rxjs';
import {switchMap, distinctUntilChanged} from 'rxjs/operators';
import {General, Preferences} from '@constants';
import {getPreferenceAsBool} from '@helpers/api/preference';
import {General} from '@constants';
import {observeMyChannel} from '@queries/servers/channel';
import {queryDraft} from '@queries/servers/drafts';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {observeCurrentChannelId, observeCurrentUserId} from '@queries/servers/system';
import ChannelModel from '@typings/database/models/servers/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 type {WithDatabaseArgs} from '@typings/database/database';
import type PreferenceModel from '@typings/database/models/servers/preference';
type EnhanceProps = WithDatabaseArgs & {
channel: ChannelModel;
isInfo?: boolean;
isUnreads?: boolean;
showTeamName?: boolean;
}
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 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 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(
switchMap((mc) => {
@@ -90,7 +62,6 @@ const enhance = withObservables(['channel', 'isUnreads', 'showTeamName'], ({chan
hasDraft,
isActive,
isMuted,
isVisible,
membersCount,
myChannel,
teamDisplayName,

View File

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

View File

@@ -76,7 +76,6 @@ const UnfilteredList = ({close, keyboardHeight, recentChannels, showTeamName, un
return (
<ChannelItem
channel={item}
collapsed={false}
isInfo={true}
onPress={onPress}
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
category={category}
locale={DEFAULT_LOCALE}
isTablet={false}
onChannelSwitch={() => undefined}
/>,
{database},
);
setTimeout(() => {
expect(wrapper.toJSON()).toMatchSnapshot({
props: {data: expect.anything()},
});
expect(wrapper.toJSON()).toBeTruthy();
done();
});
});

View File

@@ -1,8 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// 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 Animated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
import ChannelItem from '@components/channel_item';
import {DMS_CATEGORY} from '@constants/categories';
@@ -39,26 +40,40 @@ const CategoryBody = ({sortedChannels, category, hiddenChannelIds, limit, onChan
return (
<ChannelItem
channel={item}
collapsed={category.collapsed}
testID={`category.${category.displayName.replace(/ /g, '_').toLocaleLowerCase()}.channel_list_item`}
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 (
<FlatList
data={ids}
renderItem={renderItem}
keyExtractor={extractKey}
removeClippedSubviews={true}
initialNumToRender={20}
windowSize={15}
updateCellsBatchingPeriod={10}
<Animated.View
style={animatedStyle}
>
<FlatList
data={ids}
renderItem={renderItem}
keyExtractor={extractKey}
// @ts-expect-error strictMode not exposed on the types
strictMode={true}
/>
// @ts-expect-error strictMode not exposed on the types
strictMode={true}
/>
</Animated.View>
);
};

View File

@@ -5,13 +5,14 @@ import {Database} from '@nozbe/watermelondb';
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
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 {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 {observeCurrentUserId} from '@queries/servers/system';
import {observeCurrentChannelId, observeCurrentUserId, observeLastUnreadChannelId} from '@queries/servers/system';
import {WithDatabaseArgs} from '@typings/database/database';
import {getDirectChannelName} from '@utils/channel';
@@ -104,11 +105,12 @@ type EnhanceProps = {
category: CategoryModel;
locale: string;
currentUserId: string;
isTablet: boolean;
} & WithDatabaseArgs
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 sortedChannels = observedCategory.pipe(
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))),
));
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 {
limit,
hiddenChannelIds,
sortedChannels,
sortedChannels: filtered,
category: observedCategory,
};
});

View File

@@ -7,6 +7,7 @@ import {FlatList, StyleSheet} from 'react-native';
import {switchToChannelById} from '@actions/remote/channel';
import {useServerUrl} from '@context/server';
import {useIsTablet} from '@hooks/device';
import CategoryBody from './body';
import LoadCategoriesError from './error';
@@ -35,6 +36,7 @@ const Categories = ({categories, currentTeamId, unreadsOnTop}: Props) => {
const intl = useIntl();
const listRef = useRef<FlatList>(null);
const serverUrl = useServerUrl();
const isTablet = useIsTablet();
const onChannelSwitch = useCallback(async (channelId: string) => {
switchToChannelById(serverUrl, channelId);
@@ -45,6 +47,7 @@ const Categories = ({categories, currentTeamId, unreadsOnTop}: Props) => {
return (
<UnreadCategories
currentTeamId={currentTeamId}
isTablet={isTablet}
onChannelSwitch={onChannelSwitch}
/>
);
@@ -54,12 +57,13 @@ const Categories = ({categories, currentTeamId, unreadsOnTop}: Props) => {
<CategoryHeader category={data.item}/>
<CategoryBody
category={data.item}
isTablet={isTablet}
locale={intl.locale}
onChannelSwitch={onChannelSwitch}
/>
</>
);
}, [currentTeamId, intl.locale, onChannelSwitch]);
}, [currentTeamId, intl.locale, isTablet, onChannelSwitch]);
useEffect(() => {
listRef.current?.scrollToOffset({animated: false, offset: 0});
@@ -87,11 +91,7 @@ const Categories = ({categories, currentTeamId, unreadsOnTop}: Props) => {
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
keyExtractor={extractKey}
removeClippedSubviews={true}
initialNumToRender={5}
windowSize={15}
updateCellsBatchingPeriod={10}
maxToRenderPerBatch={5}
initialNumToRender={categoriesToShow.length}
// @ts-expect-error strictMode not included in the types
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 PreferenceModel from '@typings/database/models/servers/preference';
type WithDatabaseProps = { currentTeamId: string } & WithDatabaseArgs
type WithDatabaseProps = WithDatabaseArgs & {
currentTeamId: string;
isTablet: boolean;
}
type CA = [
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).
observeWithColumns(['value']).
pipe(
@@ -53,9 +56,9 @@ const enhanced = withObservables(['currentTeamId'], ({currentTeamId, database}:
const unreadChannels = unreadsOnTop.pipe(switchMap((gU) => {
if (gU) {
const lastUnread = observeLastUnreadChannelId(database).pipe(
const lastUnread = isTablet ? observeLastUnreadChannelId(database).pipe(
switchMap(getC),
);
) : of$('');
const notifyProps = observeAllMyChannelNotifyProps(database);
const unreads = queryMyChannelUnreads(database, currentTeamId).observe().pipe(

View File

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