Merge pull request #341 from matrix-org/dbkr/tab_complete_most_recently_spoke

Order tab complete by most recently spoke
pull/21833/head
David Baker 2016-07-19 18:02:09 +01:00 committed by GitHub
commit 514bc2cd51
5 changed files with 108 additions and 69 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,23 +48,39 @@ 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;
// Map tracking ordering of the room members.
// userId: integer, highest comes first.
this.memberTabOrder = {};
// monotonically increasing counter used for tracking ordering of members
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();
}
/** /**
* @param {DOMElement} * @param {DOMElement}
*/ */
@ -307,6 +326,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() {
@ -114,24 +116,7 @@ class MemberEntry extends Entry {
} }
MemberEntry.fromMemberList = function(members) { MemberEntry.fromMemberList = function(members) {
return members.sort(function(a, b) { return members.map(function(m) {
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

@ -26,9 +26,9 @@ module.exports = React.createClass({
propTypes: { propTypes: {
// 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.isRequired,
// 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
@ -233,7 +233,7 @@ module.exports = React.createClass({
<a className="mx_RoomStatusBar_resend_link" <a className="mx_RoomStatusBar_resend_link"
onClick={ this.props.onResendAllClick }> onClick={ this.props.onResendAllClick }>
Resend all Resend all
</a> or <a </a> or <a
className="mx_RoomStatusBar_resend_link" className="mx_RoomStatusBar_resend_link"
onClick={ this.props.onCancelAllClick }> onClick={ this.props.onCancelAllClick }>
cancel all cancel all
@ -247,7 +247,7 @@ module.exports = React.createClass({
// unread count trumps who is typing since the unread count is only // unread count trumps who is typing since the unread count is only
// set when you've scrolled up // set when you've scrolled up
if (this.props.numUnreadMessages) { if (this.props.numUnreadMessages) {
var unreadMsgs = this.props.numUnreadMessages + " new message" + var unreadMsgs = this.props.numUnreadMessages + " new message" +
(this.props.numUnreadMessages > 1 ? "s" : ""); (this.props.numUnreadMessages > 1 ? "s" : "");
return ( return (
@ -291,5 +291,5 @@ module.exports = React.createClass({
{content} {content}
</div> </div>
); );
}, },
}); });

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');
@ -205,8 +202,8 @@ 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._updateAutoComplete();
this._updateTabCompleteList(); this.tabComplete.loadEntries(this.state.room);
} }
if (!user_is_in_room && this.state.roomId) { if (!user_is_in_room && this.state.roomId) {
@ -360,6 +357,14 @@ module.exports = React.createClass({
}); });
} }
} }
// update the tab complete list as it depends on who most recently spoke,
// 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.
}
}, },
// called when state.room is first initialised (either at initial load, // called when state.room is first initialised (either at initial load,
@ -437,7 +442,8 @@ 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);
this._updateAutoComplete();
// 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
@ -502,8 +508,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 &&
@ -521,24 +525,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(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);
@ -1263,6 +1249,14 @@ 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() { render: function() {
var RoomHeader = sdk.getComponent('rooms.RoomHeader'); var RoomHeader = sdk.getComponent('rooms.RoomHeader');
var MessageComposer = sdk.getComponent('rooms.MessageComposer'); var MessageComposer = sdk.getComponent('rooms.MessageComposer');
@ -1376,12 +1370,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()}