From 2ca756d219ebdd816df7174014f8914572b23752 Mon Sep 17 00:00:00 2001 From: Hampus Date: Tue, 25 Aug 2026 12:57:18 +0200 Subject: [PATCH] feat(app): give the topic and bio composers channel-aware autocomplete (#2002) --- .../ChannelOverviewTopicEditor.tsx | 3 +-- .../channel_overview_tab/shared.ts | 2 -- .../lexical/composer/LexicalRichInput.tsx | 10 ++++------ .../composer/useLexicalAutocomplete.ts | 5 ++++- .../modals/tabs/my_profile_tab/BioEditor.tsx | 19 ++++++++++++++++--- 5 files changed, 25 insertions(+), 14 deletions(-) diff --git a/fluxer_app/src/features/channel/components/modals/channel_tabs/channel_overview_tab/ChannelOverviewTopicEditor.tsx b/fluxer_app/src/features/channel/components/modals/channel_tabs/channel_overview_tab/ChannelOverviewTopicEditor.tsx index 45a77f316..4dc3019fb 100644 --- a/fluxer_app/src/features/channel/components/modals/channel_tabs/channel_overview_tab/ChannelOverviewTopicEditor.tsx +++ b/fluxer_app/src/features/channel/components/modals/channel_tabs/channel_overview_tab/ChannelOverviewTopicEditor.tsx @@ -4,7 +4,6 @@ import styles from '@app/features/channel/components/modals/channel_tabs/Channel import { type FormInputs, MAX_TOPIC_LENGTH, - TOPIC_AUTOCOMPLETE_TRIGGERS, } from '@app/features/channel/components/modals/channel_tabs/channel_overview_tab/shared'; import type {Channel} from '@app/features/channel/models/Channel'; import type {FlatEmoji} from '@app/features/emoji/types/EmojiTypes'; @@ -292,7 +291,7 @@ export const ChannelOverviewTopicEditor = observer( placeholder={i18n._(ADD_A_TOPIC_TO_THIS_CHANNEL_DESCRIPTOR)} disabled={false} channel={channel} - allowedTriggers={[...TOPIC_AUTOCOMPLETE_TRIGGERS]} + allowSpecialMentions={false} markdown={true} markdownParserFlags={TOPIC_MARKDOWN_PARSER_FLAGS} singleLine={false} diff --git a/fluxer_app/src/features/channel/components/modals/channel_tabs/channel_overview_tab/shared.ts b/fluxer_app/src/features/channel/components/modals/channel_tabs/channel_overview_tab/shared.ts index 754a3001f..e109b701d 100644 --- a/fluxer_app/src/features/channel/components/modals/channel_tabs/channel_overview_tab/shared.ts +++ b/fluxer_app/src/features/channel/components/modals/channel_tabs/channel_overview_tab/shared.ts @@ -1,7 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {ChannelRtcRegion} from '@app/features/channel/commands/ChannelCommands'; -import type {TriggerType} from '@app/features/messaging/utils/AutocompleteTriggerPolicy'; import type {ComboboxOption} from '@app/features/ui/components/form/FormCombobox'; import {VOICE_CHANNEL_BITRATE_MAX, VOICE_CHANNEL_BITRATE_MIN} from '@fluxer/constants/src/LimitConstants'; @@ -24,7 +23,6 @@ export const BITRATE_KBPS_MIN = VOICE_CHANNEL_BITRATE_MIN / 1000; export const BITRATE_KBPS_MAX = VOICE_CHANNEL_BITRATE_MAX / 1000; export const BITRATE_KBPS_MARKERS: ReadonlyArray = [BITRATE_KBPS_MIN, 64, 128, 256, BITRATE_KBPS_MAX]; export const MAX_TOPIC_LENGTH = 1024; -export const TOPIC_AUTOCOMPLETE_TRIGGERS: Array = ['emoji', 'mention', 'channel']; export interface RtcRegionOption extends ComboboxOption { region: ChannelRtcRegion | null; diff --git a/fluxer_app/src/features/lexical/composer/LexicalRichInput.tsx b/fluxer_app/src/features/lexical/composer/LexicalRichInput.tsx index 8a0cc382e..ecb8390d6 100644 --- a/fluxer_app/src/features/lexical/composer/LexicalRichInput.tsx +++ b/fluxer_app/src/features/lexical/composer/LexicalRichInput.tsx @@ -34,6 +34,7 @@ export interface LexicalRichInputProps { disabled?: boolean; channel?: Channel | null; allowedTriggers?: Array; + allowSpecialMentions?: boolean; markdown?: boolean; markdownParserFlags?: number; singleLine?: boolean; @@ -69,6 +70,7 @@ export const LexicalRichInput = ({ disabled = false, channel = null, allowedTriggers, + allowSpecialMentions = true, markdown = true, markdownParserFlags, singleLine = false, @@ -130,12 +132,8 @@ export const LexicalRichInput = ({ useLexicalAutocomplete({ channel, handleRef, - allowedTriggers: - allowedTriggers == null - ? channel == null - ? SAFE_CONTEXT_FREE_TRIGGERS - : SAFE_CHANNEL_TRIGGERS - : allowedTriggers, + allowedTriggers: allowedTriggers ?? (channel == null ? SAFE_CONTEXT_FREE_TRIGGERS : SAFE_CHANNEL_TRIGGERS), + allowSpecialMentions, allowMediaOptions: false, maxActualLength: maxLength, onExceedMaxLength, diff --git a/fluxer_app/src/features/lexical/composer/useLexicalAutocomplete.ts b/fluxer_app/src/features/lexical/composer/useLexicalAutocomplete.ts index 30403dc85..ff3c2e1b0 100644 --- a/fluxer_app/src/features/lexical/composer/useLexicalAutocomplete.ts +++ b/fluxer_app/src/features/lexical/composer/useLexicalAutocomplete.ts @@ -104,6 +104,7 @@ interface UseLexicalAutocompleteParams { handleRef: RefObject; allowedTriggers?: Array; allowMediaOptions?: boolean; + allowSpecialMentions?: boolean; maxActualLength?: number; onExceedMaxLength?: () => void; i18n: I18n; @@ -161,6 +162,7 @@ export type {TriggerType} from '@app/features/messaging/utils/AutocompleteTrigge export function useLexicalAutocomplete({ channel, handleRef, + allowSpecialMentions, allowedTriggers, allowMediaOptions = true, maxActualLength, @@ -287,7 +289,8 @@ export function useLexicalAutocomplete({ setState: setGifState, }); - const canMentionEveryone = channel != null && Permission.can(Permissions.MENTION_EVERYONE, channel); + const canMentionEveryone = + allowSpecialMentions !== false && channel != null && Permission.can(Permissions.MENTION_EVERYONE, channel); const canUseCommand = useCallback( (command: Command) => { if (command.type === 'simple') { diff --git a/fluxer_app/src/features/user/components/modals/tabs/my_profile_tab/BioEditor.tsx b/fluxer_app/src/features/user/components/modals/tabs/my_profile_tab/BioEditor.tsx index b797502b0..b193b5b5f 100644 --- a/fluxer_app/src/features/user/components/modals/tabs/my_profile_tab/BioEditor.tsx +++ b/fluxer_app/src/features/user/components/modals/tabs/my_profile_tab/BioEditor.tsx @@ -5,7 +5,6 @@ import {ExpressionPickerPopout} from '@app/features/expressions/components/popou 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 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'; @@ -15,6 +14,7 @@ import surfaceStyles from '@app/features/ui/components/form/FormSurface.module.c 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'; +import {showUserErrorModal} from '@app/features/user/utils/UserErrorModalUtils'; import {msg} from '@lingui/core/macro'; import {Trans, useLingui} from '@lingui/react/macro'; import {SmileyIcon} from '@phosphor-icons/react'; @@ -26,7 +26,14 @@ import {useCallback, useEffect, useId, useRef, useState} from 'react'; 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 = Object.freeze(['emoji']); +const ABOUT_ME_IS_TOO_LONG_DESCRIPTOR = msg({ + message: 'About me is too long', + comment: 'Error modal title shown when inserting an emoji into the profile bio would exceed the bio limit.', +}); +const SHORTEN_YOUR_ABOUT_ME_AND_TRY_AGAIN_DESCRIPTOR = msg({ + message: 'Shorten your about me and try again.', + comment: 'Body of the error modal shown when inserting an emoji into the profile bio would exceed the bio limit.', +}); const COMPOSER_SURFACE_INTERACTIVE_SELECTOR = '[data-channel-textarea], button, a, input, textarea, select, [role="button"]'; @@ -96,6 +103,12 @@ export const BioEditor = observer( }, [onEmojiPickerOpenChange, onEmojiSelect], ); + const handleExceedMaxLength = useCallback(() => { + showUserErrorModal( + i18n._(ABOUT_ME_IS_TOO_LONG_DESCRIPTOR), + i18n._(SHORTEN_YOUR_ABOUT_ME_AND_TRY_AGAIN_DESCRIPTOR), + ); + }, [i18n]); let resolvedPlaceholder = ''; if (placeholder != null) { resolvedPlaceholder = placeholder; @@ -226,12 +239,12 @@ export const BioEditor = observer( placeholder={resolvedPlaceholder} disabled={disabled} channel={null} - allowedTriggers={[...BIO_ALLOWED_TRIGGERS]} markdown={true} markdownParserFlags={BIO_MARKDOWN_PARSER_FLAGS} singleLine={false} size="form" maxLength={actualMaxLength} + onExceedMaxLength={handleExceedMaxLength} className={styles.editor} autocompleteAnchor={wrapperRef.current} id={editableId}