Skip to content

Commit

Permalink
chore: Replace Dropdown in favor of MenuV2 on MessageComposer (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
dougfabris authored Jan 11, 2024
1 parent eecf782 commit 116ad55
Show file tree
Hide file tree
Showing 27 changed files with 249 additions and 324 deletions.
9 changes: 5 additions & 4 deletions apps/meteor/client/components/GenericMenu/GenericMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import GenericMenuItem from './GenericMenuItem';
import { useHandleMenuAction } from './hooks/useHandleMenuAction';

type GenericMenuCommonProps = {
icon?: ComponentProps<typeof IconButton>['icon'];
title: string;
icon?: ComponentProps<typeof IconButton>['icon'];
disabled?: boolean;
};
type GenericMenuConditionalProps =
| {
Expand All @@ -27,7 +28,7 @@ type GenericMenuConditionalProps =

type GenericMenuProps = GenericMenuCommonProps & GenericMenuConditionalProps & Omit<ComponentProps<typeof MenuV2>, 'children'>;

const GenericMenu = ({ title, icon = 'menu', onAction, ...props }: GenericMenuProps) => {
const GenericMenu = ({ title, icon = 'menu', disabled, onAction, ...props }: GenericMenuProps) => {
const t = useTranslation();

const sections = 'sections' in props && props.sections;
Expand All @@ -44,8 +45,8 @@ const GenericMenu = ({ title, icon = 'menu', onAction, ...props }: GenericMenuPr

const isMenuEmpty = !(sections && sections.length > 0) && !(items && items.length > 0);

if (isMenuEmpty) {
return <IconButton icon={icon} disabled />;
if (isMenuEmpty || disabled) {
return <IconButton small icon={icon} disabled />;
}

return (
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/client/components/GenericMenu/GenericMenuItem.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { MenuItemColumn, MenuItemContent, MenuItemIcon, MenuItemInput } from '@rocket.chat/fuselage';
import type { ComponentProps, ReactNode } from 'react';
import type { ComponentProps, MouseEvent, ReactNode } from 'react';
import React from 'react';

export type GenericMenuItemProps = {
id: string;
icon?: ComponentProps<typeof MenuItemIcon>['name'];
content?: ReactNode;
addon?: ReactNode;
onClick?: () => void;
onClick?: (e?: MouseEvent<HTMLElement>) => void;
status?: ReactNode;
disabled?: boolean;
description?: ReactNode;
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import type { IRoom, IMessage } from '@rocket.chat/core-typings';
import { Option, OptionTitle, OptionIcon, OptionContent } from '@rocket.chat/fuselage';
import type { Icon } from '@rocket.chat/fuselage';
import { MessageComposerAction, MessageComposerActionsDivider } from '@rocket.chat/ui-composer';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { ComponentProps } from 'react';
import type { TranslationKey } from '@rocket.chat/ui-contexts';
import { useUserRoom, useTranslation, useLayoutHiddenActions } from '@rocket.chat/ui-contexts';
import type { ComponentProps, MouseEvent } from 'react';
import React, { memo } from 'react';

import { messageBox } from '../../../../../../app/ui-utils/client';
import { isTruthy } from '../../../../../../lib/isTruthy';
import GenericMenu from '../../../../../components/GenericMenu/GenericMenu';
import type { GenericMenuItemProps } from '../../../../../components/GenericMenu/GenericMenuItem';
import { useMessageboxAppsActionButtons } from '../../../../../hooks/useAppActionButtons';
import { useChat } from '../../../contexts/ChatContext';
import ActionsToolbarDropdown from './ActionsToolbarDropdown';
import { useToolbarActions } from './hooks/useToolbarActions';
import { useAudioMessageAction } from './hooks/useAudioMessageAction';
import { useCreateDiscussionAction } from './hooks/useCreateDiscussionAction';
import { useFileUploadAction } from './hooks/useFileUploadAction';
import { useShareLocationAction } from './hooks/useShareLocationAction';
import { useVideoMessageAction } from './hooks/useVideoMessageAction';
import { useWebdavActions } from './hooks/useWebdavActions';

type MessageBoxActionsToolbarProps = {
canSend: boolean;
Expand All @@ -19,6 +29,13 @@ type MessageBoxActionsToolbarProps = {
tmid?: IMessage['_id'];
};

const isHidden = (hiddenActions: Array<string>, action: GenericMenuItemProps) => {
if (!action) {
return true;
}
return hiddenActions.includes(action.id);
};

const MessageBoxActionsToolbar = ({
canSend,
typing,
Expand All @@ -28,64 +45,107 @@ const MessageBoxActionsToolbar = ({
variant = 'large',
isMicrophoneDenied,
}: MessageBoxActionsToolbarProps) => {
const data = useToolbarActions({
canSend,
typing,
isRecording,
isMicrophoneDenied: Boolean(isMicrophoneDenied),
rid,
tmid,
variant,
});

const { featured, menu } = data;
const t = useTranslation();
const chatContext = useChat();

if (!chatContext) {
throw new Error('useChat must be used within a ChatProvider');
}

if (!featured.length && !menu.length) {
return null;
const room = useUserRoom(rid);

const audioMessageAction = useAudioMessageAction(!canSend || typing || isRecording || isMicrophoneDenied, isMicrophoneDenied);
const videoMessageAction = useVideoMessageAction(!canSend || typing || isRecording);
const fileUploadAction = useFileUploadAction(!canSend || typing || isRecording);
const webdavActions = useWebdavActions();
const createDiscussionAction = useCreateDiscussionAction(room);
const shareLocationAction = useShareLocationAction(room, tmid);

const apps = useMessageboxAppsActionButtons();
const { composerToolbox: hiddenActions } = useLayoutHiddenActions();

const allActions = {
...(!isHidden(hiddenActions, audioMessageAction) && { audioMessageAction }),
...(!isHidden(hiddenActions, videoMessageAction) && { videoMessageAction }),
...(!isHidden(hiddenActions, fileUploadAction) && { fileUploadAction }),
...(!isHidden(hiddenActions, createDiscussionAction) && { createDiscussionAction }),
...(!isHidden(hiddenActions, shareLocationAction) && { shareLocationAction }),
...(!hiddenActions.includes('webdav-add') && { webdavActions }),
};

const featured = [];
const createNew = [];
const share = [];

createNew.push(allActions.createDiscussionAction);

if (variant === 'small') {
featured.push(allActions.audioMessageAction);
createNew.push(allActions.videoMessageAction, allActions.fileUploadAction);
} else {
featured.push(allActions.audioMessageAction, allActions.videoMessageAction, allActions.fileUploadAction);
}

if (allActions.webdavActions) {
createNew.push(...allActions.webdavActions);
}

share.push(allActions.shareLocationAction);

const groups = {
...(apps.isSuccess &&
apps.data.length > 0 && {
Apps: apps.data,
}),
...messageBox.actions.get(),
};

const messageBoxActions = Object.entries(groups).map(([name, group]) => {
const items: GenericMenuItemProps[] = group
.filter((item) => !hiddenActions.includes(item.id))
.map((item) => ({
id: item.id,
icon: item.icon as ComponentProps<typeof Icon>['name'],
content: t(item.label),
onClick: (event?: MouseEvent<HTMLElement>) =>
item.action({
rid,
tmid,
event: event as unknown as Event,
chat: chatContext,
}),
gap: Boolean(!item.icon),
}));

return {
title: t(name as TranslationKey),
items: items || [],
};
});

const createNewFiltered = createNew.filter(isTruthy);
const shareFiltered = share.filter(isTruthy);

const renderAction = ({ id, icon, content, disabled, onClick }: GenericMenuItemProps) => {
if (!icon) {
return;
}

return <MessageComposerAction key={id} icon={icon} data-qa-id={id} title={content as string} disabled={disabled} onClick={onClick} />;
};

return (
<>
<MessageComposerActionsDivider />
{featured.map((action) => (
<MessageComposerAction key={action.id} {...action} data-qa-id={action.id} icon={action.icon} />
))}
{menu.length > 0 && (
<ActionsToolbarDropdown disabled={isRecording}>
{() =>
menu.map((option) => {
if (typeof option === 'string') {
return <OptionTitle key={option}>{t.has(option) ? t(option) : option}</OptionTitle>;
}

return (
<Option
key={option.id}
onClick={(event) =>
option.onClick({
rid,
tmid,
event: event as unknown as Event,
chat: chatContext,
})
}
gap={!option.icon}
disabled={option.disabled}
>
{option.icon && <OptionIcon name={option.icon as ComponentProps<typeof OptionIcon>['name']} />}
<OptionContent>{option.label}</OptionContent>
</Option>
);
})
}
</ActionsToolbarDropdown>
)}
{featured.map((action) => action && renderAction(action))}
<GenericMenu
disabled={isRecording}
data-qa-id='menu-more-actions'
detached
icon='plus'
sections={[{ title: t('Create_new'), items: createNewFiltered }, { title: t('Share'), items: shareFiltered }, ...messageBoxActions]}
title={t('More_actions')}
/>
</>
);
};
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useSetting, useTranslation } from '@rocket.chat/ui-contexts';
import { useSetting } from '@rocket.chat/ui-contexts';
import { useEffect, useMemo } from 'react';

import { AudioRecorder } from '../../../../../../../app/ui/client/lib/recorderjs/AudioRecorder';
import type { GenericMenuItemProps } from '../../../../../../components/GenericMenu/GenericMenuItem';
import { useChat } from '../../../../contexts/ChatContext';
import { useMediaActionTitle } from '../../hooks/useMediaActionTitle';
import { useMediaPermissions } from '../../hooks/useMediaPermissions';
import type { ToolbarAction } from './ToolbarAction';

const audioRecorder = new AudioRecorder();

export const useAudioMessageAction = (disabled: boolean, isMicrophoneDenied: boolean): ToolbarAction => {
export const useAudioMessageAction = (disabled: boolean, isMicrophoneDenied: boolean): GenericMenuItemProps => {
const isFileUploadEnabled = useSetting('FileUpload_Enabled') as boolean;
const isAudioRecorderEnabled = useSetting('Message_AudioRecorderEnabled') as boolean;
const fileUploadMediaTypeBlackList = useSetting('FileUpload_MediaTypeBlackList') as string;
const fileUploadMediaTypeWhiteList = useSetting('FileUpload_MediaTypeWhiteList') as string;
const [isPermissionDenied] = useMediaPermissions('microphone');
const t = useTranslation();

const isAllowed = useMemo(
() =>
Expand Down Expand Up @@ -57,10 +56,9 @@ export const useAudioMessageAction = (disabled: boolean, isMicrophoneDenied: boo

return {
id: 'audio-message',
title: getMediaActionTitle,
content: getMediaActionTitle,
icon: 'mic',
disabled: !isAllowed || Boolean(disabled),
onClick: handleRecordButtonClick,
icon: 'mic',
label: t('Audio_message'),
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ import { useTranslation, useSetting, usePermission, useSetModal } from '@rocket.
import React from 'react';

import CreateDiscussion from '../../../../../../components/CreateDiscussion';
import type { ToolbarAction } from './ToolbarAction';
import type { GenericMenuItemProps } from '../../../../../../components/GenericMenu/GenericMenuItem';

export const useCreateDiscussionAction = (room?: IRoom): GenericMenuItemProps => {
const t = useTranslation();
const setModal = useSetModal();

export const useCreateDiscussionAction = (room?: IRoom): ToolbarAction => {
if (!room) {
throw new Error('Invalid room');
}

const setModal = useSetModal();
const t = useTranslation();

const handleCreateDiscussion = () =>
setModal(<CreateDiscussion onClose={() => setModal(null)} defaultParentRoom={room?.prid || room?._id} />);

Expand All @@ -25,10 +25,9 @@ export const useCreateDiscussionAction = (room?: IRoom): ToolbarAction => {

return {
id: 'create-discussion',
title: !allowDiscussion ? t('Not_Available') : undefined,
content: t('Discussion'),
icon: 'discussion',
disabled: !allowDiscussion,
onClick: handleCreateDiscussion,
icon: 'discussion',
label: t('Discussion'),
};
};
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { useTranslation, useSetting } from '@rocket.chat/ui-contexts';
import { useEffect } from 'react';

import type { GenericMenuItemProps } from '../../../../../../components/GenericMenu/GenericMenuItem';
import { useFileInput } from '../../../../../../hooks/useFileInput';
import { useChat } from '../../../../contexts/ChatContext';
import type { ToolbarAction } from './ToolbarAction';

const fileInputProps = { type: 'file', multiple: true };

export const useFileUploadAction = (disabled: boolean): ToolbarAction => {
export const useFileUploadAction = (disabled: boolean): GenericMenuItemProps => {
const t = useTranslation();
const fileUploadEnabled = useSetting('FileUpload_Enabled');
const fileInputRef = useFileInput(fileInputProps);
Expand Down Expand Up @@ -43,9 +43,8 @@ export const useFileUploadAction = (disabled: boolean): ToolbarAction => {

return {
id: 'file-upload',
content: t('Upload_file'),
icon: 'clip',
label: t('File'),
title: t('File'),
onClick: handleUpload,
disabled: !fileUploadEnabled || disabled,
};
Expand Down
Loading

0 comments on commit 116ad55

Please sign in to comment.