diff --git a/src/autocomplete/AutocompleteProvider.js b/src/autocomplete/AutocompleteProvider.js index 41d5d035d1..e3332d014e 100644 --- a/src/autocomplete/AutocompleteProvider.js +++ b/src/autocomplete/AutocompleteProvider.js @@ -1,4 +1,5 @@ import Q from 'q'; +import React from 'react'; export default class AutocompleteProvider { constructor(commandRegex?: RegExp, fuseOpts?: any) { @@ -51,4 +52,12 @@ export default class AutocompleteProvider { getName(): string { return 'Default Provider'; } + + renderCompletions(completions: [React.Component]): ?React.Component { + return ( +
+ {completions} +
+ ); + } } diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index 19a366ac63..8fb7a75aed 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -74,7 +74,7 @@ export default class CommandProvider extends AutocompleteProvider { } getName() { - return 'Commands'; + return '*️⃣ Commands'; } static getInstance(): CommandProvider { @@ -83,4 +83,10 @@ export default class CommandProvider extends AutocompleteProvider { return instance; } + + renderCompletions(completions: [React.Component]): ?React.Component { + return React.cloneElement(super.renderCompletions(completions), { + className: 'mx_Autocomplete_Completion_container_block', + }); + } } diff --git a/src/autocomplete/Components.js b/src/autocomplete/Components.js index 168da00c1c..f0dbc64d65 100644 --- a/src/autocomplete/Components.js +++ b/src/autocomplete/Components.js @@ -1,19 +1,62 @@ import React from 'react'; +import ReactDOM from 'react-dom'; +import classNames from 'classnames'; -export function TextualCompletion({ - title, - subtitle, - description, -}: { - title: ?string, - subtitle: ?string, - description: ?string -}) { - return ( -
- {title} - {subtitle} - {description} -
- ); +/* These were earlier stateless functional components but had to be converted +since we need to use refs/findDOMNode to access the underlying DOM node to focus the correct completion, +something that is not entirely possible with stateless functional components. One could +presumably wrap them in a
before rendering but I think this is the better way to do it. + */ + +export class TextualCompletion extends React.Component { + render() { + const { + title, + subtitle, + description, + className, + ...restProps, + } = this.props; + return ( +
+ {title} + {subtitle} + {description} +
+ ); + } } +TextualCompletion.propTypes = { + title: React.PropTypes.string, + subtitle: React.PropTypes.string, + description: React.PropTypes.string, + className: React.PropTypes.string, +}; + +export class PillCompletion extends React.Component { + render() { + const { + title, + subtitle, + description, + initialComponent, + className, + ...restProps, + } = this.props; + return ( +
+ {initialComponent} + {title} + {subtitle} + {description} +
+ ); + } +} +PillCompletion.propTypes = { + title: React.PropTypes.string, + subtitle: React.PropTypes.string, + description: React.PropTypes.string, + initialComponent: React.PropTypes.element, + className: React.PropTypes.string, +}; diff --git a/src/autocomplete/DuckDuckGoProvider.js b/src/autocomplete/DuckDuckGoProvider.js index 1746ce0aaa..c74ffa0473 100644 --- a/src/autocomplete/DuckDuckGoProvider.js +++ b/src/autocomplete/DuckDuckGoProvider.js @@ -78,7 +78,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider { } getName() { - return 'Results from DuckDuckGo'; + return '🔍 Results from DuckDuckGo'; } static getInstance(): DuckDuckGoProvider { diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index 37a50ee8d8..8763d90749 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -3,6 +3,8 @@ import AutocompleteProvider from './AutocompleteProvider'; import Q from 'q'; import {emojioneList, shortnameToImage, shortnameToUnicode} from 'emojione'; import Fuse from 'fuse.js'; +import sdk from '../index'; +import {PillCompletion} from './Components'; const EMOJI_REGEX = /:\w*:?/g; const EMOJI_SHORTNAMES = Object.keys(emojioneList); @@ -16,28 +18,28 @@ export default class EmojiProvider extends AutocompleteProvider { } getCompletions(query: string, selection: {start: number, end: number}) { + const EmojiText = sdk.getComponent('views.elements.EmojiText'); + let completions = []; let {command, range} = this.getCurrentCommand(query, selection); if (command) { completions = this.fuse.search(command[0]).map(result => { - let shortname = EMOJI_SHORTNAMES[result]; - let imageHTML = shortnameToImage(shortname); + const shortname = EMOJI_SHORTNAMES[result]; + const unicode = shortnameToUnicode(shortname); return { - completion: shortnameToUnicode(shortname), + completion: unicode, component: ( -
-   {shortname} -
+ {unicode}} /> ), range, }; - }).slice(0, 4); + }).slice(0, 8); } return Q.when(completions); } getName() { - return 'Emoji'; + return '😃 Emoji'; } static getInstance() { @@ -45,4 +47,10 @@ export default class EmojiProvider extends AutocompleteProvider { instance = new EmojiProvider(); return instance; } + + renderCompletions(completions: [React.Component]): ?React.Component { + return React.cloneElement(super.renderCompletions(completions), { + className: 'mx_Autocomplete_Completion_container_pill', + }); + } } diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index b34fdeb59a..f27d450266 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -3,8 +3,9 @@ import AutocompleteProvider from './AutocompleteProvider'; import Q from 'q'; import MatrixClientPeg from '../MatrixClientPeg'; import Fuse from 'fuse.js'; -import {TextualCompletion} from './Components'; +import {PillCompletion} from './Components'; import {getDisplayAliasForRoom} from '../MatrixTools'; +import sdk from '../index'; const ROOM_REGEX = /(?=#)([^\s]*)/g; @@ -21,6 +22,8 @@ export default class RoomProvider extends AutocompleteProvider { } getCompletions(query: string, selection: {start: number, end: number}) { + const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar'); + let client = MatrixClientPeg.get(); let completions = []; const {command, range} = this.getCurrentCommand(query, selection); @@ -39,7 +42,7 @@ export default class RoomProvider extends AutocompleteProvider { return { completion: displayAlias, component: ( - + } title={room.name} description={displayAlias} /> ), range, }; @@ -49,7 +52,7 @@ export default class RoomProvider extends AutocompleteProvider { } getName() { - return 'Rooms'; + return '💬 Rooms'; } static getInstance() { @@ -59,4 +62,10 @@ export default class RoomProvider extends AutocompleteProvider { return instance; } + + renderCompletions(completions: [React.Component]): ?React.Component { + return React.cloneElement(super.renderCompletions(completions), { + className: 'mx_Autocomplete_Completion_container_pill', + }); + } } diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 8828f8cb70..e772d62b23 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -2,7 +2,8 @@ import React from 'react'; import AutocompleteProvider from './AutocompleteProvider'; import Q from 'q'; import Fuse from 'fuse.js'; -import {TextualCompletion} from './Components'; +import {PillCompletion} from './Components'; +import sdk from '../index'; const USER_REGEX = /@[^\s]*/g; @@ -20,6 +21,8 @@ export default class UserProvider extends AutocompleteProvider { } getCompletions(query: string, selection: {start: number, end: number}) { + const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar'); + let completions = []; let {command, range} = this.getCurrentCommand(query, selection); if (command) { @@ -29,7 +32,8 @@ export default class UserProvider extends AutocompleteProvider { return { completion: user.userId, component: ( - } title={displayName} description={user.userId} /> ), @@ -41,7 +45,7 @@ export default class UserProvider extends AutocompleteProvider { } getName() { - return 'Users'; + return '👥 Users'; } setUserList(users) { @@ -54,4 +58,10 @@ export default class UserProvider extends AutocompleteProvider { } return instance; } + + renderCompletions(completions: [React.Component]): ?React.Component { + return React.cloneElement(super.renderCompletions(completions), { + className: 'mx_Autocomplete_Completion_container_pill', + }); + } } diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index 32e568e2ba..1f62ce852c 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -1,7 +1,8 @@ import React from 'react'; -import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; +import ReactDOM from 'react-dom'; import classNames from 'classnames'; import flatMap from 'lodash/flatMap'; +import sdk from '../../../index'; import {getCompletions} from '../../../autocomplete/Autocompleter'; @@ -100,11 +101,26 @@ export default class Autocomplete extends React.Component { this.setState({selectionOffset}); } + componentDidUpdate() { + // this is the selected completion, so scroll it into view if needed + const selectedCompletion = this.refs[`completion${this.state.selectionOffset}`]; + if (selectedCompletion && this.container) { + const {offsetTop} = ReactDOM.findDOMNode(selectedCompletion); + if (offsetTop > this.container.scrollTop + this.container.offsetHeight || + offsetTop < this.container.scrollTop) { + this.container.scrollTop = offsetTop - this.container.offsetTop; + } + } + } + render() { + const EmojiText = sdk.getComponent('views.elements.EmojiText'); + let position = 0; let renderedCompletions = this.state.completions.map((completionResult, i) => { let completions = completionResult.completions.map((completion, i) => { - let className = classNames('mx_Autocomplete_Completion', { + + const className = classNames('mx_Autocomplete_Completion', { 'selected': position === this.state.selectionOffset, }); let componentPosition = position; @@ -116,40 +132,27 @@ export default class Autocomplete extends React.Component { this.onConfirm(); }; - return ( -
- {completion.component} -
- ); + return React.cloneElement(completion.component, { + key: i, + ref: `completion${i}`, + className, + onMouseOver, + onClick, + }); }); return completions.length > 0 ? (
- {completionResult.provider.getName()} - - {completions} - + {completionResult.provider.getName()} + {completionResult.provider.renderCompletions(completions)}
) : null; }); return ( -
- - {renderedCompletions} - +
this.container = e}> + {renderedCompletions}
); }