From 362db9d98d05e84dc0dbccac61c73e30c978d104 Mon Sep 17 00:00:00 2001 From: Shaz MJ Date: Thu, 5 May 2022 11:17:33 +1000 Subject: [PATCH] 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 Co-authored-by: Mattermod --- .../__snapshots__/channel_item.test.tsx.snap | 340 ++++++++---------- .../channel_item/channel_item.test.tsx | 4 - app/components/channel_item/channel_item.tsx | 133 +++---- app/components/channel_item/index.ts | 35 +- .../filtered_list/filtered_list.tsx | 2 - .../unfiltered_list/unfiltered_list.tsx | 1 - .../__snapshots__/category_body.test.tsx.snap | 167 --------- .../categories/body/category_body.test.tsx | 5 +- .../categories/body/category_body.tsx | 43 ++- .../categories_list/categories/body/index.ts | 44 ++- .../categories_list/categories/categories.tsx | 12 +- .../categories/unreads/index.ts | 11 +- .../categories/unreads/unreads.tsx | 2 - 13 files changed, 293 insertions(+), 506 deletions(-) delete mode 100644 app/screens/home/channel_list/categories_list/categories/body/__snapshots__/category_body.test.tsx.snap diff --git a/app/components/channel_item/__snapshots__/channel_item.test.tsx.snap b/app/components/channel_item/__snapshots__/channel_item.test.tsx.snap index ae6f9a82cc..0ca9a6f667 100644 --- a/app/components/channel_item/__snapshots__/channel_item.test.tsx.snap +++ b/app/components/channel_item/__snapshots__/channel_item.test.tsx.snap @@ -2,129 +2,109 @@ exports[`components/channel_list/categories/body/channel_item should match snapshot 1`] = ` - + + + - - - - - Hello! - - + Hello! + @@ -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`] = ` - + + + - - - - - Hello! - - + Hello! + diff --git a/app/components/channel_item/channel_item.test.tsx b/app/components/channel_item/channel_item.test.tsx index 592b68e9d2..b8b5a8f489 100644 --- a/app/components/channel_item/channel_item.test.tsx +++ b/app/components/channel_item/channel_item.test.tsx @@ -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} />, ); diff --git a/app/components/channel_item/channel_item.tsx b/app/components/channel_item/channel_item.tsx index b832b3f91f..17c5296eb6 100644 --- a/app/components/channel_item/channel_item.tsx +++ b/app/components/channel_item/channel_item.tsx @@ -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 ( - - - <> - - - 0} - membersCount={membersCount} - name={channel.name} - shared={channel.shared} - size={24} - type={channel.type} - isMuted={isMuted} - /> - - - {displayName} - - {isInfo && Boolean(teamDisplayName) && !isTablet && + + <> + + + 0} + membersCount={membersCount} + name={channel.name} + shared={channel.shared} + size={24} + type={channel.type} + isMuted={isMuted} + /> + + + {displayName} + + {isInfo && Boolean(teamDisplayName) && !isTablet && {teamDisplayName} - } - - {Boolean(teammateId) && - - } - {isInfo && Boolean(teamDisplayName) && isTablet && - - {teamDisplayName} - } - 0} - value={myChannel.mentionsCount} - style={[styles.badge, isMuted && styles.mutedBadge, isInfo && styles.infoBadge]} + {Boolean(teammateId) && + + } + {isInfo && Boolean(teamDisplayName) && isTablet && + + {teamDisplayName} + + } - - - + 0} + value={myChannel.mentionsCount} + style={[styles.badge, isMuted && styles.mutedBadge, isInfo && styles.infoBadge]} + /> + + + ); }; diff --git a/app/components/channel_item/index.ts b/app/components/channel_item/index.ts index e9a877b6fb..da0ada9709 100644 --- a/app/components/channel_item/index.ts +++ b/app/components/channel_item/index.ts @@ -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, diff --git a/app/screens/find_channels/filtered_list/filtered_list.tsx b/app/screens/find_channels/filtered_list/filtered_list.tsx index 09c0d64c75..e1a997a704 100644 --- a/app/screens/find_channels/filtered_list/filtered_list.tsx +++ b/app/screens/find_channels/filtered_list/filtered_list.tsx @@ -170,7 +170,6 @@ const FilteredList = ({ }, {displayName}), ); return; - return; } await close(); @@ -209,7 +208,6 @@ const FilteredList = ({ return ( - - - - - - - - - - - Channel - - - - - - - - , - ], - "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", -} -`; diff --git a/app/screens/home/channel_list/categories_list/categories/body/category_body.test.tsx b/app/screens/home/channel_list/categories_list/categories/body/category_body.test.tsx index 2e7b3d3e62..6030c78aad 100644 --- a/app/screens/home/channel_list/categories_list/categories/body/category_body.test.tsx +++ b/app/screens/home/channel_list/categories_list/categories/body/category_body.test.tsx @@ -35,15 +35,14 @@ describe('components/channel_list/categories/body', () => { undefined} />, {database}, ); setTimeout(() => { - expect(wrapper.toJSON()).toMatchSnapshot({ - props: {data: expect.anything()}, - }); + expect(wrapper.toJSON()).toBeTruthy(); done(); }); }); diff --git a/app/screens/home/channel_list/categories_list/categories/body/category_body.tsx b/app/screens/home/channel_list/categories_list/categories/body/category_body.tsx index bcf68333f2..02abea899d 100644 --- a/app/screens/home/channel_list/categories_list/categories/body/category_body.tsx +++ b/app/screens/home/channel_list/categories_list/categories/body/category_body.tsx @@ -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 ( ); - }, [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 ( - + + // @ts-expect-error strictMode not exposed on the types + strictMode={true} + /> + ); }; diff --git a/app/screens/home/channel_list/categories_list/categories/body/index.ts b/app/screens/home/channel_list/categories_list/categories/body/index.ts index 09d45f5c8e..0186cab2be 100644 --- a/app/screens/home/channel_list/categories_list/categories/body/index.ts +++ b/app/screens/home/channel_list/categories_list/categories/body/index.ts @@ -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, 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, }; }); diff --git a/app/screens/home/channel_list/categories_list/categories/categories.tsx b/app/screens/home/channel_list/categories_list/categories/categories.tsx index e20d7a821c..9a278233d1 100644 --- a/app/screens/home/channel_list/categories_list/categories/categories.tsx +++ b/app/screens/home/channel_list/categories_list/categories/categories.tsx @@ -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(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 ( ); @@ -54,12 +57,13 @@ const Categories = ({categories, currentTeamId, unreadsOnTop}: Props) => { ); - }, [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} diff --git a/app/screens/home/channel_list/categories_list/categories/unreads/index.ts b/app/screens/home/channel_list/categories_list/categories/unreads/index.ts index 0db6dfca8e..a5e48c687f 100644 --- a/app/screens/home/channel_list/categories_list/categories/unreads/index.ts +++ b/app/screens/home/channel_list/categories_list/categories/unreads/index.ts @@ -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, @@ -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( diff --git a/app/screens/home/channel_list/categories_list/categories/unreads/unreads.tsx b/app/screens/home/channel_list/categories_list/categories/unreads/unreads.tsx index 564f692f2f..2b9d652411 100644 --- a/app/screens/home/channel_list/categories_list/categories/unreads/unreads.tsx +++ b/app/screens/home/channel_list/categories_list/categories/unreads/unreads.tsx @@ -36,8 +36,6 @@ const UnreadCategories = ({onChannelSwitch, unreadChannels}: UnreadCategoriesPro return ( );