mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
fix(app): search forward targets by username and every known nickname (#1691)
This commit is contained in:
1 parent
6c68202222
commit
c61fd96df6
6 files changed
+141
-5
No files matched your search
@@ -9,6 +9,7 @@ import {ChannelTypes, Permissions} from '@fluxer/constants/src/ChannelConstants'
|
||||
import type {I18n} from '@lingui/core';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import {isForwardableChannelType} from './ForwardChannelEligibility';
|
||||
import {matchesForwardChannelSearch} from './ForwardChannelSearchMatch';
|
||||
|
||||
const GUILD_MESSAGES_DISABLED_DESCRIPTOR = msg({
|
||||
message: 'Sending messages is disabled in this community',
|
||||
@@ -92,6 +93,7 @@ export interface ForwardChannelObservation {
|
||||
readonly guildMessagesDisabled: boolean;
|
||||
readonly guildName: string | null;
|
||||
readonly memberTimedOut: boolean;
|
||||
readonly searchAliases: ReadonlyArray<string>;
|
||||
readonly slowmodeEnabled: boolean;
|
||||
readonly slowmodeRemainingMs: number;
|
||||
}
|
||||
@@ -113,6 +115,7 @@ interface IndexedForwardChannelOption extends ForwardChannelOption {
|
||||
readonly isPersonalNotes: boolean;
|
||||
readonly isSource: boolean;
|
||||
readonly recentRank: number | null;
|
||||
readonly searchAliasValues: ReadonlyArray<string>;
|
||||
}
|
||||
|
||||
interface BuildForwardChannelIndexRequest {
|
||||
@@ -194,10 +197,7 @@ export class ForwardChannelIndex {
|
||||
readonly personalNotesSearchValue: string;
|
||||
readonly option: IndexedForwardChannelOption;
|
||||
}): boolean {
|
||||
if (option.isPersonalNotes && personalNotesSearchValue.includes(normalizedQuery)) return true;
|
||||
if (option.displayNameSearchValue.includes(normalizedQuery)) return true;
|
||||
if (option.channelNameSearchValue.includes(normalizedQuery)) return true;
|
||||
return option.guildNameSearchValue.includes(normalizedQuery);
|
||||
return matchesForwardChannelSearch({normalizedQuery, personalNotesSearchValue, values: option});
|
||||
}
|
||||
|
||||
private buildOption({
|
||||
@@ -232,6 +232,7 @@ export class ForwardChannelIndex {
|
||||
isPersonalNotes: observation.channel.type === ChannelTypes.DM_PERSONAL_NOTES,
|
||||
isSource: observation.channel.id === excludedChannelId,
|
||||
recentRank,
|
||||
searchAliasValues: Object.freeze(observation.searchAliases.map((alias) => alias.toLowerCase())),
|
||||
slowmodeEnabled: observation.slowmodeEnabled,
|
||||
slowmodeRemainingMs: observation.slowmodeRemainingMs,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {describe, expect, it} from 'vitest';
|
||||
import {type ForwardChannelSearchValues, matchesForwardChannelSearch} from './ForwardChannelSearchMatch';
|
||||
|
||||
const PERSONAL_NOTES = 'personal notes';
|
||||
|
||||
function recipient(overrides: Partial<ForwardChannelSearchValues> = {}): ForwardChannelSearchValues {
|
||||
return {
|
||||
channelNameSearchValue: '',
|
||||
displayNameSearchValue: 'ada lovelace',
|
||||
guildNameSearchValue: '',
|
||||
isPersonalNotes: false,
|
||||
searchAliasValues: ['ada_dev', 'ada lovelace', 'countess', 'raid lead'],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function matches(query: string, values: ForwardChannelSearchValues = recipient()): boolean {
|
||||
return matchesForwardChannelSearch({
|
||||
normalizedQuery: query,
|
||||
personalNotesSearchValue: PERSONAL_NOTES,
|
||||
values,
|
||||
});
|
||||
}
|
||||
|
||||
describe('forward channel search matching', () => {
|
||||
it('matches the rendered global display name', () => {
|
||||
expect(matches('ada lovelace')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches the username', () => {
|
||||
expect(matches('ada_dev')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches a nickname from a guild the reader is not currently viewing', () => {
|
||||
expect(matches('raid lead')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches a relationship nickname', () => {
|
||||
expect(matches('countess')).toBe(true);
|
||||
});
|
||||
|
||||
it('does not match an unrelated query', () => {
|
||||
expect(matches('grace hopper')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not match a recipient with no aliases indexed beyond the display name', () => {
|
||||
const values = recipient({searchAliasValues: []});
|
||||
expect(matches('ada_dev', values)).toBe(false);
|
||||
expect(matches('ada lovelace', values)).toBe(true);
|
||||
});
|
||||
|
||||
it('still matches channel and guild names', () => {
|
||||
const guildChannel = recipient({
|
||||
channelNameSearchValue: 'general',
|
||||
displayNameSearchValue: 'general',
|
||||
guildNameSearchValue: 'fluxer hq',
|
||||
searchAliasValues: [],
|
||||
});
|
||||
expect(matches('general', guildChannel)).toBe(true);
|
||||
expect(matches('fluxer hq', guildChannel)).toBe(true);
|
||||
});
|
||||
|
||||
it('still matches personal notes', () => {
|
||||
const notes = recipient({displayNameSearchValue: '', isPersonalNotes: true, searchAliasValues: []});
|
||||
expect(matches('personal', notes)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
export interface ForwardChannelSearchValues {
|
||||
readonly channelNameSearchValue: string;
|
||||
readonly displayNameSearchValue: string;
|
||||
readonly guildNameSearchValue: string;
|
||||
readonly isPersonalNotes: boolean;
|
||||
readonly searchAliasValues: ReadonlyArray<string>;
|
||||
}
|
||||
|
||||
interface MatchesForwardChannelSearchRequest {
|
||||
readonly normalizedQuery: string;
|
||||
readonly personalNotesSearchValue: string;
|
||||
readonly values: ForwardChannelSearchValues;
|
||||
}
|
||||
|
||||
export function matchesForwardChannelSearch({
|
||||
normalizedQuery,
|
||||
personalNotesSearchValue,
|
||||
values,
|
||||
}: MatchesForwardChannelSearchRequest): boolean {
|
||||
if (values.isPersonalNotes && personalNotesSearchValue.includes(normalizedQuery)) return true;
|
||||
if (values.displayNameSearchValue.includes(normalizedQuery)) return true;
|
||||
if (values.channelNameSearchValue.includes(normalizedQuery)) return true;
|
||||
if (values.searchAliasValues.some((alias) => alias.includes(normalizedQuery))) return true;
|
||||
return values.guildNameSearchValue.includes(normalizedQuery);
|
||||
}
|
||||
+36
@@ -9,8 +9,10 @@ import DeveloperOptions from '@app/features/devtools/state/DeveloperOptions';
|
||||
import Guilds from '@app/features/guild/state/Guilds';
|
||||
import GuildMembers from '@app/features/member/state/GuildMembers';
|
||||
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 Users from '@app/features/user/state/Users';
|
||||
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';
|
||||
@@ -19,6 +21,8 @@ import {msg} from '@lingui/core/macro';
|
||||
import {computed, type IComputedValue} from 'mobx';
|
||||
import {useMemo, useRef} from 'react';
|
||||
|
||||
const EMPTY_SEARCH_ALIASES: ReadonlyArray<string> = Object.freeze([]);
|
||||
|
||||
const CHANNEL_DESCRIPTOR = msg({
|
||||
message: 'Channel {id}',
|
||||
comment: 'Short label in the settings dialog forward channel selection. Preserve {id}; it is inserted by code.',
|
||||
@@ -40,6 +44,7 @@ interface ForwardChannelObservationBase {
|
||||
readonly guildMessagesDisabled: boolean;
|
||||
readonly guildName: string | null;
|
||||
readonly memberTimedOut: boolean;
|
||||
readonly searchAliases: ReadonlyArray<string>;
|
||||
readonly slowmodeEnabled: boolean;
|
||||
}
|
||||
|
||||
@@ -55,6 +60,10 @@ interface ForwardChannelObservationSelection {
|
||||
}
|
||||
|
||||
function resolveChannelDisplayName(channel: Channel, i18n: I18n): string {
|
||||
if (channel.type === ChannelTypes.DM) {
|
||||
const recipient = channel.recipientIds.length > 0 ? Users.getUser(channel.recipientIds[0]) : null;
|
||||
if (recipient) return recipient.displayName;
|
||||
}
|
||||
if (
|
||||
channel.type === ChannelTypes.DM_PERSONAL_NOTES ||
|
||||
channel.type === ChannelTypes.DM ||
|
||||
@@ -66,6 +75,28 @@ function resolveChannelDisplayName(channel: Channel, i18n: I18n): string {
|
||||
return i18n._(CHANNEL_DESCRIPTOR, {id: channel.id});
|
||||
}
|
||||
|
||||
function collectRecipientSearchAliases(channel: Channel): ReadonlyArray<string> {
|
||||
if (channel.type !== ChannelTypes.DM && channel.type !== ChannelTypes.GROUP_DM) {
|
||||
return EMPTY_SEARCH_ALIASES;
|
||||
}
|
||||
const aliases = new Set<string>();
|
||||
const guilds = Guilds.getGuilds();
|
||||
for (const recipientId of channel.recipientIds) {
|
||||
const recipient = Users.getUser(recipientId);
|
||||
if (!recipient) continue;
|
||||
aliases.add(recipient.username);
|
||||
aliases.add(recipient.displayName);
|
||||
if (recipient.globalName) aliases.add(recipient.globalName);
|
||||
const relationshipNickname = Relationships.getRelationship(recipientId)?.nickname;
|
||||
if (relationshipNickname) aliases.add(relationshipNickname);
|
||||
for (const guild of guilds) {
|
||||
const nick = GuildMembers.getMember(guild.id, recipientId)?.nick;
|
||||
if (nick) aliases.add(nick);
|
||||
}
|
||||
}
|
||||
return Object.freeze([...aliases].filter((alias) => alias.length > 0));
|
||||
}
|
||||
|
||||
function resolveChannelCategoryName(channel: Channel): string | null {
|
||||
if (!channel.parentId) return null;
|
||||
const category = Channels.getChannel(channel.parentId);
|
||||
@@ -99,6 +130,8 @@ function areForwardChannelObservationBasesEqual(
|
||||
return false;
|
||||
}
|
||||
if (left.canSendMessages !== right.canSendMessages || left.canEmbedLinks !== right.canEmbedLinks) return false;
|
||||
if (left.searchAliases.length !== right.searchAliases.length) return false;
|
||||
if (left.searchAliases.some((alias, index) => alias !== right.searchAliases[index])) return false;
|
||||
return left.canAttachFiles === right.canAttachFiles && left.slowmodeEnabled === right.slowmodeEnabled;
|
||||
}
|
||||
|
||||
@@ -185,6 +218,7 @@ class ForwardChannelObservationsOwner {
|
||||
guildMessagesDisabled: false,
|
||||
guildName: null,
|
||||
memberTimedOut: false,
|
||||
searchAliases: collectRecipientSearchAliases(channel),
|
||||
slowmodeEnabled: false,
|
||||
});
|
||||
}
|
||||
@@ -213,6 +247,7 @@ class ForwardChannelObservationsOwner {
|
||||
guildMessagesDisabled,
|
||||
guildName,
|
||||
memberTimedOut: isCurrentMemberTimedOut(channel, this.request.currentUserId),
|
||||
searchAliases: collectRecipientSearchAliases(channel),
|
||||
slowmodeEnabled,
|
||||
});
|
||||
}
|
||||
@@ -243,6 +278,7 @@ function buildForwardChannelObservations(
|
||||
displayName: base.displayName,
|
||||
guildMessagesDisabled: base.guildMessagesDisabled,
|
||||
guildName: base.guildName,
|
||||
searchAliases: base.searchAliases,
|
||||
memberTimedOut: base.memberTimedOut,
|
||||
slowmodeEnabled: base.slowmodeEnabled,
|
||||
slowmodeRemainingMs: remainingMs === undefined ? 0 : remainingMs,
|
||||
|
||||
@@ -51,6 +51,7 @@ export interface LexicalRichInputProps {
|
||||
richInputRef?: React.Ref<LexicalRichInputHandle>;
|
||||
onChange?: (display: string, segments: Array<MentionSegment>, wire: string) => void;
|
||||
onSubmit?: () => void;
|
||||
submitOnEnter?: boolean;
|
||||
onKeyDown?: (event: React.KeyboardEvent<HTMLElement>) => void;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
@@ -85,6 +86,7 @@ export const LexicalRichInput = ({
|
||||
richInputRef,
|
||||
onChange,
|
||||
onSubmit,
|
||||
submitOnEnter,
|
||||
onKeyDown,
|
||||
onFocus,
|
||||
onBlur,
|
||||
@@ -226,7 +228,7 @@ export const LexicalRichInput = ({
|
||||
emojiShortcodeResolver={emojiShortcodeResolver}
|
||||
channelId={channel == null ? undefined : channel.id}
|
||||
guildId={channel == null ? undefined : channel.guildId}
|
||||
submitOnEnter={singleLine}
|
||||
submitOnEnter={submitOnEnter ?? singleLine}
|
||||
autocompleteOptions={autocompleteOptions}
|
||||
autocompleteType={autocompleteType}
|
||||
autocompleteQuery={autocompleteQuery}
|
||||
|
||||
@@ -513,6 +513,7 @@ export const ForwardModal = observer(
|
||||
disabled={isCommentComposerDisabled}
|
||||
markdown={true}
|
||||
singleLine={false}
|
||||
submitOnEnter={true}
|
||||
size="form"
|
||||
autocompleteAnchor={containerElement}
|
||||
maxLength={user.maxMessageLength}
|
||||
|
||||
Reference in new issue
Block a user