Skip to content

Commit

Permalink
Remove fuzzy @mention search
Browse files Browse the repository at this point in the history
  • Loading branch information
crsven committed Nov 4, 2020
1 parent ca83281 commit 4def45b
Show file tree
Hide file tree
Showing 10 changed files with 211 additions and 152 deletions.
2 changes: 1 addition & 1 deletion ts/components/CompositionInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
62 changes: 62 additions & 0 deletions ts/quill/memberRepository.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
3 changes: 2 additions & 1 deletion ts/quill/mentions/completion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion ts/quill/mentions/matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
56 changes: 0 additions & 56 deletions ts/quill/util.ts
Original file line number Diff line number Diff line change
@@ -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>] => {
Expand Down Expand Up @@ -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 changes: 134 additions & 0 deletions ts/test/quill/memberRepository_test.ts
Original file line number Diff line number Diff line change
@@ -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, []);
});
});
});
});
4 changes: 2 additions & 2 deletions ts/test/quill/mentions/completion_test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
);
Expand Down
2 changes: 1 addition & 1 deletion ts/test/quill/mentions/matchers_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down

0 comments on commit 4def45b

Please sign in to comment.