From f4f39e6a89fca7aa10b6c8d748fc9b2ef3f5101c Mon Sep 17 00:00:00 2001 From: Hampus Date: Fri, 25 Sep 2026 13:12:17 +0200 Subject: [PATCH] feat(app): show where forward destinations come from (#2950) --- .../dialogs/shared/UseForwardDestinations.ts | 12 +- .../modals/ForwardDestinationIcon.module.css | 52 ++++++++ .../modals/ForwardDestinationIcon.tsx | 115 ++++++++++++++++++ .../components/modals/ForwardModal.tsx | 71 +---------- 4 files changed, 184 insertions(+), 66 deletions(-) create mode 100644 fluxer_app/src/features/messaging/components/modals/ForwardDestinationIcon.module.css create mode 100644 fluxer_app/src/features/messaging/components/modals/ForwardDestinationIcon.tsx diff --git a/fluxer_app/src/features/app/components/dialogs/shared/UseForwardDestinations.ts b/fluxer_app/src/features/app/components/dialogs/shared/UseForwardDestinations.ts index f57373bd5..46f0ef57e 100644 --- a/fluxer_app/src/features/app/components/dialogs/shared/UseForwardDestinations.ts +++ b/fluxer_app/src/features/app/components/dialogs/shared/UseForwardDestinations.ts @@ -418,6 +418,16 @@ function formatGuildChannelDetail(guild: Guild | undefined, channel: Channel): s return detail === '' ? null : detail; } +function formatGroupDMDetail(channel: Channel): string | null { + if (channel.type !== ChannelTypes.GROUP_DM || (channel.name?.trim() ?? '') === '') return null; + const names: Array = []; + for (const recipientId of channel.recipientIds) { + const recipient = Users.getUser(recipientId); + if (recipient != null) names.push(NicknameUtils.getNickname(recipient, null, channel.id)); + } + return names.length === 0 ? null : names.join(', '); +} + function resolveGuildChannelDisableReason( channel: Channel, guild: Guild | undefined, @@ -469,7 +479,7 @@ function resolveForwardDestinationOption( return Object.freeze({ channel, destination, - detail: null, + detail: formatGroupDMDetail(channel), disableReason: resolveAgeRestrictedDisableReason(channel, mediaNeeds, i18n), displayName: ChannelUtils.getDMDisplayName(channel), key, diff --git a/fluxer_app/src/features/messaging/components/modals/ForwardDestinationIcon.module.css b/fluxer_app/src/features/messaging/components/modals/ForwardDestinationIcon.module.css new file mode 100644 index 000000000..77a6bec55 --- /dev/null +++ b/fluxer_app/src/features/messaging/components/modals/ForwardDestinationIcon.module.css @@ -0,0 +1,52 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ + +.frame { + --forward-icon-size: 2rem; + --forward-badge-size: 1.125rem; + --forward-badge-offset: -0.1875rem; + --forward-badge-gap: 0.125rem; + --forward-badge-center: calc(var(--forward-icon-size) - var(--forward-badge-size) / 2 - var(--forward-badge-offset)); + --forward-badge-cutout: calc(var(--forward-badge-size) / 2 + var(--forward-badge-gap)); + position: relative; + display: flex; + flex-shrink: 0; + width: var(--forward-icon-size); + height: var(--forward-icon-size); +} + +.cutout { + display: flex; + width: 100%; + height: 100%; + mask-image: radial-gradient( + circle at var(--forward-badge-center) var(--forward-badge-center), + transparent var(--forward-badge-cutout), + #000 calc(var(--forward-badge-cutout) + 0.03125rem) + ); +} + +.badge { + position: absolute; + right: var(--forward-badge-offset); + bottom: var(--forward-badge-offset); + display: flex; + align-items: center; + justify-content: center; + width: var(--forward-badge-size); + height: var(--forward-badge-size); + border-radius: 50%; + background-color: var(--background-tertiary); + color: var(--text-primary-muted); +} + +.badgeIcon { + width: 0.75rem; + height: 0.75rem; +} + +.fallbackIcon { + width: 2rem; + height: 2rem; + flex-shrink: 0; + color: var(--text-primary-muted); +} diff --git a/fluxer_app/src/features/messaging/components/modals/ForwardDestinationIcon.tsx b/fluxer_app/src/features/messaging/components/modals/ForwardDestinationIcon.tsx new file mode 100644 index 000000000..520f6c880 --- /dev/null +++ b/fluxer_app/src/features/messaging/components/modals/ForwardDestinationIcon.tsx @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import type {ForwardDestinationOption} from '@app/features/app/components/dialogs/shared/UseForwardDestinations'; +import {GroupDMAvatar} from '@app/features/app/components/shared/GroupDMAvatar'; +import type {Channel} from '@app/features/channel/models/Channel'; +import * as ChannelUtils from '@app/features/channel/utils/ChannelUtils'; +import {GuildIcon} from '@app/features/guild/components/popouts/GuildIcon'; +import Guilds from '@app/features/guild/state/Guilds'; +import styles from '@app/features/messaging/components/modals/ForwardDestinationIcon.module.css'; +import {Avatar} from '@app/features/ui/components/Avatar'; +import {StatusAwareAvatar} from '@app/features/ui/components/StatusAwareAvatar'; +import Users from '@app/features/user/state/Users'; +import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants'; +import {observer} from 'mobx-react-lite'; +import type {ReactNode} from 'react'; + +const DESTINATION_ICON_SIZE = 32; + +function BadgedIcon({badge, children}: {badge: ReactNode; children: ReactNode}) { + return ( +
+
+ {children} +
+
+ {badge} +
+
+ ); +} + +function renderChannelBadge(channel: Channel) { + return ChannelUtils.getIcon(channel, {className: styles.badgeIcon, weight: 'bold'}); +} + +function renderFallbackIcon(channel: Channel) { + return ChannelUtils.getIcon(channel, {className: styles.fallbackIcon, weight: 'bold'}); +} + +function renderGuildChannelIcon(channel: Channel) { + const guild = channel.guildId == null ? undefined : Guilds.getGuild(channel.guildId); + if (guild == null) { + return renderFallbackIcon(channel); + } + return ( + + + + ); +} + +function renderPersonalNotesIcon(channel: Channel) { + const currentUser = Users.currentUser; + if (currentUser == null) { + return renderFallbackIcon(channel); + } + return ( + + + + ); +} + +export const ForwardDestinationIcon = observer(function ForwardDestinationIcon({ + option, +}: { + option: ForwardDestinationOption; +}) { + if (option.user != null) { + return ( +
+ +
+ ); + } + const channel = option.channel; + if (channel == null) return null; + switch (channel.type) { + case ChannelTypes.GROUP_DM: + return ( +
+ +
+ ); + case ChannelTypes.DM_PERSONAL_NOTES: + return renderPersonalNotesIcon(channel); + default: + return renderGuildChannelIcon(channel); + } +}); diff --git a/fluxer_app/src/features/messaging/components/modals/ForwardModal.tsx b/fluxer_app/src/features/messaging/components/modals/ForwardModal.tsx index 9384cdecd..f21ef24cd 100644 --- a/fluxer_app/src/features/messaging/components/modals/ForwardModal.tsx +++ b/fluxer_app/src/features/messaging/components/modals/ForwardModal.tsx @@ -8,7 +8,6 @@ import { type ForwardDestinationOption, useForwardDestinations, } from '@app/features/app/components/dialogs/shared/UseForwardDestinations'; -import {GroupDMAvatar} from '@app/features/app/components/shared/GroupDMAvatar'; import {Limits} from '@app/features/app/utils/UserLimits'; import * as PrivateChannelCommands from '@app/features/channel/commands/PrivateChannelCommands'; import {MessageCharacterCounter} from '@app/features/channel/components/MessageCharacterCounter'; @@ -22,6 +21,7 @@ import {LexicalRichInput, type LexicalRichInputHandle} from '@app/features/lexic import * as MessageCommands from '@app/features/messaging/commands/MessageCommands'; import {MessageForwardFailedModal} from '@app/features/messaging/components/alerts/MessageForwardFailedModal'; import {showMessagingErrorModal} from '@app/features/messaging/components/alerts/MessagingErrorModalUtils'; +import {ForwardDestinationIcon} from '@app/features/messaging/components/modals/ForwardDestinationIcon'; import {ForwardMessagePreview} from '@app/features/messaging/components/modals/ForwardMessagePreview'; import modalStyles from '@app/features/messaging/components/modals/ForwardModal.module.css'; import {shouldNavigateAfterForward} from '@app/features/messaging/components/modals/ForwardModalUtils'; @@ -31,7 +31,6 @@ import type {MentionSegment} from '@app/features/messaging/utils/TextareaSegment import * as NavigationCommands from '@app/features/navigation/commands/NavigationCommands'; import {Logger} from '@app/features/platform/utils/AppLogger'; import {shouldDisableAutofocusOnMobile} from '@app/features/platform/utils/AutofocusUtils'; -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 * as ModalCommands from '@app/features/ui/commands/ModalCommands'; @@ -39,17 +38,15 @@ import {modal} from '@app/features/ui/commands/ModalCommands'; import * as ToastCommands from '@app/features/ui/commands/ToastCommands'; import {Input} from '@app/features/ui/components/form/FormInput'; import {Scroller} from '@app/features/ui/components/Scroller'; -import {StatusAwareAvatar} from '@app/features/ui/components/StatusAwareAvatar'; import FocusRing from '@app/features/ui/focus_ring/FocusRing'; import {Popout} from '@app/features/ui/popover/PopoverPopout'; import MobileLayout from '@app/features/ui/state/MobileLayout'; import type {User} from '@app/features/user/models/User'; -import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants'; import {MAX_MESSAGE_LENGTH_PREMIUM} from '@fluxer/constants/src/LimitConstants'; import type {I18n} from '@lingui/core'; import {msg} from '@lingui/core/macro'; import {Trans, useLingui} from '@lingui/react/macro'; -import {HashIcon, MagnifyingGlassIcon, NotePencilIcon, SmileyIcon, SpeakerHighIcon} from '@phosphor-icons/react'; +import {MagnifyingGlassIcon, SmileyIcon} from '@phosphor-icons/react'; import {clsx} from 'clsx'; import {observer} from 'mobx-react-lite'; import {type MouseEvent, useCallback, useEffect, useId, useRef, useState} from 'react'; @@ -111,8 +108,6 @@ const SEND_SELECTED_COUNT_DESCRIPTOR = msg({ }); const logger = new Logger('ForwardModal'); -const DESTINATION_ICON_SIZE = 32; - interface ForwardModalProps { message: Message; mediaSelection?: MessageCommands.ForwardMediaSelection; @@ -205,63 +200,6 @@ function insertForwardEmoji(handle: LexicalRichInputHandle | null, emoji: FlatEm return handle.insertEmoji(emoji); } -function renderForwardDestinationIcon(option: ForwardDestinationOption) { - if (option.user != null) { - return ( -
- -
- ); - } - const channel = option.channel; - if (channel == null) { - return null; - } - if (channel.type === ChannelTypes.DM_PERSONAL_NOTES) { - return ( - - ); - } - if (channel.type === ChannelTypes.GROUP_DM) { - return ( -
- -
- ); - } - if (channel.type === ChannelTypes.GUILD_VOICE) { - return ( - - ); - } - return ( - - ); -} - const EMPTY_FORWARD_COMMENT_SEGMENTS: ReadonlyArray = Object.freeze([]); export const ForwardModal = observer( @@ -470,7 +408,10 @@ export const ForwardModal = observer( data-flx="messaging.forward-modal.button" >
- {renderForwardDestinationIcon(option)} +
{option.displayName}