Show at mentions in RemoveMarkdown renderer (#7664)

This commit is contained in:
Daniel Espino García 2023-11-17 11:43:25 +01:00 committed by GitHub
parent c9fe72460b
commit 044cebc978
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 189 additions and 66 deletions

View file

@ -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<GroupModelType> = {};
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;
}

View file

@ -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<TextStyle>;
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 (
<Text style={textStyle}>
{'@' + mention}
</Text>
);
};
export default AtMention;

View file

@ -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));

View file

@ -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 <Text style={baseStyle}>{literal}</Text>;
}, [baseStyle]);
const renderAtMention = ({context, mentionName}: {context: string[]; mentionName: string}) => {
return (
<AtMention
textStyle={computeTextStyle(textStyle, baseStyle, context)}
mentionName={mentionName}
/>
);
};
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,

66
app/hooks/markdown.ts Normal file
View file

@ -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<GroupModel> = {};
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]);
}