signal-desktop/ts/quill/memberRepository.ts

125 lines
3.4 KiB
TypeScript
Raw Normal View History

2023-01-03 19:55:46 +00:00
// Copyright 2020 Signal Messenger, LLC
2020-11-04 22:04:48 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
import Fuse from 'fuse.js';
import { get } from 'lodash';
2020-11-04 22:04:48 +00:00
import type { ConversationType } from '../state/ducks/conversations';
2023-08-17 18:48:42 +00:00
import type { AciString } from '../types/ServiceId';
2023-09-14 17:04:48 +00:00
import { isAciString } from '../util/isAciString';
2021-08-17 18:55:25 +00:00
import { filter, map } from '../util/iterables';
2023-06-14 21:24:34 +00:00
import { removeDiacritics } from '../util/removeDiacritics';
2023-08-17 18:48:42 +00:00
import { isNotNil } from '../util/isNotNil';
export type MemberType = Omit<ConversationType, 'serviceId'> &
Readonly<{
aci: AciString;
}>;
function toMember({
serviceId,
...restOfConvo
}: ConversationType): MemberType | undefined {
if (!isAciString(serviceId)) {
return undefined;
}
return {
...restOfConvo,
aci: serviceId,
};
}
// Exported for testing
export function _toMembers(
conversations: ReadonlyArray<ConversationType>
): Array<MemberType> {
return conversations.map(toMember).filter(isNotNil);
}
2020-11-04 22:04:48 +00:00
const FUSE_OPTIONS = {
location: 0,
shouldSort: true,
threshold: 0,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: ['name', 'firstName', 'profileName', 'title'],
2021-08-17 18:55:25 +00:00
getFn(
2023-08-17 18:48:42 +00:00
conversation: Readonly<MemberType>,
path: string | Array<string>
2021-08-17 18:55:25 +00:00
): ReadonlyArray<string> | string {
// It'd be nice to avoid this cast, but Fuse's types don't allow it.
const rawValue = get(conversation as Record<string, unknown>, path);
2021-08-17 18:55:25 +00:00
if (typeof rawValue !== 'string') {
// It might make more sense to return `undefined` here, but [Fuse's types don't
// allow it in newer versions][0] so we just return the empty string.
//
// [0]: https://github.com/krisk/Fuse/blob/e5e3abb44e004662c98750d0964d2d9a73b87848/src/index.d.ts#L117
return '';
}
const segmenter = new Intl.Segmenter(undefined, { granularity: 'word' });
2023-06-14 21:24:34 +00:00
const segments = segmenter.segment(removeDiacritics(rawValue));
2021-08-17 18:55:25 +00:00
const wordlikeSegments = filter(segments, segment => segment.isWordLike);
const wordlikes = map(wordlikeSegments, segment => segment.segment);
return Array.from(wordlikes);
},
2020-11-04 22:04:48 +00:00
};
export class MemberRepository {
2023-08-17 18:48:42 +00:00
private members: ReadonlyArray<MemberType>;
2021-08-17 18:55:25 +00:00
private isFuseReady = false;
2020-11-04 22:04:48 +00:00
2023-08-17 18:48:42 +00:00
private fuse = new Fuse<MemberType>([], FUSE_OPTIONS);
2020-11-04 22:04:48 +00:00
2023-08-17 18:48:42 +00:00
constructor(conversations: ReadonlyArray<ConversationType> = []) {
this.members = _toMembers(conversations);
}
2020-11-04 22:04:48 +00:00
2023-08-17 18:48:42 +00:00
updateMembers(conversations: ReadonlyArray<ConversationType>): void {
this.members = _toMembers(conversations);
2021-08-17 18:55:25 +00:00
this.isFuseReady = false;
2020-11-04 22:04:48 +00:00
}
2023-08-17 18:48:42 +00:00
getMembers(omit?: Pick<MemberType, 'id'>): ReadonlyArray<MemberType> {
2020-11-04 22:04:48 +00:00
if (omit) {
return this.members.filter(({ id }) => id !== omit.id);
}
return this.members;
}
2023-08-17 18:48:42 +00:00
getMemberById(id?: string): MemberType | undefined {
2020-11-04 22:04:48 +00:00
return id
? this.members.find(({ id: memberId }) => memberId === id)
: undefined;
}
2023-08-17 18:48:42 +00:00
getMemberByAci(aci?: AciString): MemberType | undefined {
return aci
? this.members.find(({ aci: memberAci }) => memberAci === aci)
2020-11-04 22:04:48 +00:00
: undefined;
}
search(
pattern: string,
2023-08-17 18:48:42 +00:00
omit?: Pick<MemberType, 'id'>
): ReadonlyArray<MemberType> {
2021-08-17 18:55:25 +00:00
if (!this.isFuseReady) {
this.fuse.setCollection(this.members);
this.isFuseReady = true;
}
2023-06-14 21:24:34 +00:00
const results = this.fuse
.search(removeDiacritics(pattern))
.map(result => result.item);
2020-11-04 22:04:48 +00:00
if (omit) {
return results.filter(({ id }) => id !== omit.id);
}
return results;
}
}