forked from Ivasoft/mattermost-mobile
[Gekidou] Groups + group-memberships deferred fetch (#6370)
* 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 * Rebased with PR feedback * Exclusion fix, plus id order * Tidies up iterations * Loops updated * Update app/database/operator/server_data_operator/handlers/group.ts Co-authored-by: Avinash Lingaloo <avinashlng1080@gmail.com> * PR Feedback: Remove unnecessary prepare/store methods * Newline ESLint error * Extracts out id generation for group-associations * Batches if not fetchOnly Co-authored-by: Avinash Lingaloo <avinashlng1080@gmail.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
|
||||
import {fetchFilteredChannelGroups, fetchFilteredTeamGroups, fetchGroupsForAutocomplete} from '@actions/remote/groups';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {prepareGroups, queryGroupsByName, queryGroupsByNameInChannel, queryGroupsByNameInTeam} from '@queries/servers/group';
|
||||
import {queryGroupsByName, queryGroupsByNameInChannel, queryGroupsByNameInTeam} from '@queries/servers/group';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
import type GroupModel from '@typings/database/models/servers/group';
|
||||
@@ -61,8 +61,7 @@ export const searchGroupsByNameInChannel = async (serverUrl: string, name: strin
|
||||
try {
|
||||
database = DatabaseManager.getServerDatabaseAndOperator(serverUrl).database;
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('searchGroupsByNameInChannel - DB Error', e);
|
||||
logError('searchGroupsByNameInChannel - DB Error', e);
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -79,25 +78,3 @@ export const searchGroupsByNameInChannel = async (serverUrl: string, name: strin
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Store fetched groups locally
|
||||
*
|
||||
* @param serverUrl string - The Server URL
|
||||
* @param groups Group[] - The groups fetched from the API
|
||||
*/
|
||||
export const storeGroups = async (serverUrl: string, groups: Group[]) => {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
||||
const preparedGroups = await prepareGroups(operator, groups);
|
||||
|
||||
if (preparedGroups.length) {
|
||||
operator.batchRecords(preparedGroups);
|
||||
}
|
||||
|
||||
return preparedGroups;
|
||||
} catch (e) {
|
||||
return {error: e};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ import {deleteMyTeams, getAvailableTeamIds, getNthLastChannelFromTeam, queryMyTe
|
||||
import {isDMorGM} from '@utils/channel';
|
||||
import {processIsCRTEnabled} from '@utils/thread';
|
||||
|
||||
import {fetchGroupsForMember} from '../groups';
|
||||
|
||||
import type ClientError from '@client/rest/error';
|
||||
|
||||
export type AppEntryData = {
|
||||
@@ -305,6 +307,10 @@ export async function deferredAppEntryActions(
|
||||
|
||||
await fetchAllTeams(serverUrl);
|
||||
await updateAllUsersSince(serverUrl, since);
|
||||
|
||||
// Fetch groups for current user
|
||||
fetchGroupsForMember(serverUrl, currentUserId);
|
||||
|
||||
setTimeout(async () => {
|
||||
if (channelsToFetchProfiles?.size) {
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config, license);
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {storeGroups} from '@actions/local/group';
|
||||
import {Client} from '@client/rest';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {prepareGroups} from '@queries/servers/group';
|
||||
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
|
||||
@@ -15,12 +13,7 @@ export const fetchGroupsForAutocomplete = async (serverUrl: string, query: strin
|
||||
const client: Client = NetworkManager.getClient(serverUrl);
|
||||
const response = await client.getGroups(query);
|
||||
|
||||
// Save locally
|
||||
if (!fetchOnly) {
|
||||
return storeGroups(serverUrl, response);
|
||||
}
|
||||
|
||||
return prepareGroups(operator, response);
|
||||
return operator.handleGroups({groups: response, prepareRecordsOnly: fetchOnly});
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
@@ -41,11 +34,7 @@ export const fetchGroupsByNames = async (serverUrl: string, names: string[], fet
|
||||
const groups = (await Promise.all(promises)).flat();
|
||||
|
||||
// Save locally
|
||||
if (!fetchOnly) {
|
||||
return storeGroups(serverUrl, groups);
|
||||
}
|
||||
|
||||
return prepareGroups(operator, groups);
|
||||
return operator.handleGroups({groups, prepareRecordsOnly: fetchOnly});
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
@@ -58,11 +47,7 @@ export const fetchGroupsForChannel = async (serverUrl: string, channelId: string
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const response = await client.getAllGroupsAssociatedToChannel(channelId);
|
||||
|
||||
if (!fetchOnly) {
|
||||
return storeGroups(serverUrl, response.groups);
|
||||
}
|
||||
|
||||
return prepareGroups(operator, response.groups);
|
||||
return operator.handleGroups({groups: response.groups, prepareRecordsOnly: fetchOnly});
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
@@ -75,17 +60,33 @@ export const fetchGroupsForTeam = async (serverUrl: string, teamId: string, fetc
|
||||
const client: Client = NetworkManager.getClient(serverUrl);
|
||||
const response = await client.getAllGroupsAssociatedToTeam(teamId);
|
||||
|
||||
if (!fetchOnly) {
|
||||
return storeGroups(serverUrl, response.groups);
|
||||
}
|
||||
|
||||
return prepareGroups(operator, response.groups);
|
||||
return operator.handleGroups({groups: response.groups, prepareRecordsOnly: fetchOnly});
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchGroupsForMember = async (serverUrl: string, userId: string, fetchOnly = false) => {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
||||
const client: Client = NetworkManager.getClient(serverUrl);
|
||||
const response = await client.getAllGroupsAssociatedToMembership(userId);
|
||||
|
||||
const groups = await operator.handleGroups({groups: response, prepareRecordsOnly: true});
|
||||
const groupMemberships = await operator.handleGroupMembershipsForMember({groups: response, userId, prepareRecordsOnly: true});
|
||||
|
||||
if (!fetchOnly) {
|
||||
await operator.batchRecords([...groups, ...groupMemberships]);
|
||||
}
|
||||
|
||||
return {groups, groupMemberships};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchFilteredTeamGroups = async (serverUrl: string, searchTerm: string, teamId: string) => {
|
||||
try {
|
||||
const groups = await fetchGroupsForTeam(serverUrl, teamId);
|
||||
|
||||
@@ -8,10 +8,10 @@ import {PER_PAGE_DEFAULT} from './constants';
|
||||
export interface ClientGroupsMix {
|
||||
getGroups: (query?: string, filterAllowReference?: boolean, page?: number, perPage?: number, since?: number) => Promise<Group[]>;
|
||||
getAllGroupsAssociatedToChannel: (channelId: string, filterAllowReference?: boolean) => Promise<{groups: Group[]; total_group_count: number}>;
|
||||
getAllGroupsAssociatedToMembership: (userId: string, filterAllowReference?: boolean) => Promise<{groups: Group[]; total_group_count: number}>;
|
||||
getAllGroupsAssociatedToMembership: (userId: string, filterAllowReference?: boolean) => Promise<Group[]>;
|
||||
getAllGroupsAssociatedToTeam: (teamId: string, filterAllowReference?: boolean) => Promise<{groups: Group[]; total_group_count: number}>;
|
||||
getAllChannelsAssociatedToGroup: (groupId: string, filterAllowReference?: boolean) => Promise<{groupChannels: GroupChannel[]}>;
|
||||
getAllMembershipsAssociatedToGroup: (groupId: string, filterAllowReference?: boolean) => Promise<{groupMemberships: GroupMembership; total_member_count: number}>;
|
||||
getAllMembershipsAssociatedToGroup: (groupId: string, filterAllowReference?: boolean) => Promise<{groupMemberships: UserProfile[]; total_member_count: number}>;
|
||||
getAllTeamsAssociatedToGroup: (groupId: string, filterAllowReference?: boolean) => Promise<{groupTeams: GroupTeam[]}>;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import {bottomSheetSnapPoint} from '@utils/helpers';
|
||||
import {displayUsername, getUsersByUsername} from '@utils/user';
|
||||
|
||||
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 = {
|
||||
@@ -39,6 +40,7 @@ type AtMentionProps = {
|
||||
textStyle?: StyleProp<TextStyle>;
|
||||
users: UserModelType[];
|
||||
groups: GroupModel[];
|
||||
groupMemberships: GroupMembershipModel[];
|
||||
}
|
||||
|
||||
const {SERVER: {GROUP, USER}} = MM_TABLES;
|
||||
@@ -62,6 +64,7 @@ const AtMention = ({
|
||||
textStyle,
|
||||
users,
|
||||
groups,
|
||||
groupMemberships,
|
||||
}: AtMentionProps) => {
|
||||
const intl = useIntl();
|
||||
const managedConfig = useManagedConfig<ManagedConfig>();
|
||||
@@ -223,6 +226,7 @@ const AtMention = ({
|
||||
canPress = true;
|
||||
} else if (group?.name) {
|
||||
mention = group.name;
|
||||
highlighted = groupMemberships.some((gm) => gm.groupId === group.id);
|
||||
isMention = true;
|
||||
canPress = false;
|
||||
} else {
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {queryGroupsByName} from '@app/queries/servers/group';
|
||||
import {queryGroupsByName, queryGroupMembershipForMember} from '@queries/servers/group';
|
||||
import {observeCurrentUserId} from '@queries/servers/system';
|
||||
import {observeTeammateNameDisplay, queryUsersLike} from '@queries/servers/user';
|
||||
|
||||
@@ -26,6 +27,7 @@ const enhance = withObservables(['mentionName'], ({database, mentionName}: {ment
|
||||
teammateNameDisplay,
|
||||
users: queryUsersLike(database, mn).observeWithColumns(['username']),
|
||||
groups: queryGroupsByName(database, mn).observeWithColumns(['name']),
|
||||
groupMemberships: currentUserId.pipe(switchMap((userId) => queryGroupMembershipForMember(database, userId).observe())),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -2,16 +2,21 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {transformGroupRecord} from '@database/operator/server_data_operator/transformers/group';
|
||||
import {transformGroupMembershipRecord, transformGroupRecord} from '@database/operator/server_data_operator/transformers/group';
|
||||
import {getUniqueRawsBy} from '@database/operator/utils/general';
|
||||
import {queryGroupMembershipForMember} from '@queries/servers/group';
|
||||
import {generateGroupAssociationId} from '@utils/groups';
|
||||
import {logWarning} from '@utils/log';
|
||||
|
||||
import type {HandleGroupArgs} from '@typings/database/database';
|
||||
import type {HandleGroupArgs, HandleGroupMembershipForMemberArgs} from '@typings/database/database';
|
||||
import type GroupModel from '@typings/database/models/servers/group';
|
||||
import type GroupMembershipModel from '@typings/database/models/servers/group_membership';
|
||||
|
||||
const {GROUP, GROUP_MEMBERSHIP} = MM_TABLES.SERVER;
|
||||
|
||||
const {GROUP} = MM_TABLES.SERVER;
|
||||
export interface GroupHandlerMix {
|
||||
handleGroups: ({groups, prepareRecordsOnly}: HandleGroupArgs) => Promise<GroupModel[]>;
|
||||
handleGroupMembershipsForMember: ({userId, groups, prepareRecordsOnly}: HandleGroupMembershipForMemberArgs) => Promise<GroupMembershipModel[]>;
|
||||
}
|
||||
|
||||
const GroupHandler = (superclass: any) => class extends superclass implements GroupHandlerMix {
|
||||
@@ -41,6 +46,66 @@ const GroupHandler = (superclass: any) => class extends superclass implements Gr
|
||||
prepareRecordsOnly,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* handleGroupMembershipsForMember: Handler responsible for the Create/Update operations occurring on the GroupMembership table from the 'Server' schema
|
||||
* @param {string} userId
|
||||
* @param {HandleGroupMembershipForMemberArgs} groupMembershipsArgs
|
||||
* @param {GroupMembership[]} groupMembershipsArgs.groupMemberships
|
||||
* @param {boolean} groupMembershipsArgs.prepareRecordsOnly
|
||||
* @throws DataOperatorException
|
||||
* @returns {Promise<GroupMembershipModel[]>}
|
||||
*/
|
||||
handleGroupMembershipsForMember = async ({userId, groups, prepareRecordsOnly = true}: HandleGroupMembershipForMemberArgs): Promise<GroupMembershipModel[]> => {
|
||||
// Get existing group memberships
|
||||
const existingGroupMemberships = await queryGroupMembershipForMember(this.database, userId).fetch();
|
||||
|
||||
let records: GroupMembershipModel[] = [];
|
||||
let rawValues: GroupMembership[] = [];
|
||||
|
||||
// Nothing to add or remove
|
||||
if (!groups?.length && !existingGroupMemberships.length) {
|
||||
return records;
|
||||
} else if (!groups?.length && existingGroupMemberships.length) { // No groups - remove all existing ones
|
||||
records = existingGroupMemberships.map((gm) => gm.prepareDestroyPermanently());
|
||||
} else if (groups?.length && !existingGroupMemberships.length) { // No existing groups - add all new ones
|
||||
rawValues = groups.map((g) => ({id: generateGroupAssociationId(g.id, userId), user_id: userId, group_id: g.id}));
|
||||
} else if (groups?.length && existingGroupMemberships.length) { // If both, we only want to save new ones and delete one's no longer in groups
|
||||
const groupsSet: {[key: string]: GroupMembership} = {};
|
||||
|
||||
for (const g of groups) {
|
||||
groupsSet[`${g.id}`] = {id: generateGroupAssociationId(g.id, userId), user_id: userId, group_id: g.id};
|
||||
}
|
||||
|
||||
for (const gm of existingGroupMemberships) {
|
||||
// Check if existingGroups overlaps with groups
|
||||
if (groupsSet[gm.groupId]) {
|
||||
// If there is an existing group already, we don't need to add it
|
||||
delete groupsSet[gm.groupId];
|
||||
} else {
|
||||
// No group? Remove existing one
|
||||
records.push(gm.prepareDestroyPermanently());
|
||||
}
|
||||
}
|
||||
|
||||
rawValues.push(...Object.values(groupsSet));
|
||||
}
|
||||
|
||||
records.push(...(await this.handleRecords({
|
||||
fieldName: 'id',
|
||||
transformer: transformGroupMembershipRecord,
|
||||
rawValues,
|
||||
tableName: GROUP_MEMBERSHIP,
|
||||
prepareRecordsOnly: true,
|
||||
})));
|
||||
|
||||
// Batch update if there are records
|
||||
if (records.length && !prepareRecordsOnly) {
|
||||
await this.batchRecords(records);
|
||||
}
|
||||
|
||||
return records;
|
||||
};
|
||||
};
|
||||
|
||||
export default GroupHandler;
|
||||
|
||||
@@ -5,12 +5,15 @@
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {prepareBaseRecord} from '@database/operator/server_data_operator/transformers/index';
|
||||
import {OperationType} from '@typings/database/enums';
|
||||
import {generateGroupAssociationId} from '@utils/groups';
|
||||
|
||||
import type {TransformerArgs} from '@typings/database/database';
|
||||
import type GroupModel from '@typings/database/models/servers/group';
|
||||
import type GroupMembershipModel from '@typings/database/models/servers/group_membership';
|
||||
|
||||
const {
|
||||
GROUP,
|
||||
GROUP_MEMBERSHIP,
|
||||
} = MM_TABLES.SERVER;
|
||||
|
||||
/**
|
||||
@@ -42,3 +45,29 @@ export const transformGroupRecord = ({action, database, value}: TransformerArgs)
|
||||
fieldsMapper,
|
||||
}) as Promise<GroupModel>;
|
||||
};
|
||||
|
||||
/**
|
||||
* transformGroupMembershipRecord: Prepares a record of the SERVER database 'GroupMembership' table for update or create actions.
|
||||
* @param {TransformerArgs} operator
|
||||
* @param {Database} operator.database
|
||||
* @param {RecordPair} operator.value
|
||||
* @returns {Promise<GroupMembershipModel>}
|
||||
*/
|
||||
export const transformGroupMembershipRecord = ({action, database, value}: TransformerArgs): Promise<GroupMembershipModel> => {
|
||||
const raw = value.raw as GroupMembership;
|
||||
|
||||
// id of group comes from server response
|
||||
const fieldsMapper = (model: GroupMembershipModel) => {
|
||||
model._raw.id = raw.id || generateGroupAssociationId(raw.group_id, raw.user_id);
|
||||
model.groupId = raw.group_id;
|
||||
model.userId = raw.user_id;
|
||||
};
|
||||
|
||||
return prepareBaseRecord({
|
||||
action,
|
||||
database,
|
||||
tableName: GROUP_MEMBERSHIP,
|
||||
value,
|
||||
fieldsMapper,
|
||||
}) as Promise<GroupMembershipModel>;
|
||||
};
|
||||
|
||||
@@ -5,10 +5,10 @@ import {Database, Q} from '@nozbe/watermelondb';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import type GroupModel from '@typings/database/models/servers/group';
|
||||
import type GroupMembershipModel from '@typings/database/models/servers/group_membership';
|
||||
|
||||
const {SERVER: {GROUP, GROUP_CHANNEL, GROUP_TEAM}} = MM_TABLES;
|
||||
const {SERVER: {GROUP, GROUP_CHANNEL, GROUP_MEMBERSHIP, GROUP_TEAM}} = MM_TABLES;
|
||||
|
||||
export const queryGroupsByName = (database: Database, name: string) => {
|
||||
return database.collections.get<GroupModel>(GROUP).query(
|
||||
@@ -36,6 +36,8 @@ export const queryGroupsByNameInChannel = (database: Database, name: string, cha
|
||||
);
|
||||
};
|
||||
|
||||
export const prepareGroups = (operator: ServerDataOperator, groups: Group[]) => {
|
||||
return operator.handleGroups({groups, prepareRecordsOnly: true});
|
||||
export const queryGroupMembershipForMember = (database: Database, userId: string) => {
|
||||
return database.collections.get<GroupMembershipModel>(GROUP_MEMBERSHIP).query(
|
||||
Q.where('user_id', userId),
|
||||
);
|
||||
};
|
||||
|
||||
4
app/utils/groups.ts
Normal file
4
app/utils/groups.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export const generateGroupAssociationId = (groupId: string, otherId: string) => `${groupId}-${otherId}`;
|
||||
6
types/api/groups.d.ts
vendored
6
types/api/groups.d.ts
vendored
@@ -42,4 +42,8 @@ type GroupChannel = {
|
||||
update_at: number;
|
||||
}
|
||||
|
||||
type GroupMembership = UserProfile[]
|
||||
type GroupMembership = {
|
||||
id?: string;
|
||||
group_id: string;
|
||||
user_id: string;
|
||||
}
|
||||
|
||||
5
types/database/database.d.ts
vendored
5
types/database/database.d.ts
vendored
@@ -222,6 +222,11 @@ export type HandleGroupArgs = PrepareOnly & {
|
||||
groups?: Group[];
|
||||
};
|
||||
|
||||
export type HandleGroupMembershipForMemberArgs = PrepareOnly & {
|
||||
userId: string;
|
||||
groups?: Group[];
|
||||
}
|
||||
|
||||
export type HandleCategoryChannelArgs = PrepareOnly & {
|
||||
categoryChannels?: CategoryChannel[];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user