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",