[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:
parent
2230fe8a70
commit
f8140f2117
8 changed files with 174 additions and 43 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -107,7 +107,7 @@ detox/detox_pixel_4_xl_api_30
|
|||
#editor-settings
|
||||
.vscode
|
||||
.scannerwork
|
||||
launch.json
|
||||
|
||||
# Notice.txt generation
|
||||
!build/notice-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) {
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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']),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -98,7 +98,6 @@ export const getMarkdownTextStyles = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
fontFamily: 'OpenSans-Bold',
|
||||
},
|
||||
mention_highlight: {
|
||||
backgroundColor: theme.mentionHighlightBg,
|
||||
color: theme.mentionHighlightLink,
|
||||
},
|
||||
search_highlight: {
|
||||
|
|
|
|||
Loading…
Reference in a new issue