[Gekidou] Groups mentions / highlights in messages (posts) (#6338)

* WIP

* Actions updated to fetch remote first, and local on error

* Groups fetch and save

* PR Feedback: prepare vs store and undefined fix

* Forgot to add file

* Groups Mention WIP

* Groups highlight!

* Merge, PR Feedback

* PR Feedback

* PR Feedback: Try/Catch blocks

* PR Feedback
This commit is contained in:
Shaz MJ 2022-06-29 07:47:37 +10:00 committed by GitHub
parent 2230fe8a70
commit f8140f2117
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 174 additions and 43 deletions

2
.gitignore vendored
View file

@ -107,7 +107,7 @@ detox/detox_pixel_4_xl_api_30
#editor-settings
.vscode
.scannerwork
launch.json
# Notice.txt generation
!build/notice-file

View file

@ -9,9 +9,13 @@ import {logError} from '@utils/log';
import type GroupModel from '@typings/database/models/servers/group';
export const searchGroupsByName = async (serverUrl: string, name: string): Promise<GroupModel[]> => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
throw new Error(`${serverUrl} operator not found`);
let database;
try {
database = DatabaseManager.getServerDatabaseAndOperator(serverUrl).database;
} catch (e) {
logError('searchGroupsByName - DB Error', e);
return [];
}
try {
@ -23,14 +27,19 @@ export const searchGroupsByName = async (serverUrl: string, name: string): Promi
throw groups.error;
} catch (e) {
logError('searchGroupsByName - ERROR', e);
return queryGroupsByName(operator.database, name).fetch();
return queryGroupsByName(database, name).fetch();
}
};
export const searchGroupsByNameInTeam = async (serverUrl: string, name: string, teamId: string): Promise<GroupModel[]> => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
throw new Error(`${serverUrl} operator not found`);
let database;
try {
database = DatabaseManager.getServerDatabaseAndOperator(serverUrl).database;
} catch (e) {
// eslint-disable-next-line no-console
console.log('searchGroupsByNameInTeam - DB Error', e);
return [];
}
try {
@ -42,14 +51,19 @@ export const searchGroupsByNameInTeam = async (serverUrl: string, name: string,
throw groups.error;
} catch (e) {
logError('searchGroupsByNameInTeam - ERROR', e);
return queryGroupsByNameInTeam(operator.database, name, teamId).fetch();
return queryGroupsByNameInTeam(database, name, teamId).fetch();
}
};
export const searchGroupsByNameInChannel = async (serverUrl: string, name: string, channelId: string): Promise<GroupModel[]> => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
throw new Error(`${serverUrl} operator not found`);
let database;
try {
database = DatabaseManager.getServerDatabaseAndOperator(serverUrl).database;
} catch (e) {
// eslint-disable-next-line no-console
console.log('searchGroupsByNameInChannel - DB Error', e);
return [];
}
try {
@ -61,7 +75,7 @@ export const searchGroupsByNameInChannel = async (serverUrl: string, name: strin
throw groups.error;
} catch (e) {
logError('searchGroupsByNameInChannel - ERROR', e);
return queryGroupsByNameInChannel(operator.database, name, channelId).fetch();
return queryGroupsByNameInChannel(database, name, channelId).fetch();
}
};
@ -70,15 +84,11 @@ export const searchGroupsByNameInChannel = async (serverUrl: string, name: strin
*
* @param serverUrl string - The Server URL
* @param groups Group[] - The groups fetched from the API
* @param prepareRecordsOnly boolean - Wether to only prepare records without saving
*/
export const storeGroups = async (serverUrl: string, groups: Group[]) => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
throw new Error(`${serverUrl} operator not found`);
}
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const preparedGroups = await prepareGroups(operator, groups);
if (preparedGroups.length) {

View file

@ -11,20 +11,41 @@ import {forceLogoutIfNecessary} from './session';
export const fetchGroupsForAutocomplete = async (serverUrl: string, query: string, fetchOnly = false) => {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const client: Client = NetworkManager.getClient(serverUrl);
const response = await client.getGroups(query);
// Save locally
if (!fetchOnly) {
return await storeGroups(serverUrl, response);
return storeGroups(serverUrl, response);
}
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
throw new Error(`${serverUrl} operator not found`);
return prepareGroups(operator, response);
} catch (error) {
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};
export const fetchGroupsByNames = async (serverUrl: string, names: string[], fetchOnly = false) => {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const client: Client = NetworkManager.getClient(serverUrl);
const promises: Array <Promise<Group[]>> = [];
names.forEach((name) => {
promises.push(client.getGroups(name));
});
const groups = (await Promise.all(promises)).flat();
// Save locally
if (!fetchOnly) {
return storeGroups(serverUrl, groups);
}
return await prepareGroups(operator, response);
return prepareGroups(operator, groups);
} catch (error) {
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
@ -33,19 +54,15 @@ export const fetchGroupsForAutocomplete = async (serverUrl: string, query: strin
export const fetchGroupsForChannel = async (serverUrl: string, channelId: string, fetchOnly = false) => {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const client = NetworkManager.getClient(serverUrl);
const response = await client.getAllGroupsAssociatedToChannel(channelId);
if (!fetchOnly) {
return await storeGroups(serverUrl, response.groups);
return storeGroups(serverUrl, response.groups);
}
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
throw new Error(`${serverUrl} operator not found`);
}
return await prepareGroups(operator, response.groups);
return prepareGroups(operator, response.groups);
} catch (error) {
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
@ -54,20 +71,15 @@ export const fetchGroupsForChannel = async (serverUrl: string, channelId: string
export const fetchGroupsForTeam = async (serverUrl: string, teamId: string, fetchOnly = false) => {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const client: Client = NetworkManager.getClient(serverUrl);
const response = await client.getAllGroupsAssociatedToTeam(teamId);
if (!fetchOnly) {
return await storeGroups(serverUrl, response.groups);
return storeGroups(serverUrl, response.groups);
}
// return await storeGroups(serverUrl, response.groups, true);
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
throw new Error(`${serverUrl} operator not found`);
}
return await prepareGroups(operator, response.groups);
return prepareGroups(operator, response.groups);
} catch (error) {
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};

View file

@ -14,11 +14,13 @@ import DatabaseManager from '@database/manager';
import {debounce} from '@helpers/api/general';
import NetworkManager from '@managers/network_manager';
import {getMembersCountByChannelsId, queryChannelsByTypes} from '@queries/servers/channel';
import {queryGroupsByNames} from '@queries/servers/group';
import {getCurrentTeamId, getCurrentUserId} from '@queries/servers/system';
import {getCurrentUser, getUserById, prepareUsers, queryAllUsers, queryUsersById, queryUsersByUsername} from '@queries/servers/user';
import {getCurrentUser, getUserById, prepareUsers, queryAllUsers, queryUsersById, queryUsersByIdsOrUsernames, queryUsersByUsername} from '@queries/servers/user';
import {logError} from '@utils/log';
import {getUserTimezoneProps, removeUserFromList} from '@utils/user';
import {fetchGroupsByNames} from './groups';
import {forceLogoutIfNecessary} from './session';
import type {Client} from '@client/rest';
@ -278,6 +280,53 @@ export const fetchStatusInBatch = (serverUrl: string, id: string) => {
return debouncedFetchStatusesByIds.apply(null, [serverUrl]);
};
const mentionNames = new Set<string>();
export const fetchUserOrGroupsByMentionsInBatch = (serverUrl: string, mentionName: string) => {
mentionNames.add(mentionName);
return debouncedFetchUserOrGroupsByMentionNames.apply(null, [serverUrl]);
};
const debouncedFetchUserOrGroupsByMentionNames = debounce(
(serverUrl: string) => {
fetchUserOrGroupsByMentionNames(serverUrl, Array.from(mentionNames));
},
200,
false,
() => {
mentionNames.clear();
},
);
const fetchUserOrGroupsByMentionNames = async (serverUrl: string, mentions: string[]) => {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
// Get any missing users
const usersInDb = await queryUsersByIdsOrUsernames(database, [], mentions).fetch();
const usersMap = new Set(usersInDb.map((u) => u.username));
const usernamesToFetch = mentions.filter((m) => !usersMap.has(m));
let fetchedUsers;
if (usernamesToFetch.length) {
const {users} = await fetchUsersByUsernames(serverUrl, usernamesToFetch, false);
fetchedUsers = users;
}
// Get any missing groups
const fetchedUserMentions = new Set(fetchedUsers?.map((u) => u.username));
const groupsToCheck = usernamesToFetch.filter((m) => !fetchedUserMentions.has(m));
const groupsInDb = await queryGroupsByNames(database, groupsToCheck).fetch();
const groupsMap = new Set(groupsInDb.map((g) => g.name));
const groupsToFetch = groupsToCheck.filter((g) => !groupsMap.has(g));
if (groupsToFetch.length) {
await fetchGroupsByNames(serverUrl, groupsToFetch, false);
}
return {data: true};
} catch (e) {
return {error: e};
}
};
export async function fetchStatusByIds(serverUrl: string, userIds: string[], fetchOnly = false) {
let client: Client;
try {

View file

@ -4,20 +4,24 @@
import {useManagedConfig} from '@mattermost/react-native-emm';
import {Database} from '@nozbe/watermelondb';
import Clipboard from '@react-native-community/clipboard';
import React, {useCallback, useMemo} from 'react';
import React, {useCallback, useEffect, useMemo} from 'react';
import {useIntl} from 'react-intl';
import {GestureResponderEvent, Keyboard, StyleProp, StyleSheet, Text, TextStyle, View} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {fetchUserOrGroupsByMentionsInBatch} from '@actions/remote/user';
import {useServerUrl} from '@app/context/server';
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 GroupModel from '@database/models/server/group';
import UserModel from '@database/models/server/user';
import {bottomSheet, dismissBottomSheet, openAsBottomSheet} from '@screens/navigation';
import {bottomSheetSnapPoint} from '@utils/helpers';
import {displayUsername, getUsersByUsername} from '@utils/user';
import type GroupModelType from '@typings/database/models/servers/group';
import type UserModelType from '@typings/database/models/servers/user';
type AtMentionProps = {
@ -34,9 +38,10 @@ type AtMentionProps = {
teammateNameDisplay: string;
textStyle?: StyleProp<TextStyle>;
users: UserModelType[];
groups: GroupModel[];
}
const {SERVER: {USER}} = MM_TABLES;
const {SERVER: {GROUP, USER}} = MM_TABLES;
const style = StyleSheet.create({
bottomSheet: {flex: 1},
@ -56,11 +61,14 @@ const AtMention = ({
teammateNameDisplay,
textStyle,
users,
groups,
}: AtMentionProps) => {
const intl = useIntl();
const managedConfig = useManagedConfig<ManagedConfig>();
const theme = useTheme();
const insets = useSafeAreaInsets();
const serverUrl = useServerUrl();
const user = useMemo(() => {
const usersByUsername = getUsersByUsername(users);
let mn = mentionName.toLowerCase();
@ -93,6 +101,47 @@ 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]);
// Effects
useEffect(() => {
// Fetches and updates the local db store with the mention
fetchUserOrGroupsByMentionsInBatch(serverUrl, mentionName);
}, []);
const openUserProfile = () => {
const screen = Screens.USER_PROFILE;
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
@ -172,6 +221,10 @@ const AtMention = ({
mention = displayUsername(user, user.locale, teammateNameDisplay);
isMention = true;
canPress = true;
} else if (group?.name) {
mention = group.name;
isMention = true;
canPress = false;
} else {
const pattern = new RegExp(/\b(all|channel|here)(?:\.\B|_\b|\b)/, 'i');
const mentionMatch = pattern.exec(mentionName);

View file

@ -4,6 +4,7 @@
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {queryGroupsByName} from '@app/queries/servers/group';
import {observeCurrentUserId} from '@queries/servers/system';
import {observeTeammateNameDisplay, queryUsersLike} from '@queries/servers/user';
@ -24,6 +25,7 @@ const enhance = withObservables(['mentionName'], ({database, mentionName}: {ment
currentUserId,
teammateNameDisplay,
users: queryUsersLike(database, mn).observeWithColumns(['username']),
groups: queryGroupsByName(database, mn).observeWithColumns(['name']),
};
});

View file

@ -16,6 +16,12 @@ export const queryGroupsByName = (database: Database, name: string) => {
);
};
export const queryGroupsByNames = (database: Database, names: string[]) => {
return database.collections.get<GroupModel>(GROUP).query(
Q.where('name', Q.oneOf(names)),
);
};
export const queryGroupsByNameInTeam = (database: Database, name: string, teamId: string) => {
return database.collections.get<GroupModel>(GROUP).query(
Q.on(GROUP_TEAM, 'team_id', teamId),

View file

@ -98,7 +98,6 @@ export const getMarkdownTextStyles = makeStyleSheetFromTheme((theme: Theme) => {
fontFamily: 'OpenSans-Bold',
},
mention_highlight: {
backgroundColor: theme.mentionHighlightBg,
color: theme.mentionHighlightLink,
},
search_highlight: {