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