mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
fix(app): list friends with closed DMs in the forward modal (#2787)
This commit is contained in:
1 parent
951e39da3d
commit
c5aaf65a10
5 files changed
+255
-88
No files matched your search
@@ -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>): 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<ForwardChannelObservation>): 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([]);
|
||||
});
|
||||
});
|
||||
@@ -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<string>;
|
||||
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<string>;
|
||||
readonly selectedKeys: ReadonlySet<string>;
|
||||
}
|
||||
|
||||
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<IndexedForwardChannelOption>;
|
||||
private readonly optionsByChannelId: ReadonlyMap<string, IndexedForwardChannelOption>;
|
||||
private readonly optionsByKey: ReadonlyMap<string, IndexedForwardChannelOption>;
|
||||
|
||||
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<ForwardChannelOption> {
|
||||
@@ -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<string>): ReadonlyArray<ForwardChannelOption> {
|
||||
select(selectedKeys: ReadonlySet<string>): ReadonlyArray<ForwardChannelOption> {
|
||||
const selected: Array<IndexedForwardChannelOption> = [];
|
||||
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,
|
||||
|
||||
@@ -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<ForwardChannelOption>;
|
||||
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<string>;
|
||||
readonly selectedChannelOptions: ReadonlyArray<ForwardChannelOption>;
|
||||
readonly selectedKeys: ReadonlySet<string>;
|
||||
readonly setSearchQuery: Dispatch<SetStateAction<string>>;
|
||||
readonly slowmodeActiveSelectedChannelOptions: ReadonlyArray<ForwardChannelOption>;
|
||||
readonly slowmodeEnabledSelectedChannelOptions: ReadonlyArray<ForwardChannelOption>;
|
||||
}
|
||||
|
||||
function toggleForwardChannelSelection(
|
||||
channelId: string,
|
||||
maxSelections: number,
|
||||
previousChannelIds: Set<string>,
|
||||
): Set<string> {
|
||||
const nextChannelIds = new Set(previousChannelIds);
|
||||
if (nextChannelIds.has(channelId)) {
|
||||
nextChannelIds.delete(channelId);
|
||||
return nextChannelIds;
|
||||
function toggleForwardChannelSelection(key: string, maxSelections: number, previousKeys: Set<string>): Set<string> {
|
||||
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<Set<string>>(new Set());
|
||||
const [selectedKeys, setSelectedKeys] = useState<Set<string>>(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,
|
||||
|
||||
+68
-5
@@ -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<string>;
|
||||
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<string> {
|
||||
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<string> {
|
||||
if (channel.type !== ChannelTypes.DM && channel.type !== ChannelTypes.GROUP_DM) {
|
||||
return EMPTY_SEARCH_ALIASES;
|
||||
}
|
||||
return collectRecipientSearchAliases(channel.recipientIds);
|
||||
}
|
||||
|
||||
function collectRecipientSearchAliases(recipientIds: ReadonlyArray<string>): ReadonlyArray<string> {
|
||||
const aliases = new Set<string>();
|
||||
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<ForwardChannelObservationBase> = [];
|
||||
const slowmodeChannels: Array<ForwardSlowmodeChannel> = [];
|
||||
const keys = new Set<string>();
|
||||
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<ForwardChannelObservationBase> {
|
||||
if (RuntimeConfig.directMessagesDisabled) return [];
|
||||
const bases: Array<ForwardChannelObservationBase> = [];
|
||||
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,
|
||||
});
|
||||
|
||||
@@ -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<ForwardChannelOption>): Promise<Array<string>> {
|
||||
const channelIds: Array<string> = [];
|
||||
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<MentionSegment>, 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 (
|
||||
<NotePencilIcon
|
||||
className={selectorStyles.itemIcon}
|
||||
weight="fill"
|
||||
size={remFromPx(iconSize)}
|
||||
data-flx="messaging.forward-modal.get-channel-icon.note-pencil-icon"
|
||||
/>
|
||||
);
|
||||
}
|
||||
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 (
|
||||
<div className={selectorStyles.avatar} data-flx="messaging.forward-modal.get-channel-icon.div">
|
||||
@@ -361,6 +361,16 @@ export const ForwardModal = observer(
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (ch.type === ChannelTypes.DM_PERSONAL_NOTES) {
|
||||
return (
|
||||
<NotePencilIcon
|
||||
className={selectorStyles.itemIcon}
|
||||
weight="fill"
|
||||
size={remFromPx(iconSize)}
|
||||
data-flx="messaging.forward-modal.get-channel-icon.note-pencil-icon"
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (ch.type === ChannelTypes.GROUP_DM) {
|
||||
return (
|
||||
<div className={selectorStyles.avatar} data-flx="messaging.forward-modal.get-channel-icon.div--2">
|
||||
@@ -428,21 +438,20 @@ export const ForwardModal = observer(
|
||||
) : (
|
||||
<div className={selectorStyles.itemList} data-flx="messaging.forward-modal.div--4">
|
||||
{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 (
|
||||
<FocusRing
|
||||
key={ch.id}
|
||||
key={option.key}
|
||||
offset={-2}
|
||||
enabled={!isDisabled}
|
||||
data-flx="messaging.forward-modal.focus-ring"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => !isDisabled && handleToggleChannel(ch.id)}
|
||||
onClick={() => !isDisabled && handleToggleChannel(option.key)}
|
||||
disabled={isDisabled}
|
||||
aria-pressed={isSelected}
|
||||
className={clsx(
|
||||
@@ -453,7 +462,7 @@ export const ForwardModal = observer(
|
||||
data-flx="messaging.forward-modal.button"
|
||||
>
|
||||
<div className={selectorStyles.itemContent} data-flx="messaging.forward-modal.div--5">
|
||||
{getChannelIcon(ch)}
|
||||
{getChannelIcon(option)}
|
||||
<div className={selectorStyles.itemInfo} data-flx="messaging.forward-modal.div--6">
|
||||
<span className={selectorStyles.itemName} data-flx="messaging.forward-modal.span">
|
||||
{displayName}
|
||||
@@ -517,7 +526,7 @@ export const ForwardModal = observer(
|
||||
initialValue=""
|
||||
initialSegments={EMPTY_FORWARD_COMMENT_SEGMENTS}
|
||||
className={modalStyles.richInput}
|
||||
channel={composerChannel}
|
||||
channel={mostRecentlySelectedChannel}
|
||||
disabled={isCommentComposerDisabled}
|
||||
markdown={true}
|
||||
singleLine={false}
|
||||
@@ -638,7 +647,7 @@ export const ForwardModal = observer(
|
||||
<Button
|
||||
onClick={(event: MouseEvent<HTMLButtonElement>) => handleForward(event.shiftKey)}
|
||||
disabled={
|
||||
selectedChannelIds.size === 0 ||
|
||||
selectedChannelOptions.length === 0 ||
|
||||
isForwarding ||
|
||||
isSendBlockedBySlowmode ||
|
||||
(!isCommentComposerDisabled && isCommentOverLimit)
|
||||
@@ -646,7 +655,7 @@ export const ForwardModal = observer(
|
||||
data-flx="messaging.forward-modal.button.forward"
|
||||
>
|
||||
{i18n._(SEND_SELECTED_COUNT_DESCRIPTOR, {
|
||||
selectedCount: selectedChannelIds.size,
|
||||
selectedCount: selectedKeys.size,
|
||||
selectionLimit: maxSelections,
|
||||
})}
|
||||
</Button>
|
||||
|
||||
Reference in new issue
Block a user