From c5aaf65a10395a0a7cd033007d7367b112ccfd2a Mon Sep 17 00:00:00 2001 From: Hampus Date: Tue, 15 Sep 2026 00:39:18 +0200 Subject: [PATCH] fix(app): list friends with closed DMs in the forward modal (#2787) --- .../shared/ForwardChannelIndex.test.ts | 87 +++++++++++++++++++ .../dialogs/shared/ForwardChannelIndex.ts | 48 ++++++---- .../dialogs/shared/ForwardChannelSelection.ts | 60 ++++++------- .../shared/UseForwardChannelObservations.ts | 73 ++++++++++++++-- .../components/modals/ForwardModal.tsx | 75 +++++++++------- 5 files changed, 255 insertions(+), 88 deletions(-) create mode 100644 fluxer_app/src/features/app/components/dialogs/shared/ForwardChannelIndex.test.ts diff --git a/fluxer_app/src/features/app/components/dialogs/shared/ForwardChannelIndex.test.ts b/fluxer_app/src/features/app/components/dialogs/shared/ForwardChannelIndex.test.ts new file mode 100644 index 000000000..e93bf6240 --- /dev/null +++ b/fluxer_app/src/features/app/components/dialogs/shared/ForwardChannelIndex.test.ts @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import { + ForwardChannelIndex, + type ForwardChannelObservation, +} from '@app/features/app/components/dialogs/shared/ForwardChannelIndex'; +import type {Channel} from '@app/features/channel/models/Channel'; +import type {User} from '@app/features/user/models/User'; +import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants'; +import {setupI18n} from '@lingui/core'; +import {describe, expect, it, vi} from 'vitest'; + +vi.mock('@lingui/core/macro', () => ({msg: (descriptor: unknown) => descriptor})); +vi.mock('@app/features/channel/components/SlowmodeIndicator', () => ({formatSlowmodeTime: () => ''})); +vi.mock('@app/features/permissions/utils/PermissionUtils', () => ({formatPermissionLabel: () => ''})); + +const i18n = setupI18n({locale: 'en', messages: {en: {}}}); + +function observation(overrides: Partial): ForwardChannelObservation { + return { + canAttachFiles: true, + canEmbedLinks: true, + canSendMessages: true, + categoryName: null, + channel: null, + displayName: '', + guildMessagesDisabled: false, + guildName: null, + key: '', + memberTimedOut: false, + recipient: null, + searchAliases: [], + slowmodeEnabled: false, + slowmodeRemainingMs: 0, + ...overrides, + }; +} + +const general = observation({ + channel: {id: 'channel-1', name: 'general', type: ChannelTypes.GUILD_TEXT} as unknown as Channel, + displayName: 'general', + key: 'channel-1', +}); +const closedFriendDM = observation({ + displayName: 'Ada Lovelace', + key: 'user:user-1', + recipient: {id: 'user-1'} as unknown as User, + searchAliases: ['ada_dev'], +}); + +function buildIndex(observations: ReadonlyArray): ForwardChannelIndex { + return new ForwardChannelIndex({ + excludedChannelId: 'channel-1', + i18n, + mediaSelection: {hasAttachments: true, hasEmbeds: true}, + observations, + recentChannelIds: [], + }); +} + +describe('forward channel index', () => { + it('lists a friend whose DM is closed ahead of the source channel', () => { + const keys = buildIndex([general, closedFriendDM]) + .filter('') + .map((option) => option.key); + expect(keys).toEqual(['user:user-1', 'channel-1']); + }); + + it('finds a friend whose DM is closed by username', () => { + const matches = buildIndex([general, closedFriendDM]).filter('ada_dev'); + expect(matches.map((option) => option.key)).toEqual(['user:user-1']); + expect(matches[0]?.channel).toBeNull(); + expect(matches[0]?.disableReason).toBeNull(); + }); + + it('selects destinations by key', () => { + const selected = buildIndex([general, closedFriendDM]).select(new Set(['user:user-1', 'channel-1', 'missing'])); + expect(selected.map((option) => option.key)).toEqual(['user:user-1', 'channel-1']); + }); + + it('drops an observation with neither a channel nor a recipient', () => { + const keys = buildIndex([observation({displayName: 'ghost', key: 'user:ghost'})]) + .filter('') + .map((option) => option.key); + expect(keys).toEqual([]); + }); +}); diff --git a/fluxer_app/src/features/app/components/dialogs/shared/ForwardChannelIndex.ts b/fluxer_app/src/features/app/components/dialogs/shared/ForwardChannelIndex.ts index 05cbea580..7a339452d 100644 --- a/fluxer_app/src/features/app/components/dialogs/shared/ForwardChannelIndex.ts +++ b/fluxer_app/src/features/app/components/dialogs/shared/ForwardChannelIndex.ts @@ -7,6 +7,7 @@ import type {Channel} from '@app/features/channel/models/Channel'; import {PERSONAL_NOTES_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors'; import type {Message} from '@app/features/messaging/models/MessagingMessage'; import {formatPermissionLabel} from '@app/features/permissions/utils/PermissionUtils'; +import type {User} from '@app/features/user/models/User'; import {ChannelTypes, Permissions} from '@fluxer/constants/src/ChannelConstants'; import type {I18n} from '@lingui/core'; import {msg} from '@lingui/core/macro'; @@ -88,11 +89,13 @@ export interface ForwardChannelObservation { readonly canEmbedLinks: boolean; readonly canSendMessages: boolean; readonly categoryName: string | null; - readonly channel: Channel; + readonly channel: Channel | null; readonly displayName: string; readonly guildMessagesDisabled: boolean; readonly guildName: string | null; + readonly key: string; readonly memberTimedOut: boolean; + readonly recipient: User | null; readonly searchAliases: ReadonlyArray; readonly slowmodeEnabled: boolean; readonly slowmodeRemainingMs: number; @@ -100,10 +103,12 @@ export interface ForwardChannelObservation { export interface ForwardChannelOption { readonly categoryName: string | null; - readonly channel: Channel; + readonly channel: Channel | null; readonly disableReason: string | null; readonly displayName: string; readonly guildName: string | null; + readonly key: string; + readonly recipient: User | null; readonly slowmodeEnabled: boolean; readonly slowmodeRemainingMs: number; } @@ -129,13 +134,18 @@ interface BuildForwardChannelIndexRequest { interface ForwardChannelSelectionDisabledRequest { readonly maxSelections: number; readonly option: ForwardChannelOption; - readonly selectedChannelIds: ReadonlySet; + readonly selectedKeys: ReadonlySet; +} + +function isForwardableObservation(observation: ForwardChannelObservation): boolean { + if (observation.channel == null) return observation.recipient != null; + return isForwardableChannelType(observation.channel.type); } export class ForwardChannelIndex { private readonly i18n: I18n; private readonly options: ReadonlyArray; - private readonly optionsByChannelId: ReadonlyMap; + private readonly optionsByKey: ReadonlyMap; constructor({ excludedChannelId, @@ -147,11 +157,11 @@ export class ForwardChannelIndex { this.i18n = i18n; const recentRanks = ForwardChannelIndex.buildRecentRanks(recentChannelIds); const options = observations - .filter((observation) => isForwardableChannelType(observation.channel.type)) + .filter(isForwardableObservation) .map((observation) => this.buildOption({excludedChannelId, mediaSelection, observation, recentRanks})); options.sort((left, right) => this.compareOptions(left, right)); this.options = Object.freeze(options); - this.optionsByChannelId = new Map(options.map((option) => [option.channel.id, option])); + this.optionsByKey = new Map(options.map((option) => [option.key, option])); } filter(searchQuery: string): ReadonlyArray { @@ -169,16 +179,16 @@ export class ForwardChannelIndex { return matches; } - isSelectionDisabled({maxSelections, option, selectedChannelIds}: ForwardChannelSelectionDisabledRequest): boolean { + isSelectionDisabled({maxSelections, option, selectedKeys}: ForwardChannelSelectionDisabledRequest): boolean { if (option.disableReason != null) return true; - if (selectedChannelIds.has(option.channel.id)) return false; - return selectedChannelIds.size >= maxSelections; + if (selectedKeys.has(option.key)) return false; + return selectedKeys.size >= maxSelections; } - select(selectedChannelIds: ReadonlySet): ReadonlyArray { + select(selectedKeys: ReadonlySet): ReadonlyArray { const selected: Array = []; - for (const channelId of selectedChannelIds) { - const option = this.optionsByChannelId.get(channelId); + for (const key of selectedKeys) { + const option = this.optionsByKey.get(key); if (option != null) selected.push(option); } return selected; @@ -213,25 +223,29 @@ export class ForwardChannelIndex { }): IndexedForwardChannelOption { const permissionIssue = this.resolvePermissionIssue(observation, mediaSelection); const disableReason = this.resolveDisableReason(observation, permissionIssue); - const recentRankValue = recentRanks.get(observation.channel.id); + let recentRankValue: number | undefined; + if (observation.channel != null) recentRankValue = recentRanks.get(observation.channel.id); let recentRank: number | null = null; if (recentRankValue !== undefined) recentRank = recentRankValue; + const channel = observation.channel; let channelNameSearchValue = ''; - if (observation.channel.name) channelNameSearchValue = observation.channel.name.toLowerCase(); + if (channel?.name) channelNameSearchValue = channel.name.toLowerCase(); let guildNameSearchValue = ''; if (observation.guildName != null) guildNameSearchValue = observation.guildName.toLowerCase(); return Object.freeze({ categoryName: observation.categoryName, - channel: observation.channel, + channel, channelNameSearchValue, disableReason, displayName: observation.displayName, displayNameSearchValue: observation.displayName.toLowerCase(), guildName: observation.guildName, guildNameSearchValue, - isPersonalNotes: observation.channel.type === ChannelTypes.DM_PERSONAL_NOTES, - isSource: observation.channel.id === excludedChannelId, + isPersonalNotes: channel != null && channel.type === ChannelTypes.DM_PERSONAL_NOTES, + isSource: channel != null && channel.id === excludedChannelId, + key: observation.key, recentRank, + recipient: observation.recipient, searchAliasValues: Object.freeze(observation.searchAliases.map((alias) => alias.toLowerCase())), slowmodeEnabled: observation.slowmodeEnabled, slowmodeRemainingMs: observation.slowmodeRemainingMs, diff --git a/fluxer_app/src/features/app/components/dialogs/shared/ForwardChannelSelection.ts b/fluxer_app/src/features/app/components/dialogs/shared/ForwardChannelSelection.ts index 8be59a9e4..52e46b6c9 100644 --- a/fluxer_app/src/features/app/components/dialogs/shared/ForwardChannelSelection.ts +++ b/fluxer_app/src/features/app/components/dialogs/shared/ForwardChannelSelection.ts @@ -8,6 +8,7 @@ import { } from '@app/features/app/components/dialogs/shared/ForwardChannelIndex'; import {useForwardChannelObservations} from '@app/features/app/components/dialogs/shared/UseForwardChannelObservations'; import {useShallowStableArray} from '@app/features/app/hooks/useShallowStableArray'; +import type {Channel} from '@app/features/channel/models/Channel'; import Channels from '@app/features/channel/state/Channels'; import type {Message} from '@app/features/messaging/models/MessagingMessage'; import SelectedChannel from '@app/features/navigation/state/SelectedChannel'; @@ -24,30 +25,27 @@ interface UseForwardChannelSelectionOptions { export interface ForwardChannelSelectionState { readonly filteredChannels: ReadonlyArray; - readonly handleToggleChannel: (channelId: string) => void; + readonly handleToggleChannel: (key: string) => void; readonly isChannelSelectionDisabled: (option: ForwardChannelOption) => boolean; readonly maxSelections: number; - readonly mostRecentlySelectedChannelId: string | null; + readonly mostRecentlySelectedChannel: Channel | null; readonly searchQuery: string; - readonly selectedChannelIds: ReadonlySet; + readonly selectedChannelOptions: ReadonlyArray; + readonly selectedKeys: ReadonlySet; readonly setSearchQuery: Dispatch>; readonly slowmodeActiveSelectedChannelOptions: ReadonlyArray; readonly slowmodeEnabledSelectedChannelOptions: ReadonlyArray; } -function toggleForwardChannelSelection( - channelId: string, - maxSelections: number, - previousChannelIds: Set, -): Set { - const nextChannelIds = new Set(previousChannelIds); - if (nextChannelIds.has(channelId)) { - nextChannelIds.delete(channelId); - return nextChannelIds; +function toggleForwardChannelSelection(key: string, maxSelections: number, previousKeys: Set): Set { + const nextKeys = new Set(previousKeys); + if (nextKeys.has(key)) { + nextKeys.delete(key); + return nextKeys; } - if (nextChannelIds.size >= maxSelections) return previousChannelIds; - nextChannelIds.add(channelId); - return nextChannelIds; + if (nextKeys.size >= maxSelections) return previousKeys; + nextKeys.add(key); + return nextKeys; } export function useForwardChannelSelection({ @@ -79,24 +77,19 @@ export function useForwardChannelSelection({ [excludedChannelId, i18n, locale, observations, recentChannelIds, resolvedMediaSelection], ); const [searchQuery, setSearchQuery] = useState(''); - const [selectedChannelIds, setSelectedChannelIds] = useState>(new Set()); + const [selectedKeys, setSelectedKeys] = useState>(new Set()); const filteredChannels = useMemo(() => channelIndex.filter(searchQuery), [channelIndex, searchQuery]); const handleToggleChannel = useCallback( - (channelId: string) => { - setSelectedChannelIds((previousChannelIds) => - toggleForwardChannelSelection(channelId, maxSelections, previousChannelIds), - ); + (key: string) => { + setSelectedKeys((previousKeys) => toggleForwardChannelSelection(key, maxSelections, previousKeys)); }, [maxSelections], ); const isChannelSelectionDisabled = useCallback( - (option: ForwardChannelOption) => channelIndex.isSelectionDisabled({maxSelections, option, selectedChannelIds}), - [maxSelections, channelIndex, selectedChannelIds], - ); - const selectedChannelOptions = useMemo( - () => channelIndex.select(selectedChannelIds), - [channelIndex, selectedChannelIds], + (option: ForwardChannelOption) => channelIndex.isSelectionDisabled({maxSelections, option, selectedKeys}), + [maxSelections, channelIndex, selectedKeys], ); + const selectedChannelOptions = useMemo(() => channelIndex.select(selectedKeys), [channelIndex, selectedKeys]); const slowmodeEnabledSelectedChannelOptions = useMemo( () => selectedChannelOptions.filter((option) => option.slowmodeEnabled), [selectedChannelOptions], @@ -105,22 +98,23 @@ export function useForwardChannelSelection({ () => selectedChannelOptions.filter((option) => option.slowmodeRemainingMs > 0), [selectedChannelOptions], ); - const mostRecentlySelectedChannelId = useMemo(() => { - let mostRecent: string | null = null; - for (const channelId of selectedChannelIds) { - mostRecent = channelId; + const mostRecentlySelectedChannel = useMemo(() => { + let mostRecent: Channel | null = null; + for (const option of selectedChannelOptions) { + mostRecent = option.channel; } return mostRecent; - }, [selectedChannelIds]); + }, [selectedChannelOptions]); return { filteredChannels, handleToggleChannel, isChannelSelectionDisabled, maxSelections, - mostRecentlySelectedChannelId, + mostRecentlySelectedChannel, searchQuery, - selectedChannelIds, + selectedChannelOptions, + selectedKeys, setSearchQuery, slowmodeActiveSelectedChannelOptions, slowmodeEnabledSelectedChannelOptions, diff --git a/fluxer_app/src/features/app/components/dialogs/shared/UseForwardChannelObservations.ts b/fluxer_app/src/features/app/components/dialogs/shared/UseForwardChannelObservations.ts index 03a5024ec..1bfa7654b 100644 --- a/fluxer_app/src/features/app/components/dialogs/shared/UseForwardChannelObservations.ts +++ b/fluxer_app/src/features/app/components/dialogs/shared/UseForwardChannelObservations.ts @@ -2,6 +2,7 @@ import type {ForwardChannelObservation} from '@app/features/app/components/dialogs/shared/ForwardChannelIndex'; import {useShallowStableArray} from '@app/features/app/hooks/useShallowStableArray'; +import RuntimeConfig from '@app/features/app/state/RuntimeConfig'; import type {Channel} from '@app/features/channel/models/Channel'; import Channels from '@app/features/channel/state/Channels'; import * as ChannelUtils from '@app/features/channel/utils/ChannelUtils'; @@ -12,11 +13,13 @@ import Permission from '@app/features/permissions/state/Permission'; import Relationships from '@app/features/relationship/state/Relationships'; import Slowmode from '@app/features/slowmode/state/Slowmode'; import {useNow} from '@app/features/ui/state/Tick'; +import type {User} from '@app/features/user/models/User'; import Users from '@app/features/user/state/Users'; import * as NicknameUtils from '@app/features/user/utils/NicknameUtils'; import {ChannelTypes, Permissions} from '@fluxer/constants/src/ChannelConstants'; import {GuildOperations} from '@fluxer/constants/src/GuildConstants'; import {CHANNEL_RATE_LIMIT_PER_USER_MAX} from '@fluxer/constants/src/LimitConstants'; +import {RelationshipTypes} from '@fluxer/constants/src/UserConstants'; import type {I18n} from '@lingui/core'; import {msg} from '@lingui/core/macro'; import {computed, type IComputedValue} from 'mobx'; @@ -40,11 +43,13 @@ interface ForwardChannelObservationBase { readonly canEmbedLinks: boolean; readonly canSendMessages: boolean; readonly categoryName: string | null; - readonly channel: Channel; + readonly channel: Channel | null; readonly displayName: string; readonly guildMessagesDisabled: boolean; readonly guildName: string | null; + readonly key: string; readonly memberTimedOut: boolean; + readonly recipient: User | null; readonly searchAliases: ReadonlyArray; readonly slowmodeEnabled: boolean; } @@ -78,13 +83,28 @@ function resolveChannelDisplayName(channel: Channel, i18n: I18n): string { return i18n._(CHANNEL_DESCRIPTOR, {id: channel.id}); } -function collectRecipientSearchAliases(channel: Channel): ReadonlyArray { +function resolveForwardChannelKey(channel: Channel): string { + if (channel.type === ChannelTypes.DM && channel.recipientIds.length > 0) { + return resolveForwardRecipientKey(channel.recipientIds[0]); + } + return channel.id; +} + +function resolveForwardRecipientKey(userId: string): string { + return `user:${userId}`; +} + +function collectChannelSearchAliases(channel: Channel): ReadonlyArray { if (channel.type !== ChannelTypes.DM && channel.type !== ChannelTypes.GROUP_DM) { return EMPTY_SEARCH_ALIASES; } + return collectRecipientSearchAliases(channel.recipientIds); +} + +function collectRecipientSearchAliases(recipientIds: ReadonlyArray): ReadonlyArray { const aliases = new Set(); const guilds = Guilds.getGuilds(); - for (const recipientId of channel.recipientIds) { + for (const recipientId of recipientIds) { const recipient = Users.getUser(recipientId); if (!recipient) continue; aliases.add(recipient.username); @@ -128,6 +148,7 @@ function areForwardChannelObservationBasesEqual( right: ForwardChannelObservationBase, ): boolean { if (left.channel !== right.channel || left.displayName !== right.displayName) return false; + if (left.key !== right.key || left.recipient !== right.recipient) return false; if (left.guildName !== right.guildName || left.categoryName !== right.categoryName) return false; if (left.guildMessagesDisabled !== right.guildMessagesDisabled || left.memberTimedOut !== right.memberTimedOut) { return false; @@ -195,9 +216,12 @@ class ForwardChannelObservationsOwner { private buildSelection(): ForwardChannelObservationSelection { const bases: Array = []; const slowmodeChannels: Array = []; + const keys = new Set(); for (const channel of this.request.channels) { const index = bases.length; const base = this.buildBase(channel); + if (keys.has(base.key)) continue; + keys.add(base.key); bases.push(base); if (base.slowmodeEnabled) { slowmodeChannels.push( @@ -205,9 +229,42 @@ class ForwardChannelObservationsOwner { ); } } + for (const base of this.buildClosedFriendDMBases()) { + if (keys.has(base.key)) continue; + keys.add(base.key); + bases.push(base); + } return Object.freeze({bases: Object.freeze(bases), slowmodeChannels: Object.freeze(slowmodeChannels)}); } + private buildClosedFriendDMBases(): ReadonlyArray { + if (RuntimeConfig.directMessagesDisabled) return []; + const bases: Array = []; + for (const relationship of Relationships.getRelationships()) { + if (relationship.type !== RelationshipTypes.FRIEND) continue; + const recipient = Users.getUser(relationship.userId); + if (!recipient) continue; + bases.push( + Object.freeze({ + canAttachFiles: true, + canEmbedLinks: true, + canSendMessages: true, + categoryName: null, + channel: null, + displayName: NicknameUtils.getNickname(recipient, null), + guildMessagesDisabled: false, + guildName: null, + key: resolveForwardRecipientKey(recipient.id), + memberTimedOut: false, + recipient, + searchAliases: collectRecipientSearchAliases([recipient.id]), + slowmodeEnabled: false, + }), + ); + } + return bases; + } + private buildBase(channel: Channel): ForwardChannelObservationBase { const guildId = channel.guildId; if (!guildId) { @@ -220,8 +277,10 @@ class ForwardChannelObservationsOwner { displayName: resolveChannelDisplayName(channel, this.request.i18n), guildMessagesDisabled: false, guildName: null, + key: resolveForwardChannelKey(channel), memberTimedOut: false, - searchAliases: collectRecipientSearchAliases(channel), + recipient: null, + searchAliases: collectChannelSearchAliases(channel), slowmodeEnabled: false, }); } @@ -249,8 +308,10 @@ class ForwardChannelObservationsOwner { displayName: resolveChannelDisplayName(channel, this.request.i18n), guildMessagesDisabled, guildName, + key: resolveForwardChannelKey(channel), memberTimedOut: isCurrentMemberTimedOut(channel, this.request.currentUserId), - searchAliases: collectRecipientSearchAliases(channel), + recipient: null, + searchAliases: collectChannelSearchAliases(channel), slowmodeEnabled, }); } @@ -281,8 +342,10 @@ function buildForwardChannelObservations( displayName: base.displayName, guildMessagesDisabled: base.guildMessagesDisabled, guildName: base.guildName, + key: base.key, searchAliases: base.searchAliases, memberTimedOut: base.memberTimedOut, + recipient: base.recipient, slowmodeEnabled: base.slowmodeEnabled, slowmodeRemainingMs: remainingMs === undefined ? 0 : remainingMs, }); diff --git a/fluxer_app/src/features/messaging/components/modals/ForwardModal.tsx b/fluxer_app/src/features/messaging/components/modals/ForwardModal.tsx index 3fba0d960..8c66be64f 100644 --- a/fluxer_app/src/features/messaging/components/modals/ForwardModal.tsx +++ b/fluxer_app/src/features/messaging/components/modals/ForwardModal.tsx @@ -6,6 +6,7 @@ import {useForwardChannelSelection} from '@app/features/app/components/dialogs/s import selectorStyles from '@app/features/app/components/dialogs/shared/SelectorModalStyles.module.css'; 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'; import type {Channel} from '@app/features/channel/models/Channel'; import Channels from '@app/features/channel/state/Channels'; @@ -162,6 +163,21 @@ function resolveForwardReferenceGuildId(channel: Channel | null, message: Messag return null; } +async function openForwardChannels(options: ReadonlyArray): Promise> { + const channelIds: Array = []; + for (const option of options) { + if (option.channel != null) { + channelIds.push(option.channel.id); + continue; + } + if (option.recipient == null) { + throw new Error(`Forward destination ${option.key} has no channel or recipient`); + } + channelIds.push(await PrivateChannelCommands.ensureDMChannel(option.recipient.id)); + } + return channelIds; +} + function focusForwardComment(handle: LexicalRichInputHandle | null): void { if (handle != null) { handle.focus(); @@ -185,9 +201,10 @@ export const ForwardModal = observer( filteredChannels, handleToggleChannel, isChannelSelectionDisabled, - mostRecentlySelectedChannelId, + mostRecentlySelectedChannel, searchQuery, - selectedChannelIds, + selectedChannelOptions, + selectedKeys, setSearchQuery, maxSelections, slowmodeActiveSelectedChannelOptions, @@ -218,13 +235,6 @@ export const ForwardModal = observer( }); }, [i18n]); const sourceForwardChannel = resolveForwardSourceChannel(sourceChannel, message.channelId); - const composerChannel = useMemo(() => { - if (mostRecentlySelectedChannelId == null) { - return null; - } - const selectedChannel = Channels.getChannel(mostRecentlySelectedChannelId); - return selectedChannel == null ? null : selectedChannel; - }, [mostRecentlySelectedChannelId]); const handleCommentChange = useCallback((_display: string, _segments: Array, wire: string) => { setActualOptionalMessage(dropTrailingEmptyBlockquoteLines(wire)); }, []); @@ -266,7 +276,7 @@ export const ForwardModal = observer( } const isCommentCounterVisible = actualOptionalMessage.length > user.maxMessageLength * 0.8; const handleForward = async (skipNavigation = false) => { - if (selectedChannelIds.size === 0) return; + if (selectedChannelOptions.length === 0) return; if (isForwarding) return; if (isSendBlockedBySlowmode) return; if (!isCommentComposerDisabled && isCommentOverLimit) { @@ -287,7 +297,7 @@ export const ForwardModal = observer( attachmentIds = mediaSelection.attachmentIds; embedIndices = mediaSelection.embedIndices; } - const forwardedChannelIds = Array.from(selectedChannelIds); + const forwardedChannelIds = await openForwardChannels(selectedChannelOptions); const forwarded = await MessageCommands.forward( forwardedChannelIds, { @@ -335,21 +345,11 @@ export const ForwardModal = observer( setIsForwarding(false); } }; - const getChannelIcon = (ch: Channel) => { + const getChannelIcon = (option: ForwardChannelOption) => { const iconSize = 32; - if (ch.type === ChannelTypes.DM_PERSONAL_NOTES) { - return ( - - ); - } - if (ch.type === ChannelTypes.DM) { - const recipientId = ch.recipientIds[0]; - const user = Users.getUser(recipientId); + const ch = option.channel; + if (ch == null || ch.type === ChannelTypes.DM) { + const user = ch == null ? option.recipient : Users.getUser(ch.recipientIds[0]); if (!user) return null; return (
@@ -361,6 +361,16 @@ export const ForwardModal = observer(
); } + if (ch.type === ChannelTypes.DM_PERSONAL_NOTES) { + return ( + + ); + } if (ch.type === ChannelTypes.GROUP_DM) { return (
@@ -428,21 +438,20 @@ export const ForwardModal = observer( ) : (
{filteredChannels.map((option: ForwardChannelOption) => { - const ch = option.channel; - const isSelected = selectedChannelIds.has(ch.id); + const isSelected = selectedKeys.has(option.key); const isDisabledForSelection = isChannelSelectionDisabled(option); const isDisabled = isDisabledForSelection && !isSelected; const {categoryName, disableReason, displayName, guildName} = option; return (