Remove fuzzy @mention search
This commit is contained in:
parent
ca83281986
commit
4def45b86a
10 changed files with 211 additions and 152 deletions
|
@ -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);
|
||||
|
|
62
ts/quill/memberRepository.ts
Normal file
62
ts/quill/memberRepository.ts
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
134
ts/test/quill/memberRepository_test.ts
Normal file
134
ts/test/quill/memberRepository_test.ts
Normal 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, []);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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'];
|
||||
|
|
|
@ -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."
|
||||
|
|
Loading…
Reference in a new issue