From 5695510980565efd759383320cddbf6c65cb718d Mon Sep 17 00:00:00 2001 From: Hampus Date: Sat, 10 Oct 2026 23:17:29 +0200 Subject: [PATCH] refactor(app): merge duplicated components and stores (#3383) --- fluxer_app/package.json | 3 - fluxer_app/pkgs/list_utils/package.json | 12 - .../pkgs/list_utils/src/ListFormatting.ts | 61 -- .../list_utils/src/ListFormattingCache.ts | 19 - .../list_utils/src/ListFormattingFallback.ts | 29 - .../list_utils/src/ListFormattingSupport.ts | 16 - .../list_utils/src/ListFormattingTypes.ts | 19 - fluxer_app/pkgs/list_utils/tsconfig.json | 9 - fluxer_app/pkgs/number_utils/package.json | 12 - .../pkgs/number_utils/src/NumberConstants.ts | 7 - .../number_utils/src/NumberFormatterCache.ts | 25 - .../pkgs/number_utils/src/NumberFormatting.ts | 104 --- .../pkgs/number_utils/src/NumberParsing.ts | 24 - .../pkgs/number_utils/src/NumberTypes.ts | 17 - fluxer_app/pkgs/number_utils/tsconfig.json | 9 - fluxer_app/src/env.d.ts | 2 +- .../accessibility/state/Accessibility.ts | 2 +- .../components/AllSettingsRenderer.tsx | 2 +- .../components/FeatureComparisonTable.tsx | 2 +- .../components/MobileGuildSettingsView.tsx | 19 +- .../dialogs/components/PricingCard.tsx | 2 +- .../layout/sidebar_nav/AddGuildButton.tsx | 89 +- .../sidebar_nav/DownloadButton.module.css | 22 - .../layout/sidebar_nav/DownloadButton.tsx | 79 +- ...e.css => GuildListActionButton.module.css} | 0 .../sidebar_nav/GuildListActionButton.tsx | 80 ++ .../sidebar_nav/GuildListItemTooltip.tsx | 2 +- .../layout/sidebar_nav/HelpButton.module.css | 22 - .../layout/sidebar_nav/HelpButton.tsx | 87 +- .../app/keybindings/KeybindEventUtils.ts | 2 +- .../app/keybindings/KeybindManager.ts | 6 +- fluxer_app/src/features/app/state/Updater.ts | 36 +- .../src/features/auth/InstanceBranding.ts | 2 +- .../auth/commands/AuthenticationCommands.ts | 13 +- .../hooks/useBotInviteDestinations.ts | 4 +- .../auth/flow/AuthMinimalRegisterFormCore.tsx | 277 ------- .../auth/flow/AuthRegisterFormCore.tsx | 181 ++--- .../src/features/auth/flow/InviteHeader.tsx | 2 +- .../src/features/auth/flow/SubmitTooltip.tsx | 4 +- .../auth_login_core/useDesktopHandoffFlow.ts | 22 +- .../channel/components/CallMessage.tsx | 5 +- .../components/CrosspostCommunityCard.tsx | 2 +- .../channel/components/InviteEmbed.tsx | 2 +- .../channel/components/MobileMemesPicker.tsx | 37 +- .../components/channel_view/DMChannelView.tsx | 7 +- .../channel_view/GuildChannelView.tsx | 7 +- .../emoji_picker/EmojiPickerSearchBar.tsx | 138 +--- .../modals/EditGroupBottomSheet.tsx | 244 ++---- .../components/modals/EditGroupModal.tsx | 242 ++---- .../channel_overview_tab/SlowmodeControl.tsx | 5 +- .../components/modals/useEditGroupForm.tsx | 219 +++++ .../components/pickers/memes/MediaFormat.ts | 22 + .../pickers/memes/MemesPickerView.tsx | 20 +- .../shared/usePickerGridKeyboard.ts | 135 ++++ .../sticker_picker/StickerPickerSearchBar.tsx | 138 +--- .../textarea/TextareaContextMenu.tsx | 2 +- .../hooks/useChannelMemberListVisibility.ts | 15 - ...bility.ts => useChannelPanelVisibility.ts} | 2 +- .../channel/hooks/useChannelSearch.ts | 17 +- .../utils/ArboriumHighlightWorkerClient.ts | 2 +- .../utils/ArboriumHighlighting.ts | 12 +- .../connection/commands/ConnectionCommands.ts | 5 +- .../discovery/DiscoveryGuildCard.tsx | 2 +- .../discovery/DiscoverySearchFilters.tsx | 2 +- .../components/pages/GiftRegisterPage.tsx | 5 +- .../modals/GuildFolderSettingsModal.tsx | 4 +- .../add_guild_modal/GuildCreateForm.tsx | 313 +------ .../add_guild_modal/GuildCreateShared.tsx | 322 ++++++++ .../add_guild_modal/TemplateImportForm.tsx | 303 +------ .../guild_tabs/GuildAuditLogSentence.tsx | 5 +- .../src/features/i18n/locales/ar/messages.po | 32 +- .../src/features/i18n/locales/bg/messages.po | 32 +- .../src/features/i18n/locales/cs/messages.po | 32 +- .../src/features/i18n/locales/da/messages.po | 32 +- .../src/features/i18n/locales/de/messages.po | 32 +- .../src/features/i18n/locales/el/messages.po | 32 +- .../features/i18n/locales/en-GB/messages.po | 32 +- .../features/i18n/locales/en-US/messages.po | 32 +- .../features/i18n/locales/es-419/messages.po | 32 +- .../features/i18n/locales/es-ES/messages.po | 32 +- .../src/features/i18n/locales/fi/messages.po | 32 +- .../src/features/i18n/locales/fr/messages.po | 32 +- .../src/features/i18n/locales/he/messages.po | 32 +- .../src/features/i18n/locales/hi/messages.po | 32 +- .../src/features/i18n/locales/hr/messages.po | 32 +- .../src/features/i18n/locales/hu/messages.po | 32 +- .../src/features/i18n/locales/id/messages.po | 32 +- .../src/features/i18n/locales/it/messages.po | 32 +- .../src/features/i18n/locales/ja/messages.po | 32 +- .../src/features/i18n/locales/ko/messages.po | 32 +- .../src/features/i18n/locales/lt/messages.po | 32 +- .../src/features/i18n/locales/nl/messages.po | 32 +- .../src/features/i18n/locales/no/messages.po | 32 +- .../src/features/i18n/locales/pl/messages.po | 32 +- .../features/i18n/locales/pt-BR/messages.po | 32 +- .../src/features/i18n/locales/ro/messages.po | 32 +- .../src/features/i18n/locales/ru/messages.po | 32 +- .../features/i18n/locales/sv-SE/messages.po | 32 +- .../src/features/i18n/locales/th/messages.po | 32 +- .../src/features/i18n/locales/tr/messages.po | 32 +- .../src/features/i18n/locales/uk/messages.po | 32 +- .../src/features/i18n/locales/vi/messages.po | 32 +- .../features/i18n/locales/zh-CN/messages.po | 32 +- .../features/i18n/locales/zh-TW/messages.po | 32 +- .../src/features/i18n/utils/IntlCache.ts | 56 ++ .../input/components/KeybindRecorder.tsx | 6 +- .../components/KeybindRecorderCapture.ts | 6 +- .../invite/components/InviteListItem.tsx | 2 +- .../components/pages/InviteRegisterPage.tsx | 5 +- .../composer/nodes/ComposerAtomicTokenNode.ts | 51 ++ .../nodes/ComposerCustomEmojiNode.tsx | 56 +- .../composer/nodes/ComposerMentionNode.tsx | 55 +- .../nodes/ComposerStandardEmojiNode.tsx | 56 +- .../hooks/useTextareaDraftAndTyping.ts | 20 +- .../messaging/state/MessagingDrafts.ts | 2 +- .../features/messaging/state/Spellcheck.ts | 6 +- .../features/messaging/state/Translation.ts | 217 ++--- .../utils/EmoticonConversionUtils.ts | 76 +- .../messaging/utils/MessageCopyTextUtils.ts | 12 +- .../features/messaging/utils/ReactionUtils.ts | 13 +- .../utils/TypedEmojiShortcodeUtils.ts | 103 +-- .../messaging/utils/markdown/Plaintext.ts | 2 +- .../messaging/utils/markdown/UrlSpanUtils.ts | 23 +- .../utils/MessageNotificationPreview.ts | 4 +- .../platform/state/DesktopStoreAccess.ts | 2 +- .../state/LegacyOriginUpgrade.test.ts | 2 +- .../state/LegacyStorageContinuity.test.ts | 2 +- .../src/features/platform/types/Electron.ts | 762 ------------------ .../utils/DesktopModuleRequest.test.ts | 2 +- .../premium/commands/PremiumCommands.ts | 5 +- .../PlutoniumPageComparison.tsx | 2 +- .../read_state/commands/ReadStateCommands.ts | 9 +- .../search/state/ReverseImageSearch.ts | 206 +---- .../src/features/search/state/SearchEngine.ts | 191 +---- .../search/state/UrlTemplateProviderStore.ts | 140 ++++ .../components/pages/ThemeRegisterPage.tsx | 5 +- .../src/features/theme/state/ThemeLibrary.ts | 10 +- .../features/ui/action_menu/ContextMenu.tsx | 2 +- .../features/ui/action_menu/MenuItemRadio.tsx | 11 +- .../features/ui/components/MentionBadge.tsx | 6 +- .../ui/components/form/FormCombobox.tsx | 13 + .../updater/commands/UpdaterModalCommands.tsx | 2 +- .../updater/state/UpdaterStateMachine.ts | 2 +- .../updater/utils/LinuxManualUpdateOptions.ts | 2 +- .../features/user/commands/UserCommands.ts | 25 +- .../modals/CustomStatusBottomSheet.tsx | 15 +- .../components/modals/CustomStatusModal.tsx | 15 +- .../modals/tabs/AccessibilityTab.tsx | 16 +- .../components/modals/tabs/LanguageTab.tsx | 2 +- .../modals/tabs/NotificationsTab.tsx | 124 +-- ...Inline.module.css => TabInline.module.css} | 0 .../AccessibilityTabInline.tsx | 2 +- .../AccountSecurityTabInline.module.css | 6 - .../AccountSecurityTabInline.tsx | 2 +- .../AdvancedAccessibilityControls.tsx | 49 +- .../AdvancedDesktopControls.tsx | 2 +- .../AppearanceTabInline.module.css | 6 - .../appearance_tab/AppearanceTabInline.tsx | 2 +- .../tabs/appearance_tab/MessagesTab.tsx | 16 +- .../modals/tabs/appearance_tab/ScalingTab.tsx | 17 +- .../ChatSettingsTabInline.module.css | 6 - .../ChatSettingsTabInline.tsx | 2 +- .../NotificationsTabInline.module.css | 6 - .../NotificationsTabInline.tsx | 100 --- .../PrivacySafetyTabInline.module.css | 6 - .../PrivacySafetyTabInline.tsx | 2 +- .../VoiceVideoTabInline.module.css | 6 - .../voice_video_tab/VoiceVideoTabInline.tsx | 2 +- .../user/hooks/useUsernameSuggestions.ts | 7 +- .../src/features/user/state/CustomStatus.ts | 14 + .../user/state/SyncedPreferencesEngine.ts | 54 +- .../src/features/user/state/UserSettings.ts | 8 +- .../features/user/utils/DisplayNameUtils.ts | 2 +- .../src/features/user/utils/NicknameUtils.ts | 7 +- .../alerts/VoiceP2pPeerFailedModal.tsx | 4 +- .../DirectCallLobbyBottomSheet.module.css | 157 ---- .../DirectCallLobbyBottomSheet.tsx | 325 +------- .../VoiceLobbyBottomSheet.module.css | 157 ---- .../bottomsheets/VoiceLobbyBottomSheet.tsx | 316 +------- .../bottomsheets/VoiceLobbyShared.module.css | 158 ++++ .../bottomsheets/VoiceLobbyShared.tsx | 329 ++++++++ .../components/MediaVerticalVolumeControl.tsx | 30 +- .../components/MediaVolumeControl.tsx | 2 +- .../voice/components/useStatsForNerds.ts | 2 +- .../components/useVoiceCallAppFullscreen.ts | 24 +- .../voice/diagnostics/VoiceDebugStatsHtml.tsx | 2 +- .../engine/PublicationSubscriptionManager.ts | 171 ++++ .../engine/ScreenShareSubscriptionManager.ts | 174 +--- .../voice/engine/VideoSubscriptionManager.ts | 172 +--- ...VoiceEngineV2AppSystemPermissionAdapter.ts | 2 +- .../voice_screen_share_manager/shared.ts | 13 +- .../utils/ScreenShareCodecDiagnostics.ts | 2 +- .../utils/VoiceStatsForNerdsPresenter.ts | 2 +- .../WebCameraEffectCustomFrameSource.ts | 4 +- .../WebCameraEffectCustomImage.ts | 2 +- .../WebCameraEffectVideoFrameSource.ts | 37 +- .../WebCameraEffectWebGPURenderer.ts | 15 +- .../camera-effects/WebSelfieSegmenter.ts | 2 +- .../window/hooks/useFluxerDocumentTitle.ts | 2 +- fluxer_app/src/types/electron.d.ts | 12 +- fluxer_app_proxy/Dockerfile | 4 - pnpm-lock.yaml | 21 - 202 files changed, 2924 insertions(+), 6532 deletions(-) delete mode 100644 fluxer_app/pkgs/list_utils/package.json delete mode 100644 fluxer_app/pkgs/list_utils/src/ListFormatting.ts delete mode 100644 fluxer_app/pkgs/list_utils/src/ListFormattingCache.ts delete mode 100644 fluxer_app/pkgs/list_utils/src/ListFormattingFallback.ts delete mode 100644 fluxer_app/pkgs/list_utils/src/ListFormattingSupport.ts delete mode 100644 fluxer_app/pkgs/list_utils/src/ListFormattingTypes.ts delete mode 100644 fluxer_app/pkgs/list_utils/tsconfig.json delete mode 100644 fluxer_app/pkgs/number_utils/package.json delete mode 100644 fluxer_app/pkgs/number_utils/src/NumberConstants.ts delete mode 100644 fluxer_app/pkgs/number_utils/src/NumberFormatterCache.ts delete mode 100644 fluxer_app/pkgs/number_utils/src/NumberFormatting.ts delete mode 100644 fluxer_app/pkgs/number_utils/src/NumberParsing.ts delete mode 100644 fluxer_app/pkgs/number_utils/src/NumberTypes.ts delete mode 100644 fluxer_app/pkgs/number_utils/tsconfig.json delete mode 100644 fluxer_app/src/features/app/components/layout/sidebar_nav/DownloadButton.module.css rename fluxer_app/src/features/app/components/layout/sidebar_nav/{AddGuildButton.module.css => GuildListActionButton.module.css} (100%) create mode 100644 fluxer_app/src/features/app/components/layout/sidebar_nav/GuildListActionButton.tsx delete mode 100644 fluxer_app/src/features/app/components/layout/sidebar_nav/HelpButton.module.css delete mode 100644 fluxer_app/src/features/auth/flow/AuthMinimalRegisterFormCore.tsx create mode 100644 fluxer_app/src/features/channel/components/modals/useEditGroupForm.tsx create mode 100644 fluxer_app/src/features/channel/components/shared/usePickerGridKeyboard.ts delete mode 100644 fluxer_app/src/features/channel/hooks/useChannelMemberListVisibility.ts rename fluxer_app/src/features/channel/hooks/{useChannelSearchVisibility.ts => useChannelPanelVisibility.ts} (85%) create mode 100644 fluxer_app/src/features/guild/components/modals/add_guild_modal/GuildCreateShared.tsx create mode 100644 fluxer_app/src/features/lexical/composer/nodes/ComposerAtomicTokenNode.ts delete mode 100644 fluxer_app/src/features/platform/types/Electron.ts create mode 100644 fluxer_app/src/features/search/state/UrlTemplateProviderStore.ts rename fluxer_app/src/features/user/components/modals/tabs/{accessibility_tab/AccessibilityTabInline.module.css => TabInline.module.css} (100%) delete mode 100644 fluxer_app/src/features/user/components/modals/tabs/account_security_tab/AccountSecurityTabInline.module.css delete mode 100644 fluxer_app/src/features/user/components/modals/tabs/appearance_tab/AppearanceTabInline.module.css delete mode 100644 fluxer_app/src/features/user/components/modals/tabs/chat_settings_tab/ChatSettingsTabInline.module.css delete mode 100644 fluxer_app/src/features/user/components/modals/tabs/notifications_tab/NotificationsTabInline.module.css delete mode 100644 fluxer_app/src/features/user/components/modals/tabs/notifications_tab/NotificationsTabInline.tsx delete mode 100644 fluxer_app/src/features/user/components/modals/tabs/privacy_safety_tab/PrivacySafetyTabInline.module.css delete mode 100644 fluxer_app/src/features/user/components/modals/tabs/voice_video_tab/VoiceVideoTabInline.module.css create mode 100644 fluxer_app/src/features/voice/components/bottomsheets/VoiceLobbyShared.module.css create mode 100644 fluxer_app/src/features/voice/components/bottomsheets/VoiceLobbyShared.tsx create mode 100644 fluxer_app/src/features/voice/engine/PublicationSubscriptionManager.ts diff --git a/fluxer_app/package.json b/fluxer_app/package.json index 23fb4d1bd..b5108ae0e 100644 --- a/fluxer_app/package.json +++ b/fluxer_app/package.json @@ -190,8 +190,6 @@ "@lingui/react": "catalog:", "@livekit/components-react": "catalog:", "@phosphor-icons/react": "catalog:", - "@pkgs/list_utils": "workspace:*", - "@pkgs/number_utils": "workspace:*", "@radix-ui/react-checkbox": "catalog:", "@radix-ui/react-radio-group": "catalog:", "@radix-ui/react-switch": "catalog:", @@ -239,7 +237,6 @@ "rxjs": "catalog:", "thumbhash": "catalog:", "tiny-invariant": "catalog:", - "uint8array-extras": "catalog:", "unique-names-generator": "catalog:", "upng-js": "^2.1.0", "urlpattern-polyfill": "catalog:", diff --git a/fluxer_app/pkgs/list_utils/package.json b/fluxer_app/pkgs/list_utils/package.json deleted file mode 100644 index 8d4456f01..000000000 --- a/fluxer_app/pkgs/list_utils/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "@pkgs/list_utils", - "version": "0.0.0", - "type": "module", - "exports": { - "./src/*": "./src/*", - "./*": "./*" - }, - "dependencies": { - "@fluxer/constants": "workspace:*" - } -} diff --git a/fluxer_app/pkgs/list_utils/src/ListFormatting.ts b/fluxer_app/pkgs/list_utils/src/ListFormatting.ts deleted file mode 100644 index cda3f69ec..000000000 --- a/fluxer_app/pkgs/list_utils/src/ListFormatting.ts +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {Locales} from '@fluxer/constants/src/Locales'; -import {getCachedListFormatter} from '@pkgs/list_utils/src/ListFormattingCache'; -import {formatListWithFallback} from '@pkgs/list_utils/src/ListFormattingFallback'; -import {isIntlListFormatLocaleSupported, isIntlListFormatSupported} from '@pkgs/list_utils/src/ListFormattingSupport'; -import type { - ListFormatStyle, - ListFormatType, - ListFormatterConfig, - ResolvedListFormatterConfig, -} from '@pkgs/list_utils/src/ListFormattingTypes'; - -const DEFAULT_LOCALE = Locales.EN_US; -const DEFAULT_LIST_FORMAT_STYLE: ListFormatStyle = 'long'; -const DEFAULT_LIST_FORMAT_TYPE: ListFormatType = 'conjunction'; - -function normalizeLocale(locale: string): string { - if (!isIntlListFormatLocaleSupported(locale)) { - return DEFAULT_LOCALE; - } - return locale; -} - -function resolveListFormatterConfig(config: ListFormatterConfig): ResolvedListFormatterConfig { - const requestedLocale = config.locale?.trim(); - const locale = requestedLocale == null || requestedLocale === '' ? DEFAULT_LOCALE : normalizeLocale(requestedLocale); - const style = config.style ?? DEFAULT_LIST_FORMAT_STYLE; - const type = config.type ?? DEFAULT_LIST_FORMAT_TYPE; - return { - locale, - style, - type, - }; -} - -function formatWithIntl(items: ReadonlyArray, config: ResolvedListFormatterConfig): string { - try { - return getCachedListFormatter(config).format(items); - } catch { - return formatListWithFallback(items, config.type); - } -} - -function formatItems(items: ReadonlyArray, config: ResolvedListFormatterConfig): string { - if (items.length === 0) { - return ''; - } - if (items.length === 1) { - return items[0] ?? ''; - } - if (!isIntlListFormatSupported()) { - return formatListWithFallback(items, config.type); - } - return formatWithIntl(items, config); -} - -export function formatListWithConfig(items: ReadonlyArray, config: ListFormatterConfig = {}): string { - const resolvedConfig = resolveListFormatterConfig(config); - return formatItems(items, resolvedConfig); -} diff --git a/fluxer_app/pkgs/list_utils/src/ListFormattingCache.ts b/fluxer_app/pkgs/list_utils/src/ListFormattingCache.ts deleted file mode 100644 index 48eaf228b..000000000 --- a/fluxer_app/pkgs/list_utils/src/ListFormattingCache.ts +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {ResolvedListFormatterConfig} from '@pkgs/list_utils/src/ListFormattingTypes'; - -const formatterCache = new Map(); - -export function getCachedListFormatter(config: ResolvedListFormatterConfig): Intl.ListFormat { - const cacheKey = `${config.locale}:${config.style}:${config.type}`; - const cachedFormatter = formatterCache.get(cacheKey); - if (cachedFormatter != null) { - return cachedFormatter; - } - const formatter = new Intl.ListFormat(config.locale, { - style: config.style, - type: config.type, - }); - formatterCache.set(cacheKey, formatter); - return formatter; -} diff --git a/fluxer_app/pkgs/list_utils/src/ListFormattingFallback.ts b/fluxer_app/pkgs/list_utils/src/ListFormattingFallback.ts deleted file mode 100644 index b85ab6909..000000000 --- a/fluxer_app/pkgs/list_utils/src/ListFormattingFallback.ts +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {ListFormatType} from '@pkgs/list_utils/src/ListFormattingTypes'; - -function getFallbackConjunction(type: ListFormatType): string { - if (type === 'disjunction') { - return 'or'; - } - return 'and'; -} - -export function formatListWithFallback(items: ReadonlyArray, type: ListFormatType): string { - if (items.length === 0) { - return ''; - } - if (items.length === 1) { - return items[0] ?? ''; - } - if (type === 'unit') { - return items.join(', '); - } - const conjunction = getFallbackConjunction(type); - if (items.length === 2) { - return `${items[0]} ${conjunction} ${items[1]}`; - } - const lastItem = items[items.length - 1]; - const leadingItems = items.slice(0, -1).join(', '); - return `${leadingItems}, ${conjunction} ${lastItem}`; -} diff --git a/fluxer_app/pkgs/list_utils/src/ListFormattingSupport.ts b/fluxer_app/pkgs/list_utils/src/ListFormattingSupport.ts deleted file mode 100644 index 600b8c2bb..000000000 --- a/fluxer_app/pkgs/list_utils/src/ListFormattingSupport.ts +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -export function isIntlListFormatSupported(): boolean { - return typeof Intl !== 'undefined' && typeof Intl.ListFormat !== 'undefined'; -} - -export function isIntlListFormatLocaleSupported(locale: string): boolean { - if (!isIntlListFormatSupported()) { - return false; - } - try { - return Intl.ListFormat.supportedLocalesOf([locale]).length > 0; - } catch { - return false; - } -} diff --git a/fluxer_app/pkgs/list_utils/src/ListFormattingTypes.ts b/fluxer_app/pkgs/list_utils/src/ListFormattingTypes.ts deleted file mode 100644 index 9efebaeae..000000000 --- a/fluxer_app/pkgs/list_utils/src/ListFormattingTypes.ts +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -export type ListFormatStyle = 'long' | 'short' | 'narrow'; -export type ListFormatType = 'conjunction' | 'disjunction' | 'unit'; - -export interface ListFormatOptions { - style?: ListFormatStyle; - type?: ListFormatType; -} - -export interface ListFormatterConfig extends ListFormatOptions { - locale?: string; -} - -export interface ResolvedListFormatterConfig { - locale: string; - style: ListFormatStyle; - type: ListFormatType; -} diff --git a/fluxer_app/pkgs/list_utils/tsconfig.json b/fluxer_app/pkgs/list_utils/tsconfig.json deleted file mode 100644 index bfec7ccc9..000000000 --- a/fluxer_app/pkgs/list_utils/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../../tsconfigs/package.json", - "compilerOptions": { - "paths": { - "@fluxer/*": ["../../../packages/*", "../../../packages/*/src/index.ts"], - "@pkgs/*": ["../*"] - } - } -} diff --git a/fluxer_app/pkgs/number_utils/package.json b/fluxer_app/pkgs/number_utils/package.json deleted file mode 100644 index e3c5c3bfc..000000000 --- a/fluxer_app/pkgs/number_utils/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "@pkgs/number_utils", - "version": "0.0.0", - "type": "module", - "dependencies": { - "@fluxer/constants": "workspace:*" - }, - "exports": { - "./src/*": "./src/*", - "./*": "./*" - } -} diff --git a/fluxer_app/pkgs/number_utils/src/NumberConstants.ts b/fluxer_app/pkgs/number_utils/src/NumberConstants.ts deleted file mode 100644 index f2773ef44..000000000 --- a/fluxer_app/pkgs/number_utils/src/NumberConstants.ts +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {Locales} from '@fluxer/constants/src/Locales'; - -export const DEFAULT_COMPACT_MAX_FRACTION_DIGITS = 1; -export const DEFAULT_NUMBER_FALLBACK = 0; -export const DEFAULT_NUMBER_LOCALE = Locales.EN_US; diff --git a/fluxer_app/pkgs/number_utils/src/NumberFormatterCache.ts b/fluxer_app/pkgs/number_utils/src/NumberFormatterCache.ts deleted file mode 100644 index 9facfb278..000000000 --- a/fluxer_app/pkgs/number_utils/src/NumberFormatterCache.ts +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -const numberFormatterCache = new Map(); - -export function getNumberFormatter( - locale: string, - numberFormatOptions: Intl.NumberFormatOptions = {}, -): Intl.NumberFormat { - const cacheKey = buildFormatterCacheKey(locale, numberFormatOptions); - const cachedFormatter = numberFormatterCache.get(cacheKey); - if (cachedFormatter !== undefined) { - return cachedFormatter; - } - const formatter = new Intl.NumberFormat(locale, numberFormatOptions); - numberFormatterCache.set(cacheKey, formatter); - return formatter; -} - -function buildFormatterCacheKey(locale: string, numberFormatOptions: Intl.NumberFormatOptions): string { - const optionEntries = Object.entries(numberFormatOptions) - .filter(([, value]) => value !== undefined) - .sort(([left], [right]) => left.localeCompare(right)); - const optionKey = optionEntries.map(([key, value]) => `${key}:${String(value)}`).join('|'); - return `${locale}|${optionKey}`; -} diff --git a/fluxer_app/pkgs/number_utils/src/NumberFormatting.ts b/fluxer_app/pkgs/number_utils/src/NumberFormatting.ts deleted file mode 100644 index 66859597a..000000000 --- a/fluxer_app/pkgs/number_utils/src/NumberFormatting.ts +++ /dev/null @@ -1,104 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import { - DEFAULT_COMPACT_MAX_FRACTION_DIGITS, - DEFAULT_NUMBER_FALLBACK, - DEFAULT_NUMBER_LOCALE, -} from '@pkgs/number_utils/src/NumberConstants'; -import {getNumberFormatter} from '@pkgs/number_utils/src/NumberFormatterCache'; -import {parseNumberInput} from '@pkgs/number_utils/src/NumberParsing'; -import type { - CompactNumberFormatOptions, - NumberFormatBaseOptions, - NumberFormatOptions, - NumberInput, -} from '@pkgs/number_utils/src/NumberTypes'; - -interface ResolvedNumberFormatBaseOptions { - locale: string; - fallbackValue: number; -} - -interface CompactFormatOptionsInput { - maximumFractionDigits?: number; - minimumFractionDigits?: number; -} - -function resolveBaseOptions(options?: NumberFormatBaseOptions): ResolvedNumberFormatBaseOptions { - return { - locale: options?.locale ?? DEFAULT_NUMBER_LOCALE, - fallbackValue: options?.fallbackValue ?? DEFAULT_NUMBER_FALLBACK, - }; -} - -function resolveNumberFormatOptions(optionsOrLocale: NumberFormatOptions | string | undefined): NumberFormatOptions { - if (typeof optionsOrLocale === 'string') { - return {locale: optionsOrLocale}; - } - return optionsOrLocale ?? {}; -} - -function resolveCompactFormatOptions( - optionsOrLocale: CompactNumberFormatOptions | string | undefined, - maximumFractionDigits: number | undefined, -): CompactNumberFormatOptions { - if (typeof optionsOrLocale === 'string') { - return { - locale: optionsOrLocale, - maximumFractionDigits, - }; - } - if (optionsOrLocale === undefined) { - if (maximumFractionDigits === undefined) { - return {}; - } - return {maximumFractionDigits}; - } - if (maximumFractionDigits === undefined) { - return optionsOrLocale; - } - return { - ...optionsOrLocale, - maximumFractionDigits: optionsOrLocale.maximumFractionDigits ?? maximumFractionDigits, - }; -} - -function formatNumberValue( - value: NumberInput, - resolvedOptions: ResolvedNumberFormatBaseOptions, - numberFormatOptions: Intl.NumberFormatOptions = {}, -): string { - const parsedValue = parseNumberInput(value, resolvedOptions.fallbackValue); - return getNumberFormatter(resolvedOptions.locale, numberFormatOptions).format(parsedValue); -} - -function buildCompactFormatOptions(options: CompactFormatOptionsInput): Intl.NumberFormatOptions { - const numberFormatOptions: Intl.NumberFormatOptions = { - notation: 'compact', - maximumFractionDigits: options.maximumFractionDigits ?? DEFAULT_COMPACT_MAX_FRACTION_DIGITS, - }; - if (options.minimumFractionDigits !== undefined) { - numberFormatOptions.minimumFractionDigits = options.minimumFractionDigits; - } - return numberFormatOptions; -} - -export function formatNumber(value: NumberInput, locale?: string): string; -export function formatNumber(value: NumberInput, options?: NumberFormatOptions): string; -export function formatNumber(value: NumberInput, optionsOrLocale: NumberFormatOptions | string = {}): string { - const options = resolveNumberFormatOptions(optionsOrLocale); - const resolvedOptions = resolveBaseOptions(options); - return formatNumberValue(value, resolvedOptions, options.numberFormatOptions); -} - -export function formatCompactNumber(value: NumberInput, locale?: string, maximumFractionDigits?: number): string; -export function formatCompactNumber(value: NumberInput, options?: CompactNumberFormatOptions): string; -export function formatCompactNumber( - value: NumberInput, - optionsOrLocale: CompactNumberFormatOptions | string = {}, - maximumFractionDigits?: number, -): string { - const options = resolveCompactFormatOptions(optionsOrLocale, maximumFractionDigits); - const resolvedOptions = resolveBaseOptions(options); - return formatNumberValue(value, resolvedOptions, buildCompactFormatOptions(options)); -} diff --git a/fluxer_app/pkgs/number_utils/src/NumberParsing.ts b/fluxer_app/pkgs/number_utils/src/NumberParsing.ts deleted file mode 100644 index a9743a9ab..000000000 --- a/fluxer_app/pkgs/number_utils/src/NumberParsing.ts +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {DEFAULT_NUMBER_FALLBACK} from '@pkgs/number_utils/src/NumberConstants'; -import type {NumberInput} from '@pkgs/number_utils/src/NumberTypes'; - -export function parseNumberInput(value: NumberInput, fallbackValue: number = DEFAULT_NUMBER_FALLBACK): number { - if (typeof value === 'number') { - if (Number.isFinite(value)) { - return value; - } - return fallbackValue; - } - if (typeof value === 'string') { - const trimmedValue = value.trim(); - if (trimmedValue === '') { - return fallbackValue; - } - const parsedValue = Number(trimmedValue); - if (Number.isFinite(parsedValue)) { - return parsedValue; - } - } - return fallbackValue; -} diff --git a/fluxer_app/pkgs/number_utils/src/NumberTypes.ts b/fluxer_app/pkgs/number_utils/src/NumberTypes.ts deleted file mode 100644 index 12c425b6a..000000000 --- a/fluxer_app/pkgs/number_utils/src/NumberTypes.ts +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -export type NumberInput = number | string | null | undefined; - -export interface NumberFormatBaseOptions { - locale?: string; - fallbackValue?: number; -} - -export interface NumberFormatOptions extends NumberFormatBaseOptions { - numberFormatOptions?: Intl.NumberFormatOptions; -} - -export interface CompactNumberFormatOptions extends NumberFormatBaseOptions { - maximumFractionDigits?: number; - minimumFractionDigits?: number; -} diff --git a/fluxer_app/pkgs/number_utils/tsconfig.json b/fluxer_app/pkgs/number_utils/tsconfig.json deleted file mode 100644 index bfec7ccc9..000000000 --- a/fluxer_app/pkgs/number_utils/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../../tsconfigs/package.json", - "compilerOptions": { - "paths": { - "@fluxer/*": ["../../../packages/*", "../../../packages/*/src/index.ts"], - "@pkgs/*": ["../*"] - } - } -} diff --git a/fluxer_app/src/env.d.ts b/fluxer_app/src/env.d.ts index b5709013c..0e772361e 100644 --- a/fluxer_app/src/env.d.ts +++ b/fluxer_app/src/env.d.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import MediaEngineFacade from '@app/features/voice/engine/MediaEngineFacade'; -import type {ElectronAPI} from '@app/features/platform/types/Electron'; +import type {ElectronAPI} from '@app/types/electron.d'; import {Buffer} from 'buffer'; type MediaEngineInstance = typeof MediaEngineFacade; diff --git a/fluxer_app/src/features/accessibility/state/Accessibility.ts b/fluxer_app/src/features/accessibility/state/Accessibility.ts index 9b405497f..94234e09a 100644 --- a/fluxer_app/src/features/accessibility/state/Accessibility.ts +++ b/fluxer_app/src/features/accessibility/state/Accessibility.ts @@ -22,8 +22,8 @@ import {initializeStore} from '@app/features/platform/utils/StoreInitialization' import MobileLayout from '@app/features/ui/state/MobileLayout'; import {applyAppZoomToDocument} from '@app/features/ui/utils/AppZoomUtils'; import {makeSyncedField} from '@app/features/user/state/SyncedField'; -import {decodeSyncedPreferencesLenient} from '@app/features/user/state/SyncedPreferencesEngine'; import {StickerAnimationOptions} from '@fluxer/constants/src/UserConstants'; +import {decodeSyncedPreferencesLenient} from '@fluxer/schema/src/domains/user/SyncedPreferencesCodec'; import { AccessibilitySettingsSchema, ChannelTypingIndicatorMode as ProtoChannelTypingIndicatorMode, diff --git a/fluxer_app/src/features/app/components/dialogs/components/AllSettingsRenderer.tsx b/fluxer_app/src/features/app/components/dialogs/components/AllSettingsRenderer.tsx index 00cc766b1..e1bf2dea0 100644 --- a/fluxer_app/src/features/app/components/dialogs/components/AllSettingsRenderer.tsx +++ b/fluxer_app/src/features/app/components/dialogs/components/AllSettingsRenderer.tsx @@ -19,7 +19,7 @@ import KeybindsTab from '@app/features/user/components/modals/tabs/KeybindsTab'; import LanguageTab from '@app/features/user/components/modals/tabs/LanguageTab'; import LinkedAccountsTab from '@app/features/user/components/modals/tabs/LinkedAccountsTab'; import MyProfileTab from '@app/features/user/components/modals/tabs/MyProfileTab'; -import {NotificationsInlineContent} from '@app/features/user/components/modals/tabs/notifications_tab/NotificationsTabInline'; +import {NotificationsInlineContent} from '@app/features/user/components/modals/tabs/NotificationsTab'; import {PlutoniumInlineTab} from '@app/features/user/components/modals/tabs/PlutoniumTab'; import {PrivacyDashboardContent} from '@app/features/user/components/modals/tabs/privacy_safety_tab/PrivacySafetyTabInline'; import {VoiceVideoInlineContent} from '@app/features/user/components/modals/tabs/voice_video_tab/VoiceVideoTabInline'; diff --git a/fluxer_app/src/features/app/components/dialogs/components/FeatureComparisonTable.tsx b/fluxer_app/src/features/app/components/dialogs/components/FeatureComparisonTable.tsx index 304df825a..998c12091 100644 --- a/fluxer_app/src/features/app/components/dialogs/components/FeatureComparisonTable.tsx +++ b/fluxer_app/src/features/app/components/dialogs/components/FeatureComparisonTable.tsx @@ -6,6 +6,7 @@ import styles from '@app/features/app/components/dialogs/components/FeatureCompa import RuntimeConfig from '@app/features/app/state/RuntimeConfig'; import {Limits} from '@app/features/app/utils/UserLimits'; import {COMMUNITIES_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors'; +import {formatNumber} from '@app/features/i18n/utils/IntlCache'; import {formatFileSize} from '@app/features/messaging/utils/FileUtils'; import {getPremiumProductName} from '@app/features/premium/utils/PremiumUtils'; import { @@ -17,7 +18,6 @@ import { } from '@fluxer/constants/src/LimitTierPerks'; import {msg} from '@lingui/core/macro'; import {Trans, useLingui} from '@lingui/react/macro'; -import {formatNumber} from '@pkgs/number_utils/src/NumberFormatting'; import {observer} from 'mobx-react-lite'; import {useMemo} from 'react'; diff --git a/fluxer_app/src/features/app/components/dialogs/components/MobileGuildSettingsView.tsx b/fluxer_app/src/features/app/components/dialogs/components/MobileGuildSettingsView.tsx index c5426a2e0..4b3543830 100644 --- a/fluxer_app/src/features/app/components/dialogs/components/MobileGuildSettingsView.tsx +++ b/fluxer_app/src/features/app/components/dialogs/components/MobileGuildSettingsView.tsx @@ -8,13 +8,14 @@ import { MobileSettingsDangerItem, MobileSettingsList, } from '@app/features/app/components/dialogs/shared/MobileSettingsComponents'; +import {usePreservedScrollerPosition} from '@app/features/app/components/dialogs/shared/UsePreservedScrollerPosition'; import Authentication from '@app/features/auth/state/Authentication'; import {GuildDeleteModal} from '@app/features/guild/components/modals/GuildDeleteModal'; import type {Guild} from '@app/features/guild/models/Guild'; import {isStockCommunityGuild} from '@app/features/guild/utils/GuildCommunityUtils'; import * as ModalCommands from '@app/features/ui/commands/ModalCommands'; import {modal} from '@app/features/ui/commands/ModalCommands'; -import {Scroller, type ScrollerHandle} from '@app/features/ui/components/Scroller'; +import {Scroller} from '@app/features/ui/components/Scroller'; import userSettingsStyles from '@app/features/user/components/modals/UserSettingsModal.module.css'; import type { GuildSettingsTab, @@ -30,7 +31,7 @@ import {TrashIcon} from '@phosphor-icons/react'; import {AnimatePresence, motion} from 'framer-motion'; import {observer} from 'mobx-react-lite'; import type React from 'react'; -import {type UIEvent, useCallback, useEffect, useRef} from 'react'; +import {useCallback} from 'react'; const DELETE_COMMUNITY_DESCRIPTOR = msg({ message: 'Delete community', @@ -104,19 +105,7 @@ export const MobileGuildSettingsView: React.FC = o ); const showMobileList = mobileNav.isRootView; const showMobileContent = !mobileNav.isRootView; - const listScrollPositionRef = useRef(0); - const listScrollerRef = useRef(null); - const handleListScroll = useCallback((event: UIEvent) => { - listScrollPositionRef.current = event.currentTarget.scrollTop; - }, []); - useEffect(() => { - if (!showMobileList) return; - const scroller = listScrollerRef.current; - if (!scroller) return; - const target = listScrollPositionRef.current; - if (target === 0) return; - scroller.scrollTo({to: target, animate: false}); - }, [showMobileList]); + const {scrollerRef: listScrollerRef, handleScroll: handleListScroll} = usePreservedScrollerPosition(showMobileList); const dangerAction = guild.isOwner(Authentication.currentUserId) && !isStockCommunityGuild(guild.id) ? ( { const {i18n} = useLingui(); - const [hoverRef, isHovering] = useHover(); - const buttonRef = useRef(null); - const iconRef = useRef(null); - const itemRef = useRef(null); - const contextMenuOpen = useContextMenuHoverState(itemRef); - const mergedButtonRef = useMergeRefs([hoverRef, buttonRef, itemRef]); const buttonLabel = i18n._(CREATE_OR_JOIN_A_COMMUNITY_DESCRIPTOR); const handleAddGuild = (view?: AddGuildModalView) => { ModalCommands.push( @@ -91,61 +76,27 @@ export const AddGuildButton = observer(() => { )); }; - const shouldShowHoverState = isHovering || contextMenuOpen; if (RuntimeConfig.singleCommunityEnabled) { return null; } return ( -
- ( - - )} - data-flx="app.sidebar-nav.add-guild-button.tooltip" - > - - - - -
+ ( + + )} + icon={ + + } + onClick={() => handleAddGuild()} + onContextMenu={handleContextMenu} + hasDialogPopup + buttonDataFlx="app.sidebar-nav.add-guild-button.button.add-guild" + data-flx="app.sidebar-nav.add-guild-button" + /> ); }); diff --git a/fluxer_app/src/features/app/components/layout/sidebar_nav/DownloadButton.module.css b/fluxer_app/src/features/app/components/layout/sidebar_nav/DownloadButton.module.css deleted file mode 100644 index 79393dbb0..000000000 --- a/fluxer_app/src/features/app/components/layout/sidebar_nav/DownloadButton.module.css +++ /dev/null @@ -1,22 +0,0 @@ -/* SPDX-License-Identifier: AGPL-3.0-or-later */ - -.button { - position: relative; - display: flex; - width: 100%; - justify-content: center; - border: none; - background-color: transparent; - padding: 0; -} - -.iconText { - height: 1.25rem; - width: 1.25rem; - color: var(--text-primary); -} - -.menuIcon { - height: 1rem; - width: 1rem; -} diff --git a/fluxer_app/src/features/app/components/layout/sidebar_nav/DownloadButton.tsx b/fluxer_app/src/features/app/components/layout/sidebar_nav/DownloadButton.tsx index 3fbf66877..ade74a032 100644 --- a/fluxer_app/src/features/app/components/layout/sidebar_nav/DownloadButton.tsx +++ b/fluxer_app/src/features/app/components/layout/sidebar_nav/DownloadButton.tsx @@ -1,28 +1,19 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import Accessibility from '@app/features/accessibility/state/Accessibility'; -import guildStyles from '@app/features/app/components/layout/GuildsLayout.module.css'; -import styles from '@app/features/app/components/layout/sidebar_nav/DownloadButton.module.css'; +import {GuildListActionButton} from '@app/features/app/components/layout/sidebar_nav/GuildListActionButton'; +import styles from '@app/features/app/components/layout/sidebar_nav/GuildListActionButton.module.css'; import {DESKTOP_DOWNLOAD_URL} from '@app/features/app/config/I18nDisplayConstants'; -import {useContextMenuHoverState} from '@app/features/app/hooks/useContextMenuHoverState'; -import {useHover} from '@app/features/app/hooks/useHover'; -import {useMergeRefs} from '@app/features/app/hooks/useMergeRefs'; import RuntimeConfig from '@app/features/app/state/RuntimeConfig'; import HiddenGuildListButtons from '@app/features/guild/state/HiddenGuildListButtons'; import {openExternalUrlWithWarning} from '@app/features/messaging/utils/ExternalLinkUtils'; import {MenuGroup} from '@app/features/ui/action_menu/MenuGroup'; import {MenuItem} from '@app/features/ui/action_menu/MenuItem'; import * as ContextMenuCommands from '@app/features/ui/commands/ContextMenuCommands'; -import FocusRing from '@app/features/ui/focus_ring/FocusRing'; -import {Tooltip} from '@app/features/ui/tooltip/Tooltip'; import {msg} from '@lingui/core/macro'; import {Trans, useLingui} from '@lingui/react/macro'; import {DownloadSimpleIcon, EyeSlashIcon} from '@phosphor-icons/react'; -import {clsx} from 'clsx'; -import {motion} from 'framer-motion'; import {observer} from 'mobx-react-lite'; import type React from 'react'; -import {useRef} from 'react'; const DOWNLOAD_DESCRIPTOR = msg({ message: 'Download {productName}', @@ -30,12 +21,6 @@ const DOWNLOAD_DESCRIPTOR = msg({ }); export const DownloadButton = observer(() => { const {i18n} = useLingui(); - const [hoverRef, isHovering] = useHover(); - const buttonRef = useRef(null); - const iconRef = useRef(null); - const itemRef = useRef(null); - const contextMenuOpen = useContextMenuHoverState(itemRef); - const mergedButtonRef = useMergeRefs([hoverRef, buttonRef, itemRef]); if (HiddenGuildListButtons.downloadButtonHidden) { return null; } @@ -65,51 +50,21 @@ export const DownloadButton = observer(() => { )); }; - const shouldShowHoverState = isHovering || contextMenuOpen; return ( -
- i18n._(DOWNLOAD_DESCRIPTOR, {productName: RuntimeConfig.productName})} - data-flx="app.sidebar-nav.download-button.tooltip" - > - - - - -
+ i18n._(DOWNLOAD_DESCRIPTOR, {productName: RuntimeConfig.productName})} + icon={ + + } + onClick={handleDownload} + onContextMenu={handleContextMenu} + buttonDataFlx="app.sidebar-nav.download-button.button.download" + data-flx="app.sidebar-nav.download-button" + /> ); }); diff --git a/fluxer_app/src/features/app/components/layout/sidebar_nav/AddGuildButton.module.css b/fluxer_app/src/features/app/components/layout/sidebar_nav/GuildListActionButton.module.css similarity index 100% rename from fluxer_app/src/features/app/components/layout/sidebar_nav/AddGuildButton.module.css rename to fluxer_app/src/features/app/components/layout/sidebar_nav/GuildListActionButton.module.css diff --git a/fluxer_app/src/features/app/components/layout/sidebar_nav/GuildListActionButton.tsx b/fluxer_app/src/features/app/components/layout/sidebar_nav/GuildListActionButton.tsx new file mode 100644 index 000000000..7bfcdd1a8 --- /dev/null +++ b/fluxer_app/src/features/app/components/layout/sidebar_nav/GuildListActionButton.tsx @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import Accessibility from '@app/features/accessibility/state/Accessibility'; +import guildStyles from '@app/features/app/components/layout/GuildsLayout.module.css'; +import styles from '@app/features/app/components/layout/sidebar_nav/GuildListActionButton.module.css'; +import {useContextMenuHoverState} from '@app/features/app/hooks/useContextMenuHoverState'; +import {useHover} from '@app/features/app/hooks/useHover'; +import {useMergeRefs} from '@app/features/app/hooks/useMergeRefs'; +import FocusRing from '@app/features/ui/focus_ring/FocusRing'; +import {Tooltip} from '@app/features/ui/tooltip/Tooltip'; +import {clsx} from 'clsx'; +import {motion} from 'framer-motion'; +import {observer} from 'mobx-react-lite'; +import type React from 'react'; +import {useRef} from 'react'; + +interface GuildListActionButtonProps { + label: string; + tooltip: () => React.ReactNode; + icon: React.ReactNode; + onClick: () => void; + onContextMenu: (event: React.MouseEvent) => void; + hasDialogPopup?: boolean; + buttonDataFlx: string; + 'data-flx': string; +} + +export const GuildListActionButton = observer( + ({ + label, + tooltip, + icon, + onClick, + onContextMenu, + hasDialogPopup = false, + buttonDataFlx, + 'data-flx': dataFlx, + }: GuildListActionButtonProps) => { + const [hoverRef, isHovering] = useHover(); + const buttonRef = useRef(null); + const iconRef = useRef(null); + const itemRef = useRef(null); + const contextMenuOpen = useContextMenuHoverState(itemRef); + const mergedButtonRef = useMergeRefs([hoverRef, buttonRef, itemRef]); + const shouldShowHoverState = isHovering || contextMenuOpen; + return ( +
+ + + + + +
+ ); + }, +); diff --git a/fluxer_app/src/features/app/components/layout/sidebar_nav/GuildListItemTooltip.tsx b/fluxer_app/src/features/app/components/layout/sidebar_nav/GuildListItemTooltip.tsx index 21b6f703c..fb4d7b944 100644 --- a/fluxer_app/src/features/app/components/layout/sidebar_nav/GuildListItemTooltip.tsx +++ b/fluxer_app/src/features/app/components/layout/sidebar_nav/GuildListItemTooltip.tsx @@ -6,6 +6,7 @@ import type {SidebarVoiceRow} from '@app/features/app/hooks/useSidebarVoiceSumma import {GuildBadge} from '@app/features/guild/components/GuildBadge'; import type {Guild} from '@app/features/guild/models/Guild'; import type {GuildCounts} from '@app/features/guild/state/GuildCount'; +import {formatNumber} from '@app/features/i18n/utils/IntlCache'; import type {KeyCombo} from '@app/features/input/state/InputKeybind'; import {AvatarStack} from '@app/features/ui/avatars/AvatarStack'; import {KeybindHint} from '@app/features/ui/keybind_hint/KeybindHint'; @@ -14,7 +15,6 @@ import {GuildFeatures} from '@fluxer/constants/src/GuildConstants'; import {msg} from '@lingui/core/macro'; import {Trans, useLingui} from '@lingui/react/macro'; import {BellSlashIcon, MonitorPlayIcon, SpeakerHighIcon} from '@phosphor-icons/react'; -import {formatNumber} from '@pkgs/number_utils/src/NumberFormatting'; import type React from 'react'; const INVITES_PAUSED_RAID_TOOLTIP_DESCRIPTOR = msg({ diff --git a/fluxer_app/src/features/app/components/layout/sidebar_nav/HelpButton.module.css b/fluxer_app/src/features/app/components/layout/sidebar_nav/HelpButton.module.css deleted file mode 100644 index 79393dbb0..000000000 --- a/fluxer_app/src/features/app/components/layout/sidebar_nav/HelpButton.module.css +++ /dev/null @@ -1,22 +0,0 @@ -/* SPDX-License-Identifier: AGPL-3.0-or-later */ - -.button { - position: relative; - display: flex; - width: 100%; - justify-content: center; - border: none; - background-color: transparent; - padding: 0; -} - -.iconText { - height: 1.25rem; - width: 1.25rem; - color: var(--text-primary); -} - -.menuIcon { - height: 1rem; - width: 1rem; -} diff --git a/fluxer_app/src/features/app/components/layout/sidebar_nav/HelpButton.tsx b/fluxer_app/src/features/app/components/layout/sidebar_nav/HelpButton.tsx index 84b258293..279bbe941 100644 --- a/fluxer_app/src/features/app/components/layout/sidebar_nav/HelpButton.tsx +++ b/fluxer_app/src/features/app/components/layout/sidebar_nav/HelpButton.tsx @@ -1,28 +1,19 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {Routes} from '@app/app/Routes'; -import Accessibility from '@app/features/accessibility/state/Accessibility'; -import guildStyles from '@app/features/app/components/layout/GuildsLayout.module.css'; -import styles from '@app/features/app/components/layout/sidebar_nav/HelpButton.module.css'; -import {useContextMenuHoverState} from '@app/features/app/hooks/useContextMenuHoverState'; -import {useHover} from '@app/features/app/hooks/useHover'; -import {useMergeRefs} from '@app/features/app/hooks/useMergeRefs'; +import {GuildListActionButton} from '@app/features/app/components/layout/sidebar_nav/GuildListActionButton'; +import styles from '@app/features/app/components/layout/sidebar_nav/GuildListActionButton.module.css'; import HiddenGuildListButtons from '@app/features/guild/state/HiddenGuildListButtons'; import {openExternalUrlWithWarning} from '@app/features/messaging/utils/ExternalLinkUtils'; import {MenuGroup} from '@app/features/ui/action_menu/MenuGroup'; import {MenuItem} from '@app/features/ui/action_menu/MenuItem'; import * as ContextMenuCommands from '@app/features/ui/commands/ContextMenuCommands'; -import FocusRing from '@app/features/ui/focus_ring/FocusRing'; import {TooltipWithKeybind} from '@app/features/ui/keybind_hint/KeybindHint'; -import {Tooltip} from '@app/features/ui/tooltip/Tooltip'; import {msg} from '@lingui/core/macro'; import {Trans, useLingui} from '@lingui/react/macro'; import {EyeSlashIcon, QuestionMarkIcon} from '@phosphor-icons/react'; -import {clsx} from 'clsx'; -import {motion} from 'framer-motion'; import {observer} from 'mobx-react-lite'; import type React from 'react'; -import {useRef} from 'react'; const HELP_CENTER_DESCRIPTOR = msg({ message: 'Help center', @@ -30,12 +21,6 @@ const HELP_CENTER_DESCRIPTOR = msg({ }); export const HelpButton = observer(() => { const {i18n} = useLingui(); - const [hoverRef, isHovering] = useHover(); - const buttonRef = useRef(null); - const iconRef = useRef(null); - const itemRef = useRef(null); - const contextMenuOpen = useContextMenuHoverState(itemRef); - const mergedButtonRef = useMergeRefs([hoverRef, buttonRef, itemRef]); const helpUrl = Routes.help(); if (HiddenGuildListButtons.helpButtonHidden || !helpUrl) { return null; @@ -66,57 +51,23 @@ export const HelpButton = observer(() => { )); }; - const shouldShowHoverState = isHovering || contextMenuOpen; return ( -
- ( - - )} - data-flx="app.sidebar-nav.help-button.tooltip" - > - - - - -
+ ( + + )} + icon={ + + } + onClick={handleHelp} + onContextMenu={handleContextMenu} + buttonDataFlx="app.sidebar-nav.help-button.button.help" + data-flx="app.sidebar-nav.help-button" + /> ); }); diff --git a/fluxer_app/src/features/app/keybindings/KeybindEventUtils.ts b/fluxer_app/src/features/app/keybindings/KeybindEventUtils.ts index 8cb8f62ca..0b8a0dd0c 100644 --- a/fluxer_app/src/features/app/keybindings/KeybindEventUtils.ts +++ b/fluxer_app/src/features/app/keybindings/KeybindEventUtils.ts @@ -7,7 +7,7 @@ export type KeyboardShortcutEvent = Pick>; export type KeyboardShortcutPressEvent = KeyboardShortcutEvent & Pick; -const normalizeKeyboardShortcutKey = (key: string): string => { +export const normalizeKeyboardShortcutKey = (key: string): string => { if (key === ' ') return 'space'; if (key === 'Break') return 'pause'; return key.toLowerCase(); diff --git a/fluxer_app/src/features/app/keybindings/KeybindManager.ts b/fluxer_app/src/features/app/keybindings/KeybindManager.ts index 21528cffe..eee9ffd8b 100644 --- a/fluxer_app/src/features/app/keybindings/KeybindManager.ts +++ b/fluxer_app/src/features/app/keybindings/KeybindManager.ts @@ -7,6 +7,7 @@ import { keyboardEventReleasesComboModifier, keyboardEventStartsComboPress, keyboardEventTriggerMatchesCombo, + normalizeKeyboardShortcutKey, shouldAllowLocalShortcutForChannelTextarea, } from '@app/features/app/keybindings/KeybindEventUtils'; import { @@ -105,11 +106,6 @@ import type {I18n} from '@lingui/core'; import CombokeysImport from 'combokeys'; import {autorun, compareStructural, computed, reaction} from 'mobx'; -const normalizeKeyboardShortcutKey = (key: string): string => { - if (key === ' ') return 'space'; - if (key === 'Break') return 'pause'; - return key.toLowerCase(); -}; const ROUTE_ALLOWED_ACTIONS = new Set(['system_open_theme_studio_popout']); const GAMEPAD_POLL_INTERVAL_MS = 50; const PORTAL_SOURCE_ID_PREFIX = 'portal:'; diff --git a/fluxer_app/src/features/app/state/Updater.ts b/fluxer_app/src/features/app/state/Updater.ts index 46029221d..4bce426c0 100644 --- a/fluxer_app/src/features/app/state/Updater.ts +++ b/fluxer_app/src/features/app/state/Updater.ts @@ -4,7 +4,6 @@ import Config from '@app/features/app/config/Config'; import {DESKTOP_DOWNLOAD_URL} from '@app/features/app/config/I18nDisplayConstants'; import {WORKER_NAVIGATION_CACHE_PREFIX} from '@app/features/platform/service_worker/WorkerCacheCleanup'; import {getProtectedCacheStorage} from '@app/features/platform/state/ProtectedWebStorage'; -import type {UpdaterContext, UpdaterDownloadOption, UpdaterEvent} from '@app/features/platform/types/Electron'; import {Logger} from '@app/features/platform/utils/AppLogger'; import {getClientInfo} from '@app/features/platform/utils/ClientInfo'; import {flushPendingPersistWrites} from '@app/features/platform/utils/MobXPersistence'; @@ -27,9 +26,42 @@ import { type UpdaterMachineSnapshot, } from '@app/features/updater/state/UpdaterStateMachine'; import {buildLinuxManualUpdateOptions} from '@app/features/updater/utils/LinuxManualUpdateOptions'; -import type {UpdaterEvent as NativeUpdaterEvent} from '@app/types/electron.d'; +import type {UpdaterEvent as NativeUpdaterEvent, UpdaterDownloadOption} from '@app/types/electron.d'; import {makeAutoObservable, runInAction} from 'mobx'; +type UpdaterContext = NonNullable; +type UpdaterEvent = + | { + type: 'checking'; + context: UpdaterContext; + } + | { + type: 'available'; + context: UpdaterContext; + version: string | null; + downloadUrl?: string; + downloadOptions?: Array; + } + | { + type: 'not-available'; + context: UpdaterContext; + } + | { + type: 'downloaded'; + context: UpdaterContext; + } + | { + type: 'error'; + context: UpdaterContext; + message: string; + } + | { + type: 'unsupported'; + context: UpdaterContext; + reason: 'platform' | 'unpackaged' | 'managed-package'; + downloadUrl?: string; + }; + const logger = new Logger('Updater'); const CHECK_INTERVAL_MS = 5 * 60 * 1000; const MIN_CHECK_INTERVAL_MS = 60 * 1000; diff --git a/fluxer_app/src/features/auth/InstanceBranding.ts b/fluxer_app/src/features/auth/InstanceBranding.ts index 4ec0b8362..784bfe638 100644 --- a/fluxer_app/src/features/auth/InstanceBranding.ts +++ b/fluxer_app/src/features/auth/InstanceBranding.ts @@ -9,7 +9,7 @@ type BrandingSnapshot = Pick< const ASSET_PROTOCOLS: ReadonlySet = new Set(['https:', 'http:']); -function originOf(value: string | null | undefined): string | null { +export function originOf(value: string | null | undefined): string | null { if (value == null || value.length === 0) { return null; } diff --git a/fluxer_app/src/features/auth/commands/AuthenticationCommands.ts b/fluxer_app/src/features/auth/commands/AuthenticationCommands.ts index b1614bb99..f6d260b7b 100644 --- a/fluxer_app/src/features/auth/commands/AuthenticationCommands.ts +++ b/fluxer_app/src/features/auth/commands/AuthenticationCommands.ts @@ -16,10 +16,13 @@ import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes'; import type {ValueOf} from '@fluxer/constants/src/ValueOf'; import type { AuthRegistrationPendingApprovalResponse, + DesktopHandoffReturnMethod, RegisterRequest, SsoCompleteResponse, SsoStartRequest, SsoStartResponse, + UsernameAvailabilityResponse, + UsernameSuggestionsResponse, } from '@fluxer/schema/src/domains/auth/AuthSchemas'; import type {UserPartial} from '@fluxer/schema/src/domains/user/UserResponseSchemas'; import type {AuthenticationResponseJSON, PublicKeyCredentialRequestOptionsJSON} from '@simplewebauthn/browser'; @@ -131,8 +134,6 @@ interface RecoveryKitIssued { export type RecoverAccountResponse = (AuthTokenResponse | MfaLoginResponse) & RecoveryKitIssued; -export type DesktopHandoffReturnMethod = 'deep_link' | 'code'; - interface DesktopHandoffInitiateResponse { code: string; expires_at: string; @@ -443,10 +444,6 @@ export async function register(data: RegisterRequest, target: AuthRequestTarget) } } -interface UsernameSuggestionsResponse { - suggestions: Array; -} - export async function getUsernameSuggestions(globalName: string, target: InstanceHTTPTarget): Promise> { try { const response = await instanceRequest({ @@ -465,10 +462,6 @@ export async function getUsernameSuggestions(globalName: string, target: Instanc } } -interface UsernameAvailabilityResponse { - available: boolean; -} - export async function checkUsernameAvailability( username: string, target: InstanceHTTPTarget, diff --git a/fluxer_app/src/features/auth/components/pages/oauth_authorize_page/hooks/useBotInviteDestinations.ts b/fluxer_app/src/features/auth/components/pages/oauth_authorize_page/hooks/useBotInviteDestinations.ts index a2f5636ea..51530a600 100644 --- a/fluxer_app/src/features/auth/components/pages/oauth_authorize_page/hooks/useBotInviteDestinations.ts +++ b/fluxer_app/src/features/auth/components/pages/oauth_authorize_page/hooks/useBotInviteDestinations.ts @@ -9,6 +9,7 @@ import { logger, } from '@app/features/auth/components/pages/oauth_authorize_page/OAuthAuthorizePageShared'; import Accounts from '@app/features/auth/state/Accounts'; +import {formatList} from '@app/features/i18n/utils/IntlCache'; import {http} from '@app/features/platform/transport/RestTransport'; import {failureMessage} from '@app/features/platform/utils/ResponseInspection'; import * as AvatarUtils from '@app/features/user/utils/AvatarUtils'; @@ -17,7 +18,6 @@ import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants'; import type {ChannelResponse} from '@fluxer/schema/src/domains/channel/ChannelSchemas'; import {msg} from '@lingui/core/macro'; import {useLingui} from '@lingui/react/macro'; -import {formatListWithConfig} from '@pkgs/list_utils/src/ListFormatting'; import {useEffect, useMemo, useRef, useState} from 'react'; const UNKNOWN_COMMUNITY_DESCRIPTOR = msg({ @@ -102,7 +102,7 @@ function getGroupDmLabel(channel: ChannelResponse, unknownGroupDmLabel: string, const names = recipients .map((recipient) => recipient.global_name?.trim() || recipient.username?.trim()) .filter((name): name is string => Boolean(name)); - if (names.length > 0) return formatListWithConfig(names, {locale, style: 'narrow', type: 'conjunction'}); + if (names.length > 0) return formatList(names, {locale, style: 'narrow', type: 'conjunction'}); return unknownGroupDmLabel; } diff --git a/fluxer_app/src/features/auth/flow/AuthMinimalRegisterFormCore.tsx b/fluxer_app/src/features/auth/flow/AuthMinimalRegisterFormCore.tsx deleted file mode 100644 index 0052f52a1..000000000 --- a/fluxer_app/src/features/auth/flow/AuthMinimalRegisterFormCore.tsx +++ /dev/null @@ -1,277 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {RuntimeConfigSnapshot} from '@app/features/app/state/InstanceSnapshotStore'; -import * as AuthenticationCommands from '@app/features/auth/commands/AuthenticationCommands'; -import FormField from '@app/features/auth/flow/AuthFormField'; -import styles from '@app/features/auth/flow/AuthPageStyles.module.css'; -import {DateOfBirthField} from '@app/features/auth/flow/DateOfBirthField'; -import { - getRegistrationLegalConsentConfig, - RegistrationLegalConsent, -} from '@app/features/auth/flow/RegistrationLegalConsent'; -import {type MissingField, SubmitTooltip, shouldDisableSubmit} from '@app/features/auth/flow/SubmitTooltip'; -import {useAuthForm} from '@app/features/auth/hooks/useAuthForm'; -import { - type AuthRegisterFormDraft, - EMPTY_AUTH_REGISTER_FORM_DRAFT, - useAuthRegisterDraftContext, -} from '@app/features/auth/state/AuthRegisterDraftContext'; -import {authRequestTargetFromSnapshot} from '@app/features/auth/state/AuthRequestTarget'; -import { - DISPLAY_NAME_OPTIONAL_DESCRIPTOR, - WHAT_SHOULD_PEOPLE_CALL_YOU_DESCRIPTOR, -} from '@app/features/i18n/utils/CommonMessageDescriptors'; -import {useLocation} from '@app/features/platform/components/router/RouterReact'; -import {Button} from '@app/features/ui/button/Button'; -import type {ThemeType} from '@fluxer/constants/src/UserConstants'; -import {msg} from '@lingui/core/macro'; -import {Trans, useLingui} from '@lingui/react/macro'; -import {observer} from 'mobx-react-lite'; -import {useCallback, useId, useMemo, useRef, useState} from 'react'; - -const DATE_OF_BIRTH_DESCRIPTOR = msg({ - message: 'Date of birth', - comment: 'Short label in the authentication auth minimal register form core. Keep the tone plain and specific.', -}); - -interface AuthMinimalRegisterFormCoreProps { - submitLabel: React.ReactNode; - redirectPath: string; - runtimeSnapshot: RuntimeConfigSnapshot; - onRegister?: (response: AuthenticationCommands.TokenResponse) => Promise; - onAuthenticated?: () => void; - inviteCode?: string; - extraContent?: React.ReactNode; - theme?: ThemeType; -} - -export const AuthMinimalRegisterFormCore = observer(function AuthMinimalRegisterFormCore({ - submitLabel, - redirectPath, - runtimeSnapshot, - onRegister, - onAuthenticated, - inviteCode, - extraContent, - theme, -}: AuthMinimalRegisterFormCoreProps) { - const {i18n} = useLingui(); - const location = useLocation(); - const draftKey = `register:${location.pathname}${location.search}`; - const registrationUrlCode = useMemo(() => { - const value = new URLSearchParams(location.search).get('registration_url')?.trim(); - return value || undefined; - }, [location.search]); - const registrationTarget = useMemo(() => authRequestTargetFromSnapshot(runtimeSnapshot), [runtimeSnapshot]); - const isPublicRegistrationClosed = runtimeSnapshot.registration.mode === 'closed' && !registrationUrlCode; - const collectDateOfBirth = runtimeSnapshot.appPublic.registration.collect_date_of_birth; - const {getRegisterFormDraft, setRegisterFormDraft, clearRegisterFormDraft} = useAuthRegisterDraftContext(); - const globalNameId = useId(); - const initialDraft = useMemo(() => { - const persistedDraft = getRegisterFormDraft(draftKey); - if (!persistedDraft) { - return EMPTY_AUTH_REGISTER_FORM_DRAFT; - } - return { - ...persistedDraft, - formValues: {...persistedDraft.formValues}, - }; - }, [draftKey, getRegisterFormDraft]); - const draftRef = useRef({ - ...initialDraft, - formValues: {...initialDraft.formValues}, - }); - const [selectedMonth, setSelectedMonthState] = useState(initialDraft.selectedMonth); - const [selectedDay, setSelectedDayState] = useState(initialDraft.selectedDay); - const [selectedYear, setSelectedYearState] = useState(initialDraft.selectedYear); - const [consent, setConsentState] = useState(initialDraft.consent); - const [pendingApprovalUserId, setPendingApprovalUserId] = useState(null); - const legalConsentConfig = getRegistrationLegalConsentConfig(runtimeSnapshot); - const effectiveConsent = legalConsentConfig.requirement ? consent : true; - const initialValues: Record = { - global_name: initialDraft.formValues.global_name ?? '', - }; - const persistDraft = useCallback( - (partialDraft: Partial) => { - const currentDraft = draftRef.current; - const nextDraft: AuthRegisterFormDraft = { - ...currentDraft, - ...partialDraft, - formValues: partialDraft.formValues ? {...partialDraft.formValues} : currentDraft.formValues, - }; - draftRef.current = nextDraft; - setRegisterFormDraft(draftKey, nextDraft); - }, - [draftKey, setRegisterFormDraft], - ); - const handleMonthChange = useCallback( - (month: string) => { - setSelectedMonthState(month); - persistDraft({selectedMonth: month}); - }, - [persistDraft], - ); - const handleDayChange = useCallback( - (day: string) => { - setSelectedDayState(day); - persistDraft({selectedDay: day}); - }, - [persistDraft], - ); - const handleYearChange = useCallback( - (year: string) => { - setSelectedYearState(year); - persistDraft({selectedYear: year}); - }, - [persistDraft], - ); - const handleConsentChange = useCallback( - (nextConsent: boolean) => { - setConsentState(nextConsent); - persistDraft({consent: nextConsent}); - }, - [persistDraft], - ); - const handleRegisterSubmit = async (values: Record) => { - if (isPublicRegistrationClosed) { - return false; - } - setPendingApprovalUserId(null); - const dateOfBirth = - collectDateOfBirth && selectedYear && selectedMonth && selectedDay - ? `${selectedYear}-${selectedMonth.padStart(2, '0')}-${selectedDay.padStart(2, '0')}` - : undefined; - const response = await AuthenticationCommands.register( - { - global_name: values.global_name || undefined, - date_of_birth: dateOfBirth, - consent: effectiveConsent, - invite_code: inviteCode, - registration_url_code: registrationUrlCode, - theme, - }, - registrationTarget, - ); - if (AuthenticationCommands.isRegistrationPendingApprovalResponse(response)) { - clearRegisterFormDraft(draftKey); - setPendingApprovalUserId(response.user_id); - return false; - } - if (onRegister) { - await onRegister(response); - } else { - const userData = AuthenticationCommands.authResponseUserToUserData(response.user); - await AuthenticationCommands.completeLogin({ - token: response.token, - userId: response.user_id, - ...(userData ? {userData} : {}), - runtimeSnapshot, - }); - } - onAuthenticated?.(); - clearRegisterFormDraft(draftKey); - return undefined; - }; - const {form, isLoading, fieldErrors} = useAuthForm({ - initialValues, - onSubmit: handleRegisterSubmit, - redirectPath, - firstFieldName: 'global_name', - }); - const setDraftedFormValue = useCallback( - (fieldName: string, value: string) => { - form.setValue(fieldName, value); - const nextFormValues = { - ...draftRef.current.formValues, - [fieldName]: value, - }; - persistDraft({formValues: nextFormValues}); - }, - [form, persistDraft], - ); - const missingFields = useMemo(() => { - const missing: Array = []; - if (collectDateOfBirth && (!selectedMonth || !selectedDay || !selectedYear)) { - missing.push({key: 'date_of_birth', label: i18n._(DATE_OF_BIRTH_DESCRIPTOR)}); - } - return missing; - }, [selectedMonth, selectedDay, selectedYear, collectDateOfBirth, i18n.locale]); - const globalNameValue = form.getValue('global_name'); - const submitDisabled = - isLoading || - form.isSubmitting || - isPublicRegistrationClosed || - pendingApprovalUserId !== null || - shouldDisableSubmit(effectiveConsent, missingFields); - return ( -
- {isPublicRegistrationClosed ? ( -
- Registration is currently closed. Use a registration link from an admin to create an account. -
- ) : null} - {pendingApprovalUserId ? ( -
- Your account request is pending approval. You can sign in after an admin approves it. -
- ) : null} - setDraftedFormValue('global_name', value)} - error={form.getError('global_name') || fieldErrors?.get('global_name')} - data-flx="auth.flow.auth-minimal-register-form-core.form-field.set-drafted-form-value.text" - /> - {collectDateOfBirth ? ( - - ) : null} - {extraContent} - - - - - - ); -}); diff --git a/fluxer_app/src/features/auth/flow/AuthRegisterFormCore.tsx b/fluxer_app/src/features/auth/flow/AuthRegisterFormCore.tsx index 43a583b1e..089f47b18 100644 --- a/fluxer_app/src/features/auth/flow/AuthRegisterFormCore.tsx +++ b/fluxer_app/src/features/auth/flow/AuthRegisterFormCore.tsx @@ -47,6 +47,10 @@ const DATE_OF_BIRTH_DESCRIPTOR = msg({ message: 'Date of birth', comment: 'Short label in the authentication auth register form core. Keep the tone plain and specific.', }); +const DATE_OF_BIRTH_WITHOUT_USERNAME_DESCRIPTOR = msg({ + message: 'Date of birth', + comment: 'Short label in the authentication auth minimal register form core. Keep the tone plain and specific.', +}); const USERNAME_MUST_BE_CHARACTERS_OR_LESS_DESCRIPTOR = msg({ message: 'Username must be {maxUsernameLength} characters or fewer', comment: 'Registration form validation error when the username exceeds the maximum length. Limit is interpolated.', @@ -82,6 +86,7 @@ interface FieldConfig { showEmail?: boolean; showPassword?: boolean; showPasswordConfirmation?: boolean; + showUsername?: boolean; showUsernameValidation?: boolean; requireUsername?: boolean; } @@ -118,9 +123,11 @@ export const AuthRegisterFormCore = observer(function AuthRegisterFormCore({ showEmail = false, showPassword = false, showPasswordConfirmation = false, + showUsername = true, showUsernameValidation = false, requireUsername = false, } = fields; + const flx = showUsername ? 'auth.flow.auth-register-form-core' : 'auth.flow.auth-minimal-register-form-core'; const location = useLocation(); const draftKey = `register:${location.pathname}${location.search}`; const registrationUrlCode = useMemo(() => { @@ -159,8 +166,8 @@ export const AuthRegisterFormCore = observer(function AuthRegisterFormCore({ const effectiveConsent = legalConsentConfig.requirement ? consent : true; const initialValues: Record = { global_name: initialDraft.formValues.global_name ?? '', - username: initialDraft.formValues.username ?? '', }; + if (showUsername) initialValues.username = initialDraft.formValues.username ?? ''; if (showEmail) initialValues.email = initialDraft.formValues.email ?? ''; if (showPassword) initialValues.password = initialDraft.formValues.password ?? ''; if (showPassword && showPasswordConfirmation) { @@ -283,6 +290,7 @@ export const AuthRegisterFormCore = observer(function AuthRegisterFormCore({ globalName: form.getValue('global_name'), username: form.getValue('username'), target: registrationTarget.http, + enabled: showUsername, }); const missingFields = useMemo(() => { const missing: Array = []; @@ -299,7 +307,10 @@ export const AuthRegisterFormCore = observer(function AuthRegisterFormCore({ missing.push({key: 'confirm_password', label: i18n._(CONFIRM_PASSWORD_DESCRIPTOR)}); } if (collectDateOfBirth && (!selectedMonth || !selectedDay || !selectedYear)) { - missing.push({key: 'date_of_birth', label: i18n._(DATE_OF_BIRTH_DESCRIPTOR)}); + missing.push({ + key: 'date_of_birth', + label: i18n._(showUsername ? DATE_OF_BIRTH_DESCRIPTOR : DATE_OF_BIRTH_WITHOUT_USERNAME_DESCRIPTOR), + }); } return missing; }, [ @@ -311,6 +322,7 @@ export const AuthRegisterFormCore = observer(function AuthRegisterFormCore({ requireUsername, showPassword, showPasswordConfirmation, + showUsername, collectDateOfBirth, i18n.locale, ]); @@ -326,7 +338,7 @@ export const AuthRegisterFormCore = observer(function AuthRegisterFormCore({ const usernameAvailability = useUsernameAvailability( registrationTarget.http, trimmedUsername, - uniqueUsernames && trimmedUsername.length > 0 && usernameFormatValid, + showUsername && uniqueUsernames && trimmedUsername.length > 0 && usernameFormatValid, ); const helperTextState = useMemo(() => { if ((showUsernameValidation || requireUsername) && trimmedUsername.length > 0) { @@ -371,22 +383,14 @@ export const AuthRegisterFormCore = observer(function AuthRegisterFormCore({ (uniqueUsernames && usernameAvailability === 'taken') || shouldDisableSubmit(effectiveConsent, missingFields); return ( -
+ {isPublicRegistrationClosed ? ( -
+
Registration is currently closed. Use a registration link from an admin to create an account.
) : null} {pendingApprovalUserId ? ( -
+
Your account request is pending approval. You can sign in after an admin approves it.
) : null} @@ -413,75 +417,77 @@ export const AuthRegisterFormCore = observer(function AuthRegisterFormCore({ value={form.getValue('global_name')} onChange={(value) => setDraftedFormValue('global_name', value)} error={form.getError('global_name') || fieldErrors?.get('global_name')} - data-flx="auth.flow.auth-register-form-core.form-field.set-drafted-form-value.text" + data-flx={`${flx}.form-field.set-drafted-form-value.text`} /> -
- setDraftedFormValue('username', value)} - error={form.getError('username') || fieldErrors?.get('username')} - data-flx="auth.flow.auth-register-form-core.form-field.set-drafted-form-value.text--2" - /> - - {helperTextState?.type === 'error' && ( - - {helperTextState.message} - - )} - {helperTextState?.type === 'hint' && ( - - {helperTextState.message} - - )} - {helperTextState?.type === 'suggestion' && ( - - How about:{' '} - - - )} - -
+ {helperTextState.message} + + )} + {helperTextState?.type === 'hint' && ( + + {helperTextState.message} + + )} + {helperTextState?.type === 'suggestion' && ( + + How about:{' '} + + + )} + +
+ )} {showPassword && ( ) : null} {extraContent} @@ -527,20 +533,15 @@ export const AuthRegisterFormCore = observer(function AuthRegisterFormCore({ checked={consent} config={legalConsentConfig} onChange={handleConsentChange} - data-flx="auth.flow.auth-register-form-core.registration-legal-consent.consent-change" + data-flx={`${flx}.registration-legal-consent.consent-change`} /> - diff --git a/fluxer_app/src/features/auth/flow/InviteHeader.tsx b/fluxer_app/src/features/auth/flow/InviteHeader.tsx index f90fcbd51..fb4ef6ed4 100644 --- a/fluxer_app/src/features/auth/flow/InviteHeader.tsx +++ b/fluxer_app/src/features/auth/flow/InviteHeader.tsx @@ -3,6 +3,7 @@ import styles from '@app/features/auth/flow/AuthPageStyles.module.css'; import {GuildBadge} from '@app/features/guild/components/GuildBadge'; import {GuildIcon} from '@app/features/guild/components/popouts/GuildIcon'; +import {formatNumber} from '@app/features/i18n/utils/IntlCache'; import {isGroupDmInvite, isGuildInvite} from '@app/features/invite/types/InviteTypes'; import {BaseAvatar} from '@app/features/ui/components/BaseAvatar'; import * as AvatarUtils from '@app/features/user/utils/AvatarUtils'; @@ -10,7 +11,6 @@ import {getCurrentLocale} from '@app/features/user/utils/LocaleUtils'; import * as NicknameUtils from '@app/features/user/utils/NicknameUtils'; import type {GroupDmInvite, GuildInvite, Invite} from '@fluxer/schema/src/domains/invite/InviteSchemas'; import {Plural, Trans} from '@lingui/react/macro'; -import {formatNumber} from '@pkgs/number_utils/src/NumberFormatting'; import {observer} from 'mobx-react-lite'; import {useEffect, useState} from 'react'; diff --git a/fluxer_app/src/features/auth/flow/SubmitTooltip.tsx b/fluxer_app/src/features/auth/flow/SubmitTooltip.tsx index cbe68955d..99aff5f89 100644 --- a/fluxer_app/src/features/auth/flow/SubmitTooltip.tsx +++ b/fluxer_app/src/features/auth/flow/SubmitTooltip.tsx @@ -1,11 +1,11 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import styles from '@app/features/auth/flow/SubmitTooltip.module.css'; +import {formatList} from '@app/features/i18n/utils/IntlCache'; import {Tooltip} from '@app/features/ui/tooltip/Tooltip'; import type {MessageDescriptor} from '@lingui/core'; import {msg} from '@lingui/core/macro'; import {useLingui} from '@lingui/react/macro'; -import {formatListWithConfig} from '@pkgs/list_utils/src/ListFormatting'; import type {ReactNode} from 'react'; const YOU_MUST_AGREE_TO_THE_TERMS_OF_SERVICE_DESCRIPTOR = msg({ @@ -60,7 +60,7 @@ function getTooltipContentDescriptor( return getConsentRequiredDescriptor(legalConsentRequirement); } if (missingFields.length > 0) { - const fieldList = formatListWithConfig( + const fieldList = formatList( missingFields.map((f) => f.label), {locale, style: 'long', type: 'conjunction'}, ); diff --git a/fluxer_app/src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts b/fluxer_app/src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts index fbca7f098..6a83177f2 100644 --- a/fluxer_app/src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts +++ b/fluxer_app/src/features/auth/flow/auth_login_core/useDesktopHandoffFlow.ts @@ -2,16 +2,15 @@ import {PRODUCT_NAME} from '@app/features/app/config/I18nDisplayConstants'; import type {RuntimeConfigSnapshot} from '@app/features/app/state/InstanceSnapshotStore'; -import type { - DesktopHandoffInfoResponse, - DesktopHandoffReturnMethod, -} from '@app/features/auth/commands/AuthenticationCommands'; +import type {DesktopHandoffInfoResponse} from '@app/features/auth/commands/AuthenticationCommands'; import * as AuthenticationCommands from '@app/features/auth/commands/AuthenticationCommands'; import {DesktopHandoffMode} from '@app/features/auth/flow/auth_login_core/AuthLoginStepTypes'; +import {originOf} from '@app/features/auth/InstanceBranding'; import type {UserData} from '@app/features/auth/state/AccountStorage'; import {type InstanceHTTPTarget, instanceTargetFromSnapshot} from '@app/features/platform/transport/InstanceHTTP'; import {Logger} from '@app/features/platform/utils/AppLogger'; import * as FormUtils from '@app/lib/forms'; +import type {DesktopHandoffReturnMethod} from '@fluxer/schema/src/domains/auth/AuthSchemas'; import {formatDesktopHandoffCode, parseDesktopHandoffCode} from '@fluxer/schema/src/domains/auth/DesktopHandoffCode'; import {msg} from '@lingui/core/macro'; import {useLingui} from '@lingui/react/macro'; @@ -61,23 +60,12 @@ export interface DesktopHandoffRequest { readonly identifier: string | null; } -function readOrigin(value: string | null | undefined): string | null { - if (value == null || value.length === 0) { - return null; - } - try { - return new URL(value).origin; - } catch { - return null; - } -} - export function readDesktopHandoffRequest(search: string): DesktopHandoffRequest { const params = new URLSearchParams(search); const code = parseDesktopHandoffCode(params.get('code') ?? ''); return { code: code == null ? null : formatDesktopHandoffCode(code), - apiOrigin: readOrigin(params.get('api')), + apiOrigin: originOf(params.get('api')), identifier: params.get('email') ?? params.get('login'), }; } @@ -186,7 +174,7 @@ export function useDesktopHandoffFlow({enabled, hasStoredAccounts, initialMode}: setClientInfo(null); setReturnMethod(null); setReturnUrl(null); - const accountOrigin = readOrigin(runtimeSnapshot.apiEndpoint); + const accountOrigin = originOf(runtimeSnapshot.apiEndpoint); if (request.apiOrigin != null && accountOrigin != null && request.apiOrigin !== accountOrigin) { failWith( i18n._(INSTANCE_MISMATCH_DESCRIPTOR, { diff --git a/fluxer_app/src/features/channel/components/CallMessage.tsx b/fluxer_app/src/features/channel/components/CallMessage.tsx index 17c749700..416af0eb5 100644 --- a/fluxer_app/src/features/channel/components/CallMessage.tsx +++ b/fluxer_app/src/features/channel/components/CallMessage.tsx @@ -11,6 +11,7 @@ import { ONE_MONTH_DURATION_DESCRIPTOR, ONE_WEEK_DURATION_DESCRIPTOR, } from '@app/features/i18n/utils/CommonMessageDescriptors'; +import {formatList, formatNumber} from '@app/features/i18n/utils/IntlCache'; import {useSystemMessageData} from '@app/features/messaging/hooks/useSystemMessageData'; import type {Message} from '@app/features/messaging/models/MessagingMessage'; import FocusRing from '@app/features/ui/focus_ring/FocusRing'; @@ -21,8 +22,6 @@ import type {I18n} from '@lingui/core'; import {msg} from '@lingui/core/macro'; import {Trans, useLingui} from '@lingui/react/macro'; import {PhoneIcon} from '@phosphor-icons/react'; -import {formatListWithConfig} from '@pkgs/list_utils/src/ListFormatting'; -import {formatNumber} from '@pkgs/number_utils/src/NumberFormatting'; import {clsx} from 'clsx'; import {observer} from 'mobx-react-lite'; import type React from 'react'; @@ -134,7 +133,7 @@ const formatCallDuration = (i18n: I18n, durationSeconds: number): string => { return i18n._(A_MINUTE_DESCRIPTOR); } const locale = getCurrentLocale(); - return formatListWithConfig(parts, {locale, style: 'long', type: 'conjunction'}); + return formatList(parts, {locale, style: 'long', type: 'conjunction'}); }; export const CallMessage = observer(({message}: {message: Message}) => { const {i18n} = useLingui(); diff --git a/fluxer_app/src/features/channel/components/CrosspostCommunityCard.tsx b/fluxer_app/src/features/channel/components/CrosspostCommunityCard.tsx index 5435f4a0b..37585ee72 100644 --- a/fluxer_app/src/features/channel/components/CrosspostCommunityCard.tsx +++ b/fluxer_app/src/features/channel/components/CrosspostCommunityCard.tsx @@ -11,6 +11,7 @@ import {GuildIcon} from '@app/features/guild/components/popouts/GuildIcon'; import GuildCount from '@app/features/guild/state/GuildCount'; import Guilds from '@app/features/guild/state/Guilds'; import {JOIN_COMMUNITY_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors'; +import {formatNumber} from '@app/features/i18n/utils/IntlCache'; import type {Message} from '@app/features/messaging/models/MessagingMessage'; import * as NavigationCommands from '@app/features/navigation/commands/NavigationCommands'; import {Button} from '@app/features/ui/button/Button'; @@ -19,7 +20,6 @@ import * as AvatarUtils from '@app/features/user/utils/AvatarUtils'; import {getCurrentLocale} from '@app/features/user/utils/LocaleUtils'; import {msg} from '@lingui/core/macro'; import {Plural, Trans, useLingui} from '@lingui/react/macro'; -import {formatNumber} from '@pkgs/number_utils/src/NumberFormatting'; import {clsx} from 'clsx'; import {observer} from 'mobx-react-lite'; import {useCallback, useEffect, useState} from 'react'; diff --git a/fluxer_app/src/features/channel/components/InviteEmbed.tsx b/fluxer_app/src/features/channel/components/InviteEmbed.tsx index 82bd73c29..3e87bbc67 100644 --- a/fluxer_app/src/features/channel/components/InviteEmbed.tsx +++ b/fluxer_app/src/features/channel/components/InviteEmbed.tsx @@ -17,6 +17,7 @@ import {GuildIcon} from '@app/features/guild/components/popouts/GuildIcon'; import GuildCount from '@app/features/guild/state/GuildCount'; import Guilds from '@app/features/guild/state/Guilds'; import {JOIN_COMMUNITY_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors'; +import {formatNumber} from '@app/features/i18n/utils/IntlCache'; import {isKeyboardActivationKey} from '@app/features/input/utils/KeyboardUtils'; import * as InviteCommands from '@app/features/invite/commands/InviteCommands'; import Invites from '@app/features/invite/state/Invites'; @@ -56,7 +57,6 @@ import {getCurrentLocale} from '@app/features/user/utils/LocaleUtils'; import {msg} from '@lingui/core/macro'; import {Plural, Trans, useLingui} from '@lingui/react/macro'; import {QuestionIcon} from '@phosphor-icons/react'; -import {formatNumber} from '@pkgs/number_utils/src/NumberFormatting'; import {observer} from 'mobx-react-lite'; import type React from 'react'; import {useEffect, useMemo, useRef, useState} from 'react'; diff --git a/fluxer_app/src/features/channel/components/MobileMemesPicker.tsx b/fluxer_app/src/features/channel/components/MobileMemesPicker.tsx index b00d76d3b..d178f95be 100644 --- a/fluxer_app/src/features/channel/components/MobileMemesPicker.tsx +++ b/fluxer_app/src/features/channel/components/MobileMemesPicker.tsx @@ -7,6 +7,11 @@ import {useSearchInputAutofocus} from '@app/features/app/hooks/useSearchInputAut import {useShouldAnimate} from '@app/features/app/hooks/useShouldAnimate'; import styles from '@app/features/channel/components/GifPicker.module.css'; import memeStyles from '@app/features/channel/components/MemesPicker.module.css'; +import { + filterMemesByContentType, + formatDuration, + getFileExtension, +} from '@app/features/channel/components/pickers/memes/MediaFormat'; import {PickerEmptyState} from '@app/features/channel/components/shared/PickerEmptyState'; import {PickerSearchInput} from '@app/features/channel/components/shared/PickerSearchInput'; import * as FavoriteMemeCommands from '@app/features/expressions/commands/FavoriteMemeCommands'; @@ -29,7 +34,6 @@ import {modal} from '@app/features/ui/commands/ModalCommands'; import {Scroller, type ScrollerHandle} from '@app/features/ui/components/Scroller'; import {MenuBottomSheet, type MenuItemType} from '@app/features/ui/menu_bottom_sheet/MenuBottomSheet'; import {Tooltip} from '@app/features/ui/tooltip/Tooltip'; -import {formatDuration as formatDurationBase} from '@fluxer/date_utils/src/DateDuration'; import {msg} from '@lingui/core/macro'; import {useLingui} from '@lingui/react/macro'; import {GifIcon, ImageIcon, MusicNoteIcon, SmileySadIcon, VideoCameraIcon} from '@phosphor-icons/react'; @@ -106,18 +110,6 @@ interface FilterOption { icon?: React.ReactNode; } -const formatDuration = (seconds: number | null | undefined, locale: string): string => { - if (!seconds || seconds <= 0) return formatDurationBase(0, locale); - return formatDurationBase(seconds, locale); -}; -const getFileExtension = (filename: string, contentType: string): string => { - const extension = filename.split('.').pop()?.toUpperCase(); - if (extension && extension.length <= 4) { - return extension; - } - const typeMatch = contentType.match(/\/([^;]+)/); - return typeMatch?.[1]?.toUpperCase() || 'FILE'; -}; const GifIndicator = observer(() => (