diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index c8f3134a3b..95669d5e0f 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -5,12 +5,12 @@ import UserProvider from './UserProvider'; import EmojiProvider from './EmojiProvider'; const PROVIDERS = [ + UserProvider, CommandProvider, DuckDuckGoProvider, RoomProvider, - UserProvider, EmojiProvider -].map(completer => new completer()); +].map(completer => completer.getInstance()); export function getCompletions(query: String) { return PROVIDERS.map(provider => { diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index e2eac47d16..7b950c0ed0 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -39,6 +39,8 @@ const COMMANDS = [ } ]; +let instance = null; + export default class CommandProvider extends AutocompleteProvider { constructor() { super(); @@ -49,7 +51,7 @@ export default class CommandProvider extends AutocompleteProvider { getCompletions(query: String) { let completions = []; - const matches = query.match(/(^\/\w+)/); + const matches = query.match(/(^\/\w*)/); if(!!matches) { const command = matches[0]; completions = this.fuse.search(command).map(result => { @@ -66,4 +68,11 @@ export default class CommandProvider extends AutocompleteProvider { getName() { return 'Commands'; } + + static getInstance(): CommandProvider { + if(instance == null) + instance = new CommandProvider(); + + return instance; + } } diff --git a/src/autocomplete/DuckDuckGoProvider.js b/src/autocomplete/DuckDuckGoProvider.js index 2acd892498..496ce72e46 100644 --- a/src/autocomplete/DuckDuckGoProvider.js +++ b/src/autocomplete/DuckDuckGoProvider.js @@ -5,6 +5,8 @@ import 'whatwg-fetch'; const DDG_REGEX = /\/ddg\s+(.+)$/; const REFERER = 'vector'; +let instance = null; + export default class DuckDuckGoProvider extends AutocompleteProvider { static getQueryUri(query: String) { return `http://api.duckduckgo.com/?q=${encodeURIComponent(query)}` @@ -51,4 +53,11 @@ export default class DuckDuckGoProvider extends AutocompleteProvider { getName() { return 'Results from DuckDuckGo'; } + + static getInstance(): DuckDuckGoProvider { + if(instance == null) + instance = new DuckDuckGoProvider(); + + return instance; + } } diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index fefd00a7fd..684414d72a 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -6,19 +6,19 @@ import Fuse from 'fuse.js'; const EMOJI_REGEX = /:\w*:?/g; const EMOJI_SHORTNAMES = Object.keys(emojioneList); +let instance = null; + export default class EmojiProvider extends AutocompleteProvider { constructor() { super(); - console.log(EMOJI_SHORTNAMES); this.fuse = new Fuse(EMOJI_SHORTNAMES); } getCompletions(query: String) { let completions = []; - const matches = query.match(EMOJI_REGEX); - console.log(matches); - if(!!matches) { - const command = matches[0]; + let matches = query.match(EMOJI_REGEX); + let command = matches && matches[0]; + if(command) { completions = this.fuse.search(command).map(result => { let shortname = EMOJI_SHORTNAMES[result]; let imageHTML = shortnameToImage(shortname); @@ -38,4 +38,10 @@ export default class EmojiProvider extends AutocompleteProvider { getName() { return 'Emoji'; } + + static getInstance() { + if(instance == null) + instance = new EmojiProvider(); + return instance; + } } diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index 26dc5733da..c61541617d 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -1,9 +1,12 @@ import AutocompleteProvider from './AutocompleteProvider'; import Q from 'q'; import MatrixClientPeg from '../MatrixClientPeg'; +import Fuse from 'fuse.js'; const ROOM_REGEX = /(?=#)[^\s]*/g; +let instance = null; + export default class RoomProvider extends AutocompleteProvider { constructor() { super(); @@ -13,8 +16,8 @@ export default class RoomProvider extends AutocompleteProvider { let client = MatrixClientPeg.get(); let completions = []; const matches = query.match(ROOM_REGEX); - if(!!matches) { - const command = matches[0]; + const command = matches && matches[0]; + if(command) { completions = client.getRooms().map(room => { return { title: room.name, @@ -28,4 +31,11 @@ export default class RoomProvider extends AutocompleteProvider { getName() { return 'Rooms'; } + + static getInstance() { + if(instance == null) + instance = new RoomProvider(); + + return instance; + } } diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 791dd55a33..c850cea7d9 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -4,20 +4,22 @@ import MatrixClientPeg from '../MatrixClientPeg'; const ROOM_REGEX = /@[^\s]*/g; +let instance = null; + export default class UserProvider extends AutocompleteProvider { constructor() { super(); + this.users = []; } getCompletions(query: String) { - let client = MatrixClientPeg.get(); let completions = []; const matches = query.match(ROOM_REGEX); if(!!matches) { const command = matches[0]; - completions = client.getUsers().map(user => { + completions = this.users.map(user => { return { - title: user.displayName, + title: user.displayName || user.userId, description: user.userId }; }); @@ -28,4 +30,15 @@ export default class UserProvider extends AutocompleteProvider { getName() { return 'Users'; } + + setUserList(users) { + console.log('setUserList'); + this.users = users; + } + + static getInstance(): UserProvider { + if(instance == null) + instance = new UserProvider(); + return instance; + } } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index e1b4c00175..9d952e611e 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -41,6 +41,8 @@ var rate_limited_func = require('../../ratelimitedfunc'); var ObjectUtils = require('../../ObjectUtils'); var MatrixTools = require('../../MatrixTools'); +import UserProvider from '../../autocomplete/UserProvider'; + var DEBUG = false; if (DEBUG) { @@ -495,21 +497,26 @@ module.exports = React.createClass({ } }, - _updateTabCompleteList: new rate_limited_func(function() { + _updateTabCompleteList: function() { var cli = MatrixClientPeg.get(); + console.log('_updateTabCompleteList'); + console.log(this.state.room); + console.trace(); - if (!this.state.room || !this.tabComplete) { + 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()) ) ); - }, 500), + }, componentDidUpdate: function() { if (this.refs.roomView) { diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index 673cdc5bf5..0218a88195 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -16,14 +16,14 @@ export default class Autocomplete extends React.Component { getCompletions(props.query).map(completionResult => { try { - console.log(`${completionResult.provider.getName()}: ${JSON.stringify(completionResult.completions)}`); + // console.log(`${completionResult.provider.getName()}: ${JSON.stringify(completionResult.completions)}`); completionResult.completions.then(completions => { let i = this.state.completions.findIndex( completion => completion.provider === completionResult.provider ); i = i == -1 ? this.state.completions.length : i; - console.log(completionResult); + // console.log(completionResult); let newCompletions = Object.assign([], this.state.completions); completionResult.completions = completions; newCompletions[i] = completionResult; @@ -42,13 +42,6 @@ export default class Autocomplete extends React.Component { } render() { - const pinElement = document.querySelector(this.props.pinSelector); - if(!pinElement) return null; - - const position = pinElement.getBoundingClientRect(); - - - const renderedCompletions = this.state.completions.map((completionResult, i) => { // console.log(completionResult); let completions = completionResult.completions.map((completion, i) => { @@ -58,10 +51,11 @@ export default class Autocomplete extends React.Component { } return ( -
- {completion.title} - {completion.subtitle} - {completion.description} +
+ {completion.title} + {completion.subtitle} + + {completion.description}
); }); @@ -70,7 +64,7 @@ export default class Autocomplete extends React.Component { return completions.length > 0 ? (
{completionResult.provider.getName()} - + {completions}
@@ -79,7 +73,7 @@ export default class Autocomplete extends React.Component { return (
- + {renderedCompletions}
@@ -89,11 +83,5 @@ export default class Autocomplete extends React.Component { Autocomplete.propTypes = { // the query string for which to show autocomplete suggestions - query: React.PropTypes.string.isRequired, - - // CSS selector indicating which element to pin the autocomplete to - pinSelector: React.PropTypes.string.isRequired, - - // attributes on which the autocomplete should match the pinElement - pinTo: React.PropTypes.array.isRequired + query: React.PropTypes.string.isRequired }; diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 0f9dd86b09..5373ca4dc8 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -25,36 +25,22 @@ import Autocomplete from './Autocomplete'; import UserSettingsStore from '../../../UserSettingsStore'; -module.exports = React.createClass({ - displayName: 'MessageComposer', +export default class MessageComposer extends React.Component { + constructor(props, context) { + super(props, context); + this.onCallClick = this.onCallClick.bind(this); + this.onHangupClick = this.onHangupClick.bind(this); + this.onUploadClick = this.onUploadClick.bind(this); + this.onUploadFileSelected = this.onUploadFileSelected.bind(this); + this.onVoiceCallClick = this.onVoiceCallClick.bind(this); + this.onInputContentChanged = this.onInputContentChanged.bind(this); - propTypes: { - tabComplete: React.PropTypes.any, - - // a callback which is called when the height of the composer is - // changed due to a change in content. - onResize: React.PropTypes.func, - - // js-sdk Room object - room: React.PropTypes.object.isRequired, - - // string representing the current voip call state - callState: React.PropTypes.string, - - // callback when a file to upload is chosen - uploadFile: React.PropTypes.func.isRequired, - - // opacity for dynamic UI fading effects - opacity: React.PropTypes.number, - }, - - getInitialState: function () { - return { + this.state = { autocompleteQuery: '' }; - }, + } - onUploadClick: function(ev) { + onUploadClick(ev) { if (MatrixClientPeg.get().isGuest()) { var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); Modal.createDialog(NeedToRegisterDialog, { @@ -65,9 +51,9 @@ module.exports = React.createClass({ } this.refs.uploadInput.click(); - }, + } - onUploadFileSelected: function(ev) { + onUploadFileSelected(ev) { var files = ev.target.files; var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); @@ -103,9 +89,9 @@ module.exports = React.createClass({ this.refs.uploadInput.value = null; } }); - }, + } - onHangupClick: function() { + onHangupClick() { var call = CallHandler.getCallForRoom(this.props.room.roomId); //var call = CallHandler.getAnyActiveCall(); if (!call) { @@ -117,31 +103,32 @@ module.exports = React.createClass({ // (e.g. conferences which will hangup the 1:1 room instead) room_id: call.roomId }); - }, + } - onCallClick: function(ev) { + onCallClick(ev) { dis.dispatch({ action: 'place_call', type: ev.shiftKey ? "screensharing" : "video", room_id: this.props.room.roomId }); - }, + } - onVoiceCallClick: function(ev) { + onVoiceCallClick(ev) { dis.dispatch({ action: 'place_call', type: 'voice', room_id: this.props.room.roomId }); - }, + } - onInputContentChanged(content: String) { + onInputContentChanged(content: string) { this.setState({ autocompleteQuery: content - }) - }, + }); + console.log(content); + } - render: function() { + render() { var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId); var uploadInputStyle = {display: 'none'}; var MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); @@ -196,7 +183,7 @@ module.exports = React.createClass({ controls.push( this.onInputContentChanged(content) } />, + onContentChanged={this.onInputContentChanged} />, uploadButton, hangupButton, callButton, @@ -213,7 +200,7 @@ module.exports = React.createClass({ return (
- +
@@ -223,5 +210,24 @@ module.exports = React.createClass({
); } -}); +}; +MessageComposer.propTypes = { + tabComplete: React.PropTypes.any, + + // a callback which is called when the height of the composer is + // changed due to a change in content. + onResize: React.PropTypes.func, + + // js-sdk Room object + room: React.PropTypes.object.isRequired, + + // string representing the current voip call state + callState: React.PropTypes.string, + + // callback when a file to upload is chosen + uploadFile: React.PropTypes.func.isRequired, + + // opacity for dynamic UI fading effects + opacity: React.PropTypes.number +}; diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index a9ee764864..d82e9fb6c7 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -72,7 +72,7 @@ export default class MessageComposerInput extends React.Component { this.onInputClick = this.onInputClick.bind(this); this.handleReturn = this.handleReturn.bind(this); this.handleKeyCommand = this.handleKeyCommand.bind(this); - this.onChange = this.onChange.bind(this); + this.setEditorState = this.setEditorState.bind(this); let isRichtextEnabled = window.localStorage.getItem('mx_editor_rte_enabled'); if(isRichtextEnabled == null) { @@ -207,9 +207,7 @@ export default class MessageComposerInput extends React.Component { let contentJSON = window.sessionStorage.getItem("mx_messagecomposer_input_" + this.roomId); if (contentJSON) { let content = convertFromRaw(JSON.parse(contentJSON)); - component.setState({ - editorState: component.createEditorState(component.state.isRichtextEnabled, content) - }); + component.setEditorState(component.createEditorState(component.state.isRichtextEnabled, content)); } } }; @@ -344,7 +342,7 @@ export default class MessageComposerInput extends React.Component { this.refs.editor.focus(); } - onChange(editorState: EditorState) { + setEditorState(editorState: EditorState) { this.setState({editorState}); if(editorState.getCurrentContent().hasText()) { @@ -361,15 +359,11 @@ export default class MessageComposerInput extends React.Component { enableRichtext(enabled: boolean) { if (enabled) { let html = mdownToHtml(this.state.editorState.getCurrentContent().getPlainText()); - this.setState({ - editorState: this.createEditorState(enabled, RichText.HTMLtoContentState(html)) - }); + this.setEditorState(this.createEditorState(enabled, RichText.HTMLtoContentState(html))); } else { let markdown = stateToMarkdown(this.state.editorState.getCurrentContent()), contentState = ContentState.createFromText(markdown); - this.setState({ - editorState: this.createEditorState(enabled, contentState) - }); + this.setEditorState(this.createEditorState(enabled, contentState)); } window.localStorage.setItem('mx_editor_rte_enabled', enabled); @@ -412,7 +406,7 @@ export default class MessageComposerInput extends React.Component { newState = RichUtils.handleKeyCommand(this.state.editorState, command); if (newState != null) { - this.onChange(newState); + this.setEditorState(newState); return true; } return false; @@ -506,7 +500,7 @@ export default class MessageComposerInput extends React.Component {