fix(app): backport rendering improvements (#1580)

This commit is contained in:
Hampus authored and GitHub committed 2026-08-13 19:52:00 +02:00
1 parent 0c291a01da
commit 42cdc1f877
513 files changed
+98743 -70397

No files matched your search

@@ -0,0 +1,34 @@
/* SPDX-License-Identifier: AGPL-3.0-or-later */
.info {
display: flex;
flex: 1 1 auto;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 0.35rem;
overflow: hidden;
}
.name {
font-size: 0.95rem;
color: var(--text-chat);
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 0 1 auto;
min-width: 0;
max-width: 100%;
margin-top: -0.125rem;
}
.tag {
font-size: 0.75rem;
color: var(--text-chat-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 0 1 auto;
min-width: 0;
}
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {Avatar} from '@app/features/ui/components/Avatar';
import styles from '@app/features/user/components/UserIdentityRow.module.css';
import type {User} from '@app/features/user/models/User';
import * as DisplayNameUtils from '@app/features/user/utils/DisplayNameUtils';
import {flxElementClassName} from '@app/lib/react';
import {observer} from 'mobx-react-lite';
interface UserIdentityRowProps {
user: User;
guildId?: string | null;
channelId?: string;
avatarSize?: number;
className?: string;
}
export const UserIdentityRow = observer(
({user, guildId, channelId, avatarSize = 40, className}: UserIdentityRowProps) => {
const displayName = DisplayNameUtils.getNickname(user, guildId, channelId);
return (
<>
<Avatar user={user} size={avatarSize} guildId={guildId === null ? undefined : guildId} className={className} />
<flx-user-identity-row-info className={flxElementClassName(styles.info)}>
<span className={styles.name}>{displayName}</span>
<span className={styles.tag}>{user.tag}</span>
</flx-user-identity-row-info>
</>
);
},
);
@@ -4,6 +4,7 @@ import * as Modal from '@app/features/app/components/dialogs/Modal';
import {useCursorAtEnd} from '@app/features/app/hooks/useCursorAtEnd';
import {useFormSubmit} from '@app/features/app/hooks/useFormSubmit';
import {CHANGE_NICKNAME_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {Button} from '@app/features/ui/button/Button';
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
import * as ToastCommands from '@app/features/ui/commands/ToastCommands';
@@ -122,7 +123,7 @@ export const BaseChangeNicknameModal: React.FC<BaseChangeNicknameModalProps> = o
aria-label={i18n._(CLEAR_NICKNAME_DESCRIPTOR)}
data-flx="user.base-change-nickname-modal.clear-button.set-value"
>
<XIcon size={16} weight="bold" data-flx="user.base-change-nickname-modal.x-icon" />
<XIcon size={remFromPx(16)} weight="bold" data-flx="user.base-change-nickname-modal.x-icon" />
</button>
</FocusRing>
) : undefined
@@ -10,6 +10,7 @@ import {Channel} from '@app/features/channel/models/Channel';
import Channels from '@app/features/channel/state/Channels';
import {SCREEN_READER_DESCRIPTOR, TEXT_TO_SPEECH_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {Message as MessageModel} from '@app/features/messaging/models/MessagingMessage';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {Button} from '@app/features/ui/button/Button';
import type {ComboboxOption} from '@app/features/ui/components/form/FormCombobox';
import {Switch} from '@app/features/ui/components/form/FormSwitch';
@@ -320,13 +321,13 @@ export const AccessibilityTtsTabContent: React.FC = observer(() => {
leftIcon={
isSpeaking ? (
<PauseIcon
size={16}
size={remFromPx(16)}
weight="fill"
data-flx="user.accessibility-tab.accessibility-tts-tab-content.pause-icon"
/>
) : (
<PlayIcon
size={16}
size={remFromPx(16)}
weight="fill"
data-flx="user.accessibility-tab.accessibility-tts-tab-content.play-icon"
/>
@@ -10,6 +10,7 @@ import {
} from '@app/features/i18n/utils/CommonMessageDescriptors';
import Relationships from '@app/features/relationship/state/Relationships';
import * as RelationshipActionUtils from '@app/features/relationship/utils/RelationshipActionUtils';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {MenuGroup} from '@app/features/ui/action_menu/MenuGroup';
import {MenuItem} from '@app/features/ui/action_menu/MenuItem';
import {Button} from '@app/features/ui/button/Button';
@@ -71,7 +72,9 @@ export const BlockedUsersContent: React.FC = observer(() => {
<>
<MenuGroup data-flx="user.blocked-users-tab.handle-more-options-click.menu-group">
<MenuItem
icon={<UserIcon size={16} data-flx="user.blocked-users-tab.handle-more-options-click.user-icon" />}
icon={
<UserIcon size={remFromPx(16)} data-flx="user.blocked-users-tab.handle-more-options-click.user-icon" />
}
onClick={() => {
onClose();
handleViewProfile(userId);
@@ -83,7 +86,9 @@ export const BlockedUsersContent: React.FC = observer(() => {
</MenuGroup>
<MenuGroup data-flx="user.blocked-users-tab.handle-more-options-click.menu-group--2">
<MenuItem
icon={<CopyIcon size={16} data-flx="user.blocked-users-tab.handle-more-options-click.copy-icon" />}
icon={
<CopyIcon size={remFromPx(16)} data-flx="user.blocked-users-tab.handle-more-options-click.copy-icon" />
}
onClick={() => {
onClose();
TextCopyCommands.copy(i18n, user.tag, true);
@@ -95,7 +100,7 @@ export const BlockedUsersContent: React.FC = observer(() => {
<MenuItem
icon={
<IdentificationCardIcon
size={16}
size={remFromPx(16)}
data-flx="user.blocked-users-tab.handle-more-options-click.identification-card-icon"
/>
}
@@ -19,6 +19,7 @@ import {
} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {Logger} from '@app/features/platform/utils/AppLogger';
import {ComponentDispatch} from '@app/features/platform/utils/ComponentBus';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {Button} from '@app/features/ui/button/Button';
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
import {modal} from '@app/features/ui/commands/ModalCommands';
@@ -226,9 +227,13 @@ const GiftCard: React.FC<GiftCardProps> = observer(({gift, isExpanded, onToggle,
onClick={handleCopy}
leftIcon={
copied ? (
<CheckIcon size={16} weight="bold" data-flx="user.gift-inventory-tab.gift-card.check-icon" />
<CheckIcon
size={remFromPx(16)}
weight="bold"
data-flx="user.gift-inventory-tab.gift-card.check-icon"
/>
) : (
<CopyIcon size={16} data-flx="user.gift-inventory-tab.gift-card.copy-icon" />
<CopyIcon size={remFromPx(16)} data-flx="user.gift-inventory-tab.gift-card.copy-icon" />
)
}
data-flx="user.gift-inventory-tab.gift-card.button.copy"
@@ -267,7 +272,7 @@ const GiftCard: React.FC<GiftCardProps> = observer(({gift, isExpanded, onToggle,
onClick={handleShare}
leftIcon={
<ShareNetworkIcon
size={16}
size={remFromPx(16)}
weight="bold"
data-flx="user.gift-inventory-tab.gift-card.share-network-icon"
/>
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {Input} from '@app/features/ui/components/form/FormInput';
import {LinuxInputAccessSection} from '@app/features/user/components/modals/tabs/components/LinuxInputAccessSection';
import styles from '@app/features/user/components/modals/tabs/KeybindsTab.module.css';
@@ -34,7 +35,13 @@ const KeybindsTab: React.FC = observer(() => {
<div className={styles.searchRow} data-flx="user.keybinds-tab.search-row">
<Input
placeholder={i18n._(SEARCH_SHORTCUTS_DESCRIPTOR)}
leftIcon={<MagnifyingGlassIcon size={16} weight="bold" data-flx="user.keybinds-tab.magnifying-glass-icon" />}
leftIcon={
<MagnifyingGlassIcon
size={remFromPx(16)}
weight="bold"
data-flx="user.keybinds-tab.magnifying-glass-icon"
/>
}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
aria-label={i18n._(SEARCH_SHORTCUTS_2_DESCRIPTOR)}
@@ -18,12 +18,14 @@ import UserConnection from '@app/features/connection/state/UserConnection';
import {CONNECTIONS_DESCRIPTOR, DOMAIN_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {StreamerModeGate} from '@app/features/streamer_mode/components/StreamerModeGate';
import StreamerMode from '@app/features/streamer_mode/state/StreamerMode';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
import {modal} from '@app/features/ui/commands/ModalCommands';
import {BlueskyIcon} from '@app/features/ui/components/icons/BlueskyIcon';
import {UnverifiedConnectionIcon} from '@app/features/ui/components/icons/UnverifiedConnectionIcon';
import {VerifiedConnectionIcon} from '@app/features/ui/components/icons/VerifiedConnectionIcon';
import {Spinner} from '@app/features/ui/components/Spinner';
import {useDragAutoScroll} from '@app/features/ui/hooks/useDragAutoScroll';
import {Tooltip} from '@app/features/ui/tooltip/Tooltip';
import styles from '@app/features/user/components/modals/tabs/LinkedAccountsTab.module.css';
import {type ConnectionType, ConnectionTypes} from '@fluxer/constants/src/ConnectionConstants';
@@ -35,7 +37,7 @@ import {observer} from 'mobx-react-lite';
import type React from 'react';
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import type {ConnectableElement} from 'react-dnd';
import {DndProvider, useDrag, useDrop} from 'react-dnd';
import {DndProvider, useDrag, useDragLayer, useDrop} from 'react-dnd';
import {getEmptyImage, HTML5Backend} from 'react-dnd-html5-backend';
const THIS_CONNECTION_HAS_BEEN_VERIFIED_DESCRIPTOR = msg({
@@ -81,6 +83,19 @@ interface ConnectionCardProps {
onDropConnection: () => void;
}
const ConnectionDragAutoScroll: React.FC<{listRef: React.RefObject<HTMLDivElement | null>}> = ({listRef}) => {
const isDraggingConnection = useDragLayer(
(monitor) => monitor.isDragging() && monitor.getItemType() === DND_TYPES.CONNECTION,
);
const getScrollElement = useCallback(() => {
const list = listRef.current;
if (list == null) return null;
return list.closest<HTMLElement>('[data-fluxer-scroll-container="true"]');
}, [listRef]);
useDragAutoScroll({active: isDraggingConnection, getScrollElement});
return null;
};
const ConnectionCard: React.FC<ConnectionCardProps> = observer(
({connection, index, onDelete, onEdit, onMoveConnection, onDropConnection}) => {
const {i18n} = useLingui();
@@ -160,7 +175,7 @@ const ConnectionCard: React.FC<ConnectionCardProps> = observer(
<BlueskyIcon size={20} data-flx="user.linked-accounts-tab.connection-card.bluesky-icon" />
) : (
<GlobeSimpleIcon
size={20}
size={remFromPx(20)}
className={styles.domainIcon}
data-flx="user.linked-accounts-tab.connection-card.domain-icon"
/>
@@ -182,7 +197,7 @@ const ConnectionCard: React.FC<ConnectionCardProps> = observer(
data-flx="user.linked-accounts-tab.connection-card.card-drag-handle"
>
<DotsSixVerticalIcon
size={20}
size={remFromPx(20)}
weight="bold"
data-flx="user.linked-accounts-tab.connection-card.dots-six-vertical-icon"
/>
@@ -239,7 +254,10 @@ const ConnectionCard: React.FC<ConnectionCardProps> = observer(
aria-label={i18n._(EDIT_DESCRIPTOR)}
data-flx="user.linked-accounts-tab.connection-card.action-button.edit"
>
<PencilSimpleIcon size={16} data-flx="user.linked-accounts-tab.connection-card.pencil-simple-icon" />
<PencilSimpleIcon
size={remFromPx(16)}
data-flx="user.linked-accounts-tab.connection-card.pencil-simple-icon"
/>
</button>
</Tooltip>
<Tooltip text={i18n._(REMOVE_DESCRIPTOR)} data-flx="user.linked-accounts-tab.connection-card.tooltip--4">
@@ -250,7 +268,7 @@ const ConnectionCard: React.FC<ConnectionCardProps> = observer(
aria-label={i18n._(REMOVE_DESCRIPTOR)}
data-flx="user.linked-accounts-tab.connection-card.action-button.delete"
>
<TrashIcon size={16} data-flx="user.linked-accounts-tab.connection-card.trash-icon" />
<TrashIcon size={remFromPx(16)} data-flx="user.linked-accounts-tab.connection-card.trash-icon" />
</button>
</Tooltip>
</div>
@@ -262,6 +280,7 @@ const LinkedAccountsTab: React.FC = observer(() => {
const {i18n} = useLingui();
const [loaded, setLoaded] = useState(false);
const [localOrder, setLocalOrder] = useState<ReadonlyArray<Connection> | null>(null);
const connectionsListRef = useRef<HTMLDivElement>(null);
const shouldGatePersonalDetails = StreamerMode.shouldHidePersonalInformation;
useEffect(() => {
if (shouldGatePersonalDetails) return;
@@ -389,7 +408,7 @@ const LinkedAccountsTab: React.FC = observer(() => {
data-flx="user.linked-accounts-tab.platform-icon-button.add-connection--2"
>
<GlobeSimpleIcon
size={28}
size={remFromPx(28)}
className={styles.domainIcon}
data-flx="user.linked-accounts-tab.domain-icon"
/>
@@ -412,7 +431,12 @@ const LinkedAccountsTab: React.FC = observer(() => {
</div>
) : (
<DndProvider backend={HTML5Backend} data-flx="user.linked-accounts-tab.dnd-provider">
<div className={styles.connectionsList} data-flx="user.linked-accounts-tab.connections-list">
<ConnectionDragAutoScroll listRef={connectionsListRef} />
<div
ref={connectionsListRef}
className={styles.connectionsList}
data-flx="user.linked-accounts-tab.connections-list"
>
{connections.map((connection, index) => (
<ConnectionCard
key={connection.id}
@@ -12,13 +12,11 @@ import DeveloperOptions from '@app/features/devtools/state/DeveloperOptions';
import type {FlatEmoji} from '@app/features/emoji/types/EmojiTypes';
import {ExpressionPickerSheet} from '@app/features/expressions/components/modals/ExpressionPickerSheet';
import Guilds from '@app/features/guild/state/Guilds';
import type {LexicalRichInputHandle} from '@app/features/lexical/composer/LexicalRichInput';
import * as GuildMemberCommands from '@app/features/member/commands/GuildMemberCommands';
import GuildMembers from '@app/features/member/state/GuildMembers';
import {useTextareaAutocomplete} from '@app/features/messaging/hooks/useTextareaAutocomplete';
import {useTextareaEmojiPicker} from '@app/features/messaging/hooks/useTextareaEmojiPicker';
import {useTextareaPaste} from '@app/features/messaging/hooks/useTextareaPaste';
import {useTextareaSegments} from '@app/features/messaging/hooks/useTextareaSegments';
import {applyMarkdownSegments, convertMarkdownToSegments} from '@app/features/messaging/utils/MarkdownToSegmentUtils';
import {convertMarkdownToSegments} from '@app/features/messaging/utils/MarkdownToSegmentUtils';
import type {MentionSegment} from '@app/features/messaging/utils/TextareaSegmentManager';
import Permission from '@app/features/permissions/state/Permission';
import {Logger} from '@app/features/platform/utils/AppLogger';
import {shouldShowPremiumFeatures} from '@app/features/premium/utils/PremiumUtils';
@@ -57,7 +55,6 @@ import {ProfilePreview} from '@app/features/user/components/profile/ProfilePrevi
import type {Profile} from '@app/features/user/models/Profile';
import Users from '@app/features/user/state/Users';
import * as NicknameUtils from '@app/features/user/utils/NicknameUtils';
import {showUserErrorModal} from '@app/features/user/utils/UserErrorModalUtils';
import {setMeaningfulFormValue} from '@app/lib/forms/MeaningfulFormValue';
import {type RemoteFormResetReason, useRemoteFormReset} from '@app/lib/forms/RemoteFormReset';
import {Permissions} from '@fluxer/constants/src/ChannelConstants';
@@ -72,14 +69,6 @@ import {observer} from 'mobx-react-lite';
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {useForm} from 'react-hook-form';
const YOUR_PROFILE_BIO_IS_TOO_LONG_DESCRIPTOR = msg({
message: 'Your profile bio is too long.',
comment: 'Description text in the my profile tab.',
});
const SHORTEN_PROFILE_BIO_AND_TRY_AGAIN_DESCRIPTOR = msg({
message: 'Shorten your bio and try again.',
comment: 'Body of the error modal shown when the profile bio exceeds the maximum length.',
});
const COMMUNITY_PROFILE_UPDATED_DESCRIPTOR = msg({
message: 'Community profile updated',
comment: 'Short label in the my profile tab. Keep it concise.',
@@ -214,7 +203,6 @@ interface ProfileRemoteValues {
}
const MY_PROFILE_TAB_ID = 'my_profile';
const AUTOCOMPLETE_Z_INDEX = 10001;
const MyProfileTabComponent = observer(function MyProfileTabComponent({
initialGuildId,
}: {
@@ -245,66 +233,20 @@ const MyProfileTabComponent = observer(function MyProfileTabComponent({
}, []);
const avatarAsset = selectProfileAssetCustomizationState(avatarAssetSnapshot);
const bannerAsset = selectProfileAssetCustomizationState(bannerAssetSnapshot);
const bioTextareaRef = useRef<HTMLTextAreaElement | null>(null);
const bioComposerRef = useRef<LexicalRichInputHandle | null>(null);
const isPerGuildProfile = selectedGuildId !== null;
const profileIdentityKey = user?.id ? `${user.id}:${selectedGuildId ?? 'global'}` : null;
const canChangeNickname = selectedGuildId
? Permission.can(Permissions.CHANGE_NICKNAME, {guildId: selectedGuildId})
: false;
const {segmentManagerRef, previousValueRef, displayToActual, prepareTextChange, handleTextChange} =
useTextareaSegments();
const [bioValue, setBioValue] = useState('');
const [bioActualValue, setBioActualValue] = useState('');
const [bioSegments, setBioSegments] = useState<Array<MentionSegment>>([]);
const [bioHydrationKey, setBioHydrationKey] = useState(0);
const [isBioInitialized, setIsBioInitialized] = useState(false);
const originalBioRef = useRef('');
const originalBioFormValueRef = useRef<string | null>(null);
const handleBioExceedsLimit = useCallback(() => {
showUserErrorModal(
i18n._(YOUR_PROFILE_BIO_IS_TOO_LONG_DESCRIPTOR),
i18n._(SHORTEN_PROFILE_BIO_AND_TRY_AGAIN_DESCRIPTOR),
);
}, [i18n]);
const {handleEmojiSelect} = useTextareaEmojiPicker({
setValue: setBioValue,
textareaRef: bioTextareaRef,
segmentManagerRef,
previousValueRef,
prepareTextChange,
maxActualLength: user?.maxBioLength,
onExceedMaxLength: handleBioExceedsLimit,
});
const {
autocompleteQuery,
autocompleteOptions,
autocompleteType,
selectedIndex,
isAutocompleteAttached,
setSelectedIndex,
onCursorMove,
handleSelect,
} = useTextareaAutocomplete({
channel: null,
value: bioValue,
setValue: setBioValue,
textareaRef: bioTextareaRef,
segmentManagerRef,
previousValueRef,
prepareTextChange,
allowedTriggers: ['emoji'],
maxActualLength: user?.maxBioLength,
onExceedMaxLength: handleBioExceedsLimit,
});
useTextareaPaste({
channel: null,
textareaRef: bioTextareaRef,
segmentManagerRef,
setValue: setBioValue,
previousValueRef,
prepareTextChange,
maxMessageLength: user?.maxBioLength,
onPasteExceedsLimit: () => handleBioExceedsLimit(),
});
const [bioExpressionPickerOpen, setBioExpressionPickerOpen] = useState(false);
const bioContainerRef = useRef<HTMLDivElement | null>(null);
const flashTrigger = unsavedChangesState.flashTriggers[MY_PROFILE_TAB_ID] || 0;
const [lastFlashTrigger, setLastFlashTrigger] = useState(0);
const [ariaAnnouncement, setAriaAnnouncement] = useState('');
@@ -327,15 +269,22 @@ const MyProfileTabComponent = observer(function MyProfileTabComponent({
});
const updateBioFromMarkdown = useCallback(
(markdownBio: string) => {
segmentManagerRef.current.clear();
const displayBio = markdownBio
? applyMarkdownSegments(markdownBio, selectedGuildId, segmentManagerRef.current)
: '';
const converted = convertMarkdownToSegments(markdownBio, selectedGuildId);
const segments = converted.segments.map((segment) => ({
type: segment.type,
id: segment.id,
displayText: segment.displayText,
actualText: segment.actualText,
start: segment.start,
end: segment.start + segment.displayText.length,
}));
originalBioRef.current = markdownBio;
setBioValue(displayBio);
previousValueRef.current = displayBio;
setBioValue(converted.displayText);
setBioActualValue(markdownBio);
setBioSegments(segments);
setBioHydrationKey((key) => key + 1);
},
[selectedGuildId, segmentManagerRef, previousValueRef],
[selectedGuildId],
);
useEffect(() => {
if (!user?.id) return;
@@ -470,9 +419,8 @@ const MyProfileTabComponent = observer(function MyProfileTabComponent({
),
[],
);
const actualBio = useMemo(() => displayToActual(bioValue), [bioValue, displayToActual]);
const actualBio = bioActualValue;
const maxBioActualLength = user?.maxBioLength ?? 0;
const bioDisplayMaxLength = Math.max(0, bioValue.length + (maxBioActualLength - actualBio.length));
useEffect(() => {
if (!isBioInitialized) {
return;
@@ -486,16 +434,22 @@ const MyProfileTabComponent = observer(function MyProfileTabComponent({
isMeaningfullyDirty: isDirty,
});
}, [actualBio, form, isBioInitialized]);
const handleBioEmojiSelect = useCallback(
(emoji: FlatEmoji, shiftKey?: boolean) => {
const didInsert = handleEmojiSelect(emoji, shiftKey);
if (didInsert && !shiftKey) {
setBioExpressionPickerOpen(false);
}
return didInsert;
},
[handleEmojiSelect],
);
const handleBioEmojiSelect = useCallback((emoji: FlatEmoji, shiftKey?: boolean) => {
const composer = bioComposerRef.current;
if (composer == null) {
return false;
}
const didInsert = composer.insertEmoji(emoji);
if (didInsert && shiftKey !== true) {
setBioExpressionPickerOpen(false);
}
return didInsert;
}, []);
const handleBioChange = useCallback((display: string, segments: Array<MentionSegment>, wire: string) => {
setBioValue(display);
setBioSegments(segments);
setBioActualValue(wire);
}, []);
const onSubmit = useCallback(
async (data: FormInputs) => {
if (isProfileCustomizationLocked) {
@@ -940,37 +894,28 @@ const MyProfileTabComponent = observer(function MyProfileTabComponent({
data-flx="user.my-profile-tab.my-profile-tab-component.opacity-half--2"
>
<BioEditor
value={bioValue}
onChange={(newValue: string, inputType?: string) => {
handleTextChange(newValue, previousValueRef.current, inputType);
setBioValue(newValue);
}}
initialValue={bioValue}
initialSegments={bioSegments}
hydrationKey={bioHydrationKey}
onChange={handleBioChange}
onEmojiSelect={handleBioEmojiSelect}
placeholder={
isPerGuildProfile && user?.bio
? convertMarkdownToSegments(user.bio, selectedGuildId).displayText
: i18n._(DOC_I_M_FROM_THE_FUTURE_I_CAME_DESCRIPTOR)
}
displayMaxLength={bioDisplayMaxLength}
actualLength={actualBio.length}
actualMaxLength={maxBioActualLength}
disabled={isProfileCustomizationLocked || isPerGuildProfileCustomizationDisabled}
isMobile={mobileLayout.enabled}
errorMessage={form.formState.errors.bio?.message}
textareaRef={bioTextareaRef}
errorMessage={
form.formState.errors.bio != null && form.formState.errors.bio.message != null
? form.formState.errors.bio.message
: null
}
composerRef={bioComposerRef}
emojiPickerOpen={bioExpressionPickerOpen}
onEmojiPickerOpenChange={setBioExpressionPickerOpen}
containerRef={bioContainerRef}
autocompleteQuery={autocompleteQuery}
autocompleteOptions={autocompleteOptions}
autocompleteType={autocompleteType}
selectedIndex={selectedIndex}
isAutocompleteAttached={isAutocompleteAttached}
setSelectedIndex={setSelectedIndex}
onCursorMove={onCursorMove}
handleSelect={handleSelect}
autocompleteZIndex={AUTOCOMPLETE_Z_INDEX}
data-flx="user.my-profile-tab.my-profile-tab-component.bio-editor.text-change"
/>
</div>
</div>
@@ -13,6 +13,7 @@ import {isLimitToggleEnabled} from '@app/features/app/utils/LimitUtils';
import {GET_PREMIUM_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {ComponentDispatch} from '@app/features/platform/utils/ComponentBus';
import * as PremiumModalCommands from '@app/features/premium/commands/PremiumModalCommands';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {Button} from '@app/features/ui/button/Button';
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
import {modal} from '@app/features/ui/commands/ModalCommands';
@@ -288,7 +289,7 @@ export const VideoTab: React.FC<VideoTabProps> = observer(
<div className={styles.premiumHeader} data-flx="user.video-tab.premium-header">
<CrownIcon
weight="fill"
size={18}
size={remFromPx(18)}
className={styles.premiumIcon}
data-flx="user.video-tab.premium-icon"
/>
@@ -326,7 +327,7 @@ export const VideoTab: React.FC<VideoTabProps> = observer(
<div className={styles.frameRateNote} data-flx="user.video-tab.frame-rate-note">
<CrownIcon
weight="fill"
size={14}
size={remFromPx(14)}
className={styles.frameRateIcon}
data-flx="user.video-tab.frame-rate-icon"
/>
@@ -346,7 +347,9 @@ export const VideoTab: React.FC<VideoTabProps> = observer(
<Button
variant="secondary"
fitContent
leftIcon={<GearIcon size={16} weight="bold" data-flx="user.video-tab.advanced-settings.gear-icon" />}
leftIcon={
<GearIcon size={remFromPx(16)} weight="bold" data-flx="user.video-tab.advanced-settings.gear-icon" />
}
onClick={handleOpenAdvancedVideoSettings}
data-flx="user.video-tab.button.open-advanced-video-settings"
>
@@ -7,6 +7,7 @@ import * as Modal from '@app/features/app/components/dialogs/Modal';
import FavoriteGif from '@app/features/expressions/state/FavoriteGif';
import Guilds from '@app/features/guild/state/Guilds';
import Inbox from '@app/features/inbox/state/Inbox';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {Button} from '@app/features/ui/button/Button';
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
import {modal} from '@app/features/ui/commands/ModalCommands';
@@ -340,7 +341,11 @@ const SearchProviderSettingsButton = observer(({mode, title, dataFlx}: SearchPro
variant="secondary"
compact
leftIcon={
<GearIcon size={14} weight="bold" data-flx="user.advanced-settings-tab.provider-settings-button.gear-icon" />
<GearIcon
size={remFromPx(14)}
weight="bold"
data-flx="user.advanced-settings-tab.provider-settings-button.gear-icon"
/>
}
onClick={handleOpen}
data-flx={dataFlx}
@@ -517,7 +522,7 @@ const SwitchGroupSettingsButton = observer(({mode, title, dataFlx}: SwitchGroupS
compact
leftIcon={
<GearIcon
size={14}
size={remFromPx(14)}
weight="bold"
data-flx="user.advanced-settings-tab.switch-group-settings-button.gear-icon"
/>
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {Button} from '@app/features/ui/button/Button';
import {
AdvancedSettingControl,
@@ -85,7 +86,7 @@ export const AdvancedSettingRow = observer(
) : canOpen ? (
<Button
variant="secondary"
rightIcon={<ArrowRightIcon size={14} data-flx="user.advanced-settings-tab.arrow-right-icon" />}
rightIcon={<ArrowRightIcon size={remFromPx(14)} data-flx="user.advanced-settings-tab.arrow-right-icon" />}
onClick={() => onOpen(item)}
data-flx="user.advanced-settings-tab.button.open-source"
>
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import * as Modal from '@app/features/app/components/dialogs/Modal';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {Button} from '@app/features/ui/button/Button';
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
import {modal} from '@app/features/ui/commands/ModalCommands';
@@ -448,7 +449,7 @@ export const ScreenShareEncoderControls = observer(({title}: ScreenShareEncoderC
compact
leftIcon={
<GearIcon
size={14}
size={remFromPx(14)}
weight="bold"
data-flx="user.advanced-settings-tab.screen-share-encoder-controls.gear-icon"
/>
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import FocusRing from '@app/features/ui/focus_ring/FocusRing';
import styles from '@app/features/user/components/modals/tabs/appearance_tab/ThemeTab.module.css';
import type {ThemeType} from '@fluxer/constants/src/UserConstants';
@@ -74,7 +75,7 @@ export const ThemeButton = observer(
<CheckIcon
weight="bold"
className={styles.themeButtonCheckmarkIcon}
size={12}
size={remFromPx(12)}
data-flx="user.appearance-tab.theme.theme-button.theme-button-checkmark-icon"
/>
</div>
@@ -2,6 +2,7 @@
import {isKeyboardActivationKey} from '@app/features/input/utils/KeyboardUtils';
import * as ThemePreferenceCommands from '@app/features/theme/commands/ThemePreferenceCommands';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import Theme from '@app/features/theme/state/Theme';
import * as ThemeStudioCommands from '@app/features/theme_studio/commands/ThemeStudioCommands';
import {Button} from '@app/features/ui/button/Button';
@@ -146,7 +147,7 @@ export const ThemeSelector: React.FC<ThemeSelectorProps> = observer(
isLight: !systemPrefersDark,
icon: (
<ArrowsCounterClockwiseIcon
size={12}
size={remFromPx(12)}
data-flx="user.appearance-tab.theme.theme-tab-content.theme-options.arrows-counter-clockwise-icon"
/>
),
@@ -10,6 +10,7 @@ import {TRY_AGAIN_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescri
import {formatBotPermissionsQuery, getAllBotPermissions} from '@app/features/permissions/utils/PermissionUtils';
import {http} from '@app/features/platform/transport/RestTransport';
import {Logger} from '@app/features/platform/utils/AppLogger';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {Button} from '@app/features/ui/button/Button';
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
import {modal} from '@app/features/ui/commands/ModalCommands';
@@ -708,7 +709,11 @@ export const ApplicationDetail: React.FC<ApplicationDetailProps> = observer(
onClick={handleDelete}
submitting={isDeleting}
leftIcon={
<TrashIcon size={16} weight="fill" data-flx="user.applications-tab.application-detail.trash-icon" />
<TrashIcon
size={remFromPx(16)}
weight="fill"
data-flx="user.applications-tab.application-detail.trash-icon"
/>
}
fitContent
data-flx="user.applications-tab.application-detail.button.delete"
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {COPIED_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {Button} from '@app/features/ui/button/Button';
import {Input} from '@app/features/ui/components/form/FormInput';
import styles from '@app/features/user/components/modals/tabs/applications_tab/application_detail/ApplicationDetail.module.css';
@@ -55,13 +56,13 @@ export const ApplicationHeader: React.FC<ApplicationHeaderProps> = ({name, appli
leftIcon={
idCopied ? (
<CheckIcon
size={14}
size={remFromPx(14)}
weight="bold"
data-flx="user.applications-tab.application-detail.application-header.check-icon"
/>
) : (
<CopyIcon
size={14}
size={remFromPx(14)}
data-flx="user.applications-tab.application-detail.application-header.copy-icon"
/>
)
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {EXAMPLE_CALLBACK_URL} from '@app/features/app/config/I18nDisplayConstants';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {Button} from '@app/features/ui/button/Button';
import {Input} from '@app/features/ui/components/form/FormInput';
import {Switch} from '@app/features/ui/components/form/FormSwitch';
@@ -147,7 +148,7 @@ export const ApplicationInfoSection: React.FC<ApplicationInfoSectionProps> = ({
data-flx="user.applications-tab.application-detail.application-info-section.redirect-remove-button.remove-redirect"
>
<XIcon
size={18}
size={remFromPx(18)}
weight="bold"
data-flx="user.applications-tab.application-detail.application-info-section.x-icon"
/>
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {SCOPES_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {Button} from '@app/features/ui/button/Button';
import {Checkbox} from '@app/features/ui/checkbox/Checkbox';
import {Combobox, type ComboboxOption} from '@app/features/ui/components/form/FormCombobox';
@@ -212,7 +213,7 @@ export const OAuthBuilderSection: React.FC<OAuthBuilderSectionProps> = ({
aria-label={i18n._(COPY_AUTHORIZE_URL_DESCRIPTOR)}
leftIcon={
<CopyIcon
size={16}
size={remFromPx(16)}
data-flx="user.applications-tab.application-detail.o-auth-builder-section.copy-icon"
/>
}
@@ -5,6 +5,7 @@ import {SettingsTabSection} from '@app/features/app/components/dialogs/shared/Se
import Translation from '@app/features/messaging/state/Translation';
import ReverseImageSearch from '@app/features/search/state/ReverseImageSearch';
import SearchEngine from '@app/features/search/state/SearchEngine';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {Button} from '@app/features/ui/button/Button';
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
import {modal} from '@app/features/ui/commands/ModalCommands';
@@ -326,7 +327,7 @@ const EngineSection: React.FC<EngineSectionProps> = observer(
data-flx="user.chat-settings-tab.search-engines-tab.engine-section.remove-button.remove-custom-engine"
>
<TrashIcon
size={16}
size={remFromPx(16)}
weight="fill"
data-flx="user.chat-settings-tab.search-engines-tab.engine-section.trash-icon"
/>
@@ -6,6 +6,7 @@ import {
SettingsTabSection,
} from '@app/features/app/components/dialogs/shared/SettingsTabLayout';
import {PLAY_DESCRIPTOR, SETTINGS_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {Button} from '@app/features/ui/button/Button';
import * as ToastCommands from '@app/features/ui/commands/ToastCommands';
import styles from '@app/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.module.css';
@@ -147,7 +148,11 @@ export const ButtonsTab: React.FC<ButtonsTabProps> = observer(({openContextMenu}
<div className={styles.buttonsWrapper} data-flx="user.component-gallery-tab.buttons-tab.buttons-wrapper">
<Button
leftIcon={
<PlusIcon size={16} weight="bold" data-flx="user.component-gallery-tab.buttons-tab.plus-icon" />
<PlusIcon
size={remFromPx(16)}
weight="bold"
data-flx="user.component-gallery-tab.buttons-tab.plus-icon"
/>
}
onClick={() =>
ToastCommands.createToast({type: 'success', children: i18n._(PRIMARY_BUTTON_CLICKED_DESCRIPTOR)})
@@ -208,7 +213,9 @@ export const ButtonsTab: React.FC<ButtonsTabProps> = observer(({openContextMenu}
<div className={styles.buttonsWrapper} data-flx="user.component-gallery-tab.buttons-tab.buttons-wrapper--3">
<Button
small
leftIcon={<MegaphoneIcon size={14} data-flx="user.component-gallery-tab.buttons-tab.megaphone-icon" />}
leftIcon={
<MegaphoneIcon size={remFromPx(14)} data-flx="user.component-gallery-tab.buttons-tab.megaphone-icon" />
}
onClick={() =>
ToastCommands.createToast({type: 'success', children: i18n._(SMALL_BUTTON_CLICKED_DESCRIPTOR)})
}
@@ -217,7 +224,12 @@ export const ButtonsTab: React.FC<ButtonsTabProps> = observer(({openContextMenu}
<Trans>Small button</Trans>
</Button>
<Button
leftIcon={<MegaphoneIcon size={16} data-flx="user.component-gallery-tab.buttons-tab.megaphone-icon--2" />}
leftIcon={
<MegaphoneIcon
size={remFromPx(16)}
data-flx="user.component-gallery-tab.buttons-tab.megaphone-icon--2"
/>
}
onClick={() =>
ToastCommands.createToast({type: 'success', children: i18n._(REGULAR_BUTTON_CLICKED_DESCRIPTOR)})
}
@@ -228,7 +240,7 @@ export const ButtonsTab: React.FC<ButtonsTabProps> = observer(({openContextMenu}
<Button
small
variant="secondary"
leftIcon={<GearIcon size={14} data-flx="user.component-gallery-tab.buttons-tab.gear-icon" />}
leftIcon={<GearIcon size={remFromPx(14)} data-flx="user.component-gallery-tab.buttons-tab.gear-icon" />}
onClick={() =>
ToastCommands.createToast({type: 'success', children: i18n._(SMALL_SECONDARY_CLICKED_DESCRIPTOR)})
}
@@ -248,7 +260,11 @@ export const ButtonsTab: React.FC<ButtonsTabProps> = observer(({openContextMenu}
<div className={styles.buttonsWrapper} data-flx="user.component-gallery-tab.buttons-tab.buttons-wrapper--4">
<Button
leftIcon={
<PlusIcon size={16} weight="bold" data-flx="user.component-gallery-tab.buttons-tab.plus-icon--2" />
<PlusIcon
size={remFromPx(16)}
weight="bold"
data-flx="user.component-gallery-tab.buttons-tab.plus-icon--2"
/>
}
onClick={() => ToastCommands.createToast({type: 'success', children: i18n._(ADD_ACTION_DESCRIPTOR)})}
data-flx="user.component-gallery-tab.buttons-tab.button.create-toast--9"
@@ -257,7 +273,9 @@ export const ButtonsTab: React.FC<ButtonsTabProps> = observer(({openContextMenu}
</Button>
<Button
variant="secondary"
leftIcon={<GearIcon size={16} data-flx="user.component-gallery-tab.buttons-tab.gear-icon--2" />}
leftIcon={
<GearIcon size={remFromPx(16)} data-flx="user.component-gallery-tab.buttons-tab.gear-icon--2" />
}
onClick={() => ToastCommands.createToast({type: 'success', children: i18n._(SETTINGS_OPENED_DESCRIPTOR)})}
data-flx="user.component-gallery-tab.buttons-tab.button.create-toast--10"
>
@@ -265,14 +283,16 @@ export const ButtonsTab: React.FC<ButtonsTabProps> = observer(({openContextMenu}
</Button>
<Button
variant="danger"
leftIcon={<TrashIcon size={16} data-flx="user.component-gallery-tab.buttons-tab.trash-icon" />}
leftIcon={<TrashIcon size={remFromPx(16)} data-flx="user.component-gallery-tab.buttons-tab.trash-icon" />}
onClick={() => ToastCommands.createToast({type: 'error', children: i18n._(DELETE_ACTION_DESCRIPTOR)})}
data-flx="user.component-gallery-tab.buttons-tab.button.create-toast--11"
>
<Trans>Delete</Trans>
</Button>
<Button
leftIcon={<ShareFatIcon size={16} data-flx="user.component-gallery-tab.buttons-tab.share-fat-icon" />}
leftIcon={
<ShareFatIcon size={remFromPx(16)} data-flx="user.component-gallery-tab.buttons-tab.share-fat-icon" />
}
onClick={() => ToastCommands.createToast({type: 'success', children: i18n._(SHARE_ACTION_DESCRIPTOR)})}
data-flx="user.component-gallery-tab.buttons-tab.button.create-toast--12"
>
@@ -286,7 +306,7 @@ export const ButtonsTab: React.FC<ButtonsTabProps> = observer(({openContextMenu}
<Button
rightIcon={
<PaperPlaneRightIcon
size={16}
size={remFromPx(16)}
data-flx="user.component-gallery-tab.buttons-tab.paper-plane-right-icon"
/>
}
@@ -299,7 +319,7 @@ export const ButtonsTab: React.FC<ButtonsTabProps> = observer(({openContextMenu}
variant="secondary"
rightIcon={
<LinkSimpleIcon
size={16}
size={remFromPx(16)}
weight="bold"
data-flx="user.component-gallery-tab.buttons-tab.link-simple-icon"
/>
@@ -316,9 +336,18 @@ export const ButtonsTab: React.FC<ButtonsTabProps> = observer(({openContextMenu}
<div className={styles.buttonsWrapper} data-flx="user.component-gallery-tab.buttons-tab.buttons-wrapper--6">
<Button
leftIcon={
<PlusIcon size={16} weight="bold" data-flx="user.component-gallery-tab.buttons-tab.plus-icon--3" />
<PlusIcon
size={remFromPx(16)}
weight="bold"
data-flx="user.component-gallery-tab.buttons-tab.plus-icon--3"
/>
}
rightIcon={
<ShareFatIcon
size={remFromPx(16)}
data-flx="user.component-gallery-tab.buttons-tab.share-fat-icon--2"
/>
}
rightIcon={<ShareFatIcon size={16} data-flx="user.component-gallery-tab.buttons-tab.share-fat-icon--2" />}
onClick={() =>
ToastCommands.createToast({type: 'success', children: i18n._(ACTION_WITH_BOTH_ICONS_DESCRIPTOR)})
}
@@ -328,9 +357,13 @@ export const ButtonsTab: React.FC<ButtonsTabProps> = observer(({openContextMenu}
</Button>
<Button
variant="secondary"
leftIcon={<HeartIcon size={16} data-flx="user.component-gallery-tab.buttons-tab.heart-icon" />}
leftIcon={<HeartIcon size={remFromPx(16)} data-flx="user.component-gallery-tab.buttons-tab.heart-icon" />}
rightIcon={
<CheckIcon size={16} weight="bold" data-flx="user.component-gallery-tab.buttons-tab.check-icon" />
<CheckIcon
size={remFromPx(16)}
weight="bold"
data-flx="user.component-gallery-tab.buttons-tab.check-icon"
/>
}
onClick={() => ToastCommands.createToast({type: 'success', children: i18n._(SAVED_DESCRIPTOR)})}
data-flx="user.component-gallery-tab.buttons-tab.button.create-toast--16"
@@ -348,14 +381,14 @@ export const ButtonsTab: React.FC<ButtonsTabProps> = observer(({openContextMenu}
<Button
square
aria-label={i18n._(PLAY_DESCRIPTOR)}
icon={<PlayIcon size={16} data-flx="user.component-gallery-tab.buttons-tab.play-icon" />}
icon={<PlayIcon size={remFromPx(16)} data-flx="user.component-gallery-tab.buttons-tab.play-icon" />}
onClick={() => ToastCommands.createToast({type: 'success', children: i18n._(PLAY_2_DESCRIPTOR)})}
data-flx="user.component-gallery-tab.buttons-tab.button.create-toast--17"
/>
<Button
square
aria-label={i18n._(SETTINGS_DESCRIPTOR)}
icon={<GearIcon size={16} data-flx="user.component-gallery-tab.buttons-tab.gear-icon--3" />}
icon={<GearIcon size={remFromPx(16)} data-flx="user.component-gallery-tab.buttons-tab.gear-icon--3" />}
onClick={() => ToastCommands.createToast({type: 'success', children: i18n._(SETTINGS_2_DESCRIPTOR)})}
data-flx="user.component-gallery-tab.buttons-tab.button.create-toast--18"
/>
@@ -364,7 +397,10 @@ export const ButtonsTab: React.FC<ButtonsTabProps> = observer(({openContextMenu}
variant="secondary"
aria-label={i18n._(BOOKMARK_DESCRIPTOR)}
icon={
<BookmarkSimpleIcon size={16} data-flx="user.component-gallery-tab.buttons-tab.bookmark-simple-icon" />
<BookmarkSimpleIcon
size={remFromPx(16)}
data-flx="user.component-gallery-tab.buttons-tab.bookmark-simple-icon"
/>
}
onClick={() => ToastCommands.createToast({type: 'success', children: i18n._(BOOKMARKED_DESCRIPTOR)})}
data-flx="user.component-gallery-tab.buttons-tab.button.create-toast--19"
@@ -373,7 +409,7 @@ export const ButtonsTab: React.FC<ButtonsTabProps> = observer(({openContextMenu}
square
variant="secondary"
aria-label={i18n._(HEART_DESCRIPTOR)}
icon={<HeartIcon size={16} data-flx="user.component-gallery-tab.buttons-tab.heart-icon--2" />}
icon={<HeartIcon size={remFromPx(16)} data-flx="user.component-gallery-tab.buttons-tab.heart-icon--2" />}
onClick={() => ToastCommands.createToast({type: 'success', children: i18n._(LIKED_DESCRIPTOR)})}
data-flx="user.component-gallery-tab.buttons-tab.button.create-toast--20"
/>
@@ -381,7 +417,7 @@ export const ButtonsTab: React.FC<ButtonsTabProps> = observer(({openContextMenu}
square
variant="danger"
aria-label={i18n._(DELETE_DESCRIPTOR)}
icon={<TrashIcon size={16} data-flx="user.component-gallery-tab.buttons-tab.trash-icon--2" />}
icon={<TrashIcon size={remFromPx(16)} data-flx="user.component-gallery-tab.buttons-tab.trash-icon--2" />}
onClick={() => ToastCommands.createToast({type: 'error', children: i18n._(DELETED_DESCRIPTOR)})}
data-flx="user.component-gallery-tab.buttons-tab.button.create-toast--21"
/>
@@ -390,7 +426,7 @@ export const ButtonsTab: React.FC<ButtonsTabProps> = observer(({openContextMenu}
aria-label={i18n._(MORE_DESCRIPTOR)}
icon={
<DotsThreeOutlineIcon
size={16}
size={remFromPx(16)}
data-flx="user.component-gallery-tab.buttons-tab.dots-three-outline-icon"
/>
}
@@ -414,7 +450,12 @@ export const ButtonsTab: React.FC<ButtonsTabProps> = observer(({openContextMenu}
<Button
small
submitting
leftIcon={<MegaphoneIcon size={14} data-flx="user.component-gallery-tab.buttons-tab.megaphone-icon--3" />}
leftIcon={
<MegaphoneIcon
size={remFromPx(14)}
data-flx="user.component-gallery-tab.buttons-tab.megaphone-icon--3"
/>
}
data-flx="user.component-gallery-tab.buttons-tab.button--6"
>
<Trans>Small submitting</Trans>
@@ -437,7 +478,7 @@ export const ButtonsTab: React.FC<ButtonsTabProps> = observer(({openContextMenu}
<Button
leftIcon={
<DotsThreeOutlineIcon
size={16}
size={remFromPx(16)}
data-flx="user.component-gallery-tab.buttons-tab.dots-three-outline-icon--2"
/>
}
@@ -450,7 +491,7 @@ export const ButtonsTab: React.FC<ButtonsTabProps> = observer(({openContextMenu}
square
icon={
<DotsThreeOutlineIcon
size={16}
size={remFromPx(16)}
data-flx="user.component-gallery-tab.buttons-tab.dots-three-outline-icon--3"
/>
}
@@ -7,6 +7,7 @@ import {
} from '@app/features/app/components/dialogs/shared/SettingsTabLayout';
import {EXAMPLE_EMAIL, EXAMPLE_USERNAME_MENTION} from '@app/features/app/config/I18nDisplayConstants';
import {PASSWORD_DESCRIPTOR, USERNAME_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import * as ToastCommands from '@app/features/ui/commands/ToastCommands';
import {ColorPickerField} from '@app/features/ui/components/form/ColorPickerField';
import {Input, Textarea} from '@app/features/ui/components/form/FormInput';
@@ -221,7 +222,7 @@ export const InputsTab: React.FC<InputsTabProps> = observer(
placeholder={i18n._(SEARCH_FOR_ANYTHING_DESCRIPTOR)}
leftIcon={
<MagnifyingGlassIcon
size={16}
size={remFromPx(16)}
weight="bold"
data-flx="user.component-gallery-tab.inputs-tab.magnifying-glass-icon"
/>
@@ -233,7 +234,7 @@ export const InputsTab: React.FC<InputsTabProps> = observer(
<Input
label={i18n._(USER_PROFILE_DESCRIPTOR)}
placeholder={i18n._(ENTER_USERNAME_DESCRIPTOR)}
leftIcon={<UserIcon size={16} data-flx="user.component-gallery-tab.inputs-tab.user-icon" />}
leftIcon={<UserIcon size={remFromPx(16)} data-flx="user.component-gallery-tab.inputs-tab.user-icon" />}
value={inputValue3}
onChange={(e) => setInputValue3(e.target.value)}
data-flx="user.component-gallery-tab.inputs-tab.input--3"
@@ -5,6 +5,7 @@ import {
SettingsTabContent,
SettingsTabSection,
} from '@app/features/app/components/dialogs/shared/SettingsTabLayout';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {Button} from '@app/features/ui/button/Button';
import * as ToastCommands from '@app/features/ui/commands/ToastCommands';
import {Tooltip} from '@app/features/ui/tooltip/Tooltip';
@@ -146,7 +147,7 @@ export const OverlaysTab: React.FC<OverlaysTabProps> = observer(({openContextMen
<Button
leftIcon={
<DotsThreeOutlineIcon
size={16}
size={remFromPx(16)}
data-flx="user.component-gallery-tab.overlays-tab.dots-three-outline-icon"
/>
}
@@ -159,7 +160,7 @@ export const OverlaysTab: React.FC<OverlaysTabProps> = observer(({openContextMen
square
icon={
<DotsThreeOutlineIcon
size={16}
size={remFromPx(16)}
data-flx="user.component-gallery-tab.overlays-tab.dots-three-outline-icon--2"
/>
}
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {Button} from '@app/features/ui/button/Button';
import {AudioLevelMeter} from '@app/features/user/components/modals/tabs/components/AudioLevelMeter';
import styles from '@app/features/user/components/modals/tabs/components/MicTestSection.module.css';
@@ -29,9 +30,9 @@ export const MicTestSection: React.FC<MicTestSectionProps> = observer(({settings
square={true}
icon={
isTesting ? (
<StopIcon size={16} weight="fill" data-flx="user.mic-test-section.stop-icon" />
<StopIcon size={remFromPx(16)} weight="fill" data-flx="user.mic-test-section.stop-icon" />
) : (
<PlayIcon size={16} weight="fill" data-flx="user.mic-test-section.start-icon" />
<PlayIcon size={remFromPx(16)} weight="fill" data-flx="user.mic-test-section.start-icon" />
)
}
aria-label={isTesting ? 'Stop mic test' : 'Start mic test'}
@@ -6,6 +6,7 @@ import Keybind, {
type KeybindCommand,
type KeyCombo,
} from '@app/features/input/state/InputKeybind';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {MoreOptionsVerticalIcon} from '@app/features/ui/action_menu/ContextMenuIcons';
import {MenuGroup} from '@app/features/ui/action_menu/MenuGroup';
import {MenuItem} from '@app/features/ui/action_menu/MenuItem';
@@ -69,7 +70,7 @@ export const CustomKeybindRow = observer(
ContextMenuCommands.openFromEvent(event, ({onClose}) => (
<MenuGroup data-flx="user.keybinds-tab.open-row-menu.menu-group">
<MenuItem
icon={<TrashIcon size={16} data-flx="user.keybinds-tab.open-row-menu.trash-icon" />}
icon={<TrashIcon size={remFromPx(16)} data-flx="user.keybinds-tab.open-row-menu.trash-icon" />}
danger
onClick={() => {
onClose();
@@ -143,7 +144,7 @@ export const CustomKeybindRow = observer(
data-flx="user.keybinds-tab.custom-keybind-row.conflict-warning"
>
<WarningIcon
size={14}
size={remFromPx(14)}
weight="fill"
className={styles.conflictIcon}
aria-hidden
@@ -2,6 +2,7 @@
import {ConfirmModal} from '@app/features/app/components/dialogs/ConfirmModal';
import Keybind from '@app/features/input/state/InputKeybind';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {RetryIcon} from '@app/features/ui/action_menu/ContextMenuIcons';
import {Button} from '@app/features/ui/button/Button';
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
@@ -142,7 +143,13 @@ export const CustomKeybindsList: React.FC<{searchQuery: string}> = observer(({se
small
type="button"
onClick={handleAdd}
leftIcon={<PlusIcon size={14} weight="bold" data-flx="user.keybinds-tab.custom-keybinds-list.plus-icon" />}
leftIcon={
<PlusIcon
size={remFromPx(14)}
weight="bold"
data-flx="user.keybinds-tab.custom-keybinds-list.plus-icon"
/>
}
data-flx="user.keybinds-tab.custom-keybinds-list.button.add"
>
<Trans>Add shortcut</Trans>
@@ -25,3 +25,28 @@
font-size: 0.875rem;
color: var(--text-primary-muted);
}
.editor {
--composer-placeholder-top: 0.625rem;
--composer-placeholder-right: 0px;
--composer-placeholder-left: 1rem;
flex: 1 1 0%;
min-width: 0;
padding-inline-start: 1rem;
}
:global([dir='rtl']) .editor {
--composer-placeholder-right: 1rem;
--composer-placeholder-left: 0px;
}
.editor [data-channel-textarea] {
box-sizing: border-box;
min-height: 6.25rem;
max-height: 6.25rem;
padding: 0.625rem 0;
}
.characterCountContainer {
text-align: center;
}
@@ -1,17 +1,17 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {
Autocomplete,
type AutocompleteOption,
type AutocompleteType,
getAutocompleteOptionId,
} from '@app/features/channel/components/Autocomplete';
import type {FlatEmoji} from '@app/features/emoji/types/EmojiTypes';
import {ExpressionPickerPopout} from '@app/features/expressions/components/popouts/ExpressionPickerPopout';
import {LexicalRichInput, type LexicalRichInputHandle} from '@app/features/lexical/composer/LexicalRichInput';
import {MarkdownContext} from '@app/features/messaging/components/markdown/renderers/RendererTypes';
import {useMarkdownKeybinds} from '@app/features/messaging/hooks/useMarkdownKeybinds';
import {useTextareaAutocompleteKeyboard} from '@app/features/messaging/hooks/useTextareaAutocompleteKeyboard';
import type {TriggerType} from '@app/features/messaging/utils/AutocompleteTriggerPolicy';
import {getParserFlagsForContext} from '@app/features/messaging/utils/markdown/MarkdownParserFlags';
import type {MentionSegment} from '@app/features/messaging/utils/TextareaSegmentManager';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {CharacterCounter} from '@app/features/ui/character_counter/CharacterCounter';
import {Textarea} from '@app/features/ui/components/form/FormInput';
import formStyles from '@app/features/ui/components/form/FormInput.module.css';
import surfaceStyles from '@app/features/ui/components/form/FormSurface.module.css';
import FocusRing from '@app/features/ui/focus_ring/FocusRing';
import {Popout} from '@app/features/ui/popover/PopoverPopout';
import styles from '@app/features/user/components/modals/tabs/my_profile_tab/BioEditor.module.css';
@@ -21,206 +21,239 @@ import {SmileyIcon} from '@phosphor-icons/react';
import {clsx} from 'clsx';
import {observer} from 'mobx-react-lite';
import type React from 'react';
import {useCallback, useId, useState} from 'react';
import {useCallback, useEffect, useId, useRef, useState} from 'react';
const ABOUT_ME_DESCRIPTOR = msg({
message: 'About me',
comment: 'Short label in the bio editor. Keep it concise.',
});
const OPEN_EMOJI_PICKER_DESCRIPTOR = msg({
message: 'Open emoji picker',
comment: 'Button or menu action label in the bio editor. Keep it concise.',
});
const ABOUT_ME_DESCRIPTOR = msg({message: 'About me'});
const OPEN_EMOJI_PICKER_DESCRIPTOR = msg({message: 'Open emoji picker'});
const BIO_MARKDOWN_PARSER_FLAGS = getParserFlagsForContext(MarkdownContext.RESTRICTED_USER_BIO);
const BIO_ALLOWED_TRIGGERS: ReadonlyArray<TriggerType> = Object.freeze(['emoji']);
const COMPOSER_SURFACE_INTERACTIVE_SELECTOR =
'[data-channel-textarea], button, a, input, textarea, select, [role="button"]';
interface BioEditorProps {
value: string;
onChange: (value: string, inputType?: string) => void;
onEmojiSelect: (emoji: FlatEmoji, shiftKey?: boolean) => boolean;
placeholder?: string;
displayMaxLength: number;
initialValue: string;
initialSegments: ReadonlyArray<MentionSegment>;
hydrationKey: number;
onChange: (display: string, segments: Array<MentionSegment>, wire: string) => void;
onEmojiSelect: (emoji: FlatEmoji, shiftKey: boolean) => boolean;
placeholder: string | null;
actualLength: number;
actualMaxLength: number;
disabled?: boolean;
disabled: boolean;
isMobile: boolean;
errorMessage?: string;
textareaRef: React.RefObject<HTMLTextAreaElement | null>;
errorMessage: string | null;
composerRef: React.RefObject<LexicalRichInputHandle | null>;
emojiPickerOpen: boolean;
onEmojiPickerOpenChange: (open: boolean) => void;
containerRef: React.RefObject<HTMLDivElement | null>;
autocompleteQuery?: string;
autocompleteOptions?: Array<AutocompleteOption>;
autocompleteType?: AutocompleteType;
selectedIndex?: number;
isAutocompleteAttached?: boolean;
setSelectedIndex?: React.Dispatch<React.SetStateAction<number>>;
onCursorMove?: () => void;
handleSelect?: (option: AutocompleteOption) => void;
autocompleteZIndex?: number;
}
export const BioEditor = observer(
({
value,
initialValue,
initialSegments,
hydrationKey,
onChange,
onEmojiSelect,
placeholder,
displayMaxLength,
actualLength,
actualMaxLength,
disabled,
isMobile,
errorMessage,
textareaRef,
composerRef,
emojiPickerOpen,
onEmojiPickerOpenChange,
containerRef,
autocompleteQuery,
autocompleteOptions,
autocompleteType,
selectedIndex,
isAutocompleteAttached,
setSelectedIndex,
onCursorMove,
handleSelect,
autocompleteZIndex,
}: BioEditorProps) => {
const {i18n} = useLingui();
const [isFocused, setIsFocused] = useState(false);
const autocompleteListId = useId();
useMarkdownKeybinds(isFocused);
useMarkdownKeybinds(isFocused, {preserveEditableFocusActions: false});
const editableId = useId();
const labelId = `${editableId}-label`;
const descriptionId = `${editableId}-description`;
const errorId = `${editableId}-error`;
const wrapperRef = useRef<HTMLDivElement | null>(null);
const editableRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const wrapper = wrapperRef.current;
if (wrapper == null) {
editableRef.current = null;
return;
}
const editable = wrapper.querySelector<HTMLDivElement>('[data-channel-textarea]');
if (editable == null) {
editableRef.current = null;
return;
}
editableRef.current = editable;
}, [hydrationKey]);
const handleBioEmojiSelect = useCallback(
(emoji: FlatEmoji, shiftKey?: boolean) => {
(emoji: FlatEmoji, shiftKey: boolean) => {
const didInsert = onEmojiSelect(emoji, shiftKey);
if (didInsert && !shiftKey) {
if (didInsert && shiftKey !== true) {
onEmojiPickerOpenChange(false);
}
return didInsert;
},
[onEmojiSelect, onEmojiPickerOpenChange],
[onEmojiPickerOpenChange, onEmojiSelect],
);
const {handleKeyDown} = useTextareaAutocompleteKeyboard({
isAutocompleteAttached: isAutocompleteAttached || false,
autocompleteOptions: autocompleteOptions || [],
selectedIndex: selectedIndex || 0,
setSelectedIndex: setSelectedIndex || (() => {}),
handleSelect: handleSelect || (() => {}),
});
const activeAutocompleteOptionId =
isAutocompleteAttached && autocompleteOptions?.[selectedIndex || 0]
? getAutocompleteOptionId(autocompleteListId, selectedIndex || 0)
: undefined;
return (
<div data-flx="user.my-profile-tab.bio-editor.div">
{isAutocompleteAttached && handleSelect && setSelectedIndex && (
<Autocomplete
type={autocompleteType || 'emoji'}
onSelect={handleSelect}
selectedIndex={selectedIndex || 0}
options={autocompleteOptions || []}
setSelectedIndex={setSelectedIndex}
referenceElement={containerRef.current}
query={autocompleteQuery || ''}
zIndex={autocompleteZIndex}
listboxId={autocompleteListId}
data-flx="user.my-profile-tab.bio-editor.autocomplete.select"
let resolvedPlaceholder = '';
if (placeholder != null) {
resolvedPlaceholder = placeholder;
}
const hasError = errorMessage != null && errorMessage.length > 0;
let surfaceInteractionClass = formStyles.focusable;
if (hasError) {
surfaceInteractionClass = formStyles.error;
}
let describedBy = descriptionId;
if (hasError) {
describedBy = `${descriptionId} ${errorId}`;
}
let ariaErrorMessage: string | null = null;
if (hasError) {
ariaErrorMessage = errorId;
}
const emojiButton = isMobile ? (
<FocusRing offset={-2} enabled={!disabled} data-flx="user.my-profile-tab.bio-editor.focus-ring">
<button
type="button"
onClick={() => onEmojiPickerOpenChange(true)}
className={clsx(styles.emojiButton, emojiPickerOpen && styles.emojiButtonActive)}
disabled={disabled}
aria-label={i18n._(OPEN_EMOJI_PICKER_DESCRIPTOR)}
aria-haspopup="dialog"
aria-expanded={emojiPickerOpen}
>
<SmileyIcon size={remFromPx(20)} weight="fill" />
</button>
</FocusRing>
) : (
<Popout
position="bottom-end"
animationType="none"
offsetMainAxis={8}
offsetCrossAxis={0}
onOpen={() => onEmojiPickerOpenChange(true)}
onClose={() => onEmojiPickerOpenChange(false)}
returnFocusRef={editableRef}
render={({onClose}) => (
<ExpressionPickerPopout
onEmojiSelect={(emoji, shiftKey) => {
const shiftPressed = shiftKey === true;
const didInsert = handleBioEmojiSelect(emoji, shiftPressed);
if (didInsert && !shiftPressed) {
onClose();
}
}}
onClose={onClose}
visibleTabs={['emojis']}
/>
)}
<div ref={containerRef} data-flx="user.my-profile-tab.bio-editor.div--2">
<Textarea
ref={textareaRef}
label={i18n._(ABOUT_ME_DESCRIPTOR)}
placeholder={placeholder}
maxLength={displayMaxLength}
minRows={4}
maxRows={4}
showCharacterCount={true}
value={value}
onChange={(e) => {
const nativeEvent = e.nativeEvent as InputEvent;
onChange(e.target.value, typeof nativeEvent.inputType === 'string' ? nativeEvent.inputType : undefined);
}}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
onKeyDown={handleKeyDown}
aria-autocomplete="list"
aria-controls={isAutocompleteAttached ? autocompleteListId : undefined}
aria-expanded={isAutocompleteAttached}
aria-haspopup="listbox"
aria-activedescendant={activeAutocompleteOptionId}
onKeyUp={onCursorMove}
onClick={onCursorMove}
error={errorMessage}
>
<FocusRing offset={-2} enabled={!disabled}>
<button
type="button"
className={clsx(styles.emojiButton, emojiPickerOpen && styles.emojiButtonActive)}
disabled={disabled}
characterCountTooltip={() => (
<CharacterCounter
currentLength={actualLength}
maxLength={actualMaxLength}
canUpgrade={false}
premiumMaxLength={actualMaxLength}
onUpgradeClick={() => undefined}
data-flx="user.my-profile-tab.bio-editor.character-counter"
/>
)}
innerActionButton={
isMobile ? (
<FocusRing offset={-2} enabled={!disabled} data-flx="user.my-profile-tab.bio-editor.focus-ring">
<button
type="button"
onClick={() => onEmojiPickerOpenChange(true)}
className={clsx(styles.emojiButton, emojiPickerOpen && styles.emojiButtonActive)}
disabled={disabled}
aria-label={i18n._(OPEN_EMOJI_PICKER_DESCRIPTOR)}
aria-haspopup="dialog"
aria-expanded={emojiPickerOpen}
data-flx="user.my-profile-tab.bio-editor.emoji-button.emoji-picker-open-change"
>
<SmileyIcon size={20} weight="fill" data-flx="user.my-profile-tab.bio-editor.smiley-icon" />
</button>
</FocusRing>
) : (
<Popout
position="bottom-end"
animationType="none"
offsetMainAxis={8}
offsetCrossAxis={0}
onOpen={() => onEmojiPickerOpenChange(true)}
onClose={() => onEmojiPickerOpenChange(false)}
returnFocusRef={textareaRef}
render={({onClose}) => (
<ExpressionPickerPopout
onEmojiSelect={(emoji, shiftKey) => {
const didInsert = handleBioEmojiSelect(emoji, shiftKey);
if (didInsert && !shiftKey) {
onClose();
}
}}
onClose={onClose}
visibleTabs={['emojis']}
data-flx="user.my-profile-tab.bio-editor.expression-picker-popout"
aria-label={i18n._(OPEN_EMOJI_PICKER_DESCRIPTOR)}
aria-haspopup="dialog"
aria-expanded={emojiPickerOpen}
>
<SmileyIcon size={remFromPx(20)} weight="fill" />
</button>
</FocusRing>
</Popout>
);
return (
<div className="flx-element">
<fieldset className={formStyles.fieldset}>
<div className={formStyles.labelContainer}>
<label
id={labelId}
htmlFor={editableId}
onPointerDown={(event) => {
if (disabled) {
return;
}
event.preventDefault();
const composer = composerRef.current;
if (composer != null) {
composer.focus();
}
}}
className={formStyles.label}
>
{i18n._(ABOUT_ME_DESCRIPTOR)}
</label>
</div>
<div className={formStyles.inputGroup}>
<FocusRing within={true} ringTarget={wrapperRef} focusTarget={wrapperRef} offset={-2} enabled={!disabled}>
<div
ref={wrapperRef}
className={clsx(formStyles.textareaWrapper, surfaceStyles.surface, surfaceInteractionClass)}
onPointerDown={(event) => {
if (disabled) {
return;
}
const target = event.target;
if (target instanceof Element && target.closest(COMPOSER_SURFACE_INTERACTIVE_SELECTOR) != null) {
return;
}
event.preventDefault();
const composer = composerRef.current;
if (composer != null) {
composer.focus();
}
}}
>
<LexicalRichInput
key={hydrationKey}
initialValue={initialValue}
initialSegments={initialSegments}
placeholder={resolvedPlaceholder}
disabled={disabled}
channel={null}
allowedTriggers={[...BIO_ALLOWED_TRIGGERS]}
markdown={true}
markdownParserFlags={BIO_MARKDOWN_PARSER_FLAGS}
singleLine={false}
size="form"
maxLength={actualMaxLength}
className={styles.editor}
autocompleteAnchor={wrapperRef.current}
id={editableId}
ariaLabelledBy={labelId}
ariaInvalid={hasError}
ariaErrorMessage={ariaErrorMessage == null ? undefined : ariaErrorMessage}
ariaDescribedBy={describedBy}
richInputRef={composerRef}
onChange={onChange}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
i18n={i18n}
/>
<div className={formStyles.textareaActions}>
{emojiButton}
<div className={styles.characterCountContainer}>
<CharacterCounter
currentLength={actualLength}
maxLength={actualMaxLength}
canUpgrade={false}
premiumMaxLength={actualMaxLength}
onUpgradeClick={() => undefined}
/>
)}
data-flx="user.my-profile-tab.bio-editor.popout"
>
<FocusRing offset={-2} enabled={!disabled} data-flx="user.my-profile-tab.bio-editor.focus-ring--2">
<button
type="button"
className={clsx(styles.emojiButton, emojiPickerOpen && styles.emojiButtonActive)}
disabled={disabled}
aria-label={i18n._(OPEN_EMOJI_PICKER_DESCRIPTOR)}
aria-haspopup="dialog"
aria-expanded={emojiPickerOpen}
data-flx="user.my-profile-tab.bio-editor.emoji-button"
>
<SmileyIcon size={20} weight="fill" data-flx="user.my-profile-tab.bio-editor.smiley-icon--2" />
</button>
</FocusRing>
</Popout>
)
}
data-flx="user.my-profile-tab.bio-editor.textarea.cursor-move"
/>
</div>
<div className={styles.description} data-flx="user.my-profile-tab.bio-editor.description">
</div>
</div>
</div>
</FocusRing>
{hasError && (
<span id={errorId} className={formStyles.errorText}>
{errorMessage}
</span>
)}
</div>
</fieldset>
<div id={descriptionId} className={styles.description}>
<Trans>You can use links, emoji, and markdown.</Trans>
</div>
</div>
@@ -5,6 +5,7 @@ import {LimitResolver} from '@app/features/app/utils/LimitResolverAdapter';
import {isLimitToggleEnabled} from '@app/features/app/utils/LimitUtils';
import * as PremiumModalCommands from '@app/features/premium/commands/PremiumModalCommands';
import {shouldShowPremiumFeatures} from '@app/features/premium/utils/PremiumUtils';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {Button} from '@app/features/ui/button/Button';
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
import {modal} from '@app/features/ui/commands/ModalCommands';
@@ -113,7 +114,11 @@ export const UsernameSection = observer(({isClaimed, isEmailVerified, user}: Use
aria-label={i18n._(GET_TO_CUSTOMIZE_YOUR_TAG_DESCRIPTOR, {premiumProductName: PREMIUM_PRODUCT_NAME})}
data-flx="user.my-profile-tab.username-section.premium-button.open"
>
<CrownIcon weight="fill" size={18} data-flx="user.my-profile-tab.username-section.crown-icon" />
<CrownIcon
weight="fill"
size={remFromPx(18)}
data-flx="user.my-profile-tab.username-section.crown-icon"
/>
</button>
</Tooltip>
)}
@@ -4,6 +4,7 @@ import Accessibility from '@app/features/accessibility/state/Accessibility';
import {CUSTOM_SOUND_MAX_SIZE_LABEL} from '@app/features/app/config/I18nDisplayConstants';
import type * as CustomSoundDB from '@app/features/notification/utils/CustomSoundDB';
import {type SoundType, SoundType as SoundTypes} from '@app/features/notification/utils/SoundUtils';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {Accordion} from '@app/features/ui/accordion/Accordion';
import {Switch} from '@app/features/ui/components/form/FormSwitch';
import {Slider} from '@app/features/ui/components/Slider';
@@ -205,7 +206,7 @@ export const Sounds: React.FC<SoundsProps> = observer(
data-flx="user.notifications-tab.sounds.icon-button.upload-click"
>
<UploadIcon
size={16}
size={remFromPx(16)}
className={styles.uploadIcon}
data-flx="user.notifications-tab.sounds.upload-icon"
/>
@@ -220,7 +221,7 @@ export const Sounds: React.FC<SoundsProps> = observer(
data-flx="user.notifications-tab.sounds.icon-button.custom-sound-delete"
>
<TrashIcon
size={16}
size={remFromPx(16)}
className={styles.deleteIcon}
data-flx="user.notifications-tab.sounds.delete-icon"
/>
@@ -335,7 +336,11 @@ export const Sounds: React.FC<SoundsProps> = observer(
transition={soundListTransition}
data-flx="user.notifications-tab.sounds.reveal-chevron"
>
<CaretDownIcon size={16} weight="bold" data-flx="user.notifications-tab.sounds.caret-down-icon" />
<CaretDownIcon
size={remFromPx(16)}
weight="bold"
data-flx="user.notifications-tab.sounds.caret-down-icon"
/>
</motion.span>
</button>
</div>
@@ -382,7 +387,7 @@ export const Sounds: React.FC<SoundsProps> = observer(
data-flx="user.notifications-tab.sounds.sound-toggle-button.toggle-sound"
>
<SoundToggleIcon
size={14}
size={remFromPx(14)}
weight="fill"
data-flx="user.notifications-tab.sounds.sound-toggle-icon"
/>
@@ -3,6 +3,7 @@
import * as AccessibilityCommands from '@app/features/accessibility/commands/AccessibilityCommands';
import Accessibility from '@app/features/accessibility/state/Accessibility';
import {ComponentDispatch} from '@app/features/platform/utils/ComponentBus';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {Switch} from '@app/features/ui/components/form/FormSwitch';
import {RadioGroup, type RadioOption} from '@app/features/ui/radio_group/RadioGroup';
import Notification, {TTSNotificationMode} from '@app/features/ui/state/Notification';
@@ -97,7 +98,7 @@ export const TextToSpeech = observer(() => {
/>
<div className={styles.helperCallout} data-flx="user.notifications-tab.text-to-speech.helper-callout">
<InfoIcon
size={16}
size={remFromPx(16)}
weight="fill"
className={styles.helperIcon}
data-flx="user.notifications-tab.text-to-speech.helper-icon"
@@ -15,6 +15,7 @@ import {SafeMarkdown} from '@app/features/messaging/components/markdown';
import {MarkdownContext} from '@app/features/messaging/components/markdown/renderers/RendererTypes';
import {openExternalUrlWithWarning} from '@app/features/messaging/utils/ExternalLinkUtils';
import StreamerMode from '@app/features/streamer_mode/state/StreamerMode';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import markupStyles from '@app/features/theme/styles/Markup.module.css';
import {BlueskyIcon} from '@app/features/ui/components/icons/BlueskyIcon';
import {FluxerIcon} from '@app/features/ui/components/icons/FluxerIcon';
@@ -477,7 +478,7 @@ const ConnectionCard: React.FC<{
<BlueskyIcon size={18} data-flx="user.user-profile-shared.connection-card.bluesky-icon" />
) : (
<GlobeSimpleIcon
size={18}
size={remFromPx(18)}
className={styles.connectionDomainIcon}
data-flx="user.user-profile-shared.connection-card.connection-domain-icon"
/>
@@ -533,7 +534,7 @@ const ConnectionCard: React.FC<{
{icon}
{nameRow}
<ArrowSquareOutIcon
size={16}
size={remFromPx(16)}
weight="bold"
className={styles.connectionExternalArrow}
data-flx="user.user-profile-shared.connection-card.connection-external-arrow"
@@ -556,7 +557,7 @@ const ConnectionCard: React.FC<{
data-flx="user.user-profile-shared.connection-card.connection-external-link.link-click"
>
<ArrowSquareOutIcon
size={16}
size={remFromPx(16)}
weight="bold"
data-flx="user.user-profile-shared.connection-card.arrow-square-out-icon"
/>
@@ -645,7 +646,7 @@ export const UserProfileConnections: React.FC<{
/>
) : (
<GlobeSimpleIcon
size={16}
size={remFromPx(16)}
className={styles.connectionDomainIcon}
data-flx="user.user-profile-shared.user-profile-connections.connection-domain-icon"
/>