From 826c6683f8d102d58a74baddc704677e85d0e4e0 Mon Sep 17 00:00:00 2001 From: Shaz Amjad Date: Thu, 17 Feb 2022 08:07:31 +1100 Subject: [PATCH] Gekidou - Sidebar websocket handling (#5954) * Adds actions, queries, db models and rest client * Adds Websocket handling for categories * fix merge * small refactor for categories websocket events * fix bad merge * Category delete handled * Handles deletion (prune on store) * Resolves feedback, adds team handling for prune * Minor fixes; param order and return, imports, type naming Co-authored-by: Elias Nahum --- app/actions/local/category.ts | 45 ++++++++++++++- app/actions/remote/category.ts | 4 +- app/actions/remote/channel.ts | 2 +- app/actions/websocket/category.ts | 96 +++++++++++++++++++++++++++++++ app/actions/websocket/index.ts | 14 +++++ app/actions/websocket/system.ts | 2 +- app/constants/websocket.ts | 4 ++ app/queries/servers/categories.ts | 16 +++++- app/utils/helpers.ts | 2 + 9 files changed, 177 insertions(+), 8 deletions(-) create mode 100644 app/actions/websocket/category.ts diff --git a/app/actions/local/category.ts b/app/actions/local/category.ts index e3b707036..6207106f1 100644 --- a/app/actions/local/category.ts +++ b/app/actions/local/category.ts @@ -3,11 +3,36 @@ import {Model} from '@nozbe/watermelondb'; -import {prepareCategories, prepareCategoryChannels} from '@app/queries/servers/categories'; import DatabaseManager from '@database/manager'; +import {prepareCategories, prepareCategoryChannels, queryCategoriesByTeamIds, queryCategoryById} from '@queries/servers/categories'; +import {pluckUnique} from '@utils/helpers'; -export const storeCategories = async (serverUrl: string, categories: CategoryWithChannels[], prepareRecordsOnly = false) => { +export const deleteCategory = async (serverUrl: string, categoryId: string) => { + const database = DatabaseManager.serverDatabases[serverUrl]?.database; + if (!database) { + return {error: `${serverUrl} database not found`}; + } + + try { + const category = await queryCategoryById(database, categoryId); + + if (category) { + await database.write(async () => { + await category.destroyPermanently(); + }); + } + + return {category}; + } catch (error) { + // eslint-disable-next-line no-console + console.log('FAILED TO DELETE CATEGORY', categoryId); + return {error}; + } +}; + +export const storeCategories = async (serverUrl: string, categories: CategoryWithChannels[], prune = false, prepareRecordsOnly = false) => { const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; + if (!operator) { return {error: `${serverUrl} database not found`}; } @@ -25,6 +50,22 @@ export const storeCategories = async (serverUrl: string, categories: CategoryWit const models = await Promise.all(modelPromises); const flattenedModels = models.flat() as Model[]; + if (prune && categories.length) { + const {database} = operator; + const remoteCategoryIds = categories.map((cat) => cat.id); + + // If the passed categories have more than one team, we want to update across teams + const teamIds = pluckUnique('team_id')(categories) as string[]; + const localCategories = await queryCategoriesByTeamIds(database, teamIds); + + localCategories.forEach((localCategory) => { + if (!remoteCategoryIds.includes(localCategory.id)) { + localCategory.prepareDestroyPermanently(); + flattenedModels.push(localCategory); + } + }); + } + if (prepareRecordsOnly) { return {models: flattenedModels}; } diff --git a/app/actions/remote/category.ts b/app/actions/remote/category.ts index f483c35bb..47e24b3f0 100644 --- a/app/actions/remote/category.ts +++ b/app/actions/remote/category.ts @@ -13,7 +13,7 @@ export type CategoriesRequest = { error?: unknown; } -export const fetchCategories = async (serverUrl: string, teamId: string, fetchOnly = false): Promise => { +export const fetchCategories = async (serverUrl: string, teamId: string, prune = false, fetchOnly = false): Promise => { let client: Client; try { client = NetworkManager.getClient(serverUrl); @@ -25,7 +25,7 @@ export const fetchCategories = async (serverUrl: string, teamId: string, fetchOn const {categories} = await client.getCategories('me', teamId); if (!fetchOnly) { - storeCategories(serverUrl, categories); + storeCategories(serverUrl, categories, prune); } return {categories}; diff --git a/app/actions/remote/channel.ts b/app/actions/remote/channel.ts index 57b857531..0603edeb7 100644 --- a/app/actions/remote/channel.ts +++ b/app/actions/remote/channel.ts @@ -220,7 +220,7 @@ export const fetchMyChannelsForTeam = async (serverUrl: string, teamId: string, if (!fetchOnly) { storeMyChannelsForTeam(serverUrl, teamId, channels, memberships); - storeCategories(serverUrl, categories); + storeCategories(serverUrl, categories, true); // Re-sync } return {channels, memberships, categories}; diff --git a/app/actions/websocket/category.ts b/app/actions/websocket/category.ts new file mode 100644 index 000000000..76d25b343 --- /dev/null +++ b/app/actions/websocket/category.ts @@ -0,0 +1,96 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {deleteCategory, storeCategories} from '@actions/local/category'; +import {fetchCategories} from '@actions/remote/category'; +import DatabaseManager from '@database/manager'; +import {queryCategoriesById} from '@queries/servers/categories'; + +type WebsocketCategoriesMessage = { + broadcast: { + team_id: string; + }; + data: { + team_id: string; + category?: string; + category_id: string; + updatedCategories?: string; + order?: string[]; + }; +} + +const addOrUpdateCategories = async (serverUrl: string, categories: CategoryWithChannels[]) => { + try { + storeCategories(serverUrl, categories); + } catch (e) { + // eslint-disable-next-line no-console + console.log('Category WS: addOrUpdateCategories', e, categories); + } +}; + +export async function handleCategoryCreatedEvent(serverUrl: string, msg: WebsocketCategoriesMessage) { + let category; + try { + category = JSON.parse(msg.data.category!); + addOrUpdateCategories(serverUrl, [category]); + } catch (e) { + // eslint-disable-next-line no-console + console.log('Category WS: handleCategoryCreatedEvent', e, msg); + fetchCategories(serverUrl, msg.broadcast.team_id); + } +} + +export async function handleCategoryUpdatedEvent(serverUrl: string, msg: WebsocketCategoriesMessage) { + let categories; + + try { + categories = JSON.parse(msg.data.updatedCategories!); + addOrUpdateCategories(serverUrl, categories); + } catch (e) { + // eslint-disable-next-line no-console + console.log('Category WS: handleCategoryUpdatedEvent', e, msg); + fetchCategories(serverUrl, msg.broadcast.team_id, true); + } +} + +export async function handleCategoryDeletedEvent(serverUrl: string, msg: WebsocketCategoriesMessage) { + try { + // Delete the Category + await deleteCategory(serverUrl, msg.data.category_id); + + // Fetch the categories again as channels will have moved + fetchCategories(serverUrl, msg.broadcast.team_id); + } catch (e) { + // eslint-disable-next-line no-console + console.log('Category WS: handleCategoryDeletedEvent', e, msg); + } +} + +export async function handleCategoryOrderUpdatedEvent(serverUrl: string, msg: WebsocketCategoriesMessage) { + try { + const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; + + if (!operator) { + return; + } + + const {database} = operator; + + // Update category order + if (msg.data.order?.length) { + const order = msg.data.order; + const categories = await queryCategoriesById(database, order); + categories.forEach((c) => { + const findOrder = (id: string) => id === c.id; + c.prepareUpdate(() => { + c.sortOrder = order.findIndex(findOrder); + }); + }); + await operator.batchRecords(categories); + } + } catch (e) { + // eslint-disable-next-line no-console + console.log('Category WS: handleCategoryOrderUpdatedEvent', e, msg); + fetchCategories(serverUrl, msg.data.team_id); + } +} diff --git a/app/actions/websocket/index.ts b/app/actions/websocket/index.ts index 2af68827f..89453922f 100644 --- a/app/actions/websocket/index.ts +++ b/app/actions/websocket/index.ts @@ -19,6 +19,7 @@ import {queryCommonSystemValues, queryConfig, queryCurrentChannelId, queryWebSoc import {deleteMyTeams, queryTeamsById} from '@queries/servers/team'; import {isTablet} from '@utils/helpers'; +import {handleCategoryCreatedEvent, handleCategoryDeletedEvent, handleCategoryOrderUpdatedEvent, handleCategoryUpdatedEvent} from './category'; import {handleChannelDeletedEvent, handleUserAddedToChannelEvent, handleUserRemovedFromChannelEvent} from './channel'; import {handleNewPostEvent, handlePostDeleted, handlePostEdited, handlePostUnread} from './posts'; import {handlePreferenceChangedEvent, handlePreferencesChangedEvent, handlePreferencesDeletedEvent} from './preferences'; @@ -243,6 +244,19 @@ export async function handleEvent(serverUrl: string, msg: WebSocketMessage) { handleTeamMemberRoleUpdatedEvent(serverUrl, msg); break; + case WebsocketEvents.CATEGORY_CREATED: + handleCategoryCreatedEvent(serverUrl, msg); + break; + case WebsocketEvents.CATEGORY_UPDATED: + handleCategoryUpdatedEvent(serverUrl, msg); + break; + case WebsocketEvents.CATEGORY_ORDER_UPDATED: + handleCategoryOrderUpdatedEvent(serverUrl, msg); + break; + case WebsocketEvents.CATEGORY_DELETED: + handleCategoryDeletedEvent(serverUrl, msg); + break; + case WebsocketEvents.CHANNEL_CREATED: break; diff --git a/app/actions/websocket/system.ts b/app/actions/websocket/system.ts index 857e8516f..826da5698 100644 --- a/app/actions/websocket/system.ts +++ b/app/actions/websocket/system.ts @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {SYSTEM_IDENTIFIERS} from '@app/constants/database'; +import {SYSTEM_IDENTIFIERS} from '@constants/database'; import DatabaseManager from '@database/manager'; export async function handleLicenseChangedEvent(serverUrl: string, msg: WebSocketMessage): Promise { diff --git a/app/constants/websocket.ts b/app/constants/websocket.ts index f56425b9b..b7191e345 100644 --- a/app/constants/websocket.ts +++ b/app/constants/websocket.ts @@ -5,6 +5,10 @@ const WebsocketEvents = { POST_EDITED: 'post_edited', POST_DELETED: 'post_deleted', POST_UNREAD: 'post_unread', + CATEGORY_CREATED: 'sidebar_category_created', + CATEGORY_UPDATED: 'sidebar_category_updated', + CATEGORY_DELETED: 'sidebar_category_deleted', + CATEGORY_ORDER_UPDATED: 'sidebar_category_order_updated', CHANNEL_CONVERTED: 'channel_converted', CHANNEL_CREATED: 'channel_created', CHANNEL_DELETED: 'channel_deleted', diff --git a/app/queries/servers/categories.ts b/app/queries/servers/categories.ts index c136038be..25a0e00f7 100644 --- a/app/queries/servers/categories.ts +++ b/app/queries/servers/categories.ts @@ -46,6 +46,15 @@ export const queryCategoriesByTeamId = async (database: Database, teamId: string } }; +export const queryCategoriesByTeamIds = async (database: Database, teamIds: string[]): Promise => { + try { + const records = (await database.get(CATEGORY).query(Q.where('team_id', Q.oneOf(teamIds))).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( @@ -94,9 +103,12 @@ export const prepareCategoryChannels = ( }); }); - const categoryChannelRecords = operator.handleCategoryChannels({categoryChannels, prepareRecordsOnly: true}); + if (categoryChannels.length) { + const categoryChannelRecords = operator.handleCategoryChannels({categoryChannels, prepareRecordsOnly: true}); + return [categoryChannelRecords]; + } - return [categoryChannelRecords]; + return []; } catch (e) { return undefined; } diff --git a/app/utils/helpers.ts b/app/utils/helpers.ts index b88fb4867..fab3d5e62 100644 --- a/app/utils/helpers.ts +++ b/app/utils/helpers.ts @@ -137,3 +137,5 @@ export async function isTablet() { return false; } + +export const pluckUnique = (key: string) => (array: Array<{[key: string]: unknown}>) => Array.from(new Set(array.map((obj) => obj[key])));