Remove fuzzy @mention search

This commit is contained in:
Chris Svenningsen 2020-11-04 14:04:48 -08:00 committed by GitHub
parent ca83281986
commit 4def45b86a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 211 additions and 152 deletions

View File

@ -25,10 +25,10 @@ import {
} from '../quill/emoji/matchers';
import { matchMention } from '../quill/mentions/matchers';
import {
MemberRepository,
getDeltaToRemoveStaleMentions,
getTextAndMentionsFromOps,
} from '../quill/util';
import { MemberRepository } from '../quill/memberRepository';
Quill.register('formats/emoji', EmojiBlot);
Quill.register('formats/mention', MentionBlot);

View File

@ -0,0 +1,62 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import Fuse from 'fuse.js';
import { ConversationType } from '../state/ducks/conversations';
const FUSE_OPTIONS = {
location: 0,
shouldSort: true,
threshold: 0,
maxPatternLength: 32,
minMatchCharLength: 1,
tokenize: true,
keys: ['name', 'firstName', 'profileName', 'title'],
};
export class MemberRepository {
private members: Array<ConversationType>;
private fuse: Fuse<ConversationType>;
constructor(members: Array<ConversationType> = []) {
this.members = members;
this.fuse = new Fuse<ConversationType>(this.members, FUSE_OPTIONS);
}
updateMembers(members: Array<ConversationType>): void {
this.members = members;
this.fuse = new Fuse(members, FUSE_OPTIONS);
}
getMembers(omit?: ConversationType): Array<ConversationType> {
if (omit) {
return this.members.filter(({ id }) => id !== omit.id);
}
return this.members;
}
getMemberById(id?: string): ConversationType | undefined {
return id
? this.members.find(({ id: memberId }) => memberId === id)
: undefined;
}
getMemberByUuid(uuid?: string): ConversationType | undefined {
return uuid
? this.members.find(({ uuid: memberUuid }) => memberUuid === uuid)
: undefined;
}
search(pattern: string, omit?: ConversationType): Array<ConversationType> {
const results = this.fuse.search(`${pattern}`);
if (omit) {
return results.filter(({ id }) => id !== omit.id);
}
return results;
}
}

View File

@ -11,7 +11,8 @@ import { createPortal } from 'react-dom';
import { ConversationType } from '../../state/ducks/conversations';
import { Avatar } from '../../components/Avatar';
import { LocalizerType } from '../../types/Util';
import { MemberRepository } from '../util';
import { MemberRepository } from '../memberRepository';
export interface MentionCompletionOptions {
i18n: LocalizerType;

View File

@ -3,7 +3,7 @@
import Delta from 'quill-delta';
import { RefObject } from 'react';
import { MemberRepository } from '../util';
import { MemberRepository } from '../memberRepository';
export const matchMention = (
memberRepositoryRef: RefObject<MemberRepository>

View File

@ -1,21 +1,11 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import Fuse from 'fuse.js';
import Delta from 'quill-delta';
import { DeltaOperation } from 'quill';
import { ConversationType } from '../state/ducks/conversations';
import { BodyRangeType } from '../types/Util';
const FUSE_OPTIONS = {
shouldSort: true,
threshold: 0.2,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: ['name', 'firstName', 'profileName', 'title'],
};
export const getTextAndMentionsFromOps = (
ops: Array<DeltaOperation>
): [string, Array<BodyRangeType>] => {
@ -88,49 +78,3 @@ export const getDeltaToRemoveStaleMentions = (
return new Delta(newOps);
};
export class MemberRepository {
private members: Array<ConversationType>;
private fuse: Fuse<ConversationType>;
constructor(members: Array<ConversationType> = []) {
this.members = members;
this.fuse = new Fuse<ConversationType>(this.members, FUSE_OPTIONS);
}
updateMembers(members: Array<ConversationType>): void {
this.members = members;
this.fuse = new Fuse(members, FUSE_OPTIONS);
}
getMembers(omit?: ConversationType): Array<ConversationType> {
if (omit) {
return this.members.filter(({ id }) => id !== omit.id);
}
return this.members;
}
getMemberById(id?: string): ConversationType | undefined {
return id
? this.members.find(({ id: memberId }) => memberId === id)
: undefined;
}
getMemberByUuid(uuid?: string): ConversationType | undefined {
return uuid
? this.members.find(({ uuid: memberUuid }) => memberUuid === uuid)
: undefined;
}
search(pattern: string, omit?: ConversationType): Array<ConversationType> {
const results = this.fuse.search(pattern);
if (omit) {
return results.filter(({ id }) => id !== omit.id);
}
return results;
}
}

View File

@ -0,0 +1,134 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { ConversationType } from '../../state/ducks/conversations';
import { MemberRepository } from '../../quill/memberRepository';
const memberMahershala: ConversationType = {
id: '555444',
uuid: 'abcdefg',
title: 'Pal',
firstName: 'Mahershala',
profileName: 'Mr Ali',
name: 'Friend',
type: 'direct',
lastUpdated: Date.now(),
markedUnread: false,
};
const memberShia: ConversationType = {
id: '333222',
uuid: 'hijklmno',
title: 'Buddy',
firstName: 'Shia',
profileName: 'Sr LaBeouf',
name: 'Duder',
type: 'direct',
lastUpdated: Date.now(),
markedUnread: false,
};
const members: Array<ConversationType> = [memberMahershala, memberShia];
const singleMember: ConversationType = {
id: '666777',
uuid: 'pqrstuv',
title: 'The Guy',
firstName: 'Jeff',
profileName: 'Jr Klaus',
name: 'Him',
type: 'direct',
lastUpdated: Date.now(),
markedUnread: false,
};
describe('MemberRepository', () => {
describe('#updateMembers', () => {
it('updates with given members', () => {
const memberRepository = new MemberRepository(members);
assert.deepEqual(memberRepository.getMembers(), members);
const updatedMembers = [...members, singleMember];
memberRepository.updateMembers(updatedMembers);
assert.deepEqual(memberRepository.getMembers(), updatedMembers);
});
});
describe('#getMemberById', () => {
it('returns undefined when there is no search id', () => {
const memberRepository = new MemberRepository(members);
assert.isUndefined(memberRepository.getMemberById());
});
it('returns a matched member', () => {
const memberRepository = new MemberRepository(members);
assert.isDefined(memberRepository.getMemberById('555444'));
});
it('returns undefined when it does not have the member', () => {
const memberRepository = new MemberRepository(members);
assert.isUndefined(memberRepository.getMemberById('nope'));
});
});
describe('#getMemberByUuid', () => {
it('returns undefined when there is no search uuid', () => {
const memberRepository = new MemberRepository(members);
assert.isUndefined(memberRepository.getMemberByUuid());
});
it('returns a matched member', () => {
const memberRepository = new MemberRepository(members);
assert.isDefined(memberRepository.getMemberByUuid('abcdefg'));
});
it('returns undefined when it does not have the member', () => {
const memberRepository = new MemberRepository(members);
assert.isUndefined(memberRepository.getMemberByUuid('nope'));
});
});
describe('#search', () => {
describe('given a prefix-matching string on last name', () => {
it('returns the match', () => {
const memberRepository = new MemberRepository(members);
const results = memberRepository.search('a');
assert.deepEqual(results, [memberMahershala]);
});
});
describe('given a prefix-matching string on first name', () => {
it('returns the match', () => {
const memberRepository = new MemberRepository(members);
const results = memberRepository.search('ma');
assert.deepEqual(results, [memberMahershala]);
});
});
describe('given a prefix-matching string on profile name', () => {
it('returns the match', () => {
const memberRepository = new MemberRepository(members);
const results = memberRepository.search('sr');
assert.deepEqual(results, [memberShia]);
});
});
describe('given a prefix-matching string on title', () => {
it('returns the match', () => {
const memberRepository = new MemberRepository(members);
const results = memberRepository.search('d');
assert.deepEqual(results, [memberShia]);
});
});
describe('given a match in the middle of a name', () => {
it('returns zero matches', () => {
const memberRepository = new MemberRepository(members);
const results = memberRepository.search('e');
assert.deepEqual(results, []);
});
});
});
});

View File

@ -10,7 +10,7 @@ import {
MentionCompletionOptions,
} from '../../../quill/mentions/completion';
import { ConversationType } from '../../../state/ducks/conversations';
import { MemberRepository } from '../../../quill/util';
import { MemberRepository } from '../../../quill/memberRepository';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const globalAsAny = global as any;
@ -222,7 +222,7 @@ describe('mentionCompletion', () => {
});
it('stores the results, omitting `me`, and renders', () => {
expect(mentionCompletion.results).to.have.lengthOf(2);
expect(mentionCompletion.results).to.have.lengthOf(1);
expect((mentionCompletion.render as sinon.SinonStub).called).to.equal(
true
);

View File

@ -6,7 +6,7 @@ import { RefObject } from 'react';
import Delta from 'quill-delta';
import { matchMention } from '../../../quill/mentions/matchers';
import { MemberRepository } from '../../../quill/util';
import { MemberRepository } from '../../../quill/memberRepository';
import { ConversationType } from '../../../state/ducks/conversations';
class FakeTokenList<T> extends Array<T> {

View File

@ -2,93 +2,11 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import {
MemberRepository,
getDeltaToRemoveStaleMentions,
getTextAndMentionsFromOps,
} from '../../quill/util';
import { ConversationType } from '../../state/ducks/conversations';
const members: Array<ConversationType> = [
{
id: '555444',
uuid: 'abcdefg',
title: 'Mahershala Ali',
firstName: 'Mahershala',
profileName: 'Mahershala A.',
type: 'direct',
lastUpdated: Date.now(),
markedUnread: false,
},
{
id: '333222',
uuid: 'hijklmno',
title: 'Shia LaBeouf',
firstName: 'Shia',
profileName: 'Shia L.',
type: 'direct',
lastUpdated: Date.now(),
markedUnread: false,
},
];
const singleMember: ConversationType = {
id: '666777',
uuid: 'pqrstuv',
title: 'Fred Savage',
firstName: 'Fred',
profileName: 'Fred S.',
type: 'direct',
lastUpdated: Date.now(),
markedUnread: false,
};
describe('MemberRepository', () => {
describe('#updateMembers', () => {
it('updates with given members', () => {
const memberRepository = new MemberRepository(members);
assert.deepEqual(memberRepository.getMembers(), members);
const updatedMembers = [...members, singleMember];
memberRepository.updateMembers(updatedMembers);
assert.deepEqual(memberRepository.getMembers(), updatedMembers);
});
});
describe('#getMemberById', () => {
it('returns undefined when there is no search id', () => {
const memberRepository = new MemberRepository(members);
assert.isUndefined(memberRepository.getMemberById());
});
it('returns a matched member', () => {
const memberRepository = new MemberRepository(members);
assert.isDefined(memberRepository.getMemberById('555444'));
});
it('returns undefined when it does not have the member', () => {
const memberRepository = new MemberRepository(members);
assert.isUndefined(memberRepository.getMemberById('nope'));
});
});
describe('#getMemberByUuid', () => {
it('returns undefined when there is no search uuid', () => {
const memberRepository = new MemberRepository(members);
assert.isUndefined(memberRepository.getMemberByUuid());
});
it('returns a matched member', () => {
const memberRepository = new MemberRepository(members);
assert.isDefined(memberRepository.getMemberByUuid('abcdefg'));
});
it('returns undefined when it does not have the member', () => {
const memberRepository = new MemberRepository(members);
assert.isUndefined(memberRepository.getMemberByUuid('nope'));
});
});
});
describe('getDeltaToRemoveStaleMentions', () => {
const memberUuids = ['abcdef', 'ghijkl'];

View File

@ -14544,7 +14544,7 @@
"rule": "React-useRef",
"path": "ts/components/CompositionInput.js",
"line": " const emojiCompletionRef = React.useRef();",
"lineNumber": 42,
"lineNumber": 43,
"reasonCategory": "falseMatch",
"updated": "2020-10-26T19:12:24.410Z",
"reasonDetail": "Doesn't refer to a DOM element."
@ -14553,7 +14553,7 @@
"rule": "React-useRef",
"path": "ts/components/CompositionInput.js",
"line": " const mentionCompletionRef = React.useRef();",
"lineNumber": 43,
"lineNumber": 44,
"reasonCategory": "falseMatch",
"updated": "2020-10-26T23:54:34.273Z",
"reasonDetail": "Doesn't refer to a DOM element."
@ -14562,7 +14562,7 @@
"rule": "React-useRef",
"path": "ts/components/CompositionInput.js",
"line": " const quillRef = React.useRef();",
"lineNumber": 44,
"lineNumber": 45,
"reasonCategory": "falseMatch",
"updated": "2020-10-26T19:12:24.410Z",
"reasonDetail": "Doesn't refer to a DOM element."
@ -14571,7 +14571,7 @@
"rule": "React-useRef",
"path": "ts/components/CompositionInput.js",
"line": " const scrollerRef = React.useRef(null);",
"lineNumber": 45,
"lineNumber": 46,
"reasonCategory": "usageTrusted",
"updated": "2020-10-26T19:12:24.410Z",
"reasonDetail": "Used with Quill for scrolling."
@ -14580,7 +14580,7 @@
"rule": "React-useRef",
"path": "ts/components/CompositionInput.js",
"line": " const propsRef = React.useRef(props);",
"lineNumber": 46,
"lineNumber": 47,
"reasonCategory": "falseMatch",
"updated": "2020-10-26T19:12:24.410Z",
"reasonDetail": "Doesn't refer to a DOM element."
@ -14588,8 +14588,8 @@
{
"rule": "React-useRef",
"path": "ts/components/CompositionInput.js",
"line": " const memberRepositoryRef = React.useRef(new util_1.MemberRepository());",
"lineNumber": 47,
"line": " const memberRepositoryRef = React.useRef(new memberRepository_1.MemberRepository());",
"lineNumber": 48,
"reasonCategory": "falseMatch",
"updated": "2020-10-26T23:56:13.482Z",
"reasonDetail": "Doesn't refer to a DOM element."