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:
parent
f1d72296b7
commit
d5bed78a54
5 changed files with 113 additions and 94 deletions
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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()}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue