Gekidou - Sidebar - Adds actions, queries, db models and rest client (#5953)

* Adds actions, queries, db models and rest client

* move fetching categories inside fetchMyChannelsForTeam

* code cleanup

* deconstruct categoriesWithOrder

* DMs fetching for changing teams

* Fix bad merge and address feedback

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
Shaz Amjad 2022-02-12 08:40:44 +11:00 committed by GitHub
parent 0a45fd9e13
commit 9f5f73b264
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 269 additions and 14 deletions

View file

@ -0,0 +1,43 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Model} from '@nozbe/watermelondb';
import {prepareCategories, prepareCategoryChannels} from '@app/queries/servers/categories';
import DatabaseManager from '@database/manager';
export const storeCategories = async (serverUrl: string, categories: CategoryWithChannels[], prepareRecordsOnly = false) => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
const modelPromises: Array<Promise<Model[]>> = [];
const preparedCategories = prepareCategories(operator, categories);
if (preparedCategories) {
modelPromises.push(...preparedCategories);
}
const preparedCategoryChannels = prepareCategoryChannels(operator, categories);
if (preparedCategoryChannels) {
modelPromises.push(...preparedCategoryChannels);
}
const models = await Promise.all(modelPromises);
const flattenedModels = models.flat() as Model[];
if (prepareRecordsOnly) {
return {models: flattenedModels};
}
if (flattenedModels?.length > 0) {
try {
await operator.batchRecords(flattenedModels);
} catch (error) {
// eslint-disable-next-line no-console
console.log('FAILED TO BATCH CATEGORIES', error);
return {error};
}
}
return {models: flattenedModels};
};

View file

@ -0,0 +1,36 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {storeCategories} from '@actions/local/category';
import NetworkManager from '@init/network_manager';
import {forceLogoutIfNecessary} from './session';
import type {Client} from '@client/rest';
export type CategoriesRequest = {
categories?: CategoryWithChannels[];
error?: unknown;
}
export const fetchCategories = async (serverUrl: string, teamId: string, fetchOnly = false): Promise<CategoriesRequest> => {
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const {categories} = await client.getCategories('me', teamId);
if (!fetchOnly) {
storeCategories(serverUrl, categories);
}
return {categories};
} catch (error) {
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};

View file

@ -5,6 +5,7 @@
import {Model} from '@nozbe/watermelondb';
import {IntlShape} from 'react-intl';
import {storeCategories} from '@actions/local/category';
import {storeMyChannelsForTeam, switchToChannel} from '@actions/local/channel';
import {General} from '@constants';
import DatabaseManager from '@database/manager';
@ -30,6 +31,7 @@ import type MyTeamModel from '@typings/database/models/servers/my_team';
import type TeamModel from '@typings/database/models/servers/team';
export type MyChannelsRequest = {
categories?: CategoryWithChannels[];
channels?: Channel[];
memberships?: ChannelMembership[];
error?: unknown;
@ -195,16 +197,20 @@ export const fetchMyChannelsForTeam = async (serverUrl: string, teamId: string,
}
try {
let [channels, memberships]: [Channel[], ChannelMembership[]] = await Promise.all([
const [allChannels, channelMemberships, categoriesWithOrder] = await Promise.all([
client.getMyChannels(teamId, includeDeleted, since),
client.getMyChannelMembers(teamId),
client.getCategories('me', teamId),
]);
let channels = allChannels;
let memberships = channelMemberships;
if (excludeDirect) {
channels = channels.filter((c) => c.type !== General.GM_CHANNEL && c.type !== General.DM_CHANNEL);
}
const channelIds = new Set<string>(channels.map((c) => c.id));
const {categories} = categoriesWithOrder;
memberships = memberships.reduce((result: ChannelMembership[], m: ChannelMembership) => {
if (channelIds.has(m.channel_id)) {
result.push(m);
@ -214,9 +220,10 @@ export const fetchMyChannelsForTeam = async (serverUrl: string, teamId: string,
if (!fetchOnly) {
storeMyChannelsForTeam(serverUrl, teamId, channels, memberships);
storeCategories(serverUrl, categories);
}
return {channels, memberships};
return {channels, memberships, categories};
} catch (error) {
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};

View file

@ -9,7 +9,7 @@ import {Events} from '@constants';
import DatabaseManager from '@database/manager';
import NetworkManager from '@init/network_manager';
import {prepareMyChannelsForTeam, queryDefaultChannelForTeam} from '@queries/servers/channel';
import {prepareCommonSystemValues, queryCurrentTeamId} from '@queries/servers/system';
import {prepareCommonSystemValues, queryCurrentTeamId, queryWebSocketLastDisconnected} from '@queries/servers/system';
import {addTeamToTeamHistory, prepareDeleteTeam, prepareMyTeams, queryNthLastChannelFromTeam, queryTeamsById, syncTeamTable} from '@queries/servers/team';
import {isTablet} from '@utils/helpers';
@ -262,7 +262,12 @@ export const removeUserFromTeam = async (serverUrl: string, teamId: string, user
};
export const handleTeamChange = async (serverUrl: string, teamId: string) => {
const {operator, database} = DatabaseManager.serverDatabases[serverUrl];
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return;
}
const {database} = operator;
const currentTeamId = await queryCurrentTeamId(database);
if (currentTeamId === teamId) {
@ -292,7 +297,10 @@ export const handleTeamChange = async (serverUrl: string, teamId: string) => {
await operator.batchRecords(models);
}
const {channels, memberships, error} = await fetchMyChannelsForTeam(serverUrl, teamId);
// If WebSocket is not disconnected we fetch everything since this moment
const lastDisconnectedAt = (await queryWebSocketLastDisconnected(database)) || Date.now();
const {channels, memberships, error} = await fetchMyChannelsForTeam(serverUrl, teamId, true, lastDisconnectedAt, false, true);
if (error) {
DeviceEventEmitter.emit(Events.TEAM_LOAD_ERROR, serverUrl, error);
}

View file

@ -109,6 +109,18 @@ export default class ClientBase {
return `${this.getTeamMembersRoute(teamId)}/${userId}`;
}
getCategoriesRoute(userId: string, teamId: string) {
return `${this.getUserRoute(userId)}/teams/${teamId}/channels/categories`;
}
getCategoriesOrderRoute(userId: string, teamId: string) {
return `${this.getCategoriesRoute(userId, teamId)}/order`;
}
getCategoryRoute(userId: string, teamId: string, categoryId: string) {
return `${this.getCategoriesRoute(userId, teamId)}/${categoryId}`;
}
getChannelsRoute() {
return `${this.urlVersion}/channels`;
}

View file

@ -0,0 +1,31 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export interface ClientCategoriesMix {
getCategories: (userId: string, teamId: string) => Promise<CategoriesWithOrder>;
getCategoriesOrder: (userId: string, teamId: string) => Promise<string[]>;
getCategory: (userId: string, teamId: string, categoryId: string) => Promise<Category>;
}
const ClientCategories = (superclass: any) => class extends superclass {
getCategories = async (userId: string, teamId: string) => {
return this.doFetch(
`${this.getCategoriesRoute(userId, teamId)}`,
{method: 'get'},
);
};
getCategoriesOrder = async (userId: string, teamId: string) => {
return this.doFetch(
`${this.getCategoriesOrderRoute(userId, teamId)}`,
{method: 'get'},
);
};
getCategory = async (userId: string, teamId: string, categoryId: string) => {
return this.doFetch(
`${this.getCategoryRoute(userId, teamId, categoryId)}`,
{method: 'get'},
);
};
};
export default ClientCategories;

View file

@ -5,6 +5,7 @@ import mix from '@utils/mix';
import ClientApps, {ClientAppsMix} from './apps';
import ClientBase from './base';
import ClientCategories, {ClientCategoriesMix} from './categories';
import ClientChannels, {ClientChannelsMix} from './channels';
import {DEFAULT_LIMIT_AFTER, DEFAULT_LIMIT_BEFORE, HEADER_X_VERSION_ID} from './constants';
import ClientEmojis, {ClientEmojisMix} from './emojis';
@ -22,6 +23,7 @@ import type {APIClientInterface} from '@mattermost/react-native-network-client';
interface Client extends ClientBase,
ClientAppsMix,
ClientCategoriesMix,
ClientChannelsMix,
ClientEmojisMix,
ClientFilesMix,
@ -37,6 +39,7 @@ interface Client extends ClientBase,
class Client extends mix(ClientBase).with(
ClientApps,
ClientCategories,
ClientChannels,
ClientEmojis,
ClientFiles,

View file

@ -67,19 +67,17 @@ export default class CategoryModel extends Model implements CategoryInterface {
@children(CATEGORY_CHANNEL) categoryChannels!: Query<CategoryChannelModel>;
/** categoryChannelsBySortOrder : Retrieves assocated category channels sorted by sort_order */
@lazy categoryChannelsBySortOrder = this.categoryChannels.collection.query(Q.sortBy('sort_order', Q.asc));
@lazy categoryChannelsBySortOrder = this.categoryChannels.collection.query(
Q.where('category_id', this.id),
Q.sortBy('sort_order', Q.asc),
);
/** channels : Retrieves all the channels that are part of this category */
@lazy channels = this.collections.
get<ChannelModel>(CHANNEL).
query(
Q.experimentalJoinTables([CHANNEL, CATEGORY_CHANNEL]),
Q.on(CATEGORY_CHANNEL,
Q.and(
Q.on(CHANNEL, Q.where('delete_at', Q.eq(0))),
Q.where('category_id', this.id),
),
),
Q.on(CATEGORY_CHANNEL, 'category_id', this.id),
Q.where('delete_at', Q.eq(0)),
Q.sortBy('display_name'),
);

View file

@ -10,6 +10,7 @@ import {MM_TABLES} from '@constants/database';
import type CategoryModel from '@typings/database/models/servers/category';
import type CategoryChannelInterface from '@typings/database/models/servers/category_channel';
import type ChannelModel from '@typings/database/models/servers/channel';
import type MyChannelModel from '@typings/database/models/servers/my_channel';
const {CATEGORY_CHANNEL, CATEGORY, MY_CHANNEL, CHANNEL} = MM_TABLES.SERVER;
@ -50,5 +51,5 @@ export default class CategoryChannelModel extends Model implements CategoryChann
@immutableRelation(CHANNEL, 'channel_id') channel!: Relation<ChannelModel>;
/** myChannel : The related myChannel */
@immutableRelation(MY_CHANNEL, 'channel_id') myChannel!: Relation<ChannelModel>;
@immutableRelation(MY_CHANNEL, 'channel_id') myChannel!: Relation<MyChannelModel>;
}

View file

@ -0,0 +1,103 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Database, Q} from '@nozbe/watermelondb';
import {MM_TABLES} from '@constants/database';
import type ServerDataOperator from '@database/operator/server_data_operator';
import type CategoryModel from '@typings/database/models/servers/category';
const {SERVER: {CATEGORY}} = MM_TABLES;
export const queryCategoryById = async (database: Database, categoryId: string) => {
try {
const record = (await database.collections.get<CategoryModel>(CATEGORY).find(categoryId));
return record;
} catch {
return undefined;
}
};
export const queryCategoriesById = async (database: Database, categoryIds: string[]): Promise<CategoryModel[]> => {
try {
const records = (await database.get<CategoryModel>(CATEGORY).query(Q.where('id', Q.oneOf(categoryIds))).fetch());
return records;
} catch {
return Promise.resolve([] as CategoryModel[]);
}
};
export const queryCategoriesByType = async (database: Database, type: CategoryType): Promise<CategoryModel[]> => {
try {
const records = (await database.get<CategoryModel>(CATEGORY).query(Q.where('type', type)).fetch());
return records;
} catch {
return Promise.resolve([] as CategoryModel[]);
}
};
export const queryCategoriesByTeamId = async (database: Database, teamId: string): Promise<CategoryModel[]> => {
try {
const records = (await database.get<CategoryModel>(CATEGORY).query(Q.where('team_id', teamId)).fetch());
return records;
} catch {
return Promise.resolve([] as CategoryModel[]);
}
};
export const queryCategoriesByTypeTeamId = async (database: Database, type: CategoryType, teamId: string): Promise<CategoryModel[]> => {
try {
const records = await database.get<CategoryModel>(CATEGORY).query(
Q.where('team_id', teamId),
Q.where('type', type),
).fetch();
return records;
} catch {
return Promise.resolve([] as CategoryModel[]);
}
};
export const queryAllCategories = async (database: Database): Promise<CategoryModel[]> => {
try {
const records = await database.get<CategoryModel>(CATEGORY).query().fetch();
return records;
} catch {
return Promise.resolve([] as CategoryModel[]);
}
};
export const prepareCategories = (operator: ServerDataOperator, categories: CategoryWithChannels[]) => {
try {
const categoryRecords = operator.handleCategories({categories, prepareRecordsOnly: true});
return [categoryRecords];
} catch {
return undefined;
}
};
export const prepareCategoryChannels = (
operator: ServerDataOperator,
categories: CategoryWithChannels[],
) => {
try {
const categoryChannels: CategoryChannel[] = [];
categories.forEach((category) => {
category.channel_ids.forEach((channelId, index) => {
categoryChannels.push({
id: `${category.team_id}_${channelId}`,
category_id: category.id,
channel_id: channelId,
sort_order: index,
});
});
});
const categoryChannelRecords = operator.handleCategoryChannels({categoryChannels, prepareRecordsOnly: true});
return [categoryChannelRecords];
} catch (e) {
return undefined;
}
};

View file

@ -3,6 +3,7 @@
import ServerDataOperator from '@database/operator/server_data_operator';
import {prepareCategories, prepareCategoryChannels} from './categories';
import {prepareDeleteChannel, prepareMyChannelsForTeam} from './channel';
import {prepareMyPreferences} from './preference';
import {prepareDeleteTeam, prepareMyTeams} from './team';
@ -49,6 +50,18 @@ export const prepareModels = async ({operator, initialTeamId, removeTeams, remov
}
}
if (chData?.categories?.length) {
const categoryModels = prepareCategories(operator, chData.categories);
if (categoryModels) {
modelPromises.push(...categoryModels);
}
const categoryChannelModels = prepareCategoryChannels(operator, chData.categories);
if (categoryChannelModels) {
modelPromises.push(...categoryChannelModels);
}
}
if (initialTeamId && chData?.channels?.length) {
const channelModels = await prepareMyChannelsForTeam(operator, initialTeamId, chData.channels, chData.memberships || []);
if (channelModels) {