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