diff --git a/ts/components/CompositionInput.tsx b/ts/components/CompositionInput.tsx index 3fb396a51..f36308e28 100644 --- a/ts/components/CompositionInput.tsx +++ b/ts/components/CompositionInput.tsx @@ -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); diff --git a/ts/quill/memberRepository.ts b/ts/quill/memberRepository.ts new file mode 100644 index 000000000..09f0f1c6e --- /dev/null +++ b/ts/quill/memberRepository.ts @@ -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; + + private fuse: Fuse; + + constructor(members: Array = []) { + this.members = members; + this.fuse = new Fuse(this.members, FUSE_OPTIONS); + } + + updateMembers(members: Array): void { + this.members = members; + this.fuse = new Fuse(members, FUSE_OPTIONS); + } + + getMembers(omit?: ConversationType): Array { + 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 { + const results = this.fuse.search(`${pattern}`); + + if (omit) { + return results.filter(({ id }) => id !== omit.id); + } + + return results; + } +} diff --git a/ts/quill/mentions/completion.tsx b/ts/quill/mentions/completion.tsx index cf8cb7c33..3bc3e0257 100644 --- a/ts/quill/mentions/completion.tsx +++ b/ts/quill/mentions/completion.tsx @@ -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; diff --git a/ts/quill/mentions/matchers.ts b/ts/quill/mentions/matchers.ts index 5fb87093f..a5b1dd7c1 100644 --- a/ts/quill/mentions/matchers.ts +++ b/ts/quill/mentions/matchers.ts @@ -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 diff --git a/ts/quill/util.ts b/ts/quill/util.ts index d858becad..494ed0c70 100644 --- a/ts/quill/util.ts +++ b/ts/quill/util.ts @@ -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 ): [string, Array] => { @@ -88,49 +78,3 @@ export const getDeltaToRemoveStaleMentions = ( return new Delta(newOps); }; - -export class MemberRepository { - private members: Array; - - private fuse: Fuse; - - constructor(members: Array = []) { - this.members = members; - this.fuse = new Fuse(this.members, FUSE_OPTIONS); - } - - updateMembers(members: Array): void { - this.members = members; - this.fuse = new Fuse(members, FUSE_OPTIONS); - } - - getMembers(omit?: ConversationType): Array { - 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 { - const results = this.fuse.search(pattern); - - if (omit) { - return results.filter(({ id }) => id !== omit.id); - } - - return results; - } -} diff --git a/ts/test/quill/memberRepository_test.ts b/ts/test/quill/memberRepository_test.ts new file mode 100644 index 000000000..729c19fdc --- /dev/null +++ b/ts/test/quill/memberRepository_test.ts @@ -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 = [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, []); + }); + }); + }); +}); diff --git a/ts/test/quill/mentions/completion_test.tsx b/ts/test/quill/mentions/completion_test.tsx index 0a0302900..331b7dc68 100644 --- a/ts/test/quill/mentions/completion_test.tsx +++ b/ts/test/quill/mentions/completion_test.tsx @@ -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 ); diff --git a/ts/test/quill/mentions/matchers_test.ts b/ts/test/quill/mentions/matchers_test.ts index f0a19840e..6a0d44d97 100644 --- a/ts/test/quill/mentions/matchers_test.ts +++ b/ts/test/quill/mentions/matchers_test.ts @@ -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 extends Array { diff --git a/ts/test/quill/util_test.ts b/ts/test/quill/util_test.ts index b2356a361..d4a700b97 100644 --- a/ts/test/quill/util_test.ts +++ b/ts/test/quill/util_test.ts @@ -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 = [ - { - 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']; diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 0a62d7c0d..c9bc08b30 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -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."