Skip to content

Commit

Permalink
Show buttons in contexts (#23724)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
d-gubert authored Dec 14, 2021
1 parent 7c655eb commit 591e8bf
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 35 deletions.
41 changes: 40 additions & 1 deletion app/apps/server/communication/uikit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
}
}

Expand Down
41 changes: 13 additions & 28 deletions app/ui-message/client/ActionButtonSyncer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,38 @@ 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<IUIActionButton>;
let registeredButtons: Array<IUIActionButton> = [];

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

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

Expand All @@ -37,30 +48,4 @@ export const loadButtons = (): Promise<void> => 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<IUIActionButton> => {
let index = 0;

return {
next(): IteratorResult<IUIActionButton> {
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<IUIActionButton> {
return this;
},
};
};

Meteor.startup(() => loadButtons());
2 changes: 2 additions & 0 deletions app/ui-message/client/ActionManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
49 changes: 49 additions & 0 deletions app/ui-message/client/actionButtons/lib/applyButtonFilters.ts
Original file line number Diff line number Diff line change
@@ -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));
};
30 changes: 30 additions & 0 deletions app/ui-message/client/actionButtons/messageAction.ts
Original file line number Diff line number Diff line change
@@ -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));
31 changes: 31 additions & 0 deletions app/ui-message/client/actionButtons/messageBox.ts
Original file line number Diff line number Diff line change
@@ -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)));
18 changes: 13 additions & 5 deletions app/ui-message/client/actionButtons/tabbar.ts
Original file line number Diff line number Diff line change
@@ -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<any> => triggerActionButtonAction({
rid: room._id,
actionId: button.actionId,
appId: button.appId,
payload: { context: button.context },
}),
} : null));

export const onRemoved = (button: IUIActionButton): boolean => deleteAction(getIdForActionButton(button));
16 changes: 15 additions & 1 deletion definition/IRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -76,13 +76,27 @@ export interface ICreatedRoom extends IRoom {
rid: string;
}

export interface ITeamRoom extends IRoom {
teamMain: boolean;
teamId: string;
}

export const isTeamRoom = (room: Partial<IRoom>): room is ITeamRoom => !!room.teamMain;
export const isPrivateTeamRoom = (room: Partial<IRoom>): room is ITeamRoom => isTeamRoom(room) && room.t === 'p';
export const isPublicTeamRoom = (room: Partial<IRoom>): room is ITeamRoom => isTeamRoom(room) && room.t === 'c';

export const isDiscussion = (room: Partial<IRoom>): room is IRoom => !!room.prid;
export const isPrivateDiscussion = (room: Partial<IRoom>): room is IRoom => isDiscussion(room) && room.t === 'p';
export const isPublicDiscussion = (room: Partial<IRoom>): room is IRoom => isDiscussion(room) && room.t === 'c';

export interface IDirectMessageRoom extends Omit<IRoom, 'default' | 'featured' | 'u' | 'name'> {
t: 'd';
uids: Array<string>;
usernames: Array<Username>;
}

export const isDirectMessageRoom = (room: Partial<IRoom>): room is IDirectMessageRoom => room.t === 'd';
export const isMultipleDirectMessageRoom = (room: Partial<IRoom>): room is IDirectMessageRoom => isDirectMessageRoom(room) && room.uids.length > 2;

export enum OmnichannelSourceType {
WIDGET = 'widget',
Expand Down

0 comments on commit 591e8bf

Please sign in to comment.