From deb222a01dd9e498d2a7f064478d59a10fd229f1 Mon Sep 17 00:00:00 2001 From: Anurag Shivarathri Date: Tue, 12 Apr 2022 19:06:13 +0530 Subject: [PATCH] Gekidou CRT - User avatar stack (#6139) * User avatar stack * Fixed fetchPostThread & added observer * Reusing the user component * Refactor fix * fix lint Co-authored-by: Elias Nahum --- app/actions/remote/post.ts | 4 +- app/components/user_avatars_stack/index.tsx | 157 ++++++++++++++++++ .../user_avatars_stack/user_avatar/index.ts | 17 ++ .../user_avatar/user_avatar.tsx | 30 ++++ .../user_avatars_stack/users_list/index.tsx | 35 ++++ app/queries/servers/thread.ts | 9 +- assets/base/i18n/en.json | 1 + 7 files changed, 250 insertions(+), 3 deletions(-) create mode 100644 app/components/user_avatars_stack/index.tsx create mode 100644 app/components/user_avatars_stack/user_avatar/index.ts create mode 100644 app/components/user_avatars_stack/user_avatar/user_avatar.tsx create mode 100644 app/components/user_avatars_stack/users_list/index.tsx diff --git a/app/actions/remote/post.ts b/app/actions/remote/post.ts index abf2267558..bb0a74fe21 100644 --- a/app/actions/remote/post.ts +++ b/app/actions/remote/post.ts @@ -520,7 +520,8 @@ export async function fetchPostThread(serverUrl: string, postId: string, fetchOn } try { - const data = await client.getPostThread(postId); + const isCRTEnabled = await getIsCRTEnabled(operator.database); + const data = await client.getPostThread(postId, isCRTEnabled, isCRTEnabled); const result = processPostsFetched(data); if (!fetchOnly) { const models = await operator.handlePosts({ @@ -528,7 +529,6 @@ export async function fetchPostThread(serverUrl: string, postId: string, fetchOn actionType: ActionType.POSTS.RECEIVED_IN_THREAD, prepareRecordsOnly: true, }); - const isCRTEnabled = await getIsCRTEnabled(operator.database); if (isCRTEnabled) { const threadModels = await prepareThreadsFromReceivedPosts(operator, result.posts); if (threadModels?.length) { diff --git a/app/components/user_avatars_stack/index.tsx b/app/components/user_avatars_stack/index.tsx new file mode 100644 index 0000000000..4ddcf51239 --- /dev/null +++ b/app/components/user_avatars_stack/index.tsx @@ -0,0 +1,157 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useCallback} from 'react'; +import {useIntl} from 'react-intl'; +import {StyleProp, Text, TouchableOpacity, View, ViewStyle} from 'react-native'; + +import FormattedText from '@components/formatted_text'; +import {useTheme} from '@context/theme'; +import {useIsTablet} from '@hooks/device'; +import {bottomSheet} from '@screens/navigation'; +import {preventDoubleTap} from '@utils/tap'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; + +import UserAvatar from './user_avatar'; +import UsersList from './users_list'; + +import type UserModel from '@typings/database/models/servers/user'; + +const OVERFLOW_DISPLAY_LIMIT = 99; + +type Props = { + users: UserModel[]; + breakAt?: number; + style?: StyleProp; +} + +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { + const size = 24; + const imgOverlap = -6; + + return { + container: { + flexDirection: 'row', + }, + firstAvatar: { + justifyContent: 'center', + alignItems: 'center', + width: size, + height: size, + borderWidth: (size / 2) + 1, + borderColor: theme.centerChannelBg, + backgroundColor: theme.centerChannelBg, + borderRadius: size / 2, + }, + notFirstAvatars: { + justifyContent: 'center', + alignItems: 'center', + width: size, + height: size, + borderWidth: (size / 2) + 1, + borderColor: theme.centerChannelBg, + backgroundColor: theme.centerChannelBg, + borderRadius: size / 2, + marginLeft: imgOverlap, + }, + overflowContainer: { + justifyContent: 'center', + alignItems: 'center', + width: size, + height: size, + borderRadius: size / 2, + borderWidth: 1, + borderColor: theme.centerChannelBg, + backgroundColor: theme.centerChannelBg, + marginLeft: imgOverlap, + }, + overflowItem: { + justifyContent: 'center', + alignItems: 'center', + width: size, + height: size, + borderRadius: size / 2, + borderWidth: 1, + borderColor: theme.centerChannelBg, + backgroundColor: changeOpacity(theme.centerChannelColor, 0.08), + }, + overflowText: { + fontSize: 10, + fontWeight: 'bold', + color: changeOpacity(theme.centerChannelColor, 0.64), + textAlign: 'center', + }, + listHeader: { + marginBottom: 12, + }, + listHeaderText: { + color: changeOpacity(theme.centerChannelColor, 0.56), + fontSize: 12, + fontWeight: '600', + }, + }; +}); + +const UserAvatarsStack = ({breakAt = 3, style: baseContainerStyle, users}: Props) => { + const theme = useTheme(); + const intl = useIntl(); + const isTablet = useIsTablet(); + + const showParticipantsList = useCallback(preventDoubleTap(() => { + const renderContent = () => ( + <> + {!isTablet && ( + + + + )} + + + ); + + bottomSheet({ + closeButtonId: 'close-set-user-status', + renderContent, + snapPoints: [(Math.min(14, users.length) + 3) * 40, 10], + title: intl.formatMessage({id: 'mobile.participants.header', defaultMessage: 'THREAD PARTICIPANTS'}), + theme, + }); + }), [isTablet, theme, users]); + + const displayUsers = users.slice(0, breakAt); + const overflowUsersCount = Math.min(users.length - displayUsers.length, OVERFLOW_DISPLAY_LIMIT); + + const style = getStyleSheet(theme); + + return ( + + + {displayUsers.map((user, index) => ( + + ))} + {Boolean(overflowUsersCount) && ( + + + + {'+' + overflowUsersCount.toString()} + + + + )} + + + ); +}; + +export default UserAvatarsStack; diff --git a/app/components/user_avatars_stack/user_avatar/index.ts b/app/components/user_avatars_stack/user_avatar/index.ts new file mode 100644 index 0000000000..84c1b422e0 --- /dev/null +++ b/app/components/user_avatars_stack/user_avatar/index.ts @@ -0,0 +1,17 @@ +// 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 UserAvatar from './user_avatar'; + +import type UserModel from '@typings/database/models/servers/user'; + +const enhanced = withObservables(['user'], ({user}: {user: UserModel}) => { + return { + user: user.observe(), + }; +}); + +export default withDatabase(enhanced(UserAvatar)); diff --git a/app/components/user_avatars_stack/user_avatar/user_avatar.tsx b/app/components/user_avatars_stack/user_avatar/user_avatar.tsx new file mode 100644 index 0000000000..c0ed54902b --- /dev/null +++ b/app/components/user_avatars_stack/user_avatar/user_avatar.tsx @@ -0,0 +1,30 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {StyleProp, View, ViewStyle} from 'react-native'; + +import ProfilePicture from '@components/profile_picture/image'; + +import type UserModel from '@typings/database/models/servers/user'; + +export type Props = { + style: StyleProp; + user: UserModel; +}; + +const UserAvatar = ({style, user}: Props) => { + return ( + + + + ); +}; + +export default UserAvatar; diff --git a/app/components/user_avatars_stack/users_list/index.tsx b/app/components/user_avatars_stack/users_list/index.tsx new file mode 100644 index 0000000000..7bf0010d2a --- /dev/null +++ b/app/components/user_avatars_stack/users_list/index.tsx @@ -0,0 +1,35 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {StyleSheet} from 'react-native'; + +import UserItem from '@components/user_item'; + +import type UserModel from '@typings/database/models/servers/user'; + +type Props = { + users: UserModel[]; +}; + +const style = StyleSheet.create({ + container: { + paddingLeft: 0, + }, +}); + +const UsersList = ({users}: Props) => { + return ( + <> + {users.map((user) => ( + + ))} + + ); +}; + +export default UsersList; diff --git a/app/queries/servers/thread.ts b/app/queries/servers/thread.ts index 6f00027b4f..a6e85f612c 100644 --- a/app/queries/servers/thread.ts +++ b/app/queries/servers/thread.ts @@ -15,8 +15,9 @@ import {getConfig, observeConfig} from './system'; import type ServerDataOperator from '@database/operator/server_data_operator'; import type Model from '@nozbe/watermelondb/Model'; import type ThreadModel from '@typings/database/models/servers/thread'; +import type UserModel from '@typings/database/models/servers/user'; -const {SERVER: {THREADS_IN_TEAM, THREAD, POST, CHANNEL}} = MM_TABLES; +const {SERVER: {CHANNEL, POST, THREAD, THREADS_IN_TEAM, THREAD_PARTICIPANT, USER}} = MM_TABLES; export const getIsCRTEnabled = async (database: Database): Promise => { const config = await getConfig(database); @@ -190,3 +191,9 @@ export const queryThreads = (database: Database, teamId?: string, onlyUnreads = return database.get(THREAD).query(...query); }; + +export const queryThreadParticipants = (database: Database, threadId: string) => { + return database.get(USER).query( + Q.on(THREAD_PARTICIPANT, Q.where('thread_id', threadId)), + ); +}; diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index c3095d49ae..f2e2622a5e 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -332,6 +332,7 @@ "mobile.oauth.switch_to_browser.title": "Redirecting...", "mobile.oauth.try_again": "Try again", "mobile.open_gm.error": "We couldn't open a group message with those users. Please check your connection and try again.", + "mobile.participants.header": "THREAD PARTICIPANTS", "mobile.permission_denied_dismiss": "Don't Allow", "mobile.permission_denied_retry": "Settings", "mobile.post_info.add_reaction": "Add Reaction",