From ce04c745e51017281fa9daf40138ae5420a37a14 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 23 Jun 2018 01:56:04 +0100 Subject: [PATCH 1/5] strip diacritics for user autocomplete Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/autocomplete/UserProvider.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 85837d5ebb..92b27cd095 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -3,6 +3,7 @@ Copyright 2016 Aviral Dasgupta Copyright 2017 Vector Creations Ltd Copyright 2017, 2018 New Vector Ltd +Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -32,6 +33,10 @@ import type {SelectionRange} from "./Autocompleter"; const USER_REGEX = /@\S*/g; +function stripDiacritics(str: string): string { + return str.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); +} + export default class UserProvider extends AutocompleteProvider { users: Array = null; room: Room = null; @@ -107,7 +112,7 @@ export default class UserProvider extends AutocompleteProvider { const fullMatch = command[0]; // Don't search if the query is a single "@" if (fullMatch && fullMatch !== '@') { - completions = this.matcher.match(fullMatch).map((user) => { + completions = this.matcher.match(stripDiacritics(fullMatch)).map((user) => { const displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done return { // Length of completion should equal length of text in decorator. draft-js @@ -141,8 +146,15 @@ export default class UserProvider extends AutocompleteProvider { } const currentUserId = MatrixClientPeg.get().credentials.userId; - this.users = this.room.getJoinedMembers().filter((member) => { - if (member.userId !== currentUserId) return true; + + this.users = []; + this.room.getJoinedMembers().forEach(({userId, name, ...rest}) => { + if (userId === currentUserId) return; // skip self + this.users.push({ + userId, + name: stripDiacritics(name), + ...rest, + }); }); this.users = _sortBy(this.users, (member) => From 104e8d9cbac47cead2ad46ccfbc0743f34700eaa Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 23 Jun 2018 02:00:36 +0100 Subject: [PATCH 2/5] delint and add flow annotations to make my editor happy Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/autocomplete/UserProvider.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 92b27cd095..9fc59e578d 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -27,9 +27,9 @@ import FuzzyMatcher from './FuzzyMatcher'; import _sortBy from 'lodash/sortBy'; import MatrixClientPeg from '../MatrixClientPeg'; -import type {Room, RoomMember} from 'matrix-js-sdk'; +import type {MatrixEvent, Room, RoomMember, RoomState} from 'matrix-js-sdk'; import {makeUserPermalink} from "../matrix-to"; -import type {SelectionRange} from "./Autocompleter"; +import type {Completion, SelectionRange} from "./Autocompleter"; const USER_REGEX = /@\S*/g; @@ -49,7 +49,7 @@ export default class UserProvider extends AutocompleteProvider { this.matcher = new FuzzyMatcher([], { keys: ['name', 'userId'], shouldMatchPrefix: true, - shouldMatchWordsOnly: false + shouldMatchWordsOnly: false, }); this._onRoomTimelineBound = this._onRoomTimeline.bind(this); @@ -66,7 +66,7 @@ export default class UserProvider extends AutocompleteProvider { } } - _onRoomTimeline(ev, room, toStartOfTimeline, removed, data) { + _onRoomTimeline(ev: MatrixEvent, room: Room, toStartOfTimeline: boolean, removed: boolean, data: Object) { if (!room) return; if (removed) return; if (room.roomId !== this.room.roomId) return; @@ -82,7 +82,7 @@ export default class UserProvider extends AutocompleteProvider { this.onUserSpoke(ev.sender); } - _onRoomStateMember(ev, state, member) { + _onRoomStateMember(ev: MatrixEvent, state: RoomState, member: RoomMember) { // ignore members in other rooms if (member.roomId !== this.room.roomId) { return; @@ -92,7 +92,7 @@ export default class UserProvider extends AutocompleteProvider { this.users = null; } - async getCompletions(query: string, selection: SelectionRange, force?: boolean = false) { + async getCompletions(query: string, selection: SelectionRange, force?: boolean = false): Array { const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar'); // Disable autocompletions when composing commands because of various issues @@ -133,7 +133,7 @@ export default class UserProvider extends AutocompleteProvider { return completions; } - getName() { + getName(): string { return '👥 ' + _t('Users'); } From 54bccd20160356ef96ce17c8ce1f946399582124 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 23 Jun 2018 02:10:31 +0100 Subject: [PATCH 3/5] make Accent Insensitive match more generic and apply to rooms too Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/autocomplete/Autocompleter.js | 4 ++++ src/autocomplete/RoomProvider.js | 4 +++- src/autocomplete/UserProvider.js | 5 +---- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index f5fec4c502..b8988e889f 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -43,6 +43,10 @@ export type Completion = { href: ?string, }; +export function stripDiacritics(str: string): string { + return str.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); +} + const PROVIDERS = [ UserProvider, RoomProvider, diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index c222ae95d4..e4cc9567a0 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -2,6 +2,7 @@ Copyright 2016 Aviral Dasgupta Copyright 2017 Vector Creations Ltd Copyright 2017, 2018 New Vector Ltd +Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -27,6 +28,7 @@ import sdk from '../index'; import _sortBy from 'lodash/sortBy'; import {makeRoomPermalink} from "../matrix-to"; import type {Completion, SelectionRange} from "./Autocompleter"; +import {stripDiacritics} from "./Autocompleter"; const ROOM_REGEX = /(?=#)(\S*)/g; @@ -71,7 +73,7 @@ export default class RoomProvider extends AutocompleteProvider { }; })); const matchedString = command[0]; - completions = this.matcher.match(matchedString); + completions = this.matcher.match(stripDiacritics(matchedString)); completions = _sortBy(completions, [ (c) => score(matchedString, c.displayedAlias), (c) => c.displayedAlias.length, diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 9fc59e578d..c98304fff8 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -30,13 +30,10 @@ import MatrixClientPeg from '../MatrixClientPeg'; import type {MatrixEvent, Room, RoomMember, RoomState} from 'matrix-js-sdk'; import {makeUserPermalink} from "../matrix-to"; import type {Completion, SelectionRange} from "./Autocompleter"; +import {stripDiacritics} from "./Autocompleter"; const USER_REGEX = /@\S*/g; -function stripDiacritics(str: string): string { - return str.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); -} - export default class UserProvider extends AutocompleteProvider { users: Array = null; room: Room = null; From 7cdc91856be44c6d5be92c6ccbe72c6e0259751a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 23 Jun 2018 02:12:01 +0100 Subject: [PATCH 4/5] retain pre-stripped text so it can be used for rendering Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/autocomplete/RoomProvider.js | 3 ++- src/autocomplete/UserProvider.js | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index e4cc9567a0..a72cc3fce2 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -45,7 +45,7 @@ export default class RoomProvider extends AutocompleteProvider { constructor() { super(ROOM_REGEX); this.matcher = new FuzzyMatcher([], { - keys: ['displayedAlias', 'name'], + keys: ['displayedAlias', '_name'], }); } @@ -69,6 +69,7 @@ export default class RoomProvider extends AutocompleteProvider { return { room: room, name: room.name, + _name: stripDiacritics(room.name), displayedAlias: getDisplayAliasForRoom(room), }; })); diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index c98304fff8..42fcb5899b 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -40,11 +40,11 @@ export default class UserProvider extends AutocompleteProvider { constructor(room: Room) { super(USER_REGEX, { - keys: ['name'], + keys: ['_name'], }); this.room = room; this.matcher = new FuzzyMatcher([], { - keys: ['name', 'userId'], + keys: ['_name', 'userId'], shouldMatchPrefix: true, shouldMatchWordsOnly: false, }); @@ -149,7 +149,8 @@ export default class UserProvider extends AutocompleteProvider { if (userId === currentUserId) return; // skip self this.users.push({ userId, - name: stripDiacritics(name), + name, + _name: stripDiacritics(name), ...rest, }); }); From 6a84a7ab323f26e4ab5ee2ba99ced6558b43efdd Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 23 Jun 2018 02:20:41 +0100 Subject: [PATCH 5/5] apply stripDiacritics to QueryMatcher instead of individually Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/autocomplete/Autocompleter.js | 4 ---- src/autocomplete/QueryMatcher.js | 16 +++++++++++----- src/autocomplete/RoomProvider.js | 7 ++----- src/autocomplete/UserProvider.js | 23 +++++------------------ 4 files changed, 18 insertions(+), 32 deletions(-) diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index b8988e889f..f5fec4c502 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -43,10 +43,6 @@ export type Completion = { href: ?string, }; -export function stripDiacritics(str: string): string { - return str.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); -} - const PROVIDERS = [ UserProvider, RoomProvider, diff --git a/src/autocomplete/QueryMatcher.js b/src/autocomplete/QueryMatcher.js index 762b285685..9d4d4d0598 100644 --- a/src/autocomplete/QueryMatcher.js +++ b/src/autocomplete/QueryMatcher.js @@ -1,6 +1,7 @@ //@flow /* Copyright 2017 Aviral Dasgupta +Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -27,6 +28,10 @@ class KeyMap { priorityMap = new Map(); } +function stripDiacritics(str: string): string { + return str.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); +} + export default class QueryMatcher { /** * @param {object[]} objects the objects to perform a match on @@ -46,10 +51,11 @@ export default class QueryMatcher { objects.forEach((object, i) => { const keyValues = _at(object, keys); for (const keyValue of keyValues) { - if (!map.hasOwnProperty(keyValue)) { - map[keyValue] = []; + const key = stripDiacritics(keyValue).toLowerCase(); + if (!map.hasOwnProperty(key)) { + map[key] = []; } - map[keyValue].push(object); + map[key].push(object); } keyMap.priorityMap.set(object, i); }); @@ -82,7 +88,7 @@ export default class QueryMatcher { } match(query: String): Array { - query = query.toLowerCase(); + query = stripDiacritics(query).toLowerCase(); if (this.options.shouldMatchWordsOnly) { query = query.replace(/[^\w]/g, ''); } @@ -91,7 +97,7 @@ export default class QueryMatcher { } const results = []; this.keyMap.keys.forEach((key) => { - let resultKey = key.toLowerCase(); + let resultKey = key; if (this.options.shouldMatchWordsOnly) { resultKey = resultKey.replace(/[^\w]/g, ''); } diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index a72cc3fce2..c222ae95d4 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -2,7 +2,6 @@ Copyright 2016 Aviral Dasgupta Copyright 2017 Vector Creations Ltd Copyright 2017, 2018 New Vector Ltd -Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -28,7 +27,6 @@ import sdk from '../index'; import _sortBy from 'lodash/sortBy'; import {makeRoomPermalink} from "../matrix-to"; import type {Completion, SelectionRange} from "./Autocompleter"; -import {stripDiacritics} from "./Autocompleter"; const ROOM_REGEX = /(?=#)(\S*)/g; @@ -45,7 +43,7 @@ export default class RoomProvider extends AutocompleteProvider { constructor() { super(ROOM_REGEX); this.matcher = new FuzzyMatcher([], { - keys: ['displayedAlias', '_name'], + keys: ['displayedAlias', 'name'], }); } @@ -69,12 +67,11 @@ export default class RoomProvider extends AutocompleteProvider { return { room: room, name: room.name, - _name: stripDiacritics(room.name), displayedAlias: getDisplayAliasForRoom(room), }; })); const matchedString = command[0]; - completions = this.matcher.match(stripDiacritics(matchedString)); + completions = this.matcher.match(matchedString); completions = _sortBy(completions, [ (c) => score(matchedString, c.displayedAlias), (c) => c.displayedAlias.length, diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 42fcb5899b..9c2fb58084 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -30,7 +30,6 @@ import MatrixClientPeg from '../MatrixClientPeg'; import type {MatrixEvent, Room, RoomMember, RoomState} from 'matrix-js-sdk'; import {makeUserPermalink} from "../matrix-to"; import type {Completion, SelectionRange} from "./Autocompleter"; -import {stripDiacritics} from "./Autocompleter"; const USER_REGEX = /@\S*/g; @@ -40,11 +39,11 @@ export default class UserProvider extends AutocompleteProvider { constructor(room: Room) { super(USER_REGEX, { - keys: ['_name'], + keys: ['name'], }); this.room = room; this.matcher = new FuzzyMatcher([], { - keys: ['_name', 'userId'], + keys: ['name', 'userId'], shouldMatchPrefix: true, shouldMatchWordsOnly: false, }); @@ -109,7 +108,7 @@ export default class UserProvider extends AutocompleteProvider { const fullMatch = command[0]; // Don't search if the query is a single "@" if (fullMatch && fullMatch !== '@') { - completions = this.matcher.match(stripDiacritics(fullMatch)).map((user) => { + completions = this.matcher.match(fullMatch).map((user) => { const displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done return { // Length of completion should equal length of text in decorator. draft-js @@ -143,21 +142,9 @@ export default class UserProvider extends AutocompleteProvider { } const currentUserId = MatrixClientPeg.get().credentials.userId; + this.users = this.room.getJoinedMembers().filter(({userId}) => userId !== currentUserId); - this.users = []; - this.room.getJoinedMembers().forEach(({userId, name, ...rest}) => { - if (userId === currentUserId) return; // skip self - this.users.push({ - userId, - name, - _name: stripDiacritics(name), - ...rest, - }); - }); - - this.users = _sortBy(this.users, (member) => - 1E20 - lastSpoken[member.userId] || 1E20, - ); + this.users = _sortBy(this.users, (member) => 1E20 - lastSpoken[member.userId] || 1E20); this.matcher.setObjects(this.users); }