Skip to content

Commit

Permalink
Shrink MessageAction API
Browse files Browse the repository at this point in the history
  • Loading branch information
tassoevan committed Dec 6, 2024
1 parent 020bcf3 commit 205060d
Show file tree
Hide file tree
Showing 15 changed files with 126 additions and 164 deletions.
60 changes: 10 additions & 50 deletions apps/meteor/app/ui-utils/client/lib/MessageAction.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import type { IMessage, IUser, ISubscription, IRoom, SettingValue, ITranslatedMessage } from '@rocket.chat/core-typings';
import type { Keys as IconName } from '@rocket.chat/icons';
import type { TranslationKey } from '@rocket.chat/ui-contexts';
import mem from 'mem';
import type { ContextType } from 'react';

import type { AutoTranslateOptions } from '../../../../client/views/room/MessageList/hooks/useAutoTranslate';
import type { ChatContext } from '../../../../client/views/room/contexts/ChatContext';
import type { RoomToolboxContextValue } from '../../../../client/views/room/contexts/RoomToolboxContext';

type MessageActionGroup = 'message' | 'menu';

Expand All @@ -25,60 +19,30 @@ export type MessageActionContext =

type MessageActionType = 'communication' | 'interaction' | 'duplication' | 'apps' | 'management';

export type MessageActionConditionProps = {
message: IMessage;
user: IUser | undefined;
room: IRoom;
subscription?: ISubscription;
context?: MessageActionContext;
settings: { [key: string]: SettingValue };
chat: ContextType<typeof ChatContext>;
};

export type MessageActionConfig = {
id: string;
icon: IconName;
variant?: 'danger' | 'success' | 'warning';
label: TranslationKey;
order?: number;
order: number;
/* @deprecated */
color?: string;
role?: string;
group?: MessageActionGroup | MessageActionGroup[];
color?: 'alert';
group: MessageActionGroup;
context?: MessageActionContext[];
action: (
e: Pick<Event, 'preventDefault' | 'stopPropagation' | 'currentTarget'> | undefined,
{
message,
tabbar,
room,
chat,
autoTranslateOptions,
}: {
message: IMessage & Partial<ITranslatedMessage>;
tabbar: RoomToolboxContextValue;
room?: IRoom;
chat: ContextType<typeof ChatContext>;
autoTranslateOptions?: AutoTranslateOptions;
},
) => any;
condition?: (props: MessageActionConditionProps) => Promise<boolean> | boolean;
action: (e: Pick<Event, 'preventDefault' | 'stopPropagation' | 'currentTarget'> | undefined) => any;
condition?: () => Promise<boolean> | boolean;
type?: MessageActionType;
disabled?: (props: MessageActionConditionProps) => boolean;
disabled?: boolean;
};

class MessageAction {
public buttons: Record<MessageActionConfig['id'], MessageActionConfig> = {};
private buttons: Record<MessageActionConfig['id'], MessageActionConfig> = {};

public addButton(config: MessageActionConfig): void {
if (!config?.id) {
return;
}

if (!config.group) {
config.group = 'menu';
}

if (config.condition) {
config.condition = mem(config.condition, { maxAge: 1000, cacheKey: JSON.stringify });
}
Expand All @@ -90,19 +54,15 @@ class MessageAction {
delete this.buttons[id];
}

public async getAll(
props: MessageActionConditionProps,
context: MessageActionContext,
group: MessageActionGroup,
): Promise<MessageActionConfig[]> {
public async getAll(context: MessageActionContext, group: MessageActionGroup): Promise<MessageActionConfig[]> {
return (
await Promise.all(
Object.values(this.buttons)
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
.filter((button) => !button.group || (Array.isArray(button.group) ? button.group.includes(group) : button.group === group))
.filter((button) => button.group === group)
.filter((button) => !button.context || button.context.includes(context))
.map(async (button) => {
return [button, !button.condition || (await button.condition({ ...props, context }))] as const;
return [button, !button.condition || (await button.condition())] as const;
}),
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { MouseEvent, ReactElement } from 'react';
import React from 'react';
import { useTranslation } from 'react-i18next';

import type { MessageActionConditionProps, MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction';
import type { MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction';

type MessageActionConfigOption = Omit<MessageActionConfig, 'condition' | 'context' | 'order' | 'action'> & {
action: (e?: MouseEvent<HTMLElement>) => void;
Expand All @@ -19,11 +19,10 @@ type MessageActionSection = {
type MessageActionMenuProps = {
onChangeMenuVisibility: (visible: boolean) => void;
options: MessageActionConfigOption[];
context: MessageActionConditionProps;
isMessageEncrypted: boolean;
};

const MessageActionMenu = ({ options, onChangeMenuVisibility, context, isMessageEncrypted }: MessageActionMenuProps): ReactElement => {
const MessageActionMenu = ({ options, onChangeMenuVisibility, isMessageEncrypted }: MessageActionMenuProps): ReactElement => {
const { t } = useTranslation();
const id = useUniqueId();
const groupOptions = options
Expand All @@ -34,9 +33,9 @@ const MessageActionMenu = ({ options, onChangeMenuVisibility, context, isMessage
content: t(option.label),
onClick: option.action,
type: option.type,
...(option.disabled && { disabled: option?.disabled?.(context) }),
...(option.disabled &&
option?.disabled?.(context) && { tooltip: t('Action_not_available_encrypted_content', { action: t(option.label) }) }),
...(typeof option.disabled === 'boolean' && { disabled: option.disabled }),
...(typeof option.disabled === 'boolean' &&
option.disabled && { tooltip: t('Action_not_available_encrypted_content', { action: t(option.label) }) }),
}))
.reduce(
(acc, option) => {
Expand Down
59 changes: 20 additions & 39 deletions apps/meteor/client/components/message/toolbar/MessageToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import type { IMessage, IRoom, ISubscription, ITranslatedMessage } from '@rocket
import { isThreadMessage, isRoomFederated, isVideoConfMessage, isE2EEMessage } from '@rocket.chat/core-typings';
import { MessageToolbar as FuselageMessageToolbar, MessageToolbarItem } from '@rocket.chat/fuselage';
import { useFeaturePreview } from '@rocket.chat/ui-client';
import { useUser, useSettings, useTranslation, useMethod, useLayoutHiddenActions, useSetting } from '@rocket.chat/ui-contexts';
import { useUser, useTranslation, useMethod, useLayoutHiddenActions, useSetting } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';
import type { ComponentProps, ReactElement } from 'react';
import React, { memo, useMemo, useRef } from 'react';
import React, { memo, useRef } from 'react';

import MessageActionMenu from './MessageActionMenu';
import MessageToolbarStarsActionMenu from './MessageToolbarStarsActionMenu';
Expand Down Expand Up @@ -42,9 +42,6 @@ import { useEmbeddedLayout } from '../../../hooks/useEmbeddedLayout';
import { roomsQueryKeys } from '../../../lib/queryKeys';
import EmojiElement from '../../../views/composer/EmojiPicker/EmojiElement';
import { useIsSelecting } from '../../../views/room/MessageList/contexts/SelectedMessagesContext';
import { useAutoTranslate } from '../../../views/room/MessageList/hooks/useAutoTranslate';
import { useChat } from '../../../views/room/contexts/ChatContext';
import { useRoomToolbox } from '../../../views/room/contexts/RoomToolboxContext';

const getMessageContext = (message: IMessage, room: IRoom, context?: MessageActionContext): MessageActionContext => {
if (context) {
Expand Down Expand Up @@ -84,7 +81,6 @@ const MessageToolbar = ({
}: MessageToolbarProps): ReactElement | null => {
const t = useTranslation();
const user = useUser() ?? undefined;
const settings = useSettings();
const isLayoutEmbedded = useEmbeddedLayout();

const toolbarRef = useRef(null);
Expand All @@ -96,21 +92,18 @@ const MessageToolbar = ({

const context = getMessageContext(message, room, messageContext);

const mapSettings = useMemo(() => Object.fromEntries(settings.map((setting) => [setting._id, setting.value])), [settings]);

const chat = useChat();
const { quickReactions, addRecentEmoji } = useEmojiPickerData();

const actionButtonApps = useMessageActionAppsActionButtons(context);
const actionButtonApps = useMessageActionAppsActionButtons(message, context);

const starsAction = useMessageActionAppsActionButtons(context, 'ai');
const starsAction = useMessageActionAppsActionButtons(message, context, 'ai');

const { messageToolbox: hiddenActions } = useLayoutHiddenActions();
const allowStarring = useSetting('Message_AllowStarring');

// TODO: move this to another place
useWebDAVMessageAction();
useNewDiscussionMessageAction();
useWebDAVMessageAction(message, { subscription });
useNewDiscussionMessageAction(message, { user, room, subscription });
useUnpinMessageAction(message, { room, subscription });
usePinMessageAction(message, { room, subscription });
useStarMessageAction(message, { room, user });
Expand Down Expand Up @@ -158,13 +151,11 @@ const MessageToolbar = ({
useShowMessageReactionsAction(message);
useReadReceiptsDetailsAction(message);

const actionsQueryResult = useQuery({
const { isSuccess, data } = useQuery({
queryKey: roomsQueryKeys.messageActionsWithParameters(room._id, message),
queryFn: async () => {
const props = { message, room, user, subscription, settings: mapSettings, chat };

const toolboxItems = await MessageAction.getAll(props, context, 'message');
const menuItems = await MessageAction.getAll(props, context, 'menu');
const toolboxItems = await MessageAction.getAll(context, 'message');
const menuItems = await MessageAction.getAll(context, 'menu');

return {
message: toolboxItems.filter((action) => !hiddenActions.includes(action.id)),
Expand All @@ -174,17 +165,13 @@ const MessageToolbar = ({
keepPreviousData: true,
});

const toolbox = useRoomToolbox();

const selecting = useIsSelecting();

const autoTranslateOptions = useAutoTranslate(subscription);

if (selecting || (!actionsQueryResult.data?.message.length && !actionsQueryResult.data?.menu.length)) {
if (selecting || (!data?.message.length && !data?.menu.length)) {
return null;
}

const isReactionAllowed = actionsQueryResult.data?.message.find(({ id }) => id === 'reaction-message');
const isReactionAllowed = data?.message.find(({ id }) => id === 'reaction-message');

const handleSetReaction = (emoji: string) => {
setReaction(`:${emoji}:`, message._id);
Expand All @@ -198,44 +185,38 @@ const MessageToolbar = ({
quickReactions.slice(0, 3).map(({ emoji, image }) => {
return <EmojiElement small key={emoji} title={emoji} emoji={emoji} image={image} onClick={() => handleSetReaction(emoji)} />;
})}
{actionsQueryResult.isSuccess &&
actionsQueryResult.data.message.map((action) => (
{isSuccess &&
data.message.map((action) => (
<MessageToolbarItem
onClick={(e): void => action.action(e, { message, tabbar: toolbox, room, chat, autoTranslateOptions })}
onClick={(e): void => action.action(e)}
key={action.id}
icon={action.icon}
title={
action?.disabled?.({ message, room, user, subscription, settings: mapSettings, chat, context })
? t('Action_not_available_encrypted_content', { action: t(action.label) })
: t(action.label)
}
title={action?.disabled ? t('Action_not_available_encrypted_content', { action: t(action.label) }) : t(action.label)}
data-qa-id={action.label}
data-qa-type='message-action-menu'
disabled={action?.disabled?.({ message, room, user, subscription, settings: mapSettings, chat, context })}
disabled={action?.disabled}
/>
))}
{starsAction.data && starsAction.data.length > 0 && (
<MessageToolbarStarsActionMenu
options={starsAction.data.map((action) => ({
...action,
action: (e) => action.action(e, { message, tabbar: toolbox, room, chat, autoTranslateOptions }),
action: (e) => action.action(e),
}))}
onChangeMenuVisibility={onChangeMenuVisibility}
data-qa-type='message-action-stars-menu-options'
context={{ message, room, user, subscription, settings: mapSettings, chat, context }}
isMessageEncrypted={isE2EEMessage(message)}
/>
)}

{actionsQueryResult.isSuccess && actionsQueryResult.data.menu.length > 0 && (
{isSuccess && data.menu.length > 0 && (
<MessageActionMenu
options={[...actionsQueryResult.data?.menu, ...(actionButtonApps.data ?? [])].filter(Boolean).map((action) => ({
options={[...data?.menu, ...(actionButtonApps.data ?? [])].filter(Boolean).map((action) => ({
...action,
action: (e) => action.action(e, { message, tabbar: toolbox, room, chat, autoTranslateOptions }),
action: (e) => action.action(e),
}))}
onChangeMenuVisibility={onChangeMenuVisibility}
data-qa-type='message-action-menu-options'
context={{ message, room, user, subscription, settings: mapSettings, chat, context }}
isMessageEncrypted={isE2EEMessage(message)}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { MouseEvent, ReactElement } from 'react';
import React from 'react';
import { useTranslation } from 'react-i18next';

import type { MessageActionConditionProps, MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction';
import type { MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction';

type MessageActionConfigOption = Omit<MessageActionConfig, 'condition' | 'context' | 'order' | 'action'> & {
action: (e?: MouseEvent<HTMLElement>) => void;
Expand All @@ -19,16 +19,10 @@ type MessageActionSection = {
type MessageActionMenuProps = {
onChangeMenuVisibility: (visible: boolean) => void;
options: MessageActionConfigOption[];
context: MessageActionConditionProps;
isMessageEncrypted: boolean;
};

const MessageToolbarStarsActionMenu = ({
options,
onChangeMenuVisibility,
context,
isMessageEncrypted,
}: MessageActionMenuProps): ReactElement => {
const MessageToolbarStarsActionMenu = ({ options, onChangeMenuVisibility, isMessageEncrypted }: MessageActionMenuProps): ReactElement => {
const { t } = useTranslation();
const id = useUniqueId();

Expand All @@ -40,9 +34,9 @@ const MessageToolbarStarsActionMenu = ({
content: t(option.label),
onClick: option.action,
type: option.type,
...(option.disabled && { disabled: option?.disabled?.(context) }),
...(option.disabled &&
option?.disabled?.(context) && { tooltip: t('Action_not_available_encrypted_content', { action: t(option.label) }) }),
...(typeof option.disabled === 'boolean' && { disabled: option.disabled }),
...(typeof option.disabled === 'boolean' &&
option.disabled && { tooltip: t('Action_not_available_encrypted_content', { action: t(option.label) }) }),
};

const group = option.type || '';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const useCopyAction = (message: IMessage, { subscription }: { subscriptio
label: 'Copy_text',
context: ['message', 'message-mobile', 'threads', 'federated'],
type: 'duplication',
async action(_, { message }) {
async action() {
const msgText = getMainMessageText(message).msg;
await navigator.clipboard.writeText(msgText);
dispatchToastMessage({ type: 'success', message: t('Copied') });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const useForwardMessageAction = (message: IMessage) => {
},
order: 0,
group: 'message',
disabled: () => encrypted,
disabled: encrypted,
});

return () => {
Expand Down
Loading

0 comments on commit 205060d

Please sign in to comment.