diff --git a/examples/vite/src/stream-imports-layout.scss b/examples/vite/src/stream-imports-layout.scss index 9b13ffc4f..d298996e9 100644 --- a/examples/vite/src/stream-imports-layout.scss +++ b/examples/vite/src/stream-imports-layout.scss @@ -27,7 +27,7 @@ @use 'stream-chat-react/dist/scss/v2/LoadingIndicator/LoadingIndicator-layout'; @use 'stream-chat-react/dist/scss/v2/Location/Location-layout'; //@use 'stream-chat-react/dist/scss/v2/Message/Message-layout'; -@use 'stream-chat-react/dist/scss/v2/MessageActionsBox/MessageActionsBox-layout'; +//@use 'stream-chat-react/dist/scss/v2/MessageActionsBox/MessageActionsBox-layout'; @use 'stream-chat-react/dist/scss/v2/MessageBouncePrompt/MessageBouncePrompt-layout'; //@use 'stream-chat-react/dist/scss/v2/MessageInput/MessageInput-layout'; // X @use 'stream-chat-react/dist/scss/v2/MessageList/MessageList-layout'; diff --git a/examples/vite/src/stream-imports-theme.scss b/examples/vite/src/stream-imports-theme.scss index a41570fe1..e3934226a 100644 --- a/examples/vite/src/stream-imports-theme.scss +++ b/examples/vite/src/stream-imports-theme.scss @@ -24,7 +24,7 @@ @use 'stream-chat-react/dist/scss/v2/LoadingIndicator/LoadingIndicator-theme'; @use 'stream-chat-react/dist/scss/v2/Location/Location-theme'; //@use 'stream-chat-react/dist/scss/v2/Message/Message-theme'; -@use 'stream-chat-react/dist/scss/v2/MessageActionsBox/MessageActionsBox-theme'; +//@use 'stream-chat-react/dist/scss/v2/MessageActionsBox/MessageActionsBox-theme'; @use 'stream-chat-react/dist/scss/v2/MessageBouncePrompt/MessageBouncePrompt-theme'; @use 'stream-chat-react/dist/scss/v2/MessageList/MessageList-theme'; @use 'stream-chat-react/dist/scss/v2/MessageList/VirtualizedMessageList-theme'; diff --git a/src/components/Dialog/styling/ContextMenu.scss b/src/components/Dialog/styling/ContextMenu.scss index 4d242c612..3058491fb 100644 --- a/src/components/Dialog/styling/ContextMenu.scss +++ b/src/components/Dialog/styling/ContextMenu.scss @@ -132,7 +132,6 @@ .str-chat__context-menu__button__label { @include utils.ellipsis-text; - max-width: 80%; flex: auto; text-align: left; color: var(--text-primary); diff --git a/src/components/MessageActions/MessageActions.tsx b/src/components/MessageActions/MessageActions.tsx index 3371fb92c..2522bd94b 100644 --- a/src/components/MessageActions/MessageActions.tsx +++ b/src/components/MessageActions/MessageActions.tsx @@ -1,25 +1,39 @@ import clsx from 'clsx'; -import React, { useState } from 'react'; -import type { PropsWithChildren } from 'react'; +import React, { useMemo, useState } from 'react'; import { useChatContext, useMessageContext, useTranslationContext } from '../../context'; -import { ActionsIcon } from '../../components/Message/icons'; import { + ContextMenu, + ContextMenuButton, + type ContextMenuItemComponent, + type ContextMenuItemProps, DialogAnchor, useDialogIsOpen, useDialogOnNearestManager, -} from '../../components/Dialog'; -import { MessageActionsWrapper } from './MessageActionsWrapper'; +} from '../Dialog'; import { useBaseMessageActionSetFilter, useSplitMessageActionSet } from './hooks'; import { defaultMessageActionSet } from './defaults'; -import type { MESSAGE_ACTIONS } from '../Message/utils'; +import { ActionsIcon, type MESSAGE_ACTIONS } from '../Message'; -export type MessageActionSetItem = { - Component: React.ComponentType; +type BaseMessageActionSetItem = { placement: 'quick' | 'dropdown'; type: keyof typeof MESSAGE_ACTIONS | (string & {}); }; +export type QuickMessageActionSetItem = BaseMessageActionSetItem & { + Component: React.ComponentType; + placement: 'quick'; +}; + +export type DropdownMessageActionSetItem = BaseMessageActionSetItem & { + Component: React.ComponentType; + placement: 'dropdown'; +}; + +export type MessageActionSetItem = + | QuickMessageActionSetItem + | DropdownMessageActionSetItem; + export type MessageActionsProps = { disableBaseMessageActionSetFilter?: boolean; messageActionSet?: MessageActionSetItem[]; @@ -39,7 +53,7 @@ export const MessageActions = ({ const { isMyMessage, message } = useMessageContext(); const { t } = useTranslationContext(); const [actionsBoxButtonElement, setActionsBoxButtonElement] = - useState(null); + useState(null); const filteredMessageActionSet = useBaseMessageActionSetFilter( messageActionSet, @@ -59,6 +73,17 @@ export const MessageActions = ({ dialogManager?.id, ); + const contextMenuItems = useMemo( + () => + dropdownActionSet.map(({ Component }) => { + const ActionItem: ContextMenuItemComponent = (menuProps) => ( + + ); + return ActionItem; + }), + [dropdownActionSet], + ); + // do not render anything if total action count is zero if (dropdownActionSet.length + quickActionSet.length === 0) { return null; @@ -70,20 +95,22 @@ export const MessageActions = ({ 'str-chat__message-options--active': dropdownDialogIsOpen || reactionSelectorDialogIsOpen, })} - data-testid='message-actions-host' > {dropdownActionSet.length > 0 && ( - - + - - {dropdownActionSet.map(({ Component: DropdownActionComponent, type }) => ( - - ))} - + - + )} {quickActionSet.map(({ Component: QuickActionComponent, type }) => ( @@ -107,22 +139,3 @@ export const MessageActions = ({ ); }; - -const DropdownBox = ({ children, open }: PropsWithChildren<{ open: boolean }>) => { - const { t } = useTranslationContext(); - return ( -
-
- {children} -
-
- ); -}; diff --git a/src/components/MessageActions/RemindMeSubmenu.tsx b/src/components/MessageActions/RemindMeSubmenu.tsx index c68ba18d7..b7e12e8e5 100644 --- a/src/components/MessageActions/RemindMeSubmenu.tsx +++ b/src/components/MessageActions/RemindMeSubmenu.tsx @@ -1,29 +1,49 @@ import React from 'react'; import { useChatContext, useMessageContext, useTranslationContext } from '../../context'; -import { ContextMenuButton } from '../Dialog'; -import type { ComponentProps } from 'react'; +import { + ContextMenuBackButton, + ContextMenuButton, + ContextMenuHeader, + useContextMenuContext, +} from '../Dialog'; +import type { BaseContextMenuButtonProps } from '../Dialog'; +import { IconChevronRight } from '../Icons'; + +// todo: do we need to have isMine as a prop? +export type RemindMeActionButtonProps = { isMine: boolean } & BaseContextMenuButtonProps; export const RemindMeActionButton = ({ className, -}: { isMine: boolean } & ComponentProps<'button'>) => { + isMine: _, // eslint-disable-line @typescript-eslint/no-unused-vars + ...props +}: RemindMeActionButtonProps) => { const { t } = useTranslationContext(); return ( - + {t('Remind Me')} ); }; +export const RemindMeSubmenuHeader = () => { + const { t } = useTranslationContext(); + const { returnToParentMenu } = useContextMenuContext(); + return ( + + + + {t('Remind Me')} + + + ); +}; + export const RemindMeSubmenu = () => { const { t } = useTranslationContext(); const { client } = useChatContext(); const { message } = useMessageContext(); + const { closeMenu } = useContextMenuContext(); return (
{ role='listbox' > {client.reminders.scheduledOffsetsMs.map((offsetMs) => ( - + ))} {/* todo: potential improvement to add a custom option that would trigger rendering modal with custom date picker - we need date picker */}
diff --git a/src/components/MessageActions/defaults.tsx b/src/components/MessageActions/defaults.tsx index 3b833f4cc..575365e65 100644 --- a/src/components/MessageActions/defaults.tsx +++ b/src/components/MessageActions/defaults.tsx @@ -1,5 +1,4 @@ /* eslint-disable sort-keys */ -import type { ComponentPropsWithoutRef } from 'react'; import React from 'react'; import { isUserMuted, useMessageComposer, useMessageReminder } from '../../components'; @@ -9,27 +8,21 @@ import { } from '../../components/Message/icons'; import { ReactionSelectorWithButton } from '../../components/Reactions/ReactionSelectorWithButton'; import { useChatContext, useMessageContext, useTranslationContext } from '../../context'; -import { RemindMeActionButton } from '../../components/MessageActions/RemindMeSubmenu'; +import { + RemindMeActionButton, + RemindMeSubmenu, + RemindMeSubmenuHeader, +} from '../../components/MessageActions/RemindMeSubmenu'; +import { ContextMenuButton } from '../../components/Dialog'; +import type { ContextMenuItemProps } from '../../components/Dialog'; import type { MessageActionSetItem } from './MessageActions'; const msgActionsBoxButtonClassName = 'str-chat__message-actions-list-item-button' as const; -export const DefaultDropdownActionButton = ({ - 'aria-selected': ariaSelected = 'false', - children, - className = msgActionsBoxButtonClassName, - role = 'option', - ...rest -}: ComponentPropsWithoutRef<'button'>) => ( - -); - const DefaultMessageActionComponents = { dropdown: { - Quote() { + Quote({ closeMenu }: ContextMenuItemProps) { const { message } = useMessageContext(); const { t } = useTranslationContext(); const messageComposer = useMessageComposer(); @@ -48,100 +41,153 @@ const DefaultMessageActionComponents = { }; return ( - + { + handleQuote(); + closeMenu(); + }} + > {t('Quote')} - +
); }, - Pin() { + Pin({ closeMenu }: ContextMenuItemProps) { const { handlePin, message } = useMessageContext(); const { t } = useTranslationContext(); return ( - + { + handlePin(event); + closeMenu(); + }} + > {!message.pinned ? t('Pin') : t('Unpin')} - + ); }, - MarkUnread() { + MarkUnread({ closeMenu }: ContextMenuItemProps) { const { handleMarkUnread } = useMessageContext(); const { t } = useTranslationContext(); return ( - + { + handleMarkUnread(event); + closeMenu(); + }} + > {t('Mark as unread')} - + ); }, - Flag() { + Flag({ closeMenu }: ContextMenuItemProps) { const { handleFlag } = useMessageContext(); const { t } = useTranslationContext(); return ( - + { + handleFlag(event); + closeMenu(); + }} + > {t('Flag')} - + ); }, - Mute() { + Mute({ closeMenu }: ContextMenuItemProps) { const { handleMute, message } = useMessageContext(); const { mutes } = useChatContext(); const { t } = useTranslationContext(); return ( - + { + handleMute(event); + closeMenu(); + }} + > {isUserMuted(message, mutes) ? t('Unmute') : t('Mute')} - + ); }, - Edit() { + Edit({ closeMenu }: ContextMenuItemProps) { const messageComposer = useMessageComposer(); const { message } = useMessageContext(); const { t } = useTranslationContext(); return ( - messageComposer.initState({ composition: message })} + { + messageComposer.initState({ composition: message }); + closeMenu(); + }} > {t('Edit Message')} - + ); }, - Delete() { + Delete({ closeMenu }: ContextMenuItemProps) { const { handleDelete } = useMessageContext(); const { t } = useTranslationContext(); return ( - + { + handleDelete(event); + closeMenu(); + }} + > {t('Delete')} - + ); }, - RemindMe() { + RemindMe({ openSubmenu }: ContextMenuItemProps) { const { isMyMessage } = useMessageContext(); return ( { + openSubmenu({ + Header: RemindMeSubmenuHeader, + Submenu: RemindMeSubmenu, + }); + }} /> ); }, - SaveForLater() { + SaveForLater({ closeMenu }: ContextMenuItemProps) { const { client } = useChatContext(); const { message } = useMessageContext(); const { t } = useTranslationContext(); const reminder = useMessageReminder(message.id); return ( - - reminder - ? client.reminders.deleteReminder(reminder.id) - : client.reminders.createReminder({ messageId: message.id }) - } + { + if (reminder) { + client.reminders.deleteReminder(reminder.id); + } else { + client.reminders.createReminder({ messageId: message.id }); + } + closeMenu(); + }} > {reminder ? t('Remove reminder') : t('Save for later')} - + ); }, }, diff --git a/src/components/MessageActions/hooks/useSplitMessageActionSet.ts b/src/components/MessageActions/hooks/useSplitMessageActionSet.ts index d824c5038..17f3ecfab 100644 --- a/src/components/MessageActions/hooks/useSplitMessageActionSet.ts +++ b/src/components/MessageActions/hooks/useSplitMessageActionSet.ts @@ -1,16 +1,20 @@ import { useMemo } from 'react'; -import type { MessageActionSetItem } from '../MessageActions'; +import type { + DropdownMessageActionSetItem, + MessageActionSetItem, + QuickMessageActionSetItem, +} from '../MessageActions'; export const useSplitMessageActionSet = (messageActionSet: MessageActionSetItem[]) => useMemo(() => { - const quickActionSet: MessageActionSetItem[] = []; - const dropdownActionSet: MessageActionSetItem[] = []; + const quickActionSet: QuickMessageActionSetItem[] = []; + const dropdownActionSet: DropdownMessageActionSetItem[] = []; for (const action of messageActionSet) { if (action.placement === 'quick') quickActionSet.push(action); if (action.placement === 'dropdown') dropdownActionSet.push(action); } - return { dropdownActionSet, quickActionSet }; + return { dropdownActionSet, quickActionSet } as const; }, [messageActionSet]); diff --git a/src/components/MessageActions/styling/MessageActions.scss b/src/components/MessageActions/styling/MessageActions.scss new file mode 100644 index 000000000..e48bf8cc9 --- /dev/null +++ b/src/components/MessageActions/styling/MessageActions.scss @@ -0,0 +1,3 @@ +.str-chat__message-actions-box { + min-width: 180px; +} \ No newline at end of file diff --git a/src/components/MessageActions/styling/index.scss b/src/components/MessageActions/styling/index.scss new file mode 100644 index 000000000..734fc67d7 --- /dev/null +++ b/src/components/MessageActions/styling/index.scss @@ -0,0 +1 @@ +@use "MessageActions"; \ No newline at end of file diff --git a/src/components/MessageInput/MessageComposerActions.tsx b/src/components/MessageInput/MessageComposerActions.tsx index 4cad184e6..a1450dd7b 100644 --- a/src/components/MessageInput/MessageComposerActions.tsx +++ b/src/components/MessageInput/MessageComposerActions.tsx @@ -39,7 +39,7 @@ export const MessageComposerActions = () => { messageComposerStateSelector, ); - const { command, text } = useStateStore( + const { command } = useStateStore( messageComposer.textComposer.state, textComposerStateSelector, ); @@ -81,7 +81,7 @@ export const MessageComposerActions = () => { if (isCooldownActive) { content = ; - } else if ((compositionIsEmpty || (editedMessage && !text)) && recordingEnabled) { + } else if (compositionIsEmpty && !editedMessage && recordingEnabled) { content = ; } diff --git a/src/styling/index.scss b/src/styling/index.scss index 7d90d3136..3740fea36 100644 --- a/src/styling/index.scss +++ b/src/styling/index.scss @@ -24,6 +24,7 @@ @use "../components/Avatar/styling/Avatar" as Avatar; @use "../components/MediaRecorder/AudioRecorder/styling" as AudioRecorder; @use "../components/Message/styling" as Message; +@use "../components/MessageActions/styling" as MessageActions; @use "../components/MessageInput/styling" as MessageComposer; @use "../components/Reactions/styling/ReactionSelector" as ReactionSelector; @use "../components/TextareaComposer/styling/" as TextareaComposer;