mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
fix(app): backport rendering improvements (#1580)
This commit is contained in:
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
-2
@@ -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"
|
||||
/>
|
||||
|
||||
+2
-1
@@ -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"
|
||||
>
|
||||
|
||||
+2
-1
@@ -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"
|
||||
/>
|
||||
|
||||
+2
-1
@@ -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
-1
@@ -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"
|
||||
/>
|
||||
),
|
||||
|
||||
+6
-1
@@ -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"
|
||||
|
||||
+3
-2
@@ -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"
|
||||
/>
|
||||
)
|
||||
|
||||
+2
-1
@@ -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"
|
||||
/>
|
||||
|
||||
+2
-1
@@ -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"
|
||||
/>
|
||||
}
|
||||
|
||||
+2
-1
@@ -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"
|
||||
/>
|
||||
|
||||
+64
-23
@@ -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"
|
||||
/>
|
||||
}
|
||||
|
||||
+3
-2
@@ -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"
|
||||
|
||||
+3
-2
@@ -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'}
|
||||
|
||||
+3
-2
@@ -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
|
||||
|
||||
+8
-1
@@ -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
@@ -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;
|
||||
}
|
||||
+205
-172
@@ -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>
|
||||
|
||||
+6
-1
@@ -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>
|
||||
)}
|
||||
|
||||
+9
-4
@@ -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"
|
||||
/>
|
||||
|
||||
+2
-1
@@ -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"
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user