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

Order tab complete by most recently spoke
This commit is contained in:
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
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 KEY_TAB = 9;
@ -45,23 +48,39 @@ class TabComplete {
this.isFirstWord = false; // true if you tab-complete on the first word
this.enterTabCompleteTimerId = null;
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) {
this.list = completeList;
onEntryClick(entry) {
if (this.opts.onClickCompletes) {
// assign onClick listeners for each entry to complete the text
this.list.forEach((l) => {
l.onClick = () => {
this.completeTo(l);
}
});
this.completeTo(entry);
}
}
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}
*/
@ -307,6 +326,49 @@ class TabComplete {
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;

View file

@ -69,6 +69,7 @@ class Entry {
class CommandEntry extends Entry {
constructor(cmd, cmdWithArgs) {
super(cmdWithArgs);
this.kind = 'command';
this.cmd = cmd;
}
@ -95,6 +96,7 @@ class MemberEntry extends Entry {
constructor(member) {
super(member.name || member.userId);
this.member = member;
this.kind = 'member';
}
getImageJsx() {
@ -114,24 +116,7 @@ class MemberEntry extends Entry {
}
MemberEntry.fromMemberList = function(members) {
return members.sort(function(a, b) {
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 members.map(function(m) {
return new MemberEntry(m);
});
}

View file

@ -26,9 +26,9 @@ module.exports = React.createClass({
propTypes: {
// the room this statusbar is representing.
room: React.PropTypes.object.isRequired,
// a list of TabCompleteEntries.Entry objects
tabCompleteEntries: React.PropTypes.array,
// a TabComplete object
tabComplete: React.PropTypes.object.isRequired,
// the number of messages which have arrived since we've been scrolled up
numUnreadMessages: React.PropTypes.number,
@ -208,11 +208,11 @@ module.exports = React.createClass({
);
}
if (this.props.tabCompleteEntries) {
if (this.props.tabComplete.isTabCompleting()) {
return (
<div className="mx_RoomStatusBar_tabCompleteBar">
<div className="mx_RoomStatusBar_tabCompleteWrapper">
<TabCompleteBar entries={this.props.tabCompleteEntries} />
<TabCompleteBar tabComplete={this.props.tabComplete} />
<div className="mx_RoomStatusBar_tabCompleteEol" title="->|">
<TintableSvg src="img/eol.svg" width="22" height="16"/>
Auto-complete
@ -233,7 +233,7 @@ module.exports = React.createClass({
<a className="mx_RoomStatusBar_resend_link"
onClick={ this.props.onResendAllClick }>
Resend all
</a> or <a
</a> or <a
className="mx_RoomStatusBar_resend_link"
onClick={ this.props.onCancelAllClick }>
cancel all
@ -247,7 +247,7 @@ module.exports = React.createClass({
// unread count trumps who is typing since the unread count is only
// set when you've scrolled up
if (this.props.numUnreadMessages) {
var unreadMsgs = this.props.numUnreadMessages + " new message" +
var unreadMsgs = this.props.numUnreadMessages + " new message" +
(this.props.numUnreadMessages > 1 ? "s" : "");
return (
@ -291,5 +291,5 @@ module.exports = React.createClass({
{content}
</div>
);
},
},
});

View file

@ -31,10 +31,7 @@ var Modal = require("../../Modal");
var sdk = require('../../index');
var CallHandler = require('../../CallHandler');
var TabComplete = require("../../TabComplete");
var MemberEntry = require("../../TabCompleteEntries").MemberEntry;
var CommandEntry = require("../../TabCompleteEntries").CommandEntry;
var Resend = require("../../Resend");
var SlashCommands = require("../../SlashCommands");
var dis = require("../../dispatcher");
var Tinter = require("../../Tinter");
var rate_limited_func = require('../../ratelimitedfunc');
@ -205,8 +202,8 @@ module.exports = React.createClass({
MatrixClientPeg.get().credentials.userId, 'join'
);
// update the tab complete list now we have a room
this._updateTabCompleteList();
this._updateAutoComplete();
this.tabComplete.loadEntries(this.state.room);
}
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,
@ -437,7 +442,8 @@ module.exports = React.createClass({
}
// 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
// means we have finished joining a room we were previously peeking
@ -502,8 +508,6 @@ module.exports = React.createClass({
window.addEventListener('resize', this.onResize);
this.onResize();
this._updateTabCompleteList();
// XXX: EVIL HACK to autofocus inviting on empty rooms.
// We use the setTimeout to avoid racing with focus_composer.
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() {
if (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() {
var RoomHeader = sdk.getComponent('rooms.RoomHeader');
var MessageComposer = sdk.getComponent('rooms.MessageComposer');
@ -1376,12 +1370,10 @@ module.exports = React.createClass({
statusBar = <UploadBar room={this.state.room} />
} else if (!this.state.searchResults) {
var RoomStatusBar = sdk.getComponent('structures.RoomStatusBar');
var tabEntries = this.tabComplete.isTabCompleting() ?
this.tabComplete.peek(6) : null;
statusBar = <RoomStatusBar
room={this.state.room}
tabCompleteEntries={tabEntries}
tabComplete={this.tabComplete}
numUnreadMessages={this.state.numUnreadMessages}
hasUnsentMessages={this.state.hasUnsentMessages}
atEndOfLiveTimeline={this.state.atEndOfLiveTimeline}

View file

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