From 066775ca825d3490a19a04b3907850a124804293 Mon Sep 17 00:00:00 2001 From: Shaz Amjad Date: Tue, 1 Mar 2022 08:25:00 +1100 Subject: [PATCH] Gekidou - Sidebar UI components (#5955) * Squash, rebase, test fixes * PR Feedback addressed * fix snapshot tests with SafeAreaProvider * Addresses feedback * Adds Category & Channel error * Tests fixed * MyChannel count * finalize DM and GM avatars and icon as well as other small fixes Co-authored-by: Elias Nahum --- app/actions/remote/channel.ts | 16 +- .../channel_icon/dm_avatar/dm_avatar.tsx | 37 ++ .../channel_icon/dm_avatar/index.ts | 45 ++ app/components/channel_icon/index.tsx | 13 +- .../__snapshots__/index.test.tsx.snap | 299 +++++++++++- .../__snapshots__/index.test.tsx.snap | 430 ------------------ .../__snapshots__/category_body.test.tsx.snap | 123 +++++ .../body/__snapshots__/index.test.tsx.snap | 127 ------ .../categories/body/category_body.test.tsx | 64 +++ .../categories/body/category_body.tsx | 47 ++ .../channel_list_item.test.tsx.snap | 114 +++++ .../channel/__snapshots__/index.test.tsx.snap | 43 -- .../body/channel/channel_list_item.test.tsx | 41 ++ .../body/channel/channel_list_item.tsx | 106 +++++ .../categories/body/channel/index.test.tsx | 19 - .../categories/body/channel/index.ts | 54 +++ .../categories/body/channel/index.tsx | 75 --- .../categories/body/index.test.tsx | 21 - .../channel_list/categories/body/index.ts | 17 + .../channel_list/categories/body/index.tsx | 32 -- .../categories/categories.test.tsx | 27 ++ .../channel_list/categories/categories.tsx | 51 +++ .../channel_list/categories/error.tsx | 36 ++ ...dex.test.tsx.snap => header.test.tsx.snap} | 5 +- .../categories/header/header.test.tsx | 38 ++ .../header/{index.tsx => header.tsx} | 15 +- .../categories/header/index.test.tsx | 16 - .../channel_list/categories/header/index.ts | 15 + .../channel_list/categories/index.test.tsx | 38 -- .../channel_list/categories/index.ts | 31 ++ .../channel_list/categories/index.tsx | 40 -- app/components/channel_list/index.test.tsx | 36 +- app/components/channel_list/index.tsx | 36 +- app/components/channel_list/search/index.tsx | 2 +- app/components/channel_list/tempo-types.d.ts | 16 - .../channel_list/threads/index.test.tsx | 24 +- app/components/channel_list/threads/index.tsx | 41 +- app/components/profile_picture/status.tsx | 3 +- .../home/channel_list/channel_list.tsx | 4 +- app/screens/home/channel_list/index.ts | 6 +- assets/base/i18n/en.json | 4 +- package.json | 1 + test/test_helper.js | 83 +++- 43 files changed, 1335 insertions(+), 956 deletions(-) create mode 100644 app/components/channel_icon/dm_avatar/dm_avatar.tsx create mode 100644 app/components/channel_icon/dm_avatar/index.ts delete mode 100644 app/components/channel_list/categories/__snapshots__/index.test.tsx.snap create mode 100644 app/components/channel_list/categories/body/__snapshots__/category_body.test.tsx.snap delete mode 100644 app/components/channel_list/categories/body/__snapshots__/index.test.tsx.snap create mode 100644 app/components/channel_list/categories/body/category_body.test.tsx create mode 100644 app/components/channel_list/categories/body/category_body.tsx create mode 100644 app/components/channel_list/categories/body/channel/__snapshots__/channel_list_item.test.tsx.snap delete mode 100644 app/components/channel_list/categories/body/channel/__snapshots__/index.test.tsx.snap create mode 100644 app/components/channel_list/categories/body/channel/channel_list_item.test.tsx create mode 100644 app/components/channel_list/categories/body/channel/channel_list_item.tsx delete mode 100644 app/components/channel_list/categories/body/channel/index.test.tsx create mode 100644 app/components/channel_list/categories/body/channel/index.ts delete mode 100644 app/components/channel_list/categories/body/channel/index.tsx delete mode 100644 app/components/channel_list/categories/body/index.test.tsx create mode 100644 app/components/channel_list/categories/body/index.ts delete mode 100644 app/components/channel_list/categories/body/index.tsx create mode 100644 app/components/channel_list/categories/categories.test.tsx create mode 100644 app/components/channel_list/categories/categories.tsx create mode 100644 app/components/channel_list/categories/error.tsx rename app/components/channel_list/categories/header/__snapshots__/{index.test.tsx.snap => header.test.tsx.snap} (75%) create mode 100644 app/components/channel_list/categories/header/header.test.tsx rename app/components/channel_list/categories/header/{index.tsx => header.tsx} (69%) delete mode 100644 app/components/channel_list/categories/header/index.test.tsx create mode 100644 app/components/channel_list/categories/header/index.ts delete mode 100644 app/components/channel_list/categories/index.test.tsx create mode 100644 app/components/channel_list/categories/index.ts delete mode 100644 app/components/channel_list/categories/index.tsx delete mode 100644 app/components/channel_list/tempo-types.d.ts diff --git a/app/actions/remote/channel.ts b/app/actions/remote/channel.ts index 290001f17..accc15c8e 100644 --- a/app/actions/remote/channel.ts +++ b/app/actions/remote/channel.ts @@ -14,6 +14,7 @@ import NetworkManager from '@init/network_manager'; import {prepareMyChannelsForTeam, queryChannelById, queryChannelByName, queryMyChannel} from '@queries/servers/channel'; import {queryCommonSystemValues, queryCurrentTeamId, queryCurrentUserId} from '@queries/servers/system'; import {prepareMyTeams, queryNthLastChannelFromTeam, queryMyTeamById, queryTeamById, queryTeamByName} from '@queries/servers/team'; +import {queryCurrentUser} from '@queries/servers/user'; import {getDirectChannelName} from '@utils/channel'; import {PERMALINK_GENERIC_TEAM_NAME_REDIRECT} from '@utils/url'; import {displayGroupMessageName, displayUsername} from '@utils/user'; @@ -267,9 +268,9 @@ export const fetchMyChannel = async (serverUrl: string, teamId: string, channelI } }; -export const fetchMissingSidebarInfo = async (serverUrl: string, directChannels: Channel[], locale?: string, teammateDisplayNameSetting?: string, exludeUserId?: string, fetchOnly = false) => { +export const fetchMissingSidebarInfo = async (serverUrl: string, directChannels: Channel[], locale?: string, teammateDisplayNameSetting?: string, currentUserId?: string, fetchOnly = false) => { const channelIds = directChannels.sort((a, b) => b.last_post_at - a.last_post_at).map((dc) => dc.id); - const result = await fetchProfilesPerChannels(serverUrl, channelIds, exludeUserId, false); + const result = await fetchProfilesPerChannels(serverUrl, channelIds, currentUserId, false); if (result.error) { return {error: result.error}; } @@ -280,7 +281,7 @@ export const fetchMissingSidebarInfo = async (serverUrl: string, directChannels: result.data.forEach((data) => { if (data.users) { if (data.users.length > 1) { - displayNameByChannel[data.channelId] = displayGroupMessageName(data.users, locale, teammateDisplayNameSetting, exludeUserId); + displayNameByChannel[data.channelId] = displayGroupMessageName(data.users, locale, teammateDisplayNameSetting, currentUserId); } else { displayNameByChannel[data.channelId] = displayUsername(data.users[0], locale, teammateDisplayNameSetting); } @@ -295,6 +296,15 @@ export const fetchMissingSidebarInfo = async (serverUrl: string, directChannels: } }); + if (currentUserId) { + const ownDirectChannel = directChannels.find((dm) => dm.name === getDirectChannelName(currentUserId, currentUserId)); + const database = DatabaseManager.serverDatabases[serverUrl]?.database; + if (ownDirectChannel && database) { + const currentUser = await queryCurrentUser(database); + ownDirectChannel.display_name = displayUsername(currentUser, locale, teammateDisplayNameSetting); + } + } + if (!fetchOnly) { const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; if (operator) { diff --git a/app/components/channel_icon/dm_avatar/dm_avatar.tsx b/app/components/channel_icon/dm_avatar/dm_avatar.tsx new file mode 100644 index 000000000..290962d25 --- /dev/null +++ b/app/components/channel_icon/dm_avatar/dm_avatar.tsx @@ -0,0 +1,37 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import ProfilePicture from '@components/profile_picture'; +import {useTheme} from '@context/theme'; +import {makeStyleSheetFromTheme} from '@utils/theme'; + +import type UserModel from '@typings/database/models/servers/user'; + +type Props = { + author?: UserModel; +} + +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ + status: { + backgroundColor: theme.sidebarBg, + borderWidth: 0, + }, +})); + +const DmAvatar = ({author}: Props) => { + const theme = useTheme(); + const style = getStyleSheet(theme); + return ( + + ); +}; + +export default DmAvatar; diff --git a/app/components/channel_icon/dm_avatar/index.ts b/app/components/channel_icon/dm_avatar/index.ts new file mode 100644 index 000000000..0f2359f44 --- /dev/null +++ b/app/components/channel_icon/dm_avatar/index.ts @@ -0,0 +1,45 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Q} from '@nozbe/watermelondb'; +import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; +import withObservables from '@nozbe/with-observables'; +import {of as of$} from 'rxjs'; +import {switchMap} from 'rxjs/operators'; + +import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database'; +import {getUserIdFromChannelName} from '@utils/user'; + +import DmAvatar from './dm_avatar'; + +import type {WithDatabaseArgs} from '@typings/database/database'; +import type SystemModel from '@typings/database/models/servers/system'; +import type UserModel from '@typings/database/models/servers/user'; + +const {SERVER: {USER, SYSTEM}} = MM_TABLES; +const {CURRENT_USER_ID} = SYSTEM_IDENTIFIERS; + +const enhance = withObservables(['channelName'], ({channelName, database}: {channelName: string} & WithDatabaseArgs) => { + const currentUserId = database.get(SYSTEM).findAndObserve(CURRENT_USER_ID).pipe( + switchMap(({value}) => of$(value)), + ); + + const authorId = currentUserId.pipe( + switchMap((userId) => of$(getUserIdFromChannelName(userId, channelName))), + ); + + const author = authorId.pipe( + switchMap((id) => { + return database.get(USER).query(Q.where('id', id)).observe().pipe( + // eslint-disable-next-line max-nested-callbacks + switchMap((users) => (users[0] ? of$(users[0]) : of$(undefined))), + ); + }), + ); + + return { + author, + }; +}); + +export default withDatabase(enhance(DmAvatar)); diff --git a/app/components/channel_icon/index.tsx b/app/components/channel_icon/index.tsx index 6477e3624..fcd535333 100644 --- a/app/components/channel_icon/index.tsx +++ b/app/components/channel_icon/index.tsx @@ -8,6 +8,9 @@ import CompassIcon from '@components/compass_icon'; import General from '@constants/general'; import {useTheme} from '@context/theme'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {typography} from '@utils/typography'; + +import DmAvatar from './dm_avatar'; type ChannelIconProps = { hasDraft?: boolean; @@ -16,6 +19,7 @@ type ChannelIconProps = { isInfo?: boolean; isUnread?: boolean; membersCount?: number; + name: string; shared: boolean; size?: number; style?: StyleProp; @@ -58,8 +62,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { }, group: { color: theme.sidebarText, - fontSize: 10, - fontFamily: 'OpenSans-SemiBold', + ...typography('Body', 75, 'SemiBold'), }, groupActive: { color: theme.sidebarTextActiveColor, @@ -80,6 +83,7 @@ const ChannelIcon = ({ isInfo = false, isUnread = false, membersCount = 0, + name, shared, size = 12, style, @@ -159,10 +163,9 @@ const ChannelIcon = ({ ); } else if (type === General.GM_CHANNEL) { const fontSize = size - 12; - const boxSize = size - 4; icon = ( ); } else if (type === General.DM_CHANNEL) { - //todo: Implement ProfilePicture component + icon = (); } return ( diff --git a/app/components/channel_list/__snapshots__/index.test.tsx.snap b/app/components/channel_list/__snapshots__/index.test.tsx.snap index d0c7d8347..49acd1e40 100644 --- a/app/components/channel_list/__snapshots__/index.test.tsx.snap +++ b/app/components/channel_list/__snapshots__/index.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`components/channel_list should match snapshot 1`] = ` +exports[`components/channel_list should render channels error 1`] = ` - Couldn't load + Couldn't load Test Team! + + + There was a problem loading content for this team. + + + + + Retry + + + + + +`; + +exports[`components/channel_list should render team error 1`] = ` + + + + + + + + + + + Threads + + + + + + + + + + Couldn't load categories for - - - - - - - - Threads - - - - - - - - - MY FIRST CATEGORY - - - - - - - - Just a channel - - - - - - - - Highlighted!!! - - - - - - - - - ANOTHER CAT - - - - - - - - Just a channel - - - - - - - - Highlighted!!! - - - - - - - -`; 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 new file mode 100644 index 000000000..7221c7a67 --- /dev/null +++ b/app/components/channel_list/categories/body/__snapshots__/category_body.test.tsx.snap @@ -0,0 +1,123 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/channel_list/categories/body should match snapshot 1`] = ` +Object { + "children": Array [ + + + + + + + + + + Channel + + + + + + , + ], + "props": Object { + "data": Anything, + "getItem": [Function], + "getItemCount": [Function], + "invertStickyHeaders": undefined, + "keyExtractor": [Function], + "onContentSizeChange": [Function], + "onLayout": [Function], + "onMomentumScrollBegin": [Function], + "onMomentumScrollEnd": [Function], + "onScroll": [Function], + "onScrollBeginDrag": [Function], + "onScrollEndDrag": [Function], + "removeClippedSubviews": false, + "renderItem": [Function], + "scrollEventThrottle": 50, + "stickyHeaderIndices": Array [], + "style": undefined, + "viewabilityConfigCallbackPairs": Array [], + }, + "type": "RCTScrollView", +} +`; diff --git a/app/components/channel_list/categories/body/__snapshots__/index.test.tsx.snap b/app/components/channel_list/categories/body/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 2014360a3..000000000 --- a/app/components/channel_list/categories/body/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,127 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Category Body Component should match snapshot 1`] = ` - - - - - - - Just a channel - - - - - - - - Highlighted!!! - - - - - -`; diff --git a/app/components/channel_list/categories/body/category_body.test.tsx b/app/components/channel_list/categories/body/category_body.test.tsx new file mode 100644 index 000000000..7a0027400 --- /dev/null +++ b/app/components/channel_list/categories/body/category_body.test.tsx @@ -0,0 +1,64 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Database, Q} from '@nozbe/watermelondb'; +import React from 'react'; + +import {MM_TABLES} from '@constants/database'; +import {renderWithEverything} from '@test/intl-test-helper'; +import TestHelper from '@test/test_helper'; + +import CategoryBody from './category_body'; + +import type CategoryModel from '@typings/database/models/servers/category'; +import type CategoryChannelModel from '@typings/database/models/servers/category_channel'; +import type ChannelModel from '@typings/database/models/servers/channel'; +import type MyChannelModel from '@typings/database/models/servers/my_channel'; + +const {SERVER: {CATEGORY, CATEGORY_CHANNEL, CHANNEL, MY_CHANNEL}} = MM_TABLES; + +describe('components/channel_list/categories/body', () => { + let database: Database; + let category: CategoryModel; + let categoryChannels: CategoryChannelModel[]; + let myChannels: MyChannelModel[]; + let channels: ChannelModel[]; + + beforeAll(async () => { + const server = await TestHelper.setupServerDatabase(); + database = server.database; + + const categories = await database.get(CATEGORY).query( + Q.take(1), + ).fetch(); + + category = categories[0]; + + categoryChannels = await database.get(CATEGORY_CHANNEL).query( + Q.where('category_id', category.id), + ).fetch(); + + const channelIds = await database.get(CHANNEL).query( + Q.on(CATEGORY_CHANNEL, 'category_id', category.id), + ).fetchIds(); + + myChannels = await database.get(MY_CHANNEL).query( + Q.where('id', Q.oneOf(channelIds)), + ).fetch(); + }); + + it('should match snapshot', () => { + const wrapper = renderWithEverything( + , + {database}, + ); + expect(wrapper.toJSON()).toMatchSnapshot({ + props: {data: expect.anything()}, + }); + }); +}); diff --git a/app/components/channel_list/categories/body/category_body.tsx b/app/components/channel_list/categories/body/category_body.tsx new file mode 100644 index 000000000..806324c4c --- /dev/null +++ b/app/components/channel_list/categories/body/category_body.tsx @@ -0,0 +1,47 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useMemo} from 'react'; +import {FlatList} from 'react-native'; + +import ChannelListItem from './channel'; + +import type CategoryModel from '@typings/database/models/servers/category'; +import type CategoryChannelModel from '@typings/database/models/servers/category_channel'; +import type ChannelModel from '@typings/database/models/servers/channel'; +import type MyChannelModel from '@typings/database/models/servers/my_channel'; + +type Props = { + category: CategoryModel; + channels: ChannelModel[]; + myChannels: MyChannelModel[]; + categoryChannels: CategoryChannelModel[]; +}; + +const ChannelItem = ({item}: {item: string}) => { + return ( + + ); +}; + +const CategoryBody = ({category, categoryChannels, channels, myChannels}: Props) => { + const data: string[] = useMemo(() => { + switch (category.sorting) { + case 'alpha': + return channels.map((c) => c.id); + case 'manual': + return categoryChannels.map((cc) => cc.channelId); + default: + return myChannels.map((m) => m.id); + } + }, [category.sorting, categoryChannels, channels, myChannels]); + + return ( + + ); +}; + +export default CategoryBody; 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 new file mode 100644 index 000000000..4cda3d4c9 --- /dev/null +++ b/app/components/channel_list/categories/body/channel/__snapshots__/channel_list_item.test.tsx.snap @@ -0,0 +1,114 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/channel_list/categories/body/channel/item should match snapshot 1`] = ` + + + + + + + 1 + + + + + Hello! + + + + +`; diff --git a/app/components/channel_list/categories/body/channel/__snapshots__/index.test.tsx.snap b/app/components/channel_list/categories/body/channel/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 55ea35f10..000000000 --- a/app/components/channel_list/categories/body/channel/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,43 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Category Channel List Item should match snapshot 1`] = ` - - - - Channel Name - - -`; 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 new file mode 100644 index 000000000..1eeca24aa --- /dev/null +++ b/app/components/channel_list/categories/body/channel/channel_list_item.test.tsx @@ -0,0 +1,41 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Database, Q} from '@nozbe/watermelondb'; +import React from 'react'; + +import {MM_TABLES} from '@constants/database'; +import {renderWithIntlAndTheme} from '@test/intl-test-helper'; +import TestHelper from '@test/test_helper'; + +import ChannelListItem from './channel_list_item'; + +import type MyChannelModel from '@typings/database/models/servers/my_channel'; + +describe('components/channel_list/categories/body/channel/item', () => { + let database: Database; + let myChannel: MyChannelModel; + + beforeAll(async () => { + const server = await TestHelper.setupServerDatabase(); + database = server.database; + + const myChannels = await database.get(MM_TABLES.SERVER.MY_CHANNEL).query( + Q.take(1), + ).fetch(); + + myChannel = myChannels[0]; + }); + + it('should match snapshot', () => { + const wrapper = renderWithIntlAndTheme( + , + ); + + expect(wrapper.toJSON()).toMatchSnapshot(); + }); +}); 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 new file mode 100644 index 000000000..e8af22910 --- /dev/null +++ b/app/components/channel_list/categories/body/channel/channel_list_item.tsx @@ -0,0 +1,106 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useMemo} from 'react'; +import {useIntl} from 'react-intl'; +import {StyleSheet, Text, View} from 'react-native'; +import {TouchableOpacity} from 'react-native-gesture-handler'; + +import {switchToChannelById} from '@actions/remote/channel'; +import ChannelIcon from '@components/channel_icon'; +import {General} from '@constants'; +import {useServerUrl} from '@context/server'; +import {useTheme} from '@context/theme'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {typography} from '@utils/typography'; + +import type ChannelModel from '@typings/database/models/servers/channel'; +import type MyChannelModel from '@typings/database/models/servers/my_channel'; + +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ + container: { + flexDirection: 'row', + marginBottom: 8, + paddingLeft: 2, + paddingVertical: 4, + }, + icon: { + fontSize: 24, + lineHeight: 28, + color: changeOpacity(theme.sidebarText, 0.72), + }, + text: { + marginTop: -1, + color: changeOpacity(theme.sidebarText, 0.72), + paddingLeft: 12, + flex: 1, + }, + highlight: { + color: theme.sidebarText, + }, +})); + +const textStyle = StyleSheet.create({ + bright: typography('Body', 200, 'SemiBold'), + regular: typography('Body', 200, 'Regular'), +}); + +type Props = { + channel: Pick; + isOwnDirectMessage: boolean; + myChannel: MyChannelModel; +} + +const ChannelListItem = ({channel, isOwnDirectMessage, myChannel}: Props) => { + const {formatMessage} = useIntl(); + const theme = useTheme(); + const styles = getStyleSheet(theme); + const serverUrl = useServerUrl(); + + // Make it brighter if it's highlighted, or has unreads + const bright = myChannel.isUnread || myChannel.mentionsCount > 0; + + const switchChannels = () => switchToChannelById(serverUrl, myChannel.id); + const membersCount = useMemo(() => { + if (channel.type === General.GM_CHANNEL) { + return channel.displayName?.split(',').length; + } + + return 0; + }, [channel.type, channel.displayName]); + + const textStyles = useMemo(() => [ + bright ? textStyle.bright : textStyle.regular, + styles.text, + bright && styles.highlight, + ], [bright, styles]); + + let displayName = channel.displayName; + if (isOwnDirectMessage) { + displayName = formatMessage({id: 'channel_header.directchannel.you', defaultMessage: '{displayName} (you)'}, {displayName}); + } + + return ( + + + + + {displayName} + + + + + ); +}; + +export default ChannelListItem; diff --git a/app/components/channel_list/categories/body/channel/index.test.tsx b/app/components/channel_list/categories/body/channel/index.test.tsx deleted file mode 100644 index ccb818945..000000000 --- a/app/components/channel_list/categories/body/channel/index.test.tsx +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -import {renderWithIntlAndTheme} from '@test/intl-test-helper'; - -import ChannelItem from './index'; - -test('Category Channel List Item should match snapshot', () => { - const {toJSON} = renderWithIntlAndTheme( - , - ); - - expect(toJSON()).toMatchSnapshot(); -}); diff --git a/app/components/channel_list/categories/body/channel/index.ts b/app/components/channel_list/categories/body/channel/index.ts new file mode 100644 index 000000000..d0cdfc494 --- /dev/null +++ b/app/components/channel_list/categories/body/channel/index.ts @@ -0,0 +1,54 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; +import withObservables from '@nozbe/with-observables'; +import {combineLatest, of as of$} from 'rxjs'; +import {switchMap} from 'rxjs/operators'; + +import {General} from '@constants'; +import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database'; +import {getUserIdFromChannelName} from '@utils/user'; + +import ChannelListItem from './channel_list_item'; + +import type {WithDatabaseArgs} from '@typings/database/database'; +import type ChannelModel from '@typings/database/models/servers/channel'; +import type MyChannelModel from '@typings/database/models/servers/my_channel'; +import type SystemModel from '@typings/database/models/servers/system'; + +const {SERVER: {MY_CHANNEL, SYSTEM}} = MM_TABLES; +const {CURRENT_USER_ID} = SYSTEM_IDENTIFIERS; + +const enhance = withObservables(['channelId'], ({channelId, database}: {channelId: string} & WithDatabaseArgs) => { + const myChannel = database.get(MY_CHANNEL).findAndObserve(channelId); + const currentUserId = database.get(SYSTEM).findAndObserve(CURRENT_USER_ID).pipe( + switchMap(({value}) => of$(value)), + ); + + const channel = myChannel.pipe(switchMap((my) => my.channel.observe())); + const isOwnDirectMessage = combineLatest([currentUserId, channel]).pipe( + switchMap(([userId, ch]) => { + if (ch?.type === General.DM_CHANNEL) { + const teammateId = getUserIdFromChannelName(userId, ch.name); + return of$(userId === teammateId); + } + + return of$(false); + }), + ); + return { + isOwnDirectMessage, + myChannel, + channel: channel.pipe( + switchMap((c: ChannelModel) => of$({ + displayName: c.displayName, + name: c.name, + shared: c.shared, + type: c.type, + })), + ), + }; +}); + +export default withDatabase(enhance(ChannelListItem)); diff --git a/app/components/channel_list/categories/body/channel/index.tsx b/app/components/channel_list/categories/body/channel/index.tsx deleted file mode 100644 index ae3b09e8d..000000000 --- a/app/components/channel_list/categories/body/channel/index.tsx +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {StyleSheet, Text, View} from 'react-native'; - -import CompassIcon from '@components/compass_icon'; -import {useTheme} from '@context/theme'; -import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; -import {typography} from '@utils/typography'; - -const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ - container: { - paddingVertical: 4, - flexDirection: 'row', - }, - icon: { - fontSize: 24, - lineHeight: 28, - color: changeOpacity(theme.sidebarText, 0.72), - }, - text: { - marginTop: 1, - color: changeOpacity(theme.sidebarText, 0.72), - paddingLeft: 12, - }, - highlight: { - color: theme.sidebarText, - }, -})); - -const textStyle = StyleSheet.create({ - bright: typography('Body', 200, 'SemiBold'), - regular: typography('Body', 200, 'Regular'), -}); - -type Props = { - unreadCount?: number; - highlight?: boolean; - name: string; - icon: string; -} - -const ChannelListItem = (props: Props) => { - const theme = useTheme(); - const styles = getStyleSheet(theme); - - const {unreadCount, highlight, name, icon} = props; - - // Make it brighter if it's highlighted, or has unreads - const bright = highlight || (unreadCount && unreadCount > 0); - - return ( - - {icon && ( - - )} - - {name} - - - - ); -}; - -export default ChannelListItem; diff --git a/app/components/channel_list/categories/body/index.test.tsx b/app/components/channel_list/categories/body/index.test.tsx deleted file mode 100644 index 612428cc1..000000000 --- a/app/components/channel_list/categories/body/index.test.tsx +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -import {renderWithIntlAndTheme} from '@test/intl-test-helper'; - -import CategoryBody from './index'; - -const channels: TempoChannel[] = [ - {id: '1', name: 'Just a channel'}, - {id: '2', name: 'Highlighted!!!', highlight: true}, -]; - -test('Category Body Component should match snapshot', () => { - const {toJSON} = renderWithIntlAndTheme( - , - ); - - expect(toJSON()).toMatchSnapshot(); -}); diff --git a/app/components/channel_list/categories/body/index.ts b/app/components/channel_list/categories/body/index.ts new file mode 100644 index 000000000..241cb4a35 --- /dev/null +++ b/app/components/channel_list/categories/body/index.ts @@ -0,0 +1,17 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import withObservables from '@nozbe/with-observables'; + +import CategoryBody from './category_body'; + +import type CategoryModel from '@typings/database/models/servers/category'; + +const withCategory = withObservables(['category'], ({category}: {category: CategoryModel}) => ({ + category, + categoryChannels: category.categoryChannelsBySortOrder.observeWithColumns(['sort_order']), + myChannels: category.myChannels.observeWithColumns(['last_post_at']), + channels: category.channels.observeWithColumns(['display_name']), +})); + +export default withCategory(CategoryBody); diff --git a/app/components/channel_list/categories/body/index.tsx b/app/components/channel_list/categories/body/index.tsx deleted file mode 100644 index 3ac9d8b50..000000000 --- a/app/components/channel_list/categories/body/index.tsx +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {FlatList} from 'react-native'; - -import ChannelListItem from './channel'; - -const renderChannelItem = (data: { item: TempoChannel }) => { - return ( - - ); -}; - -type Props = { - channels: TempoChannel[]; -}; - -const CategoryBody = (props: Props) => { - return ( - - ); -}; - -export default CategoryBody; diff --git a/app/components/channel_list/categories/categories.test.tsx b/app/components/channel_list/categories/categories.test.tsx new file mode 100644 index 000000000..a9efa8699 --- /dev/null +++ b/app/components/channel_list/categories/categories.test.tsx @@ -0,0 +1,27 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import Database from '@nozbe/watermelondb/Database'; +import React from 'react'; + +import {renderWithEverything} from '@test/intl-test-helper'; +import TestHelper from '@test/test_helper'; + +import Categories from './'; + +describe('components/channel_list/categories', () => { + let database: Database; + beforeAll(async () => { + const server = await TestHelper.setupServerDatabase(); + database = server.database; + }); + + it('render without error', () => { + const wrapper = renderWithEverything( + , + {database}, + ); + + expect(wrapper.toJSON()).toBeTruthy(); + }); +}); diff --git a/app/components/channel_list/categories/categories.tsx b/app/components/channel_list/categories/categories.tsx new file mode 100644 index 000000000..8ab4e14e7 --- /dev/null +++ b/app/components/channel_list/categories/categories.tsx @@ -0,0 +1,51 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {FlatList, StyleSheet} from 'react-native'; + +import CategoryBody from './body'; +import LoadCategoriesError from './error'; +import CategoryHeader from './header'; + +import type {CategoryModel} from '@database/models/server'; + +type Props = { + categories: CategoryModel[]; +} + +const styles = StyleSheet.create({ + flex: { + flex: 1, + }, +}); + +const renderCategory = (data: {item: CategoryModel}) => { + return ( + <> + + + + ); +}; + +const Categories = (props: Props) => { + if (!props.categories.length) { + return ; + } + + // Sort Categories + props.categories.sort((a, b) => a.sortOrder - b.sortOrder); + + return ( + + ); +}; + +export default Categories; diff --git a/app/components/channel_list/categories/error.tsx b/app/components/channel_list/categories/error.tsx new file mode 100644 index 000000000..1866d27c8 --- /dev/null +++ b/app/components/channel_list/categories/error.tsx @@ -0,0 +1,36 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useCallback, useState} from 'react'; +import {useIntl} from 'react-intl'; + +import {retryInitialTeamAndChannel} from '@actions/remote/retry'; +import LoadingError from '@components/channel_list/loading_error'; +import {useServerDisplayName, useServerUrl} from '@context/server'; + +const LoadCategoriesError = () => { + const {formatMessage} = useIntl(); + const serverUrl = useServerUrl(); + const serverName = useServerDisplayName(); + const [loading, setLoading] = useState(false); + + const onRetryTeams = useCallback(async () => { + setLoading(true); + const {error} = await retryInitialTeamAndChannel(serverUrl); + + if (error) { + setLoading(false); + } + }, []); + + return ( + + ); +}; + +export default LoadCategoriesError; diff --git a/app/components/channel_list/categories/header/__snapshots__/index.test.tsx.snap b/app/components/channel_list/categories/header/__snapshots__/header.test.tsx.snap similarity index 75% rename from app/components/channel_list/categories/header/__snapshots__/index.test.tsx.snap rename to app/components/channel_list/categories/header/__snapshots__/header.test.tsx.snap index 2baacfda8..12a68fea7 100644 --- a/app/components/channel_list/categories/header/__snapshots__/index.test.tsx.snap +++ b/app/components/channel_list/categories/header/__snapshots__/header.test.tsx.snap @@ -1,10 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Category Header Component should match snapshot 1`] = ` +exports[`components/channel_list/categories/header should match snapshot 1`] = ` - CAT HEADING + TEST CATEGORY `; diff --git a/app/components/channel_list/categories/header/header.test.tsx b/app/components/channel_list/categories/header/header.test.tsx new file mode 100644 index 000000000..bb0dc5e89 --- /dev/null +++ b/app/components/channel_list/categories/header/header.test.tsx @@ -0,0 +1,38 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Database, Q} from '@nozbe/watermelondb'; +import React from 'react'; + +import {MM_TABLES} from '@constants/database'; +import {renderWithIntlAndTheme} from '@test/intl-test-helper'; +import TestHelper from '@test/test_helper'; + +import CategoryHeader from './header'; + +import type CategoryModel from '@typings/database/models/servers/category'; + +describe('components/channel_list/categories/header', () => { + let database: Database; + let category: CategoryModel; + beforeAll(async () => { + const server = await TestHelper.setupServerDatabase(); + + database = server.database; + const categories = await database.get(MM_TABLES.SERVER.CATEGORY).query( + Q.take(1), + ).fetch(); + + category = categories[0]; + }); + + it('should match snapshot', () => { + const wrapper = renderWithIntlAndTheme( + , + ); + expect(wrapper.toJSON()).toMatchSnapshot(); + }); +}); diff --git a/app/components/channel_list/categories/header/index.tsx b/app/components/channel_list/categories/header/header.tsx similarity index 69% rename from app/components/channel_list/categories/header/index.tsx rename to app/components/channel_list/categories/header/header.tsx index c2741dafa..89adf8ffe 100644 --- a/app/components/channel_list/categories/header/index.tsx +++ b/app/components/channel_list/categories/header/header.tsx @@ -8,10 +8,13 @@ import {useTheme} from '@context/theme'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; +import type CategoryModel from '@typings/database/models/servers/category'; + const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ container: { paddingVertical: 8, marginTop: 12, + paddingLeft: 2, }, heading: { color: changeOpacity(theme.sidebarText, 0.64), @@ -20,17 +23,23 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ })); type Props = { - heading: string; + category: CategoryModel; + hasChannels: boolean; } -const CategoryHeader = (props: Props) => { +const CategoryHeader = ({category, hasChannels}: Props) => { const theme = useTheme(); const styles = getStyleSheet(theme); + // Hide favs if empty + if (!hasChannels && category.type === 'favorites') { + return (null); + } + return ( - {props.heading.toUpperCase()} + {category.displayName.toUpperCase()} ); diff --git a/app/components/channel_list/categories/header/index.test.tsx b/app/components/channel_list/categories/header/index.test.tsx deleted file mode 100644 index 231e446f9..000000000 --- a/app/components/channel_list/categories/header/index.test.tsx +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -import {renderWithIntlAndTheme} from '@test/intl-test-helper'; - -import Header from './index'; - -test('Category Header Component should match snapshot', () => { - const {toJSON} = renderWithIntlAndTheme( -
, - ); - - expect(toJSON()).toMatchSnapshot(); -}); diff --git a/app/components/channel_list/categories/header/index.ts b/app/components/channel_list/categories/header/index.ts new file mode 100644 index 000000000..c543400d9 --- /dev/null +++ b/app/components/channel_list/categories/header/index.ts @@ -0,0 +1,15 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import withObservables from '@nozbe/with-observables'; + +import CategoryHeader from './header'; + +import type CategoryModel from '@typings/database/models/servers/category'; + +const enhanced = withObservables(['category'], ({category}: {category: CategoryModel}) => ({ + category, + hasChannels: category.hasChannels, +})); + +export default enhanced(CategoryHeader); diff --git a/app/components/channel_list/categories/index.test.tsx b/app/components/channel_list/categories/index.test.tsx deleted file mode 100644 index b5804400c..000000000 --- a/app/components/channel_list/categories/index.test.tsx +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -import {renderWithEverything} from '@test/intl-test-helper'; -import TestHelper from '@test/test_helper'; - -import Category from './index'; - -import type Database from '@nozbe/watermelondb/Database'; - -const channels: TempoChannel[] = [ - {id: '1', name: 'Just a channel'}, - {id: '2', name: 'Highlighted!!!', highlight: true}, -]; - -const categories: TempoCategory[] = [ - {id: '1', title: 'My first Category', channels}, - {id: '2', title: 'Another cat', channels}, -]; - -describe('Category List Component ', () => { - let database: Database | undefined; - beforeAll(async () => { - const server = await TestHelper.setupServerDatabase(); - database = server.database; - }); - - test('should match snapshot', () => { - const {toJSON} = renderWithEverything( - , - {database}, - ); - - expect(toJSON()).toMatchSnapshot(); - }); -}); diff --git a/app/components/channel_list/categories/index.ts b/app/components/channel_list/categories/index.ts new file mode 100644 index 000000000..d169db261 --- /dev/null +++ b/app/components/channel_list/categories/index.ts @@ -0,0 +1,31 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Q} from '@nozbe/watermelondb'; +import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; +import withObservables from '@nozbe/with-observables'; + +import {MM_TABLES} from '@constants/database'; + +import Categories from './categories'; + +import type {WithDatabaseArgs} from '@typings/database/database'; +import type CategoryModel from '@typings/database/models/servers/category'; + +const {SERVER: {CATEGORY}} = MM_TABLES; + +type WithDatabaseProps = {currentTeamId: string } & WithDatabaseArgs + +const enhanced = withObservables( + ['currentTeamId'], + ({currentTeamId, database}: WithDatabaseProps) => { + const categories = database.get(CATEGORY).query( + Q.where('team_id', currentTeamId), + ).observeWithColumns(['sort_order']); + + return { + categories, + }; + }); + +export default withDatabase(enhanced(Categories)); diff --git a/app/components/channel_list/categories/index.tsx b/app/components/channel_list/categories/index.tsx deleted file mode 100644 index 82927dd21..000000000 --- a/app/components/channel_list/categories/index.tsx +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {FlatList, StyleSheet} from 'react-native'; - -import ThreadsButton from '../threads'; - -import CategoryBody from './body'; -import CategoryHeader from './header'; - -type Props = { - categories: TempoCategory[]; -} - -const styles = StyleSheet.create({ - flex: { - flex: 1, - }, -}); - -const renderCategory = (data: {item: TempoCategory}) => ( - <> - - - -); - -const Categories = (props: Props) => { - return ( - - ); -}; - -export default Categories; diff --git a/app/components/channel_list/index.test.tsx b/app/components/channel_list/index.test.tsx index fdccc6eb7..6533469c6 100644 --- a/app/components/channel_list/index.test.tsx +++ b/app/components/channel_list/index.test.tsx @@ -10,7 +10,7 @@ import {TeamModel} from '@database/models/server'; import {renderWithEverything} from '@test/intl-test-helper'; import TestHelper from '@test/test_helper'; -import ChannelsList from './index'; +import ChannelsList from './'; describe('components/channel_list', () => { let database: Database; @@ -26,12 +26,44 @@ describe('components/channel_list', () => { }); }); - it('should match snapshot', () => { + it('should render', () => { const wrapper = renderWithEverything( + , + {database}, + ); + expect(wrapper.toJSON()).toBeTruthy(); + }); + + it('should render team error', () => { + const wrapper = renderWithEverything( + + + , + {database}, + ); + expect(wrapper.toJSON()).toMatchSnapshot(); + }); + + it('should render channels error', () => { + const wrapper = renderWithEverything( + + , {database}, diff --git a/app/components/channel_list/index.tsx b/app/components/channel_list/index.tsx index 0dc649589..61013b9bc 100644 --- a/app/components/channel_list/index.tsx +++ b/app/components/channel_list/index.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useEffect, useState} from 'react'; +import React, {useEffect} from 'react'; import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import {TABLET_SIDEBAR_WIDTH, TEAM_SIDEBAR_WIDTH} from '@constants/view'; @@ -13,38 +13,28 @@ import ChannelListHeader from './header'; import LoadChannelsError from './load_channels_error'; import LoadTeamsError from './load_teams_error'; import SearchField from './search'; - -// import Loading from '@components/loading'; - -const channels: TempoChannel[] = [ - {id: '1', name: 'Just a channel'}, - {id: '2', name: 'A Highlighted Channel!!!', highlight: true}, - {id: '3', name: 'And a longer channel name.'}, -]; - -const categories: TempoCategory[] = [ - {id: '1', title: 'My first Category', channels}, - {id: '2', title: 'Another Cat', channels}, -]; +import Threads from './threads'; const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ container: { flex: 1, backgroundColor: theme.sidebarBg, - paddingHorizontal: 20, + paddingLeft: 18, + paddingRight: 20, paddingVertical: 10, }, })); type ChannelListProps = { + channelsCount: number; currentTeamId?: string; iconPad?: boolean; isTablet: boolean; teamsCount: number; } -const ChannelList = ({currentTeamId, iconPad, isTablet, teamsCount}: ChannelListProps) => { +const ChannelList = ({channelsCount, currentTeamId, iconPad, isTablet, teamsCount}: ChannelListProps) => { const theme = useTheme(); const styles = getStyleSheet(theme); const tabletWidth = useSharedValue(TABLET_SIDEBAR_WIDTH); @@ -64,26 +54,28 @@ const ChannelList = ({currentTeamId, iconPad, isTablet, teamsCount}: ChannelList } }, [isTablet, teamsCount]); - const [showCats, setShowCats] = useState(true); let content; + if (!currentTeamId) { content = (); - } else if (showCats) { + } else if (channelsCount < 1) { + content = (); + } else { content = ( <> - + + ); - } else { - content = (); } return ( setShowCats(!showCats)} /> {content} diff --git a/app/components/channel_list/search/index.tsx b/app/components/channel_list/search/index.tsx index 718d81117..b7e8a2f1a 100644 --- a/app/components/channel_list/search/index.tsx +++ b/app/components/channel_list/search/index.tsx @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import React from 'react'; -import {StyleSheet, TextInput, View} from 'react-native'; +import {TextInput, StyleSheet, View} from 'react-native'; import CompassIcon from '@components/compass_icon'; import {useTheme} from '@context/theme'; diff --git a/app/components/channel_list/tempo-types.d.ts b/app/components/channel_list/tempo-types.d.ts deleted file mode 100644 index e0993d6c0..000000000 --- a/app/components/channel_list/tempo-types.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -// THIS IS TEMPORARY POC/MVP - WILL BE REMOVEd -// @to-do - Wire up actual models -type TempoChannel = { - id: string; - name: string; - highlight?: boolean; -}; - -type TempoCategory = { - id: string; - title: string; - channels: TempoChannel[]; -} diff --git a/app/components/channel_list/threads/index.test.tsx b/app/components/channel_list/threads/index.test.tsx index 0a3d4b581..daa5cf9fb 100644 --- a/app/components/channel_list/threads/index.test.tsx +++ b/app/components/channel_list/threads/index.test.tsx @@ -3,26 +3,14 @@ import React from 'react'; -import {renderWithEverything} from '@test/intl-test-helper'; -import TestHelper from '@test/test_helper'; +import {renderWithIntlAndTheme} from '@test/intl-test-helper'; import Threads from './index'; -import type Database from '@nozbe/watermelondb/Database'; +test('Threads Component should match snapshot', () => { + const {toJSON} = renderWithIntlAndTheme( + , + ); -describe('Threads Component', () => { - let database: Database | undefined; - beforeAll(async () => { - const server = await TestHelper.setupServerDatabase(); - database = server.database; - }); - - test('should match snapshot', () => { - const {toJSON} = renderWithEverything( - , - {database}, - ); - - expect(toJSON()).toMatchSnapshot(); - }); + expect(toJSON()).toMatchSnapshot(); }); diff --git a/app/components/channel_list/threads/index.tsx b/app/components/channel_list/threads/index.tsx index d723eb6c2..17fca262e 100644 --- a/app/components/channel_list/threads/index.tsx +++ b/app/components/channel_list/threads/index.tsx @@ -1,28 +1,17 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Q} from '@nozbe/watermelondb'; -import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; -import withObservables from '@nozbe/with-observables'; import React from 'react'; import {StyleSheet, Text, View} from 'react-native'; -import {of as of$} from 'rxjs'; -import {switchMap} from 'rxjs/operators'; -import {switchToChannelById} from '@actions/remote/channel'; import CompassIcon from '@components/compass_icon'; import TouchableWithFeedback from '@components/touchable_with_feedback'; -import {General} from '@constants'; -import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database'; -import {useServerUrl} from '@context/server'; +import {Screens} from '@constants'; import {useTheme} from '@context/theme'; +import {goToScreen} from '@screens/navigation'; import {makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; -import type {WithDatabaseArgs} from '@typings/database/database'; -import type ChannelModel from '@typings/database/models/servers/channel'; -import type SystemModel from '@typings/database/models/servers/system'; - const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ container: { display: 'flex', @@ -41,19 +30,18 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ const textStyle = StyleSheet.create([typography('Body', 200, 'SemiBold')]); -const ThreadsButton = ({channelId}: {channelId?: string}) => { +const ThreadsButton = () => { const theme = useTheme(); - const serverUrl = useServerUrl(); const styles = getStyleSheet(theme); /* * @to-do: - * - Check if there are threads, else return null (think of doing this before mounting this component) - * - Change to button, navigate to threads view instead of the current team Town Square + * - Check if there are threads, else return null + * - Change to button, navigate to threads view * - Add right-side number badge */ return ( - (channelId ? switchToChannelById(serverUrl, channelId) : true)} > + goToScreen(Screens.CHANNEL, 'Channel', {}, {topBar: {visible: false}})} > { ); }; -const enhanced = withObservables([], ({database}: WithDatabaseArgs) => { - const currentTeamId = database.get(MM_TABLES.SERVER.SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID); - const channelId = currentTeamId.pipe( - switchMap((model) => database.get(MM_TABLES.SERVER.CHANNEL).query( - Q.where('team_id', model.value), - Q.where('name', General.DEFAULT_CHANNEL), - ).observe().pipe( - // eslint-disable-next-line max-nested-callbacks - switchMap((channels) => (channels.length ? of$(channels[0].id) : of$(undefined))), - )), - ); - - return {channelId}; -}); - -export default withDatabase(enhanced(ThreadsButton)); +export default ThreadsButton; diff --git a/app/components/profile_picture/status.tsx b/app/components/profile_picture/status.tsx index 6be404b7a..c416c972b 100644 --- a/app/components/profile_picture/status.tsx +++ b/app/components/profile_picture/status.tsx @@ -38,7 +38,8 @@ const Status = ({author, statusSize, statusStyle, theme}: Props) => { styles.statusWrapper, statusStyle, {borderRadius: statusSize / 2}, - ]), []); + ]), [statusStyle]); + if (author?.status && !author.isBot) { return ( { teamsCount={props.teamsCount} /> {isTablet && Boolean(props.currentTeamId) && diff --git a/app/screens/home/channel_list/index.ts b/app/screens/home/channel_list/index.ts index ee1890448..d2a42724a 100644 --- a/app/screens/home/channel_list/index.ts +++ b/app/screens/home/channel_list/index.ts @@ -7,14 +7,15 @@ import {catchError, of as of$} from 'rxjs'; import {switchMap} from 'rxjs/operators'; import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database'; -import MyTeamModel from '@typings/database/models/servers/my_team'; import ChannelsList from './channel_list'; import type {WithDatabaseArgs} from '@typings/database/database'; +import type MyChannelModel from '@typings/database/models/servers/my_channel'; +import type MyTeamModel from '@typings/database/models/servers/my_team'; import type SystemModel from '@typings/database/models/servers/system'; -const {SERVER: {MY_TEAM, SYSTEM}} = MM_TABLES; +const {SERVER: {MY_CHANNEL, MY_TEAM, SYSTEM}} = MM_TABLES; const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({ currentTeamId: database.get(SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID).pipe( @@ -22,6 +23,7 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({ catchError(() => of$(undefined)), ), teamsCount: database.get(MY_TEAM).query().observeCount(), + channelsCount: database.get(MY_CHANNEL).query().observeCount(), })); export default withDatabase(enhanced(ChannelsList)); diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 546d693b9..dc4950d33 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -39,7 +39,7 @@ "camera_type.video.option": "Record Video", "center_panel.archived.closeChannel": "Close Channel", "channel": "{count, plural, one {# member} other {# members}}", - "channel_header.directchannel.you": "{displayname} (you)", + "channel_header.directchannel.you": "{displayName} (you)", "channel_loader.someone": "Someone", "channel_modal.optional": "(optional)", "combined_system_message.added_to_channel.many_expanded": "{users} and {lastUser} were **added to the channel** by {actor}.", @@ -150,6 +150,8 @@ "last_users_message.others": "{numOthers} others ", "last_users_message.removed_from_channel.type": "were **removed from the channel**.", "last_users_message.removed_from_team.type": "were **removed from the team**.", + "load_categories_error.message": "There was a problem loading content for this server.", + "load_categories_error.title": "Couldn't load categories for {serverName}", "load_channels_error.message": "There was a problem loading content for this team.", "load_channels_error.title": "Couldn't load {teamDisplayName}", "load_teams_error.message": "There was a problem loading content for this server.", diff --git a/package.json b/package.json index 17d472911..0a7f8df47 100644 --- a/package.json +++ b/package.json @@ -162,6 +162,7 @@ "build:ios-sim": "./scripts/build.sh ipa simulator", "build:ios-unsigned": "./scripts/build.sh ipa unsigned", "check": "npm run lint && npm run tsc", + "check-test": "npm run lint && npm run tsc && npm run test", "clean": "./scripts/clean.sh", "e2e:android": "cd detox && npm run e2e:android-build && npm run e2e:android-test && cd ..", "e2e:ios": "cd detox && npm run e2e:ios-test && cd ..", diff --git a/test/test_helper.js b/test/test_helper.js index 67082bda1..36d8d5fa2 100644 --- a/test/test_helper.js +++ b/test/test_helper.js @@ -3,6 +3,7 @@ import assert from 'assert'; +import {random} from 'lodash'; import nock from 'nock'; import Config from '@assets/config.json'; @@ -22,8 +23,11 @@ class TestHelper { this.basicUser = null; this.basicTeam = null; this.basicTeamMember = null; + this.basicCategory = null; + this.basicCategoryChannel = null; this.basicChannel = null; this.basicChannelMember = null; + this.basicMyChannel = null; this.basicPost = null; this.basicRoles = null; this.basicScheme = null; @@ -52,6 +56,25 @@ class TestHelper { prepareRecordsOnly: false, }); + // Add a category and associated channel entities + await operator.handleCategories({ + categories: [this.basicCategory], + prepareRecordsOnly: false, + }); + await operator.handleCategoryChannels({ + categoryChannels: [this.basicCategoryChannel], + prepareRecordsOnly: false, + }); + await operator.handleChannel({ + channels: [this.basicChannel], + prepareRecordsOnly: false, + }); + await operator.handleMyChannel({ + prepareRecordsOnly: false, + channels: [this.basicChannel], + myChannels: [this.basicMyChannel], + }); + const systems = await prepareCommonSystemValues(operator, { config: {}, license: {}, @@ -93,13 +116,53 @@ class TestHelper { return new Client(mockApiClient, mockApiClient.baseUrl); }; + fakeCategory = (teamId) => { + return { + display_name: 'Test Category', + type: 'custom', + sort_order: 0, + sorting: 'manual', + muted: false, + collapsed: false, + team_id: teamId, + }; + }; + + fakeCategoryWithId = (teamId) => { + return { + ...this.fakeCategory(teamId), + id: this.generateId(), + }; + }; + + fakeCategoryChannel = (categoryId, channelId) => { + return { + category_id: categoryId, + channel_id: channelId, + sort_order: random(0, 10, false), + }; + }; + + fakeCategoryChannelWithId = (teamId, categoryId, channelId) => { + return { + id: teamId + channelId, + category_id: categoryId, + channel_id: channelId, + sort_order: random(0, 10, false), + }; + }; + fakeChannel = (teamId) => { const name = this.generateId(); return { name, team_id: teamId, - display_name: `Unit Test ${name}`, + + // @to-do: Make tests more detriministic; + // https://jestjs.io/docs/snapshot-testing#2-tests-should-be-deterministic + // display_name: `Unit Test ${name}`, + display_name: 'Channel', type: 'O', delete_at: 0, total_msg_count: 0, @@ -144,6 +207,21 @@ class TestHelper { }; }; + fakeMyChannel = (channelId) => { + return { + id: channelId, + channel_id: channelId, + last_post_at: 0, + last_viewed_at: 0, + manually_unread: false, + mentions_count: 0, + message_count: 0, + is_unread: false, + roles: '', + viewed_at: 0, + }; + }; + fakeEmail = () => { return 'success' + this.generateId() + '@simulator.amazonses.com'; }; @@ -318,8 +396,11 @@ class TestHelper { this.basicUser.roles = 'system_user system_admin'; this.basicTeam = this.fakeTeamWithId(); this.basicTeamMember = this.fakeTeamMember(this.basicUser.id, this.basicTeam.id); + this.basicCategory = this.fakeCategoryWithId(this.basicTeam.id); this.basicChannel = this.fakeChannelWithId(this.basicTeam.id); + this.basicCategoryChannel = this.fakeCategoryChannelWithId(this.basicTeam.id, this.basicCategory.id, this.basicChannel.id); this.basicChannelMember = this.fakeChannelMember(this.basicUser.id, this.basicChannel.id); + this.basicMyChannel = this.fakeMyChannel(this.basicChannel.id); this.basicPost = {...this.fakePostWithId(this.basicChannel.id), create_at: 1507841118796}; this.basicRoles = { system_admin: {