diff --git a/app/components/markdown/at_mention/at_mention.tsx b/app/components/markdown/at_mention/at_mention.tsx index e53d7683c..bdadc9775 100644 --- a/app/components/markdown/at_mention/at_mention.tsx +++ b/app/components/markdown/at_mention/at_mention.tsx @@ -11,24 +11,20 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'; import {fetchUserOrGroupsByMentionsInBatch} from '@actions/remote/user'; import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item'; import {Screens} from '@constants'; -import {MM_TABLES} from '@constants/database'; import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; import GroupModel from '@database/models/server/group'; -import UserModel from '@database/models/server/user'; +import {useMemoMentionedGroup, useMemoMentionedUser} from '@hooks/markdown'; import {bottomSheet, dismissBottomSheet, openAsBottomSheet} from '@screens/navigation'; import {bottomSheetSnapPoint} from '@utils/helpers'; -import {displayUsername, getUsersByUsername} from '@utils/user'; +import {displayUsername} from '@utils/user'; -import type {Database} from '@nozbe/watermelondb'; -import type GroupModelType from '@typings/database/models/servers/group'; import type GroupMembershipModel from '@typings/database/models/servers/group_membership'; import type UserModelType from '@typings/database/models/servers/user'; type AtMentionProps = { channelId?: string; currentUserId: string; - database: Database; disableAtChannelMentionHighlight?: boolean; isSearchResult?: boolean; location: string; @@ -43,8 +39,6 @@ type AtMentionProps = { groupMemberships: GroupMembershipModel[]; } -const {SERVER: {GROUP, USER}} = MM_TABLES; - const style = StyleSheet.create({ bottomSheet: {flex: 1}, }); @@ -52,7 +46,6 @@ const style = StyleSheet.create({ const AtMention = ({ channelId, currentUserId, - database, disableAtChannelMentionHighlight, isSearchResult, location, @@ -72,31 +65,15 @@ const AtMention = ({ const {bottom} = useSafeAreaInsets(); const serverUrl = useServerUrl(); - const user = useMemo(() => { - const usersByUsername = getUsersByUsername(users); - let mn = mentionName.toLowerCase(); - - while (mn.length > 0) { - if (usersByUsername[mn]) { - return usersByUsername[mn]; - } - - // Repeatedly trim off trailing punctuation in case this is at the end of a sentence - if ((/[._-]$/).test(mn)) { - mn = mn.substring(0, mn.length - 1); - } else { - break; - } - } - - // @ts-expect-error: The model constructor is hidden within WDB type definition - return new UserModel(database.get(USER), {username: ''}); - }, [users, mentionName]); + const user = useMemoMentionedUser(users, mentionName); const userMentionKeys = useMemo(() => { if (mentionKeys) { return mentionKeys; } + if (!user) { + return []; + } if (user.id !== currentUserId) { return []; @@ -105,50 +82,21 @@ const AtMention = ({ return user.mentionKeys; }, [currentUserId, mentionKeys, user]); - // Checks if the mention is a group - const group = useMemo(() => { - if (user?.username) { - return undefined; - } - const getGroupsByName = (gs: GroupModelType[]) => { - const groupsByName: Dictionary = {}; - - for (const g of gs) { - groupsByName[g.name] = g; - } - - return groupsByName; - }; - - const groupsByName = getGroupsByName(groups); - let mn = mentionName.toLowerCase(); - - while (mn.length > 0) { - if (groupsByName[mn]) { - return groupsByName[mn]; - } - - // Repeatedly trim off trailing punctuation in case this is at the end of a sentence - if ((/[._-]$/).test(mn)) { - mn = mn.substring(0, mn.length - 1); - } else { - break; - } - } - - // @ts-expect-error: The model constructor is hidden within WDB type definition - return new GroupModel(database.get(GROUP), {name: ''}); - }, [groups, user, mentionName]); + const group = useMemoMentionedGroup(groups, user, mentionName); // Effects useEffect(() => { // Fetches and updates the local db store with the mention - if (!user.username && !group?.name) { + if (!user?.username && !group?.name) { fetchUserOrGroupsByMentionsInBatch(serverUrl, mentionName); } }, []); const openUserProfile = () => { + if (!user) { + return; + } + const screen = Screens.USER_PROFILE; const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}); const closeButtonId = 'close-user-profile'; @@ -171,7 +119,7 @@ const AtMention = ({ onPress={() => { dismissBottomSheet(); let username = mentionName; - if (user.username) { + if (user?.username) { username = user.username; } diff --git a/app/components/remove_markdown/at_mention/at_mention.tsx b/app/components/remove_markdown/at_mention/at_mention.tsx new file mode 100644 index 000000000..c648a50d5 --- /dev/null +++ b/app/components/remove_markdown/at_mention/at_mention.tsx @@ -0,0 +1,69 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useEffect} from 'react'; +import {type StyleProp, Text, type TextStyle} from 'react-native'; + +import {fetchUserOrGroupsByMentionsInBatch} from '@actions/remote/user'; +import {useServerUrl} from '@context/server'; +import GroupModel from '@database/models/server/group'; +import {useMemoMentionedGroup, useMemoMentionedUser} from '@hooks/markdown'; +import {displayUsername} from '@utils/user'; + +import type {Database} from '@nozbe/watermelondb'; +import type UserModelType from '@typings/database/models/servers/user'; + +type AtMentionProps = { + database: Database; + mentionName: string; + teammateNameDisplay: string; + textStyle?: StyleProp; + users: UserModelType[]; + groups: GroupModel[]; +} + +const AtMention = ({ + mentionName, + teammateNameDisplay, + textStyle, + users, + groups, +}: AtMentionProps) => { + const serverUrl = useServerUrl(); + + const user = useMemoMentionedUser(users, mentionName); + const group = useMemoMentionedGroup(groups, user, mentionName); + + // Effects + useEffect(() => { + // Fetches and updates the local db store with the mention + if (!user?.username && !group?.name) { + fetchUserOrGroupsByMentionsInBatch(serverUrl, mentionName); + } + }, []); + + let mention; + + if (user?.username) { + mention = displayUsername(user, user.locale, teammateNameDisplay); + } else if (group?.name) { + mention = group.name; + } else { + const pattern = new RegExp(/\b(all|channel|here)(?:\.\B|_\b|\b)/, 'i'); + const mentionMatch = pattern.exec(mentionName); + + if (mentionMatch) { + mention = mentionMatch.length > 1 ? mentionMatch[1] : mentionMatch[0]; + } else { + mention = mentionName; + } + } + + return ( + + {'@' + mention} + + ); +}; + +export default AtMention; diff --git a/app/components/remove_markdown/at_mention/index.ts b/app/components/remove_markdown/at_mention/index.ts new file mode 100644 index 000000000..a95fbfebb --- /dev/null +++ b/app/components/remove_markdown/at_mention/index.ts @@ -0,0 +1,29 @@ +// 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 {queryGroupsByName} from '@queries/servers/group'; +import {observeTeammateNameDisplay, queryUsersLike} from '@queries/servers/user'; + +import AtMention from './at_mention'; + +import type {WithDatabaseArgs} from '@typings/database/database'; + +const enhance = withObservables(['mentionName'], ({database, mentionName}: {mentionName: string} & WithDatabaseArgs) => { + const teammateNameDisplay = observeTeammateNameDisplay(database); + + let mn = mentionName.toLowerCase(); + if ((/[._-]$/).test(mn)) { + mn = mn.substring(0, mn.length - 1); + } + + return { + teammateNameDisplay, + users: queryUsersLike(database, mn).observeWithColumns(['username']), + groups: queryGroupsByName(database, mn).observeWithColumns(['name']), + }; +}); + +export default withDatabase(enhance(AtMention)); diff --git a/app/components/remove_markdown/index.tsx b/app/components/remove_markdown/index.tsx index aa4cfb61d..af9a3e7e1 100644 --- a/app/components/remove_markdown/index.tsx +++ b/app/components/remove_markdown/index.tsx @@ -9,6 +9,8 @@ import {type StyleProp, Text, type TextStyle} from 'react-native'; import Emoji from '@components/emoji'; import {computeTextStyle} from '@utils/markdown'; +import AtMention from './at_mention'; + import type {MarkdownBaseRenderer, MarkdownEmojiRenderer, MarkdownTextStyles} from '@typings/global/markdown'; type Props = { @@ -45,6 +47,15 @@ const RemoveMarkdown = ({enableEmoji, enableHardBreak, enableSoftBreak, enableCo return {literal}; }, [baseStyle]); + const renderAtMention = ({context, mentionName}: {context: string[]; mentionName: string}) => { + return ( + + ); + }; + const renderCodeSpan = useCallback(({context, literal}: MarkdownBaseRenderer) => { if (!enableCodeSpan) { return renderText({literal}); @@ -76,7 +87,7 @@ const RemoveMarkdown = ({enableEmoji, enableHardBreak, enableSoftBreak, enableCo code: renderCodeSpan, link: Renderer.forwardChildren, image: renderNull, - atMention: Renderer.forwardChildren, + atMention: renderAtMention, channelLink: Renderer.forwardChildren, emoji: renderEmoji, hashtag: Renderer.forwardChildren, diff --git a/app/hooks/markdown.ts b/app/hooks/markdown.ts new file mode 100644 index 000000000..7ea287808 --- /dev/null +++ b/app/hooks/markdown.ts @@ -0,0 +1,66 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {useMemo} from 'react'; + +import {getUsersByUsername} from '@utils/user'; + +import type GroupModel from '@typings/database/models/servers/group'; +import type UserModel from '@typings/database/models/servers/user'; + +export function useMemoMentionedUser(users: UserModel[], mentionName: string) { + return useMemo(() => { + const usersByUsername = getUsersByUsername(users); + let mn = mentionName.toLowerCase(); + + while (mn.length > 0) { + if (usersByUsername[mn]) { + return usersByUsername[mn]; + } + + // Repeatedly trim off trailing punctuation in case this is at the end of a sentence + if ((/[._-]$/).test(mn)) { + mn = mn.substring(0, mn.length - 1); + } else { + break; + } + } + + return undefined; + }, [users, mentionName]); +} + +export function useMemoMentionedGroup(groups: GroupModel[], user: UserModel | undefined, mentionName: string) { + // Checks if the mention is a group + return useMemo(() => { + if (user?.username) { + return undefined; + } + const getGroupsByName = (gs: GroupModel[]) => { + const groupsByName: Dictionary = {}; + + for (const g of gs) { + groupsByName[g.name] = g; + } + + return groupsByName; + }; + + const groupsByName = getGroupsByName(groups); + let mn = mentionName.toLowerCase(); + + while (mn.length > 0) { + if (groupsByName[mn]) { + return groupsByName[mn]; + } + + // Repeatedly trim off trailing punctuation in case this is at the end of a sentence + if ((/[._-]$/).test(mn)) { + mn = mn.substring(0, mn.length - 1); + } else { + break; + } + } + + return undefined; + }, [groups, user, mentionName]); +}