Rejig tab complete to make it faster

Now do a lot less when people speak. Also move more of the tab completion logic into TabComplete.js and out of RoomView.
This commit is contained in:
David Baker 2016-07-15 16:10:27 +01:00
parent f1d72296b7
commit d5bed78a54
5 changed files with 113 additions and 94 deletions

View file

@ -13,7 +13,10 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
var Entry = require("./TabCompleteEntries").Entry;
import { Entry, MemberEntry, CommandEntry } from './TabCompleteEntries';
import SlashCommands from './SlashCommands';
import MatrixClientPeg from './MatrixClientPeg';
const DELAY_TIME_MS = 1000; const DELAY_TIME_MS = 1000;
const KEY_TAB = 9; const KEY_TAB = 9;
@ -45,21 +48,32 @@ class TabComplete {
this.isFirstWord = false; // true if you tab-complete on the first word this.isFirstWord = false; // true if you tab-complete on the first word
this.enterTabCompleteTimerId = null; this.enterTabCompleteTimerId = null;
this.inPassiveMode = false; this.inPassiveMode = false;
this.memberTabOrder = {};
this.memberOrderSeq = 0;
} }
/** /**
* @param {Entry[]} completeList * Call this when a a UI element representing a tab complete entry has been clicked
* @param {entry} The entry that was clicked
*/ */
setCompletionList(completeList) { onEntryClick(entry) {
this.list = completeList;
if (this.opts.onClickCompletes) { if (this.opts.onClickCompletes) {
// assign onClick listeners for each entry to complete the text this.completeTo(entry);
this.list.forEach((l) => {
l.onClick = () => {
this.completeTo(l);
} }
});
} }
loadEntries(room) {
this._makeEntries(room);
this._initSorting(room);
this._sortEntries();
}
onMemberSpoke(member) {
if (this.memberTabOrder[member.userId] === undefined) {
this.list.push(new MemberEntry(member));
}
this.memberTabOrder[member.userId] = this.memberOrderSeq++;
this._sortEntries();
} }
/** /**
@ -307,6 +321,49 @@ class TabComplete {
this.opts.onStateChange(this.completing); this.opts.onStateChange(this.completing);
} }
} }
_sortEntries() {
// largest comes first
const KIND_ORDER = {
command: 1,
member: 2,
};
this.list.sort((a, b) => {
const kindOrderDifference = KIND_ORDER[b.kind] - KIND_ORDER[a.kind];
if (kindOrderDifference != 0) {
return kindOrderDifference;
}
if (a.kind == 'member') {
return this.memberTabOrder[b.member.userId] - this.memberTabOrder[a.member.userId];
}
// anything else we have no ordering for
return 0;
});
}
_makeEntries(room) {
const myUserId = MatrixClientPeg.get().credentials.userId;
const members = room.getJoinedMembers().filter(function(member) {
if (member.userId !== myUserId) return true;
});
this.list = MemberEntry.fromMemberList(members).concat(
CommandEntry.fromCommands(SlashCommands.getCommandList())
);
}
_initSorting(room) {
this.memberTabOrder = {};
this.memberOrderSeq = 0;
for (const ev of room.getLiveTimeline().getEvents()) {
this.memberTabOrder[ev.getSender()] = this.memberOrderSeq++;
}
}
}; };
module.exports = TabComplete; module.exports = TabComplete;

View file

@ -69,6 +69,7 @@ class Entry {
class CommandEntry extends Entry { class CommandEntry extends Entry {
constructor(cmd, cmdWithArgs) { constructor(cmd, cmdWithArgs) {
super(cmdWithArgs); super(cmdWithArgs);
this.kind = 'command';
this.cmd = cmd; this.cmd = cmd;
} }
@ -95,6 +96,7 @@ class MemberEntry extends Entry {
constructor(member) { constructor(member) {
super(member.name || member.userId); super(member.name || member.userId);
this.member = member; this.member = member;
this.kind = 'member';
} }
getImageJsx() { getImageJsx() {
@ -113,42 +115,8 @@ class MemberEntry extends Entry {
} }
} }
MemberEntry.fromMemberList = function(room, members) { MemberEntry.fromMemberList = function(members) {
// build up a dict of when, in the history we have cached, return members.map(function(m) {
// each member last spoke
const lastSpoke = {};
const timelineEvents = room.getLiveTimeline().getEvents();
for (const ev of room.getLiveTimeline().getEvents()) {
lastSpoke[ev.getSender()] = ev.getTs();
}
return members.sort(function(a, b) {
const lastSpokeA = lastSpoke[a.userId] || 0;
const lastSpokeB = lastSpoke[b.userId] || 0;
if (lastSpokeA != lastSpokeB) {
// B - A here because the highest value
// is most recent
return lastSpokeB - lastSpokeA;
}
var userA = a.user;
var userB = b.user;
if (userA && !userB) {
return -1; // a comes first
}
else if (!userA && userB) {
return 1; // b comes first
}
else if (!userA && !userB) {
return 0; // don't care
}
else { // both User objects exist
var lastActiveAgoA = userA.lastActiveAgo || Number.MAX_SAFE_INTEGER;
var lastActiveAgoB = userB.lastActiveAgo || Number.MAX_SAFE_INTEGER;
return lastActiveAgoA - lastActiveAgoB;
}
}).map(function(m) {
return new MemberEntry(m); return new MemberEntry(m);
}); });
} }

View file

@ -27,8 +27,8 @@ module.exports = React.createClass({
// the room this statusbar is representing. // the room this statusbar is representing.
room: React.PropTypes.object.isRequired, room: React.PropTypes.object.isRequired,
// a list of TabCompleteEntries.Entry objects // a TabComplete object
tabCompleteEntries: React.PropTypes.array, tabComplete: React.PropTypes.object,
// the number of messages which have arrived since we've been scrolled up // the number of messages which have arrived since we've been scrolled up
numUnreadMessages: React.PropTypes.number, numUnreadMessages: React.PropTypes.number,
@ -208,11 +208,11 @@ module.exports = React.createClass({
); );
} }
if (this.props.tabCompleteEntries) { if (this.props.tabComplete.isTabCompleting()) {
return ( return (
<div className="mx_RoomStatusBar_tabCompleteBar"> <div className="mx_RoomStatusBar_tabCompleteBar">
<div className="mx_RoomStatusBar_tabCompleteWrapper"> <div className="mx_RoomStatusBar_tabCompleteWrapper">
<TabCompleteBar entries={this.props.tabCompleteEntries} /> <TabCompleteBar tabComplete={this.props.tabComplete} />
<div className="mx_RoomStatusBar_tabCompleteEol" title="->|"> <div className="mx_RoomStatusBar_tabCompleteEol" title="->|">
<TintableSvg src="img/eol.svg" width="22" height="16"/> <TintableSvg src="img/eol.svg" width="22" height="16"/>
Auto-complete Auto-complete

View file

@ -31,10 +31,7 @@ var Modal = require("../../Modal");
var sdk = require('../../index'); var sdk = require('../../index');
var CallHandler = require('../../CallHandler'); var CallHandler = require('../../CallHandler');
var TabComplete = require("../../TabComplete"); var TabComplete = require("../../TabComplete");
var MemberEntry = require("../../TabCompleteEntries").MemberEntry;
var CommandEntry = require("../../TabCompleteEntries").CommandEntry;
var Resend = require("../../Resend"); var Resend = require("../../Resend");
var SlashCommands = require("../../SlashCommands");
var dis = require("../../dispatcher"); var dis = require("../../dispatcher");
var Tinter = require("../../Tinter"); var Tinter = require("../../Tinter");
var rate_limited_func = require('../../ratelimitedfunc'); var rate_limited_func = require('../../ratelimitedfunc');
@ -136,12 +133,6 @@ module.exports = React.createClass({
}, },
componentWillMount: function() { componentWillMount: function() {
this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on("Room", this.onRoom);
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData);
MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember);
this.tabComplete = new TabComplete({ this.tabComplete = new TabComplete({
allowLooping: false, allowLooping: false,
autoEnterTabComplete: true, autoEnterTabComplete: true,
@ -151,6 +142,12 @@ module.exports = React.createClass({
} }
}); });
this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on("Room", this.onRoom);
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData);
MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember);
if (this.props.roomAddress[0] == '#') { if (this.props.roomAddress[0] == '#') {
// we always look up the alias from the directory server: // we always look up the alias from the directory server:
// we want the room that the given alias is pointing to // we want the room that the given alias is pointing to
@ -205,8 +202,13 @@ module.exports = React.createClass({
MatrixClientPeg.get().credentials.userId, 'join' MatrixClientPeg.get().credentials.userId, 'join'
); );
// update the tab complete list now we have a room this.tabComplete.loadEntries(this.state.room);
this._updateTabCompleteList();
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);
} }
if (!user_is_in_room && this.state.roomId) { if (!user_is_in_room && this.state.roomId) {
@ -363,7 +365,15 @@ module.exports = React.createClass({
// update ther tab complete list as it depends on who most recently spoke, // update ther tab complete list as it depends on who most recently spoke,
// and that has probably just changed // and that has probably just changed
this._updateTabCompleteList(); if (ev.sender) {
this.tabComplete.onMemberSpoke(ev.sender);
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);
}
}, },
// called when state.room is first initialised (either at initial load, // called when state.room is first initialised (either at initial load,
@ -441,7 +451,13 @@ module.exports = React.createClass({
} }
// a member state changed in this room, refresh the tab complete list // a member state changed in this room, refresh the tab complete list
this._updateTabCompleteList(); this.tabComplete.loadEntries(this.state.room);
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);
// if we are now a member of the room, where we were not before, that // 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 // means we have finished joining a room we were previously peeking
@ -506,8 +522,6 @@ module.exports = React.createClass({
window.addEventListener('resize', this.onResize); window.addEventListener('resize', this.onResize);
this.onResize(); this.onResize();
this._updateTabCompleteList();
// XXX: EVIL HACK to autofocus inviting on empty rooms. // XXX: EVIL HACK to autofocus inviting on empty rooms.
// We use the setTimeout to avoid racing with focus_composer. // We use the setTimeout to avoid racing with focus_composer.
if (this.state.room && if (this.state.room &&
@ -525,24 +539,6 @@ module.exports = React.createClass({
} }
}, },
_updateTabCompleteList: function() {
var cli = MatrixClientPeg.get();
if (!this.state.room) {
return;
}
var members = this.state.room.getJoinedMembers().filter(function(member) {
if (member.userId !== cli.credentials.userId) return true;
});
UserProvider.getInstance().setUserList(members);
this.tabComplete.setCompletionList(
MemberEntry.fromMemberList(this.state.room, members).concat(
CommandEntry.fromCommands(SlashCommands.getCommandList())
)
);
},
componentDidUpdate: function() { componentDidUpdate: function() {
if (this.refs.roomView) { if (this.refs.roomView) {
var roomView = ReactDOM.findDOMNode(this.refs.roomView); var roomView = ReactDOM.findDOMNode(this.refs.roomView);
@ -1380,12 +1376,10 @@ module.exports = React.createClass({
statusBar = <UploadBar room={this.state.room} /> statusBar = <UploadBar room={this.state.room} />
} else if (!this.state.searchResults) { } else if (!this.state.searchResults) {
var RoomStatusBar = sdk.getComponent('structures.RoomStatusBar'); var RoomStatusBar = sdk.getComponent('structures.RoomStatusBar');
var tabEntries = this.tabComplete.isTabCompleting() ?
this.tabComplete.peek(6) : null;
statusBar = <RoomStatusBar statusBar = <RoomStatusBar
room={this.state.room} room={this.state.room}
tabCompleteEntries={tabEntries} tabComplete={this.tabComplete}
numUnreadMessages={this.state.numUnreadMessages} numUnreadMessages={this.state.numUnreadMessages}
hasUnsentMessages={this.state.hasUnsentMessages} hasUnsentMessages={this.state.hasUnsentMessages}
atEndOfLiveTimeline={this.state.atEndOfLiveTimeline} atEndOfLiveTimeline={this.state.atEndOfLiveTimeline}

View file

@ -24,17 +24,17 @@ module.exports = React.createClass({
displayName: 'TabCompleteBar', displayName: 'TabCompleteBar',
propTypes: { propTypes: {
entries: React.PropTypes.array.isRequired tabComplete: React.PropTypes.object.isRequired
}, },
render: function() { render: function() {
return ( return (
<div className="mx_TabCompleteBar"> <div className="mx_TabCompleteBar">
{this.props.entries.map(function(entry, i) { {this.props.tabComplete.peek(6).map((entry, i) => {
return ( return (
<div key={entry.getKey() || i + ""} <div key={entry.getKey() || i + ""}
className={ "mx_TabCompleteBar_item " + (entry instanceof CommandEntry ? "mx_TabCompleteBar_command" : "") } className={ "mx_TabCompleteBar_item " + (entry instanceof CommandEntry ? "mx_TabCompleteBar_command" : "") }
onClick={entry.onClick.bind(entry)} > onClick={this.props.tabComplete.onEntryClick.bind(this.props.tabComplete, entry)} >
{entry.getImageJsx()} {entry.getImageJsx()}
<span className="mx_TabCompleteBar_text"> <span className="mx_TabCompleteBar_text">
{entry.getText()} {entry.getText()}