Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
Merge pull request #732 from matrix-org/luke/fuse-test
Browse files Browse the repository at this point in the history
Test to see how fuse feels
  • Loading branch information
lukebarnard1 authored Mar 3, 2017
2 parents 7be55ce + b822bc6 commit ab9aaa9
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 89 deletions.
147 changes: 71 additions & 76 deletions src/components/views/dialogs/ChatInviteDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import dis from '../../../dispatcher';
import Modal from '../../../Modal';
import AccessibleButton from '../elements/AccessibleButton';
import q from 'q';
import Fuse from 'fuse.js';

const TRUNCATE_QUERY_LIST = 40;

Expand Down Expand Up @@ -85,6 +86,19 @@ module.exports = React.createClass({
// Set the cursor at the end of the text input
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();
},

Expand Down Expand Up @@ -167,45 +181,60 @@ module.exports = React.createClass({
const query = ev.target.value;
let queryList = [];

// Only do search if there is something to search
if (query.length > 0 && query != '@') {
// filter the known users list
queryList = this._userList.filter((user) => {
return this._matches(query, user);
}).map((user) => {
// Return objects, structure of which is defined
// by InviteAddressType
return {
addressType: 'mx',
address: user.userId,
displayName: user.displayName,
avatarMxc: user.avatarUrl,
isKnown: true,
}
});
if (query.length < 2) {
return;
}

if (this.queryChangedDebouncer) {
clearTimeout(this.queryChangedDebouncer);
}
this.queryChangedDebouncer = setTimeout(() => {
// Only do search if there is something to search
if (query.length > 0 && query != '@') {
// Weighted keys prefer to match userIds when first char is @
this._fuse.options.keys = [{
name: 'displayName',
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
// by InviteAddressType
return {
addressType: 'mx',
address: user.userId,
displayName: user.displayName,
avatarMxc: user.avatarUrl,
isKnown: true,
}
});

// If the query isn't a user we know about, but is a
// valid address, add an entry for that
if (queryList.length == 0) {
const addrType = getAddressType(query);
if (addrType !== null) {
queryList[0] = {
addressType: addrType,
address: query,
isKnown: false,
};
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
if (addrType == 'email') {
this._lookupThreepid(addrType, query).done();
// If the query isn't a user we know about, but is a
// valid address, add an entry for that
if (queryList.length == 0) {
const addrType = getAddressType(query);
if (addrType !== null) {
queryList[0] = {
addressType: addrType,
address: query,
isKnown: false,
};
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
if (addrType == 'email') {
this._lookupThreepid(addrType, query).done();
}
}
}
}
}

this.setState({
queryList: queryList,
error: false,
});
this.setState({
queryList: queryList,
error: false,
}, () => {
this.addressSelector.moveSelectionTop();
});
}, 200);
},

onDismissed: function(index) {
Expand Down Expand Up @@ -331,48 +360,14 @@ module.exports = React.createClass({
_updateUserList: new rate_limited_func(function() {
// Get all the users
this._userList = MatrixClientPeg.get().getUsers();
}, 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;
}
// Remove current user
const meIx = this._userList.findIndex((u) => {
return u.userId === MatrixClientPeg.get().credentials.userId;
});
this._userList.splice(meIx, 1);

return false;
},
this._fuse.set(this._userList);
}, 500),

_isOnInviteList: function(uid) {
for (let i = 0; i < this.state.inviteList.length; i++) {
Expand Down
18 changes: 17 additions & 1 deletion src/components/views/elements/AddressSelector.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@ export default React.createClass({
}
},

moveSelectionTop: function() {
if (this.state.selected > 0) {
this.setState({
selected: 0,
hover: false,
});
}
},

moveSelectionUp: function() {
if (this.state.selected > 0) {
this.setState({
Expand Down Expand Up @@ -124,7 +133,14 @@ export default React.createClass({
// Saving the addressListElement so we can use it to work out, in the componentDidUpdate
// method, how far to scroll when using the arrow keys
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" />
</div>
);
Expand Down
19 changes: 7 additions & 12 deletions src/components/views/elements/AddressTile.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,14 @@ export default React.createClass({
const address = this.props.address;
const name = address.displayName || address.address;

let imgUrl;
if (address.avatarMxc) {
imgUrl = MatrixClientPeg.get().mxcUrlToHttp(
address.avatarMxc, 25, 25, 'crop'
);
}
let imgUrls = [];

if (address.addressType === "mx") {
if (!imgUrl) imgUrl = 'img/icon-mx-user.svg';
if (address.addressType === "mx" && address.avatarMxc) {
imgUrls.push(MatrixClientPeg.get().mxcUrlToHttp(
address.avatarMxc, 25, 25, 'crop'
));
} else if (address.addressType === 'email') {
if (!imgUrl) imgUrl = 'img/icon-email-user.svg';
} else {
if (!imgUrl) imgUrl = "img/avatar-error.svg";
imgUrls.push('img/icon-email-user.svg');
}

// Removing networks for now as they're not really supported
Expand Down Expand Up @@ -168,7 +163,7 @@ export default React.createClass({
return (
<div className={classes}>
<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>
{ info }
{ dismiss }
Expand Down

0 comments on commit ab9aaa9

Please sign in to comment.