diff --git a/fluxer_app/src/features/channel/components/LexicalChannelTextareaContent.tsx b/fluxer_app/src/features/channel/components/LexicalChannelTextareaContent.tsx index a7e165575..8ddf7ea2b 100644 --- a/fluxer_app/src/features/channel/components/LexicalChannelTextareaContent.tsx +++ b/fluxer_app/src/features/channel/components/LexicalChannelTextareaContent.tsx @@ -1375,7 +1375,10 @@ export const LexicalChannelTextareaContent = observer( showStickersButton={showStickersButton} showEmojiButton={showEmojiButton} showMessageSendButton={showMessageSendButton} - showVoiceMessageButton={false} + canRecordVoice={canAttachFilesInChannel(channel)} + isEditingMessage={isEditingMessageInComposer || editingMessageId != null} + hasPendingSticker={hasPendingSticker} + voiceTooltipAnchorRef={contentAreaRef} expressionPickerOpen={expressionPickerOpen} selectedTab={selectedTab} isMobile={mobileLayout.enabled} diff --git a/fluxer_app/src/features/channel/components/VoiceMessageRecorder.module.css b/fluxer_app/src/features/channel/components/VoiceMessageRecorder.module.css index f2b610dcf..cec209143 100644 --- a/fluxer_app/src/features/channel/components/VoiceMessageRecorder.module.css +++ b/fluxer_app/src/features/channel/components/VoiceMessageRecorder.module.css @@ -7,6 +7,34 @@ justify-content: center; } +.voiceButton { + display: flex; + align-items: center; + justify-content: center; + min-width: var(--textarea-button-height); + height: var(--textarea-button-height); + padding: 0; + color: var(--text-primary-muted); + border-radius: var(--radius-md); + background: transparent; + border: none; + outline: none; + cursor: pointer; + flex-shrink: 0; + touch-action: none; +} + +.voiceButton:disabled { + cursor: not-allowed; + opacity: 0.7; +} + +.voiceButtonIcon { + width: var(--textarea-button-icon-size); + height: var(--textarea-button-icon-size); + flex-shrink: 0; +} + .holdTooltip { background-color: var(--brand-primary); color: var(--text-on-brand-primary); @@ -33,6 +61,10 @@ } .overlayInner { + display: flex; + align-items: flex-end; + gap: 0.375rem; + padding: 0.5rem; width: 100%; min-width: 0; box-sizing: border-box; @@ -133,6 +165,7 @@ flex-shrink: 0; display: flex; align-items: center; + width: 2.25rem; height: 2.25rem; } diff --git a/fluxer_app/src/features/channel/components/VoiceMessageRecorder.tsx b/fluxer_app/src/features/channel/components/VoiceMessageRecorder.tsx index be3f606e0..c671b5e49 100644 --- a/fluxer_app/src/features/channel/components/VoiceMessageRecorder.tsx +++ b/fluxer_app/src/features/channel/components/VoiceMessageRecorder.tsx @@ -2,7 +2,6 @@ import Accessibility from '@app/features/accessibility/state/Accessibility'; import {LimitResolver} from '@app/features/app/utils/LimitResolverAdapter'; -import layoutStyles from '@app/features/channel/components/textarea/MobileTextareaLayout.module.css'; import styles from '@app/features/channel/components/VoiceMessageRecorder.module.css'; import {Logger} from '@app/features/platform/utils/AppLogger'; import {usePortalHost} from '@app/features/ui/overlay/PortalHostContext'; @@ -529,7 +528,7 @@ export default function VoiceMessageRecorder({channelId, disabled, tooltipAnchor > @@ -578,10 +577,7 @@ export default function VoiceMessageRecorder({channelId, disabled, tooltipAnchor transition={resolvedOverlayMotion.transition} data-flx="channel.voice-message-recorder.recording-overlay" > -
+
-
- )} -
-
-
- -
- -
-
-
- {!disabled && ( -
- -
- )} -
-
- - {shouldShowVoiceButton ? ( - - - - ) : ( - - - - )} - -
-
-
- ); -} - -MobileTextareaLayout.displayName = 'MobileTextareaLayout'; diff --git a/fluxer_app/src/features/channel/components/textarea/TextareaButtons.module.css b/fluxer_app/src/features/channel/components/textarea/TextareaButtons.module.css index b2e252b1f..bf8d301df 100644 --- a/fluxer_app/src/features/channel/components/textarea/TextareaButtons.module.css +++ b/fluxer_app/src/features/channel/components/textarea/TextareaButtons.module.css @@ -14,33 +14,17 @@ align-items: stretch; } -.holdButtonWrapper { +.mobileRightButtonContainer { + position: relative; + flex-shrink: 0; + width: var(--textarea-button-height); + height: var(--textarea-button-height); +} + +.mobileRightButtonSlot { + position: absolute; + inset: 0; display: flex; - flex-direction: column; align-items: center; - gap: 0.25rem; - touch-action: none; -} - -.holdButton { - touch-action: none; -} - -.holdButtonRecording { - box-shadow: 0 0 0 0.125rem rgba(252, 95, 105, 0.35); -} - -.holdIndicator { - font-size: 0.625rem; - text-transform: uppercase; - color: var(--text-primary-muted); -} - -.holdIndicatorRecording { - color: var(--accent-danger); -} - -.holdIndicatorError { - color: var(--accent-danger); - font-weight: 600; + justify-content: center; } diff --git a/fluxer_app/src/features/channel/components/textarea/TextareaButtons.tsx b/fluxer_app/src/features/channel/components/textarea/TextareaButtons.tsx index 35bba4b40..4969fa41f 100644 --- a/fluxer_app/src/features/channel/components/textarea/TextareaButtons.tsx +++ b/fluxer_app/src/features/channel/components/textarea/TextareaButtons.tsx @@ -1,10 +1,11 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import Accessibility from '@app/features/accessibility/state/Accessibility'; -import HoldToRecordButton from '@app/features/channel/components/textarea/HoldToRecordButton'; +import {transitionMobileTextareaButtonState} from '@app/features/channel/components/textarea/MobileTextareaButtonStateMachine'; import {TextareaButton} from '@app/features/channel/components/textarea/TextareaButton'; import textareaButtonsStyles from '@app/features/channel/components/textarea/TextareaButtons.module.css'; import styles from '@app/features/channel/components/textarea/TextareaInput.module.css'; +import VoiceMessageRecorder from '@app/features/channel/components/VoiceMessageRecorder'; import type {ExpressionPickerTabType} from '@app/features/expressions/components/popouts/ExpressionPickerPopout'; import { EMOJIS_DESCRIPTOR, @@ -12,29 +13,28 @@ import { MEDIA_DESCRIPTOR, STICKERS_DESCRIPTOR, } from '@app/features/i18n/utils/CommonMessageDescriptors'; -import {getReducedMotionProps} from '@app/features/ui/utils/ReducedMotionAnimation'; +import {getReducedMotionProps, type MotionAnimation} from '@app/features/ui/utils/ReducedMotionAnimation'; import {msg} from '@lingui/core/macro'; import {useLingui} from '@lingui/react/macro'; -import { - GifIcon, - ImageSquareIcon, - MicrophoneIcon, - PaperPlaneRightIcon, - SmileyIcon, - StickerIcon, -} from '@phosphor-icons/react'; +import {GifIcon, ImageSquareIcon, PaperPlaneRightIcon, SmileyIcon, StickerIcon} from '@phosphor-icons/react'; import {clsx} from 'clsx'; import {AnimatePresence, motion} from 'framer-motion'; -import React from 'react'; +import React, {useMemo} from 'react'; -const VOICE_MESSAGE_DESCRIPTOR = msg({ - message: 'Voice message', - comment: 'Short label in the channel and chat textarea buttons. Keep it concise.', -}); const SEND_MESSAGE_DESCRIPTOR = msg({ message: 'Send message', comment: 'Button or menu action label in the channel and chat textarea buttons. Keep it concise.', }); +const MOBILE_BUTTON_SWAP_MOTION: MotionAnimation = { + initial: {opacity: 0, scale: 0.86, y: 3}, + animate: {opacity: 1, scale: 1, y: 0}, + exit: {opacity: 0, scale: 0.9, y: -2}, + transition: { + opacity: {duration: 0.08}, + y: {duration: 0.08, ease: 'easeOut'}, + scale: {type: 'spring', stiffness: 560, damping: 28, mass: 0.55}, + }, +}; interface TextareaButtonsProps { disabled: boolean; @@ -44,8 +44,10 @@ interface TextareaButtonsProps { showStickersButton: boolean; showEmojiButton: boolean; showMessageSendButton: boolean; - showVoiceMessageButton?: boolean; - onVoiceMessageClick?: () => void; + canRecordVoice: boolean; + isEditingMessage: boolean; + hasPendingSticker: boolean; + voiceTooltipAnchorRef?: React.RefObject; channelId: string; expressionPickerOpen: boolean; selectedTab: ExpressionPickerTabType; @@ -71,8 +73,10 @@ export const TextareaButtons = React.forwardRef { const {i18n} = useLingui(); + const buttonModel = useMemo( + () => + transitionMobileTextareaButtonState({ + disabled, + canRecordVoice, + value: '', + isSlowmodeActive, + isOverCharacterLimit: isOverLimit, + isEditingMessage, + hasContent, + hasAttachments, + hasPendingSticker, + isEditingScheduledMessage: false, + }), + [ + canRecordVoice, + disabled, + hasAttachments, + hasContent, + hasPendingSticker, + isEditingMessage, + isOverLimit, + isSlowmodeActive, + ], + ); if (disabled) { return null; } - const buttonSwapMotion = getReducedMotionProps( - { - initial: {scale: 0.8, opacity: 0}, - animate: {scale: 1, opacity: 1}, - exit: {scale: 0.8, opacity: 0}, - transition: {duration: 0.15, ease: 'easeOut'}, - }, - Accessibility.useReducedMotion, - ); + const buttonSwapMotion = getReducedMotionProps(MOBILE_BUTTON_SWAP_MOTION, Accessibility.useReducedMotion); const shouldShowDesktopSendButton = showMessageSendButton; - const baseSendDisabled = isSlowmodeActive || isOverLimit || disableSendButton; - const sendButtonDisabled = baseSendDisabled || (!hasContent && !hasAttachments); - const shouldShowHoldToRecord = isMobile && showVoiceMessageButton && !hasContent && !hasAttachments; + const desktopSendDisabled = + isSlowmodeActive || isOverLimit || (!hasContent && !hasAttachments) || disableSendButton; + const shouldShowVoiceButton = buttonModel.visibleButton === 'voice'; + const mobileSendDisabled = buttonModel.sendButton.disabled || Boolean(disableSendButton); return (
- {isMobile && showVoiceMessageButton && onVoiceMessageClick && !shouldShowHoldToRecord && ( - - )} {isMobile && ( - - {shouldShowHoldToRecord ? ( - - - - ) : ( - - - - )} - +
+ + {shouldShowVoiceButton ? ( + + + + ) : ( + + + + )} + +
)} {!isMobile && shouldShowDesktopSendButton && ( <>