mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
fix(app): rework composer layout and footer alignment (#1591)
This commit is contained in:
1 parent
d56c1e5674
commit
01432bc682
76 files changed
+8772
-8759
No files matched your search
@@ -164,6 +164,9 @@ export {
|
||||
isSpecialMention,
|
||||
isSticker,
|
||||
} from '@app/features/channel/components/AutocompleteTypes';
|
||||
|
||||
const ATTACHED_AUTOCOMPLETE_GAP = 4;
|
||||
|
||||
export const Autocomplete = observer(
|
||||
({
|
||||
type,
|
||||
@@ -214,11 +217,11 @@ export const Autocomplete = observer(
|
||||
setReferenceState(resolveReferenceElement(referenceElement));
|
||||
}, [referenceElement]);
|
||||
const portalHost = usePortalHost();
|
||||
let resolvedMainAxisOffset = attached ? 0 : 8;
|
||||
let resolvedMainAxisOffset = attached ? ATTACHED_AUTOCOMPLETE_GAP : 8;
|
||||
if (mainAxisOffset != null) {
|
||||
resolvedMainAxisOffset = mainAxisOffset;
|
||||
}
|
||||
const resolvedCrossAxisOffset = attached ? 8 : 0;
|
||||
const resolvedCrossAxisOffset = 0;
|
||||
const heading = resolveAutocompleteHeading(type, options, i18n);
|
||||
const {refs, floatingStyles} = useFloating({
|
||||
placement: 'top-start',
|
||||
@@ -230,7 +233,7 @@ export const Autocomplete = observer(
|
||||
flip({padding: 16}),
|
||||
size({
|
||||
apply({rects, elements}) {
|
||||
const width = attached ? Math.max(0, rects.reference.width - 16) : rects.reference.width;
|
||||
const width = rects.reference.width;
|
||||
Object.assign(elements.floating.style, {
|
||||
width: `${width}px`,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
|
||||
.container {
|
||||
--composer-surface-color: var(--background-secondary-lighter);
|
||||
--composer-status-line-height: 1.125rem;
|
||||
--composer-status-safe-gap: 0.5rem;
|
||||
--composer-status-safe-area: calc(
|
||||
var(--composer-status-line-height) +
|
||||
var(--composer-status-safe-gap) +
|
||||
var(--composer-status-safe-gap)
|
||||
);
|
||||
--messages-bottom-clearance: max(0px, calc(var(--composer-status-safe-area) - var(--composer-box-inset, 0px)));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
@@ -9,7 +18,7 @@
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
background-color: var(--background-secondary-lighter);
|
||||
background-color: var(--composer-surface-color);
|
||||
contain: layout style;
|
||||
}
|
||||
|
||||
@@ -33,7 +42,6 @@
|
||||
z-index: 1;
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
background-color: var(--background-secondary-lighter);
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {ComponentDispatch} from '@app/features/platform/utils/ComponentBus';
|
||||
|
||||
export interface ChannelComposerDismissalRequest {
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
export function requestChannelComposerAffordanceDismissal(channelId: string): boolean {
|
||||
const result = ComponentDispatch.dispatchToFirstResult(
|
||||
'TEXTAREA_DISMISS_AFFORDANCE',
|
||||
{channelId},
|
||||
(candidate) => candidate === true,
|
||||
);
|
||||
return result === true;
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import {TypingUsers, usePresentableTypingUsers} from '@app/features/channel/comp
|
||||
import wrapperStyles from '@app/features/channel/components/textarea/InputWrapper.module.css';
|
||||
import type {Channel} from '@app/features/channel/models/Channel';
|
||||
import type {Message} from '@app/features/messaging/models/MessagingMessage';
|
||||
import {clsx} from 'clsx';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import type React from 'react';
|
||||
|
||||
@@ -24,7 +23,6 @@ interface ChannelComposerStatusRailProps {
|
||||
slowmodeRemaining: number;
|
||||
slowmodeImmune: boolean;
|
||||
mobile: boolean;
|
||||
leadingContentInFlow: boolean;
|
||||
onCancelEdit: () => void;
|
||||
}
|
||||
|
||||
@@ -41,7 +39,6 @@ export const ChannelComposerStatusRail = observer(function ChannelComposerStatus
|
||||
slowmodeRemaining,
|
||||
slowmodeImmune,
|
||||
mobile,
|
||||
leadingContentInFlow,
|
||||
onCancelEdit,
|
||||
}: ChannelComposerStatusRailProps) {
|
||||
const presentableTypingUsers = usePresentableTypingUsers(channel);
|
||||
@@ -50,9 +47,6 @@ export const ChannelComposerStatusRail = observer(function ChannelComposerStatus
|
||||
const leadingContentVisible = mobileEditVisible || replyVisible;
|
||||
const typingVisible = showTypingStatus && !autocompleteVisible && presentableTypingUsers.length > 0;
|
||||
const slowmodeVisible = showSlowmodeStatus && slowmodeEnabled;
|
||||
if (!leadingContentVisible && !typingVisible && !slowmodeVisible) {
|
||||
return null;
|
||||
}
|
||||
let topBar: React.ReactNode = null;
|
||||
if (mobileEditVisible) {
|
||||
topBar = <EditBar channel={channel} onCancel={onCancelEdit} data-flx="channel.composer-status-rail.edit-bar" />;
|
||||
@@ -68,35 +62,33 @@ export const ChannelComposerStatusRail = observer(function ChannelComposerStatus
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={clsx(wrapperStyles.statusRail, leadingContentInFlow && wrapperStyles.statusRailInFlow)}
|
||||
data-flx="channel.composer-status-rail.container"
|
||||
>
|
||||
<div
|
||||
className={clsx(wrapperStyles.statusRailLeft, leadingContentVisible && wrapperStyles.statusRailLeftWithLeading)}
|
||||
data-flx="channel.composer-status-rail.left"
|
||||
>
|
||||
{topBar !== null && (
|
||||
<div className={wrapperStyles.topBarContainer} data-flx="channel.composer-status-rail.top-bar">
|
||||
{topBar}
|
||||
</div>
|
||||
)}
|
||||
{typingVisible && (
|
||||
<div className={wrapperStyles.statusTypingSlot} data-flx="channel.composer-status-rail.typing-slot">
|
||||
<TypingUsers channel={channel} data-flx="channel.composer-status-rail.typing-users" />
|
||||
<>
|
||||
<div className={wrapperStyles.statusRail} data-flx="channel.composer-status-rail.container">
|
||||
<div className={wrapperStyles.statusRailLeft} data-flx="channel.composer-status-rail.left">
|
||||
{typingVisible && (
|
||||
<div className={wrapperStyles.statusTypingSlot} data-flx="channel.composer-status-rail.typing-slot">
|
||||
<TypingUsers channel={channel} showAvatars={true} data-flx="channel.composer-status-rail.typing-users" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{slowmodeVisible && (
|
||||
<div className={wrapperStyles.statusSlowmodeSlot} data-flx="channel.composer-status-rail.slowmode-slot">
|
||||
<SlowmodeIndicator
|
||||
slowmodeRemaining={slowmodeRemaining}
|
||||
slowmodeDuration={channel.rateLimitPerUser * 1000}
|
||||
isImmune={slowmodeImmune}
|
||||
data-flx="channel.composer-status-rail.slowmode-indicator"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{slowmodeVisible && (
|
||||
<div className={wrapperStyles.statusSlowmodeSlot} data-flx="channel.composer-status-rail.slowmode-slot">
|
||||
<SlowmodeIndicator
|
||||
slowmodeRemaining={slowmodeRemaining}
|
||||
slowmodeDuration={channel.rateLimitPerUser * 1000}
|
||||
isImmune={slowmodeImmune}
|
||||
data-flx="channel.composer-status-rail.slowmode-indicator"
|
||||
/>
|
||||
{leadingContentVisible && (
|
||||
<div className={wrapperStyles.composerActionStack} data-flx="channel.composer-status-rail.action-stack">
|
||||
<div className={wrapperStyles.composerActionRow} data-flx="channel.composer-status-rail.action-row">
|
||||
{topBar}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -23,20 +23,16 @@
|
||||
flex-shrink: 0;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
width: var(--composer-status-pill-height, 1.5rem);
|
||||
height: var(--composer-status-pill-height, 1.5rem);
|
||||
width: var(--composer-action-button-size, 2rem);
|
||||
height: var(--composer-action-button-size, 2rem);
|
||||
padding: 0;
|
||||
border-radius: var(--radius-full);
|
||||
color: var(--text-primary-muted);
|
||||
line-height: 0;
|
||||
transition:
|
||||
color 200ms,
|
||||
background-color 200ms;
|
||||
transition: color 200ms;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
color: var(--text-primary);
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
.scrollerSpacer {
|
||||
display: block;
|
||||
height: var(--scroller-spacer-height);
|
||||
height: var(--messages-bottom-clearance, var(--scroller-spacer-height));
|
||||
pointer-events: none;
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
@@ -147,7 +147,7 @@
|
||||
|
||||
.messageBottomPill {
|
||||
top: auto;
|
||||
bottom: 0.625rem;
|
||||
bottom: calc(0.625rem + var(--composer-status-safe-area, 0px));
|
||||
}
|
||||
|
||||
.jumpToPresentBar {
|
||||
|
||||
@@ -324,12 +324,17 @@ export const Messages = observer(function Messages({
|
||||
ReadStateCommands.ack(channel.id, true, false);
|
||||
}
|
||||
}, [channel.id, state.messages?.hasMoreAfter, state.visualUnreadMessageId, scrollManager]);
|
||||
const onEscapePressed = useCallback(() => {
|
||||
if (scrollManager.jumpReturnToOrigin()) {
|
||||
return;
|
||||
}
|
||||
onScrollToPresentAndAck();
|
||||
}, [onScrollToPresentAndAck, scrollManager]);
|
||||
const onEscapePressed = useCallback(
|
||||
(payload?: unknown) => {
|
||||
const data = payload as {channelId?: string} | undefined;
|
||||
if (data?.channelId && data.channelId !== channel.id) return;
|
||||
if (scrollManager.jumpReturnToOrigin()) {
|
||||
return;
|
||||
}
|
||||
onScrollToPresentAndAck();
|
||||
},
|
||||
[channel.id, onScrollToPresentAndAck, scrollManager],
|
||||
);
|
||||
const onRetryLoadMessages = useCallback(() => {
|
||||
void MessageCommands.fetchMessages(channel.id, null, null, MAX_MESSAGES_PER_CHANNEL);
|
||||
}, [channel.id]);
|
||||
|
||||
@@ -11,8 +11,9 @@
|
||||
|
||||
.replyTargetButton {
|
||||
display: block;
|
||||
justify-self: start;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
@@ -35,7 +36,7 @@
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
gap: 0.125rem;
|
||||
min-height: var(--composer-status-pill-height, 1.5rem);
|
||||
min-height: var(--composer-action-button-size, 2rem);
|
||||
}
|
||||
|
||||
.mentionToggle {
|
||||
@@ -43,7 +44,7 @@
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: calc(var(--composer-status-pill-height, 1.5rem) - 0.25rem);
|
||||
height: calc(var(--composer-action-button-size, 2rem) - 0.25rem);
|
||||
inline-size: 3.5rem;
|
||||
flex: 0 0 3.5rem;
|
||||
padding: 0 0.5rem;
|
||||
@@ -91,20 +92,16 @@
|
||||
flex-shrink: 0;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
width: var(--composer-status-pill-height, 1.5rem);
|
||||
height: var(--composer-status-pill-height, 1.5rem);
|
||||
width: var(--composer-action-button-size, 2rem);
|
||||
height: var(--composer-action-button-size, 2rem);
|
||||
padding: 0;
|
||||
border-radius: var(--radius-full);
|
||||
color: var(--text-primary-muted);
|
||||
line-height: 0;
|
||||
transition:
|
||||
color 200ms,
|
||||
background-color 200ms;
|
||||
transition: color 200ms;
|
||||
}
|
||||
|
||||
.closeButton:hover {
|
||||
color: var(--text-primary);
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.closeIcon {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
|
||||
.container {
|
||||
border-color: var(--background-header-secondary);
|
||||
background-color: var(--background-secondary-lighter);
|
||||
border-color: var(--composer-divider-color, var(--background-modifier-accent));
|
||||
padding: 0.5rem 1rem;
|
||||
width: fit-content;
|
||||
}
|
||||
@@ -13,7 +12,7 @@
|
||||
}
|
||||
|
||||
.standalone {
|
||||
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
||||
border-radius: var(--radius-lg);
|
||||
border-width: 0.0625rem;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
@@ -165,14 +165,13 @@ export const EditingMessageInput = observer(
|
||||
ComponentDispatch.dispatch('INBOX_OPEN');
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Escape' && !event.shiftKey) {
|
||||
if (event.key === 'Escape' && !event.shiftKey && !event.defaultPrevented && !event.nativeEvent.isComposing) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
ComponentDispatch.dispatch('ESCAPE_PRESSED', {channelId: channel.id});
|
||||
onCancel();
|
||||
}
|
||||
},
|
||||
[actualContent, channel.id, onCancel],
|
||||
[actualContent, onCancel],
|
||||
);
|
||||
useEffect(() => {
|
||||
if (editingDisabled || hasFocusedInitiallyRef.current) {
|
||||
|
||||
@@ -7,6 +7,10 @@ import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
|
||||
import {Limits} from '@app/features/app/utils/UserLimits';
|
||||
import {fetchSlowmodeState} from '@app/features/channel/commands/ChannelCommands';
|
||||
import {ChannelAttachmentArea} from '@app/features/channel/components/ChannelAttachmentArea';
|
||||
import {
|
||||
type ChannelComposerDismissalRequest,
|
||||
requestChannelComposerAffordanceDismissal,
|
||||
} from '@app/features/channel/components/ChannelComposerDismissal';
|
||||
import {EditBar} from '@app/features/channel/components/ChannelEditBar';
|
||||
import {ReplyBar} from '@app/features/channel/components/ChannelReplyBar';
|
||||
import {ChannelStickersArea} from '@app/features/channel/components/ChannelStickersArea';
|
||||
@@ -99,7 +103,6 @@ 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 {SlashCommandIcon} from '@app/features/ui/components/icons/SlashCommandIcon';
|
||||
import FocusRing from '@app/features/ui/focus_ring/FocusRing';
|
||||
import {openPopout} from '@app/features/ui/popover/PopoverPopout';
|
||||
import ContextMenuState from '@app/features/ui/state/ContextMenu';
|
||||
import KeyboardMode from '@app/features/ui/state/KeyboardMode';
|
||||
@@ -110,7 +113,7 @@ import {openVoiceMessageComposerModal} from '@app/features/voice/components/Voic
|
||||
import {flxElementClassName} from '@app/lib/react';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import {useLingui} from '@lingui/react/macro';
|
||||
import {PlusCircleIcon} from '@phosphor-icons/react';
|
||||
import {PlusIcon} from '@phosphor-icons/react';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import type React from 'react';
|
||||
import {useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState} from 'react';
|
||||
@@ -128,6 +131,8 @@ const CLEAR_COMMAND_DESCRIPTOR = msg({
|
||||
comment: 'Accessible label for the composer button that clears the slash command currently being composed.',
|
||||
});
|
||||
|
||||
const PLUS_ICON_PROPS = {weight: 'bold'} as const;
|
||||
|
||||
export const LexicalChannelTextareaContent = observer(
|
||||
({
|
||||
channel,
|
||||
@@ -672,21 +677,88 @@ export const LexicalChannelTextareaContent = observer(
|
||||
onMentionConfirmationNeeded: handleMentionConfirmationNeeded,
|
||||
i18n: i18n,
|
||||
});
|
||||
const handleClearSlashCommand = useCallback(() => {
|
||||
const handle = handleRef.current;
|
||||
if (handle !== null) {
|
||||
handle.clear();
|
||||
}
|
||||
setValue('');
|
||||
clearSegments();
|
||||
DraftCommands.deleteDraft(channel.id);
|
||||
if (handle !== null) {
|
||||
handle.focus();
|
||||
}
|
||||
}, [channel.id, clearSegments]);
|
||||
const handleCancelEdit = useCallback(() => {
|
||||
setValue('');
|
||||
clearSegments();
|
||||
}, [clearSegments]);
|
||||
const focusComposer = useCallback(() => {
|
||||
const handle = handleRef.current;
|
||||
if (handle !== null) {
|
||||
handle.focus();
|
||||
}
|
||||
}, []);
|
||||
const dismissTopmostComposerAffordance = useCallback((): boolean => {
|
||||
const editingInline = MessageEdit.getEditingMessageId(channel.id) !== null;
|
||||
if (editingInline) {
|
||||
MessageCommands.stopEdit(channel.id);
|
||||
return true;
|
||||
}
|
||||
if (hasPendingSticker) {
|
||||
ChannelSticker.removePendingSticker(channel.id);
|
||||
focusComposer();
|
||||
return true;
|
||||
}
|
||||
if (hasAttachments) {
|
||||
CloudUpload.clearTextarea(channel.id);
|
||||
focusComposer();
|
||||
return true;
|
||||
}
|
||||
const slashCommandResolution = LexicalMessageCommandResolver.resolve(handleRef.current);
|
||||
if (slashCommandResolution.status !== LexicalMessageCommandResolutionStatus.NO_COMMAND) {
|
||||
handleClearSlashCommand();
|
||||
return true;
|
||||
}
|
||||
if (editingMessageForComposer !== null && mobileLayout.enabled) {
|
||||
MessageCommands.stopEditMobile(channel.id);
|
||||
handleCancelEdit();
|
||||
focusComposer();
|
||||
return true;
|
||||
}
|
||||
if (replyingMessage !== null) {
|
||||
MessageCommands.stopReply(channel.id);
|
||||
focusComposer();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, [
|
||||
channel.id,
|
||||
editingMessageForComposer,
|
||||
focusComposer,
|
||||
handleCancelEdit,
|
||||
handleClearSlashCommand,
|
||||
hasAttachments,
|
||||
hasPendingSticker,
|
||||
mobileLayout.enabled,
|
||||
replyingMessage,
|
||||
]);
|
||||
useEffect(() => {
|
||||
return ComponentDispatch.subscribe('TEXTAREA_DISMISS_AFFORDANCE', (request?: unknown) => {
|
||||
const dismissalRequest = request as ChannelComposerDismissalRequest | undefined;
|
||||
if (dismissalRequest?.channelId !== channel.id) {
|
||||
return false;
|
||||
}
|
||||
return dismissTopmostComposerAffordance();
|
||||
});
|
||||
}, [channel.id, dismissTopmostComposerAffordance]);
|
||||
const handleEscapeKey = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLElement>) => {
|
||||
if (event.key !== 'Escape') return;
|
||||
if (event.shiftKey) return;
|
||||
if (hasAttachments || hasPendingSticker || replyingMessage) {
|
||||
if (event.defaultPrevented || event.nativeEvent.isComposing) return;
|
||||
if (event.key !== 'Escape' || event.shiftKey) return;
|
||||
if (requestChannelComposerAffordanceDismissal(channel.id)) {
|
||||
event.preventDefault();
|
||||
if (hasAttachments) {
|
||||
CloudUpload.clearTextarea(channel.id);
|
||||
}
|
||||
if (hasPendingSticker) {
|
||||
ChannelSticker.removePendingSticker(channel.id);
|
||||
}
|
||||
if (replyingMessage) {
|
||||
MessageCommands.stopReply(channel.id);
|
||||
}
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
if (isInputAreaFocused && KeyboardMode.keyboardModeEnabled) {
|
||||
@@ -698,15 +770,7 @@ export const LexicalChannelTextareaContent = observer(
|
||||
KeyboardMode.exitKeyboardMode();
|
||||
}
|
||||
},
|
||||
[
|
||||
channel.id,
|
||||
hasAttachments,
|
||||
hasPendingSticker,
|
||||
replyingMessage,
|
||||
isInputAreaFocused,
|
||||
KeyboardMode.keyboardModeEnabled,
|
||||
Accessibility.escapeExitsKeyboardMode,
|
||||
],
|
||||
[channel.id, isInputAreaFocused, KeyboardMode.keyboardModeEnabled, Accessibility.escapeExitsKeyboardMode],
|
||||
);
|
||||
const slotResolvers = useMemo<SlashSlotResolvers>(() => {
|
||||
const guildId = channel.guildId;
|
||||
@@ -776,18 +840,6 @@ export const LexicalChannelTextareaContent = observer(
|
||||
}
|
||||
MessageCommands.startEdit(channel.id, message.id, message.content);
|
||||
}, [channel.id]);
|
||||
const handleClearSlashCommand = useCallback(() => {
|
||||
const handle = handleRef.current;
|
||||
if (handle !== null) {
|
||||
handle.clear();
|
||||
}
|
||||
setValue('');
|
||||
clearSegments();
|
||||
DraftCommands.deleteDraft(channel.id);
|
||||
if (handle !== null) {
|
||||
handle.focus();
|
||||
}
|
||||
}, [channel.id, clearSegments]);
|
||||
useTextareaDraftAndTyping({
|
||||
channelId: channel.id,
|
||||
value,
|
||||
@@ -808,11 +860,6 @@ export const LexicalChannelTextareaContent = observer(
|
||||
textareaInputDisabled,
|
||||
isFocused,
|
||||
handleArrowUpEmpty,
|
||||
editingMessage,
|
||||
replyingMessage,
|
||||
mobileLayout,
|
||||
setValue,
|
||||
clearSegments,
|
||||
});
|
||||
const messageLabel = i18n._(MESSAGE_DESCRIPTOR);
|
||||
const messagePrefix = `${messageLabel} `;
|
||||
@@ -933,10 +980,6 @@ export const LexicalChannelTextareaContent = observer(
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [mobileLayout.enabled]);
|
||||
const handleCancelEdit = useCallback(() => {
|
||||
setValue('');
|
||||
clearSegments();
|
||||
}, [clearSegments]);
|
||||
const isPlusContextMenuOpen = useCallback(() => {
|
||||
const plusButton = plusButtonRef.current;
|
||||
const contextMenu = ContextMenuState.contextMenu;
|
||||
@@ -1093,36 +1136,29 @@ export const LexicalChannelTextareaContent = observer(
|
||||
],
|
||||
);
|
||||
const isSlashParamBarVisible = slashCommandState.activeSlot != null;
|
||||
const hasStackedSections = Boolean(
|
||||
referencedMessage ||
|
||||
(editingMessage && mobileLayout.enabled) ||
|
||||
uploadAttachments.length > 0 ||
|
||||
hasPendingSticker ||
|
||||
isSlashParamBarVisible,
|
||||
);
|
||||
const isMobileEditBarVisible = editingMessage && mobileLayout.enabled;
|
||||
const isReplyBarVisible = !isMobileEditBarVisible && referencedMessage != null;
|
||||
const presentableTypingUsers = usePresentableTypingUsers(channel);
|
||||
const isTypingStatusVisible = !isAutocompleteVisible && presentableTypingUsers.length > 0;
|
||||
const isSlowmodeIndicatorVisible = isSlowmodeEnabled;
|
||||
const hasLeadingStatusContent = isMobileEditBarVisible || isReplyBarVisible || isSlashParamBarVisible;
|
||||
const hasComposerStatusRail = isTypingStatusVisible || hasLeadingStatusContent || isSlowmodeIndicatorVisible;
|
||||
let shouldReplyMention = false;
|
||||
if (replyingMessage !== null && replyingMessage !== undefined) {
|
||||
shouldReplyMention = replyingMessage.mentioning;
|
||||
}
|
||||
const topBarContent = isMobileEditBarVisible ? (
|
||||
<EditBar channel={channel} onCancel={handleCancelEdit} />
|
||||
) : (
|
||||
referencedMessage && (
|
||||
let topBarContent: React.ReactNode = null;
|
||||
if (isMobileEditBarVisible) {
|
||||
topBarContent = <EditBar channel={channel} onCancel={handleCancelEdit} />;
|
||||
} else if (referencedMessage !== null) {
|
||||
topBarContent = (
|
||||
<ReplyBar
|
||||
replyingMessageObject={referencedMessage}
|
||||
shouldReplyMention={shouldReplyMention}
|
||||
setShouldReplyMention={(mentioning) => MessageCommands.setReplyMentioning(channel.id, mentioning)}
|
||||
channel={channel}
|
||||
/>
|
||||
)
|
||||
);
|
||||
);
|
||||
}
|
||||
const renderSection = (content: React.ReactNode, sectionClassName?: string) => (
|
||||
<flx-channel-textarea-section className={flxElementClassName(wrapperStyles.stackSection, sectionClassName)}>
|
||||
{content}
|
||||
@@ -1130,171 +1166,155 @@ export const LexicalChannelTextareaContent = observer(
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<FocusRing
|
||||
focusTarget={editableRef}
|
||||
ringTarget={containerRef}
|
||||
offset={0}
|
||||
enabled={!textareaInputDisabled && Accessibility.showTextareaFocusRing}
|
||||
ringClassName={styles.textareaFocusRing}
|
||||
<flx-channel-textarea
|
||||
ref={containerRef}
|
||||
className={flxElementClassName(
|
||||
wrapperStyles.box,
|
||||
wrapperStyles.composerRoot,
|
||||
wrapperStyles.wrapperSides,
|
||||
styles.textareaOuter,
|
||||
mobileLayout.enabled && styles.textareaOuterMobile,
|
||||
wrapperStyles.roundedAll,
|
||||
textareaInputDisabled && wrapperStyles.disabled,
|
||||
!mobileLayout.enabled && styles.textareaOuterRow,
|
||||
)}
|
||||
>
|
||||
<flx-channel-textarea
|
||||
ref={containerRef}
|
||||
className={flxElementClassName(
|
||||
wrapperStyles.box,
|
||||
wrapperStyles.wrapperSides,
|
||||
styles.textareaOuter,
|
||||
mobileLayout.enabled && styles.textareaOuterMobile,
|
||||
hasStackedSections ? wrapperStyles.roundedBottom : wrapperStyles.roundedAll,
|
||||
wrapperStyles.bottomSpacing,
|
||||
textareaInputDisabled && wrapperStyles.disabled,
|
||||
!mobileLayout.enabled && styles.textareaOuterMinHeight,
|
||||
)}
|
||||
>
|
||||
{hasComposerStatusRail && (
|
||||
<flx-channel-textarea-status-rail className={flxElementClassName(wrapperStyles.statusRail)}>
|
||||
<flx-channel-textarea-status-rail-left
|
||||
className={flxElementClassName(
|
||||
wrapperStyles.statusRailLeft,
|
||||
hasLeadingStatusContent && wrapperStyles.statusRailLeftWithLeading,
|
||||
)}
|
||||
>
|
||||
{topBarContent && (
|
||||
<flx-channel-textarea-top-bar className={flxElementClassName(wrapperStyles.topBarContainer)}>
|
||||
{topBarContent}
|
||||
</flx-channel-textarea-top-bar>
|
||||
)}
|
||||
{slashCommandState.activeSlot != null && (
|
||||
<flx-channel-textarea-slash-command-bar
|
||||
className={flxElementClassName(wrapperStyles.topBarContainer)}
|
||||
>
|
||||
<SlashCommandParamBar
|
||||
activeSlot={slashCommandState.activeSlot}
|
||||
onClear={handleClearSlashCommand}
|
||||
/>
|
||||
</flx-channel-textarea-slash-command-bar>
|
||||
)}
|
||||
{isTypingStatusVisible && (
|
||||
<flx-channel-textarea-typing-slot className={flxElementClassName(wrapperStyles.statusTypingSlot)}>
|
||||
<TypingUsers channel={channel} withText={true} showAvatars={true} />
|
||||
</flx-channel-textarea-typing-slot>
|
||||
)}
|
||||
</flx-channel-textarea-status-rail-left>
|
||||
{isSlowmodeIndicatorVisible && (
|
||||
<flx-channel-textarea-slowmode-slot className={flxElementClassName(wrapperStyles.statusSlowmodeSlot)}>
|
||||
<SlowmodeIndicator
|
||||
slowmodeRemaining={slowmodeRemaining}
|
||||
slowmodeDuration={channel.rateLimitPerUser * 1000}
|
||||
isImmune={isSlowmodeImmune}
|
||||
/>
|
||||
</flx-channel-textarea-slowmode-slot>
|
||||
)}
|
||||
</flx-channel-textarea-status-rail>
|
||||
)}
|
||||
{showAttachments &&
|
||||
renderSection(<ChannelAttachmentArea channelId={channel.id} />, styles.collapsibleSection)}
|
||||
{showStickers &&
|
||||
renderSection(
|
||||
<ChannelStickersArea channelId={channel.id} hasAttachments={hasAttachments} />,
|
||||
styles.collapsibleSection,
|
||||
<flx-channel-textarea-status-rail className={flxElementClassName(wrapperStyles.statusRail)}>
|
||||
<flx-channel-textarea-status-rail-left className={flxElementClassName(wrapperStyles.statusRailLeft)}>
|
||||
{isTypingStatusVisible && (
|
||||
<flx-channel-textarea-typing-slot className={flxElementClassName(wrapperStyles.statusTypingSlot)}>
|
||||
<TypingUsers channel={channel} withText={true} showAvatars={true} />
|
||||
</flx-channel-textarea-typing-slot>
|
||||
)}
|
||||
{renderSection(
|
||||
<flx-channel-textarea-box
|
||||
className={flxElementClassName(
|
||||
styles.mainWrapperDense,
|
||||
textareaInputDisabled && wrapperStyles.disabled,
|
||||
)}
|
||||
>
|
||||
<flx-channel-textarea-upload-column
|
||||
className={flxElementClassName(styles.uploadButtonColumn, styles.sideButtonPadding)}
|
||||
>
|
||||
<TextareaButton
|
||||
icon={slashCommandState.hasSlots ? SlashCommandIcon : PlusCircleIcon}
|
||||
label={slashCommandState.hasSlots ? i18n._(CLEAR_COMMAND_DESCRIPTOR) : i18n._(OPEN_MENU_DESCRIPTOR)}
|
||||
disabled={textareaInputDisabled}
|
||||
aria-hidden={textareaInputDisabled ? true : undefined}
|
||||
onMouseDown={slashCommandState.hasSlots ? undefined : handlePlusMenuMouseDown}
|
||||
onClick={slashCommandState.hasSlots ? handleClearSlashCommand : handlePlusMenuClick}
|
||||
forceHover={!slashCommandState.hasSlots && plusContextMenuOpen}
|
||||
className={plusContextMenuOpen ? styles.plusButtonAboveBackdrop : undefined}
|
||||
ref={plusButtonRef}
|
||||
/>
|
||||
</flx-channel-textarea-upload-column>
|
||||
<flx-channel-textarea-content
|
||||
ref={contentAreaRef}
|
||||
className={flxElementClassName(styles.contentAreaDense)}
|
||||
>
|
||||
<flx-channel-textarea-composer className={flxElementClassName(lexicalStyles.composerHost)}>
|
||||
<LexicalComposerInput
|
||||
placeholder={placeholderText}
|
||||
disabled={textareaInputDisabled}
|
||||
handleRef={handleRef}
|
||||
initialValue={initialDraftRef.current.display}
|
||||
initialSegments={initialDraftRef.current.segments}
|
||||
slotResolvers={slotResolvers}
|
||||
emojiShortcodeResolver={composerEmojiResolver}
|
||||
channelId={channel.id}
|
||||
guildId={channel.guildId}
|
||||
submitOnEnter={!mobileLayout.enabled}
|
||||
className={lexicalStyles.composerEditable}
|
||||
autocompleteOptions={autocompleteOptions}
|
||||
autocompleteType={autocompleteType}
|
||||
autocompleteQuery={autocompleteQuery}
|
||||
autocompleteEnabled={!textareaInputDisabled}
|
||||
slotMenuActive={isSlotMenu}
|
||||
autocompleteReferenceElement={containerRef.current}
|
||||
autocompleteListboxId={autocompleteListId}
|
||||
onAutocompleteSelect={handleSelect}
|
||||
onChange={handleEditorChange}
|
||||
onCursorMove={onCursorMove}
|
||||
onEnter={handleSubmit}
|
||||
onArrowUp={handleArrowUpEmpty}
|
||||
onKeyDown={handleEditorKeyDown}
|
||||
onFocus={() => {
|
||||
setIsFocused(true);
|
||||
setIsInputAreaFocused(true);
|
||||
ChannelSearch.setInputFocused(channel.id, false);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setIsFocused(false);
|
||||
setIsInputAreaFocused(false);
|
||||
}}
|
||||
onSlashCommandStateChange={setSlashCommandState}
|
||||
/>
|
||||
</flx-channel-textarea-composer>
|
||||
</flx-channel-textarea-content>
|
||||
<TextareaButtons
|
||||
disabled={textareaInputDisabled}
|
||||
showAllButtons={showAllButtons}
|
||||
showGifButton={showGifButton}
|
||||
showMemesButton={showMemesButton}
|
||||
showStickersButton={showStickersButton}
|
||||
showEmojiButton={showEmojiButton}
|
||||
showMessageSendButton={showMessageSendButton}
|
||||
showVoiceMessageButton={false}
|
||||
expressionPickerOpen={expressionPickerOpen}
|
||||
selectedTab={selectedTab}
|
||||
isMobile={mobileLayout.enabled}
|
||||
isSlowmodeActive={isSubmissionBlockedBySlowmode}
|
||||
isOverLimit={isOverCharacterLimit}
|
||||
hasContent={hasMessageContent}
|
||||
hasAttachments={uploadAttachments.length > 0}
|
||||
expressionPickerTriggerRef={expressionPickerTriggerRef}
|
||||
invisibleExpressionPickerTriggerRef={invisibleExpressionPickerTriggerRef}
|
||||
onExpressionPickerToggle={handleExpressionPickerTabToggle}
|
||||
onSubmit={handleSubmit}
|
||||
channelId={channel.id}
|
||||
</flx-channel-textarea-status-rail-left>
|
||||
{isSlowmodeIndicatorVisible && (
|
||||
<flx-channel-textarea-slowmode-slot className={flxElementClassName(wrapperStyles.statusSlowmodeSlot)}>
|
||||
<SlowmodeIndicator
|
||||
slowmodeRemaining={slowmodeRemaining}
|
||||
slowmodeDuration={channel.rateLimitPerUser * 1000}
|
||||
isImmune={isSlowmodeImmune}
|
||||
/>
|
||||
</flx-channel-textarea-box>,
|
||||
styles.inputSection,
|
||||
</flx-channel-textarea-slowmode-slot>
|
||||
)}
|
||||
<MessageCharacterCounter
|
||||
currentLength={trimmedMessageContent.length}
|
||||
maxLength={maxMessageLength}
|
||||
canUpgrade={maxMessageLength < premiumMaxLength}
|
||||
premiumMaxLength={premiumMaxLength}
|
||||
/>
|
||||
</flx-channel-textarea>
|
||||
</FocusRing>
|
||||
</flx-channel-textarea-status-rail>
|
||||
{hasLeadingStatusContent && (
|
||||
<div className={wrapperStyles.composerActionStack} data-flx="channel.textarea.composer-action-stack">
|
||||
{topBarContent !== null && (
|
||||
<div className={wrapperStyles.composerActionRow} data-flx="channel.textarea.composer-action-row">
|
||||
{topBarContent}
|
||||
</div>
|
||||
)}
|
||||
{slashCommandState.activeSlot !== null && (
|
||||
<div className={wrapperStyles.composerActionRow} data-flx="channel.textarea.slash-command-action-row">
|
||||
<SlashCommandParamBar activeSlot={slashCommandState.activeSlot} onClear={handleClearSlashCommand} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{showAttachments &&
|
||||
renderSection(<ChannelAttachmentArea channelId={channel.id} />, styles.collapsibleSection)}
|
||||
{showStickers &&
|
||||
renderSection(
|
||||
<ChannelStickersArea channelId={channel.id} hasAttachments={hasAttachments} />,
|
||||
styles.collapsibleSection,
|
||||
)}
|
||||
{renderSection(
|
||||
<flx-channel-textarea-box
|
||||
className={flxElementClassName(styles.mainWrapperDense, textareaInputDisabled && wrapperStyles.disabled)}
|
||||
>
|
||||
<flx-channel-textarea-upload-column
|
||||
className={flxElementClassName(styles.uploadButtonColumn, styles.sideButtonPadding)}
|
||||
>
|
||||
<TextareaButton
|
||||
iconProps={PLUS_ICON_PROPS}
|
||||
icon={slashCommandState.hasSlots ? SlashCommandIcon : PlusIcon}
|
||||
label={slashCommandState.hasSlots ? i18n._(CLEAR_COMMAND_DESCRIPTOR) : i18n._(OPEN_MENU_DESCRIPTOR)}
|
||||
disabled={textareaInputDisabled}
|
||||
aria-hidden={textareaInputDisabled ? true : undefined}
|
||||
onMouseDown={slashCommandState.hasSlots ? undefined : handlePlusMenuMouseDown}
|
||||
onClick={slashCommandState.hasSlots ? handleClearSlashCommand : handlePlusMenuClick}
|
||||
forceHover={!slashCommandState.hasSlots && plusContextMenuOpen}
|
||||
className={plusContextMenuOpen ? styles.plusButtonAboveBackdrop : undefined}
|
||||
ref={plusButtonRef}
|
||||
/>
|
||||
</flx-channel-textarea-upload-column>
|
||||
<flx-channel-textarea-content
|
||||
ref={contentAreaRef}
|
||||
className={flxElementClassName(styles.contentAreaDense)}
|
||||
>
|
||||
<flx-channel-textarea-composer className={flxElementClassName(lexicalStyles.composerHost)}>
|
||||
<LexicalComposerInput
|
||||
placeholder={placeholderText}
|
||||
disabled={textareaInputDisabled}
|
||||
handleRef={handleRef}
|
||||
initialValue={initialDraftRef.current.display}
|
||||
initialSegments={initialDraftRef.current.segments}
|
||||
slotResolvers={slotResolvers}
|
||||
emojiShortcodeResolver={composerEmojiResolver}
|
||||
channelId={channel.id}
|
||||
guildId={channel.guildId}
|
||||
submitOnEnter={!mobileLayout.enabled}
|
||||
focusRingTarget={containerRef}
|
||||
focusRingEnabled={!textareaInputDisabled && Accessibility.showTextareaFocusRing}
|
||||
className={lexicalStyles.composerEditable}
|
||||
autocompleteOptions={autocompleteOptions}
|
||||
autocompleteType={autocompleteType}
|
||||
autocompleteQuery={autocompleteQuery}
|
||||
autocompleteEnabled={!textareaInputDisabled}
|
||||
slotMenuActive={isSlotMenu}
|
||||
autocompleteReferenceElement={containerRef.current}
|
||||
autocompleteListboxId={autocompleteListId}
|
||||
onAutocompleteSelect={handleSelect}
|
||||
onChange={handleEditorChange}
|
||||
onCursorMove={onCursorMove}
|
||||
onEnter={handleSubmit}
|
||||
onArrowUp={handleArrowUpEmpty}
|
||||
onKeyDown={handleEditorKeyDown}
|
||||
onFocus={() => {
|
||||
setIsFocused(true);
|
||||
setIsInputAreaFocused(true);
|
||||
ChannelSearch.setInputFocused(channel.id, false);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setIsFocused(false);
|
||||
setIsInputAreaFocused(false);
|
||||
}}
|
||||
onSlashCommandStateChange={setSlashCommandState}
|
||||
/>
|
||||
</flx-channel-textarea-composer>
|
||||
</flx-channel-textarea-content>
|
||||
<TextareaButtons
|
||||
disabled={textareaInputDisabled}
|
||||
showAllButtons={showAllButtons}
|
||||
showGifButton={showGifButton}
|
||||
showMemesButton={showMemesButton}
|
||||
showStickersButton={showStickersButton}
|
||||
showEmojiButton={showEmojiButton}
|
||||
showMessageSendButton={showMessageSendButton}
|
||||
showVoiceMessageButton={false}
|
||||
expressionPickerOpen={expressionPickerOpen}
|
||||
selectedTab={selectedTab}
|
||||
isMobile={mobileLayout.enabled}
|
||||
isSlowmodeActive={isSubmissionBlockedBySlowmode}
|
||||
isOverLimit={isOverCharacterLimit}
|
||||
hasContent={hasMessageContent}
|
||||
hasAttachments={uploadAttachments.length > 0}
|
||||
expressionPickerTriggerRef={expressionPickerTriggerRef}
|
||||
invisibleExpressionPickerTriggerRef={invisibleExpressionPickerTriggerRef}
|
||||
onExpressionPickerToggle={handleExpressionPickerTabToggle}
|
||||
onSubmit={handleSubmit}
|
||||
channelId={channel.id}
|
||||
/>
|
||||
</flx-channel-textarea-box>,
|
||||
styles.inputSection,
|
||||
)}
|
||||
<MessageCharacterCounter
|
||||
currentLength={trimmedMessageContent.length}
|
||||
maxLength={maxMessageLength}
|
||||
canUpgrade={maxMessageLength < premiumMaxLength}
|
||||
premiumMaxLength={premiumMaxLength}
|
||||
/>
|
||||
</flx-channel-textarea>
|
||||
{mobileLayout.enabled && (
|
||||
<>
|
||||
<ExpressionPickerSheet
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
.container {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
bottom: 0.5rem;
|
||||
right: var(--composer-box-padding-inline, 0px);
|
||||
bottom: calc(100% + var(--composer-status-safe-gap, 0.5rem));
|
||||
z-index: 4;
|
||||
}
|
||||
@@ -4,9 +4,11 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-height: var(--composer-status-pill-height, 1.5rem);
|
||||
min-height: 2.25rem;
|
||||
min-width: 0;
|
||||
padding: 0 0.25rem 0 0.625rem;
|
||||
width: 100%;
|
||||
padding: 0 0.375rem 0 0.75rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.text {
|
||||
@@ -15,7 +17,7 @@
|
||||
gap: 0.4375rem;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
flex: 0 1 auto;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.name {
|
||||
@@ -46,6 +48,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.button {
|
||||
@@ -56,17 +59,15 @@
|
||||
flex-shrink: 0;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
width: var(--composer-status-pill-height, 1.5rem);
|
||||
height: var(--composer-status-pill-height, 1.5rem);
|
||||
width: var(--composer-action-button-size, 2rem);
|
||||
height: var(--composer-action-button-size, 2rem);
|
||||
padding: 0;
|
||||
border-radius: var(--radius-full);
|
||||
color: var(--text-primary-muted);
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
color: var(--text-primary);
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
||||
@@ -4,34 +4,17 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.1875rem;
|
||||
height: var(--composer-status-pill-height, var(--slowmode-indicator-height));
|
||||
height: var(--slowmode-indicator-height, 1.125rem);
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
padding: 0 0.5rem;
|
||||
border-radius: var(--radius-full);
|
||||
background-color: var(--background-tertiary);
|
||||
border: 0.0625rem solid color-mix(in srgb, var(--background-modifier-accent) 80%, transparent);
|
||||
color: var(--text-primary-muted);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1;
|
||||
transition:
|
||||
color 150ms ease,
|
||||
background-color 150ms ease,
|
||||
border-color 150ms ease;
|
||||
transition: color 150ms ease;
|
||||
}
|
||||
|
||||
.cooldown {
|
||||
color: var(--status-danger);
|
||||
background-color: color-mix(in srgb, var(--status-danger) 14%, transparent);
|
||||
border-color: color-mix(in srgb, var(--status-danger) 45%, transparent);
|
||||
}
|
||||
|
||||
.time {
|
||||
flex: 0 0 auto;
|
||||
font-weight: 500;
|
||||
font-family: monospace;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.label {
|
||||
@@ -42,4 +25,9 @@
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
@@ -16,22 +16,29 @@ import {ClockIcon} from '@phosphor-icons/react';
|
||||
import {clsx} from 'clsx';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
|
||||
const SLOWMODE_IS_ENABLED_BUT_YOU_ARE_IMMUNE_DESCRIPTOR = msg({
|
||||
message: 'Slowmode is enabled, but you are immune.',
|
||||
comment: 'Description text in the channel and chat slowmode indicator.',
|
||||
});
|
||||
const YOU_ARE_IN_SLOWMODE_PLEASE_WAIT_BEFORE_SENDING_DESCRIPTOR = msg({
|
||||
message: "You're in slowmode. Wait before sending another message.",
|
||||
comment: 'Description text in the channel and chat slowmode indicator.',
|
||||
});
|
||||
const SLOWMODE_IS_ENABLED_FOR_THIS_CHANNEL_DESCRIPTOR = msg({
|
||||
message: 'Slowmode is enabled for this channel.',
|
||||
comment: 'Description text in the channel and chat slowmode indicator.',
|
||||
});
|
||||
const SLOWMODE_DESCRIPTOR = msg({
|
||||
message: '{durationLabel} slowmode',
|
||||
const SLOWMODE_IS_SET_BUT_YOU_ARE_IMMUNE_DESCRIPTOR = msg({
|
||||
message: 'Slowmode is set to {durationLabel}, but you are immune.',
|
||||
comment:
|
||||
'Short label in the channel and chat slowmode indicator. Keep it concise. Preserve {durationLabel}; it is inserted by code.',
|
||||
'Tooltip on the composer slowmode indicator when the reader can bypass slowmode. Preserve {durationLabel}; it is inserted by code.',
|
||||
});
|
||||
const SLOWMODE_IS_SET_WAIT_BEFORE_SENDING_DESCRIPTOR = msg({
|
||||
message: 'Slowmode is set to {durationLabel}. Wait before sending another message.',
|
||||
comment:
|
||||
'Tooltip on the composer slowmode indicator while the reader is counting down. Preserve {durationLabel}; it is inserted by code.',
|
||||
});
|
||||
const SLOWMODE_IS_SET_FOR_THIS_CHANNEL_DESCRIPTOR = msg({
|
||||
message: 'Slowmode is set to {durationLabel} for this channel.',
|
||||
comment:
|
||||
'Tooltip on the composer slowmode indicator when the reader is free to send. Preserve {durationLabel}; it is inserted by code.',
|
||||
});
|
||||
const SLOWMODE_IS_ENABLED_DESCRIPTOR = msg({
|
||||
message: 'Slowmode is enabled',
|
||||
comment: 'Short label in the composer slowmode indicator when the reader is free to send. Keep it concise.',
|
||||
});
|
||||
const SLOWMODE_IS_ACTIVE_DESCRIPTOR = msg({
|
||||
message: 'Slowmode is active ({remaining})',
|
||||
comment:
|
||||
'Short label in the composer slowmode indicator while the countdown runs. Keep it concise. Preserve {remaining}; it is an mm:ss or hh:mm:ss timer inserted by code.',
|
||||
});
|
||||
|
||||
interface SlowmodeIndicatorProps {
|
||||
@@ -83,28 +90,36 @@ export const SlowmodeIndicator = observer(({slowmodeRemaining, slowmodeDuration,
|
||||
const {i18n} = useLingui();
|
||||
const locale = i18n.locale;
|
||||
const onCooldown = !isImmune && slowmodeRemaining > 0;
|
||||
const tooltipText = isImmune
|
||||
? i18n._(SLOWMODE_IS_ENABLED_BUT_YOU_ARE_IMMUNE_DESCRIPTOR)
|
||||
: onCooldown
|
||||
? i18n._(YOU_ARE_IN_SLOWMODE_PLEASE_WAIT_BEFORE_SENDING_DESCRIPTOR)
|
||||
: i18n._(SLOWMODE_IS_ENABLED_FOR_THIS_CHANNEL_DESCRIPTOR);
|
||||
const durationLabel = formatSlowmodeDuration(slowmodeDuration, locale);
|
||||
let tooltipText: string;
|
||||
if (isImmune) {
|
||||
tooltipText = i18n._(SLOWMODE_IS_SET_BUT_YOU_ARE_IMMUNE_DESCRIPTOR, {durationLabel});
|
||||
} else if (onCooldown) {
|
||||
tooltipText = i18n._(SLOWMODE_IS_SET_WAIT_BEFORE_SENDING_DESCRIPTOR, {durationLabel});
|
||||
} else {
|
||||
tooltipText = i18n._(SLOWMODE_IS_SET_FOR_THIS_CHANNEL_DESCRIPTOR, {durationLabel});
|
||||
}
|
||||
let statusLabel: string;
|
||||
if (onCooldown) {
|
||||
statusLabel = i18n._(SLOWMODE_IS_ACTIVE_DESCRIPTOR, {remaining: formatSlowmodeTime(slowmodeRemaining, locale)});
|
||||
} else {
|
||||
statusLabel = i18n._(SLOWMODE_IS_ENABLED_DESCRIPTOR);
|
||||
}
|
||||
return (
|
||||
<Tooltip text={tooltipText} data-flx="channel.slowmode-indicator.tooltip">
|
||||
<div
|
||||
className={clsx(styles.container, onCooldown && styles.cooldown)}
|
||||
data-flx="channel.slowmode-indicator.container"
|
||||
>
|
||||
<ClockIcon size={remFromPx(12)} weight="fill" data-flx="channel.slowmode-indicator.clock-icon" />
|
||||
{onCooldown ? (
|
||||
<span className={styles.time} data-flx="channel.slowmode-indicator.time">
|
||||
{formatSlowmodeTime(slowmodeRemaining, locale)}
|
||||
</span>
|
||||
) : (
|
||||
<span className={styles.label} data-flx="channel.slowmode-indicator.label">
|
||||
{i18n._(SLOWMODE_DESCRIPTOR, {durationLabel})}
|
||||
</span>
|
||||
)}
|
||||
<span className={styles.label} data-flx="channel.slowmode-indicator.label">
|
||||
{statusLabel}
|
||||
</span>
|
||||
<ClockIcon
|
||||
size={remFromPx(12)}
|
||||
weight="fill"
|
||||
className={styles.icon}
|
||||
data-flx="channel.slowmode-indicator.clock-icon"
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -5,9 +5,19 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-primary);
|
||||
color: var(--text-primary-muted);
|
||||
}
|
||||
|
||||
.username {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.composerStatus {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: var(--typing-pill-height, 1.125rem);
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
color: var(--text-primary-muted);
|
||||
pointer-events: auto;
|
||||
}
|
||||
@@ -118,10 +118,7 @@ export const TypingUsers = observer(
|
||||
className={`${messageStyles.typingContainer} ${messageStyles.typingCluster} ${messageStyles.typingClusterComposerStatus}`}
|
||||
data-flx="channel.typing-users.div"
|
||||
>
|
||||
<div
|
||||
className={`${messageStyles.typingPill} ${messageStyles.typingPillComposerStatus}`}
|
||||
data-flx="channel.typing-users.div--2"
|
||||
>
|
||||
<div className={styles.composerStatus} data-flx="channel.typing-users.div--2">
|
||||
<div className={messageStyles.typingIndicator} data-flx="channel.typing-users.div--3">
|
||||
<Typing
|
||||
className={styles.typing}
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
.barrierLayout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, auto) minmax(0, 1fr) minmax(0, auto);
|
||||
align-items: flex-start;
|
||||
align-items: start;
|
||||
position: relative;
|
||||
min-height: var(--textarea-min-height);
|
||||
box-sizing: border-box;
|
||||
padding: var(--user-area-padding-y) 0;
|
||||
padding: var(--textarea-padding-y) 0;
|
||||
column-gap: var(--textarea-upload-gap);
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
@@ -51,12 +51,18 @@
|
||||
|
||||
.actionArea > button {
|
||||
margin: 0;
|
||||
height: var(--textarea-button-height);
|
||||
min-height: var(--textarea-button-height);
|
||||
}
|
||||
|
||||
.timer {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
height: var(--textarea-button-height);
|
||||
border-radius: 0.375rem;
|
||||
background-color: var(--background-secondary-alt);
|
||||
padding: 0.375rem 0.75rem;
|
||||
padding: 0 0.75rem;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
|
||||
@@ -55,9 +55,8 @@ const BarrierBase = observer(({message, action, icon}: BarrierBaseProps) => {
|
||||
wrapperStyles.box,
|
||||
wrapperStyles.wrapperSides,
|
||||
textareaStyles.textareaOuter,
|
||||
textareaStyles.textareaOuterMinHeight,
|
||||
textareaStyles.textareaOuterRow,
|
||||
wrapperStyles.roundedAll,
|
||||
wrapperStyles.bottomSpacing,
|
||||
)}
|
||||
data-flx="channel.barriers.barrier-components.barrier-base.div"
|
||||
>
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
|
||||
.wrapperSides {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.box {
|
||||
position: relative;
|
||||
background-color: var(--background-secondary-lighter);
|
||||
border: none;
|
||||
transition: border-color 0.2s ease;
|
||||
margin-bottom: 0;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -28,15 +21,11 @@
|
||||
}
|
||||
|
||||
.stackSection + .stackSection {
|
||||
border-top: 0.0625rem solid var(--user-area-divider-color);
|
||||
}
|
||||
|
||||
.box:focus-within {
|
||||
border-top: none;
|
||||
border-top: 0.0625rem solid var(--composer-divider-color, var(--user-area-divider-color));
|
||||
}
|
||||
|
||||
.roundedAll {
|
||||
border-radius: 0;
|
||||
border-radius: var(--composer-box-radius, 0px);
|
||||
}
|
||||
|
||||
.roundedTop {
|
||||
@@ -44,32 +33,42 @@
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.roundedBottom {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.noBottomBorder {
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
|
||||
.bottomSpacing {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.statusRail {
|
||||
--composer-status-pill-height: 1.5rem;
|
||||
--typing-pill-height: var(--composer-status-pill-height);
|
||||
--slowmode-indicator-height: var(--composer-status-pill-height);
|
||||
.composerRoot {
|
||||
--composer-action-button-size: 2rem;
|
||||
--composer-divider-color: var(--floating-surface-ring-color-strong);
|
||||
}
|
||||
|
||||
.composerRoot::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset-inline: var(--composer-status-rail-inset, 0);
|
||||
top: 0;
|
||||
transform: translateY(-50%);
|
||||
bottom: calc(100% + var(--composer-box-inset, 0px));
|
||||
inset-inline: calc(-1 * var(--composer-box-inset-inline, 0px));
|
||||
height: max(0px, calc(var(--composer-status-safe-area) - var(--composer-box-inset, 0px)));
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
color-mix(in srgb, var(--composer-surface-color) 42%, transparent) 34%,
|
||||
color-mix(in srgb, var(--composer-surface-color) 82%, transparent) 66%,
|
||||
var(--composer-surface-color) 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.statusRail {
|
||||
--typing-pill-height: var(--composer-status-line-height, 1.125rem);
|
||||
--slowmode-indicator-height: var(--composer-status-line-height, 1.125rem);
|
||||
position: absolute;
|
||||
inset-inline: var(--composer-box-padding-inline, 0px);
|
||||
bottom: calc(100% + var(--composer-status-safe-gap, 0.5rem));
|
||||
z-index: 3;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, auto);
|
||||
@@ -77,24 +76,17 @@
|
||||
gap: 0.5rem;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
min-height: calc(var(--composer-status-pill-height) + 0.5rem);
|
||||
padding: 0.25rem 0;
|
||||
height: var(--composer-status-line-height, 1.125rem);
|
||||
box-sizing: border-box;
|
||||
text-shadow:
|
||||
0 0 0.1875rem var(--composer-surface-color),
|
||||
0 0 0.5rem var(--composer-surface-color);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.statusRailInFlow {
|
||||
position: static;
|
||||
inset: auto;
|
||||
transform: none;
|
||||
z-index: auto;
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.statusRailLeft {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
@@ -109,11 +101,6 @@
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.statusRailLeftWithLeading .statusTypingSlot {
|
||||
margin-left: auto;
|
||||
max-width: min(22rem, 45%);
|
||||
}
|
||||
|
||||
.statusTypingSlot > * {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
@@ -126,17 +113,36 @@
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.topBarContainer {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
.composerActionStack {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
pointer-events: auto;
|
||||
width: fit-content;
|
||||
flex: 0 1 auto;
|
||||
flex: 0 0 auto;
|
||||
flex-direction: column;
|
||||
margin-inline: calc(-1 * var(--composer-box-padding-inline, 0px));
|
||||
min-width: 0;
|
||||
width: auto;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 0.0625rem solid var(--composer-divider-color, var(--background-modifier-accent));
|
||||
border-radius: var(--composer-box-radius, 0px) var(--composer-box-radius, 0px) 0 0;
|
||||
background-color: color-mix(in srgb, var(--background-textarea) 84%, var(--background-tertiary) 16%);
|
||||
}
|
||||
|
||||
.composerActionRow {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.composerActionRow + .composerActionRow {
|
||||
border-top: 0.0625rem solid color-mix(in srgb, var(--background-modifier-accent) 72%, transparent);
|
||||
}
|
||||
|
||||
.composerActionRow > * {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.barInner {
|
||||
@@ -150,39 +156,16 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.topBarContainer > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.topBarContainer > .box {
|
||||
width: fit-content;
|
||||
max-width: min(30rem, 100%);
|
||||
border-radius: var(--radius-full);
|
||||
background-color: color-mix(in srgb, var(--background-textarea) 84%, var(--background-tertiary) 16%);
|
||||
border: 0.0625rem solid color-mix(in srgb, var(--background-modifier-accent) 82%, transparent);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.topBarContainer .barInner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-height: var(--composer-status-pill-height);
|
||||
padding: 0 0.25rem 0 0.625rem;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.topBarContainer .barInner > :first-child {
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.separator {
|
||||
height: 0.0625rem;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.topBarContainer .separator {
|
||||
.composerActionRow .barInner {
|
||||
min-height: 2.25rem;
|
||||
padding-inline: 0.75rem 0.375rem;
|
||||
}
|
||||
|
||||
.composerActionRow .separator {
|
||||
display: none;
|
||||
}
|
||||
@@ -9,7 +9,10 @@
|
||||
height: var(--textarea-button-height);
|
||||
padding: 0;
|
||||
color: var(--text-primary-muted);
|
||||
transition: color var(--transition-normal);
|
||||
border-radius: var(--radius-md);
|
||||
transition:
|
||||
color var(--transition-normal),
|
||||
background-color var(--transition-normal);
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: none;
|
||||
@@ -20,6 +23,7 @@
|
||||
.button:hover:not(:disabled),
|
||||
.button.contextMenuHover:not(:disabled) {
|
||||
color: var(--text-primary);
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
|
||||
@@ -2,19 +2,17 @@
|
||||
|
||||
:root {
|
||||
--textarea-font-size: var(--font-size, 1rem);
|
||||
--textarea-button-height: var(--user-area-content-height);
|
||||
--textarea-button-icon-size: 1.625rem;
|
||||
--textarea-button-height: 2rem;
|
||||
--textarea-button-icon-size: 1.375rem;
|
||||
--textarea-button-padding-x: 0px;
|
||||
--textarea-button-min-width: auto;
|
||||
|
||||
--textarea-button-compact-height: var(--user-area-content-height);
|
||||
--textarea-button-compact-icon-size: 1.375rem;
|
||||
--textarea-button-compact-height: 2rem;
|
||||
--textarea-button-compact-icon-size: 1.25rem;
|
||||
|
||||
--textarea-container-padding-y: 0px;
|
||||
--textarea-container-padding-x: 0px;
|
||||
--textarea-min-height: var(--input-container-min-height);
|
||||
--textarea-horizontal-padding: var(--chat-horizontal-padding, var(--spacing-4));
|
||||
--textarea-content-offset: calc((var(--user-area-content-height) - var(--textarea-line-height)) / 2);
|
||||
--textarea-content-offset: max(0rem, calc((var(--textarea-button-height) - var(--textarea-line-height)) / 2));
|
||||
--textarea-upload-gap: var(--message-gutter, 1rem);
|
||||
--textarea-side-button-padding: max(
|
||||
0px,
|
||||
@@ -92,7 +90,7 @@
|
||||
}
|
||||
|
||||
.textarea::placeholder {
|
||||
color: var(--text-primary-muted);
|
||||
color: var(--text-tertiary-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -168,7 +166,7 @@
|
||||
width: 0.0625rem;
|
||||
height: 1.5rem;
|
||||
background-color: var(--background-modifier-hover);
|
||||
margin: 0 0.25rem;
|
||||
margin-inline: var(--composer-action-gap);
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
@@ -176,17 +174,12 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0 1 auto;
|
||||
min-height: 0;
|
||||
padding-left: var(--textarea-horizontal-padding);
|
||||
padding-right: var(--textarea-horizontal-padding);
|
||||
padding-inline: var(--composer-box-padding-inline);
|
||||
box-sizing: border-box;
|
||||
box-shadow: inset 0 0.0625rem 0 var(--user-area-divider-color);
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
contain: inline-size;
|
||||
overflow: visible;
|
||||
--composer-status-rail-inset: var(--textarea-horizontal-padding);
|
||||
}
|
||||
|
||||
.collapsibleSection {
|
||||
@@ -201,7 +194,12 @@
|
||||
|
||||
.textareaOuterMobile {
|
||||
padding: 0;
|
||||
--composer-status-rail-inset: 0px;
|
||||
min-height: 0;
|
||||
--textarea-min-height: var(--composer-mobile-box-height);
|
||||
--textarea-padding-y: var(--composer-mobile-padding-y);
|
||||
--composer-box-inset: 0px;
|
||||
--composer-box-inset-inline: 0px;
|
||||
--composer-box-padding-inline: 0px;
|
||||
}
|
||||
|
||||
.textareaMobile {
|
||||
@@ -211,21 +209,17 @@
|
||||
.mainWrapperDense {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, auto) minmax(0, 1fr) minmax(0, auto);
|
||||
align-items: flex-start;
|
||||
align-items: start;
|
||||
position: relative;
|
||||
min-height: var(--textarea-min-height);
|
||||
box-sizing: border-box;
|
||||
padding: var(--user-area-padding-y) 0;
|
||||
padding: var(--textarea-padding-y) 0;
|
||||
column-gap: var(--textarea-upload-gap);
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.textareaFocusRing {
|
||||
border-radius: var(--radius-xl);
|
||||
}
|
||||
|
||||
.mainWrapperEditing {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@@ -234,7 +228,7 @@
|
||||
background-color: var(--background-textarea);
|
||||
border: 0.0625rem solid var(--background-modifier-accent);
|
||||
border-radius: var(--radius-md);
|
||||
transition: colors;
|
||||
transition: border-color 0.15s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--scrollbar-track-bg: var(--background-textarea);
|
||||
}
|
||||
|
||||
@@ -253,7 +247,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: var(--user-area-content-height);
|
||||
min-height: var(--textarea-button-height);
|
||||
min-width: 0;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
@@ -271,7 +265,7 @@
|
||||
grid-column: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: var(--user-area-content-height);
|
||||
min-height: var(--textarea-button-height);
|
||||
min-width: 0;
|
||||
padding-top: var(--textarea-content-offset);
|
||||
}
|
||||
@@ -300,9 +294,9 @@
|
||||
.buttonContainerDense {
|
||||
grid-column: 3;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.625rem;
|
||||
min-height: var(--user-area-content-height);
|
||||
align-items: center;
|
||||
gap: var(--composer-action-gap);
|
||||
min-height: var(--textarea-button-height);
|
||||
min-width: 0;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
@@ -371,6 +365,26 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.textareaOuterMinHeight {
|
||||
min-height: var(--input-container-min-height);
|
||||
.textareaOuterRow {
|
||||
min-height: var(--textarea-min-height);
|
||||
margin-block: var(--composer-box-inset);
|
||||
margin-inline: var(--composer-box-inset-inline);
|
||||
background-color: var(--background-textarea);
|
||||
--composer-box-radius: var(--footer-box-radius);
|
||||
--composer-box-ring-color: var(--floating-surface-ring-color);
|
||||
--scrollbar-track-bg: var(--background-textarea);
|
||||
}
|
||||
|
||||
.textareaOuterRow::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
box-shadow: inset 0 0 0 0.0625rem var(--composer-box-ring-color);
|
||||
transition: box-shadow 0.15s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.textareaOuterRow:focus-within {
|
||||
--composer-box-ring-color: var(--floating-surface-ring-color-strong);
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {requestChannelComposerAffordanceDismissal} from '@app/features/channel/components/ChannelComposerDismissal';
|
||||
import type {Channel} from '@app/features/channel/models/Channel';
|
||||
import type {ComposerHandle} from '@app/features/lexical/composer/ComposerHandle';
|
||||
import * as MessageCommands from '@app/features/messaging/commands/MessageCommands';
|
||||
import type {Message} from '@app/features/messaging/models/MessagingMessage';
|
||||
import MessageEdit from '@app/features/messaging/state/MessageEdit';
|
||||
import MessageFocus from '@app/features/messaging/state/MessageFocus';
|
||||
import type {MessageReplyState} from '@app/features/messaging/state/MessageReply';
|
||||
import {ComponentDispatch} from '@app/features/platform/utils/ComponentBus';
|
||||
import {canFocusTextarea, safeFocus} from '@app/features/platform/utils/InputFocusManager';
|
||||
import {isTextInputKeyEvent} from '@app/features/platform/utils/IsTextInputKeyEvent';
|
||||
@@ -23,15 +20,6 @@ interface UseChannelComposerGlobalShortcutsParams {
|
||||
textareaInputDisabled: boolean;
|
||||
isFocused: boolean;
|
||||
handleArrowUpEmpty: () => void;
|
||||
editingMessage: Message | null | undefined;
|
||||
replyingMessage: MessageReplyState | null;
|
||||
mobileLayout: ComposerMobileLayout;
|
||||
setValue: React.Dispatch<React.SetStateAction<string>>;
|
||||
clearSegments: () => void;
|
||||
}
|
||||
|
||||
interface ComposerMobileLayout {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export function useChannelComposerGlobalShortcuts({
|
||||
@@ -41,11 +29,6 @@ export function useChannelComposerGlobalShortcuts({
|
||||
textareaInputDisabled,
|
||||
isFocused,
|
||||
handleArrowUpEmpty,
|
||||
editingMessage,
|
||||
replyingMessage,
|
||||
mobileLayout,
|
||||
setValue,
|
||||
clearSegments,
|
||||
}: UseChannelComposerGlobalShortcutsParams): void {
|
||||
useEffect(() => {
|
||||
if (textareaInputDisabled) {
|
||||
@@ -108,31 +91,19 @@ export function useChannelComposerGlobalShortcuts({
|
||||
return;
|
||||
}
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key !== 'Escape') return;
|
||||
if (event.shiftKey) return;
|
||||
ComponentDispatch.dispatch('ESCAPE_PRESSED', {channelId: channel.id});
|
||||
const isEditingInline = MessageEdit.getEditingMessageId(channel.id) != null;
|
||||
if (isEditingInline) {
|
||||
if (event.defaultPrevented || event.isComposing) return;
|
||||
if (event.key !== 'Escape' || event.shiftKey) return;
|
||||
if (requestChannelComposerAffordanceDismissal(channel.id)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
MessageCommands.stopEdit(channel.id);
|
||||
return;
|
||||
}
|
||||
if (editingMessage && mobileLayout.enabled) {
|
||||
event.preventDefault();
|
||||
MessageCommands.stopEditMobile(channel.id);
|
||||
setValue('');
|
||||
clearSegments();
|
||||
} else if (replyingMessage) {
|
||||
event.preventDefault();
|
||||
MessageCommands.stopReply(channel.id);
|
||||
} else {
|
||||
event.preventDefault();
|
||||
}
|
||||
ComponentDispatch.dispatch('ESCAPE_PRESSED', {channelId: channel.id});
|
||||
event.preventDefault();
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [channel.id, editingMessage, replyingMessage, mobileLayout.enabled, textareaInputDisabled, clearSegments]);
|
||||
}, [channel.id, textareaInputDisabled]);
|
||||
}
|
||||
Reference in new issue
Block a user