From 9f5f73b2641292610cd76a7d24a17d3b5307858b Mon Sep 17 00:00:00 2001 From: Shaz Amjad Date: Sat, 12 Feb 2022 08:40:44 +1100 Subject: [PATCH] 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 --- app/actions/local/category.ts | 43 ++++++++ app/actions/remote/category.ts | 36 ++++++ app/actions/remote/channel.ts | 11 +- app/actions/remote/team.ts | 14 ++- app/client/rest/base.ts | 12 ++ app/client/rest/categories.ts | 31 ++++++ app/client/rest/index.ts | 3 + app/database/models/server/category.ts | 14 +-- .../models/server/category_channel.ts | 3 +- app/queries/servers/categories.ts | 103 ++++++++++++++++++ app/queries/servers/entry.ts | 13 +++ 11 files changed, 269 insertions(+), 14 deletions(-) create mode 100644 app/actions/local/category.ts create mode 100644 app/actions/remote/category.ts create mode 100644 app/client/rest/categories.ts create mode 100644 app/queries/servers/categories.ts diff --git a/app/actions/local/category.ts b/app/actions/local/category.ts new file mode 100644 index 000000000..e3b707036 --- /dev/null +++ b/app/actions/local/category.ts @@ -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> = []; + 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}; +}; diff --git a/app/actions/remote/category.ts b/app/actions/remote/category.ts new file mode 100644 index 000000000..f483c35bb --- /dev/null +++ b/app/actions/remote/category.ts @@ -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 => { + 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}; + } +}; diff --git a/app/actions/remote/channel.ts b/app/actions/remote/channel.ts index b38761d49..806a4cf45 100644 --- a/app/actions/remote/channel.ts +++ b/app/actions/remote/channel.ts @@ -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(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}; diff --git a/app/actions/remote/team.ts b/app/actions/remote/team.ts index 14ef9d59c..30cc2850e 100644 --- a/app/actions/remote/team.ts +++ b/app/actions/remote/team.ts @@ -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); } diff --git a/app/client/rest/base.ts b/app/client/rest/base.ts index 9cf403127..5cbe56029 100644 --- a/app/client/rest/base.ts +++ b/app/client/rest/base.ts @@ -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`; } diff --git a/app/client/rest/categories.ts b/app/client/rest/categories.ts new file mode 100644 index 000000000..08cdfd977 --- /dev/null +++ b/app/client/rest/categories.ts @@ -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; + getCategoriesOrder: (userId: string, teamId: string) => Promise; + getCategory: (userId: string, teamId: string, categoryId: string) => Promise; +} + +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; diff --git a/app/client/rest/index.ts b/app/client/rest/index.ts index 52444f3c6..36cb12f50 100644 --- a/app/client/rest/index.ts +++ b/app/client/rest/index.ts @@ -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, diff --git a/app/database/models/server/category.ts b/app/database/models/server/category.ts index d748d5c86..f79b1e978 100644 --- a/app/database/models/server/category.ts +++ b/app/database/models/server/category.ts @@ -67,19 +67,17 @@ export default class CategoryModel extends Model implements CategoryInterface { @children(CATEGORY_CHANNEL) categoryChannels!: Query; /** 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(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'), ); diff --git a/app/database/models/server/category_channel.ts b/app/database/models/server/category_channel.ts index 5b9563c91..0db4942f2 100644 --- a/app/database/models/server/category_channel.ts +++ b/app/database/models/server/category_channel.ts @@ -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; /** myChannel : The related myChannel */ - @immutableRelation(MY_CHANNEL, 'channel_id') myChannel!: Relation; + @immutableRelation(MY_CHANNEL, 'channel_id') myChannel!: Relation; } diff --git a/app/queries/servers/categories.ts b/app/queries/servers/categories.ts new file mode 100644 index 000000000..c136038be --- /dev/null +++ b/app/queries/servers/categories.ts @@ -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(CATEGORY).find(categoryId)); + return record; + } catch { + return undefined; + } +}; + +export const queryCategoriesById = async (database: Database, categoryIds: string[]): Promise => { + try { + const records = (await database.get(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 => { + try { + const records = (await database.get(CATEGORY).query(Q.where('type', type)).fetch()); + return records; + } catch { + return Promise.resolve([] as CategoryModel[]); + } +}; + +export const queryCategoriesByTeamId = async (database: Database, teamId: string): Promise => { + try { + const records = (await database.get(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 => { + try { + const records = await database.get(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 => { + try { + const records = await database.get(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; + } +}; diff --git a/app/queries/servers/entry.ts b/app/queries/servers/entry.ts index e3fbb1dbc..7906a4026 100644 --- a/app/queries/servers/entry.ts +++ b/app/queries/servers/entry.ts @@ -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) {