From afa801434093b5d1e35bc00790d32bc196db9b43 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Tue, 9 Nov 2021 16:48:02 -0300 Subject: [PATCH 1/9] Add endpoint for fetching buttons --- .../endpoints/actionButtonsHandler.ts | 16 ++++++++++++++++ app/apps/server/communication/rest.js | 4 ++++ 2 files changed, 20 insertions(+) create mode 100644 app/apps/server/communication/endpoints/actionButtonsHandler.ts diff --git a/app/apps/server/communication/endpoints/actionButtonsHandler.ts b/app/apps/server/communication/endpoints/actionButtonsHandler.ts new file mode 100644 index 000000000000..9cdab5431c94 --- /dev/null +++ b/app/apps/server/communication/endpoints/actionButtonsHandler.ts @@ -0,0 +1,16 @@ +import { AppManager } from '@rocket.chat/apps-engine/server/AppManager'; + +import { API } from '../../../../api/server'; +import { AppsRestApi } from '../rest'; + +export const actionButtonsHandler = (apiManager: AppsRestApi): [Record, Record] => [{ + authRequired: false, +}, { + get(): any { + const manager = apiManager._manager as AppManager; + + const buttons = manager.getUIActionButtonManager().getAllActionButtons(); + + return API.v1.success(buttons); + }, +}]; diff --git a/app/apps/server/communication/rest.js b/app/apps/server/communication/rest.js index 6d84f3797b95..6dfa11e86c6f 100644 --- a/app/apps/server/communication/rest.js +++ b/app/apps/server/communication/rest.js @@ -9,6 +9,7 @@ import { Info } from '../../../utils'; import { Settings, Users } from '../../../models/server'; import { Apps } from '../orchestrator'; import { formatAppInstanceForRest } from '../../lib/misc/formatAppInstanceForRest'; +import { actionButtonsHandler } from './endpoints/actionButtonsHandler'; const appsEngineVersionForMarketplace = Info.marketplaceApiVersion.replace(/-.*/g, ''); const getDefaultHeaders = () => ({ @@ -60,6 +61,9 @@ export class AppsRestApi { return API.v1.failure(); }; + this.api.addRoute('actionButtons', ...actionButtonsHandler(this)); + + // WE NEED TO MOVE EACH ENDPOINT HANDLER TO IT'S OWN FILE this.api.addRoute('', { authRequired: true, permissionsRequired: ['manage-apps'] }, { get() { const baseUrl = orchestrator.getMarketplaceUrl(); From b7a14a77007efd68ff0608ad8e8a2cc9582da0e0 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Tue, 9 Nov 2021 16:48:25 -0300 Subject: [PATCH 2/9] Add a sync manager --- app/ui-message/client/ActionButtonSyncer.ts | 66 +++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 app/ui-message/client/ActionButtonSyncer.ts diff --git a/app/ui-message/client/ActionButtonSyncer.ts b/app/ui-message/client/ActionButtonSyncer.ts new file mode 100644 index 000000000000..41ad083cdfe5 --- /dev/null +++ b/app/ui-message/client/ActionButtonSyncer.ts @@ -0,0 +1,66 @@ +import { Meteor } from 'meteor/meteor'; +import { IUIActionButton, UIActionButtonContext } from '@rocket.chat/apps-engine/definition/ui'; + +import { APIClient } from '../../utils/client'; +import * as TabBar from './actionButtons/tabbar'; + +let registeredButtons: Array; + +export const addButton = (button: IUIActionButton): void => { + switch (button.context) { + case UIActionButtonContext.MESSAGE_ACTION: + // onMessageActionAdded(button); + break; + case UIActionButtonContext.ROOM_ACTION: + TabBar.onAdded(button); + break; + } + + registeredButtons.push(Object.freeze(button)); +}; + +export const removeButton = (button: IUIActionButton): void => { + switch (button.context) { + case UIActionButtonContext.ROOM_ACTION: + TabBar.onRemoved(button); + break; + } +}; + +export const loadButtons = (): Promise => APIClient.get('apps/actionButtons') + .then((value: Array) => { + registeredButtons.forEach((button) => removeButton(button)); + registeredButtons = []; + value.map(addButton); + return registeredButtons; + }) + .then(console.log) + .catch(console.error); + +/** + * Returns an iterator so we preserve the original Array + * without needing to copy it + */ +export const getActionButtonsIterator = (filter?: (value: IUIActionButton) => boolean): IterableIterator => { + let index = 0; + + return { + next(): IteratorResult { + let value; + do { + if (index >= registeredButtons.length) { + return { done: true, value: undefined }; + } + + value = registeredButtons[index++]; + } while (filter && !filter(value)); + + return { done: false, value }; + }, + [Symbol.iterator](): IterableIterator { + return this; + }, + }; +}; + +Meteor.startup(() => loadButtons()); From 2416dabacabe7feaa0ec522d46161e52ed1686cc Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Tue, 9 Nov 2021 16:48:49 -0300 Subject: [PATCH 3/9] Add new websocket event --- app/apps/client/communication/websockets.js | 5 +++++ app/apps/server/bridges/activation.ts | 4 ++++ app/apps/server/communication/websockets.js | 13 ++++++++++++- app/ui-message/client/actionButtons/tabbar.ts | 17 +++++++++++++++++ 4 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 app/ui-message/client/actionButtons/tabbar.ts diff --git a/app/apps/client/communication/websockets.js b/app/apps/client/communication/websockets.js index 2539b94b7264..24468eb89f76 100644 --- a/app/apps/client/communication/websockets.js +++ b/app/apps/client/communication/websockets.js @@ -3,6 +3,7 @@ import { Emitter } from '@rocket.chat/emitter'; import { slashCommands, APIClient } from '../../../utils'; import { CachedCollectionManager } from '../../../ui-cached-collection'; +import { loadButtons } from '../../../ui-message/client/ActionButtonSyncer'; export const AppEvents = Object.freeze({ APP_ADDED: 'app/added', @@ -14,6 +15,7 @@ export const AppEvents = Object.freeze({ COMMAND_DISABLED: 'command/disabled', COMMAND_UPDATED: 'command/updated', COMMAND_REMOVED: 'command/removed', + ACTIONS_CHANGED: 'actions/changed', }); export class AppWebsocketReceiver extends Emitter { @@ -36,6 +38,7 @@ export class AppWebsocketReceiver extends Emitter { this.streamer.on(AppEvents.COMMAND_UPDATED, this.onCommandAddedOrUpdated); this.streamer.on(AppEvents.COMMAND_REMOVED, this.onCommandRemovedOrDisabled); this.streamer.on(AppEvents.COMMAND_DISABLED, this.onCommandRemovedOrDisabled); + this.streamer.on(AppEvents.COMMAND_DISABLED, this.onActionsChanged); } registerListener(event, listener) { @@ -55,4 +58,6 @@ export class AppWebsocketReceiver extends Emitter { onCommandRemovedOrDisabled = (command) => { delete slashCommands.commands[command]; } + + onActionsChanged = () => loadButtons(); } diff --git a/app/apps/server/bridges/activation.ts b/app/apps/server/bridges/activation.ts index ee2f5746fa5a..ac0a2654cae8 100644 --- a/app/apps/server/bridges/activation.ts +++ b/app/apps/server/bridges/activation.ts @@ -30,4 +30,8 @@ export class AppActivationBridge extends ActivationBridge { await this.orch.getNotifier().appStatusUpdated(app.getID(), status); } + + protected async actionsChanged(): Promise { + await this.orch.getNotifier().actionsChanged(); + } } diff --git a/app/apps/server/communication/websockets.js b/app/apps/server/communication/websockets.js index dd8da2b64c89..0ddc670373f2 100644 --- a/app/apps/server/communication/websockets.js +++ b/app/apps/server/communication/websockets.js @@ -13,6 +13,7 @@ export const AppEvents = Object.freeze({ COMMAND_DISABLED: 'command/disabled', COMMAND_UPDATED: 'command/updated', COMMAND_REMOVED: 'command/removed', + ACTIONS_CHANGED: 'actions/changed', }); export class AppServerListener { @@ -25,7 +26,8 @@ export class AppServerListener { this.engineStreamer.on(AppEvents.APP_STATUS_CHANGE, this.onAppStatusUpdated.bind(this)); this.engineStreamer.on(AppEvents.APP_REMOVED, this.onAppRemoved.bind(this)); this.engineStreamer.on(AppEvents.APP_UPDATED, this.onAppUpdated.bind(this)); - this.engineStreamer.on(AppEvents.APP_ADDED, this.onAppAdded.bind(this)); + this.engineStreamer.on(AppEvents.APP_ADDED, this.onAppActionsChanged.bind(this)); + this.engineStreamer.on(AppEvents.ACTIONS_CHANGED, this.onActionsChanged.bind(this)); this.engineStreamer.on(AppEvents.APP_SETTING_UPDATED, this.onAppSettingUpdated.bind(this)); this.engineStreamer.on(AppEvents.COMMAND_ADDED, this.onCommandAdded.bind(this)); @@ -102,6 +104,10 @@ export class AppServerListener { async onCommandRemoved(command) { this.clientStreamer.emitWithoutBroadcast(AppEvents.COMMAND_REMOVED, command); } + + async onActionsChanged() { + this.clientStreamer.emitWithoutBroadcast(AppEvents.ACTIONS_CHANGED); + } } export class AppServerNotifier { @@ -177,4 +183,9 @@ export class AppServerNotifier { this.engineStreamer.emit(AppEvents.COMMAND_REMOVED, command); this.clientStreamer.emitWithoutBroadcast(AppEvents.COMMAND_REMOVED, command); } + + async actionsChanged() { + this.engineStreamer.emit(AppEvents.ACTIONS_CHANGED); + this.clientStreamer.emitWithoutBroadcast(AppEvents.ACTIONS_CHANGED); + } } diff --git a/app/ui-message/client/actionButtons/tabbar.ts b/app/ui-message/client/actionButtons/tabbar.ts new file mode 100644 index 000000000000..6ed7a28f60f0 --- /dev/null +++ b/app/ui-message/client/actionButtons/tabbar.ts @@ -0,0 +1,17 @@ +import { IUIActionButton } from '@rocket.chat/apps-engine/definition/ui'; + +import { addAction, deleteAction } from '../../../../client/views/room/lib/Toolbox'; + +const getIdForActionButton = ({ appId, actionId }: IUIActionButton): string => `${ appId }/${ actionId }`; + +// eslint-disable-next-line no-void +export const onAdded = (button: IUIActionButton): void => void addAction(getIdForActionButton(button), { + id: button.actionId, + icon: '', + title: button.nameI18n as any, + // Introduce a mapper from Apps-engine's RoomTypes to these + // Determine what 'group' and 'team' are + groups: ['group', 'channel', 'live', 'team', 'direct', 'direct_multiple'], +}); + +export const onRemoved = (button: IUIActionButton): boolean => deleteAction(getIdForActionButton(button)); From b2cd4bb73fe01cd47eafa4559f88d70289bfb2a5 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Wed, 10 Nov 2021 18:46:11 -0300 Subject: [PATCH 4/9] Remove unnecessary call in action event --- app/apps/server/communication/websockets.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/apps/server/communication/websockets.js b/app/apps/server/communication/websockets.js index 0ddc670373f2..3d59adac8ee4 100644 --- a/app/apps/server/communication/websockets.js +++ b/app/apps/server/communication/websockets.js @@ -185,7 +185,6 @@ export class AppServerNotifier { } async actionsChanged() { - this.engineStreamer.emit(AppEvents.ACTIONS_CHANGED); this.clientStreamer.emitWithoutBroadcast(AppEvents.ACTIONS_CHANGED); } } From 4541a1d94d6c08c06a250beba3c4e19462691998 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Thu, 11 Nov 2021 18:18:09 -0300 Subject: [PATCH 5/9] Fix reference error --- app/apps/server/communication/websockets.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/apps/server/communication/websockets.js b/app/apps/server/communication/websockets.js index 3d59adac8ee4..0eb08b2f6b84 100644 --- a/app/apps/server/communication/websockets.js +++ b/app/apps/server/communication/websockets.js @@ -26,7 +26,7 @@ export class AppServerListener { this.engineStreamer.on(AppEvents.APP_STATUS_CHANGE, this.onAppStatusUpdated.bind(this)); this.engineStreamer.on(AppEvents.APP_REMOVED, this.onAppRemoved.bind(this)); this.engineStreamer.on(AppEvents.APP_UPDATED, this.onAppUpdated.bind(this)); - this.engineStreamer.on(AppEvents.APP_ADDED, this.onAppActionsChanged.bind(this)); + this.engineStreamer.on(AppEvents.APP_ADDED, this.onAppAdded.bind(this)); this.engineStreamer.on(AppEvents.ACTIONS_CHANGED, this.onActionsChanged.bind(this)); this.engineStreamer.on(AppEvents.APP_SETTING_UPDATED, this.onAppSettingUpdated.bind(this)); From 591e8bf7dace6a5f48848ce95f5724c9391c8594 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Tue, 14 Dec 2021 10:01:36 -0300 Subject: [PATCH 6/9] Show buttons in contexts (#23724) * Show buttons in contexts * Change tabbar filter * Improve room type filter * Remove testing code and refactor IRoom auxiliary functions * Add startup loadButtons * Trigger new UI button interactions (#23798) * Show buttons in contexts * Change tabbar filter * Handle action button interactions in endpoint * Trigger new interaction type on UI buttons --- app/apps/server/communication/uikit.js | 41 +++++++++++++++- app/ui-message/client/ActionButtonSyncer.ts | 41 +++++----------- app/ui-message/client/ActionManager.js | 2 + .../actionButtons/lib/applyButtonFilters.ts | 49 +++++++++++++++++++ .../client/actionButtons/messageAction.ts | 30 ++++++++++++ .../client/actionButtons/messageBox.ts | 31 ++++++++++++ app/ui-message/client/actionButtons/tabbar.ts | 18 +++++-- definition/IRoom.ts | 16 +++++- 8 files changed, 193 insertions(+), 35 deletions(-) create mode 100644 app/ui-message/client/actionButtons/lib/applyButtonFilters.ts create mode 100644 app/ui-message/client/actionButtons/messageAction.ts create mode 100644 app/ui-message/client/actionButtons/messageBox.ts diff --git a/app/apps/server/communication/uikit.js b/app/apps/server/communication/uikit.js index 9f12a44b0614..bb434331737d 100644 --- a/app/apps/server/communication/uikit.js +++ b/app/apps/server/communication/uikit.js @@ -297,8 +297,47 @@ const appsRoutes = (orch) => (req, res) => { break; } + case UIKitIncomingInteractionType.ACTION_BUTTON: { + const { + type, + actionId, + triggerId, + rid, + mid, + payload: { + context, + }, + } = req.body; + + const room = orch.getConverters().get('rooms').convertById(rid); + const user = orch.getConverters().get('users').convertToApp(req.user); + const message = mid && orch.getConverters().get('messages').convertById(mid); + + const action = { + type, + appId, + actionId, + triggerId, + user, + room, + message, + payload: { + context, + }, + }; + + try { + const result = Promise.await(orch.triggerEvent('IUIKitInteractionHandler', action)); + + res.send(result); + } catch (e) { + res.status(500).send(e.message); + } + break; + } + default: { - res.status(500).send({ error: 'Unknown action' }); + res.status(400).send({ error: 'Unknown action' }); } } diff --git a/app/ui-message/client/ActionButtonSyncer.ts b/app/ui-message/client/ActionButtonSyncer.ts index 41ad083cdfe5..878ac3670d2a 100644 --- a/app/ui-message/client/ActionButtonSyncer.ts +++ b/app/ui-message/client/ActionButtonSyncer.ts @@ -3,17 +3,22 @@ import { IUIActionButton, UIActionButtonContext } from '@rocket.chat/apps-engine import { APIClient } from '../../utils/client'; import * as TabBar from './actionButtons/tabbar'; +import * as MessageAction from './actionButtons/messageAction'; +import * as MessageBox from './actionButtons/messageBox'; -let registeredButtons: Array; +let registeredButtons: Array = []; export const addButton = (button: IUIActionButton): void => { switch (button.context) { case UIActionButtonContext.MESSAGE_ACTION: - // onMessageActionAdded(button); + MessageAction.onAdded(button); break; case UIActionButtonContext.ROOM_ACTION: TabBar.onAdded(button); break; + case UIActionButtonContext.MESSAGE_BOX_ACTION: + MessageBox.onAdded(button); + break; } registeredButtons.push(Object.freeze(button)); @@ -21,9 +26,15 @@ export const addButton = (button: IUIActionButton): void => { export const removeButton = (button: IUIActionButton): void => { switch (button.context) { + case UIActionButtonContext.MESSAGE_ACTION: + MessageAction.onRemoved(button); + break; case UIActionButtonContext.ROOM_ACTION: TabBar.onRemoved(button); break; + case UIActionButtonContext.MESSAGE_BOX_ACTION: + MessageBox.onRemoved(button); + break; } }; @@ -37,30 +48,4 @@ export const loadButtons = (): Promise => APIClient.get('apps/actionButton .then(console.log) .catch(console.error); -/** - * Returns an iterator so we preserve the original Array - * without needing to copy it - */ -export const getActionButtonsIterator = (filter?: (value: IUIActionButton) => boolean): IterableIterator => { - let index = 0; - - return { - next(): IteratorResult { - let value; - do { - if (index >= registeredButtons.length) { - return { done: true, value: undefined }; - } - - value = registeredButtons[index++]; - } while (filter && !filter(value)); - - return { done: false, value }; - }, - [Symbol.iterator](): IterableIterator { - return this; - }, - }; -}; - Meteor.startup(() => loadButtons()); diff --git a/app/ui-message/client/ActionManager.js b/app/ui-message/client/ActionManager.js index 81dc5fee53ad..ec78058e0e16 100644 --- a/app/ui-message/client/ActionManager.js +++ b/app/ui-message/client/ActionManager.js @@ -142,6 +142,8 @@ export const triggerAction = async ({ type, actionId, appId, rid, mid, viewId, c export const triggerBlockAction = (options) => triggerAction({ type: UIKitIncomingInteractionType.BLOCK, ...options }); +export const triggerActionButtonAction = (options) => triggerAction({ type: UIKitIncomingInteractionType.ACTION_BUTTON, ...options }); + export const triggerSubmitView = async ({ viewId, ...options }) => { const close = () => { const instance = instances.get(viewId); diff --git a/app/ui-message/client/actionButtons/lib/applyButtonFilters.ts b/app/ui-message/client/actionButtons/lib/applyButtonFilters.ts new file mode 100644 index 000000000000..5318001b1062 --- /dev/null +++ b/app/ui-message/client/actionButtons/lib/applyButtonFilters.ts @@ -0,0 +1,49 @@ +/* Style disabled as having some arrow functions in one-line hurts readability */ +/* eslint-disable arrow-body-style */ + +import { Meteor } from 'meteor/meteor'; +import { IUIActionButton, TemporaryRoomTypeFilter } from '@rocket.chat/apps-engine/definition/ui'; + +import { hasAtLeastOnePermission, hasPermission, hasRole } from '../../../../authorization/client'; +import { + IRoom, + isDirectMessageRoom, + isMultipleDirectMessageRoom, + isOmnichannelRoom, + isPrivateDiscussion, + isPrivateTeamRoom, + isPublicDiscussion, + isPublicTeamRoom, +} from '../../../../../definition/IRoom'; + +export const applyAuthFilter = (button: IUIActionButton, room?: IRoom): boolean => { + const { hasAllPermissions, hasOnePermission, hasAllRoles, hasOneRole } = button.when || {}; + + const hasAllPermissionsResult = hasAllPermissions ? hasPermission(hasAllPermissions) : true; + const hasOnePermissionResult = hasOnePermission ? hasAtLeastOnePermission(hasOnePermission) : true; + const hasAllRolesResult = hasAllRoles ? hasAllRoles.every((role) => hasRole(Meteor.userId(), role, room?._id)) : true; + const hasOneRoleResult = hasOneRole ? hasRole(Meteor.userId(), hasOneRole, room?._id) : true; + + return hasAllPermissionsResult && hasOnePermissionResult && hasAllRolesResult && hasOneRoleResult; +}; + +const enumToFilter: {[k in TemporaryRoomTypeFilter]: (room: IRoom) => boolean} = { + [TemporaryRoomTypeFilter.PUBLIC_CHANNEL]: (room) => room.t === 'c', + [TemporaryRoomTypeFilter.PRIVATE_CHANNEL]: (room) => room.t === 'p', + [TemporaryRoomTypeFilter.PUBLIC_TEAM]: isPublicTeamRoom, + [TemporaryRoomTypeFilter.PRIVATE_TEAM]: isPrivateTeamRoom, + [TemporaryRoomTypeFilter.PUBLIC_DISCUSSION]: isPublicDiscussion, + [TemporaryRoomTypeFilter.PRIVATE_DISCUSSION]: isPrivateDiscussion, + [TemporaryRoomTypeFilter.DIRECT]: isDirectMessageRoom, + [TemporaryRoomTypeFilter.DIRECT_MULTIPLE]: isMultipleDirectMessageRoom, + [TemporaryRoomTypeFilter.LIVE_CHAT]: isOmnichannelRoom, +}; + +export const applyRoomFilter = (button: IUIActionButton, room: IRoom): boolean => { + const { roomTypes } = button.when || {}; + return !roomTypes || roomTypes.some((filter): boolean => enumToFilter[filter]?.(room)); +}; + +export const applyButtonFilters = (button: IUIActionButton, room?: IRoom): boolean => { + return applyAuthFilter(button, room) && (!room || applyRoomFilter(button, room)); +}; diff --git a/app/ui-message/client/actionButtons/messageAction.ts b/app/ui-message/client/actionButtons/messageAction.ts new file mode 100644 index 000000000000..2e926c48c48f --- /dev/null +++ b/app/ui-message/client/actionButtons/messageAction.ts @@ -0,0 +1,30 @@ +import { IUIActionButton } from '@rocket.chat/apps-engine/definition/ui'; + +import { MessageAction, messageArgs } from '../../../ui-utils/client'; +import { triggerActionButtonAction } from '../ActionManager'; +import { applyButtonFilters } from './lib/applyButtonFilters'; + +const getIdForActionButton = ({ appId, actionId }: IUIActionButton): string => `${ appId }/${ actionId }`; + +// eslint-disable-next-line no-void +export const onAdded = (button: IUIActionButton): void => void MessageAction.addButton({ + id: getIdForActionButton(button), + icon: button.icon || '', + label: button.nameI18n, + context: button.when?.messageActionContext || ['message', 'message-mobile', 'threads', 'starred'], + condition({ room }: any) { + return applyButtonFilters(button, room); + }, + async action() { + const { msg } = messageArgs(this); + triggerActionButtonAction({ + rid: msg.rid, + mid: msg._id, + actionId: button.actionId, + appId: button.appId, + payload: { context: button.context }, + }); + }, +}); + +export const onRemoved = (button: IUIActionButton): void => MessageAction.removeButton(getIdForActionButton(button)); diff --git a/app/ui-message/client/actionButtons/messageBox.ts b/app/ui-message/client/actionButtons/messageBox.ts new file mode 100644 index 000000000000..4c51bb0576ed --- /dev/null +++ b/app/ui-message/client/actionButtons/messageBox.ts @@ -0,0 +1,31 @@ +import { Session } from 'meteor/session'; +import { IUIActionButton } from '@rocket.chat/apps-engine/definition/ui'; + +import { Rooms } from '../../../models/client'; +import { messageBox } from '../../../ui-utils/client'; +import { applyButtonFilters } from './lib/applyButtonFilters'; +import { triggerActionButtonAction } from '../ActionManager'; + +const getIdForActionButton = ({ appId, actionId }: IUIActionButton): string => `${ appId }/${ actionId }`; + +const APP_GROUP = 'Create_new'; + +// eslint-disable-next-line no-void +export const onAdded = (button: IUIActionButton): void => void messageBox.actions.add(APP_GROUP, button.nameI18n, { + id: getIdForActionButton(button), + icon: button.icon || '', + condition() { + return applyButtonFilters(button, Rooms.findOne(Session.get('openedRoom'))); + }, + action() { + triggerActionButtonAction({ + rid: Session.get('openedRoom'), + actionId: button.actionId, + appId: button.appId, + payload: { context: button.context }, + }); + }, +}); + +// eslint-disable-next-line no-void +export const onRemoved = (button: IUIActionButton): void => void messageBox.actions.remove(APP_GROUP, new RegExp(getIdForActionButton(button))); diff --git a/app/ui-message/client/actionButtons/tabbar.ts b/app/ui-message/client/actionButtons/tabbar.ts index 6ed7a28f60f0..bfd5c8102163 100644 --- a/app/ui-message/client/actionButtons/tabbar.ts +++ b/app/ui-message/client/actionButtons/tabbar.ts @@ -1,17 +1,25 @@ import { IUIActionButton } from '@rocket.chat/apps-engine/definition/ui'; import { addAction, deleteAction } from '../../../../client/views/room/lib/Toolbox'; +import { triggerActionButtonAction } from '../ActionManager'; +import { applyButtonFilters } from './lib/applyButtonFilters'; const getIdForActionButton = ({ appId, actionId }: IUIActionButton): string => `${ appId }/${ actionId }`; // eslint-disable-next-line no-void -export const onAdded = (button: IUIActionButton): void => void addAction(getIdForActionButton(button), { +export const onAdded = (button: IUIActionButton): void => void addAction(getIdForActionButton(button), ({ room }) => (applyButtonFilters(button, room) ? { id: button.actionId, - icon: '', + icon: button.icon || '', title: button.nameI18n as any, - // Introduce a mapper from Apps-engine's RoomTypes to these - // Determine what 'group' and 'team' are + // Filters were applied in the applyButtonFilters function + // if the code made it this far, the button should be shown groups: ['group', 'channel', 'live', 'team', 'direct', 'direct_multiple'], -}); + action: (): Promise => triggerActionButtonAction({ + rid: room._id, + actionId: button.actionId, + appId: button.appId, + payload: { context: button.context }, + }), +} : null)); export const onRemoved = (button: IUIActionButton): boolean => deleteAction(getIdForActionButton(button)); diff --git a/definition/IRoom.ts b/definition/IRoom.ts index f070c35f14d7..0fe6b44da65b 100644 --- a/definition/IRoom.ts +++ b/definition/IRoom.ts @@ -2,7 +2,7 @@ import { IRocketChatRecord } from './IRocketChatRecord'; import { IMessage } from './IMessage'; import { IUser, Username } from './IUser'; -type RoomType = 'c' | 'd' | 'p' | 'l'; +export type RoomType = 'c' | 'd' | 'p' | 'l'; type CallStatus = 'ringing' | 'ended' | 'declined' | 'ongoing'; export type RoomID = string; @@ -76,6 +76,19 @@ export interface ICreatedRoom extends IRoom { rid: string; } +export interface ITeamRoom extends IRoom { + teamMain: boolean; + teamId: string; +} + +export const isTeamRoom = (room: Partial): room is ITeamRoom => !!room.teamMain; +export const isPrivateTeamRoom = (room: Partial): room is ITeamRoom => isTeamRoom(room) && room.t === 'p'; +export const isPublicTeamRoom = (room: Partial): room is ITeamRoom => isTeamRoom(room) && room.t === 'c'; + +export const isDiscussion = (room: Partial): room is IRoom => !!room.prid; +export const isPrivateDiscussion = (room: Partial): room is IRoom => isDiscussion(room) && room.t === 'p'; +export const isPublicDiscussion = (room: Partial): room is IRoom => isDiscussion(room) && room.t === 'c'; + export interface IDirectMessageRoom extends Omit { t: 'd'; uids: Array; @@ -83,6 +96,7 @@ export interface IDirectMessageRoom extends Omit): room is IDirectMessageRoom => room.t === 'd'; +export const isMultipleDirectMessageRoom = (room: Partial): room is IDirectMessageRoom => isDirectMessageRoom(room) && room.uids.length > 2; export enum OmnichannelSourceType { WIDGET = 'widget', From b548aecb949a4958c7e9eaa729ff244932ac0cfb Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Thu, 16 Dec 2021 14:35:46 -0300 Subject: [PATCH 7/9] Handle timeout error --- app/ui-message/client/ActionManager.js | 16 ++++++++++++-- .../actionButtons/lib/applyButtonFilters.ts | 22 +++++++++---------- packages/rocketchat-i18n/i18n/en.i18n.json | 3 ++- packages/rocketchat-i18n/i18n/pt-BR.i18n.json | 3 ++- 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/app/ui-message/client/ActionManager.js b/app/ui-message/client/ActionManager.js index ec78058e0e16..7b9c4bcbb203 100644 --- a/app/ui-message/client/ActionManager.js +++ b/app/ui-message/client/ActionManager.js @@ -9,6 +9,8 @@ import { modal } from '../../ui-utils/client/lib/modal'; import { APIClient } from '../../utils'; import { UIKitInteractionTypes } from '../../../definition/UIKit'; import * as banners from '../../../client/lib/banners'; +import { dispatchToastMessage } from '../../../client/lib/toast'; +import { t } from '../../utils/client'; const events = new Emitter(); @@ -22,6 +24,8 @@ export const off = (...args) => { const TRIGGER_TIMEOUT = 5000; +const TRIGGER_TIMEOUT_ERROR = 'TRIGGER_TIMEOUT_ERROR'; + const triggersId = new Map(); const instances = new Map(); @@ -130,7 +134,7 @@ export const triggerAction = async ({ type, actionId, appId, rid, mid, viewId, c const payload = rest.payload || rest; - setTimeout(reject, TRIGGER_TIMEOUT, triggerId); + setTimeout(reject, TRIGGER_TIMEOUT, [TRIGGER_TIMEOUT_ERROR, { triggerId, appId }]); const { type: interactionType, ...data } = await APIClient.post( `apps/ui.interaction/${ appId }`, @@ -142,7 +146,15 @@ export const triggerAction = async ({ type, actionId, appId, rid, mid, viewId, c export const triggerBlockAction = (options) => triggerAction({ type: UIKitIncomingInteractionType.BLOCK, ...options }); -export const triggerActionButtonAction = (options) => triggerAction({ type: UIKitIncomingInteractionType.ACTION_BUTTON, ...options }); +export const triggerActionButtonAction = (options) => + triggerAction({ type: UIKitIncomingInteractionType.ACTION_BUTTON, ...options }).catch(async (reason) => { + if (Array.isArray(reason) && reason[0] === TRIGGER_TIMEOUT_ERROR) { + dispatchToastMessage({ + type: 'error', + message: t('UIKit_Interaction_Timeout'), + }); + } + }); export const triggerSubmitView = async ({ viewId, ...options }) => { const close = () => { diff --git a/app/ui-message/client/actionButtons/lib/applyButtonFilters.ts b/app/ui-message/client/actionButtons/lib/applyButtonFilters.ts index 5318001b1062..ba3c2d1daffa 100644 --- a/app/ui-message/client/actionButtons/lib/applyButtonFilters.ts +++ b/app/ui-message/client/actionButtons/lib/applyButtonFilters.ts @@ -2,7 +2,7 @@ /* eslint-disable arrow-body-style */ import { Meteor } from 'meteor/meteor'; -import { IUIActionButton, TemporaryRoomTypeFilter } from '@rocket.chat/apps-engine/definition/ui'; +import { IUIActionButton, RoomTypeFilter } from '@rocket.chat/apps-engine/definition/ui'; import { hasAtLeastOnePermission, hasPermission, hasRole } from '../../../../authorization/client'; import { @@ -27,16 +27,16 @@ export const applyAuthFilter = (button: IUIActionButton, room?: IRoom): boolean return hasAllPermissionsResult && hasOnePermissionResult && hasAllRolesResult && hasOneRoleResult; }; -const enumToFilter: {[k in TemporaryRoomTypeFilter]: (room: IRoom) => boolean} = { - [TemporaryRoomTypeFilter.PUBLIC_CHANNEL]: (room) => room.t === 'c', - [TemporaryRoomTypeFilter.PRIVATE_CHANNEL]: (room) => room.t === 'p', - [TemporaryRoomTypeFilter.PUBLIC_TEAM]: isPublicTeamRoom, - [TemporaryRoomTypeFilter.PRIVATE_TEAM]: isPrivateTeamRoom, - [TemporaryRoomTypeFilter.PUBLIC_DISCUSSION]: isPublicDiscussion, - [TemporaryRoomTypeFilter.PRIVATE_DISCUSSION]: isPrivateDiscussion, - [TemporaryRoomTypeFilter.DIRECT]: isDirectMessageRoom, - [TemporaryRoomTypeFilter.DIRECT_MULTIPLE]: isMultipleDirectMessageRoom, - [TemporaryRoomTypeFilter.LIVE_CHAT]: isOmnichannelRoom, +const enumToFilter: {[k in RoomTypeFilter]: (room: IRoom) => boolean} = { + [RoomTypeFilter.PUBLIC_CHANNEL]: (room) => room.t === 'c', + [RoomTypeFilter.PRIVATE_CHANNEL]: (room) => room.t === 'p', + [RoomTypeFilter.PUBLIC_TEAM]: isPublicTeamRoom, + [RoomTypeFilter.PRIVATE_TEAM]: isPrivateTeamRoom, + [RoomTypeFilter.PUBLIC_DISCUSSION]: isPublicDiscussion, + [RoomTypeFilter.PRIVATE_DISCUSSION]: isPrivateDiscussion, + [RoomTypeFilter.DIRECT]: isDirectMessageRoom, + [RoomTypeFilter.DIRECT_MULTIPLE]: isMultipleDirectMessageRoom, + [RoomTypeFilter.LIVE_CHAT]: isOmnichannelRoom, }; export const applyRoomFilter = (button: IUIActionButton, room: IRoom): boolean => { diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index ce54f0dcc6ca..2840cd98cbe0 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -3595,6 +3595,7 @@ "Same_Origin": "Same origin", "Strict_Origin": "Strict origin", "Strict_Origin_When_Cross_Origin": "Strict origin when cross origin", + "UIKit_Interaction_Timeout": "App has failed to respond. Please try again or contact your admin", "Unsafe_Url": "Unsafe URL", "Rocket_Chat_Alert": "Rocket.Chat Alert", "Role": "Role", @@ -4760,4 +4761,4 @@ "Your_temporary_password_is_password": "Your temporary password is [password].", "Your_TOTP_has_been_reset": "Your Two Factor TOTP has been reset.", "Your_workspace_is_ready": "Your workspace is ready to use 🎉" -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index 6f2e9780080e..02b7ce8eb984 100644 --- a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -3595,6 +3595,7 @@ "Same_Origin": "Same origin", "Strict_Origin": "Strict origin", "Strict_Origin_When_Cross_Origin": "Strict origin when cross origin", + "UIKit_Interaction_Timeout": "O app não respondeu à interação. Por favor, tente novamente ou contate o seu administrador", "Unsafe_Url": "URL insegura", "Rocket_Chat_Alert": "Alerta Rocket.Chat", "Role": "Papel", @@ -4760,4 +4761,4 @@ "Your_temporary_password_is_password": "Sua senha temporária é [password].", "Your_TOTP_has_been_reset": "Seu TOTP de dois fatores foi redefinido.", "Your_workspace_is_ready": "O seu espaço de trabalho está pronto para usar 🎉" -} \ No newline at end of file +} From 928d31b1a5cb825874d817a1984e5c419afce622 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Thu, 16 Dec 2021 18:06:16 -0300 Subject: [PATCH 8/9] Handle translation of the buttons names (#23975) * Handle translation of the buttons names --- .../client/actionButtons/messageAction.ts | 6 ++-- .../client/actionButtons/messageBox.ts | 35 +++++++++++-------- app/ui-message/client/actionButtons/tabbar.ts | 6 ++-- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/app/ui-message/client/actionButtons/messageAction.ts b/app/ui-message/client/actionButtons/messageAction.ts index 2e926c48c48f..3d220b88a361 100644 --- a/app/ui-message/client/actionButtons/messageAction.ts +++ b/app/ui-message/client/actionButtons/messageAction.ts @@ -1,6 +1,8 @@ import { IUIActionButton } from '@rocket.chat/apps-engine/definition/ui'; +import { Utilities } from '../../../apps/lib/misc/Utilities'; import { MessageAction, messageArgs } from '../../../ui-utils/client'; +import { t } from '../../../utils/client'; import { triggerActionButtonAction } from '../ActionManager'; import { applyButtonFilters } from './lib/applyButtonFilters'; @@ -9,8 +11,8 @@ const getIdForActionButton = ({ appId, actionId }: IUIActionButton): string => ` // eslint-disable-next-line no-void export const onAdded = (button: IUIActionButton): void => void MessageAction.addButton({ id: getIdForActionButton(button), - icon: button.icon || '', - label: button.nameI18n, + // icon: button.icon || '', + label: t(Utilities.getI18nKeyForApp(button.labelI18n, button.appId)), context: button.when?.messageActionContext || ['message', 'message-mobile', 'threads', 'starred'], condition({ room }: any) { return applyButtonFilters(button, room); diff --git a/app/ui-message/client/actionButtons/messageBox.ts b/app/ui-message/client/actionButtons/messageBox.ts index 4c51bb0576ed..e79f88a8dcad 100644 --- a/app/ui-message/client/actionButtons/messageBox.ts +++ b/app/ui-message/client/actionButtons/messageBox.ts @@ -5,27 +5,32 @@ import { Rooms } from '../../../models/client'; import { messageBox } from '../../../ui-utils/client'; import { applyButtonFilters } from './lib/applyButtonFilters'; import { triggerActionButtonAction } from '../ActionManager'; +import { t } from '../../../utils/client'; +import { Utilities } from '../../../apps/lib/misc/Utilities'; const getIdForActionButton = ({ appId, actionId }: IUIActionButton): string => `${ appId }/${ actionId }`; const APP_GROUP = 'Create_new'; // eslint-disable-next-line no-void -export const onAdded = (button: IUIActionButton): void => void messageBox.actions.add(APP_GROUP, button.nameI18n, { - id: getIdForActionButton(button), - icon: button.icon || '', - condition() { - return applyButtonFilters(button, Rooms.findOne(Session.get('openedRoom'))); - }, - action() { - triggerActionButtonAction({ - rid: Session.get('openedRoom'), - actionId: button.actionId, - appId: button.appId, - payload: { context: button.context }, - }); - }, -}); +export const onAdded = (button: IUIActionButton): void => void messageBox.actions.add( + APP_GROUP, + t(Utilities.getI18nKeyForApp(button.labelI18n, button.appId)), + { + id: getIdForActionButton(button), + // icon: button.icon || '', + condition() { + return applyButtonFilters(button, Rooms.findOne(Session.get('openedRoom'))); + }, + action() { + triggerActionButtonAction({ + rid: Session.get('openedRoom'), + actionId: button.actionId, + appId: button.appId, + payload: { context: button.context }, + }); + }, + }); // eslint-disable-next-line no-void export const onRemoved = (button: IUIActionButton): void => void messageBox.actions.remove(APP_GROUP, new RegExp(getIdForActionButton(button))); diff --git a/app/ui-message/client/actionButtons/tabbar.ts b/app/ui-message/client/actionButtons/tabbar.ts index bfd5c8102163..cb8a7d808670 100644 --- a/app/ui-message/client/actionButtons/tabbar.ts +++ b/app/ui-message/client/actionButtons/tabbar.ts @@ -1,6 +1,8 @@ import { IUIActionButton } from '@rocket.chat/apps-engine/definition/ui'; import { addAction, deleteAction } from '../../../../client/views/room/lib/Toolbox'; +import { Utilities } from '../../../apps/lib/misc/Utilities'; +import { t } from '../../../utils/client'; import { triggerActionButtonAction } from '../ActionManager'; import { applyButtonFilters } from './lib/applyButtonFilters'; @@ -9,8 +11,8 @@ const getIdForActionButton = ({ appId, actionId }: IUIActionButton): string => ` // eslint-disable-next-line no-void export const onAdded = (button: IUIActionButton): void => void addAction(getIdForActionButton(button), ({ room }) => (applyButtonFilters(button, room) ? { id: button.actionId, - icon: button.icon || '', - title: button.nameI18n as any, + icon: '', // Apps won't provide icons for now + title: t(Utilities.getI18nKeyForApp(button.labelI18n, button.appId)) as any, // Filters were applied in the applyButtonFilters function // if the code made it this far, the button should be shown groups: ['group', 'channel', 'live', 'team', 'direct', 'direct_multiple'], From e3864581bce78de95d66f1efbdb898e5775257ad Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Mon, 20 Dec 2021 18:54:37 -0300 Subject: [PATCH 9/9] Update Apps-Engine --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 11dd022bca92..d07af93e1560 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5324,9 +5324,9 @@ } }, "@rocket.chat/apps-engine": { - "version": "1.29.0-alpha.0.5683", - "resolved": "https://registry.npmjs.org/@rocket.chat/apps-engine/-/apps-engine-1.29.0-alpha.0.5683.tgz", - "integrity": "sha512-95bnpFxYD8dxaRQuR+oykhdDNfNJBdL91geMO0YqFef9sBnUgsayDq1nrmlpKGL1Nw5ir5OGnACOe8enB50gqw==", + "version": "1.29.0-alpha.0.5706", + "resolved": "https://registry.npmjs.org/@rocket.chat/apps-engine/-/apps-engine-1.29.0-alpha.0.5706.tgz", + "integrity": "sha512-ML+B8yv47tvwqmw8ictaq93EI9DTVefD9tyMvBYWrjMwNtt1MEmh0PWWRgHp9yM3SxsExOqguqJDGLv7Muuakw==", "requires": { "adm-zip": "^0.4.9", "cryptiles": "^4.1.3", diff --git a/package.json b/package.json index 80db690d1185..764159e48f9f 100644 --- a/package.json +++ b/package.json @@ -173,7 +173,7 @@ "@nivo/heatmap": "0.73.0", "@nivo/line": "0.62.0", "@nivo/pie": "0.73.0", - "@rocket.chat/apps-engine": "^1.29.0-alpha.0.5683", + "@rocket.chat/apps-engine": "^1.29.0-alpha.0.5706", "@rocket.chat/css-in-js": "^0.30.1", "@rocket.chat/emitter": "^0.30.1", "@rocket.chat/fuselage": "^0.6.3-dev.368",