General ChatInviteDialog optimisations
- Use avatar initial instead of "R" or "?" - Use Fuse.js to do case-insensitive fuzzy search. This allows for better sorting of results as well as search based on weighted keys (so userId has a high weight when the input starts with "@"). - Added debounce of 200ms to prevent analysis on every key stroke. Fuse seems to degrade performance vs. simple, non-fuzzy, unsorted matching, but the debounce should prevent too much computation. - Move the selection to the top when the query is changed. There's no point in staying mid-way through the items at that point.pull/21833/head
							parent
							
								
									b41787c335
								
							
						
					
					
						commit
						439bde309e
					
				|  | @ -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; | ||||
| 
 | ||||
|  | @ -85,6 +86,21 @@ 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
 | ||||
|         if (!this._fuse) { | ||||
|             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(); | ||||
|     }, | ||||
| 
 | ||||
|  | @ -177,12 +193,15 @@ module.exports = React.createClass({ | |||
|         this.queryChangedDebouncer = setTimeout(() => { | ||||
|             // 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); | ||||
|                 }).sort((userA, userB) => { | ||||
|                     return this._sortedMatches(query, userA, userB); | ||||
|                 }).map((user) => { | ||||
|                 // 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 { | ||||
|  | @ -214,6 +233,8 @@ module.exports = React.createClass({ | |||
|             this.setState({ | ||||
|                 queryList: queryList, | ||||
|                 error: false, | ||||
|             }, () => { | ||||
|                 this.addressSelector.moveSelectionTop(); | ||||
|             }); | ||||
|         }, 200); | ||||
|     }, | ||||
|  | @ -341,59 +362,15 @@ module.exports = React.createClass({ | |||
|     _updateUserList: new rate_limited_func(function() { | ||||
|         // Get all the users
 | ||||
|         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), | ||||
| 
 | ||||
|     // 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; | ||||
|         } | ||||
| 
 | ||||
|         // positional matches
 | ||||
|         if (name.indexOf(query) !== -1 || uid.indexOf(query) !== -1) { | ||||
|             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; | ||||
|     }, | ||||
| 
 | ||||
|     _sortedMatches: function(query, userA, userB) { | ||||
|         if (userA.displayName.startsWith(query) || userA.userId.startsWith(query)) { | ||||
|             return -1; | ||||
|         } | ||||
|         if (userA.displayName.length === query.length) { | ||||
|             return -1; | ||||
|         } | ||||
|         return 0; | ||||
|     }, | ||||
| 
 | ||||
|     _isOnInviteList: function(uid) { | ||||
|         for (let i = 0; i < this.state.inviteList.length; i++) { | ||||
|             if ( | ||||
|  |  | |||
|  | @ -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({ | ||||
|  | @ -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> | ||||
|                 ); | ||||
|  |  | |||
|  | @ -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
 | ||||
|  | @ -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 } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Luke Barnard
						Luke Barnard