mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
perf(app): show the message action bar from css not remounting (#1896)
This commit is contained in:
1 parent
c54d7bcc88
commit
3a6bc4bc7b
4 files changed
+217
-211
No files matched your search
@@ -16,8 +16,7 @@ import MessageEdit from '@app/features/messaging/state/MessageEdit';
|
||||
import MessageFocus from '@app/features/messaging/state/MessageFocus';
|
||||
import MessageReply from '@app/features/messaging/state/MessageReply';
|
||||
import {getMessageComponent} from '@app/features/messaging/utils/MessageComponentUtils';
|
||||
import {getParserFlagsForContext} from '@app/features/messaging/utils/markdown/MarkdownParserFlags';
|
||||
import {parseAndRenderToPlaintext} from '@app/features/messaging/utils/markdown/Plaintext';
|
||||
import {renderAstToPlaintext} from '@app/features/messaging/utils/markdown/Plaintext';
|
||||
import {NodeType} from '@app/features/messaging/utils/markdown/parser/Enums';
|
||||
import {SystemMessageUtils} from '@app/features/messaging/utils/SystemMessageUtils';
|
||||
import {subscribeWindowFocus} from '@app/features/platform/utils/WindowFocusBroadcast';
|
||||
@@ -89,7 +88,6 @@ const shouldApplyGroupedLayout = (message: MessageModel, _prevMessage?: MessageM
|
||||
const isDisplaySystemMessage = (message: MessageModel): boolean =>
|
||||
message.type !== MessageTypes.DEFAULT && message.type !== MessageTypes.REPLY;
|
||||
const isActivationKey = (key: string) => key === 'Enter' || key === ' ' || key === 'Spacebar' || key === 'Space';
|
||||
const MESSAGE_ARIA_PARSER_FLAGS = getParserFlagsForContext(MarkdownContext.STANDARD_WITHOUT_JUMBO);
|
||||
const MAX_ARIA_MESSAGE_TEXT_LENGTH = 220;
|
||||
const LONG_PRESS_DELAY = 500;
|
||||
const MOVEMENT_THRESHOLD = 10;
|
||||
@@ -335,12 +333,20 @@ export const Message: React.FC<MessageProps> = observer((props) => {
|
||||
behaviorOverrides?.messageGroupSpacing ?? Accessibility.getMessageGroupSpacingValue(messageDisplayCompact);
|
||||
const authorName =
|
||||
previewOverrides?.displayName || NicknameUtils.getNickname(message.author, channel.guildId, channel.id);
|
||||
const {nodes: astNodes} = useMemo(
|
||||
() =>
|
||||
parse({
|
||||
content: message.content,
|
||||
context: MarkdownContext.STANDARD_WITH_JUMBO,
|
||||
}),
|
||||
[message.content],
|
||||
);
|
||||
const messageAriaLabel = useMemo(() => {
|
||||
const timeLabel = DateUtils.getFormattedDateTime(message.timestamp);
|
||||
const systemText = message.isSystemMessage() ? SystemMessageUtils.stringify(message, i18n) : null;
|
||||
let text =
|
||||
systemText ||
|
||||
parseAndRenderToPlaintext(message.content, MESSAGE_ARIA_PARSER_FLAGS, {
|
||||
renderAstToPlaintext(astNodes, {
|
||||
channelId: channel.id,
|
||||
preserveMarkdown: false,
|
||||
includeEmojiNames: true,
|
||||
@@ -369,6 +375,7 @@ export const Message: React.FC<MessageProps> = observer((props) => {
|
||||
}
|
||||
return `${authorName}, ${truncateAriaLabelText(text)}, ${timeLabel}, ${i18n._(MESSAGE_DESCRIPTOR)}`;
|
||||
}, [
|
||||
astNodes,
|
||||
authorName,
|
||||
channel.id,
|
||||
i18n.locale,
|
||||
@@ -764,14 +771,6 @@ export const Message: React.FC<MessageProps> = observer((props) => {
|
||||
{getMessageComponent(message, channel, forceUnknownMessageType)}
|
||||
</MessageViewContextProvider>
|
||||
);
|
||||
const {nodes: astNodes} = useMemo(
|
||||
() =>
|
||||
parse({
|
||||
content: message.content,
|
||||
context: MarkdownContext.STANDARD_WITH_JUMBO,
|
||||
}),
|
||||
[message.content],
|
||||
);
|
||||
const shouldHideContent =
|
||||
UserSettings.getRenderEmbeds() &&
|
||||
message.embeds.length > 0 &&
|
||||
@@ -848,10 +847,14 @@ export const Message: React.FC<MessageProps> = observer((props) => {
|
||||
!mobileLayoutEnabled,
|
||||
[previewContext, suppressMessageActions, message.state, isEditing, mobileLayoutEnabled],
|
||||
);
|
||||
const shouldRenderInlineActionBar = useMemo(
|
||||
() => shouldShowActionBar && (previewMode || isHovering || isKeyboardFocused || contextMenuOpen || isPopoutOpen),
|
||||
[shouldShowActionBar, previewMode, isHovering, isKeyboardFocused, contextMenuOpen, isPopoutOpen],
|
||||
const isActionBarForcedVisible = useMemo(
|
||||
() => Boolean(previewMode || isKeyboardFocused || contextMenuOpen || isPopoutOpen),
|
||||
[previewMode, isKeyboardFocused, contextMenuOpen, isPopoutOpen],
|
||||
);
|
||||
const isActionBarActive = isHovering || isActionBarForcedVisible;
|
||||
const wasActionBarActivatedRef = useRef(false);
|
||||
wasActionBarActivatedRef.current = wasActionBarActivatedRef.current || isActionBarActive;
|
||||
const shouldMountActionBar = shouldShowActionBar && wasActionBarActivatedRef.current;
|
||||
const shouldShowBottomSheet = useMemo(
|
||||
() =>
|
||||
mobileLayoutEnabled && showActionBar && !previewContext && message.state !== MessageStates.SENDING && !isEditing,
|
||||
@@ -894,6 +897,8 @@ export const Message: React.FC<MessageProps> = observer((props) => {
|
||||
data-flx-edited={message.editedTimestamp != null ? 'true' : undefined}
|
||||
data-flx-compact={messageDisplayCompact ? 'true' : undefined}
|
||||
data-flx-grouped={shouldGroup && shouldApplyGroupedLayout(message, prevMessage) ? 'true' : undefined}
|
||||
data-flx-action-bar={shouldShowActionBar ? 'true' : undefined}
|
||||
data-flx-action-bar-forced={shouldShowActionBar && isActionBarForcedVisible ? 'true' : undefined}
|
||||
tabIndex={keyboardModeEnabled ? -1 : undefined}
|
||||
className={messageClasses}
|
||||
ref={messageRef}
|
||||
@@ -911,7 +916,7 @@ export const Message: React.FC<MessageProps> = observer((props) => {
|
||||
data-flx="channel.message.article.alt-click"
|
||||
>
|
||||
{messageComponent}
|
||||
{shouldRenderInlineActionBar &&
|
||||
{shouldMountActionBar &&
|
||||
(previewMode ? (
|
||||
<MessageActionBarCore
|
||||
message={message}
|
||||
@@ -926,8 +931,8 @@ export const Message: React.FC<MessageProps> = observer((props) => {
|
||||
canForwardMessage: true,
|
||||
shouldRenderSuppressEmbeds: true,
|
||||
}}
|
||||
isSaved={false}
|
||||
developerMode={false}
|
||||
isActive={isActionBarActive}
|
||||
onPopoutToggle={handleMessagePopoutToggle}
|
||||
data-flx="channel.message.message-action-bar-core"
|
||||
/>
|
||||
@@ -936,6 +941,7 @@ export const Message: React.FC<MessageProps> = observer((props) => {
|
||||
message={message}
|
||||
handleDelete={handleDelete}
|
||||
sourceChannel={channel}
|
||||
isActive={isActionBarActive}
|
||||
onPopoutToggle={handleMessagePopoutToggle}
|
||||
data-flx="channel.message.message-action-bar"
|
||||
/>
|
||||
|
||||
@@ -6,10 +6,44 @@
|
||||
right: 0;
|
||||
z-index: var(--z-index-elevated-1);
|
||||
padding: 0 0.875rem 0.375rem 2rem;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:global([data-flx-action-bar]:hover) .actionBarContainer,
|
||||
:global([data-flx-action-bar]) .actionBarContainer:has(:focus-visible) {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
@media (pointer: coarse) {
|
||||
:global([data-flx-action-bar]:not([data-flx-action-bar-forced]):hover) .actionBarContainer {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
:global([data-flx-action-bar][data-flx-action-bar-forced]) .actionBarContainer,
|
||||
.actionBarContainer.actionBarPinned {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
:global(html:not(.window-focused):not(.unfocused-fully-interactive)) .actionBarContainer,
|
||||
:global(.window-focus-activation-guard) .actionBarContainer {
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
.actionBar {
|
||||
composes: toolbar from '@app/features/ui/action_toolbar/ActionToolbar.module.css';
|
||||
--action-toolbar-gap: 0;
|
||||
--action-toolbar-padding: 0.125rem;
|
||||
--message-action-bar-surface: var(--settings-surface-background, var(--form-surface-background));
|
||||
--message-action-bar-border: var(--settings-border-color, var(--background-modifier-accent));
|
||||
--message-action-bar-hover-background: var(--settings-hover-background, var(--surface-interactive-hover-bg));
|
||||
@@ -41,6 +75,11 @@
|
||||
|
||||
.button {
|
||||
composes: item from '@app/features/ui/action_toolbar/ActionToolbar.module.css';
|
||||
--action-toolbar-item-border-width: 0;
|
||||
}
|
||||
|
||||
.quickReactionGroupEnd {
|
||||
margin-right: 0.5625rem;
|
||||
}
|
||||
|
||||
.button[hidden] {
|
||||
@@ -74,8 +113,8 @@
|
||||
}
|
||||
|
||||
.actionBarIcon {
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -83,8 +122,8 @@
|
||||
}
|
||||
|
||||
.actionBarIcon > svg {
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
display: block;
|
||||
flex: 0 0 auto;
|
||||
object-fit: contain;
|
||||
@@ -103,8 +142,8 @@
|
||||
}
|
||||
|
||||
.emojiImage {
|
||||
height: 1.125rem;
|
||||
width: 1.125rem;
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
}
|
||||
|
||||
img.emojiImage {
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
// 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 {useShouldAnimate} from '@app/features/app/hooks/useShouldAnimate';
|
||||
import styles from '@app/features/channel/components/MessageActionBar.module.css';
|
||||
import {useShiftKey} from '@app/features/channel/components/MessageActionBarShiftKey';
|
||||
import {
|
||||
createMessageActionHandlers,
|
||||
getCopyableMessageText,
|
||||
isClientSystemMessage,
|
||||
isEmbedsSuppressed,
|
||||
useMessagePermissions,
|
||||
} from '@app/features/channel/components/MessageActionUtils';
|
||||
import {REACT_WITH_EMOJI_DESCRIPTOR} from '@app/features/channel/components/QuickReactionsRow';
|
||||
@@ -18,34 +17,28 @@ import {MessageDebugModal} from '@app/features/devtools/components/debug/Message
|
||||
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 {buildCustomEmojiURL} from '@app/features/expressions/utils/CustomEmojiImageUrl';
|
||||
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 {ComponentBus} 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,
|
||||
@@ -55,7 +48,6 @@ import {
|
||||
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';
|
||||
@@ -68,13 +60,12 @@ 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 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';
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
|
||||
const CLICK_TO_REACT_DESCRIPTOR = msg({
|
||||
message: 'Click to react',
|
||||
@@ -92,100 +83,10 @@ 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);
|
||||
};
|
||||
|
||||
interface MessageActionBarButtonProps {
|
||||
label: string;
|
||||
@@ -250,13 +151,14 @@ MessageActionBarButton.displayName = 'MessageActionBarButton';
|
||||
interface QuickReactionButtonProps {
|
||||
emoji: FlatEmoji;
|
||||
onReact: (emoji: FlatEmoji) => void;
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
const QuickReactionButton = observer(
|
||||
React.forwardRef<HTMLButtonElement, QuickReactionButtonProps>(({emoji, onReact}, ref) => {
|
||||
export const QuickReactionButton = observer(
|
||||
React.forwardRef<HTMLButtonElement, QuickReactionButtonProps>(({emoji, onReact, hidden}, ref) => {
|
||||
const {i18n} = useLingui();
|
||||
const isAnimatedEmoji = emoji.animated === true;
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const animatedMediaPlaybackAllowed = useAnimatedMediaPlaybackAllowed();
|
||||
const {url: displayUrl} = getEmojiDisplayData(emoji);
|
||||
const handleClick = useCallback(
|
||||
(event: React.MouseEvent | React.KeyboardEvent) => {
|
||||
@@ -267,19 +169,13 @@ const QuickReactionButton = observer(
|
||||
},
|
||||
[emoji, onReact],
|
||||
);
|
||||
const handleMouseEnter = useCallback(() => setIsHovered(true), []);
|
||||
const handleMouseLeave = useCallback(() => setIsHovered(false), []);
|
||||
const beginEmojiHover = useMemo(() => (isAnimatedEmoji ? () => setIsHovered(true) : undefined), [isAnimatedEmoji]);
|
||||
const endEmojiHover = useMemo(() => (isAnimatedEmoji ? () => setIsHovered(false) : undefined), [isAnimatedEmoji]);
|
||||
const emojiNameWithColons = useMemo(() => getEmojiNameWithColons(toReactionEmoji(emoji)), [emoji]);
|
||||
const shouldShowAnimated = useMemo(
|
||||
() => emoji.animated && isHovered && animatedMediaPlaybackAllowed,
|
||||
[animatedMediaPlaybackAllowed, emoji.animated, isHovered],
|
||||
);
|
||||
const shouldShowAnimated = useShouldAnimate({kind: 'emoji', isAnimated: isAnimatedEmoji, isHovering: isHovered});
|
||||
const emojiSrc = useMemo(
|
||||
() =>
|
||||
emoji.animated && emoji.id
|
||||
? AvatarUtils.getEmojiURL({id: emoji.id, animated: shouldShowAnimated})
|
||||
: (displayUrl ?? ''),
|
||||
[emoji.animated, emoji.id, displayUrl, shouldShowAnimated],
|
||||
() => (emoji.id ? buildCustomEmojiURL({id: emoji.id, animated: shouldShowAnimated}) : (displayUrl ?? '')),
|
||||
[emoji.id, displayUrl, shouldShowAnimated],
|
||||
);
|
||||
const tooltipContent = useCallback(
|
||||
() => (
|
||||
@@ -303,17 +199,19 @@ const QuickReactionButton = observer(
|
||||
type="button"
|
||||
ref={ref}
|
||||
aria-label={ariaLabel}
|
||||
hidden={hidden}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onFocus={handleMouseEnter}
|
||||
onBlur={handleMouseLeave}
|
||||
onMouseEnter={beginEmojiHover}
|
||||
onMouseLeave={endEmojiHover}
|
||||
onFocus={beginEmojiHover}
|
||||
onBlur={endEmojiHover}
|
||||
className={styles.button}
|
||||
data-flx="channel.message-action-bar.button.click"
|
||||
>
|
||||
<img
|
||||
src={emojiSrc}
|
||||
alt={emoji.name}
|
||||
aria-hidden={true}
|
||||
className={styles.emojiImage}
|
||||
data-flx="channel.message-action-bar.emoji-image"
|
||||
/>
|
||||
@@ -339,19 +237,20 @@ interface MessageActionBarCoreProps {
|
||||
canForwardMessage: boolean;
|
||||
shouldRenderSuppressEmbeds: boolean;
|
||||
};
|
||||
isSaved: boolean;
|
||||
developerMode: boolean;
|
||||
isActive: boolean;
|
||||
onPopoutToggle?: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
export const MessageActionBarCore: React.FC<MessageActionBarCoreProps> = observer(
|
||||
({message, handleDelete, permissions, isSaved, developerMode, onPopoutToggle}) => {
|
||||
({message, handleDelete, permissions, developerMode, isActive, 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 emojiPickerOpenRef = useRef(false);
|
||||
const contextMenuOpen = useContextMenuHoverState(actionBarRef);
|
||||
const moreMenuOpen = useContextMenuHoverState(moreOptionsButtonRef);
|
||||
const showMessageActionBar = Accessibility.showMessageActionBar;
|
||||
@@ -361,16 +260,10 @@ export const MessageActionBarCore: React.FC<MessageActionBarCoreProps> = observe
|
||||
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 showFullActions = shouldListenForShift && shiftPressed;
|
||||
const {canSendMessages, canAddReactions, canEditMessage, canDeleteMessage, canPinMessage, canForwardMessage} =
|
||||
permissions;
|
||||
const showsEditInTail = message.isUserMessage() && !message.messageSnapshots && canEditMessage;
|
||||
const supportsInteractiveActions = useMemo(() => !isClientSystemMessage(message), [message]);
|
||||
const handlers = useMemo(
|
||||
() => createMessageActionHandlers(message, {i18n, channel: permissions.channel}),
|
||||
@@ -388,8 +281,23 @@ export const MessageActionBarCore: React.FC<MessageActionBarCoreProps> = observe
|
||||
}
|
||||
requestAnimationFrame(() => emojiPickerButtonRef.current?.blur());
|
||||
}, [keyboardModeEnabled]);
|
||||
useEffect(() => {
|
||||
if (isActive || keyboardModeEnabled) {
|
||||
return;
|
||||
}
|
||||
const container = actionBarRef.current;
|
||||
if (container == null) {
|
||||
return;
|
||||
}
|
||||
const focused = container.ownerDocument.activeElement;
|
||||
if (!(focused instanceof HTMLElement) || !container.contains(focused) || focused.matches(':focus-visible')) {
|
||||
return;
|
||||
}
|
||||
focused.blur();
|
||||
}, [isActive, keyboardModeEnabled]);
|
||||
const handleEmojiPickerToggle = useCallback(
|
||||
(open: boolean) => {
|
||||
emojiPickerOpenRef.current = open;
|
||||
setEmojiPickerOpen(open);
|
||||
onPopoutToggle?.(open);
|
||||
if (!open) {
|
||||
@@ -405,7 +313,7 @@ export const MessageActionBarCore: React.FC<MessageActionBarCoreProps> = observe
|
||||
}, []);
|
||||
const handleMoreMenuClose = useCallback(
|
||||
(closeMenu: () => void) => {
|
||||
if (!keepOpenOnNextMoreMenuCloseRef.current) {
|
||||
if (!keepOpenOnNextMoreMenuCloseRef.current || !emojiPickerOpenRef.current) {
|
||||
onPopoutToggle?.(false);
|
||||
}
|
||||
closeMenu();
|
||||
@@ -413,8 +321,9 @@ export const MessageActionBarCore: React.FC<MessageActionBarCoreProps> = observe
|
||||
[onPopoutToggle],
|
||||
);
|
||||
const handleMoreMenuClosed = useCallback(() => {
|
||||
if (keepOpenOnNextMoreMenuCloseRef.current) {
|
||||
keepOpenOnNextMoreMenuCloseRef.current = false;
|
||||
const handedOffToReactionPicker = keepOpenOnNextMoreMenuCloseRef.current;
|
||||
keepOpenOnNextMoreMenuCloseRef.current = false;
|
||||
if (handedOffToReactionPicker && emojiPickerOpenRef.current) {
|
||||
return;
|
||||
}
|
||||
onPopoutToggle?.(false);
|
||||
@@ -534,6 +443,7 @@ export const MessageActionBarCore: React.FC<MessageActionBarCoreProps> = observe
|
||||
styles.actionBarContainer,
|
||||
messageStyles.buttons,
|
||||
(emojiPickerOpen || contextMenuOpen) && messageStyles.emojiPickerOpen,
|
||||
(emojiPickerOpen || contextMenuOpen || moreMenuOpen) && styles.actionBarPinned,
|
||||
)}
|
||||
data-flx="channel.message-action-bar.message-action-bar-core.action-bar-container"
|
||||
>
|
||||
@@ -551,13 +461,13 @@ export const MessageActionBarCore: React.FC<MessageActionBarCoreProps> = observe
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{!showFullActions &&
|
||||
canAddReactions &&
|
||||
{canAddReactions &&
|
||||
showQuickReactions &&
|
||||
quickReactionEmojis.map((emoji) => (
|
||||
<QuickReactionButton
|
||||
key={emoji.name}
|
||||
key={emoji.id ?? emoji.uniqueName}
|
||||
emoji={emoji}
|
||||
hidden={showFullActions}
|
||||
onReact={handlers.handleEmojiSelect}
|
||||
data-flx="channel.message-action-bar.message-action-bar-core.quick-reaction-button"
|
||||
/>
|
||||
@@ -601,36 +511,6 @@ export const MessageActionBarCore: React.FC<MessageActionBarCoreProps> = observe
|
||||
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
|
||||
@@ -642,20 +522,6 @@ export const MessageActionBarCore: React.FC<MessageActionBarCoreProps> = observe
|
||||
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={
|
||||
@@ -666,6 +532,19 @@ export const MessageActionBarCore: React.FC<MessageActionBarCoreProps> = observe
|
||||
data-flx="channel.message-action-bar.message-action-bar-core.message-action-bar-button.pin-message"
|
||||
/>
|
||||
)}
|
||||
{showsEditInTail && supportsInteractiveActions && canSendMessages && (
|
||||
<MessageActionBarButton
|
||||
icon={
|
||||
<ReplyIcon
|
||||
size={20}
|
||||
data-flx="channel.message-action-bar.message-action-bar-core.reply-icon--2"
|
||||
/>
|
||||
}
|
||||
label={i18n._(REPLY_DESCRIPTOR)}
|
||||
onClick={handlers.handleReply}
|
||||
data-flx="channel.message-action-bar.message-action-bar-core.message-action-bar-button.reply--2"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{canAddReactions && (
|
||||
@@ -701,7 +580,7 @@ export const MessageActionBarCore: React.FC<MessageActionBarCoreProps> = observe
|
||||
/>
|
||||
</Popout>
|
||||
)}
|
||||
{message.isUserMessage() && !message.messageSnapshots && canEditMessage && (
|
||||
{showsEditInTail && (
|
||||
<MessageActionBarButton
|
||||
icon={
|
||||
<EditMessageIcon
|
||||
@@ -714,7 +593,7 @@ export const MessageActionBarCore: React.FC<MessageActionBarCoreProps> = observe
|
||||
data-flx="channel.message-action-bar.message-action-bar-core.message-action-bar-button.edit-message"
|
||||
/>
|
||||
)}
|
||||
{message.isUserMessage() && supportsInteractiveActions && canSendMessages && (
|
||||
{message.isUserMessage() && supportsInteractiveActions && canSendMessages && !showsEditInTail && (
|
||||
<MessageActionBarButton
|
||||
icon={
|
||||
<ReplyIcon size={20} data-flx="channel.message-action-bar.message-action-bar-core.reply-icon" />
|
||||
@@ -754,7 +633,7 @@ export const MessageActionBarCore: React.FC<MessageActionBarCoreProps> = observe
|
||||
<MoreIcon size={20} data-flx="channel.message-action-bar.message-action-bar-core.more-icon--2" />
|
||||
}
|
||||
label={i18n._(MORE_DESCRIPTOR)}
|
||||
hidden={showFullActions}
|
||||
hidden={showFullActions && canDeleteMessage}
|
||||
onPointerDownCapture={handleMoreOptionsPointerDown}
|
||||
onClick={openMoreOptionsMenu}
|
||||
isActive={moreMenuOpen}
|
||||
@@ -791,14 +670,15 @@ export const MessageActionBar = observer(
|
||||
message,
|
||||
handleDelete,
|
||||
sourceChannel,
|
||||
isActive,
|
||||
onPopoutToggle,
|
||||
}: {
|
||||
message: Message;
|
||||
handleDelete: (bypassConfirm?: boolean) => void;
|
||||
sourceChannel?: Channel | null;
|
||||
isActive: boolean;
|
||||
onPopoutToggle?: (isOpen: boolean) => void;
|
||||
}) => {
|
||||
const isSaved = SavedMessages.isSaved(message.id);
|
||||
const developerMode = UserSettings.developerMode;
|
||||
const permissions = useMessagePermissions(message, sourceChannel);
|
||||
if (!permissions) {
|
||||
@@ -809,8 +689,8 @@ export const MessageActionBar = observer(
|
||||
message={message}
|
||||
handleDelete={handleDelete}
|
||||
permissions={permissions}
|
||||
isSaved={isSaved}
|
||||
developerMode={developerMode}
|
||||
isActive={isActive}
|
||||
onPopoutToggle={onPopoutToggle}
|
||||
data-flx="channel.message-action-bar.message-action-bar-core"
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {useCallback, useSyncExternalStore} from 'react';
|
||||
|
||||
export 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 clearShiftPressed = () => {
|
||||
setShiftPressed(false);
|
||||
};
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState !== 'visible') {
|
||||
setShiftPressed(false);
|
||||
}
|
||||
};
|
||||
const handlePointerModifierChange = (event: MouseEvent) => {
|
||||
setShiftPressed(event.getModifierState?.('Shift') ?? event.shiftKey);
|
||||
};
|
||||
const supportsPointerModifierEvents = (): boolean => 'PointerEvent' in window;
|
||||
const registerListeners = () => {
|
||||
window.addEventListener('keydown', handleKeyDown, true);
|
||||
window.addEventListener('keyup', handleKeyUp, true);
|
||||
window.addEventListener('blur', clearShiftPressed);
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
if (supportsPointerModifierEvents()) {
|
||||
window.addEventListener('pointermove', handlePointerModifierChange, true);
|
||||
window.addEventListener('pointerdown', handlePointerModifierChange, true);
|
||||
window.addEventListener('pointerup', handlePointerModifierChange, true);
|
||||
return;
|
||||
}
|
||||
window.addEventListener('mousemove', handlePointerModifierChange, true);
|
||||
window.addEventListener('mousedown', handlePointerModifierChange, true);
|
||||
window.addEventListener('mouseup', handlePointerModifierChange, true);
|
||||
};
|
||||
registerListeners();
|
||||
return {
|
||||
subscribe: (listener: () => void) => {
|
||||
listeners.add(listener);
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
};
|
||||
},
|
||||
getSnapshot: () => isShiftPressed,
|
||||
getServerSnapshot: () => false,
|
||||
};
|
||||
})();
|
||||
|
||||
export 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);
|
||||
};
|
||||
Reference in new issue
Block a user