diff --git a/.flowconfig b/.flowconfig new file mode 100644 index 0000000000..81770c6585 --- /dev/null +++ b/.flowconfig @@ -0,0 +1,6 @@ +[include] +src/**/*.js +test/**/*.js + +[ignore] +node_modules/ diff --git a/src/autocomplete/FuzzyMatcher.js b/src/autocomplete/FuzzyMatcher.js index bd19fc53e8..c22e2a1101 100644 --- a/src/autocomplete/FuzzyMatcher.js +++ b/src/autocomplete/FuzzyMatcher.js @@ -14,7 +14,12 @@ class KeyMap { const DEFAULT_RESULT_COUNT = 10; const DEFAULT_DISTANCE = 5; -export default class FuzzyMatcher { +// FIXME Until Fuzzy matching works better, we use prefix matching. + +import PrefixMatcher from './QueryMatcher'; +export default PrefixMatcher; + +class FuzzyMatcher { /** * Given an array of objects and keys, returns a KeyMap * Keys can refer to object properties by name and as in JavaScript (for nested properties) diff --git a/src/autocomplete/QueryMatcher.js b/src/autocomplete/QueryMatcher.js new file mode 100644 index 0000000000..b4c27a7179 --- /dev/null +++ b/src/autocomplete/QueryMatcher.js @@ -0,0 +1,62 @@ +//@flow + +import _at from 'lodash/at'; +import _flatMap from 'lodash/flatMap'; +import _sortBy from 'lodash/sortBy'; +import _sortedUniq from 'lodash/sortedUniq'; +import _keys from 'lodash/keys'; + +class KeyMap { + keys: Array<String>; + objectMap: {[String]: Array<Object>}; + priorityMap = new Map(); +} + +export default class QueryMatcher { + /** + * Given an array of objects and keys, returns a KeyMap + * Keys can refer to object properties by name and as in JavaScript (for nested properties) + * + * To use, simply presort objects by required criteria, run through this function and create a QueryMatcher with the + * resulting KeyMap. + * + * TODO: Handle arrays and objects (Fuse did this, RoomProvider uses it) + */ + static valuesToKeyMap(objects: Array<Object>, keys: Array<String>): KeyMap { + const keyMap = new KeyMap(); + const map = {}; + + objects.forEach((object, i) => { + const keyValues = _at(object, keys); + for (const keyValue of keyValues) { + if (!map.hasOwnProperty(keyValue)) { + map[keyValue] = []; + } + map[keyValue].push(object); + } + keyMap.priorityMap.set(object, i); + }); + + keyMap.objectMap = map; + keyMap.keys = _keys(map); + return keyMap; + } + + constructor(objects: Array<Object>, options: {[Object]: Object} = {}) { + this.options = options; + this.keys = options.keys; + this.setObjects(objects); + } + + setObjects(objects: Array<Object>) { + this.keyMap = QueryMatcher.valuesToKeyMap(objects, this.keys); + } + + match(query: String): Array<Object> { + query = query.toLowerCase().replace(/[^\w]/g, ''); + const results = _sortedUniq(_sortBy(_flatMap(this.keyMap.keys, (key) => { + return key.toLowerCase().replace(/[^\w]/g, '').indexOf(query) >= 0 ? this.keyMap.objectMap[key] : []; + }), (candidate) => this.keyMap.priorityMap.get(candidate))); + return results; + } +} diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index b65439181c..589dfec9fa 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -1,20 +1,27 @@ +//@flow import React from 'react'; import AutocompleteProvider from './AutocompleteProvider'; import Q from 'q'; import {PillCompletion} from './Components'; import sdk from '../index'; import FuzzyMatcher from './FuzzyMatcher'; +import _pull from 'lodash/pull'; +import _sortBy from 'lodash/sortBy'; +import MatrixClientPeg from '../MatrixClientPeg'; + +import type {Room, RoomMember} from 'matrix-js-sdk'; const USER_REGEX = /@\S*/g; let instance = null; export default class UserProvider extends AutocompleteProvider { + users: Array<RoomMember> = []; + constructor() { super(USER_REGEX, { keys: ['name', 'userId'], }); - this.users = []; this.matcher = new FuzzyMatcher([], { keys: ['name', 'userId'], }); @@ -53,8 +60,30 @@ export default class UserProvider extends AutocompleteProvider { return '👥 Users'; } - setUserList(users) { - this.users = users; + setUserListFromRoom(room: Room) { + const events = room.getLiveTimeline().getEvents(); + const lastSpoken = {}; + + for(const event of events) { + lastSpoken[event.getSender()] = event.getTs(); + } + + const currentUserId = MatrixClientPeg.get().credentials.userId; + this.users = room.getJoinedMembers().filter((member) => { + if (member.userId !== currentUserId) return true; + }); + + this.users = _sortBy(this.users, (user) => 1E20 - lastSpoken[user.userId] || 1E20); + + this.matcher.setObjects(this.users); + } + + onUserSpoke(user: RoomMember) { + if(user.userId === MatrixClientPeg.get().credentials.userId) return; + + // Probably unsafe to compare by reference here? + _pull(this.users, user); + this.users.splice(0, 0, user); this.matcher.setObjects(this.users); } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 696d15f84a..936d88c0ee 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -225,7 +225,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().credentials.userId, 'join' ); - this._updateAutoComplete(); + UserProvider.getInstance().setUserListFromRoom(this.state.room); this.tabComplete.loadEntries(this.state.room); } @@ -479,8 +479,7 @@ module.exports = React.createClass({ // and that has probably just changed if (ev.sender) { this.tabComplete.onMemberSpoke(ev.sender); - // nb. we don't need to update the new autocomplete here since - // its results are currently ordered purely by search score. + UserProvider.getInstance().onUserSpoke(ev.sender); } }, @@ -658,7 +657,7 @@ module.exports = React.createClass({ // refresh the tab complete list this.tabComplete.loadEntries(this.state.room); - this._updateAutoComplete(); + UserProvider.getInstance().setUserListFromRoom(this.state.room); // if we are now a member of the room, where we were not before, that // means we have finished joining a room we were previously peeking @@ -1437,14 +1436,6 @@ module.exports = React.createClass({ } }, - _updateAutoComplete: function() { - const myUserId = MatrixClientPeg.get().credentials.userId; - const members = this.state.room.getJoinedMembers().filter(function(member) { - if (member.userId !== myUserId) return true; - }); - UserProvider.getInstance().setUserList(members); - }, - render: function() { var RoomHeader = sdk.getComponent('rooms.RoomHeader'); var MessageComposer = sdk.getComponent('rooms.MessageComposer');