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 <nahumhbl@gmail.com>
This commit is contained in:
Shaz Amjad 2022-02-17 08:07:31 +11:00 committed by GitHub
parent b256f7fcce
commit 826c6683f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 177 additions and 8 deletions

View file

@ -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};
}

View file

@ -13,7 +13,7 @@ export type CategoriesRequest = {
error?: unknown;
}
export const fetchCategories = async (serverUrl: string, teamId: string, fetchOnly = false): Promise<CategoriesRequest> => {
export const fetchCategories = async (serverUrl: string, teamId: string, prune = false, fetchOnly = false): Promise<CategoriesRequest> => {
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};

View file

@ -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};

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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<void> {

View file

@ -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',

View file

@ -46,6 +46,15 @@ export const queryCategoriesByTeamId = async (database: Database, teamId: string
}
};
export const queryCategoriesByTeamIds = async (database: Database, teamIds: string[]): Promise<CategoryModel[]> => {
try {
const records = (await database.get<CategoryModel>(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<CategoryModel[]> => {
try {
const records = await database.get<CategoryModel>(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;
}

View file

@ -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])));