diff --git a/app/actions/local/category.ts b/app/actions/local/category.ts index 7d81af71c..ab06a5a24 100644 --- a/app/actions/local/category.ts +++ b/app/actions/local/category.ts @@ -84,3 +84,28 @@ export const storeCategories = async (serverUrl: string, categories: CategoryWit return {models: flattenedModels}; }; + +export const toggleCollapseCategory = async (serverUrl: string, categoryId: string) => { + const database = DatabaseManager.serverDatabases[serverUrl].database; + if (!database) { + return {error: `${serverUrl} database not found`}; + } + + try { + const category = await queryCategoryById(database, categoryId); + + if (category) { + await database.write(async () => { + await category.update(() => { + category.collapsed = !category.collapsed; + }); + }); + } + + return {category}; + } catch (error) { + // eslint-disable-next-line no-console + console.log('FAILED TO COLLAPSE CATEGORY', categoryId, error); + return {error}; + } +}; diff --git a/app/components/channel_list/categories/body/__snapshots__/category_body.test.tsx.snap b/app/components/channel_list/categories/body/__snapshots__/category_body.test.tsx.snap index 83fa3f29e..2391f5257 100644 --- a/app/components/channel_list/categories/body/__snapshots__/category_body.test.tsx.snap +++ b/app/components/channel_list/categories/body/__snapshots__/category_body.test.tsx.snap @@ -5,95 +5,114 @@ Object { "children": Array [ - - + - + > + + + + Channel + - - Channel - - - + + , ], @@ -101,7 +120,6 @@ Object { "data": Anything, "getItem": [Function], "getItemCount": [Function], - "getItemLayout": [Function], "initialNumToRender": 20, "invertStickyHeaders": undefined, "keyExtractor": [Function], diff --git a/app/components/channel_list/categories/body/category_body.tsx b/app/components/channel_list/categories/body/category_body.tsx index ea8405d3e..c1dadddb2 100644 --- a/app/components/channel_list/categories/body/category_body.tsx +++ b/app/components/channel_list/categories/body/category_body.tsx @@ -6,22 +6,23 @@ import {FlatList} from 'react-native'; import ChannelListItem from './channel'; +import type CategoryModel from '@typings/database/models/servers/category'; + type Props = { currentChannelId: string; sortedIds: string[]; + category: CategoryModel; }; const extractKey = (item: any) => item; -const itemLayout = (d: any, index: number) => ( - {length: 40, offset: 40 * index, index} -); -const CategoryBody = ({currentChannelId, sortedIds}: Props) => { +const CategoryBody = ({currentChannelId, sortedIds, category}: Props) => { const ChannelItem = useCallback(({item}: {item: string}) => { return ( ); }, [currentChannelId]); @@ -35,7 +36,6 @@ const CategoryBody = ({currentChannelId, sortedIds}: Props) => { initialNumToRender={20} windowSize={15} updateCellsBatchingPeriod={10} - getItemLayout={itemLayout} /> ); }; diff --git a/app/components/channel_list/categories/body/channel/__snapshots__/channel_list_item.test.tsx.snap b/app/components/channel_list/categories/body/channel/__snapshots__/channel_list_item.test.tsx.snap index 4cda3d4c9..1742dfae0 100644 --- a/app/components/channel_list/categories/body/channel/__snapshots__/channel_list_item.test.tsx.snap +++ b/app/components/channel_list/categories/body/channel/__snapshots__/channel_list_item.test.tsx.snap @@ -1,47 +1,49 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`components/channel_list/categories/body/channel/item should match snapshot 1`] = ` - - + - - 1 - + + 1 + + + + Hello! + - - Hello! - - - + + `; diff --git a/app/components/channel_list/categories/body/channel/channel_list_item.test.tsx b/app/components/channel_list/categories/body/channel/channel_list_item.test.tsx index 0ac86245a..4458155fc 100644 --- a/app/components/channel_list/categories/body/channel/channel_list_item.test.tsx +++ b/app/components/channel_list/categories/body/channel/channel_list_item.test.tsx @@ -34,6 +34,7 @@ describe('components/channel_list/categories/body/channel/item', () => { isActive={false} isOwnDirectMessage={false} myChannel={myChannel} + collapsed={false} />, ); diff --git a/app/components/channel_list/categories/body/channel/channel_list_item.tsx b/app/components/channel_list/categories/body/channel/channel_list_item.tsx index 0259377aa..13a4ec1bd 100644 --- a/app/components/channel_list/categories/body/channel/channel_list_item.tsx +++ b/app/components/channel_list/categories/body/channel/channel_list_item.tsx @@ -1,10 +1,11 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useMemo} from 'react'; +import React, {useEffect, useMemo} from 'react'; import {useIntl} from 'react-intl'; import {StyleSheet, Text, View} from 'react-native'; import {TouchableOpacity} from 'react-native-gesture-handler'; +import Animated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import {switchToChannelById} from '@actions/remote/channel'; import ChannelIcon from '@components/channel_icon'; @@ -50,9 +51,10 @@ type Props = { isActive: boolean; isOwnDirectMessage: boolean; myChannel: MyChannelModel; + collapsed: boolean; } -const ChannelListItem = ({channel, isActive, isOwnDirectMessage, myChannel}: Props) => { +const ChannelListItem = ({channel, isActive, isOwnDirectMessage, myChannel, collapsed}: Props) => { const {formatMessage} = useIntl(); const theme = useTheme(); const styles = getStyleSheet(theme); @@ -61,6 +63,19 @@ const ChannelListItem = ({channel, isActive, isOwnDirectMessage, myChannel}: Pro // Make it brighter if it's highlighted, or has unreads const bright = myChannel.isUnread || myChannel.mentionsCount > 0; + const sharedValue = useSharedValue(collapsed && !bright); + + useEffect(() => { + sharedValue.value = collapsed && !bright; + }, [collapsed, bright]); + + const animatedStyle = useAnimatedStyle(() => { + return { + height: withTiming(sharedValue.value ? 0 : 40, {duration: 500}), + opacity: withTiming(sharedValue.value ? 0 : 1, {duration: 500, easing: Easing.inOut(Easing.exp)}), + }; + }); + const switchChannels = () => switchToChannelById(serverUrl, myChannel.id); const membersCount = useMemo(() => { if (channel.type === General.GM_CHANNEL) { @@ -86,27 +101,29 @@ const ChannelListItem = ({channel, isActive, isOwnDirectMessage, myChannel}: Pro } return ( - - - 0} - membersCount={membersCount} - name={channel.name} - shared={channel.shared} - size={24} - type={channel.type} - /> - - {displayName} - + + + + 0} + membersCount={membersCount} + name={channel.name} + shared={channel.shared} + size={24} + type={channel.type} + /> + + {displayName} + - - + + + ); }; diff --git a/app/components/channel_list/categories/body/index.ts b/app/components/channel_list/categories/body/index.ts index d720c35cd..db4c0ab32 100644 --- a/app/components/channel_list/categories/body/index.ts +++ b/app/components/channel_list/categories/body/index.ts @@ -82,12 +82,14 @@ const getSortedIds = (database: Database, category: CategoryModel, locale: strin }; const enhance = withObservables(['category'], ({category, locale, database}: {category: CategoryModel; locale: string} & WithDatabaseArgs) => { - const sortedIds = category.observe().pipe( + const observedCategory = category.observe(); + const sortedIds = observedCategory.pipe( switchMap((c) => getSortedIds(database, c, locale)), ); return { sortedIds, + category: observedCategory, }; }); diff --git a/app/components/channel_list/categories/header/__snapshots__/header.test.tsx.snap b/app/components/channel_list/categories/header/__snapshots__/header.test.tsx.snap index 12a68fea7..78edcb292 100644 --- a/app/components/channel_list/categories/header/__snapshots__/header.test.tsx.snap +++ b/app/components/channel_list/categories/header/__snapshots__/header.test.tsx.snap @@ -2,26 +2,76 @@ exports[`components/channel_list/categories/header should match snapshot 1`] = ` - - TEST CATEGORY - + + + TEST CATEGORY + + `; diff --git a/app/components/channel_list/categories/header/header.tsx b/app/components/channel_list/categories/header/header.tsx index 89adf8ffe..3c376e55d 100644 --- a/app/components/channel_list/categories/header/header.tsx +++ b/app/components/channel_list/categories/header/header.tsx @@ -1,9 +1,13 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React from 'react'; -import {Text, View} from 'react-native'; +import React, {useCallback, useEffect} from 'react'; +import {Text, TouchableOpacity, View} from 'react-native'; +import Animated, {Easing, useAnimatedStyle, useDerivedValue, useSharedValue, withTiming} from 'react-native-reanimated'; +import {toggleCollapseCategory} from '@actions/local/category'; +import CompassIcon from '@app/components/compass_icon'; +import {useServerUrl} from '@app/context/server'; import {useTheme} from '@context/theme'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; @@ -15,11 +19,20 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ paddingVertical: 8, marginTop: 12, paddingLeft: 2, + flexDirection: 'row', + alignItems: 'flex-start', }, heading: { color: changeOpacity(theme.sidebarText, 0.64), ...typography('Heading', 75), }, + chevron: { + marginTop: -2, + marginRight: 2, + color: changeOpacity(theme.sidebarText, 0.64), + width: 20, + height: 20, + }, })); type Props = { @@ -27,9 +40,33 @@ type Props = { hasChannels: boolean; } +const AnimatedCompassIcon = Animated.createAnimatedComponent(CompassIcon); + const CategoryHeader = ({category, hasChannels}: Props) => { const theme = useTheme(); const styles = getStyleSheet(theme); + const serverUrl = useServerUrl(); + const collapsed = useSharedValue(category.collapsed); + + // Action + const toggleCollapse = useCallback(() => toggleCollapseCategory(serverUrl, category.id), [category.id, serverUrl]); + + const rotate = useDerivedValue(() => { + return withTiming(collapsed.value ? -90 : 0, { + duration: 100, + easing: Easing.linear, + }); + }); + + const animatedStyle = useAnimatedStyle(() => { + return { + transform: [{rotate: `${rotate.value}deg`}], + }; + }); + + useEffect(() => { + collapsed.value = category.collapsed; + }, [category.collapsed]); // Hide favs if empty if (!hasChannels && category.type === 'favorites') { @@ -37,11 +74,18 @@ const CategoryHeader = ({category, hasChannels}: Props) => { } return ( - - - {category.displayName.toUpperCase()} - - + + + + + {category.displayName.toUpperCase()} + + + ); }; diff --git a/package-lock.json b/package-lock.json index aa9964001..597ddf270 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "mattermost-mobile", "version": "2.0.0", "hasInstallScript": true, "license": "Apache 2.0",