Skip to content
2 changes: 1 addition & 1 deletion examples/vite/src/stream-imports-layout.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 1 addition & 1 deletion examples/vite/src/stream-imports-theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
1 change: 0 additions & 1 deletion src/components/Dialog/styling/ContextMenu.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
89 changes: 51 additions & 38 deletions src/components/MessageActions/MessageActions.tsx
Original file line number Diff line number Diff line change
@@ -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<ContextMenuItemProps>;
placement: 'dropdown';
};

export type MessageActionSetItem =
| QuickMessageActionSetItem
| DropdownMessageActionSetItem;

export type MessageActionsProps = {
disableBaseMessageActionSetFilter?: boolean;
messageActionSet?: MessageActionSetItem[];
Expand All @@ -39,7 +53,7 @@ export const MessageActions = ({
const { isMyMessage, message } = useMessageContext();
const { t } = useTranslationContext();
const [actionsBoxButtonElement, setActionsBoxButtonElement] =
useState<HTMLButtonElement | null>(null);
useState<HTMLSpanElement | null>(null);

const filteredMessageActionSet = useBaseMessageActionSetFilter(
messageActionSet,
Expand All @@ -59,6 +73,17 @@ export const MessageActions = ({
dialogManager?.id,
);

const contextMenuItems = useMemo<ContextMenuItemComponent[]>(
() =>
dropdownActionSet.map(({ Component }) => {
const ActionItem: ContextMenuItemComponent = (menuProps) => (
<Component {...menuProps} />
);
return ActionItem;
}),
[dropdownActionSet],
);

// do not render anything if total action count is zero
if (dropdownActionSet.length + quickActionSet.length === 0) {
return null;
Expand All @@ -70,20 +95,22 @@ export const MessageActions = ({
'str-chat__message-options--active':
dropdownDialogIsOpen || reactionSelectorDialogIsOpen,
})}
data-testid='message-actions-host'
>
{dropdownActionSet.length > 0 && (
<MessageActionsWrapper inline={false} toggleOpen={dialog?.toggle}>
<button
<>
<ContextMenuButton
aria-expanded={dropdownDialogIsOpen}
aria-haspopup='true'
aria-label={t('aria/Open Message Actions Menu')}
className='str-chat__message-actions-box-button'
data-testid='message-actions-toggle-button'
onClick={() => {
dialog?.toggle();
}}
ref={setActionsBoxButtonElement}
>
<ActionsIcon className='str-chat__message-action-icon' />
</button>
</ContextMenuButton>

<DialogAnchor
dialogManagerId={dialogManager?.id}
Expand All @@ -93,36 +120,22 @@ export const MessageActions = ({
tabIndex={-1}
trapFocus
>
<DropdownBox open={dropdownDialogIsOpen}>
{dropdownActionSet.map(({ Component: DropdownActionComponent, type }) => (
<DropdownActionComponent key={type} />
))}
</DropdownBox>
<ContextMenu
backLabel={t('Back')}
className={clsx(
'str-chat__message-actions-box',
{ 'str-chat__message-actions-box--open': dropdownDialogIsOpen },
'str-chat__dialog-menu',
)}
items={contextMenuItems}
onClose={dialog?.close}
/>
</DialogAnchor>
</MessageActionsWrapper>
</>
)}
{quickActionSet.map(({ Component: QuickActionComponent, type }) => (
<QuickActionComponent key={type} />
))}
</div>
);
};

const DropdownBox = ({ children, open }: PropsWithChildren<{ open: boolean }>) => {
const { t } = useTranslationContext();
return (
<div
className={clsx('str-chat__message-actions-box', {
'str-chat__message-actions-box--open': open,
})}
>
<div
aria-label={t('aria/Message Options')}
className='str-chat__message-actions-list'
role='listbox'
>
{children}
</div>
</div>
);
};
43 changes: 32 additions & 11 deletions src/components/MessageActions/RemindMeSubmenu.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,69 @@
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 (
<ContextMenuButton
aria-selected='false'
className={className}
// Submenu={RemindMeSubmenu}
// submenuPlacement={isMine ? 'left-start' : 'right-start'}
>
<ContextMenuButton aria-selected='false' className={className} {...props}>
{t('Remind Me')}
</ContextMenuButton>
);
};

export const RemindMeSubmenuHeader = () => {
const { t } = useTranslationContext();
const { returnToParentMenu } = useContextMenuContext();
return (
<ContextMenuHeader>
<ContextMenuBackButton onClick={returnToParentMenu}>
<IconChevronRight />
<span>{t('Remind Me')}</span>
</ContextMenuBackButton>
</ContextMenuHeader>
);
};

export const RemindMeSubmenu = () => {
const { t } = useTranslationContext();
const { client } = useChatContext();
const { message } = useMessageContext();
const { closeMenu } = useContextMenuContext();
return (
<div
aria-label={t('aria/Remind Me Options')}
className='str-chat__message-actions-box__submenu'
role='listbox'
>
{client.reminders.scheduledOffsetsMs.map((offsetMs) => (
<button
<ContextMenuButton
className='str-chat__message-actions-list-item-button'
key={`reminder-offset-option--${offsetMs}`}
onClick={() => {
client.reminders.upsertReminder({
messageId: message.id,
remind_at: new Date(new Date().getTime() + offsetMs).toISOString(),
});
closeMenu();
}}
>
{t('duration/Remind Me', { milliseconds: offsetMs })}
</button>
</ContextMenuButton>
))}
{/* todo: potential improvement to add a custom option that would trigger rendering modal with custom date picker - we need date picker */}
</div>
Expand Down
Loading
Loading