mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
839 lines
30 KiB
TypeScript
839 lines
30 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import Accessibility from '@app/features/accessibility/state/Accessibility';
|
|
import {useAnimatedMediaPlaybackAllowed} from '@app/features/app/hooks/useAnimatedMediaPlayback';
|
|
import {useContextMenuHoverState} from '@app/features/app/hooks/useContextMenuHoverState';
|
|
import styles from '@app/features/channel/components/MessageActionBar.module.css';
|
|
import {
|
|
createMessageActionHandlers,
|
|
getCopyableMessageText,
|
|
isClientSystemMessage,
|
|
isEmbedsSuppressed,
|
|
useMessagePermissions,
|
|
} from '@app/features/channel/components/MessageActionUtils';
|
|
import {REACT_WITH_EMOJI_DESCRIPTOR} from '@app/features/channel/components/QuickReactionsRow';
|
|
import type {Channel} from '@app/features/channel/models/Channel';
|
|
import {useQuickReactionEmojis} from '@app/features/channel/state/QuickReactionStore';
|
|
import {MessageDebugModal} from '@app/features/devtools/components/debug/MessageDebugModal';
|
|
import * as EmojiPickerCommands from '@app/features/emoji/commands/EmojiPickerCommands';
|
|
import {EmojiPickerPopout} from '@app/features/emoji/components/popouts/EmojiPickerPopout';
|
|
import type {FlatEmoji} from '@app/features/emoji/types/EmojiTypes';
|
|
import {getEmojiDisplayData} from '@app/features/expressions/utils/SkinToneUtils';
|
|
import {
|
|
ADD_REACTION_DESCRIPTOR,
|
|
BOOKMARK_MESSAGE_DESCRIPTOR,
|
|
COPY_MESSAGE_ID_DESCRIPTOR,
|
|
COPY_MESSAGE_LINK_DESCRIPTOR,
|
|
COPY_TEXT_DESCRIPTOR,
|
|
DELETE_MESSAGE_DESCRIPTOR,
|
|
EDIT_MESSAGE_DESCRIPTOR,
|
|
MARK_AS_UNREAD_DESCRIPTOR,
|
|
PIN_MESSAGE_DESCRIPTOR,
|
|
REMOVE_BOOKMARK_DESCRIPTOR,
|
|
REPLY_DESCRIPTOR,
|
|
SUPPRESS_EMBEDS_DESCRIPTOR,
|
|
TRY_AGAIN_DESCRIPTOR,
|
|
UNPIN_MESSAGE_DESCRIPTOR,
|
|
} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
|
import type {Message} from '@app/features/messaging/models/MessagingMessage';
|
|
import SavedMessages from '@app/features/messaging/state/SavedMessages';
|
|
import {getEmojiNameWithColons, toReactionEmoji} from '@app/features/messaging/utils/ReactionUtils';
|
|
import {ComponentDispatch} from '@app/features/platform/utils/ComponentBus';
|
|
import messageStyles from '@app/features/theme/styles/Message.module.css';
|
|
import {
|
|
AddReactionIcon,
|
|
BookmarkIcon,
|
|
CopyIdIcon,
|
|
CopyLinkIcon,
|
|
CopyMessageTextIcon,
|
|
DebugMessageIcon,
|
|
DeleteIcon,
|
|
EditMessageIcon,
|
|
ForwardIcon,
|
|
MarkAsUnreadIcon,
|
|
MoreIcon,
|
|
PinIcon,
|
|
ReplyIcon,
|
|
RetryIcon,
|
|
SuppressEmbedsIcon,
|
|
} from '@app/features/ui/action_menu/ContextMenuIcons';
|
|
import {MessageContextMenu} from '@app/features/ui/action_menu/MessageContextMenu';
|
|
import * as ContextMenuCommands from '@app/features/ui/commands/ContextMenuCommands';
|
|
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
|
|
import {modal} from '@app/features/ui/commands/ModalCommands';
|
|
import * as PopoutCommands from '@app/features/ui/commands/PopoutCommands';
|
|
import FocusRing from '@app/features/ui/focus_ring/FocusRing';
|
|
import {Popout} from '@app/features/ui/popover/PopoverPopout';
|
|
import ContextMenu from '@app/features/ui/state/ContextMenu';
|
|
import KeyboardMode from '@app/features/ui/state/KeyboardMode';
|
|
import {Tooltip} from '@app/features/ui/tooltip/Tooltip';
|
|
import {canUseWindowFocusedActivationClick} from '@app/features/ui/utils/WindowFocusInteractionGuard';
|
|
import UserSettings from '@app/features/user/state/UserSettings';
|
|
import * as AvatarUtils from '@app/features/user/utils/AvatarUtils';
|
|
import {MessageStates} from '@fluxer/constants/src/ChannelConstants';
|
|
import {msg} from '@lingui/core/macro';
|
|
import {useLingui} from '@lingui/react/macro';
|
|
import {clsx} from 'clsx';
|
|
import {observer} from 'mobx-react-lite';
|
|
import React, {useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore} from 'react';
|
|
|
|
const CLICK_TO_REACT_DESCRIPTOR = msg({
|
|
message: 'Click to react',
|
|
comment: 'Tooltip on the add-reaction button in the inline message hover action bar.',
|
|
});
|
|
const MESSAGE_DEBUG_DESCRIPTOR = msg({
|
|
message: 'Message debug',
|
|
comment: 'Title of the developer-mode message debug modal opened from the message action bar.',
|
|
});
|
|
const MORE_DESCRIPTOR = msg({
|
|
message: 'More',
|
|
comment: 'Tooltip on the overflow button in the inline message hover action bar. Opens the full action menu.',
|
|
});
|
|
const DEBUG_MESSAGE_DESCRIPTOR = msg({
|
|
message: 'Debug message',
|
|
comment: 'Developer-mode item in the message action bar overflow menu. Opens the message debug modal.',
|
|
});
|
|
const UNSUPPRESS_EMBEDS_DESCRIPTOR = msg({
|
|
message: 'Unsuppress embeds',
|
|
comment: 'Item in the message action bar overflow menu. Re-shows previously hidden link previews and embeds.',
|
|
});
|
|
const FORWARD_DESCRIPTOR = msg({
|
|
message: 'Forward',
|
|
comment: 'Tooltip on the forward button in the inline message hover action bar.',
|
|
});
|
|
const shiftKeyManager = (() => {
|
|
let isShiftPressed = false;
|
|
const listeners = new Set<() => void>();
|
|
const notify = () => {
|
|
listeners.forEach((listener) => listener());
|
|
};
|
|
const setShiftPressed = (pressed: boolean) => {
|
|
if (isShiftPressed !== pressed) {
|
|
isShiftPressed = pressed;
|
|
notify();
|
|
}
|
|
};
|
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
if (event.key === 'Shift') {
|
|
setShiftPressed(true);
|
|
}
|
|
};
|
|
const handleKeyUp = (event: KeyboardEvent) => {
|
|
if (event.key === 'Shift') {
|
|
setShiftPressed(false);
|
|
}
|
|
};
|
|
const handleWindowBlur = () => {
|
|
setShiftPressed(false);
|
|
};
|
|
const handlePointerModifierChange = (event: MouseEvent) => {
|
|
setShiftPressed(event.getModifierState?.('Shift') ?? event.shiftKey);
|
|
};
|
|
const supportsPointerModifierEvents = (): boolean => 'PointerEvent' in window;
|
|
const addPointerModifierListeners = () => {
|
|
if (supportsPointerModifierEvents()) {
|
|
window.addEventListener('pointerover', handlePointerModifierChange, true);
|
|
window.addEventListener('pointerdown', handlePointerModifierChange, true);
|
|
window.addEventListener('pointerup', handlePointerModifierChange, true);
|
|
return;
|
|
}
|
|
window.addEventListener('mouseover', handlePointerModifierChange, true);
|
|
window.addEventListener('mousedown', handlePointerModifierChange, true);
|
|
window.addEventListener('mouseup', handlePointerModifierChange, true);
|
|
};
|
|
const removePointerModifierListeners = () => {
|
|
if (supportsPointerModifierEvents()) {
|
|
window.removeEventListener('pointerover', handlePointerModifierChange, true);
|
|
window.removeEventListener('pointerdown', handlePointerModifierChange, true);
|
|
window.removeEventListener('pointerup', handlePointerModifierChange, true);
|
|
return;
|
|
}
|
|
window.removeEventListener('mouseover', handlePointerModifierChange, true);
|
|
window.removeEventListener('mousedown', handlePointerModifierChange, true);
|
|
window.removeEventListener('mouseup', handlePointerModifierChange, true);
|
|
};
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
window.addEventListener('keyup', handleKeyUp);
|
|
window.addEventListener('blur', handleWindowBlur);
|
|
return {
|
|
subscribe: (listener: () => void) => {
|
|
if (listeners.size === 0) {
|
|
addPointerModifierListeners();
|
|
}
|
|
listeners.add(listener);
|
|
return () => {
|
|
listeners.delete(listener);
|
|
if (listeners.size === 0) {
|
|
removePointerModifierListeners();
|
|
}
|
|
};
|
|
},
|
|
getSnapshot: () => isShiftPressed,
|
|
getServerSnapshot: () => false,
|
|
};
|
|
})();
|
|
const useShiftKey = (enabled: boolean) => {
|
|
const subscribe = useCallback(
|
|
(listener: () => void) => {
|
|
if (!enabled) {
|
|
return () => undefined;
|
|
}
|
|
return shiftKeyManager.subscribe(listener);
|
|
},
|
|
[enabled],
|
|
);
|
|
const getSnapshot = useCallback(() => {
|
|
return enabled ? shiftKeyManager.getSnapshot() : false;
|
|
}, [enabled]);
|
|
return useSyncExternalStore(subscribe, getSnapshot, shiftKeyManager.getServerSnapshot);
|
|
};
|
|
const suppressBlockedActivationClick = (event: React.SyntheticEvent): boolean => {
|
|
if (canUseWindowFocusedActivationClick()) {
|
|
return false;
|
|
}
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
return true;
|
|
};
|
|
|
|
interface MessageActionBarButtonProps {
|
|
label: string;
|
|
icon: React.ReactNode;
|
|
onClick?: (event: React.MouseEvent | React.KeyboardEvent) => void;
|
|
onPointerDownCapture?: (event: React.PointerEvent) => void;
|
|
danger?: boolean;
|
|
isActive?: boolean;
|
|
hidden?: boolean;
|
|
dataAction?: string;
|
|
}
|
|
|
|
const MessageActionBarButton = React.forwardRef<HTMLButtonElement, MessageActionBarButtonProps>(
|
|
({label, icon, onClick, onPointerDownCapture, danger, isActive, hidden, dataAction}, ref) => {
|
|
const handleClick = useCallback(
|
|
(event: React.MouseEvent | React.KeyboardEvent) => {
|
|
if (suppressBlockedActivationClick(event)) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
onClick?.(event);
|
|
},
|
|
[onClick],
|
|
);
|
|
const handlePointerDownCapture = useCallback(
|
|
(event: React.PointerEvent) => {
|
|
if (suppressBlockedActivationClick(event)) {
|
|
return;
|
|
}
|
|
onPointerDownCapture?.(event);
|
|
},
|
|
[onPointerDownCapture],
|
|
);
|
|
const buttonClassName = useMemo(
|
|
() => clsx(styles.button, danger && styles.danger, isActive && styles.active),
|
|
[danger, isActive],
|
|
);
|
|
return (
|
|
<Tooltip text={label} data-flx="channel.message-action-bar.message-action-bar-button.tooltip">
|
|
<FocusRing data-flx="channel.message-action-bar.message-action-bar-button.focus-ring">
|
|
<button
|
|
type="button"
|
|
ref={ref}
|
|
aria-label={label}
|
|
hidden={hidden}
|
|
onClick={handleClick}
|
|
onPointerDownCapture={handlePointerDownCapture}
|
|
className={buttonClassName}
|
|
data-action={dataAction}
|
|
data-flx="channel.message-action-bar.message-action-bar-button.button.click"
|
|
>
|
|
<div
|
|
className={styles.actionBarIcon}
|
|
data-flx="channel.message-action-bar.message-action-bar-button.action-bar-icon"
|
|
>
|
|
{icon}
|
|
</div>
|
|
</button>
|
|
</FocusRing>
|
|
</Tooltip>
|
|
);
|
|
},
|
|
);
|
|
|
|
MessageActionBarButton.displayName = 'MessageActionBarButton';
|
|
|
|
interface QuickReactionButtonProps {
|
|
emoji: FlatEmoji;
|
|
onReact: (emoji: FlatEmoji) => void;
|
|
}
|
|
|
|
const QuickReactionButton = observer(
|
|
React.forwardRef<HTMLButtonElement, QuickReactionButtonProps>(({emoji, onReact}, ref) => {
|
|
const {i18n} = useLingui();
|
|
const [isHovered, setIsHovered] = useState(false);
|
|
const animatedMediaPlaybackAllowed = useAnimatedMediaPlaybackAllowed();
|
|
const {url: displayUrl} = getEmojiDisplayData(emoji);
|
|
const handleClick = useCallback(
|
|
(event: React.MouseEvent | React.KeyboardEvent) => {
|
|
if (suppressBlockedActivationClick(event)) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
EmojiPickerCommands.trackEmojiUsage(emoji);
|
|
onReact(emoji);
|
|
},
|
|
[emoji, onReact],
|
|
);
|
|
const handleMouseEnter = useCallback(() => setIsHovered(true), []);
|
|
const handleMouseLeave = useCallback(() => setIsHovered(false), []);
|
|
const emojiNameWithColons = useMemo(() => getEmojiNameWithColons(toReactionEmoji(emoji)), [emoji]);
|
|
const shouldShowAnimated = useMemo(
|
|
() => emoji.animated && isHovered && animatedMediaPlaybackAllowed,
|
|
[animatedMediaPlaybackAllowed, emoji.animated, isHovered],
|
|
);
|
|
const emojiSrc = useMemo(
|
|
() =>
|
|
emoji.animated && emoji.id
|
|
? AvatarUtils.getEmojiURL({id: emoji.id, animated: shouldShowAnimated})
|
|
: (displayUrl ?? ''),
|
|
[emoji.animated, emoji.id, displayUrl, shouldShowAnimated],
|
|
);
|
|
const tooltipContent = useCallback(
|
|
() => (
|
|
<div className={styles.tooltipContent} data-flx="channel.message-action-bar.tooltip-content.tooltip-content">
|
|
<span data-flx="channel.message-action-bar.tooltip-content.span">{emojiNameWithColons}</span>
|
|
<span className={styles.tooltipHint} data-flx="channel.message-action-bar.tooltip-content.tooltip-hint">
|
|
{i18n._(CLICK_TO_REACT_DESCRIPTOR)}
|
|
</span>
|
|
</div>
|
|
),
|
|
[emojiNameWithColons, i18n],
|
|
);
|
|
const ariaLabel = useMemo(
|
|
() => i18n._(REACT_WITH_EMOJI_DESCRIPTOR, {emojiShortcode: emojiNameWithColons}),
|
|
[emojiNameWithColons, i18n.locale],
|
|
);
|
|
return (
|
|
<Tooltip text={tooltipContent} data-flx="channel.message-action-bar.tooltip">
|
|
<FocusRing data-flx="channel.message-action-bar.focus-ring">
|
|
<button
|
|
type="button"
|
|
ref={ref}
|
|
aria-label={ariaLabel}
|
|
onClick={handleClick}
|
|
onMouseEnter={handleMouseEnter}
|
|
onMouseLeave={handleMouseLeave}
|
|
onFocus={handleMouseEnter}
|
|
onBlur={handleMouseLeave}
|
|
className={styles.button}
|
|
data-flx="channel.message-action-bar.button.click"
|
|
>
|
|
<img
|
|
src={emojiSrc}
|
|
alt={emoji.name}
|
|
className={styles.emojiImage}
|
|
data-flx="channel.message-action-bar.emoji-image"
|
|
/>
|
|
</button>
|
|
</FocusRing>
|
|
</Tooltip>
|
|
);
|
|
}),
|
|
);
|
|
|
|
QuickReactionButton.displayName = 'QuickReactionButton';
|
|
|
|
interface MessageActionBarCoreProps {
|
|
message: Message;
|
|
handleDelete: (bypassConfirm?: boolean) => void;
|
|
permissions: {
|
|
channel: Channel;
|
|
canSendMessages: boolean;
|
|
canAddReactions: boolean;
|
|
canEditMessage: boolean;
|
|
canDeleteMessage: boolean;
|
|
canPinMessage: boolean;
|
|
canForwardMessage: boolean;
|
|
shouldRenderSuppressEmbeds: boolean;
|
|
};
|
|
isSaved: boolean;
|
|
developerMode: boolean;
|
|
onPopoutToggle?: (isOpen: boolean) => void;
|
|
}
|
|
|
|
export const MessageActionBarCore: React.FC<MessageActionBarCoreProps> = observer(
|
|
({message, handleDelete, permissions, isSaved, developerMode, onPopoutToggle}) => {
|
|
const {i18n} = useLingui();
|
|
const [emojiPickerOpen, setEmojiPickerOpen] = useState(false);
|
|
const moreOptionsButtonRef = useRef<HTMLButtonElement>(null);
|
|
const emojiPickerButtonRef = useRef<HTMLButtonElement>(null);
|
|
const actionBarRef = useRef<HTMLDivElement>(null);
|
|
const keepOpenOnNextMoreMenuCloseRef = useRef(false);
|
|
const contextMenuOpen = useContextMenuHoverState(actionBarRef);
|
|
const moreMenuOpen = useContextMenuHoverState(moreOptionsButtonRef);
|
|
const showMessageActionBar = Accessibility.showMessageActionBar;
|
|
const showQuickReactions = Accessibility.showMessageActionBarQuickReactions;
|
|
const showShiftExpand = Accessibility.showMessageActionBarShiftExpand;
|
|
const onlyMoreButton = Accessibility.showMessageActionBarOnlyMoreButton;
|
|
const keyboardModeEnabled = KeyboardMode.keyboardModeEnabled;
|
|
const shouldListenForShift = showShiftExpand && showMessageActionBar && !onlyMoreButton && !keyboardModeEnabled;
|
|
const shiftPressed = useShiftKey(shouldListenForShift);
|
|
const showFullActions = showShiftExpand && shiftPressed;
|
|
const {
|
|
canSendMessages,
|
|
canAddReactions,
|
|
canEditMessage,
|
|
canDeleteMessage,
|
|
canPinMessage,
|
|
canForwardMessage,
|
|
shouldRenderSuppressEmbeds,
|
|
} = permissions;
|
|
const supportsInteractiveActions = useMemo(() => !isClientSystemMessage(message), [message]);
|
|
const handlers = useMemo(
|
|
() => createMessageActionHandlers(message, {i18n, channel: permissions.channel}),
|
|
[message, i18n.locale, permissions.channel],
|
|
);
|
|
const channel = permissions.channel;
|
|
const quickReactionEmojis = useQuickReactionEmojis(
|
|
channel,
|
|
3,
|
|
canAddReactions && showQuickReactions && message.state === MessageStates.SENT,
|
|
);
|
|
const blurEmojiPickerTrigger = useCallback(() => {
|
|
if (keyboardModeEnabled) {
|
|
return;
|
|
}
|
|
requestAnimationFrame(() => emojiPickerButtonRef.current?.blur());
|
|
}, [keyboardModeEnabled]);
|
|
const handleEmojiPickerToggle = useCallback(
|
|
(open: boolean) => {
|
|
setEmojiPickerOpen(open);
|
|
onPopoutToggle?.(open);
|
|
if (!open) {
|
|
blurEmojiPickerTrigger();
|
|
}
|
|
},
|
|
[onPopoutToggle, blurEmojiPickerTrigger],
|
|
);
|
|
const handleEmojiPickerOpen = useCallback(() => handleEmojiPickerToggle(true), [handleEmojiPickerToggle]);
|
|
const handleEmojiPickerClose = useCallback(() => handleEmojiPickerToggle(false), [handleEmojiPickerToggle]);
|
|
const handleMoreMenuOpenReactionPicker = useCallback(() => {
|
|
keepOpenOnNextMoreMenuCloseRef.current = true;
|
|
}, []);
|
|
const handleMoreMenuClose = useCallback(
|
|
(closeMenu: () => void) => {
|
|
if (!keepOpenOnNextMoreMenuCloseRef.current) {
|
|
onPopoutToggle?.(false);
|
|
}
|
|
closeMenu();
|
|
},
|
|
[onPopoutToggle],
|
|
);
|
|
const handleMoreMenuClosed = useCallback(() => {
|
|
if (keepOpenOnNextMoreMenuCloseRef.current) {
|
|
keepOpenOnNextMoreMenuCloseRef.current = false;
|
|
return;
|
|
}
|
|
onPopoutToggle?.(false);
|
|
}, [onPopoutToggle]);
|
|
useEffect(() => {
|
|
return () => {
|
|
if (emojiPickerOpen) {
|
|
onPopoutToggle?.(false);
|
|
}
|
|
};
|
|
}, [emojiPickerOpen, onPopoutToggle]);
|
|
const handleDebugClick = useCallback(() => {
|
|
ModalCommands.push(
|
|
modal(() => (
|
|
<MessageDebugModal
|
|
title={i18n._(MESSAGE_DEBUG_DESCRIPTOR)}
|
|
message={message}
|
|
data-flx="channel.message-action-bar.handle-debug-click.message-debug-modal"
|
|
/>
|
|
)),
|
|
);
|
|
}, [message, i18n]);
|
|
useEffect(() => {
|
|
const unsubscribe = ComponentDispatch.subscribe('EMOJI_PICKER_OPEN', (payload?: unknown) => {
|
|
const data = (payload ?? {}) as {messageId?: string};
|
|
if (data.messageId === message.id && emojiPickerButtonRef.current) {
|
|
PopoutCommands.open({
|
|
key: `emoji_picker-${message.id}`,
|
|
position: 'left-start',
|
|
render: ({onClose}) => (
|
|
<EmojiPickerPopout
|
|
channelId={message.channelId}
|
|
handleSelect={handlers.handleEmojiSelect}
|
|
onClose={onClose}
|
|
data-flx="channel.message-action-bar.unsubscribe.emoji-picker-popout"
|
|
/>
|
|
),
|
|
target: emojiPickerButtonRef.current,
|
|
shouldAutoUpdate: false,
|
|
animationType: 'none',
|
|
onOpen: () => handleEmojiPickerToggle(true),
|
|
onClose: () => handleEmojiPickerToggle(false),
|
|
});
|
|
}
|
|
});
|
|
return () => unsubscribe();
|
|
}, [message.id, message.channelId, handlers.handleEmojiSelect, handleEmojiPickerToggle]);
|
|
const handleMoreOptionsPointerDown = useCallback(
|
|
(event: React.PointerEvent) => {
|
|
const contextMenu = ContextMenu.contextMenu;
|
|
const isOpen = !!contextMenu && contextMenu.target.target === moreOptionsButtonRef.current;
|
|
if (isOpen) {
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
ContextMenuCommands.close();
|
|
onPopoutToggle?.(false);
|
|
}
|
|
},
|
|
[onPopoutToggle],
|
|
);
|
|
useEffect(() => {
|
|
if (!showFullActions || !moreMenuOpen) {
|
|
return;
|
|
}
|
|
const contextMenu = ContextMenu.contextMenu;
|
|
if (contextMenu?.target.target === moreOptionsButtonRef.current) {
|
|
ContextMenuCommands.close();
|
|
}
|
|
}, [moreMenuOpen, showFullActions]);
|
|
const openMoreOptionsMenu = useCallback(
|
|
(event: React.MouseEvent | React.KeyboardEvent) => {
|
|
if (!showMessageActionBar || showFullActions) {
|
|
return;
|
|
}
|
|
const contextMenu = ContextMenu.contextMenu;
|
|
const isOpen = !!contextMenu && contextMenu.target.target === event.currentTarget;
|
|
if (isOpen) {
|
|
return;
|
|
}
|
|
onPopoutToggle?.(true);
|
|
ContextMenuCommands.openFromElementLeftStart(
|
|
event,
|
|
(props) => (
|
|
<MessageContextMenu
|
|
message={message}
|
|
sourceChannel={channel}
|
|
onClose={() => {
|
|
handleMoreMenuClose(props.onClose);
|
|
}}
|
|
onOpenReactionPicker={handleMoreMenuOpenReactionPicker}
|
|
onDelete={handleDelete}
|
|
data-flx="channel.message-action-bar.open-more-options-menu.message-context-menu"
|
|
/>
|
|
),
|
|
{
|
|
config: {
|
|
onClose: handleMoreMenuClosed,
|
|
},
|
|
},
|
|
);
|
|
},
|
|
[
|
|
channel,
|
|
handleDelete,
|
|
handleMoreMenuClose,
|
|
handleMoreMenuClosed,
|
|
handleMoreMenuOpenReactionPicker,
|
|
message,
|
|
showFullActions,
|
|
showMessageActionBar,
|
|
],
|
|
);
|
|
return (
|
|
<div
|
|
ref={actionBarRef}
|
|
className={clsx(
|
|
styles.actionBarContainer,
|
|
messageStyles.buttons,
|
|
(emojiPickerOpen || contextMenuOpen) && messageStyles.emojiPickerOpen,
|
|
)}
|
|
data-flx="channel.message-action-bar.message-action-bar-core.action-bar-container"
|
|
>
|
|
<div className={styles.actionBar} data-flx="channel.message-action-bar.message-action-bar-core.action-bar">
|
|
{message.state === MessageStates.SENT &&
|
|
(onlyMoreButton ? (
|
|
<MessageActionBarButton
|
|
ref={moreOptionsButtonRef}
|
|
icon={<MoreIcon size={20} data-flx="channel.message-action-bar.message-action-bar-core.more-icon" />}
|
|
label={i18n._(MORE_DESCRIPTOR)}
|
|
onPointerDownCapture={handleMoreOptionsPointerDown}
|
|
onClick={openMoreOptionsMenu}
|
|
isActive={moreMenuOpen}
|
|
data-flx="channel.message-action-bar.message-action-bar-core.message-action-bar-button.open-more-options-menu"
|
|
/>
|
|
) : (
|
|
<>
|
|
{!showFullActions &&
|
|
canAddReactions &&
|
|
showQuickReactions &&
|
|
quickReactionEmojis.map((emoji) => (
|
|
<QuickReactionButton
|
|
key={emoji.name}
|
|
emoji={emoji}
|
|
onReact={handlers.handleEmojiSelect}
|
|
data-flx="channel.message-action-bar.message-action-bar-core.quick-reaction-button"
|
|
/>
|
|
))}
|
|
{showFullActions && (
|
|
<>
|
|
{developerMode && (
|
|
<MessageActionBarButton
|
|
icon={
|
|
<DebugMessageIcon
|
|
size={20}
|
|
data-flx="channel.message-action-bar.message-action-bar-core.debug-message-icon"
|
|
/>
|
|
}
|
|
label={i18n._(DEBUG_MESSAGE_DESCRIPTOR)}
|
|
onClick={handleDebugClick}
|
|
data-flx="channel.message-action-bar.message-action-bar-core.message-action-bar-button.debug-click"
|
|
/>
|
|
)}
|
|
<MessageActionBarButton
|
|
icon={
|
|
<CopyIdIcon
|
|
size={20}
|
|
data-flx="channel.message-action-bar.message-action-bar-core.copy-id-icon"
|
|
/>
|
|
}
|
|
label={i18n._(COPY_MESSAGE_ID_DESCRIPTOR)}
|
|
onClick={handlers.handleCopyMessageId}
|
|
data-flx="channel.message-action-bar.message-action-bar-core.message-action-bar-button.copy-message-id"
|
|
/>
|
|
{supportsInteractiveActions && (
|
|
<MessageActionBarButton
|
|
icon={
|
|
<CopyLinkIcon
|
|
size={20}
|
|
data-flx="channel.message-action-bar.message-action-bar-core.copy-link-icon"
|
|
/>
|
|
}
|
|
label={i18n._(COPY_MESSAGE_LINK_DESCRIPTOR)}
|
|
onClick={handlers.handleCopyMessageLink}
|
|
data-flx="channel.message-action-bar.message-action-bar-core.message-action-bar-button.copy-message-link"
|
|
/>
|
|
)}
|
|
{getCopyableMessageText(message, i18n) && (
|
|
<MessageActionBarButton
|
|
icon={
|
|
<CopyMessageTextIcon
|
|
size={20}
|
|
data-flx="channel.message-action-bar.message-action-bar-core.copy-message-text-icon"
|
|
/>
|
|
}
|
|
label={i18n._(COPY_TEXT_DESCRIPTOR)}
|
|
onClick={handlers.handleCopyMessage}
|
|
data-flx="channel.message-action-bar.message-action-bar-core.message-action-bar-button.copy-message"
|
|
/>
|
|
)}
|
|
{shouldRenderSuppressEmbeds && (
|
|
<MessageActionBarButton
|
|
icon={
|
|
<SuppressEmbedsIcon
|
|
size={20}
|
|
data-flx="channel.message-action-bar.message-action-bar-core.suppress-embeds-icon"
|
|
/>
|
|
}
|
|
label={
|
|
isEmbedsSuppressed(message)
|
|
? i18n._(UNSUPPRESS_EMBEDS_DESCRIPTOR)
|
|
: i18n._(SUPPRESS_EMBEDS_DESCRIPTOR)
|
|
}
|
|
onClick={handlers.handleToggleSuppressEmbeds}
|
|
data-flx="channel.message-action-bar.message-action-bar-core.message-action-bar-button.toggle-suppress-embeds"
|
|
/>
|
|
)}
|
|
<MessageActionBarButton
|
|
icon={
|
|
<MarkAsUnreadIcon
|
|
size={20}
|
|
data-flx="channel.message-action-bar.message-action-bar-core.mark-as-unread-icon"
|
|
/>
|
|
}
|
|
label={i18n._(MARK_AS_UNREAD_DESCRIPTOR)}
|
|
onClick={handlers.handleMarkAsUnread}
|
|
data-flx="channel.message-action-bar.message-action-bar-core.message-action-bar-button.mark-as-unread"
|
|
/>
|
|
{message.isUserMessage() && supportsInteractiveActions && (
|
|
<MessageActionBarButton
|
|
icon={
|
|
<BookmarkIcon
|
|
size={20}
|
|
filled={isSaved}
|
|
data-flx="channel.message-action-bar.message-action-bar-core.bookmark-icon"
|
|
/>
|
|
}
|
|
label={isSaved ? i18n._(REMOVE_BOOKMARK_DESCRIPTOR) : i18n._(BOOKMARK_MESSAGE_DESCRIPTOR)}
|
|
onClick={handlers.handleSaveMessage(isSaved)}
|
|
data-flx="channel.message-action-bar.message-action-bar-core.message-action-bar-button.save-message"
|
|
/>
|
|
)}
|
|
{message.isUserMessage() && canPinMessage && (
|
|
<MessageActionBarButton
|
|
icon={
|
|
<PinIcon size={20} data-flx="channel.message-action-bar.message-action-bar-core.pin-icon" />
|
|
}
|
|
label={message.pinned ? i18n._(UNPIN_MESSAGE_DESCRIPTOR) : i18n._(PIN_MESSAGE_DESCRIPTOR)}
|
|
onClick={handlers.handlePinMessage}
|
|
data-flx="channel.message-action-bar.message-action-bar-core.message-action-bar-button.pin-message"
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
{canAddReactions && (
|
|
<Popout
|
|
render={({onClose}) => (
|
|
<EmojiPickerPopout
|
|
channelId={message.channelId}
|
|
handleSelect={handlers.handleEmojiSelect}
|
|
onClose={onClose}
|
|
data-flx="channel.message-action-bar.message-action-bar-core.emoji-picker-popout"
|
|
/>
|
|
)}
|
|
position="left-start"
|
|
uniqueId={`emoji_picker-actionbar-${message.id}`}
|
|
shouldAutoUpdate={false}
|
|
animationType="none"
|
|
shouldOpenOnClick={() => canUseWindowFocusedActivationClick()}
|
|
onOpen={handleEmojiPickerOpen}
|
|
onClose={handleEmojiPickerClose}
|
|
data-flx="channel.message-action-bar.message-action-bar-core.popout"
|
|
>
|
|
<MessageActionBarButton
|
|
ref={emojiPickerButtonRef}
|
|
icon={
|
|
<AddReactionIcon
|
|
size={20}
|
|
data-flx="channel.message-action-bar.message-action-bar-core.add-reaction-icon"
|
|
/>
|
|
}
|
|
label={i18n._(ADD_REACTION_DESCRIPTOR)}
|
|
isActive={emojiPickerOpen}
|
|
dataAction="message-add-reaction-button"
|
|
data-flx="channel.message-action-bar.message-action-bar-core.message-action-bar-button"
|
|
/>
|
|
</Popout>
|
|
)}
|
|
{message.isUserMessage() && !message.messageSnapshots && canEditMessage && (
|
|
<MessageActionBarButton
|
|
icon={
|
|
<EditMessageIcon
|
|
size={20}
|
|
data-flx="channel.message-action-bar.message-action-bar-core.edit-message-icon"
|
|
/>
|
|
}
|
|
label={i18n._(EDIT_MESSAGE_DESCRIPTOR)}
|
|
onClick={handlers.handleEditMessage}
|
|
data-flx="channel.message-action-bar.message-action-bar-core.message-action-bar-button.edit-message"
|
|
/>
|
|
)}
|
|
{message.isUserMessage() && supportsInteractiveActions && canSendMessages && (
|
|
<MessageActionBarButton
|
|
icon={
|
|
<ReplyIcon size={20} data-flx="channel.message-action-bar.message-action-bar-core.reply-icon" />
|
|
}
|
|
label={i18n._(REPLY_DESCRIPTOR)}
|
|
onClick={handlers.handleReply}
|
|
data-flx="channel.message-action-bar.message-action-bar-core.message-action-bar-button.reply"
|
|
/>
|
|
)}
|
|
{message.isUserMessage() && supportsInteractiveActions && canForwardMessage && (
|
|
<MessageActionBarButton
|
|
icon={
|
|
<ForwardIcon
|
|
size={20}
|
|
data-flx="channel.message-action-bar.message-action-bar-core.forward-icon"
|
|
/>
|
|
}
|
|
label={i18n._(FORWARD_DESCRIPTOR)}
|
|
onClick={handlers.handleForward}
|
|
data-flx="channel.message-action-bar.message-action-bar-core.message-action-bar-button.forward"
|
|
/>
|
|
)}
|
|
{showFullActions && canDeleteMessage && (
|
|
<MessageActionBarButton
|
|
danger={true}
|
|
icon={
|
|
<DeleteIcon size={20} data-flx="channel.message-action-bar.message-action-bar-core.delete-icon" />
|
|
}
|
|
label={i18n._(DELETE_MESSAGE_DESCRIPTOR)}
|
|
onClick={(event) => handleDelete(event.shiftKey)}
|
|
data-flx="channel.message-action-bar.message-action-bar-core.message-action-bar-button.delete"
|
|
/>
|
|
)}
|
|
<MessageActionBarButton
|
|
ref={moreOptionsButtonRef}
|
|
icon={
|
|
<MoreIcon size={20} data-flx="channel.message-action-bar.message-action-bar-core.more-icon--2" />
|
|
}
|
|
label={i18n._(MORE_DESCRIPTOR)}
|
|
hidden={showFullActions}
|
|
onPointerDownCapture={handleMoreOptionsPointerDown}
|
|
onClick={openMoreOptionsMenu}
|
|
isActive={moreMenuOpen}
|
|
data-flx="channel.message-action-bar.message-action-bar-core.message-action-bar-button.open-more-options-menu--2"
|
|
/>
|
|
</>
|
|
))}
|
|
{message.state === MessageStates.FAILED && (
|
|
<>
|
|
<MessageActionBarButton
|
|
icon={<RetryIcon size={20} data-flx="channel.message-action-bar.message-action-bar-core.retry-icon" />}
|
|
label={i18n._(TRY_AGAIN_DESCRIPTOR)}
|
|
onClick={handlers.handleRetryMessage}
|
|
data-flx="channel.message-action-bar.message-action-bar-core.message-action-bar-button.retry-message"
|
|
/>
|
|
<MessageActionBarButton
|
|
danger={true}
|
|
icon={
|
|
<DeleteIcon size={20} data-flx="channel.message-action-bar.message-action-bar-core.delete-icon--2" />
|
|
}
|
|
label={i18n._(DELETE_MESSAGE_DESCRIPTOR)}
|
|
onClick={handlers.handleFailedMessageDelete}
|
|
data-flx="channel.message-action-bar.message-action-bar-core.message-action-bar-button.failed-message-delete"
|
|
/>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
);
|
|
export const MessageActionBar = observer(
|
|
({
|
|
message,
|
|
handleDelete,
|
|
sourceChannel,
|
|
onPopoutToggle,
|
|
}: {
|
|
message: Message;
|
|
handleDelete: (bypassConfirm?: boolean) => void;
|
|
sourceChannel?: Channel | null;
|
|
onPopoutToggle?: (isOpen: boolean) => void;
|
|
}) => {
|
|
const isSaved = SavedMessages.isSaved(message.id);
|
|
const developerMode = UserSettings.developerMode;
|
|
const permissions = useMessagePermissions(message, sourceChannel);
|
|
if (!permissions) {
|
|
return null;
|
|
}
|
|
return (
|
|
<MessageActionBarCore
|
|
message={message}
|
|
handleDelete={handleDelete}
|
|
permissions={permissions}
|
|
isSaved={isSaved}
|
|
developerMode={developerMode}
|
|
onPopoutToggle={onPopoutToggle}
|
|
data-flx="channel.message-action-bar.message-action-bar-core"
|
|
/>
|
|
);
|
|
},
|
|
);
|