Merge pull request #732 from matrix-org/luke/fuse-test
Test to see how fuse feels
This commit is contained in:
commit
ab9aaa9174
3 changed files with 95 additions and 89 deletions
|
@ -26,6 +26,7 @@ import dis from '../../../dispatcher';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import q from 'q';
|
import q from 'q';
|
||||||
|
import Fuse from 'fuse.js';
|
||||||
|
|
||||||
const TRUNCATE_QUERY_LIST = 40;
|
const TRUNCATE_QUERY_LIST = 40;
|
||||||
|
|
||||||
|
@ -85,6 +86,19 @@ module.exports = React.createClass({
|
||||||
// Set the cursor at the end of the text input
|
// Set the cursor at the end of the text input
|
||||||
this.refs.textinput.value = this.props.value;
|
this.refs.textinput.value = this.props.value;
|
||||||
}
|
}
|
||||||
|
// Create a Fuse instance for fuzzy searching this._userList
|
||||||
|
this._fuse = new Fuse(
|
||||||
|
// Use an empty list at first that will later be populated
|
||||||
|
// (see this._updateUserList)
|
||||||
|
[],
|
||||||
|
{
|
||||||
|
shouldSort: true,
|
||||||
|
location: 0, // The index of the query in the test string
|
||||||
|
distance: 5, // The distance away from location the query can be
|
||||||
|
// 0.0 = exact match, 1.0 = match anything
|
||||||
|
threshold: 0.3,
|
||||||
|
}
|
||||||
|
);
|
||||||
this._updateUserList();
|
this._updateUserList();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -167,12 +181,25 @@ module.exports = React.createClass({
|
||||||
const query = ev.target.value;
|
const query = ev.target.value;
|
||||||
let queryList = [];
|
let queryList = [];
|
||||||
|
|
||||||
|
if (query.length < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.queryChangedDebouncer) {
|
||||||
|
clearTimeout(this.queryChangedDebouncer);
|
||||||
|
}
|
||||||
|
this.queryChangedDebouncer = setTimeout(() => {
|
||||||
// Only do search if there is something to search
|
// Only do search if there is something to search
|
||||||
if (query.length > 0 && query != '@') {
|
if (query.length > 0 && query != '@') {
|
||||||
// filter the known users list
|
// Weighted keys prefer to match userIds when first char is @
|
||||||
queryList = this._userList.filter((user) => {
|
this._fuse.options.keys = [{
|
||||||
return this._matches(query, user);
|
name: 'displayName',
|
||||||
}).map((user) => {
|
weight: query[0] === '@' ? 0.1 : 0.9,
|
||||||
|
},{
|
||||||
|
name: 'userId',
|
||||||
|
weight: query[0] === '@' ? 0.9 : 0.1,
|
||||||
|
}];
|
||||||
|
queryList = this._fuse.search(query).map((user) => {
|
||||||
// Return objects, structure of which is defined
|
// Return objects, structure of which is defined
|
||||||
// by InviteAddressType
|
// by InviteAddressType
|
||||||
return {
|
return {
|
||||||
|
@ -201,11 +228,13 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
queryList: queryList,
|
queryList: queryList,
|
||||||
error: false,
|
error: false,
|
||||||
|
}, () => {
|
||||||
|
this.addressSelector.moveSelectionTop();
|
||||||
});
|
});
|
||||||
|
}, 200);
|
||||||
},
|
},
|
||||||
|
|
||||||
onDismissed: function(index) {
|
onDismissed: function(index) {
|
||||||
|
@ -331,49 +360,15 @@ module.exports = React.createClass({
|
||||||
_updateUserList: new rate_limited_func(function() {
|
_updateUserList: new rate_limited_func(function() {
|
||||||
// Get all the users
|
// Get all the users
|
||||||
this._userList = MatrixClientPeg.get().getUsers();
|
this._userList = MatrixClientPeg.get().getUsers();
|
||||||
|
// Remove current user
|
||||||
|
const meIx = this._userList.findIndex((u) => {
|
||||||
|
return u.userId === MatrixClientPeg.get().credentials.userId;
|
||||||
|
});
|
||||||
|
this._userList.splice(meIx, 1);
|
||||||
|
|
||||||
|
this._fuse.set(this._userList);
|
||||||
}, 500),
|
}, 500),
|
||||||
|
|
||||||
// This is the search algorithm for matching users
|
|
||||||
_matches: function(query, user) {
|
|
||||||
var name = user.displayName.toLowerCase();
|
|
||||||
var uid = user.userId.toLowerCase();
|
|
||||||
query = query.toLowerCase();
|
|
||||||
|
|
||||||
// don't match any that are already on the invite list
|
|
||||||
if (this._isOnInviteList(uid)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ignore current user
|
|
||||||
if (uid === MatrixClientPeg.get().credentials.userId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// direct prefix matches
|
|
||||||
if (name.indexOf(query) === 0 || uid.indexOf(query) === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// strip @ on uid and try matching again
|
|
||||||
if (uid.length > 1 && uid[0] === "@" && uid.substring(1).indexOf(query) === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to find the query following a "word boundary", except that
|
|
||||||
// this does avoids using \b because it only considers letters from
|
|
||||||
// the roman alphabet to be word characters.
|
|
||||||
// Instead, we look for the query following either:
|
|
||||||
// * The start of the string
|
|
||||||
// * Whitespace, or
|
|
||||||
// * A fixed number of punctuation characters
|
|
||||||
const expr = new RegExp("(?:^|[\\s\\(\)'\",\.-_@\?;:{}\\[\\]\\#~`\\*\\&\\$])" + escapeRegExp(query));
|
|
||||||
if (expr.test(name)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
_isOnInviteList: function(uid) {
|
_isOnInviteList: function(uid) {
|
||||||
for (let i = 0; i < this.state.inviteList.length; i++) {
|
for (let i = 0; i < this.state.inviteList.length; i++) {
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -61,6 +61,15 @@ export default React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
moveSelectionTop: function() {
|
||||||
|
if (this.state.selected > 0) {
|
||||||
|
this.setState({
|
||||||
|
selected: 0,
|
||||||
|
hover: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
moveSelectionUp: function() {
|
moveSelectionUp: function() {
|
||||||
if (this.state.selected > 0) {
|
if (this.state.selected > 0) {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -124,7 +133,14 @@ export default React.createClass({
|
||||||
// Saving the addressListElement so we can use it to work out, in the componentDidUpdate
|
// Saving the addressListElement so we can use it to work out, in the componentDidUpdate
|
||||||
// method, how far to scroll when using the arrow keys
|
// method, how far to scroll when using the arrow keys
|
||||||
addressList.push(
|
addressList.push(
|
||||||
<div className={classes} onClick={this.onClick.bind(this, i)} onMouseEnter={this.onMouseEnter.bind(this, i)} onMouseLeave={this.onMouseLeave} key={i} ref={(ref) => { this.addressListElement = ref; }} >
|
<div
|
||||||
|
className={classes}
|
||||||
|
onClick={this.onClick.bind(this, i)}
|
||||||
|
onMouseEnter={this.onMouseEnter.bind(this, i)}
|
||||||
|
onMouseLeave={this.onMouseLeave}
|
||||||
|
key={this.props.addressList[i].userId}
|
||||||
|
ref={(ref) => { this.addressListElement = ref; }}
|
||||||
|
>
|
||||||
<AddressTile address={this.props.addressList[i]} justified={true} networkName="vector" networkUrl="img/search-icon-vector.svg" />
|
<AddressTile address={this.props.addressList[i]} justified={true} networkName="vector" networkUrl="img/search-icon-vector.svg" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -64,19 +64,14 @@ export default React.createClass({
|
||||||
const address = this.props.address;
|
const address = this.props.address;
|
||||||
const name = address.displayName || address.address;
|
const name = address.displayName || address.address;
|
||||||
|
|
||||||
let imgUrl;
|
let imgUrls = [];
|
||||||
if (address.avatarMxc) {
|
|
||||||
imgUrl = MatrixClientPeg.get().mxcUrlToHttp(
|
|
||||||
address.avatarMxc, 25, 25, 'crop'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (address.addressType === "mx") {
|
if (address.addressType === "mx" && address.avatarMxc) {
|
||||||
if (!imgUrl) imgUrl = 'img/icon-mx-user.svg';
|
imgUrls.push(MatrixClientPeg.get().mxcUrlToHttp(
|
||||||
|
address.avatarMxc, 25, 25, 'crop'
|
||||||
|
));
|
||||||
} else if (address.addressType === 'email') {
|
} else if (address.addressType === 'email') {
|
||||||
if (!imgUrl) imgUrl = 'img/icon-email-user.svg';
|
imgUrls.push('img/icon-email-user.svg');
|
||||||
} else {
|
|
||||||
if (!imgUrl) imgUrl = "img/avatar-error.svg";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Removing networks for now as they're not really supported
|
// Removing networks for now as they're not really supported
|
||||||
|
@ -168,7 +163,7 @@ export default React.createClass({
|
||||||
return (
|
return (
|
||||||
<div className={classes}>
|
<div className={classes}>
|
||||||
<div className="mx_AddressTile_avatar">
|
<div className="mx_AddressTile_avatar">
|
||||||
<BaseAvatar width={25} height={25} name={name} title={name} url={imgUrl} />
|
<BaseAvatar defaultToInitialLetter={true} width={25} height={25} name={name} title={name} urls={imgUrls} />
|
||||||
</div>
|
</div>
|
||||||
{ info }
|
{ info }
|
||||||
{ dismiss }
|
{ dismiss }
|
||||||
|
|
Loading…
Reference in a new issue