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 type {I18n} from '@lingui/core';
|
||||||
import {msg} from '@lingui/core/macro';
|
import {msg} from '@lingui/core/macro';
|
||||||
import {isForwardableChannelType} from './ForwardChannelEligibility';
|
import {isForwardableChannelType} from './ForwardChannelEligibility';
|
||||||
|
import {matchesForwardChannelSearch} from './ForwardChannelSearchMatch';
|
||||||
|
|
||||||
const GUILD_MESSAGES_DISABLED_DESCRIPTOR = msg({
|
const GUILD_MESSAGES_DISABLED_DESCRIPTOR = msg({
|
||||||
message: 'Sending messages is disabled in this community',
|
message: 'Sending messages is disabled in this community',
|
||||||
@@ -92,6 +93,7 @@ export interface ForwardChannelObservation {
|
|||||||
readonly guildMessagesDisabled: boolean;
|
readonly guildMessagesDisabled: boolean;
|
||||||
readonly guildName: string | null;
|
readonly guildName: string | null;
|
||||||
readonly memberTimedOut: boolean;
|
readonly memberTimedOut: boolean;
|
||||||
|
readonly searchAliases: ReadonlyArray<string>;
|
||||||
readonly slowmodeEnabled: boolean;
|
readonly slowmodeEnabled: boolean;
|
||||||
readonly slowmodeRemainingMs: number;
|
readonly slowmodeRemainingMs: number;
|
||||||
}
|
}
|
||||||
@@ -113,6 +115,7 @@ interface IndexedForwardChannelOption extends ForwardChannelOption {
|
|||||||
readonly isPersonalNotes: boolean;
|
readonly isPersonalNotes: boolean;
|
||||||
readonly isSource: boolean;
|
readonly isSource: boolean;
|
||||||
readonly recentRank: number | null;
|
readonly recentRank: number | null;
|
||||||
|
readonly searchAliasValues: ReadonlyArray<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BuildForwardChannelIndexRequest {
|
interface BuildForwardChannelIndexRequest {
|
||||||
@@ -194,10 +197,7 @@ export class ForwardChannelIndex {
|
|||||||
readonly personalNotesSearchValue: string;
|
readonly personalNotesSearchValue: string;
|
||||||
readonly option: IndexedForwardChannelOption;
|
readonly option: IndexedForwardChannelOption;
|
||||||
}): boolean {
|
}): boolean {
|
||||||
if (option.isPersonalNotes && personalNotesSearchValue.includes(normalizedQuery)) return true;
|
return matchesForwardChannelSearch({normalizedQuery, personalNotesSearchValue, values: option});
|
||||||
if (option.displayNameSearchValue.includes(normalizedQuery)) return true;
|
|
||||||
if (option.channelNameSearchValue.includes(normalizedQuery)) return true;
|
|
||||||
return option.guildNameSearchValue.includes(normalizedQuery);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildOption({
|
private buildOption({
|
||||||
@@ -232,6 +232,7 @@ export class ForwardChannelIndex {
|
|||||||
isPersonalNotes: observation.channel.type === ChannelTypes.DM_PERSONAL_NOTES,
|
isPersonalNotes: observation.channel.type === ChannelTypes.DM_PERSONAL_NOTES,
|
||||||
isSource: observation.channel.id === excludedChannelId,
|
isSource: observation.channel.id === excludedChannelId,
|
||||||
recentRank,
|
recentRank,
|
||||||
|
searchAliasValues: Object.freeze(observation.searchAliases.map((alias) => alias.toLowerCase())),
|
||||||
slowmodeEnabled: observation.slowmodeEnabled,
|
slowmodeEnabled: observation.slowmodeEnabled,
|
||||||
slowmodeRemainingMs: observation.slowmodeRemainingMs,
|
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 Guilds from '@app/features/guild/state/Guilds';
|
||||||
import GuildMembers from '@app/features/member/state/GuildMembers';
|
import GuildMembers from '@app/features/member/state/GuildMembers';
|
||||||
import Permission from '@app/features/permissions/state/Permission';
|
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 Slowmode from '@app/features/slowmode/state/Slowmode';
|
||||||
import {useNow} from '@app/features/ui/state/Tick';
|
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 {ChannelTypes, Permissions} from '@fluxer/constants/src/ChannelConstants';
|
||||||
import {GuildOperations} from '@fluxer/constants/src/GuildConstants';
|
import {GuildOperations} from '@fluxer/constants/src/GuildConstants';
|
||||||
import {CHANNEL_RATE_LIMIT_PER_USER_MAX} from '@fluxer/constants/src/LimitConstants';
|
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 {computed, type IComputedValue} from 'mobx';
|
||||||
import {useMemo, useRef} from 'react';
|
import {useMemo, useRef} from 'react';
|
||||||
|
|
||||||
|
const EMPTY_SEARCH_ALIASES: ReadonlyArray<string> = Object.freeze([]);
|
||||||
|
|
||||||
const CHANNEL_DESCRIPTOR = msg({
|
const CHANNEL_DESCRIPTOR = msg({
|
||||||
message: 'Channel {id}',
|
message: 'Channel {id}',
|
||||||
comment: 'Short label in the settings dialog forward channel selection. Preserve {id}; it is inserted by code.',
|
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 guildMessagesDisabled: boolean;
|
||||||
readonly guildName: string | null;
|
readonly guildName: string | null;
|
||||||
readonly memberTimedOut: boolean;
|
readonly memberTimedOut: boolean;
|
||||||
|
readonly searchAliases: ReadonlyArray<string>;
|
||||||
readonly slowmodeEnabled: boolean;
|
readonly slowmodeEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,6 +60,10 @@ interface ForwardChannelObservationSelection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveChannelDisplayName(channel: Channel, i18n: I18n): string {
|
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 (
|
if (
|
||||||
channel.type === ChannelTypes.DM_PERSONAL_NOTES ||
|
channel.type === ChannelTypes.DM_PERSONAL_NOTES ||
|
||||||
channel.type === ChannelTypes.DM ||
|
channel.type === ChannelTypes.DM ||
|
||||||
@@ -66,6 +75,28 @@ function resolveChannelDisplayName(channel: Channel, i18n: I18n): string {
|
|||||||
return i18n._(CHANNEL_DESCRIPTOR, {id: channel.id});
|
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 {
|
function resolveChannelCategoryName(channel: Channel): string | null {
|
||||||
if (!channel.parentId) return null;
|
if (!channel.parentId) return null;
|
||||||
const category = Channels.getChannel(channel.parentId);
|
const category = Channels.getChannel(channel.parentId);
|
||||||
@@ -99,6 +130,8 @@ function areForwardChannelObservationBasesEqual(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (left.canSendMessages !== right.canSendMessages || left.canEmbedLinks !== right.canEmbedLinks) 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;
|
return left.canAttachFiles === right.canAttachFiles && left.slowmodeEnabled === right.slowmodeEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,6 +218,7 @@ class ForwardChannelObservationsOwner {
|
|||||||
guildMessagesDisabled: false,
|
guildMessagesDisabled: false,
|
||||||
guildName: null,
|
guildName: null,
|
||||||
memberTimedOut: false,
|
memberTimedOut: false,
|
||||||
|
searchAliases: collectRecipientSearchAliases(channel),
|
||||||
slowmodeEnabled: false,
|
slowmodeEnabled: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -213,6 +247,7 @@ class ForwardChannelObservationsOwner {
|
|||||||
guildMessagesDisabled,
|
guildMessagesDisabled,
|
||||||
guildName,
|
guildName,
|
||||||
memberTimedOut: isCurrentMemberTimedOut(channel, this.request.currentUserId),
|
memberTimedOut: isCurrentMemberTimedOut(channel, this.request.currentUserId),
|
||||||
|
searchAliases: collectRecipientSearchAliases(channel),
|
||||||
slowmodeEnabled,
|
slowmodeEnabled,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -243,6 +278,7 @@ function buildForwardChannelObservations(
|
|||||||
displayName: base.displayName,
|
displayName: base.displayName,
|
||||||
guildMessagesDisabled: base.guildMessagesDisabled,
|
guildMessagesDisabled: base.guildMessagesDisabled,
|
||||||
guildName: base.guildName,
|
guildName: base.guildName,
|
||||||
|
searchAliases: base.searchAliases,
|
||||||
memberTimedOut: base.memberTimedOut,
|
memberTimedOut: base.memberTimedOut,
|
||||||
slowmodeEnabled: base.slowmodeEnabled,
|
slowmodeEnabled: base.slowmodeEnabled,
|
||||||
slowmodeRemainingMs: remainingMs === undefined ? 0 : remainingMs,
|
slowmodeRemainingMs: remainingMs === undefined ? 0 : remainingMs,
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export interface LexicalRichInputProps {
|
|||||||
richInputRef?: React.Ref<LexicalRichInputHandle>;
|
richInputRef?: React.Ref<LexicalRichInputHandle>;
|
||||||
onChange?: (display: string, segments: Array<MentionSegment>, wire: string) => void;
|
onChange?: (display: string, segments: Array<MentionSegment>, wire: string) => void;
|
||||||
onSubmit?: () => void;
|
onSubmit?: () => void;
|
||||||
|
submitOnEnter?: boolean;
|
||||||
onKeyDown?: (event: React.KeyboardEvent<HTMLElement>) => void;
|
onKeyDown?: (event: React.KeyboardEvent<HTMLElement>) => void;
|
||||||
onFocus?: () => void;
|
onFocus?: () => void;
|
||||||
onBlur?: () => void;
|
onBlur?: () => void;
|
||||||
@@ -85,6 +86,7 @@ export const LexicalRichInput = ({
|
|||||||
richInputRef,
|
richInputRef,
|
||||||
onChange,
|
onChange,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
submitOnEnter,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
onFocus,
|
onFocus,
|
||||||
onBlur,
|
onBlur,
|
||||||
@@ -226,7 +228,7 @@ export const LexicalRichInput = ({
|
|||||||
emojiShortcodeResolver={emojiShortcodeResolver}
|
emojiShortcodeResolver={emojiShortcodeResolver}
|
||||||
channelId={channel == null ? undefined : channel.id}
|
channelId={channel == null ? undefined : channel.id}
|
||||||
guildId={channel == null ? undefined : channel.guildId}
|
guildId={channel == null ? undefined : channel.guildId}
|
||||||
submitOnEnter={singleLine}
|
submitOnEnter={submitOnEnter ?? singleLine}
|
||||||
autocompleteOptions={autocompleteOptions}
|
autocompleteOptions={autocompleteOptions}
|
||||||
autocompleteType={autocompleteType}
|
autocompleteType={autocompleteType}
|
||||||
autocompleteQuery={autocompleteQuery}
|
autocompleteQuery={autocompleteQuery}
|
||||||
|
|||||||
@@ -513,6 +513,7 @@ export const ForwardModal = observer(
|
|||||||
disabled={isCommentComposerDisabled}
|
disabled={isCommentComposerDisabled}
|
||||||
markdown={true}
|
markdown={true}
|
||||||
singleLine={false}
|
singleLine={false}
|
||||||
|
submitOnEnter={true}
|
||||||
size="form"
|
size="form"
|
||||||
autocompleteAnchor={containerElement}
|
autocompleteAnchor={containerElement}
|
||||||
maxLength={user.maxMessageLength}
|
maxLength={user.maxMessageLength}
|
||||||
|
|||||||
Reference in new issue
Block a user