From 2f07b7afc88f57c2135755e8619cbc0721353862 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Mon, 6 Jun 2022 11:27:25 -0400 Subject: [PATCH] [Gekidou] user profile (#6353) * User profile * Access User Profile from reaction list * Fix threads participants list & open user profile on tap * Extra bottom padding * Profile long press tutorial * Adjust heights * Reduce label margin from 12 to 8 --- app/actions/app/global.ts | 13 ++ app/actions/local/user.ts | 26 ++- app/actions/remote/channel.ts | 7 + app/actions/remote/user.ts | 40 ++++ app/actions/websocket/channel.ts | 5 + app/actions/websocket/roles.ts | 7 + app/components/channel_item/index.ts | 2 +- .../formatted_markdown_text/index.tsx | 6 +- .../markdown/at_mention/at_mention.tsx | 36 ++-- app/components/markdown/markdown.tsx | 7 +- app/components/post_draft/archived/index.tsx | 1 + .../post_draft/post_input/post_input.tsx | 17 +- .../combined_user_activity.tsx | 7 +- .../combined_user_activity/last_users.tsx | 10 +- .../post_list/post/avatar/avatar.tsx | 41 ++-- .../post/body/add_members/add_members.tsx | 7 +- .../content/embedded_bindings/embed_text.tsx | 6 +- .../content/embedded_bindings/embed_title.tsx | 6 +- .../embedded_bindings/embedded_binding.tsx | 7 +- .../body/content/embedded_bindings/index.tsx | 4 +- .../post_list/post/body/content/index.tsx | 2 + .../message_attachments/attachment_fields.tsx | 6 +- .../attachment_pretext.tsx | 4 + .../message_attachments/attachment_text.tsx | 6 +- .../message_attachments/attachment_title.tsx | 6 +- .../content/message_attachments/index.tsx | 4 +- .../message_attachment.tsx | 11 +- app/components/post_list/post/body/index.tsx | 2 + .../post_list/post/body/message/message.tsx | 1 + .../post/body/reactions/reactions.tsx | 6 +- .../post_list/post/footer/footer.tsx | 6 +- .../post/header/display_name/index.tsx | 62 ++---- .../post_list/post/header/header.tsx | 13 +- app/components/post_list/post/header/index.ts | 20 +- app/components/post_list/post/post.tsx | 8 +- .../post/system_message/system_message.tsx | 47 +++-- app/components/post_list/post_list.tsx | 1 + app/components/settings/footer.tsx | 6 + app/components/tutorial_highlight/index.tsx | 2 +- .../tutorial_highlight/long_press.tsx | 62 ++++++ .../long_press_illustration.svg | 22 +++ app/components/user_avatars_stack/index.tsx | 16 +- .../user_avatars_stack/users_list/index.tsx | 97 ++++++++-- app/components/user_list_row/index.tsx | 181 ++++++++++++------ app/constants/database.ts | 1 + app/constants/events.ts | 1 + app/constants/screens.ts | 1 - app/queries/app/global.ts | 12 ++ app/queries/servers/user.ts | 22 ++- app/screens/apps_form/apps_form_component.tsx | 2 + app/screens/apps_form/apps_form_field.tsx | 2 + .../intro/direct_channel/direct_channel.tsx | 1 + .../intro/direct_channel/member/member.tsx | 34 ++-- app/screens/channel_info/extra/extra.tsx | 6 +- .../create_direct_message.tsx | 3 + app/screens/create_direct_message/index.ts | 2 + .../create_direct_message/user_list.tsx | 34 +++- .../threads_list/thread/thread.tsx | 7 +- .../thread/thread_footer/thread_footer.tsx | 6 +- .../threads_list/threads_list.tsx | 3 +- app/screens/index.tsx | 51 ++--- .../dialog_introduction_text.tsx | 1 + app/screens/navigation.ts | 28 ++- app/screens/reactions/reactions.tsx | 6 +- app/screens/reactions/reactors_list/index.tsx | 8 +- .../reactions/reactors_list/reactor/index.ts | 6 + .../reactors_list/reactor/reactor.tsx | 35 +++- app/screens/user_profile/index.ts | 60 ++++++ app/screens/user_profile/label.tsx | 43 +++++ app/screens/user_profile/options.tsx | 113 +++++++++++ app/screens/user_profile/title/avatar.tsx | 50 +++++ app/screens/user_profile/title/index.tsx | 108 +++++++++++ app/screens/user_profile/title/tag.tsx | 67 +++++++ app/screens/user_profile/user_profile.tsx | 160 ++++++++++++++++ app/utils/permalink/index.ts | 7 +- 75 files changed, 1428 insertions(+), 298 deletions(-) create mode 100644 app/components/tutorial_highlight/long_press.tsx create mode 100644 app/components/tutorial_highlight/long_press_illustration.svg create mode 100644 app/screens/user_profile/index.ts create mode 100644 app/screens/user_profile/label.tsx create mode 100644 app/screens/user_profile/options.tsx create mode 100644 app/screens/user_profile/title/avatar.tsx create mode 100644 app/screens/user_profile/title/index.tsx create mode 100644 app/screens/user_profile/title/tag.tsx create mode 100644 app/screens/user_profile/user_profile.tsx diff --git a/app/actions/app/global.ts b/app/actions/app/global.ts index b5aa1f40b..9fcdaaa4f 100644 --- a/app/actions/app/global.ts +++ b/app/actions/app/global.ts @@ -29,3 +29,16 @@ export const storeMultiServerTutorial = async (prepareRecordsOnly = false) => { prepareRecordsOnly, }); }; + +export const storeProfileLongPressTutorial = async (prepareRecordsOnly = false) => { + const operator = DatabaseManager.appDatabase?.operator; + + if (!operator) { + return {error: 'No App database found'}; + } + + return operator.handleGlobal({ + globals: [{id: GLOBAL_IDENTIFIERS.PROFILE_LONG_PRESS_TUTORIAL, value: 'true'}], + prepareRecordsOnly, + }); +}; diff --git a/app/actions/local/user.ts b/app/actions/local/user.ts index 331cac00e..738aa1d23 100644 --- a/app/actions/local/user.ts +++ b/app/actions/local/user.ts @@ -5,7 +5,7 @@ import {SYSTEM_IDENTIFIERS} from '@constants/database'; import General from '@constants/general'; import DatabaseManager from '@database/manager'; import {getRecentCustomStatuses} from '@queries/servers/system'; -import {getCurrentUser} from '@queries/servers/user'; +import {getCurrentUser, getUserById} from '@queries/servers/user'; import {addRecentReaction} from './reactions'; @@ -141,3 +141,27 @@ export const updateLocalUser = async ( return {}; }; + +export const storeProfile = async (serverUrl: string, profile: UserProfile) => { + const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; + if (!operator) { + return {error: `${serverUrl} database not found`}; + } + + try { + const {database} = operator; + const user = await getUserById(database, profile.id); + if (user) { + return {user}; + } + + const records = await operator.handleUsers({ + users: [profile], + prepareRecordsOnly: false, + }); + + return {user: records[0]}; + } catch (error) { + return {error}; + } +}; diff --git a/app/actions/remote/channel.ts b/app/actions/remote/channel.ts index 88a5be719..b66c372fe 100644 --- a/app/actions/remote/channel.ts +++ b/app/actions/remote/channel.ts @@ -797,6 +797,13 @@ export async function createDirectChannel(serverUrl: string, userId: string, dis if (!currentUser) { return {error: 'Cannot get the current user'}; } + + const channelName = getDirectChannelName(currentUser.id, userId); + const channel = await getChannelByName(database, channelName); + if (channel) { + return {data: channel.toApi()}; + } + EphemeralStore.creatingDMorGMTeammates = [userId]; const created = await client.createDirectChannel([userId, currentUser.id]); const profiles: UserProfile[] = []; diff --git a/app/actions/remote/user.ts b/app/actions/remote/user.ts index 69e42b7aa..bf7fb823b 100644 --- a/app/actions/remote/user.ts +++ b/app/actions/remote/user.ts @@ -822,3 +822,43 @@ export const autoUpdateTimezone = async (serverUrl: string, {deviceTimezone, use } return null; }; + +export const fetchTeamAndChannelMembership = async (serverUrl: string, userId: string, teamId: string, channelId?: string) => { + const operator = DatabaseManager.serverDatabases[serverUrl].operator; + if (!operator) { + return {error: `No database present for ${serverUrl}`}; + } + + let client: Client; + try { + client = NetworkManager.getClient(serverUrl); + } catch (error) { + return {error}; + } + + try { + const requests = await Promise.all([ + client.getTeamMember(teamId, userId), + channelId ? client.getChannelMember(channelId, userId) : undefined, + ]); + + const modelPromises: Array> = []; + modelPromises.push(operator.handleTeamMemberships({ + teamMemberships: [requests[0]], + prepareRecordsOnly: true, + })); + const channelMemberships = requests[1]; + if (channelMemberships) { + modelPromises.push(operator.handleChannelMembership({ + channelMemberships: [channelMemberships], + prepareRecordsOnly: true, + })); + } + + const models = await Promise.all(modelPromises); + await operator.batchRecords(models.flat()); + return {error: undefined}; + } catch (error) { + return {error}; + } +}; diff --git a/app/actions/websocket/channel.ts b/app/actions/websocket/channel.ts index a57facae4..40cf294d6 100644 --- a/app/actions/websocket/channel.ts +++ b/app/actions/websocket/channel.ts @@ -159,6 +159,11 @@ export async function handleChannelMemberUpdatedEvent(serverUrl: string, msg: an settings: [updatedChannelMember], prepareRecordsOnly: true, })); + + models.push(...await operator.handleChannelMembership({ + channelMemberships: [updatedChannelMember], + prepareRecordsOnly: true, + })); const rolesRequest = await fetchRolesIfNeeded(serverUrl, updatedChannelMember.roles.split(','), true); if (rolesRequest.roles?.length) { models.push(...await operator.handleRole({roles: rolesRequest.roles, prepareRecordsOnly: true})); diff --git a/app/actions/websocket/roles.ts b/app/actions/websocket/roles.ts index a13c2158d..8cb73847d 100644 --- a/app/actions/websocket/roles.ts +++ b/app/actions/websocket/roles.ts @@ -111,6 +111,13 @@ export async function handleTeamMemberRoleUpdatedEvent(serverUrl: string, msg: W models.push(...myTeamRecords); + // update TeamMembership table + const teamMembership = await operator.handleTeamMemberships({ + teamMemberships: [member], + prepareRecordsOnly: true, + }); + models.push(...teamMembership); + await operator.batchRecords(models); } catch { // do nothing diff --git a/app/components/channel_item/index.ts b/app/components/channel_item/index.ts index 324f7879c..0904afb29 100644 --- a/app/components/channel_item/index.ts +++ b/app/components/channel_item/index.ts @@ -58,7 +58,7 @@ const enhance = withObservables(['channel', 'showTeamName'], ({channel, database let membersCount = of$(0); if (channel.type === General.GM_CHANNEL) { - membersCount = channel.members.observeCount(); + membersCount = channel.members.observeCount(false); } const isUnread = myChannel.pipe( diff --git a/app/components/formatted_markdown_text/index.tsx b/app/components/formatted_markdown_text/index.tsx index dbee2feb3..85ee32f57 100644 --- a/app/components/formatted_markdown_text/index.tsx +++ b/app/components/formatted_markdown_text/index.tsx @@ -17,8 +17,10 @@ import type {PrimitiveType} from 'intl-messageformat'; type Props = { baseTextStyle: StyleProp; + channelId?: string; defaultMessage: string; id: string; + location: string; onPostPress?: (e: GestureResponderEvent) => void; style?: StyleProp; textStyles: { @@ -47,7 +49,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { }; }); -const FormattedMarkdownText = ({baseTextStyle, defaultMessage, id, onPostPress, style, textStyles, values}: Props) => { +const FormattedMarkdownText = ({baseTextStyle, channelId, defaultMessage, id, location, onPostPress, style, textStyles, values}: Props) => { const intl = useIntl(); const theme = useTheme(); const styles = getStyleSheet(theme); @@ -84,8 +86,10 @@ const FormattedMarkdownText = ({baseTextStyle, defaultMessage, id, onPostPress, const renderAtMention = ({context, mentionName}: {context: string[]; mentionName: string}) => { return ( diff --git a/app/components/markdown/at_mention/at_mention.tsx b/app/components/markdown/at_mention/at_mention.tsx index 2ce0a37e5..5518bcb3d 100644 --- a/app/components/markdown/at_mention/at_mention.tsx +++ b/app/components/markdown/at_mention/at_mention.tsx @@ -6,25 +6,27 @@ import {Database} from '@nozbe/watermelondb'; import Clipboard from '@react-native-community/clipboard'; import React, {useCallback, useMemo} from 'react'; import {useIntl} from 'react-intl'; -import {GestureResponderEvent, StyleProp, StyleSheet, Text, TextStyle, View} from 'react-native'; +import {GestureResponderEvent, Keyboard, StyleProp, StyleSheet, Text, TextStyle, View} from 'react-native'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; -import CompassIcon from '@components/compass_icon'; import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item'; +import {Screens} from '@constants'; import {MM_TABLES} from '@constants/database'; import {useTheme} from '@context/theme'; import UserModel from '@database/models/server/user'; -import {bottomSheet, dismissBottomSheet, showModal} from '@screens/navigation'; +import {bottomSheet, dismissBottomSheet, openAsBottomSheet} from '@screens/navigation'; import {bottomSheetSnapPoint} from '@utils/helpers'; import {displayUsername, getUsersByUsername} from '@utils/user'; import type UserModelType from '@typings/database/models/servers/user'; type AtMentionProps = { + channelId?: string; currentUserId: string; database: Database; disableAtChannelMentionHighlight?: boolean; isSearchResult?: boolean; + location: string; mentionKeys?: Array<{key: string }>; mentionName: string; mentionStyle: TextStyle; @@ -41,10 +43,12 @@ const style = StyleSheet.create({ }); const AtMention = ({ + channelId, currentUserId, database, disableAtChannelMentionHighlight, isSearchResult, + location, mentionName, mentionKeys, mentionStyle, @@ -89,26 +93,14 @@ const AtMention = ({ return user.mentionKeys; }, [currentUserId, mentionKeys, user]); - const goToUserProfile = () => { - const screen = 'UserProfile'; + const openUserProfile = () => { + const screen = Screens.USER_PROFILE; const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}); - const passProps = { - userId: user.id, - }; + const closeButtonId = 'close-user-profile'; + const props = {closeButtonId, location, userId: user.id, channelId}; - const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor); - - const options = { - topBar: { - leftButtons: [{ - id: 'close-settings', - icon: closeButton, - testID: 'close.settings.button', - }], - }, - }; - - showModal(screen, title, passProps, options); + Keyboard.dismiss(); + openAsBottomSheet({screen, title, theme, closeButtonId, props}); }; const handleLongPress = useCallback(() => { @@ -196,7 +188,7 @@ const AtMention = ({ if (canPress) { onLongPress = handleLongPress; - onPress = (isSearchResult ? onPostPress : goToUserProfile); + onPress = (isSearchResult ? onPostPress : openUserProfile); } if (suffix) { diff --git a/app/components/markdown/markdown.tsx b/app/components/markdown/markdown.tsx index ac12ecea0..8e6a6d87a 100644 --- a/app/components/markdown/markdown.tsx +++ b/app/components/markdown/markdown.tsx @@ -38,6 +38,7 @@ type MarkdownProps = { autolinkedUrlSchemes?: string[]; baseTextStyle: StyleProp; blockStyles?: MarkdownBlockStyles; + channelId?: string; channelMentions?: ChannelMentions; disableAtChannelMentionHighlight?: boolean; disableAtMentions?: boolean; @@ -57,7 +58,7 @@ type MarkdownProps = { isSearchResult?: boolean; layoutHeight?: number; layoutWidth?: number; - location?: string; + location: string; mentionKeys?: UserMentionKey[]; minimumHashtagLength?: number; onPostPress?: (event: GestureResponderEvent) => void; @@ -124,7 +125,7 @@ const computeTextStyle = (textStyles: MarkdownTextStyles, baseStyle: StyleProp