From 2db53c228493311ad2fd044936af6f930f570270 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 19 Feb 2017 03:04:42 +0200 Subject: [PATCH 001/491] whitelist data & mxc URIs on img tags: readds PR #333 now that punkave/sanitize-html#137 has landed --- package.json | 2 +- src/HtmlUtils.js | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index a07e2236aa..9b260e341a 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "react-addons-css-transition-group": "15.3.2", "react-dom": "^15.4.0", "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef", - "sanitize-html": "^1.11.1", + "sanitize-html": "^1.14.1", "text-encoding-utf-8": "^1.0.1", "velocity-vector": "vector-im/velocity#059e3b2", "whatwg-fetch": "^1.0.0" diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index b9d0ce67e8..8ae2c0a4a8 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -87,7 +87,7 @@ var sanitizeHtmlParams = { // deliberately no h1/h2 to stop people shouting. 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', 'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div', - 'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre' + 'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'img', ], allowedAttributes: { // custom ones first: @@ -102,10 +102,10 @@ var sanitizeHtmlParams = { // URL schemes we permit allowedSchemes: ['http', 'https', 'ftp', 'mailto'], - // DO NOT USE. sanitize-html allows all URL starting with '//' - // so this will always allow links to whatever scheme the - // host page is served over. - allowedSchemesByTag: {}, + allowedSchemesByTag: { + img: [ 'data', 'mxc' ], + }, + allowProtocolRelative: false, transformTags: { // custom to matrix // add blank targets to all hyperlinks except vector URLs From ab3b6497f99ccd35e1be8db9e8867efdb164a79e Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Tue, 11 Oct 2016 19:16:35 +0530 Subject: [PATCH 002/491] Disable "syntax highlighting" in MD mode (RTE) --- src/RichText.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/RichText.js b/src/RichText.js index b1793d0ddf..e662c22d6a 100644 --- a/src/RichText.js +++ b/src/RichText.js @@ -146,9 +146,9 @@ export function getScopedMDDecorators(scope: any): CompositeDecorator { ) }); - markdownDecorators.push(emojiDecorator); - - return markdownDecorators; + // markdownDecorators.push(emojiDecorator); + // TODO Consider renabling "syntax highlighting" when we can do it properly + return [emojiDecorator]; } /** From f2ad4bee8b5243766616cab150ea86e18660035f Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Tue, 11 Oct 2016 19:17:57 +0530 Subject: [PATCH 003/491] Disable force completion for RoomProvider (RTE) --- src/autocomplete/RoomProvider.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index 8d1e555e56..b589425b20 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -66,8 +66,4 @@ export default class RoomProvider extends AutocompleteProvider { {completions} ; } - - shouldForceComplete(): boolean { - return true; - } } From f4c0baaa2f02b5650597eddbe2f2b75344b9e8e3 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Wed, 30 Nov 2016 22:46:33 +0530 Subject: [PATCH 004/491] refactor MessageComposerInput: bind -> class props --- package.json | 2 +- .../views/rooms/MessageComposerInput.js | 156 ++++++++---------- 2 files changed, 73 insertions(+), 85 deletions(-) diff --git a/package.json b/package.json index a07e2236aa..1e5ee29d2d 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "browser-request": "^0.3.3", "classnames": "^2.1.2", "commonmark": "^0.27.0", - "draft-js": "^0.8.1", + "draft-js": "^0.9.1", "draft-js-export-html": "^0.5.0", "draft-js-export-markdown": "^0.2.0", "emojione": "2.2.3", diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 61dd1e1b1c..9ae420fde4 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -59,6 +59,29 @@ function stateToMarkdown(state) { * The textInput part of the MessageComposer */ export default class MessageComposerInput extends React.Component { + static 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, + + // called with current plaintext content (as a string) whenever it changes + onContentChanged: React.PropTypes.func, + + onUpArrow: React.PropTypes.func, + + onDownArrow: React.PropTypes.func, + + // attempts to confirm currently selected completion, returns whether actually confirmed + tryComplete: React.PropTypes.func, + + onInputStateChanged: React.PropTypes.func, + }; + static getKeyBinding(e: SyntheticKeyboardEvent): string { // C-m => Toggles between rich text and markdown modes if (e.keyCode === KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) { @@ -81,17 +104,6 @@ export default class MessageComposerInput extends React.Component { constructor(props, context) { super(props, context); - this.onAction = this.onAction.bind(this); - this.handleReturn = this.handleReturn.bind(this); - this.handleKeyCommand = this.handleKeyCommand.bind(this); - this.onEditorContentChanged = this.onEditorContentChanged.bind(this); - this.setEditorState = this.setEditorState.bind(this); - this.onUpArrow = this.onUpArrow.bind(this); - this.onDownArrow = this.onDownArrow.bind(this); - this.onTab = this.onTab.bind(this); - this.onEscape = this.onEscape.bind(this); - this.setDisplayedCompletion = this.setDisplayedCompletion.bind(this); - this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this); const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', true); @@ -120,7 +132,7 @@ export default class MessageComposerInput extends React.Component { */ createEditorState(richText: boolean, contentState: ?ContentState): EditorState { let decorators = richText ? RichText.getScopedRTDecorators(this.props) : - RichText.getScopedMDDecorators(this.props), + RichText.getScopedMDDecorators(this.props), compositeDecorator = new CompositeDecorator(decorators); let editorState = null; @@ -147,7 +159,7 @@ export default class MessageComposerInput extends React.Component { // The textarea element to set text to. element: null, - init: function(element, roomId) { + init: function (element, roomId) { this.roomId = roomId; this.element = element; this.position = -1; @@ -162,7 +174,7 @@ export default class MessageComposerInput extends React.Component { } }, - push: function(text) { + push: function (text) { // store a message in the sent history this.data.unshift(text); window.sessionStorage.setItem( @@ -175,7 +187,7 @@ export default class MessageComposerInput extends React.Component { }, // move in the history. Returns true if we managed to move. - next: function(offset) { + next: function (offset) { if (this.position === -1) { // user is going into the history, save the current line. this.originalText = this.element.value; @@ -208,7 +220,7 @@ export default class MessageComposerInput extends React.Component { return true; }, - saveLastTextEntry: function() { + saveLastTextEntry: function () { // save the currently entered text in order to restore it later. // NB: This isn't 'originalText' because we want to restore // sent history items too! @@ -216,7 +228,7 @@ export default class MessageComposerInput extends React.Component { window.sessionStorage.setItem("mx_messagecomposer_input_" + this.roomId, contentJSON); }, - setLastTextEntry: function() { + setLastTextEntry: function () { let contentJSON = window.sessionStorage.getItem("mx_messagecomposer_input_" + this.roomId); if (contentJSON) { let content = convertFromRaw(JSON.parse(contentJSON)); @@ -248,7 +260,7 @@ export default class MessageComposerInput extends React.Component { } } - onAction(payload) { + onAction = payload => { let editor = this.refs.editor; let contentState = this.state.editorState.getCurrentContent(); @@ -270,7 +282,7 @@ export default class MessageComposerInput extends React.Component { this.onEditorContentChanged(editorState); editor.focus(); } - break; + break; case 'quote': { let {body, formatted_body} = payload.event.getContent(); @@ -297,9 +309,9 @@ export default class MessageComposerInput extends React.Component { editor.focus(); } } - break; + break; } - } + }; onTypingActivity() { this.isTyping = true; @@ -320,7 +332,7 @@ export default class MessageComposerInput extends React.Component { startUserTypingTimer() { this.stopUserTypingTimer(); var self = this; - this.userTypingTimer = setTimeout(function() { + this.userTypingTimer = setTimeout(function () { self.isTyping = false; self.sendTyping(self.isTyping); self.userTypingTimer = null; @@ -337,7 +349,7 @@ export default class MessageComposerInput extends React.Component { startServerTypingTimer() { if (!this.serverTypingTimer) { var self = this; - this.serverTypingTimer = setTimeout(function() { + this.serverTypingTimer = setTimeout(function () { if (self.isTyping) { self.sendTyping(self.isTyping); self.startServerTypingTimer(); @@ -368,7 +380,7 @@ export default class MessageComposerInput extends React.Component { } // Called by Draft to change editor contents, and by setEditorState - onEditorContentChanged(editorState: EditorState, didRespondToUserInput: boolean = true) { + onEditorContentChanged = (editorState: EditorState, didRespondToUserInput: boolean = true) => { editorState = RichText.attachImmutableEntitiesToEmoji(editorState); const contentChanged = Q.defer(); @@ -392,11 +404,11 @@ export default class MessageComposerInput extends React.Component { this.props.onContentChanged(textContent, selection); } return contentChanged.promise; - } + }; - setEditorState(editorState: EditorState) { + setEditorState = (editorState: EditorState) => { return this.onEditorContentChanged(editorState, false); - } + }; enableRichtext(enabled: boolean) { let contentState = null; @@ -420,7 +432,7 @@ export default class MessageComposerInput extends React.Component { }); } - handleKeyCommand(command: string): boolean { + handleKeyCommand = (command: string): boolean => { if (command === 'toggle-mode') { this.enableRichtext(!this.state.isRichtextEnabled); return true; @@ -451,7 +463,7 @@ export default class MessageComposerInput extends React.Component { 'code': text => `\`${text}\``, 'blockquote': text => text.split('\n').map(line => `> ${line}\n`).join(''), 'unordered-list-item': text => text.split('\n').map(line => `- ${line}\n`).join(''), - 'ordered-list-item': text => text.split('\n').map((line, i) => `${i+1}. ${line}\n`).join(''), + 'ordered-list-item': text => text.split('\n').map((line, i) => `${i + 1}. ${line}\n`).join(''), }[command]; if (modifyFn) { @@ -473,9 +485,9 @@ export default class MessageComposerInput extends React.Component { } return false; - } + }; - handleReturn(ev) { + handleReturn = ev => { if (ev.shiftKey) { this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState)); return true; @@ -497,9 +509,9 @@ export default class MessageComposerInput extends React.Component { }); } if (cmd.promise) { - cmd.promise.then(function() { + cmd.promise.then(function () { console.log("Command success."); - }, function(err) { + }, function (err) { console.error("Command failure: %s", err); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { @@ -567,45 +579,44 @@ export default class MessageComposerInput extends React.Component { this.autocomplete.hide(); return true; - } + }; - async onUpArrow(e) { + onUpArrow = async e => { const completion = this.autocomplete.onUpArrow(); if (completion != null) { e.preventDefault(); } return await this.setDisplayedCompletion(completion); - } + }; - async onDownArrow(e) { + onDownArrow = async e => { const completion = this.autocomplete.onDownArrow(); e.preventDefault(); return await this.setDisplayedCompletion(completion); - } + }; // tab and shift-tab are mapped to down and up arrow respectively - async onTab(e) { + onTab = async e => { e.preventDefault(); // we *never* want tab's default to happen, but we do want up/down sometimes const didTab = await (e.shiftKey ? this.onUpArrow : this.onDownArrow)(e); if (!didTab && this.autocomplete) { - this.autocomplete.forceComplete().then(() => { - this.onDownArrow(e); - }); + await this.autocomplete.forceComplete(); + this.onDownArrow(e); } - } + }; - onEscape(e) { + onEscape = e => { e.preventDefault(); if (this.autocomplete) { this.autocomplete.onEscape(e); } this.setDisplayedCompletion(null); // restore originalEditorState - } + }; /* If passed null, restores the original editor content from state.originalEditorState. * If passed a non-null displayedCompletion, modifies state.originalEditorState to compute new state.editorState. */ - async setDisplayedCompletion(displayedCompletion: ?Completion): boolean { + setDisplayedCompletion = async (displayedCompletion: ?Completion): boolean => { const activeEditorState = this.state.originalEditorState || this.state.editorState; if (displayedCompletion == null) { @@ -633,21 +644,21 @@ export default class MessageComposerInput extends React.Component { // for some reason, doing this right away does not update the editor :( setTimeout(() => this.refs.editor.focus(), 50); return true; - } + }; onFormatButtonClicked(name: "bold" | "italic" | "strike" | "code" | "underline" | "quote" | "bullet" | "numbullet", e) { e.preventDefault(); // don't steal focus from the editor! const command = { - code: 'code-block', - quote: 'blockquote', - bullet: 'unordered-list-item', - numbullet: 'ordered-list-item', - }[name] || name; + code: 'code-block', + quote: 'blockquote', + bullet: 'unordered-list-item', + numbullet: 'ordered-list-item', + }[name] || name; this.handleKeyCommand(command); } /* returns inline style and block type of current SelectionState so MessageComposer can render formatting - buttons. */ + buttons. */ getSelectionInfo(editorState: EditorState) { const styleName = { BOLD: 'bold', @@ -658,8 +669,8 @@ export default class MessageComposerInput extends React.Component { const originalStyle = editorState.getCurrentInlineStyle().toArray(); const style = originalStyle - .map(style => styleName[style] || null) - .filter(styleName => !!styleName); + .map(style => styleName[style] || null) + .filter(styleName => !!styleName); const blockName = { 'code-block': 'code', @@ -678,10 +689,10 @@ export default class MessageComposerInput extends React.Component { }; } - onMarkdownToggleClicked(e) { + onMarkdownToggleClicked = e => { e.preventDefault(); // don't steal focus from the editor! this.handleKeyCommand('toggle-mode'); - } + }; render() { const activeEditorState = this.state.originalEditorState || this.state.editorState; @@ -698,7 +709,7 @@ export default class MessageComposerInput extends React.Component { } const className = classNames('mx_MessageComposer_input', { - mx_MessageComposer_input_empty: hidePlaceholder, + mx_MessageComposer_input_empty: hidePlaceholder, }); const content = activeEditorState.getCurrentContent(); @@ -713,13 +724,13 @@ export default class MessageComposerInput extends React.Component { ref={(e) => this.autocomplete = e} onConfirm={this.setDisplayedCompletion} query={contentText} - selection={selection} /> + selection={selection}/>
+ src={`img/button-md-${!this.state.isRichtextEnabled}.png`}/> + spellCheck={true}/>
); } } - -MessageComposerInput.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, - - // called with current plaintext content (as a string) whenever it changes - onContentChanged: React.PropTypes.func, - - onUpArrow: React.PropTypes.func, - - onDownArrow: React.PropTypes.func, - - // attempts to confirm currently selected completion, returns whether actually confirmed - tryComplete: React.PropTypes.func, - - onInputStateChanged: React.PropTypes.func, -}; From edd5903ed7e6bd6522eea588e752e59f45623d7e Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Wed, 30 Nov 2016 23:12:03 +0530 Subject: [PATCH 005/491] autocomplete: add space after completing room name --- src/autocomplete/RoomProvider.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index b589425b20..85f94926d9 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -38,7 +38,7 @@ export default class RoomProvider extends AutocompleteProvider { completions = this.fuse.search(command[0]).map(room => { let displayAlias = getDisplayAliasForRoom(room.room) || room.roomId; return { - completion: displayAlias, + completion: displayAlias + ' ', component: ( } title={room.name} description={displayAlias} /> ), From 78641a80ddf7a6fa2cb951fa526249406a814495 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Thu, 1 Dec 2016 12:06:57 +0530 Subject: [PATCH 006/491] autocomplete: replace Fuse.js with liblevenshtein --- package.json | 2 +- src/autocomplete/AutocompleteProvider.js | 2 +- src/autocomplete/CommandProvider.js | 6 +- src/autocomplete/EmojiProvider.js | 6 +- src/autocomplete/FuzzyMatcher.js | 74 ++++++++++++++++++++++++ src/autocomplete/RoomProvider.js | 14 ++--- src/autocomplete/UserProvider.js | 8 +-- 7 files changed, 92 insertions(+), 20 deletions(-) create mode 100644 src/autocomplete/FuzzyMatcher.js diff --git a/package.json b/package.json index 1e5ee29d2d..1015eb3fe9 100644 --- a/package.json +++ b/package.json @@ -55,10 +55,10 @@ "file-saver": "^1.3.3", "filesize": "^3.1.2", "flux": "^2.0.3", - "fuse.js": "^2.2.0", "glob": "^5.0.14", "highlight.js": "^8.9.1", "isomorphic-fetch": "^2.2.1", + "liblevenshtein": "^2.0.4", "linkifyjs": "^2.1.3", "lodash": "^4.13.1", "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop", diff --git a/src/autocomplete/AutocompleteProvider.js b/src/autocomplete/AutocompleteProvider.js index 5c90990295..c361dd295b 100644 --- a/src/autocomplete/AutocompleteProvider.js +++ b/src/autocomplete/AutocompleteProvider.js @@ -2,7 +2,7 @@ import React from 'react'; import type {Completion, SelectionRange} from './Autocompleter'; export default class AutocompleteProvider { - constructor(commandRegex?: RegExp, fuseOpts?: any) { + constructor(commandRegex?: RegExp) { if (commandRegex) { if (!commandRegex.global) { throw new Error('commandRegex must have global flag set'); diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index 60171bc72f..8f98bf1aa5 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -1,6 +1,6 @@ import React from 'react'; import AutocompleteProvider from './AutocompleteProvider'; -import Fuse from 'fuse.js'; +import FuzzyMatcher from './FuzzyMatcher'; import {TextualCompletion} from './Components'; const COMMANDS = [ @@ -53,7 +53,7 @@ let instance = null; export default class CommandProvider extends AutocompleteProvider { constructor() { super(COMMAND_RE); - this.fuse = new Fuse(COMMANDS, { + this.matcher = new FuzzyMatcher(COMMANDS, { keys: ['command', 'args', 'description'], }); } @@ -62,7 +62,7 @@ export default class CommandProvider extends AutocompleteProvider { let completions = []; let {command, range} = this.getCurrentCommand(query, selection); if (command) { - completions = this.fuse.search(command[0]).map(result => { + completions = this.matcher.match(command[0]).map(result => { return { completion: result.command + ' ', component: ( { + completions = this.matcher.match(command[0]).map(result => { const shortname = EMOJI_SHORTNAMES[result]; const unicode = shortnameToUnicode(shortname); return { diff --git a/src/autocomplete/FuzzyMatcher.js b/src/autocomplete/FuzzyMatcher.js new file mode 100644 index 0000000000..c02ee9bbc0 --- /dev/null +++ b/src/autocomplete/FuzzyMatcher.js @@ -0,0 +1,74 @@ +import Levenshtein from 'liblevenshtein'; +import _at from 'lodash/at'; +import _flatMap from 'lodash/flatMap'; +import _sortBy from 'lodash/sortBy'; +import _sortedUniq from 'lodash/sortedUniq'; +import _keys from 'lodash/keys'; + +class KeyMap { + keys: Array; + objectMap: {[String]: Array}; + priorityMap: {[String]: number} +} + +const DEFAULT_RESULT_COUNT = 10; +const DEFAULT_DISTANCE = 5; + +export default class FuzzyMatcher { + /** + * Given an array of objects and keys, returns a KeyMap + * Keys can refer to object properties by name and as in JavaScript (for nested properties) + * + * To use, simply presort objects by required criteria, run through this function and create a FuzzyMatcher with the + * resulting KeyMap. + * + * TODO: Handle arrays and objects (Fuse did this, RoomProvider uses it) + */ + static valuesToKeyMap(objects: Array, keys: Array): KeyMap { + const keyMap = new KeyMap(); + const map = {}; + const priorities = {}; + + objects.forEach((object, i) => { + const keyValues = _at(object, keys); + console.log(object, keyValues, keys); + for (const keyValue of keyValues) { + if (!map.hasOwnProperty(keyValue)) { + map[keyValue] = []; + } + map[keyValue].push(object); + } + priorities[object] = i; + }); + + keyMap.objectMap = map; + keyMap.priorityMap = priorities; + keyMap.keys = _sortBy(_keys(map), [value => priorities[value]]); + return keyMap; + } + + constructor(objects: Array, options: {[Object]: Object} = {}) { + this.options = options; + this.keys = options.keys; + this.setObjects(objects); + } + + setObjects(objects: Array) { + this.keyMap = FuzzyMatcher.valuesToKeyMap(objects, this.keys); + console.log(this.keyMap.keys); + this.matcher = new Levenshtein.Builder() + .dictionary(this.keyMap.keys, true) + .algorithm('transposition') + .sort_candidates(false) + .case_insensitive_sort(true) + .include_distance(false) + .maximum_candidates(this.options.resultCount || DEFAULT_RESULT_COUNT) // result count 0 doesn't make much sense + .build(); + } + + match(query: String): Array { + const candidates = this.matcher.transduce(query, this.options.distance || DEFAULT_DISTANCE); + return _sortedUniq(_sortBy(_flatMap(candidates, candidate => this.keyMap.objectMap[candidate]), + candidate => this.keyMap.priorityMap[candidate])); + } +} diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index 85f94926d9..8659b8501f 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -1,7 +1,7 @@ import React from 'react'; import AutocompleteProvider from './AutocompleteProvider'; import MatrixClientPeg from '../MatrixClientPeg'; -import Fuse from 'fuse.js'; +import FuzzyMatcher from './FuzzyMatcher'; import {PillCompletion} from './Components'; import {getDisplayAliasForRoom} from '../Rooms'; import sdk from '../index'; @@ -12,11 +12,9 @@ let instance = null; export default class RoomProvider extends AutocompleteProvider { constructor() { - super(ROOM_REGEX, { - keys: ['displayName', 'userId'], - }); - this.fuse = new Fuse([], { - keys: ['name', 'roomId', 'aliases'], + super(ROOM_REGEX); + this.matcher = new FuzzyMatcher([], { + keys: ['name', 'aliases'], }); } @@ -28,14 +26,14 @@ export default class RoomProvider extends AutocompleteProvider { const {command, range} = this.getCurrentCommand(query, selection, force); if (command) { // the only reason we need to do this is because Fuse only matches on properties - this.fuse.set(client.getRooms().filter(room => !!room).map(room => { + this.matcher.setObjects(client.getRooms().filter(room => !!room).map(room => { return { room: room, name: room.name, aliases: room.getAliases(), }; })); - completions = this.fuse.search(command[0]).map(room => { + completions = this.matcher.match(command[0]).map(room => { let displayAlias = getDisplayAliasForRoom(room.room) || room.roomId; return { completion: displayAlias + ' ', diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 4d40fbdf94..b65439181c 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -1,9 +1,9 @@ import React from 'react'; import AutocompleteProvider from './AutocompleteProvider'; import Q from 'q'; -import Fuse from 'fuse.js'; import {PillCompletion} from './Components'; import sdk from '../index'; +import FuzzyMatcher from './FuzzyMatcher'; const USER_REGEX = /@\S*/g; @@ -15,7 +15,7 @@ export default class UserProvider extends AutocompleteProvider { keys: ['name', 'userId'], }); this.users = []; - this.fuse = new Fuse([], { + this.matcher = new FuzzyMatcher([], { keys: ['name', 'userId'], }); } @@ -26,8 +26,7 @@ export default class UserProvider extends AutocompleteProvider { let completions = []; let {command, range} = this.getCurrentCommand(query, selection, force); if (command) { - this.fuse.set(this.users); - completions = this.fuse.search(command[0]).map(user => { + completions = this.matcher.match(command[0]).map(user => { let displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done let completion = displayName; if (range.start === 0) { @@ -56,6 +55,7 @@ export default class UserProvider extends AutocompleteProvider { setUserList(users) { this.users = users; + this.matcher.setObjects(this.users); } static getInstance(): UserProvider { From 48376a32c251d463d525541c1edc0a4370300e04 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Fri, 30 Dec 2016 19:42:36 +0530 Subject: [PATCH 007/491] refactor: MessageComposer.setEditorState to overridden setState The old approach led to a confusing proliferation of repeated setState calls. --- .../views/rooms/MessageComposerInput.js | 97 +++++++++++-------- 1 file changed, 58 insertions(+), 39 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 9ae420fde4..b830d52239 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -232,7 +232,9 @@ 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.setEditorState(component.createEditorState(component.state.isRichtextEnabled, content)); + component.setState({ + editorState: component.createEditorState(component.state.isRichtextEnabled, content) + }); } }, }; @@ -379,36 +381,54 @@ export default class MessageComposerInput extends React.Component { } } - // Called by Draft to change editor contents, and by setEditorState - onEditorContentChanged = (editorState: EditorState, didRespondToUserInput: boolean = true) => { + // Called by Draft to change editor contents + onEditorContentChanged = (editorState: EditorState) => { editorState = RichText.attachImmutableEntitiesToEmoji(editorState); - const contentChanged = Q.defer(); - /* If a modification was made, set originalEditorState to null, since newState is now our original */ + /* Since a modification was made, set originalEditorState to null, since newState is now our original */ this.setState({ editorState, - originalEditorState: didRespondToUserInput ? null : this.state.originalEditorState, - }, () => contentChanged.resolve()); - - if (editorState.getCurrentContent().hasText()) { - this.onTypingActivity(); - } else { - this.onFinishedTyping(); - } - - if (this.props.onContentChanged) { - const textContent = editorState.getCurrentContent().getPlainText(); - const selection = RichText.selectionStateToTextOffsets(editorState.getSelection(), - editorState.getCurrentContent().getBlocksAsArray()); - - this.props.onContentChanged(textContent, selection); - } - return contentChanged.promise; + originalEditorState: null, + }); }; - setEditorState = (editorState: EditorState) => { - return this.onEditorContentChanged(editorState, false); - }; + /** + * We're overriding setState here because it's the most convenient way to monitor changes to the editorState. + * Doing it using a separate function that calls setState is a possibility (and was the old approach), but that + * approach requires a callback and an extra setState whenever trying to set multiple state properties. + * + * @param state + * @param callback + */ + setState(state, callback) { + if (state.editorState != null) { + state.editorState = RichText.attachImmutableEntitiesToEmoji(state.editorState); + + if (state.editorState.getCurrentContent().hasText()) { + this.onTypingActivity(); + } else { + this.onFinishedTyping(); + } + + if (!state.hasOwnProperty('originalEditorState')) { + state.originalEditorState = null; + } + } + + super.setState(state, (state, props, context) => { + if (callback != null) { + callback(state, props, context); + } + + if (this.props.onContentChanged) { + const textContent = state.editorState.getCurrentContent().getPlainText(); + const selection = RichText.selectionStateToTextOffsets(state.editorState.getSelection(), + state.editorState.getCurrentContent().getBlocksAsArray()); + + this.props.onContentChanged(textContent, selection); + } + }); + } enableRichtext(enabled: boolean) { let contentState = null; @@ -423,13 +443,11 @@ export default class MessageComposerInput extends React.Component { contentState = ContentState.createFromText(markdown); } - this.setEditorState(this.createEditorState(enabled, contentState)).then(() => { - this.setState({ - isRichtextEnabled: enabled, - }); - - UserSettingsStore.setSyncedSetting('MessageComposerInput.isRichTextEnabled', enabled); + this.setState({ + editorState: this.createEditorState(enabled, contentState), + isRichtextEnabled: enabled, }); + UserSettingsStore.setSyncedSetting('MessageComposerInput.isRichTextEnabled', enabled); } handleKeyCommand = (command: string): boolean => { @@ -446,10 +464,14 @@ export default class MessageComposerInput extends React.Component { const blockCommands = ['code-block', 'blockquote', 'unordered-list-item', 'ordered-list-item']; if (blockCommands.includes(command)) { - this.setEditorState(RichUtils.toggleBlockType(this.state.editorState, command)); + this.setState({ + editorState: RichUtils.toggleBlockType(this.state.editorState, command) + }); } else if (command === 'strike') { // this is the only inline style not handled by Draft by default - this.setEditorState(RichUtils.toggleInlineStyle(this.state.editorState, 'STRIKETHROUGH')); + this.setState({ + editorState: RichUtils.toggleInlineStyle(this.state.editorState, 'STRIKETHROUGH') + }); } } else { let contentState = this.state.editorState.getCurrentContent(), @@ -480,7 +502,7 @@ export default class MessageComposerInput extends React.Component { } if (newState != null) { - this.setEditorState(newState); + this.setState({editorState: newState}); return true; } @@ -621,7 +643,7 @@ export default class MessageComposerInput extends React.Component { if (displayedCompletion == null) { if (this.state.originalEditorState) { - this.setEditorState(this.state.originalEditorState); + this.setState({editorState: this.state.originalEditorState}); } return false; } @@ -636,10 +658,7 @@ export default class MessageComposerInput extends React.Component { let editorState = EditorState.push(activeEditorState, contentState, 'insert-characters'); editorState = EditorState.forceSelection(editorState, contentState.getSelectionAfter()); - const originalEditorState = activeEditorState; - - await this.setEditorState(editorState); - this.setState({originalEditorState}); + this.setState({editorState, originalEditorState: activeEditorState}); // for some reason, doing this right away does not update the editor :( setTimeout(() => this.refs.editor.focus(), 50); From aaac06c6d3f98473a58ab9a839b754e0787ce30b Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Fri, 10 Feb 2017 01:33:06 +0530 Subject: [PATCH 008/491] run eslint --fix over MessageComposerInput --- .../views/rooms/MessageComposerInput.js | 119 +++++++++--------- 1 file changed, 58 insertions(+), 61 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index b830d52239..b83e5d8dbf 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -159,12 +159,12 @@ export default class MessageComposerInput extends React.Component { // The textarea element to set text to. element: null, - init: function (element, roomId) { + init: function(element, roomId) { this.roomId = roomId; this.element = element; this.position = -1; - var storedData = window.sessionStorage.getItem( - "mx_messagecomposer_history_" + roomId + const storedData = window.sessionStorage.getItem( + "mx_messagecomposer_history_" + roomId, ); if (storedData) { this.data = JSON.parse(storedData); @@ -174,12 +174,12 @@ export default class MessageComposerInput extends React.Component { } }, - push: function (text) { + push: function(text) { // store a message in the sent history this.data.unshift(text); window.sessionStorage.setItem( "mx_messagecomposer_history_" + this.roomId, - JSON.stringify(this.data) + JSON.stringify(this.data), ); // reset history position this.position = -1; @@ -187,12 +187,11 @@ export default class MessageComposerInput extends React.Component { }, // move in the history. Returns true if we managed to move. - next: function (offset) { + next: function(offset) { if (this.position === -1) { // user is going into the history, save the current line. this.originalText = this.element.value; - } - else { + } else { // user may have modified this line in the history; remember it. this.data[this.position] = this.element.value; } @@ -203,7 +202,7 @@ export default class MessageComposerInput extends React.Component { } // retrieve the next item (bounded). - var newPosition = this.position + offset; + let newPosition = this.position + offset; newPosition = Math.max(-1, newPosition); newPosition = Math.min(newPosition, this.data.length - 1); this.position = newPosition; @@ -211,8 +210,7 @@ export default class MessageComposerInput extends React.Component { if (this.position !== -1) { // show the message this.element.value = this.data[this.position]; - } - else if (this.originalText !== undefined) { + } else if (this.originalText !== undefined) { // restore the original text the user was typing. this.element.value = this.originalText; } @@ -220,20 +218,20 @@ export default class MessageComposerInput extends React.Component { return true; }, - saveLastTextEntry: function () { + saveLastTextEntry: function() { // save the currently entered text in order to restore it later. // NB: This isn't 'originalText' because we want to restore // sent history items too! - let contentJSON = JSON.stringify(convertToRaw(component.state.editorState.getCurrentContent())); + const contentJSON = JSON.stringify(convertToRaw(component.state.editorState.getCurrentContent())); window.sessionStorage.setItem("mx_messagecomposer_input_" + this.roomId, contentJSON); }, - setLastTextEntry: function () { - let contentJSON = window.sessionStorage.getItem("mx_messagecomposer_input_" + this.roomId); + setLastTextEntry: function() { + const contentJSON = window.sessionStorage.getItem("mx_messagecomposer_input_" + this.roomId); if (contentJSON) { - let content = convertFromRaw(JSON.parse(contentJSON)); + const content = convertFromRaw(JSON.parse(contentJSON)); component.setState({ - editorState: component.createEditorState(component.state.isRichtextEnabled, content) + editorState: component.createEditorState(component.state.isRichtextEnabled, content), }); } }, @@ -244,7 +242,7 @@ export default class MessageComposerInput extends React.Component { this.dispatcherRef = dis.register(this.onAction); this.sentHistory.init( this.refs.editor, - this.props.room.roomId + this.props.room.roomId, ); } @@ -262,8 +260,8 @@ export default class MessageComposerInput extends React.Component { } } - onAction = payload => { - let editor = this.refs.editor; + onAction = (payload) => { + const editor = this.refs.editor; let contentState = this.state.editorState.getCurrentContent(); switch (payload.action) { @@ -277,7 +275,7 @@ export default class MessageComposerInput extends React.Component { contentState = Modifier.replaceText( contentState, this.state.editorState.getSelection(), - `${payload.displayname}: ` + `${payload.displayname}: `, ); let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters'); editorState = EditorState.forceSelection(editorState, contentState.getSelectionAfter()); @@ -306,7 +304,7 @@ export default class MessageComposerInput extends React.Component { if (this.state.isRichtextEnabled) { contentState = Modifier.setBlockType(contentState, startSelection, 'blockquote'); } - let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters'); + const editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters'); this.onEditorContentChanged(editorState); editor.focus(); } @@ -333,8 +331,8 @@ export default class MessageComposerInput extends React.Component { startUserTypingTimer() { this.stopUserTypingTimer(); - var self = this; - this.userTypingTimer = setTimeout(function () { + const self = this; + this.userTypingTimer = setTimeout(function() { self.isTyping = false; self.sendTyping(self.isTyping); self.userTypingTimer = null; @@ -350,8 +348,8 @@ export default class MessageComposerInput extends React.Component { startServerTypingTimer() { if (!this.serverTypingTimer) { - var self = this; - this.serverTypingTimer = setTimeout(function () { + const self = this; + this.serverTypingTimer = setTimeout(function() { if (self.isTyping) { self.sendTyping(self.isTyping); self.startServerTypingTimer(); @@ -370,7 +368,7 @@ export default class MessageComposerInput extends React.Component { sendTyping(isTyping) { MatrixClientPeg.get().sendTyping( this.props.room.roomId, - this.isTyping, TYPING_SERVER_TIMEOUT + this.isTyping, TYPING_SERVER_TIMEOUT, ).done(); } @@ -465,34 +463,34 @@ export default class MessageComposerInput extends React.Component { if (blockCommands.includes(command)) { this.setState({ - editorState: RichUtils.toggleBlockType(this.state.editorState, command) + editorState: RichUtils.toggleBlockType(this.state.editorState, command), }); } else if (command === 'strike') { // this is the only inline style not handled by Draft by default this.setState({ - editorState: RichUtils.toggleInlineStyle(this.state.editorState, 'STRIKETHROUGH') + editorState: RichUtils.toggleInlineStyle(this.state.editorState, 'STRIKETHROUGH'), }); } } else { let contentState = this.state.editorState.getCurrentContent(), selection = this.state.editorState.getSelection(); - let modifyFn = { - 'bold': text => `**${text}**`, - 'italic': text => `*${text}*`, - 'underline': text => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug* - 'strike': text => `~~${text}~~`, - 'code': text => `\`${text}\``, - 'blockquote': text => text.split('\n').map(line => `> ${line}\n`).join(''), - 'unordered-list-item': text => text.split('\n').map(line => `- ${line}\n`).join(''), - 'ordered-list-item': text => text.split('\n').map((line, i) => `${i + 1}. ${line}\n`).join(''), + const modifyFn = { + 'bold': (text) => `**${text}**`, + 'italic': (text) => `*${text}*`, + 'underline': (text) => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug* + 'strike': (text) => `~~${text}~~`, + 'code': (text) => `\`${text}\``, + 'blockquote': (text) => text.split('\n').map((line) => `> ${line}\n`).join(''), + 'unordered-list-item': (text) => text.split('\n').map((line) => `- ${line}\n`).join(''), + 'ordered-list-item': (text) => text.split('\n').map((line, i) => `${i + 1}. ${line}\n`).join(''), }[command]; if (modifyFn) { newState = EditorState.push( this.state.editorState, RichText.modifyText(contentState, selection, modifyFn), - 'insert-characters' + 'insert-characters', ); } } @@ -509,7 +507,7 @@ export default class MessageComposerInput extends React.Component { return false; }; - handleReturn = ev => { + handleReturn = (ev) => { if (ev.shiftKey) { this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState)); return true; @@ -523,31 +521,30 @@ export default class MessageComposerInput extends React.Component { let contentText = contentState.getPlainText(), contentHTML; - var cmd = SlashCommands.processInput(this.props.room.roomId, contentText); + const cmd = SlashCommands.processInput(this.props.room.roomId, contentText); if (cmd) { if (!cmd.error) { this.setState({ - editorState: this.createEditorState() + editorState: this.createEditorState(), }); } if (cmd.promise) { - cmd.promise.then(function () { + cmd.promise.then(function() { console.log("Command success."); - }, function (err) { + }, function(err) { console.error("Command failure: %s", err); - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Server error", - description: err.message + description: err.message, }); }); - } - else if (cmd.error) { + } else if (cmd.error) { console.error(cmd.error); - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Command error", - description: cmd.error + description: cmd.error, }); } return true; @@ -555,7 +552,7 @@ export default class MessageComposerInput extends React.Component { if (this.state.isRichtextEnabled) { contentHTML = HtmlUtils.stripParagraphs( - RichText.contentStateToHTML(contentState) + RichText.contentStateToHTML(contentState), ); } else { const md = new Markdown(contentText); @@ -582,7 +579,7 @@ export default class MessageComposerInput extends React.Component { let sendMessagePromise; if (contentHTML) { sendMessagePromise = sendHtmlFn.call( - this.client, this.props.room.roomId, contentText, contentHTML + this.client, this.props.room.roomId, contentText, contentHTML, ); } else { sendMessagePromise = sendTextFn.call(this.client, this.props.room.roomId, contentText); @@ -603,7 +600,7 @@ export default class MessageComposerInput extends React.Component { return true; }; - onUpArrow = async e => { + onUpArrow = async (e) => { const completion = this.autocomplete.onUpArrow(); if (completion != null) { e.preventDefault(); @@ -611,14 +608,14 @@ export default class MessageComposerInput extends React.Component { return await this.setDisplayedCompletion(completion); }; - onDownArrow = async e => { + onDownArrow = async (e) => { const completion = this.autocomplete.onDownArrow(); e.preventDefault(); return await this.setDisplayedCompletion(completion); }; // tab and shift-tab are mapped to down and up arrow respectively - onTab = async e => { + onTab = async (e) => { e.preventDefault(); // we *never* want tab's default to happen, but we do want up/down sometimes const didTab = await (e.shiftKey ? this.onUpArrow : this.onDownArrow)(e); if (!didTab && this.autocomplete) { @@ -627,7 +624,7 @@ export default class MessageComposerInput extends React.Component { } }; - onEscape = e => { + onEscape = (e) => { e.preventDefault(); if (this.autocomplete) { this.autocomplete.onEscape(e); @@ -650,10 +647,10 @@ export default class MessageComposerInput extends React.Component { const {range = {}, completion = ''} = displayedCompletion; - let contentState = Modifier.replaceText( + const contentState = Modifier.replaceText( activeEditorState.getCurrentContent(), RichText.textOffsetsToSelectionState(range, activeEditorState.getCurrentContent().getBlocksAsArray()), - completion + completion, ); let editorState = EditorState.push(activeEditorState, contentState, 'insert-characters'); @@ -688,8 +685,8 @@ export default class MessageComposerInput extends React.Component { const originalStyle = editorState.getCurrentInlineStyle().toArray(); const style = originalStyle - .map(style => styleName[style] || null) - .filter(styleName => !!styleName); + .map((style) => styleName[style] || null) + .filter((styleName) => !!styleName); const blockName = { 'code-block': 'code', @@ -708,7 +705,7 @@ export default class MessageComposerInput extends React.Component { }; } - onMarkdownToggleClicked = e => { + onMarkdownToggleClicked = (e) => { e.preventDefault(); // don't steal focus from the editor! this.handleKeyCommand('toggle-mode'); }; From 46d30c378d647cce7187ae128562170ea9e28726 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Fri, 10 Feb 2017 02:06:06 +0530 Subject: [PATCH 009/491] fix tab focus issue in MessageComposerInput onTab was incorrectly implemented causing forceComplete instead of focusing the editor --- src/components/views/rooms/Autocomplete.js | 6 ++++++ .../views/rooms/MessageComposerInput.js | 21 ++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index 9be91e068a..9a3a04376d 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -149,6 +149,7 @@ export default class Autocomplete extends React.Component { const done = Q.defer(); this.setState({ forceComplete: true, + hide: false, }, () => { this.complete(this.props.query, this.props.selection).then(() => { done.resolve(); @@ -185,6 +186,11 @@ export default class Autocomplete extends React.Component { } } + setState(state, func) { + super.setState(state, func); + console.log(state); + } + render() { const EmojiText = sdk.getComponent('views.elements.EmojiText'); diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index b83e5d8dbf..c0d19987c7 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -400,7 +400,8 @@ export default class MessageComposerInput extends React.Component { */ setState(state, callback) { if (state.editorState != null) { - state.editorState = RichText.attachImmutableEntitiesToEmoji(state.editorState); + state.editorState = RichText.attachImmutableEntitiesToEmoji( + state.editorState); if (state.editorState.getCurrentContent().hasText()) { this.onTypingActivity(); @@ -413,15 +414,17 @@ export default class MessageComposerInput extends React.Component { } } - super.setState(state, (state, props, context) => { + super.setState(state, () => { if (callback != null) { - callback(state, props, context); + callback(); } if (this.props.onContentChanged) { - const textContent = state.editorState.getCurrentContent().getPlainText(); - const selection = RichText.selectionStateToTextOffsets(state.editorState.getSelection(), - state.editorState.getCurrentContent().getBlocksAsArray()); + const textContent = this.state.editorState + .getCurrentContent().getPlainText(); + const selection = RichText.selectionStateToTextOffsets( + this.state.editorState.getSelection(), + this.state.editorState.getCurrentContent().getBlocksAsArray()); this.props.onContentChanged(textContent, selection); } @@ -616,11 +619,13 @@ export default class MessageComposerInput extends React.Component { // tab and shift-tab are mapped to down and up arrow respectively onTab = async (e) => { + console.log('onTab'); e.preventDefault(); // we *never* want tab's default to happen, but we do want up/down sometimes - const didTab = await (e.shiftKey ? this.onUpArrow : this.onDownArrow)(e); - if (!didTab && this.autocomplete) { + if (this.autocomplete.state.completionList.length === 0) { await this.autocomplete.forceComplete(); this.onDownArrow(e); + } else { + await (e.shiftKey ? this.onUpArrow : this.onDownArrow)(e); } }; From 5fbe06ed91497eeff46e395f1e38164d99475d6d Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Fri, 10 Feb 2017 03:40:57 +0530 Subject: [PATCH 010/491] force editor rerender when we swap editorStates --- src/components/views/rooms/Autocomplete.js | 23 +++++++++---------- .../views/rooms/MessageComposerInput.js | 19 +++++++++++---- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index 9a3a04376d..c06786a80c 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -58,7 +58,7 @@ export default class Autocomplete extends React.Component { return; } - const completionList = flatMap(completions, provider => provider.completions); + const completionList = flatMap(completions, (provider) => provider.completions); // Reset selection when completion list becomes empty. let selectionOffset = COMPOSER_SELECTED; @@ -69,7 +69,7 @@ export default class Autocomplete extends React.Component { const currentSelection = this.state.selectionOffset === 0 ? null : this.state.completionList[this.state.selectionOffset - 1].completion; selectionOffset = completionList.findIndex( - completion => completion.completion === currentSelection); + (completion) => completion.completion === currentSelection); if (selectionOffset === -1) { selectionOffset = COMPOSER_SELECTED; } else { @@ -82,8 +82,8 @@ export default class Autocomplete extends React.Component { let hide = this.state.hide; // These are lists of booleans that indicate whether whether the corresponding provider had a matching pattern - const oldMatches = this.state.completions.map(completion => !!completion.command.command), - newMatches = completions.map(completion => !!completion.command.command); + const oldMatches = this.state.completions.map((completion) => !!completion.command.command), + newMatches = completions.map((completion) => !!completion.command.command); // So, essentially, we re-show autocomplete if any provider finds a new pattern or stops finding an old one if (!isEqual(oldMatches, newMatches)) { @@ -170,7 +170,7 @@ export default class Autocomplete extends React.Component { } setSelection(selectionOffset: number) { - this.setState({selectionOffset}); + this.setState({selectionOffset, hide: false}); } componentDidUpdate() { @@ -195,17 +195,16 @@ export default class Autocomplete extends React.Component { const EmojiText = sdk.getComponent('views.elements.EmojiText'); let position = 1; - let renderedCompletions = this.state.completions.map((completionResult, i) => { - let completions = completionResult.completions.map((completion, i) => { - + const renderedCompletions = this.state.completions.map((completionResult, i) => { + const completions = completionResult.completions.map((completion, i) => { const className = classNames('mx_Autocomplete_Completion', { 'selected': position === this.state.selectionOffset, }); - let componentPosition = position; + const componentPosition = position; position++; - let onMouseOver = () => this.setSelection(componentPosition); - let onClick = () => { + const onMouseOver = () => this.setSelection(componentPosition); + const onClick = () => { this.setSelection(componentPosition); this.onCompletionClicked(); }; @@ -226,7 +225,7 @@ export default class Autocomplete extends React.Component { {completionResult.provider.renderCompletions(completions)} ) : null; - }).filter(completion => !!completion); + }).filter((completion) => !!completion); return !this.state.hide && renderedCompletions.length > 0 ? (
this.container = e}> diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index c0d19987c7..7908d7f375 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -414,6 +414,8 @@ export default class MessageComposerInput extends React.Component { } } + console.log(state); + super.setState(state, () => { if (callback != null) { callback(); @@ -425,7 +427,7 @@ export default class MessageComposerInput extends React.Component { const selection = RichText.selectionStateToTextOffsets( this.state.editorState.getSelection(), this.state.editorState.getCurrentContent().getBlocksAsArray()); - + console.log(textContent); this.props.onContentChanged(textContent, selection); } }); @@ -629,12 +631,12 @@ export default class MessageComposerInput extends React.Component { } }; - onEscape = (e) => { + onEscape = async (e) => { e.preventDefault(); if (this.autocomplete) { this.autocomplete.onEscape(e); } - this.setDisplayedCompletion(null); // restore originalEditorState + await this.setDisplayedCompletion(null); // restore originalEditorState }; /* If passed null, restores the original editor content from state.originalEditorState. @@ -645,7 +647,14 @@ export default class MessageComposerInput extends React.Component { if (displayedCompletion == null) { if (this.state.originalEditorState) { - this.setState({editorState: this.state.originalEditorState}); + console.log('setting editorState to originalEditorState'); + let editorState = this.state.originalEditorState; + // This is a workaround from https://github.com/facebook/draft-js/issues/458 + // Due to the way we swap editorStates, Draft does not rerender at times + editorState = EditorState.forceSelection(editorState, + editorState.getSelection()); + this.setState({editorState}); + } return false; } @@ -663,7 +672,7 @@ export default class MessageComposerInput extends React.Component { this.setState({editorState, originalEditorState: activeEditorState}); // for some reason, doing this right away does not update the editor :( - setTimeout(() => this.refs.editor.focus(), 50); + // setTimeout(() => this.refs.editor.focus(), 50); return true; }; From c7d065276222cb5cb6506adccfae9ce249256201 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Fri, 10 Feb 2017 04:26:36 +0530 Subject: [PATCH 011/491] actually sort autocomplete results by distance --- src/autocomplete/FuzzyMatcher.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/autocomplete/FuzzyMatcher.js b/src/autocomplete/FuzzyMatcher.js index c02ee9bbc0..bd19fc53e8 100644 --- a/src/autocomplete/FuzzyMatcher.js +++ b/src/autocomplete/FuzzyMatcher.js @@ -61,14 +61,24 @@ export default class FuzzyMatcher { .algorithm('transposition') .sort_candidates(false) .case_insensitive_sort(true) - .include_distance(false) + .include_distance(true) .maximum_candidates(this.options.resultCount || DEFAULT_RESULT_COUNT) // result count 0 doesn't make much sense .build(); } match(query: String): Array { const candidates = this.matcher.transduce(query, this.options.distance || DEFAULT_DISTANCE); - return _sortedUniq(_sortBy(_flatMap(candidates, candidate => this.keyMap.objectMap[candidate]), - candidate => this.keyMap.priorityMap[candidate])); + // TODO FIXME This is hideous. Clean up when possible. + const val = _sortedUniq(_sortBy(_flatMap(candidates, candidate => { + return this.keyMap.objectMap[candidate[0]].map(value => { + return { + distance: candidate[1], + ...value, + }; + }); + }), + [candidate => candidate.distance, candidate => this.keyMap.priorityMap[candidate]])); + console.log(val); + return val; } } From 0653343319f72f3e4dff3d0f5fc6f11ad29ee991 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Fri, 10 Feb 2017 22:34:52 +0530 Subject: [PATCH 012/491] order User completions by last spoken --- .flowconfig | 6 +++ src/autocomplete/FuzzyMatcher.js | 7 ++- src/autocomplete/QueryMatcher.js | 62 +++++++++++++++++++++++++++ src/autocomplete/UserProvider.js | 35 +++++++++++++-- src/components/structures/RoomView.js | 15 ++----- 5 files changed, 109 insertions(+), 16 deletions(-) create mode 100644 .flowconfig create mode 100644 src/autocomplete/QueryMatcher.js diff --git a/.flowconfig b/.flowconfig new file mode 100644 index 0000000000..81770c6585 --- /dev/null +++ b/.flowconfig @@ -0,0 +1,6 @@ +[include] +src/**/*.js +test/**/*.js + +[ignore] +node_modules/ diff --git a/src/autocomplete/FuzzyMatcher.js b/src/autocomplete/FuzzyMatcher.js index bd19fc53e8..c22e2a1101 100644 --- a/src/autocomplete/FuzzyMatcher.js +++ b/src/autocomplete/FuzzyMatcher.js @@ -14,7 +14,12 @@ class KeyMap { const DEFAULT_RESULT_COUNT = 10; const DEFAULT_DISTANCE = 5; -export default class FuzzyMatcher { +// FIXME Until Fuzzy matching works better, we use prefix matching. + +import PrefixMatcher from './QueryMatcher'; +export default PrefixMatcher; + +class FuzzyMatcher { /** * Given an array of objects and keys, returns a KeyMap * Keys can refer to object properties by name and as in JavaScript (for nested properties) diff --git a/src/autocomplete/QueryMatcher.js b/src/autocomplete/QueryMatcher.js new file mode 100644 index 0000000000..b4c27a7179 --- /dev/null +++ b/src/autocomplete/QueryMatcher.js @@ -0,0 +1,62 @@ +//@flow + +import _at from 'lodash/at'; +import _flatMap from 'lodash/flatMap'; +import _sortBy from 'lodash/sortBy'; +import _sortedUniq from 'lodash/sortedUniq'; +import _keys from 'lodash/keys'; + +class KeyMap { + keys: Array; + objectMap: {[String]: Array}; + priorityMap = new Map(); +} + +export default class QueryMatcher { + /** + * Given an array of objects and keys, returns a KeyMap + * Keys can refer to object properties by name and as in JavaScript (for nested properties) + * + * To use, simply presort objects by required criteria, run through this function and create a QueryMatcher with the + * resulting KeyMap. + * + * TODO: Handle arrays and objects (Fuse did this, RoomProvider uses it) + */ + static valuesToKeyMap(objects: Array, keys: Array): KeyMap { + const keyMap = new KeyMap(); + const map = {}; + + objects.forEach((object, i) => { + const keyValues = _at(object, keys); + for (const keyValue of keyValues) { + if (!map.hasOwnProperty(keyValue)) { + map[keyValue] = []; + } + map[keyValue].push(object); + } + keyMap.priorityMap.set(object, i); + }); + + keyMap.objectMap = map; + keyMap.keys = _keys(map); + return keyMap; + } + + constructor(objects: Array, options: {[Object]: Object} = {}) { + this.options = options; + this.keys = options.keys; + this.setObjects(objects); + } + + setObjects(objects: Array) { + this.keyMap = QueryMatcher.valuesToKeyMap(objects, this.keys); + } + + match(query: String): Array { + query = query.toLowerCase().replace(/[^\w]/g, ''); + const results = _sortedUniq(_sortBy(_flatMap(this.keyMap.keys, (key) => { + return key.toLowerCase().replace(/[^\w]/g, '').indexOf(query) >= 0 ? this.keyMap.objectMap[key] : []; + }), (candidate) => this.keyMap.priorityMap.get(candidate))); + return results; + } +} diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index b65439181c..589dfec9fa 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -1,20 +1,27 @@ +//@flow import React from 'react'; import AutocompleteProvider from './AutocompleteProvider'; import Q from 'q'; import {PillCompletion} from './Components'; import sdk from '../index'; import FuzzyMatcher from './FuzzyMatcher'; +import _pull from 'lodash/pull'; +import _sortBy from 'lodash/sortBy'; +import MatrixClientPeg from '../MatrixClientPeg'; + +import type {Room, RoomMember} from 'matrix-js-sdk'; const USER_REGEX = /@\S*/g; let instance = null; export default class UserProvider extends AutocompleteProvider { + users: Array = []; + constructor() { super(USER_REGEX, { keys: ['name', 'userId'], }); - this.users = []; this.matcher = new FuzzyMatcher([], { keys: ['name', 'userId'], }); @@ -53,8 +60,30 @@ export default class UserProvider extends AutocompleteProvider { return '👥 Users'; } - setUserList(users) { - this.users = users; + setUserListFromRoom(room: Room) { + const events = room.getLiveTimeline().getEvents(); + const lastSpoken = {}; + + for(const event of events) { + lastSpoken[event.getSender()] = event.getTs(); + } + + const currentUserId = MatrixClientPeg.get().credentials.userId; + this.users = room.getJoinedMembers().filter((member) => { + if (member.userId !== currentUserId) return true; + }); + + this.users = _sortBy(this.users, (user) => 1E20 - lastSpoken[user.userId] || 1E20); + + this.matcher.setObjects(this.users); + } + + onUserSpoke(user: RoomMember) { + if(user.userId === MatrixClientPeg.get().credentials.userId) return; + + // Probably unsafe to compare by reference here? + _pull(this.users, user); + this.users.splice(0, 0, user); this.matcher.setObjects(this.users); } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 696d15f84a..936d88c0ee 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -225,7 +225,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().credentials.userId, 'join' ); - this._updateAutoComplete(); + UserProvider.getInstance().setUserListFromRoom(this.state.room); this.tabComplete.loadEntries(this.state.room); } @@ -479,8 +479,7 @@ module.exports = React.createClass({ // and that has probably just changed if (ev.sender) { this.tabComplete.onMemberSpoke(ev.sender); - // nb. we don't need to update the new autocomplete here since - // its results are currently ordered purely by search score. + UserProvider.getInstance().onUserSpoke(ev.sender); } }, @@ -658,7 +657,7 @@ module.exports = React.createClass({ // refresh the tab complete list this.tabComplete.loadEntries(this.state.room); - this._updateAutoComplete(); + UserProvider.getInstance().setUserListFromRoom(this.state.room); // if we are now a member of the room, where we were not before, that // means we have finished joining a room we were previously peeking @@ -1437,14 +1436,6 @@ module.exports = React.createClass({ } }, - _updateAutoComplete: function() { - const myUserId = MatrixClientPeg.get().credentials.userId; - const members = this.state.room.getJoinedMembers().filter(function(member) { - if (member.userId !== myUserId) return true; - }); - UserProvider.getInstance().setUserList(members); - }, - render: function() { var RoomHeader = sdk.getComponent('rooms.RoomHeader'); var MessageComposer = sdk.getComponent('rooms.MessageComposer'); From e65744abdce812b649302f887779c1866f3746c6 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Fri, 10 Feb 2017 23:35:13 +0530 Subject: [PATCH 013/491] fix EmojiProvider for new QueryMatcher --- src/autocomplete/EmojiProvider.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index 52bc47e7b6..e613f41c52 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -7,14 +7,20 @@ import {PillCompletion} from './Components'; import type {SelectionRange, Completion} from './Autocompleter'; const EMOJI_REGEX = /:\w*:?/g; -const EMOJI_SHORTNAMES = Object.keys(emojioneList); +const EMOJI_SHORTNAMES = Object.keys(emojioneList).map(shortname => { + return { + shortname, + }; +}); let instance = null; export default class EmojiProvider extends AutocompleteProvider { constructor() { super(EMOJI_REGEX); - this.matcher = new FuzzyMatcher(EMOJI_SHORTNAMES); + this.matcher = new FuzzyMatcher(EMOJI_SHORTNAMES, { + keys: 'shortname', + }); } async getCompletions(query: string, selection: SelectionRange) { @@ -24,7 +30,7 @@ export default class EmojiProvider extends AutocompleteProvider { let {command, range} = this.getCurrentCommand(query, selection); if (command) { completions = this.matcher.match(command[0]).map(result => { - const shortname = EMOJI_SHORTNAMES[result]; + const {shortname} = result; const unicode = shortnameToUnicode(shortname); return { completion: unicode, From 2d39b2533487a266ddce7bf2a3e8c80681afc146 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Fri, 10 Feb 2017 23:44:04 +0530 Subject: [PATCH 014/491] turn off force complete when editor content changes --- src/components/views/rooms/Autocomplete.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index c06786a80c..bd43b3a85e 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -75,11 +75,11 @@ export default class Autocomplete extends React.Component { } else { selectionOffset++; // selectionOffset is 1-indexed! } - } else { - // If no completions were returned, we should turn off force completion. - forceComplete = false; } + // If no completions were returned, we should turn off force completion. + forceComplete = false; + let hide = this.state.hide; // These are lists of booleans that indicate whether whether the corresponding provider had a matching pattern const oldMatches = this.state.completions.map((completion) => !!completion.command.command), From 32dd89774e78a907215ef3e317faac9e0400206c Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Mon, 20 Feb 2017 19:26:40 +0530 Subject: [PATCH 015/491] add support for autocomplete delay --- src/UserSettingsStore.js | 4 ++-- src/autocomplete/Autocompleter.js | 2 +- src/components/structures/UserSettings.js | 9 +++++++++ src/components/views/rooms/Autocomplete.js | 15 ++++++++++++--- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index 66a872958c..0ee78b4f2e 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -139,7 +139,7 @@ module.exports = { getSyncedSetting: function(type, defaultValue = null) { var settings = this.getSyncedSettings(); - return settings.hasOwnProperty(type) ? settings[type] : null; + return settings.hasOwnProperty(type) ? settings[type] : defaultValue; }, setSyncedSetting: function(type, value) { @@ -156,7 +156,7 @@ module.exports = { getLocalSetting: function(type, defaultValue = null) { var settings = this.getLocalSettings(); - return settings.hasOwnProperty(type) ? settings[type] : null; + return settings.hasOwnProperty(type) ? settings[type] : defaultValue; }, setLocalSetting: function(type, value) { diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index 1bf1b1dc14..2906a5a0f7 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -43,7 +43,7 @@ export async function getCompletions(query: string, selection: SelectionRange, f PROVIDERS.map(provider => { return Q(provider.getCompletions(query, selection, force)) .timeout(PROVIDER_COMPLETION_TIMEOUT); - }) + }), ); return completionsList diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 10ffbca0d3..5ab69e1a15 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -508,6 +508,15 @@ module.exports = React.createClass({ { this._renderUrlPreviewSelector() } { SETTINGS_LABELS.map( this._renderSyncedSetting ) } { THEMES.map( this._renderThemeSelector ) } + + + + + + + +
Autocomplete Delay (ms): UserSettingsStore.setLocalSetting('autocompleteDelay', +e.target.value)} />
); diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index bd43b3a85e..09b13e8076 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -6,6 +6,7 @@ import isEqual from 'lodash/isEqual'; import sdk from '../../../index'; import type {Completion, SelectionRange} from '../../../autocomplete/Autocompleter'; import Q from 'q'; +import UserSettingsStore from '../../../UserSettingsStore'; import {getCompletions} from '../../../autocomplete/Autocompleter'; @@ -77,9 +78,6 @@ export default class Autocomplete extends React.Component { } } - // If no completions were returned, we should turn off force completion. - forceComplete = false; - let hide = this.state.hide; // These are lists of booleans that indicate whether whether the corresponding provider had a matching pattern const oldMatches = this.state.completions.map((completion) => !!completion.command.command), @@ -90,6 +88,17 @@ export default class Autocomplete extends React.Component { hide = false; } + const autocompleteDelay = UserSettingsStore.getSyncedSetting('autocompleteDelay', 200); + + // We had no completions before, but do now, so we should apply our display delay here + if (this.state.completionList.length === 0 && completionList.length > 0 && + !forceComplete && autocompleteDelay > 0) { + await Q.delay(autocompleteDelay); + } + + // Force complete is turned off each time since we can't edit the query in that case + forceComplete = false; + this.setState({ completions, completionList, From 3a07fc1601ae8b6e35cc45632403650b4e8ece17 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Wed, 22 Feb 2017 02:51:57 +0530 Subject: [PATCH 016/491] fix code-block for markdown mode --- src/components/views/rooms/MessageComposerInput.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 7908d7f375..af5627273c 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -485,7 +485,7 @@ export default class MessageComposerInput extends React.Component { 'italic': (text) => `*${text}*`, 'underline': (text) => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug* 'strike': (text) => `~~${text}~~`, - 'code': (text) => `\`${text}\``, + 'code-block': (text) => `\`\`\`\n${text}\n\`\`\``, 'blockquote': (text) => text.split('\n').map((line) => `> ${line}\n`).join(''), 'unordered-list-item': (text) => text.split('\n').map((line) => `- ${line}\n`).join(''), 'ordered-list-item': (text) => text.split('\n').map((line, i) => `${i + 1}. ${line}\n`).join(''), From feac919c0a527f440d23bfaf25099659eb31e675 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Wed, 22 Feb 2017 03:10:15 +0530 Subject: [PATCH 017/491] fix rendering of UNDERLINE inline style in RTE --- src/RichText.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/RichText.js b/src/RichText.js index e662c22d6a..219af472e8 100644 --- a/src/RichText.js +++ b/src/RichText.js @@ -30,7 +30,15 @@ const USERNAME_REGEX = /@\S+:\S+/g; const ROOM_REGEX = /#\S+:\S+/g; const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp, 'g'); -export const contentStateToHTML = stateToHTML; +export const contentStateToHTML = (contentState: ContentState) => { + return stateToHTML(contentState, { + inlineStyles: { + UNDERLINE: { + element: 'u' + } + } + }); +}; export function HTMLtoContentState(html: string): ContentState { return ContentState.createFromBlockArray(convertFromHTML(html)); From 9946cadc2d3fe62c71959ceb89ed915961cdebb9 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Tue, 7 Mar 2017 04:08:06 +0530 Subject: [PATCH 018/491] autocomplete: fix RoomProvider regression --- src/autocomplete/RoomProvider.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index 8659b8501f..726d28db88 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -14,7 +14,7 @@ export default class RoomProvider extends AutocompleteProvider { constructor() { super(ROOM_REGEX); this.matcher = new FuzzyMatcher([], { - keys: ['name', 'aliases'], + keys: ['name', 'roomId', 'aliases'], }); } @@ -26,7 +26,7 @@ export default class RoomProvider extends AutocompleteProvider { const {command, range} = this.getCurrentCommand(query, selection, force); if (command) { // the only reason we need to do this is because Fuse only matches on properties - this.matcher.setObjects(client.getRooms().filter(room => !!room).map(room => { + this.matcher.setObjects(client.getRooms().filter(room => !!room && !!getDisplayAliasForRoom(room)).map(room => { return { room: room, name: room.name, From f5b52fb48844c22d61dff3475b3519bd5dd4acd6 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Tue, 7 Mar 2017 04:15:28 +0530 Subject: [PATCH 019/491] rte: change list behaviour in markdown mode --- src/components/views/rooms/MessageComposerInput.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index af5627273c..5d9496e78d 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -487,8 +487,8 @@ export default class MessageComposerInput extends React.Component { 'strike': (text) => `~~${text}~~`, 'code-block': (text) => `\`\`\`\n${text}\n\`\`\``, 'blockquote': (text) => text.split('\n').map((line) => `> ${line}\n`).join(''), - 'unordered-list-item': (text) => text.split('\n').map((line) => `- ${line}\n`).join(''), - 'ordered-list-item': (text) => text.split('\n').map((line, i) => `${i + 1}. ${line}\n`).join(''), + 'unordered-list-item': (text) => text.split('\n').map((line) => `\n- ${line}`).join(''), + 'ordered-list-item': (text) => text.split('\n').map((line, i) => `\n${i + 1}. ${line}`).join(''), }[command]; if (modifyFn) { From 79f481f81e8b9d1d11535f91f5b8da5d19006d7e Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Tue, 7 Mar 2017 04:39:38 +0530 Subject: [PATCH 020/491] rte: special return handling for some block types --- src/components/views/rooms/MessageComposerInput.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 5d9496e78d..e3063babb1 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -513,9 +513,16 @@ export default class MessageComposerInput extends React.Component { }; handleReturn = (ev) => { - if (ev.shiftKey) { - this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState)); - return true; + const currentBlockType = RichUtils.getCurrentBlockType(this.state.editorState); + // If we're in any of these three types of blocks, shift enter should insert soft newlines + // And just enter should end the block + if(['blockquote', 'unordered-list-item', 'ordered-list-item'].includes(currentBlockType)) { + if(ev.shiftKey) { + this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState)); + return true; + } + + return false; } const contentState = this.state.editorState.getCurrentContent(); From b977b559de6e369adbb55fd3de5a929c01c394c6 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Tue, 7 Mar 2017 04:46:55 +0530 Subject: [PATCH 021/491] autocomplete: add missing commands to CommandProvider --- src/autocomplete/CommandProvider.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index 8f98bf1aa5..a30af5674d 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -9,11 +9,21 @@ const COMMANDS = [ args: '', description: 'Displays action', }, + { + command: '/part', + args: '[#alias:domain]', + description: 'Leave room', + }, { command: '/ban', args: ' [reason]', description: 'Bans user with given id', }, + { + command: '/unban', + args: '', + description: 'Unbans user with given id', + }, { command: '/deop', args: '', @@ -43,6 +53,11 @@ const COMMANDS = [ command: '/ddg', args: '', description: 'Searches DuckDuckGo for results', + }, + { + command: '/op', + args: ' []', + description: 'Define the power level of a user', } ]; From 6004f6d6107dbdafcd70295715040cef6c4a3109 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Fri, 10 Mar 2017 20:34:31 +0530 Subject: [PATCH 022/491] rte: fix history --- .eslintrc.js | 2 +- src/ComposerHistoryManager.js | 63 +++++++ src/RichText.js | 10 ++ .../views/rooms/MessageComposerInput.js | 155 ++++-------------- 4 files changed, 108 insertions(+), 122 deletions(-) create mode 100644 src/ComposerHistoryManager.js diff --git a/.eslintrc.js b/.eslintrc.js index 6cd0e1015e..74790a2964 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -64,7 +64,7 @@ module.exports = { // to JSX. ignorePattern: '^\\s*<', ignoreComments: true, - code: 90, + code: 120, }], "valid-jsdoc": ["warn"], "new-cap": ["warn"], diff --git a/src/ComposerHistoryManager.js b/src/ComposerHistoryManager.js new file mode 100644 index 0000000000..5f9cf04e6f --- /dev/null +++ b/src/ComposerHistoryManager.js @@ -0,0 +1,63 @@ +//@flow + +import {ContentState} from 'draft-js'; +import * as RichText from './RichText'; +import Markdown from './Markdown'; +import _flow from 'lodash/flow'; +import _clamp from 'lodash/clamp'; + +type MessageFormat = 'html' | 'markdown'; + +class HistoryItem { + message: string = ''; + format: MessageFormat = 'html'; + + constructor(message: string, format: MessageFormat) { + this.message = message; + this.format = format; + } + + toContentState(format: MessageFormat): ContentState { + let {message} = this; + if (format === 'markdown') { + if (this.format === 'html') { + message = _flow([RichText.HTMLtoContentState, RichText.stateToMarkdown])(message); + } + return ContentState.createFromText(message); + } else { + if (this.format === 'markdown') { + message = new Markdown(message).toHTML(); + } + return RichText.HTMLtoContentState(message); + } + } +} + +export default class ComposerHistoryManager { + history: Array = []; + prefix: string; + lastIndex: number = 0; + currentIndex: number = -1; + + constructor(roomId: string, prefix: string = 'mx_composer_history_') { + this.prefix = prefix + roomId; + + // TODO: Performance issues? + for(; sessionStorage.getItem(`${this.prefix}[${this.lastIndex}]`); this.lastIndex++, this.currentIndex++) { + history.push(JSON.parse(sessionStorage.getItem(`${this.prefix}[${this.lastIndex}]`))); + } + } + + addItem(message: string, format: MessageFormat) { + const item = new HistoryItem(message, format); + this.history.push(item); + this.currentIndex = this.lastIndex; + sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item)); + } + + getItem(offset: number, format: MessageFormat): ?ContentState { + this.currentIndex = _clamp(this.currentIndex + offset, 0, this.lastIndex - 1); + const item = this.history[this.currentIndex]; + return item ? item.toContentState(format) : null; + } +} diff --git a/src/RichText.js b/src/RichText.js index 219af472e8..6edde23129 100644 --- a/src/RichText.js +++ b/src/RichText.js @@ -16,6 +16,7 @@ import * as sdk from './index'; import * as emojione from 'emojione'; import {stateToHTML} from 'draft-js-export-html'; import {SelectionRange} from "./autocomplete/Autocompleter"; +import {stateToMarkdown as __stateToMarkdown} from 'draft-js-export-markdown'; const MARKDOWN_REGEX = { LINK: /(?:\[([^\]]+)\]\(([^\)]+)\))|\<(\w+:\/\/[^\>]+)\>/g, @@ -30,6 +31,15 @@ const USERNAME_REGEX = /@\S+:\S+/g; const ROOM_REGEX = /#\S+:\S+/g; const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp, 'g'); +const ZWS_CODE = 8203; +const ZWS = String.fromCharCode(ZWS_CODE); // zero width space +export function stateToMarkdown(state) { + return __stateToMarkdown(state) + .replace( + ZWS, // draft-js-export-markdown adds these + ''); // this is *not* a zero width space, trust me :) +} + export const contentStateToHTML = (contentState: ContentState) => { return stateToHTML(contentState, { inlineStyles: { diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index e3063babb1..33f184c446 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -20,7 +20,6 @@ import {Editor, EditorState, RichUtils, CompositeDecorator, convertFromRaw, convertToRaw, Modifier, EditorChangeType, getDefaultKeyBinding, KeyBindingUtil, ContentState, ContentBlock, SelectionState} from 'draft-js'; -import {stateToMarkdown as __stateToMarkdown} from 'draft-js-export-markdown'; import classNames from 'classnames'; import escape from 'lodash/escape'; import Q from 'q'; @@ -40,21 +39,13 @@ import * as HtmlUtils from '../../../HtmlUtils'; import Autocomplete from './Autocomplete'; import {Completion} from "../../../autocomplete/Autocompleter"; import Markdown from '../../../Markdown'; +import ComposerHistoryManager from '../../../ComposerHistoryManager'; import {onSendMessageFailed} from './MessageComposerInputOld'; const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000; const KEY_M = 77; -const ZWS_CODE = 8203; -const ZWS = String.fromCharCode(ZWS_CODE); // zero width space -function stateToMarkdown(state) { - return __stateToMarkdown(state) - .replace( - ZWS, // draft-js-export-markdown adds these - ''); // this is *not* a zero width space, trust me :) -} - /* * The textInput part of the MessageComposer */ @@ -101,6 +92,7 @@ export default class MessageComposerInput extends React.Component { client: MatrixClient; autocomplete: Autocomplete; + historyManager: ComposerHistoryManager; constructor(props, context) { super(props, context); @@ -145,110 +137,13 @@ export default class MessageComposerInput extends React.Component { return EditorState.moveFocusToEnd(editorState); } - componentWillMount() { - const component = this; - this.sentHistory = { - // The list of typed messages. Index 0 is more recent - data: [], - // The position in data currently displayed - position: -1, - // The room the history is for. - roomId: null, - // The original text before they hit UP - originalText: null, - // The textarea element to set text to. - element: null, - - init: function(element, roomId) { - this.roomId = roomId; - this.element = element; - this.position = -1; - const storedData = window.sessionStorage.getItem( - "mx_messagecomposer_history_" + roomId, - ); - if (storedData) { - this.data = JSON.parse(storedData); - } - if (this.roomId) { - this.setLastTextEntry(); - } - }, - - push: function(text) { - // store a message in the sent history - this.data.unshift(text); - window.sessionStorage.setItem( - "mx_messagecomposer_history_" + this.roomId, - JSON.stringify(this.data), - ); - // reset history position - this.position = -1; - this.originalText = null; - }, - - // move in the history. Returns true if we managed to move. - next: function(offset) { - if (this.position === -1) { - // user is going into the history, save the current line. - this.originalText = this.element.value; - } else { - // user may have modified this line in the history; remember it. - this.data[this.position] = this.element.value; - } - - if (offset > 0 && this.position === (this.data.length - 1)) { - // we've run out of history - return false; - } - - // retrieve the next item (bounded). - let newPosition = this.position + offset; - newPosition = Math.max(-1, newPosition); - newPosition = Math.min(newPosition, this.data.length - 1); - this.position = newPosition; - - if (this.position !== -1) { - // show the message - this.element.value = this.data[this.position]; - } else if (this.originalText !== undefined) { - // restore the original text the user was typing. - this.element.value = this.originalText; - } - - return true; - }, - - saveLastTextEntry: function() { - // save the currently entered text in order to restore it later. - // NB: This isn't 'originalText' because we want to restore - // sent history items too! - const contentJSON = JSON.stringify(convertToRaw(component.state.editorState.getCurrentContent())); - window.sessionStorage.setItem("mx_messagecomposer_input_" + this.roomId, contentJSON); - }, - - setLastTextEntry: function() { - const contentJSON = window.sessionStorage.getItem("mx_messagecomposer_input_" + this.roomId); - if (contentJSON) { - const content = convertFromRaw(JSON.parse(contentJSON)); - component.setState({ - editorState: component.createEditorState(component.state.isRichtextEnabled, content), - }); - } - }, - }; - } - componentDidMount() { this.dispatcherRef = dis.register(this.onAction); - this.sentHistory.init( - this.refs.editor, - this.props.room.roomId, - ); + this.historyManager = new ComposerHistoryManager(this.props.room.roomId); } componentWillUnmount() { dis.unregister(this.dispatcherRef); - this.sentHistory.saveLastTextEntry(); } componentWillUpdate(nextProps, nextState) { @@ -290,7 +185,7 @@ export default class MessageComposerInput extends React.Component { if (formatted_body) { let content = RichText.HTMLtoContentState(`
${formatted_body}
`); if (!this.state.isRichtextEnabled) { - content = ContentState.createFromText(stateToMarkdown(content)); + content = ContentState.createFromText(RichText.stateToMarkdown(content)); } const blockMap = content.getBlockMap(); @@ -414,8 +309,6 @@ export default class MessageComposerInput extends React.Component { } } - console.log(state); - super.setState(state, () => { if (callback != null) { callback(); @@ -434,12 +327,14 @@ export default class MessageComposerInput extends React.Component { } enableRichtext(enabled: boolean) { + if (enabled === this.state.isRichtextEnabled) return; + let contentState = null; if (enabled) { const md = new Markdown(this.state.editorState.getCurrentContent().getPlainText()); contentState = RichText.HTMLtoContentState(md.toHTML()); } else { - let markdown = stateToMarkdown(this.state.editorState.getCurrentContent()); + let markdown = RichText.stateToMarkdown(this.state.editorState.getCurrentContent()); if (markdown[markdown.length - 1] === '\n') { markdown = markdown.substring(0, markdown.length - 1); // stateToMarkdown tacks on an extra newline (?!?) } @@ -513,15 +408,15 @@ export default class MessageComposerInput extends React.Component { }; handleReturn = (ev) => { + if(ev.shiftKey) { + this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState)); + return true; + } + const currentBlockType = RichUtils.getCurrentBlockType(this.state.editorState); // If we're in any of these three types of blocks, shift enter should insert soft newlines // And just enter should end the block if(['blockquote', 'unordered-list-item', 'ordered-list-item'].includes(currentBlockType)) { - if(ev.shiftKey) { - this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState)); - return true; - } - return false; } @@ -586,8 +481,10 @@ export default class MessageComposerInput extends React.Component { sendTextFn = this.client.sendEmoteMessage; } - // XXX: We don't actually seem to use this history? - this.sentHistory.push(contentHTML || contentText); + this.historyManager.addItem( + this.state.isRichtextEnabled ? contentHTML : contentState.getPlainText(), + this.state.isRichtextEnabled ? 'html' : 'markdown'); + let sendMessagePromise; if (contentHTML) { sendMessagePromise = sendHtmlFn.call( @@ -614,14 +511,30 @@ export default class MessageComposerInput extends React.Component { onUpArrow = async (e) => { const completion = this.autocomplete.onUpArrow(); - if (completion != null) { - e.preventDefault(); + if (completion == null) { + const newContent = this.historyManager.getItem(-1, this.state.isRichtextEnabled ? 'html' : 'markdown'); + if (!newContent) return false; + const editorState = EditorState.push(this.state.editorState, + newContent, + 'insert-characters'); + this.setState({editorState}); + return true; } + e.preventDefault(); return await this.setDisplayedCompletion(completion); }; onDownArrow = async (e) => { const completion = this.autocomplete.onDownArrow(); + if (completion == null) { + const newContent = this.historyManager.getItem(+1, this.state.isRichtextEnabled ? 'html' : 'markdown'); + if (!newContent) return false; + const editorState = EditorState.push(this.state.editorState, + newContent, + 'insert-characters'); + this.setState({editorState}); + return true; + } e.preventDefault(); return await this.setDisplayedCompletion(completion); }; From 8dc7f8efe29c2bd796f17c21c41c89a4d6fd858f Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Fri, 10 Mar 2017 21:10:27 +0530 Subject: [PATCH 023/491] rte: remove logging and fix new history --- src/ComposerHistoryManager.js | 10 ++++++++-- src/components/views/rooms/Autocomplete.js | 1 - src/components/views/rooms/MessageComposerInput.js | 3 --- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/ComposerHistoryManager.js b/src/ComposerHistoryManager.js index 5f9cf04e6f..face75ea8a 100644 --- a/src/ComposerHistoryManager.js +++ b/src/ComposerHistoryManager.js @@ -44,14 +44,20 @@ export default class ComposerHistoryManager { // TODO: Performance issues? for(; sessionStorage.getItem(`${this.prefix}[${this.lastIndex}]`); this.lastIndex++, this.currentIndex++) { - history.push(JSON.parse(sessionStorage.getItem(`${this.prefix}[${this.lastIndex}]`))); + this.history.push( + Object.assign( + new HistoryItem(), + JSON.parse(sessionStorage.getItem(`${this.prefix}[${this.lastIndex}]`)), + ), + ); } + this.currentIndex--; } addItem(message: string, format: MessageFormat) { const item = new HistoryItem(message, format); this.history.push(item); - this.currentIndex = this.lastIndex; + this.currentIndex = this.lastIndex + 1; sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item)); } diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index 09b13e8076..5329cde8f2 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -197,7 +197,6 @@ export default class Autocomplete extends React.Component { setState(state, func) { super.setState(state, func); - console.log(state); } render() { diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 33f184c446..2a0a62ebf7 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -320,7 +320,6 @@ export default class MessageComposerInput extends React.Component { const selection = RichText.selectionStateToTextOffsets( this.state.editorState.getSelection(), this.state.editorState.getCurrentContent().getBlocksAsArray()); - console.log(textContent); this.props.onContentChanged(textContent, selection); } }); @@ -541,7 +540,6 @@ export default class MessageComposerInput extends React.Component { // tab and shift-tab are mapped to down and up arrow respectively onTab = async (e) => { - console.log('onTab'); e.preventDefault(); // we *never* want tab's default to happen, but we do want up/down sometimes if (this.autocomplete.state.completionList.length === 0) { await this.autocomplete.forceComplete(); @@ -567,7 +565,6 @@ export default class MessageComposerInput extends React.Component { if (displayedCompletion == null) { if (this.state.originalEditorState) { - console.log('setting editorState to originalEditorState'); let editorState = this.state.originalEditorState; // This is a workaround from https://github.com/facebook/draft-js/issues/458 // Due to the way we swap editorStates, Draft does not rerender at times From f5a23c14df83c8c8bc91c0daa7ded05c73fbb71c Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 8 May 2017 17:32:26 +0100 Subject: [PATCH 024/491] Remove redundant bind --- src/components/views/rooms/MessageComposerInput.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 2d16b202d1..7d52d87dbf 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -101,7 +101,6 @@ export default class MessageComposerInput extends React.Component { this.handleKeyCommand = this.handleKeyCommand.bind(this); this.handlePastedFiles = this.handlePastedFiles.bind(this); this.onEditorContentChanged = this.onEditorContentChanged.bind(this); - this.setEditorState = this.setEditorState.bind(this); this.onUpArrow = this.onUpArrow.bind(this); this.onDownArrow = this.onDownArrow.bind(this); this.onTab = this.onTab.bind(this); From e22712514eec3c8037a0a77449b1494bccc160f3 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Wed, 17 May 2017 10:58:59 +0100 Subject: [PATCH 025/491] Add show / hide apps button --- src/components/views/rooms/MessageComposer.js | 41 +++++++++++++++++-- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 8a3b128908..90b738f343 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -32,6 +32,8 @@ export default class MessageComposer extends React.Component { this.onCallClick = this.onCallClick.bind(this); this.onHangupClick = this.onHangupClick.bind(this); this.onUploadClick = this.onUploadClick.bind(this); + this.onShowAppsClick = this.onShowAppsClick.bind(this); + this.onHideAppsClick = this.onHideAppsClick.bind(this); this.onUploadFileSelected = this.onUploadFileSelected.bind(this); this.onVoiceCallClick = this.onVoiceCallClick.bind(this); this.onInputContentChanged = this.onInputContentChanged.bind(this); @@ -145,6 +147,7 @@ export default class MessageComposer extends React.Component { } onCallClick(ev) { + console.warn("Call but clicked!"); dis.dispatch({ action: 'place_call', type: ev.shiftKey ? "screensharing" : "video", @@ -160,6 +163,22 @@ export default class MessageComposer extends React.Component { }); } + onShowAppsClick(ev) { + console.warn("Showing apps"); + dis.dispatch({ + action: 'showApps', + room_id: this.props.room.roomId, + }); + } + + onHideAppsClick(ev) { + dis.dispatch({ + action: 'hideApps', + room_id: this.props.room.roomId, + }); + console.warn("Hiding apps"); + } + onInputContentChanged(content: string, selection: {start: number, end: number}) { this.setState({ autocompleteQuery: content, @@ -241,14 +260,13 @@ export default class MessageComposer extends React.Component { alt={e2eTitle} title={e2eTitle} /> ); - var callButton, videoCallButton, hangupButton; + var callButton, videoCallButton, hangupButton, showAppsButton, hideAppsButton; if (this.props.callState && this.props.callState !== 'ended') { hangupButton =
Hangup
; - } - else { + } else { callButton =
@@ -259,6 +277,19 @@ export default class MessageComposer extends React.Component {
; } + // Apps + if (this.props.showAppsState && this.props.showAppsState == 'visible') { + hideAppsButton = +
+ +
; + } else { + showAppsButton = +
+ +
; + } + var canSendMessages = this.props.room.currentState.maySendMessage( MatrixClientPeg.get().credentials.userId); @@ -308,7 +339,9 @@ export default class MessageComposer extends React.Component { uploadButton, hangupButton, callButton, - videoCallButton + videoCallButton, + showAppsButton, + hideAppsButton, ); } else { controls.push( From 9dd0b9bdd1eb648207fa78fadd2f5d84eec0da68 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Wed, 17 May 2017 11:31:01 +0100 Subject: [PATCH 026/491] Fix lint errrors / warnings --- src/components/views/rooms/MessageComposer.js | 101 +++++++++--------- 1 file changed, 53 insertions(+), 48 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 90b738f343..7aab5f1d50 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -13,14 +13,14 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -var React = require('react'); +const React = require('react'); -var CallHandler = require('../../../CallHandler'); -var MatrixClientPeg = require('../../../MatrixClientPeg'); -var Modal = require('../../../Modal'); -var sdk = require('../../../index'); -var dis = require('../../../dispatcher'); -import Autocomplete from './Autocomplete'; +const CallHandler = require('../../../CallHandler'); +const MatrixClientPeg = require('../../../MatrixClientPeg'); +const Modal = require('../../../Modal'); +const sdk = require('../../../index'); +const dis = require('../../../dispatcher'); +// import Autocomplete from './Autocomplete'; import classNames from 'classnames'; import UserSettingsStore from '../../../UserSettingsStore'; @@ -57,7 +57,6 @@ export default class MessageComposer extends React.Component { }, showFormatting: UserSettingsStore.getSyncedSetting('MessageComposer.showFormatting', false), }; - } componentDidMount() { @@ -82,7 +81,7 @@ export default class MessageComposer extends React.Component { onUploadClick(ev) { if (MatrixClientPeg.get().isGuest()) { - let NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); + const NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); Modal.createDialog(NeedToRegisterDialog, { title: "Please Register", description: "Guest users can't upload files. Please register to upload.", @@ -94,13 +93,14 @@ export default class MessageComposer extends React.Component { } onUploadFileSelected(files, isPasted) { - if (!isPasted) + if (!isPasted) { files = files.target.files; + } - let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - let TintableSvg = sdk.getComponent("elements.TintableSvg"); + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + const TintableSvg = sdk.getComponent("elements.TintableSvg"); - let fileList = []; + const fileList = []; for (let i=0; i {files[i].name || 'Attachment'} @@ -121,7 +121,7 @@ export default class MessageComposer extends React.Component { if(shouldUpload) { // MessageComposer shouldn't have to rely on its parent passing in a callback to upload a file if (files) { - for(var i=0; i - + , ); let e2eImg, e2eTitle, e2eClass; @@ -258,9 +260,9 @@ export default class MessageComposer extends React.Component { controls.push( {e2eTitle} + />, ); - var callButton, videoCallButton, hangupButton, showAppsButton, hideAppsButton; + let callButton, videoCallButton, hangupButton, showAppsButton, hideAppsButton; if (this.props.callState && this.props.callState !== 'ended') { hangupButton =
@@ -290,14 +292,14 @@ export default class MessageComposer extends React.Component {
; } - var canSendMessages = this.props.room.currentState.maySendMessage( + const canSendMessages = this.props.room.currentState.maySendMessage( MatrixClientPeg.get().credentials.userId); if (canSendMessages) { // This also currently includes the call buttons. Really we should // check separately for whether we can call, but this is slightly // complex because of conference calls. - var uploadButton = ( + const uploadButton = (
@@ -323,7 +325,7 @@ export default class MessageComposer extends React.Component { controls.push( this.messageComposerInput = c} + ref={(c) => this.messageComposerInput = c} key="controls_input" onResize={this.props.onResize} room={this.props.room} @@ -347,25 +349,25 @@ export default class MessageComposer extends React.Component { controls.push(
You do not have permission to post to this room -
+
, ); } - let autoComplete; - if (UserSettingsStore.isFeatureEnabled('rich_text_editor')) { - autoComplete =
- -
; - } + // let autoComplete; + // if (UserSettingsStore.isFeatureEnabled('rich_text_editor')) { + // autoComplete =
+ // + //
; + // } const {style, blockType} = this.state.inputState; const formatButtons = ["bold", "italic", "strike", "underline", "code", "quote", "bullet", "numbullet"].map( - name => { + (name) => { const active = style.includes(name) || blockType === name; const suffix = active ? '-o-n' : ''; const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name); @@ -428,5 +430,8 @@ MessageComposer.propTypes = { uploadFile: React.PropTypes.func.isRequired, // opacity for dynamic UI fading effects - opacity: React.PropTypes.number + opacity: React.PropTypes.number, + + // string representing the current room app drawer state + showAppsState: React.PropTypes.string, }; From 95988bd5ecbabc58c3ed80f1d35b17255ec0a9e6 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Wed, 17 May 2017 12:35:25 +0100 Subject: [PATCH 027/491] Dispatch show hide app drawer events --- src/components/views/rooms/MessageComposer.js | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 7aab5f1d50..22b84b73cb 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -164,21 +164,19 @@ export default class MessageComposer extends React.Component { } onShowAppsClick(ev) { - alert("Showing apps"); console.warn("Showing apps"); - // dis.dispatch({ - // action: 'showApps', - // room_id: this.props.room.roomId, - // }); + dis.dispatch({ + action: 'showApps', + room_id: this.props.room.roomId, + }); } onHideAppsClick(ev) { - alert("Hiding apps"); console.warn("Hiding apps"); - // dis.dispatch({ - // action: 'hideApps', - // room_id: this.props.room.roomId, - // }); + dis.dispatch({ + action: 'hideApps', + room_id: this.props.room.roomId, + }); } onInputContentChanged(content: string, selection: {start: number, end: number}) { @@ -282,12 +280,12 @@ export default class MessageComposer extends React.Component { // Apps if (this.props.showAppsState && this.props.showAppsState == 'visible') { hideAppsButton = -
+
; } else { showAppsButton = -
+
; } From 7e1de2ac350e7f2ea5eab9eb11e1f7caf2db63e2 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Wed, 17 May 2017 21:15:57 +0100 Subject: [PATCH 028/491] Show/hide apps panel and misc formatting and lint fixes --- src/components/structures/RoomView.js | 25 ++++++++++---- src/components/views/rooms/AuxPanel.js | 34 +++++++++++-------- src/components/views/rooms/MessageComposer.js | 14 ++++---- 3 files changed, 45 insertions(+), 28 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index b22d867acf..49aa3a0af5 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -43,7 +43,7 @@ import KeyCode from '../../KeyCode'; import UserProvider from '../../autocomplete/UserProvider'; -var DEBUG = false; +const DEBUG = false; if (DEBUG) { // using bind means that we get to keep useful line numbers in the console @@ -133,6 +133,7 @@ module.exports = React.createClass({ callState: null, guestsCanJoin: false, canPeek: false, + showApps: false, // error object, as from the matrix client/server API // If we failed to load information about the room, @@ -168,7 +169,7 @@ module.exports = React.createClass({ onClickCompletes: true, onStateChange: (isCompleting) => { this.forceUpdate(); - } + }, }); if (this.props.roomAddress[0] == '#') { @@ -434,9 +435,14 @@ module.exports = React.createClass({ this._updateConfCallNotification(); this.setState({ - callState: callState + callState: callState, }); + break; + case 'appsDrawer': + this.setState({ + showApps: payload.show ? true : false, + }); break; } }, @@ -1638,7 +1644,8 @@ module.exports = React.createClass({ draggingFile={this.state.draggingFile} displayConfCallNotification={this.state.displayConfCallNotification} maxHeight={this.state.auxPanelMaxHeight} - onResize={this.onChildResize} > + onResize={this.onChildResize} + showApps={this.state.showApps} > { aux } ); @@ -1651,8 +1658,14 @@ module.exports = React.createClass({ if (canSpeak) { messageComposer = ; + room={this.state.room} + onResize={this.onChildResize} + uploadFile={this.uploadFile} + callState={this.state.callState} + tabComplete={this.tabComplete} + opacity={ this.props.opacity } + showApps={ this.state.showApps } + />; } // TODO: Why aren't we storing the term/scope/count in this format diff --git a/src/components/views/rooms/AuxPanel.js b/src/components/views/rooms/AuxPanel.js index 365cc18f99..31739a890f 100644 --- a/src/components/views/rooms/AuxPanel.js +++ b/src/components/views/rooms/AuxPanel.js @@ -14,11 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -var React = require('react'); -var MatrixClientPeg = require("../../../MatrixClientPeg"); -var sdk = require('../../../index'); -var dis = require("../../../dispatcher"); -var ObjectUtils = require('../../../ObjectUtils'); +const React = require('react'); +const MatrixClientPeg = require("../../../MatrixClientPeg"); +const sdk = require('../../../index'); +const dis = require("../../../dispatcher"); +const ObjectUtils = require('../../../ObjectUtils'); +const AppsDrawer = require('./AppsDrawer'); module.exports = React.createClass({ displayName: 'AuxPanel', @@ -68,10 +69,10 @@ module.exports = React.createClass({ }, render: function() { - var CallView = sdk.getComponent("voip.CallView"); - var TintableSvg = sdk.getComponent("elements.TintableSvg"); + const CallView = sdk.getComponent("voip.CallView"); + const TintableSvg = sdk.getComponent("elements.TintableSvg"); - var fileDropTarget = null; + let fileDropTarget = null; if (this.props.draggingFile) { fileDropTarget = (
@@ -85,19 +86,18 @@ module.exports = React.createClass({ ); } - var conferenceCallNotification = null; + let conferenceCallNotification = null; if (this.props.displayConfCallNotification) { - var supportedText, joinText; + let supportedText; + let joinText; if (!MatrixClientPeg.get().supportsVoip()) { supportedText = " (unsupported)"; - } - else { + } else { joinText = ( Join as { this.onConferenceNotificationClick(event, 'voice');}} href="#">voice or { this.onConferenceNotificationClick(event, 'video'); }} href="#">video. ); - } conferenceCallNotification = (
@@ -106,7 +106,7 @@ module.exports = React.createClass({ ); } - var callView = ( + const callView = ( ); + let appsDrawer = null; + if(this.props.showApps) { + appsDrawer = ; + } + return (
+ { appsDrawer } { fileDropTarget } { callView } { conferenceCallNotification } diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 22b84b73cb..a90de8cb5c 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -164,18 +164,16 @@ export default class MessageComposer extends React.Component { } onShowAppsClick(ev) { - console.warn("Showing apps"); dis.dispatch({ - action: 'showApps', - room_id: this.props.room.roomId, + action: 'appsDrawer', + show: true, }); } onHideAppsClick(ev) { - console.warn("Hiding apps"); dis.dispatch({ - action: 'hideApps', - room_id: this.props.room.roomId, + action: 'appsDrawer', + show: false, }); } @@ -278,7 +276,7 @@ export default class MessageComposer extends React.Component { } // Apps - if (this.props.showAppsState && this.props.showAppsState == 'visible') { + if (this.props.showApps) { hideAppsButton =
@@ -431,5 +429,5 @@ MessageComposer.propTypes = { opacity: React.PropTypes.number, // string representing the current room app drawer state - showAppsState: React.PropTypes.string, + showApps: React.PropTypes.bool, }; From 0e5657333fe17df739f5736efd834ea0e7f926d5 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Wed, 17 May 2017 23:21:02 +0100 Subject: [PATCH 029/491] Add app drawer and app dialog --- src/components/views/dialogs/AddAppDialog.js | 74 ++++++++++++++++++++ src/components/views/rooms/AppsDrawer.js | 52 ++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 src/components/views/dialogs/AddAppDialog.js create mode 100644 src/components/views/rooms/AppsDrawer.js diff --git a/src/components/views/dialogs/AddAppDialog.js b/src/components/views/dialogs/AddAppDialog.js new file mode 100644 index 0000000000..4a3c1dfb5b --- /dev/null +++ b/src/components/views/dialogs/AddAppDialog.js @@ -0,0 +1,74 @@ +/* +Copyright 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import sdk from '../../../index'; + +/** + * Prompt the user for address of iframe widget + * + * On success, `onFinished(true, newAppWidget)` is called. + */ +export default React.createClass({ + displayName: 'AddAppDialog', + propTypes: { + onFinished: React.PropTypes.func.isRequired, + }, + + getInitialState: function() { + }, + + componentDidMount: function() { + this.refs.input_value.select(); + }, + + onValueChange: function(ev) { + this.setState({ + value: ev.target.value, + }); + }, + + onFormSubmit: function(ev) { + ev.preventDefault(); + this.props.onFinished(true, this.state.value); + return false; + }, + + render: function() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + return ( + +
+ Please enter the URL of the app / widget to add. +
+
+
+ +
+
+ +
+
+
+ ); + }, +}); diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js new file mode 100644 index 0000000000..82abb07e5c --- /dev/null +++ b/src/components/views/rooms/AppsDrawer.js @@ -0,0 +1,52 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +'use strict'; + +const React = require('react'); +const AddAppDialog = require('../dialogs/AddAppDialog'); + +module.exports = React.createClass({ + displayName: 'AppsDrawer', + + propTypes: { + }, + + componentDidMount: function() { + }, + + getInitialState: function() { + // this.onClickAddWidget = this.onClickAddWidget.bind(this); + return { + addAppWidget: false, + }; + }, + + onClickAddWidget: function() { + this.setState({ + addAppWidget: true, + }); + }, + + render: function() { + return ( +
+
[+] Add a widget
+ {this.state.addAppWidget && AddAppDialog} +
+ ); + }, +}); From e8837d28ef7b8a37756b16e5c411a5ff7c3becf6 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Mon, 22 May 2017 12:34:27 +0100 Subject: [PATCH 030/491] App tile and app dialog styling --- src/components/views/dialogs/AddAppDialog.js | 9 +- src/components/views/elements/AppTile.js | 53 +++++++++++ src/components/views/elements/MemberTile.js | 97 ++++++++++++++++++++ src/components/views/rooms/AppsDrawer.js | 39 ++++++-- 4 files changed, 188 insertions(+), 10 deletions(-) create mode 100644 src/components/views/elements/AppTile.js create mode 100644 src/components/views/elements/MemberTile.js diff --git a/src/components/views/dialogs/AddAppDialog.js b/src/components/views/dialogs/AddAppDialog.js index 4a3c1dfb5b..b72a3809b4 100644 --- a/src/components/views/dialogs/AddAppDialog.js +++ b/src/components/views/dialogs/AddAppDialog.js @@ -29,6 +29,9 @@ export default React.createClass({ }, getInitialState: function() { + return { + value: "", + }; }, componentDidMount: function() { @@ -36,9 +39,7 @@ export default React.createClass({ }, onValueChange: function(ev) { - this.setState({ - value: ev.target.value, - }); + this.setState({ value: ev.target.value}); }, onFormSubmit: function(ev) { @@ -61,7 +62,7 @@ export default React.createClass({
diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js new file mode 100644 index 0000000000..66c32a16a1 --- /dev/null +++ b/src/components/views/elements/AppTile.js @@ -0,0 +1,53 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +'use strict'; + +const React = require('react'); + +export default React.createClass({ + displayName: 'AppTile', + + propTypes: { + id: React.PropTypes.string.isRequired, + url: React.PropTypes.string.isRequired, + name: React.PropTypes.string.isRequired, + }, + + getDefaultProps: function() { + return { + url: "", + }; + }, + + render: function() { + return ( +
+
+ {this.props.name} + + Edit + Cancel + {/* x */} + +
+
+ +
+
+ ); + }, +}); diff --git a/src/components/views/elements/MemberTile.js b/src/components/views/elements/MemberTile.js new file mode 100644 index 0000000000..5becef9ede --- /dev/null +++ b/src/components/views/elements/MemberTile.js @@ -0,0 +1,97 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +'use strict'; + +var React = require('react'); + +var MatrixClientPeg = require('../../../MatrixClientPeg'); +var sdk = require('../../../index'); +var dis = require('../../../dispatcher'); +var Modal = require("../../../Modal"); + +module.exports = React.createClass({ + displayName: 'MemberTile', + + propTypes: { + member: React.PropTypes.any.isRequired, // RoomMember + }, + + getInitialState: function() { + return {}; + }, + + shouldComponentUpdate: function(nextProps, nextState) { + if ( + this.member_last_modified_time === undefined || + this.member_last_modified_time < nextProps.member.getLastModifiedTime() + ) { + return true; + } + if ( + nextProps.member.user && + (this.user_last_modified_time === undefined || + this.user_last_modified_time < nextProps.member.user.getLastModifiedTime()) + ) { + return true; + } + return false; + }, + + onClick: function(e) { + dis.dispatch({ + action: 'view_user', + member: this.props.member, + }); + }, + + _getDisplayName: function() { + return this.props.member.name; + }, + + getPowerLabel: function() { + return this.props.member.userId + " (power " + this.props.member.powerLevel + ")"; + }, + + render: function() { + var MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); + var BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); + var EntityTile = sdk.getComponent('rooms.EntityTile'); + + var member = this.props.member; + var name = this._getDisplayName(); + var active = -1; + var presenceState = member.user ? member.user.presence : null; + + var av = ( + + ); + + if (member.user) { + this.user_last_modified_time = member.user.getLastModifiedTime(); + } + this.member_last_modified_time = member.getLastModifiedTime(); + + return ( + + ); + } +}); diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 82abb07e5c..ef0cbcf2b9 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -18,6 +18,8 @@ limitations under the License. const React = require('react'); const AddAppDialog = require('../dialogs/AddAppDialog'); +const AppTile = require('../elements/AppTile'); +const Modal = require("../../../Modal"); module.exports = React.createClass({ displayName: 'AppsDrawer', @@ -26,26 +28,51 @@ module.exports = React.createClass({ }, componentDidMount: function() { + const as = this.state.apps; + as.push({ + id: "bbcApp", + url: "http://news.bbc.co.uk", + name: "BBC News", + }); + this.setState({apps: as}); }, getInitialState: function() { - // this.onClickAddWidget = this.onClickAddWidget.bind(this); return { - addAppWidget: false, + apps: [{ + id: "googleApp", + url: "http://matrix.org/grafana/dashboard/db/golang-metrics?panelId=2&fullscreen&edit&var-bucket_size=1m&var-job=riot-bot&var-handler=All&from=1495188444653&to=1495210044654", + name: "Google", + }], }; }, onClickAddWidget: function() { - this.setState({ - addAppWidget: true, + Modal.createDialog(AddAppDialog, { + onFinished: (proceed, reason) => { + if (!proceed) return; + + this.state.apps.push(); + }, }); }, render: function() { + const apps = this.state.apps.map( + (app) => ); + return (
-
[+] Add a widget
- {this.state.addAppWidget && AddAppDialog} +
+ {apps} +
+
+ [+] Add a widget +
); }, From b111579aed578dae1089c7c349a72c2a0977c551 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Mon, 22 May 2017 18:00:17 +0100 Subject: [PATCH 031/491] App tile events --- src/components/views/elements/AppTile.js | 27 ++++++++++++++++++++---- src/components/views/rooms/AppsDrawer.js | 6 +++--- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 66c32a16a1..72d59d0ecd 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -33,19 +33,38 @@ export default React.createClass({ }; }, + _onEditClick: function() { + console.log("Edit widget %s", this.props.id); + }, + + _onDeleteClick: function() { + console.log("Delete widget %s", this.props.id); + }, + render: function() { return (
{this.props.name} - Edit - Cancel - {/* x */} + {/* Edit widget */} + Edit + + {/* Delete widget */} + Cancel
- +
); diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index ef0cbcf2b9..1318d07d8b 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -40,9 +40,9 @@ module.exports = React.createClass({ getInitialState: function() { return { apps: [{ - id: "googleApp", - url: "http://matrix.org/grafana/dashboard/db/golang-metrics?panelId=2&fullscreen&edit&var-bucket_size=1m&var-job=riot-bot&var-handler=All&from=1495188444653&to=1495210044654", - name: "Google", + id: "riot-bot", + url: "https://matrix.org/_matrix/media/v1/thumbnail/matrix.org/LvHiqFMHWxAjFUMVCvaPbRYs?width=150&height=150", + name: "Riot-bot", }], }; }, From ec03cf4de39ec9a815af6a8b2d6d098c712146b4 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Tue, 30 May 2017 10:46:51 +0100 Subject: [PATCH 032/491] disable iframe sandboxing. Remove BBC news iframe --- src/components/views/elements/AppTile.js | 3 +- src/components/views/rooms/AppsDrawer.js | 36 ++++++++++++++++-------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 72d59d0ecd..18b2148829 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -64,7 +64,8 @@ export default React.createClass({
- + {/* */} +
); diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 1318d07d8b..041e0f0943 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -28,22 +28,34 @@ module.exports = React.createClass({ }, componentDidMount: function() { - const as = this.state.apps; - as.push({ - id: "bbcApp", - url: "http://news.bbc.co.uk", - name: "BBC News", - }); - this.setState({apps: as}); + // const as = this.state.apps; + // as.push({ + // id: "bbcApp", + // url: "http://news.bbc.co.uk", + // name: "BBC News", + // }); + // this.setState({apps: as}); }, getInitialState: function() { return { - apps: [{ - id: "riot-bot", - url: "https://matrix.org/_matrix/media/v1/thumbnail/matrix.org/LvHiqFMHWxAjFUMVCvaPbRYs?width=150&height=150", - name: "Riot-bot", - }], + apps: [ + // { + // id: "riot-bot", + // url: "https://matrix.org/_matrix/media/v1/thumbnail/matrix.org/LvHiqFMHWxAjFUMVCvaPbRYs?width=150&height=150", + // name: "Riot-bot", + // }, + { + id: "youtube", + url: "https://www.youtube.com/embed/ZJy1ajvMU1k?controls=0&enablejsapi=1&iv_load_policy=3&modestbranding=1&playsinline=1", + name: "Live stream - Boeuf Bourguignon", + }, + { + id: "recipie", + url: "https://www.bbcgoodfood.com/recipes/5032/beef-bourguignon", + name: "Ingredients - Boeuf Bourguignon", + }, + ], }; }, From 143f68ec56242b7eea84a9e3255bd5eb1b9c6af8 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Tue, 30 May 2017 11:40:29 +0100 Subject: [PATCH 033/491] Add locally hosted recepie widget --- src/components/views/rooms/AppsDrawer.js | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 041e0f0943..3e71d46feb 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -28,23 +28,11 @@ module.exports = React.createClass({ }, componentDidMount: function() { - // const as = this.state.apps; - // as.push({ - // id: "bbcApp", - // url: "http://news.bbc.co.uk", - // name: "BBC News", - // }); - // this.setState({apps: as}); }, getInitialState: function() { return { apps: [ - // { - // id: "riot-bot", - // url: "https://matrix.org/_matrix/media/v1/thumbnail/matrix.org/LvHiqFMHWxAjFUMVCvaPbRYs?width=150&height=150", - // name: "Riot-bot", - // }, { id: "youtube", url: "https://www.youtube.com/embed/ZJy1ajvMU1k?controls=0&enablejsapi=1&iv_load_policy=3&modestbranding=1&playsinline=1", @@ -52,7 +40,7 @@ module.exports = React.createClass({ }, { id: "recipie", - url: "https://www.bbcgoodfood.com/recipes/5032/beef-bourguignon", + url: "http://localhost:8000/recepie.html", name: "Ingredients - Boeuf Bourguignon", }, ], From 0e7bb6791f5448cbd93efbc8bdeebfc2a92260d4 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Tue, 30 May 2017 13:47:17 +0100 Subject: [PATCH 034/491] Static widget config per room --- src/components/views/rooms/AppsDrawer.js | 36 ++++++++++++++++-------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 3e71d46feb..a571cc8047 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -21,6 +21,22 @@ const AddAppDialog = require('../dialogs/AddAppDialog'); const AppTile = require('../elements/AppTile'); const Modal = require("../../../Modal"); +// FIXME -- Hard coded widget config +const roomWidgetConfig = { + '!IAkkwswSrOSzPRWksX:matrix.org': [ + { + id: "youtube", + url: "https://www.youtube.com/embed/ZJy1ajvMU1k?controls=0&enablejsapi=1&iv_load_policy=3&modestbranding=1&playsinline=1", + name: "Live stream - Boeuf Bourguignon", + }, + { + id: "recipie", + url: "http://localhost:8000/recepie.html", + name: "Ingredients - Boeuf Bourguignon", + }, + ], +}; + module.exports = React.createClass({ displayName: 'AppsDrawer', @@ -31,19 +47,15 @@ module.exports = React.createClass({ }, getInitialState: function() { + for (const key in roomWidgetConfig) { + if(key == this.props.room.roomId) { + return { + apps: roomWidgetConfig[key], + }; + } + } return { - apps: [ - { - id: "youtube", - url: "https://www.youtube.com/embed/ZJy1ajvMU1k?controls=0&enablejsapi=1&iv_load_policy=3&modestbranding=1&playsinline=1", - name: "Live stream - Boeuf Bourguignon", - }, - { - id: "recipie", - url: "http://localhost:8000/recepie.html", - name: "Ingredients - Boeuf Bourguignon", - }, - ], + apps: [], }; }, From 0d7e3a15f7f2600507ed89a568777dad4779edd3 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Tue, 30 May 2017 15:32:53 +0100 Subject: [PATCH 035/491] Add room config --- src/components/views/rooms/AppsDrawer.js | 34 ++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index a571cc8047..86c20544d9 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -23,10 +23,11 @@ const Modal = require("../../../Modal"); // FIXME -- Hard coded widget config const roomWidgetConfig = { + // Cooking room '!IAkkwswSrOSzPRWksX:matrix.org': [ { id: "youtube", - url: "https://www.youtube.com/embed/ZJy1ajvMU1k?controls=0&enablejsapi=1&iv_load_policy=3&modestbranding=1&playsinline=1", + url: "https://www.youtube.com/embed/ZJy1ajvMU1k?controls=0&enablejsapi=1&iv_load_policy=3&modestbranding=1&playsinline=1&autoplay=1", name: "Live stream - Boeuf Bourguignon", }, { @@ -35,6 +36,35 @@ const roomWidgetConfig = { name: "Ingredients - Boeuf Bourguignon", }, ], + // Grafana room + '!JWeMRscvtWqfNuzmSf:matrix.org': [ + { + id: "grafana", + url: "http://localhost:8000/grafana.html", + name: "Monitoring our Single-Point-Of-Failure DB", + }, + { + id: "recipie", + url: "http://localhost:8000/recepie.html", + name: "Ingredients - Boeuf Bourguignon", + }, + ], + // Chat room - https://www.youtube.com/watch?v=ZfkwW4GgAiU + '!wQqrqwOipOOWALxJNe:matrix.org': [ + { + id: "youtube", + url: "https://www.youtube.com/embed/ZfkwW4GgAiU?controls=0&enablejsapi=1&iv_load_policy=3&modestbranding=1&playsinline=1&autoplay=1", + name: "Live stream - ChatGirl86", + }, + ], + // Game room - https://www.youtube.com/watch?v=Dm2Ma1dOFO4 + '!dYSCwtVljhTdBlgNxq:matrix.org': [ + { + id: "youtube", + url: "https://www.youtube.com/embed/Dm2Ma1dOFO4?controls=0&enablejsapi=1&iv_load_policy=3&modestbranding=1&playsinline=1&autoplay=1", + name: "Live stream - Overwatch Balle Royale", + }, + ], }; module.exports = React.createClass({ @@ -71,7 +101,7 @@ module.exports = React.createClass({ render: function() { const apps = this.state.apps.map( - (app) => ); + (app) => ); return (
From ae1753bce6710cdf2e70f27c597d1c66f4d8cb44 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Tue, 30 May 2017 18:39:51 +0100 Subject: [PATCH 036/491] Add tipping widgets --- src/components/views/rooms/AppsDrawer.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 86c20544d9..7fcf26e69d 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -43,11 +43,6 @@ const roomWidgetConfig = { url: "http://localhost:8000/grafana.html", name: "Monitoring our Single-Point-Of-Failure DB", }, - { - id: "recipie", - url: "http://localhost:8000/recepie.html", - name: "Ingredients - Boeuf Bourguignon", - }, ], // Chat room - https://www.youtube.com/watch?v=ZfkwW4GgAiU '!wQqrqwOipOOWALxJNe:matrix.org': [ @@ -56,6 +51,11 @@ const roomWidgetConfig = { url: "https://www.youtube.com/embed/ZfkwW4GgAiU?controls=0&enablejsapi=1&iv_load_policy=3&modestbranding=1&playsinline=1&autoplay=1", name: "Live stream - ChatGirl86", }, + { + id: "thermometer", + url: "http://localhost:8000/index.html", + name: "Tip Me!!! -- Send me cash $$$", + }, ], // Game room - https://www.youtube.com/watch?v=Dm2Ma1dOFO4 '!dYSCwtVljhTdBlgNxq:matrix.org': [ @@ -64,6 +64,11 @@ const roomWidgetConfig = { url: "https://www.youtube.com/embed/Dm2Ma1dOFO4?controls=0&enablejsapi=1&iv_load_policy=3&modestbranding=1&playsinline=1&autoplay=1", name: "Live stream - Overwatch Balle Royale", }, + { + id: "thermometer", + url: "http://localhost:8000/index.html", + name: "Tip Me!!! -- Send me cash $$$", + }, ], }; From dac154f8280bc4db13f8494288ebea0e75ea5bbd Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Wed, 31 May 2017 10:08:39 +0100 Subject: [PATCH 037/491] Add full width widgets --- src/components/views/elements/AppTile.js | 2 +- src/components/views/rooms/AppsDrawer.js | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 18b2148829..338c03ceeb 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -43,7 +43,7 @@ export default React.createClass({ render: function() { return ( -
+
{this.props.name} diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 7fcf26e69d..f729c2032d 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -106,7 +106,13 @@ module.exports = React.createClass({ render: function() { const apps = this.state.apps.map( - (app) => ); + (app, index, arr) => ); return (
From fc935170949dc6a745cc949c9b77217baeb0a32e Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 31 May 2017 13:36:19 +0100 Subject: [PATCH 038/491] WIP join/part hiding - Doesn't work with MELS - Doesn't work with read markers - Doesn't work with jumping to events Shelving this for now as I fix some of this mess. --- src/components/structures/MessagePanel.js | 57 +++++++++++++++++++---- src/components/structures/UserSettings.js | 4 ++ src/i18n/strings/en_EN.json | 1 + 3 files changed, 52 insertions(+), 10 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 6b80223e42..25d1ec425f 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -20,6 +20,7 @@ var dis = require("../../dispatcher"); var sdk = require('../../index'); var MatrixClientPeg = require('../../MatrixClientPeg'); +const UserSettingsStore = require('../../UserSettingsStore'); const MILLIS_IN_DAY = 86400000; @@ -235,6 +236,44 @@ module.exports = React.createClass({ return !this._isMounted; }, + // TODO: Implement granular (per-room) hide options + _shouldShowEvent: function(mxEv) { + console.log("_shouldShowEvent " + mxEv.getId()); + const EventTile = sdk.getComponent('rooms.EventTile'); + if (!EventTile.haveTileForEvent(mxEv)) { + return false; // no tile = no show + } + + const isMemberEvent = mxEv.getType() === "m.room.member" && mxEv.getStateKey() !== undefined; + if (!isMemberEvent) { + return true; // bail early: all the checks below concern member events only + } + + // TODO: These checks are done to make sure we're dealing with membership transitions not avatar changes / dupe joins + // These checks are also being done in TextForEvent and should really reside in the JS SDK as a helper function + const membership = mxEv.getContent().membership; + const prevMembership = mxEv.getPrevContent().membership; + if (membership === prevMembership && membership === "join") { + // join -> join : This happens when display names change / avatars are set / genuine dupe joins with no changes. + // Find out which we're dealing with. + if (mxEv.getPrevContent().displayname !== mxEv.getContent().displayname) { + return true; // display name changed + } + if (mxEv.getPrevContent().avatar_url !== mxEv.getContent().avatar_url) { + return true; // avatar url changed + } + // dupe join event, fall through to hide rules + } + + // this only applies to joins/leaves not invites/kicks/bans + const isJoinOrLeave = membership === "join" || (membership === "leave" && mxEv.getStateKey() === mxEv.getSender()); + const hideJoinLeavesGlobally = UserSettingsStore.getSyncedSetting("hideJoinLeaves", false); + if (isJoinOrLeave && hideJoinLeavesGlobally) { + return false; + } + return true; + }, + _getEventTiles: function() { const EventTile = sdk.getComponent('rooms.EventTile'); const DateSeparator = sdk.getComponent('messages.DateSeparator'); @@ -246,7 +285,7 @@ module.exports = React.createClass({ // first figure out which is the last event in the list which we're // actually going to show; this allows us to behave slightly - // differently for the last event in the list. + // differently for the last event in the list. (eg show timestamps) // // we also need to figure out which is the last event we show which isn't // a local echo, to manage the read-marker. @@ -254,7 +293,7 @@ module.exports = React.createClass({ var lastShownNonLocalEchoIndex = -1; for (i = this.props.events.length-1; i >= 0; i--) { var mxEv = this.props.events[i]; - if (!EventTile.haveTileForEvent(mxEv)) { + if (!this._shouldShowEvent(mxEv)) { continue; } @@ -289,16 +328,14 @@ module.exports = React.createClass({ for (i = 0; i < this.props.events.length; i++) { let mxEv = this.props.events[i]; - let wantTile = true; let eventId = mxEv.getId(); let readMarkerInMels = false; - - if (!EventTile.haveTileForEvent(mxEv)) { - wantTile = false; - } - let last = (i == lastShownEventIndex); + if (!this._shouldShowEvent(mxEv)) { + continue; + } + // Wrap consecutive member events in a ListSummary, ignore if redacted if (isMembershipChange(mxEv) && EventTile.haveTileForEvent(mxEv) && @@ -346,7 +383,7 @@ module.exports = React.createClass({ // of MemberEventListSummary, render each member event as if the previous // one was itself. This way, the timestamp of the previous event === the // timestamp of the current event, and no DateSeperator is inserted. - let ret = this._getTilesForEvent(e, e); + let ret = this._getTilesForEvent(e, e, last); prevEvent = e; return ret; } @@ -373,7 +410,7 @@ module.exports = React.createClass({ continue; } - if (wantTile) { + if (EventTile.haveTileForEvent(mxEv)) { // make sure we unpack the array returned by _getTilesForEvent, // otherwise react will auto-generate keys and we will end up // replacing all of the DOM elements every time we paginate. diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 101ec2c378..968406a4ee 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -79,6 +79,10 @@ const SETTINGS_LABELS = [ id: 'showTwelveHourTimestamps', label: 'Show timestamps in 12 hour format (e.g. 2:30pm)', }, + { + id: 'hideJoinLeaves', + label: 'Hide join/leave messages (invites/kicks/bans unaffected)', + } /* { id: 'useCompactLayout', diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 36b9486cd0..a5c5342296 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -288,6 +288,7 @@ "Guests cannot join this room even if explicitly invited.": "Guests cannot join this room even if explicitly invited.", "had": "had", "Hangup": "Hangup", + "Hide join/leave messages (invites/kicks/bans unaffected)": "Hide join/leave messages (invites/kicks/bans unaffected)", "Hide read receipts": "Hide read receipts", "Hide Text Formatting Toolbar": "Hide Text Formatting Toolbar", "Historical": "Historical", From c6991fd33c7c99818a4784f0cf52634d22371867 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Sun, 4 Jun 2017 23:00:52 +0100 Subject: [PATCH 039/491] Periodically pass messages to embedded iframe --- src/components/views/elements/AppTile.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 338c03ceeb..134db04926 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -33,6 +33,16 @@ export default React.createClass({ }; }, + componentDidMount: function() { + console.log("App component %s mounted", this.props.id); + setInterval(() => { + const msg = "Message from riot"; + const domain = 'http://localhost:8000'; + this.refs.appFrame.contentWindow.postMessage(msg, domain); + console.log("Sending message"); + }, 3000); + }, + _onEditClick: function() { console.log("Edit widget %s", this.props.id); }, @@ -65,7 +75,7 @@ export default React.createClass({
{/* */} - +
); From 998a55a5902e44c6f9ae815a64bd2cfda51826fe Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 5 Jun 2017 16:51:50 +0100 Subject: [PATCH 040/491] Add basic group view --- src/PageTypes.js | 1 + src/components/structures/GroupView.js | 130 ++++++++++++++++++++++ src/components/structures/LoggedInView.js | 7 ++ src/components/structures/MatrixChat.js | 23 ++++ src/linkify-matrix.js | 49 ++++++++ 5 files changed, 210 insertions(+) create mode 100644 src/components/structures/GroupView.js diff --git a/src/PageTypes.js b/src/PageTypes.js index d87b363a6f..b2346c62c3 100644 --- a/src/PageTypes.js +++ b/src/PageTypes.js @@ -22,4 +22,5 @@ export default { CreateRoom: "create_room", RoomDirectory: "room_directory", UserView: "user_view", + GroupView: "group_view", }; diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js new file mode 100644 index 0000000000..2368c44319 --- /dev/null +++ b/src/components/structures/GroupView.js @@ -0,0 +1,130 @@ +/* +Copyright 2017 Vector Creations Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import MatrixClientPeg from '../../MatrixClientPeg'; +import sdk from '../../index'; +import sanitizeHtml from "sanitize-html"; +import { sanitizeHtmlParams } from '../../HtmlUtils'; + + +module.exports = React.createClass({ + displayName: 'GroupView', + + propTypes: { + groupId: React.PropTypes.string.isRequired, + }, + + getInitialState: function() { + return { + phase: "GroupView.LOADING", // LOADING / DISPLAY / ERROR / NOT_FOUND + summary: null, + }; + }, + + componentWillMount: function() { + this.setState({ + phase: "GroupView.LOADING", + summary: null, + }) + this._loadGroupFromServer(this.props.groupId) + }, + + componentWillReceiveProps: function(new_props) { + if (this.props.groupId != new_props.groupId) { + this.setState({ + phase: "GroupView.LOADING", + summary: null, + }) + this._loadGroupFromServer(new_props.groupId); + } + }, + + _loadGroupFromServer: function(groupId) { + const self = this; + MatrixClientPeg.get().getGroupSummary(groupId).done(function(res) { + self.setState({ + phase: "GroupView.DISPLAY", + summary: res, + }); + }, function(err) { + self.setState({ + phase: err.errcode == 404 ? "GroupView.NOT_FOUND" :"GroupView.ERROR", + summary: null, + }); + }); + }, + + render: function() { + var BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); + var Loader = sdk.getComponent("elements.Spinner"); + + if (this.state.phase == "GroupView.LOADING") { + return ( +
+ +
+ ); + } else if (this.state.phase == "GroupView.DISPLAY") { + const summary = this.state.summary; + let avatar_url = null; + if (summary.profile.avatar_url) { + avatar_url = MatrixClientPeg.get().mxcUrlToHttp(summary.profile.avatar_url); + } + let description = null; + if (summary.profile.long_description) { + description = sanitizeHtml(summary.profile.long_description); + } + return ( +
+
+
+
+ +
+
+
+ {summary.profile.name} + + ({this.props.groupId}) + +
+
+ {summary.profile.short_description} +
+
+
+
+
+
+ ); + } else if (this.state.phase == "GroupView.NOT_FOUND") { +
+ Group {this.props.groupId} not found +
+ } else { + return ( +
+ Failed to load {this.props.groupId} +
+ ); + } + }, +}); diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index e2fdeb4687..cf9a8310b1 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -179,6 +179,7 @@ export default React.createClass({ const CreateRoom = sdk.getComponent('structures.CreateRoom'); const RoomDirectory = sdk.getComponent('structures.RoomDirectory'); const HomePage = sdk.getComponent('structures.HomePage'); + const GroupView = sdk.getComponent('structures.GroupView'); const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar'); const GuestWarningBar = sdk.getComponent('globals.GuestWarningBar'); const NewVersionBar = sdk.getComponent('globals.NewVersionBar'); @@ -247,6 +248,12 @@ export default React.createClass({ page_element = null; // deliberately null for now right_panel = ; break; + case PageTypes.GroupView: + // TODO + page_element = + break; } var topBar; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 0dedc02270..8771231e68 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -261,6 +261,9 @@ module.exports = React.createClass({ if (this.onUserClick) { linkifyMatrix.onUserClick = this.onUserClick; } + if (this.onGroupClick) { + linkifyMatrix.onGroupClick = this.onGroupClick; + } window.addEventListener('resize', this.handleResize); this.handleResize(); @@ -458,6 +461,12 @@ module.exports = React.createClass({ this._setPage(PageTypes.RoomDirectory); this.notifyNewScreen('directory'); break; + case 'view_group': + const groupId = payload.group_id; + this.setState({currentGroupId: groupId}); + this._setPage(PageTypes.GroupView); + this.notifyNewScreen('group/' + groupId); + break; case 'view_home_page': if (!this._teamToken) { dis.dispatch({action: 'view_room_directory'}); @@ -1001,6 +1010,15 @@ module.exports = React.createClass({ member: member, }); } + } else if (screen.indexOf('group/') == 0) { + const groupId = screen.substring(6); + + // TODO: Check valid group ID + + dis.dispatch({ + action: 'view_group', + group_id: groupId, + }); } else { console.info("Ignoring showScreen for '%s'", screen); } @@ -1029,6 +1047,11 @@ module.exports = React.createClass({ }); }, + onGroupClick: function(event, groupId) { + event.preventDefault(); + dis.dispatch({action: 'view_group', group_id: groupId}); + }, + onLogoutClick: function(event) { dis.dispatch({ action: 'logout', diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js index d9b0b78982..b96730145a 100644 --- a/src/linkify-matrix.js +++ b/src/linkify-matrix.js @@ -108,11 +108,53 @@ function matrixLinkify(linkify) { S_AT_NAME_COLON_DOMAIN.on(TT.DOT, S_AT_NAME_COLON_DOMAIN_DOT); S_AT_NAME_COLON_DOMAIN_DOT.on(TT.DOMAIN, S_AT_NAME_COLON_DOMAIN); S_AT_NAME_COLON_DOMAIN_DOT.on(TT.TLD, S_USERID); + + + var GROUPID = function(value) { + MultiToken.call(this, value); + this.type = 'groupid'; + this.isLink = true; + }; + GROUPID.prototype = new MultiToken(); + + var S_PLUS = new linkify.parser.State(); + var S_PLUS_NAME = new linkify.parser.State(); + var S_PLUS_NAME_COLON = new linkify.parser.State(); + var S_PLUS_NAME_COLON_DOMAIN = new linkify.parser.State(); + var S_PLUS_NAME_COLON_DOMAIN_DOT = new linkify.parser.State(); + var S_GROUPID = new linkify.parser.State(GROUPID); + + var groupid_tokens = [ + TT.DOT, + TT.UNDERSCORE, + TT.PLUS, + TT.NUM, + TT.DOMAIN, + TT.TLD, + + // as in roomname_tokens + TT.LOCALHOST, + ]; + + S_START.on(TT.PLUS, S_PLUS); + + S_PLUS.on(groupid_tokens, S_PLUS_NAME); + S_PLUS_NAME.on(groupid_tokens, S_PLUS_NAME); + S_PLUS_NAME.on(TT.DOMAIN, S_PLUS_NAME); + + S_PLUS_NAME.on(TT.COLON, S_PLUS_NAME_COLON); + + S_PLUS_NAME_COLON.on(TT.DOMAIN, S_PLUS_NAME_COLON_DOMAIN); + S_PLUS_NAME_COLON.on(TT.LOCALHOST, S_GROUPID); // accept +foo:localhost + S_PLUS_NAME_COLON_DOMAIN.on(TT.DOT, S_PLUS_NAME_COLON_DOMAIN_DOT); + S_PLUS_NAME_COLON_DOMAIN_DOT.on(TT.DOMAIN, S_PLUS_NAME_COLON_DOMAIN); + S_PLUS_NAME_COLON_DOMAIN_DOT.on(TT.TLD, S_GROUPID); } // stubs, overwritten in MatrixChat's componentDidMount matrixLinkify.onUserClick = function(e, userId) { e.preventDefault(); }; matrixLinkify.onAliasClick = function(e, roomAlias) { e.preventDefault(); }; +matrixLinkify.onGroupClick = function(e, groupId) { e.preventDefault(); }; var escapeRegExp = function(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); @@ -143,6 +185,12 @@ matrixLinkify.options = { matrixLinkify.onAliasClick(e, href); } }; + case "groupid": + return { + click: function(e) { + matrixLinkify.onGroupClick(e, href); + } + }; } }, @@ -150,6 +198,7 @@ matrixLinkify.options = { switch (type) { case 'roomalias': case 'userid': + case "groupid": return matrixLinkify.MATRIXTO_BASE_URL + '/#/' + href; default: var m; From dc4f321707da8d7431c857ab6abf8240e671fb31 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Mon, 5 Jun 2017 18:21:31 +0100 Subject: [PATCH 041/491] Pass room and user id to apps draw --- src/components/structures/RoomView.js | 4 ++++ src/components/views/elements/AppTile.js | 3 +-- src/components/views/rooms/AppsDrawer.js | 30 ++++++++++++++++++++++-- src/components/views/rooms/AuxPanel.js | 5 +++- 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 49aa3a0af5..af5429fe4e 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -122,6 +122,7 @@ module.exports = React.createClass({ return { room: null, roomId: null, + userId: null, roomLoading: true, editingRoomSettings: false, uploadingRoomSettings: false, @@ -185,6 +186,7 @@ module.exports = React.createClass({ this.setState({ room: room, roomId: result.room_id, + userId: MatrixClientPeg.get().credentials.userId, roomLoading: !room, unsentMessageError: this._getUnsentMessageError(room), }, this._onHaveRoom); @@ -198,6 +200,7 @@ module.exports = React.createClass({ var room = MatrixClientPeg.get().getRoom(this.props.roomAddress); this.setState({ roomId: this.props.roomAddress, + userId: MatrixClientPeg.get().credentials.userId, room: room, roomLoading: !room, unsentMessageError: this._getUnsentMessageError(room), @@ -1640,6 +1643,7 @@ module.exports = React.createClass({ var auxPanel = (
- {/* */} - +
); diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index f729c2032d..ed21fb6d77 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -44,7 +44,7 @@ const roomWidgetConfig = { name: "Monitoring our Single-Point-Of-Failure DB", }, ], - // Chat room - https://www.youtube.com/watch?v=ZfkwW4GgAiU + // Camgirl room - https://www.youtube.com/watch?v=ZfkwW4GgAiU '!wQqrqwOipOOWALxJNe:matrix.org': [ { id: "youtube", @@ -70,6 +70,14 @@ const roomWidgetConfig = { name: "Tip Me!!! -- Send me cash $$$", }, ], + // Game room - !BLQjREzUgbtIsgrvRn:matrix.org + '!BLQjREzUgbtIsgrvRn:matrix.org': [ + { + id: "etherpad", + url: "http://localhost:8000/etherpad.html", + name: "Etherpad", + }, + ], }; module.exports = React.createClass({ @@ -81,11 +89,27 @@ module.exports = React.createClass({ componentDidMount: function() { }, + initAppConfig: function(appConfig) { + console.log("App props: ", this.props); + appConfig = appConfig.map( + (app, index, arr) => { + switch(app.id) { + case 'etherpad': + app.url = app.url + '?userName=' + this.props.userId + + '&padId=' + this.props.room.roomId; + break; + } + + return app; + }); + return appConfig; + }, + getInitialState: function() { for (const key in roomWidgetConfig) { if(key == this.props.room.roomId) { return { - apps: roomWidgetConfig[key], + apps: this.initAppConfig(roomWidgetConfig[key]), }; } } @@ -112,6 +136,8 @@ module.exports = React.createClass({ url={app.url} name={app.name} fullWdith={arr.length<2 ? true : false} + roomId={this.props.roomId} + userId={this.props.userId} />); return ( diff --git a/src/components/views/rooms/AuxPanel.js b/src/components/views/rooms/AuxPanel.js index 31739a890f..ec2de2558c 100644 --- a/src/components/views/rooms/AuxPanel.js +++ b/src/components/views/rooms/AuxPanel.js @@ -116,7 +116,10 @@ module.exports = React.createClass({ let appsDrawer = null; if(this.props.showApps) { - appsDrawer = ; + appsDrawer = ; } return ( From e9f110a4c53e0b0a2a85033700afb2e4d73f27de Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Tue, 6 Jun 2017 14:50:43 +0100 Subject: [PATCH 042/491] Don't show add widget if there are more than one existing widgets --- src/components/views/rooms/AppsDrawer.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index ed21fb6d77..37cddf8a2e 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -140,18 +140,21 @@ module.exports = React.createClass({ userId={this.props.userId} />); + const addWidget = this.state.apps && this.state.apps.length < 2 && + (
+ [+] Add a widget +
); + return (
{apps}
-
- [+] Add a widget -
+ {addWidget}
); }, From e8353edb064b2b3d2509ab99826e542763cdf973 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Tue, 6 Jun 2017 15:57:40 +0100 Subject: [PATCH 043/491] Disable test postmessag --- src/components/views/elements/AppTile.js | 12 ++++++------ src/components/views/rooms/AppsDrawer.js | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index fd59735014..a3713d2b96 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -35,12 +35,12 @@ export default React.createClass({ componentDidMount: function() { console.log("App component %s mounted", this.props.id); - setInterval(() => { - const msg = "Message from riot"; - const domain = 'http://localhost:8000'; - this.refs.appFrame.contentWindow.postMessage(msg, domain); - console.log("Sending message"); - }, 3000); + // setInterval(() => { + // const msg = "Message from riot"; + // const domain = 'http://localhost:8000'; + // this.refs.appFrame.contentWindow.postMessage(msg, domain); + // console.log("Sending message"); + // }, 3000); }, _onEditClick: function() { diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 37cddf8a2e..af262a9470 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -32,7 +32,7 @@ const roomWidgetConfig = { }, { id: "recipie", - url: "http://localhost:8000/recepie.html", + url: "http://10.9.64.88:8000/recepie.html", name: "Ingredients - Boeuf Bourguignon", }, ], @@ -40,7 +40,7 @@ const roomWidgetConfig = { '!JWeMRscvtWqfNuzmSf:matrix.org': [ { id: "grafana", - url: "http://localhost:8000/grafana.html", + url: "http://10.9.64.88:8000/grafana.html", name: "Monitoring our Single-Point-Of-Failure DB", }, ], @@ -53,7 +53,7 @@ const roomWidgetConfig = { }, { id: "thermometer", - url: "http://localhost:8000/index.html", + url: "http://10.9.64.88:8000/index.html", name: "Tip Me!!! -- Send me cash $$$", }, ], @@ -66,7 +66,7 @@ const roomWidgetConfig = { }, { id: "thermometer", - url: "http://localhost:8000/index.html", + url: "http://10.9.64.88:8000/index.html", name: "Tip Me!!! -- Send me cash $$$", }, ], @@ -74,7 +74,7 @@ const roomWidgetConfig = { '!BLQjREzUgbtIsgrvRn:matrix.org': [ { id: "etherpad", - url: "http://localhost:8000/etherpad.html", + url: "http://10.9.64.88:8000/etherpad.html", name: "Etherpad", }, ], From f6f660fa9a240de088b8284ba41557660968438c Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Tue, 6 Jun 2017 23:45:17 +0100 Subject: [PATCH 044/491] Initial app icon tiles --- src/components/structures/AppWidget.js | 28 +++++++++++++ src/components/structures/ModularWidgets.js | 13 +++++++ src/components/views/dialogs/AddAppDialog.js | 18 ++++----- src/components/views/elements/AppIconTile.js | 41 ++++++++++++++++++++ src/components/views/rooms/AppsDrawer.js | 10 ++--- 5 files changed, 96 insertions(+), 14 deletions(-) create mode 100644 src/components/structures/AppWidget.js create mode 100644 src/components/structures/ModularWidgets.js create mode 100644 src/components/views/elements/AppIconTile.js diff --git a/src/components/structures/AppWidget.js b/src/components/structures/AppWidget.js new file mode 100644 index 0000000000..5ab2207f60 --- /dev/null +++ b/src/components/structures/AppWidget.js @@ -0,0 +1,28 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +import ModularWidgets from 'ModularWidgets'; + +class AppWidget { + constructor(type, url, options) { + if(!ModularWidgets.widgetTypes.includes(type) || url === "") { + return null; + } + this.type = type; + this.url = url; + this.options = options || {}; + } +} +export default AppWidget; diff --git a/src/components/structures/ModularWidgets.js b/src/components/structures/ModularWidgets.js new file mode 100644 index 0000000000..16b00dfbf3 --- /dev/null +++ b/src/components/structures/ModularWidgets.js @@ -0,0 +1,13 @@ +class ModularWidgets { + static widgetTypes = [ + { + type: 'etherpad', + icon: '', + }, + { + type: 'grafana', + icon: '', + }, + ]; +} +export default ModularWidgets; diff --git a/src/components/views/dialogs/AddAppDialog.js b/src/components/views/dialogs/AddAppDialog.js index b72a3809b4..90bb53f3c3 100644 --- a/src/components/views/dialogs/AddAppDialog.js +++ b/src/components/views/dialogs/AddAppDialog.js @@ -53,22 +53,22 @@ export default React.createClass({ return (
- Please enter the URL of the app / widget to add. -
-
-
+ + {/*
+ +
Or enter the URL of the widget to add.
-
+
- -
- + +
*/} +
); }, diff --git a/src/components/views/elements/AppIconTile.js b/src/components/views/elements/AppIconTile.js new file mode 100644 index 0000000000..2c67efcede --- /dev/null +++ b/src/components/views/elements/AppIconTile.js @@ -0,0 +1,41 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +'use strict'; +import React from 'react'; + +class AppIconTile extends React.Component { + render() { + return ( +
+ {this.props.name} +
+

{this.props.name}

+

{this.props.description}

+
+
+ ); + } +} + +AppIconTile.propTypes = { + type: React.PropTypes.string.isRequired, + icon: React.PropTypes.string.isRequired, + name: React.PropTypes.string.isRequired, + description: React.PropTypes.string.isRequired, +}; + +export default AppIconTile; diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index af262a9470..0386128a10 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -32,7 +32,7 @@ const roomWidgetConfig = { }, { id: "recipie", - url: "http://10.9.64.88:8000/recepie.html", + url: "http://10.9.64.55:8000/recepie.html", name: "Ingredients - Boeuf Bourguignon", }, ], @@ -40,7 +40,7 @@ const roomWidgetConfig = { '!JWeMRscvtWqfNuzmSf:matrix.org': [ { id: "grafana", - url: "http://10.9.64.88:8000/grafana.html", + url: "http://10.9.64.55:8000/grafana.html", name: "Monitoring our Single-Point-Of-Failure DB", }, ], @@ -53,7 +53,7 @@ const roomWidgetConfig = { }, { id: "thermometer", - url: "http://10.9.64.88:8000/index.html", + url: "http://10.9.64.55:8000/index.html", name: "Tip Me!!! -- Send me cash $$$", }, ], @@ -66,7 +66,7 @@ const roomWidgetConfig = { }, { id: "thermometer", - url: "http://10.9.64.88:8000/index.html", + url: "http://10.9.64.55:8000/index.html", name: "Tip Me!!! -- Send me cash $$$", }, ], @@ -74,7 +74,7 @@ const roomWidgetConfig = { '!BLQjREzUgbtIsgrvRn:matrix.org': [ { id: "etherpad", - url: "http://10.9.64.88:8000/etherpad.html", + url: "http://10.9.64.55:8000/etherpad.html", name: "Etherpad", }, ], From c552f7f336f9490fd17dd1b3d4afb8c40e9d2f53 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Wed, 7 Jun 2017 10:55:49 +0100 Subject: [PATCH 045/491] App icon styling --- src/components/structures/ModularWidgets.js | 8 ++++++-- src/components/views/dialogs/AddAppDialog.js | 13 ++++++++++++- src/components/views/elements/AppIconTile.js | 4 +++- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/components/structures/ModularWidgets.js b/src/components/structures/ModularWidgets.js index 16b00dfbf3..306a645a6f 100644 --- a/src/components/structures/ModularWidgets.js +++ b/src/components/structures/ModularWidgets.js @@ -2,11 +2,15 @@ class ModularWidgets { static widgetTypes = [ { type: 'etherpad', - icon: '', + icon: 'http://localhost:8000/static/etherpad.svg', + name: 'Etherpad', + description: 'Collaborative text editor', }, { type: 'grafana', - icon: '', + icon: 'http://localhost:8000/static/grafana.svg', + name: 'Grafana', + description: 'Graph and monitor all the things!', }, ]; } diff --git a/src/components/views/dialogs/AddAppDialog.js b/src/components/views/dialogs/AddAppDialog.js index 90bb53f3c3..0d24c641a6 100644 --- a/src/components/views/dialogs/AddAppDialog.js +++ b/src/components/views/dialogs/AddAppDialog.js @@ -16,6 +16,8 @@ limitations under the License. import React from 'react'; import sdk from '../../../index'; +import AppIconTile from '../elements/AppIconTile'; +import ModularWidgets from '../../structures/ModularWidgets'; /** * Prompt the user for address of iframe widget @@ -50,13 +52,22 @@ export default React.createClass({ render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const appCards = ModularWidgets.widgetTypes.map((widgetType, index) => + , + ); + return (
- + {appCards} {/*
Or enter the URL of the widget to add.
diff --git a/src/components/views/elements/AppIconTile.js b/src/components/views/elements/AppIconTile.js index 2c67efcede..9e9dbe6d41 100644 --- a/src/components/views/elements/AppIconTile.js +++ b/src/components/views/elements/AppIconTile.js @@ -21,7 +21,9 @@ class AppIconTile extends React.Component { render() { return (
- {this.props.name} +
+ {this.props.name} +

{this.props.name}

{this.props.description}

From a09001933a4c48610c1b39ff40f056a2e77ed226 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Wed, 7 Jun 2017 15:39:27 +0100 Subject: [PATCH 046/491] git status --- src/components/structures/ModularWidgets.js | 6 ++++++ src/components/views/dialogs/AddAppDialog.js | 4 ++-- src/components/views/elements/AppIconTile.js | 8 +++++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/components/structures/ModularWidgets.js b/src/components/structures/ModularWidgets.js index 306a645a6f..4e59fd9cfd 100644 --- a/src/components/structures/ModularWidgets.js +++ b/src/components/structures/ModularWidgets.js @@ -12,6 +12,12 @@ class ModularWidgets { name: 'Grafana', description: 'Graph and monitor all the things!', }, + { + type: 'custom', + icon: 'http://localhost:8000/static/blocks.png', + name: 'Custom Widget', + description: 'Add your own custom widget', + }, ]; } export default ModularWidgets; diff --git a/src/components/views/dialogs/AddAppDialog.js b/src/components/views/dialogs/AddAppDialog.js index 0d24c641a6..6d722365e8 100644 --- a/src/components/views/dialogs/AddAppDialog.js +++ b/src/components/views/dialogs/AddAppDialog.js @@ -68,7 +68,7 @@ export default React.createClass({ >
{appCards} - {/*
+
Or enter the URL of the widget to add.
-
*/} +
); diff --git a/src/components/views/elements/AppIconTile.js b/src/components/views/elements/AppIconTile.js index 9e9dbe6d41..282a33743c 100644 --- a/src/components/views/elements/AppIconTile.js +++ b/src/components/views/elements/AppIconTile.js @@ -18,9 +18,15 @@ limitations under the License. import React from 'react'; class AppIconTile extends React.Component { + render() { + const contentClasses = ['mx_AppIconTile']; + // if(this.props.type == 'custom') { + // contentClasses.push('mx_AppIconTile_active'); + // } + return ( -
+
{this.props.name}
From 6d1d43524788e8278681fcef2ba466ba96addeed Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 9 Jun 2017 11:21:37 +0100 Subject: [PATCH 047/491] Scalar: add in set_widget and get_widgets --- src/ScalarMessaging.js | 141 +++++++++++++++++++++++++++++++++++- src/i18n/strings/en_EN.json | 1 + 2 files changed, 141 insertions(+), 1 deletion(-) diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index c1b975e8e8..2ceb021a93 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -1,5 +1,6 @@ /* Copyright 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -109,6 +110,76 @@ Example: response: 78 } +set_widget +---------- +Set a new widget in the room. Clobbers based on the ID. + +Request: + - `room_id` (String) is the room to set the widget in. + - `widget_id` (String) is the ID of the widget to add (or replace if it already exists). + It can be an arbitrary UTF8 string and is purely for distinguishing between widgets. + - `url` (String) is the URL that clients should load in an iframe to run the widget. + All widgets must have a valid URL. If the URL is `null` (not `undefined`), the + widget will be removed from the room. + - `type` (String) is the type of widget, which is provided as a hint for matrix clients so they + can configure/lay out the widget in different ways. All widgets must have a type. + - `name` (String) is an optional human-readable string about the widget. + - `data` (Object) is some optional data about the widget, and can contain arbitrary key/value pairs. +Response: +{ + success: true +} +Example: +{ + action: "set_widget", + room_id: "!foo:bar", + widget_id: "abc123", + url: "http://widget.url", + type: "example", + response: { + success: true + } +} + +get_widgets +----------- +Get a list of all widgets in the room. The response is the `content` field +of the state event. + +Request: + - `room_id` (String) is the room to get the widgets in. +Response: +{ + $widget_id: { + type: "example", + url: "http://widget.url", + name: "Example Widget", + data: { + key: "val" + } + }, + $widget_id: { ... } +} +Example: +{ + action: "get_widgets", + room_id: "!foo:bar", + widget_id: "abc123", + url: "http://widget.url", + type: "example", + response: { + $widget_id: { + type: "example", + url: "http://widget.url", + name: "Example Widget", + data: { + key: "val" + } + }, + $widget_id: { ... } + } +} + membership_state AND bot_options -------------------------------- @@ -191,6 +262,68 @@ function inviteUser(event, roomId, userId) { }); } +function setWidget(event, roomId) { + // check required fields exist + const widgetId = event.data.widget_id; + const widgetType = event.data.type; + const widgetUrl = event.data.url; + if (!widgetId || widgetUrl === undefined || !widgetType) { + sendError(event, _t("Unable to create widget."), new Error("Missing required widget fields.")); + return; + } + const client = MatrixClientPeg.get(); + if (!client) { + sendError(event, _t('You need to be logged in.')); + return; + } + + // check types of fields + const widgetName = event.data.name; // optional + const widgetData = event.data.data; // optional + if (widgetName !== undefined && typeof widgetName !== 'string') { + sendError(event, _t("Unable to create widget."), new Error("Optional field 'name' must be a string.")); + return; + } + if (widgetData !== undefined && !(widgetData instanceof Object)) { + sendError(event, _t("Unable to create widget."), new Error("Optional field 'data' must be an Object.")); + return; + } + if (typeof widgetType !== 'string') { + sendError(event, _t("Unable to create widget."), new Error("Field 'type' must be a string.")); + return; + } + if (widgetUrl !== null && typeof widgetUrl !== 'string') { + sendError(event, _t("Unable to create widget."), new Error("Field 'url' must be a string or null.")); + return; + } + + // TODO: same dance we do for power levels. It'd be nice if the JS SDK had helper methods to do this. + client.getStateEvent(roomId, "im.vector.modular.widgets", "").then((widgets) => { + if (widgetUrl === null) { + delete widgets[widgetId]; + } + else { + widgets[widgetId] = { + type: widgetType, + url: widgetUrl, + name: widgetName, + data: widgetData, + }; + } + return client.sendStateEvent(roomId, "im.vector.modular.widgets", widgets); + }).done(() => { + sendResponse(event, { + success: true, + }); + }, (err) => { + sendError(event, _t('Failed to send request.'), err); + }); +} + +function getWidgets(event, roomId) { + returnStateEvent(event, roomId, "im.vector.modular.widgets", ""); +} + function setPlumbingState(event, roomId, status) { if (typeof status !== 'string') { throw new Error('Plumbing state status should be a string'); @@ -367,7 +500,7 @@ const onMessage = function(event) { return; } - // Getting join rules does not require userId + // These APIs don't require userId if (event.data.action === "join_rules_state") { getJoinRules(event, roomId); return; @@ -377,6 +510,12 @@ const onMessage = function(event) { } else if (event.data.action === "get_membership_count") { getMembershipCount(event, roomId); return; + } else if (event.data.action === "set_widget") { + setWidget(event, roomId); + return; + } else if (event.data.action === "get_widgets") { + getWidgets(event, roomId); + return; } if (!userId) { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3540feddee..7494d9378f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -580,6 +580,7 @@ "Turn Markdown on": "Turn Markdown on", "%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).": "%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).", "Unable to add email address": "Unable to add email address", + "Unable to create widget.": "Unable to create widget.", "Unable to remove contact information": "Unable to remove contact information", "Unable to restore previous session": "Unable to restore previous session", "Unable to verify email address.": "Unable to verify email address.", From b70881f07831115128cd9247e0535f0810f6b4cf Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 9 Jun 2017 12:34:19 +0100 Subject: [PATCH 048/491] Rejig to support deletions better --- src/ScalarMessaging.js | 46 +++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index 2ceb021a93..61a76289b6 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -263,38 +263,42 @@ function inviteUser(event, roomId, userId) { } function setWidget(event, roomId) { - // check required fields exist const widgetId = event.data.widget_id; const widgetType = event.data.type; const widgetUrl = event.data.url; - if (!widgetId || widgetUrl === undefined || !widgetType) { - sendError(event, _t("Unable to create widget."), new Error("Missing required widget fields.")); - return; - } + const widgetName = event.data.name; // optional + const widgetData = event.data.data; // optional + const client = MatrixClientPeg.get(); if (!client) { sendError(event, _t('You need to be logged in.')); return; } - // check types of fields - const widgetName = event.data.name; // optional - const widgetData = event.data.data; // optional - if (widgetName !== undefined && typeof widgetName !== 'string') { - sendError(event, _t("Unable to create widget."), new Error("Optional field 'name' must be a string.")); + // both adding/removing widgets need these checks + if (!widgetId || widgetUrl === undefined) { + sendError(event, _t("Unable to create widget."), new Error("Missing required widget fields.")); return; } - if (widgetData !== undefined && !(widgetData instanceof Object)) { - sendError(event, _t("Unable to create widget."), new Error("Optional field 'data' must be an Object.")); - return; - } - if (typeof widgetType !== 'string') { - sendError(event, _t("Unable to create widget."), new Error("Field 'type' must be a string.")); - return; - } - if (widgetUrl !== null && typeof widgetUrl !== 'string') { - sendError(event, _t("Unable to create widget."), new Error("Field 'url' must be a string or null.")); - return; + + if (widgetUrl !== null) { // if url is null it is being deleted, don't need to check name/type/etc + // check types of fields + if (widgetName !== undefined && typeof widgetName !== 'string') { + sendError(event, _t("Unable to create widget."), new Error("Optional field 'name' must be a string.")); + return; + } + if (widgetData !== undefined && !(widgetData instanceof Object)) { + sendError(event, _t("Unable to create widget."), new Error("Optional field 'data' must be an Object.")); + return; + } + if (typeof widgetType !== 'string') { + sendError(event, _t("Unable to create widget."), new Error("Field 'type' must be a string.")); + return; + } + if (typeof widgetUrl !== 'string') { + sendError(event, _t("Unable to create widget."), new Error("Field 'url' must be a string or null.")); + return; + } } // TODO: same dance we do for power levels. It'd be nice if the JS SDK had helper methods to do this. From 34d7d793b7d0208be49df0de24a9e95225d88a84 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 9 Jun 2017 15:06:09 +0100 Subject: [PATCH 049/491] Handle M_NOT_FOUND --- src/ScalarMessaging.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index 61a76289b6..49f1a5c6f9 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -315,6 +315,18 @@ function setWidget(event, roomId) { }; } return client.sendStateEvent(roomId, "im.vector.modular.widgets", widgets); + }, (err) => { + if (err.errcode === "M_NOT_FOUND") { + return client.sendStateEvent(roomId, "im.vector.modular.widgets", { + [widgetId]: { + type: widgetType, + url: widgetUrl, + name: widgetName, + data: widgetData, + } + }); + } + throw err; }).done(() => { sendResponse(event, { success: true, From b893887707079ac23e2312a59ad7edf846a718e9 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Mon, 12 Jun 2017 14:52:41 +0100 Subject: [PATCH 050/491] Fix merge conflict --- src/components/views/rooms/MessageComposer.js | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 0f9ec5cd4e..c4e13a5fe6 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -354,26 +354,21 @@ export default class MessageComposer extends React.Component { } else { controls.push(
-<<<<<<< HEAD - You do not have permission to post to this room -
, -======= { _t('You do not have permission to post to this room') } -
->>>>>>> 31f1e421f226bd471b68cdf1f69a8e049a443e5d +
, ); } - // let autoComplete; - // if (UserSettingsStore.isFeatureEnabled('rich_text_editor')) { - // autoComplete =
- // - //
; - // } + let autoComplete; + if (UserSettingsStore.isFeatureEnabled('rich_text_editor')) { + autoComplete =
+ +
; + } const {style, blockType} = this.state.inputState; From 2da30137ec5bc7ef1230c5560fc9f221d9b6fec5 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Tue, 13 Jun 2017 10:31:16 +0100 Subject: [PATCH 051/491] Fix import path and add LG widget --- src/components/structures/AppWidget.js | 2 +- src/components/views/rooms/AppsDrawer.js | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/structures/AppWidget.js b/src/components/structures/AppWidget.js index 5ab2207f60..283efb9fcd 100644 --- a/src/components/structures/AppWidget.js +++ b/src/components/structures/AppWidget.js @@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import ModularWidgets from 'ModularWidgets'; +import ModularWidgets from './ModularWidgets'; class AppWidget { constructor(type, url, options) { diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 0386128a10..9c49fdb663 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -78,6 +78,14 @@ const roomWidgetConfig = { name: "Etherpad", }, ], + // Insurance room - !nTUetaZELiqWcWYshy:matrix.org + '!nTUetaZELiqWcWYshy:matrix.org': [ + { + id: "lg", + url: "http://localhost:8000/lg.html", + name: "L&G Insurance Policy", + }, + ], }; module.exports = React.createClass({ From 99b1de7f0e886166a50885418cb3e021e5a0de7e Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Tue, 13 Jun 2017 15:19:06 +0200 Subject: [PATCH 052/491] RoomView: Display AppsDrawer if apps in room state --- src/components/structures/RoomView.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 29534727a2..4d7ac46ab4 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -251,12 +251,18 @@ module.exports = React.createClass({ } else if (isUserJoined) { MatrixClientPeg.get().stopPeeking(); this.setState({ + showApps: this._shouldShowApps(room), unsentMessageError: this._getUnsentMessageError(room), }); this._onRoomLoaded(room); } }, + _shouldShowApps: function(room) { + const appsStateEvents = room.currentState.getStateEvents('im.vector.modular.widgets', ''); + return appsStateEvents && Object.keys(appsStateEvents.getContent()).length > 0; + }, + componentDidMount: function() { var call = this._getCallForRoom(); var callState = call ? call.call_state : "ended"; From e2759774fc027a7d4057571955217e6182fd43b8 Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Tue, 13 Jun 2017 15:19:38 +0200 Subject: [PATCH 053/491] RoomView: Correctly pass userId from matrix client It isn't set in the state anywhere. --- src/components/structures/RoomView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 4d7ac46ab4..0bf3d4e181 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1604,7 +1604,7 @@ module.exports = React.createClass({ var auxPanel = ( Date: Tue, 13 Jun 2017 15:28:37 +0200 Subject: [PATCH 054/491] AppsDrawer: Populate apps from room state --- src/components/views/rooms/AppsDrawer.js | 155 +++++++++++------------ 1 file changed, 74 insertions(+), 81 deletions(-) diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 0386128a10..d2b6cf8b21 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -17,107 +17,100 @@ limitations under the License. 'use strict'; const React = require('react'); +const MatrixClientPeg = require('../../../MatrixClientPeg'); const AddAppDialog = require('../dialogs/AddAppDialog'); const AppTile = require('../elements/AppTile'); const Modal = require("../../../Modal"); - -// FIXME -- Hard coded widget config -const roomWidgetConfig = { - // Cooking room - '!IAkkwswSrOSzPRWksX:matrix.org': [ - { - id: "youtube", - url: "https://www.youtube.com/embed/ZJy1ajvMU1k?controls=0&enablejsapi=1&iv_load_policy=3&modestbranding=1&playsinline=1&autoplay=1", - name: "Live stream - Boeuf Bourguignon", - }, - { - id: "recipie", - url: "http://10.9.64.55:8000/recepie.html", - name: "Ingredients - Boeuf Bourguignon", - }, - ], - // Grafana room - '!JWeMRscvtWqfNuzmSf:matrix.org': [ - { - id: "grafana", - url: "http://10.9.64.55:8000/grafana.html", - name: "Monitoring our Single-Point-Of-Failure DB", - }, - ], - // Camgirl room - https://www.youtube.com/watch?v=ZfkwW4GgAiU - '!wQqrqwOipOOWALxJNe:matrix.org': [ - { - id: "youtube", - url: "https://www.youtube.com/embed/ZfkwW4GgAiU?controls=0&enablejsapi=1&iv_load_policy=3&modestbranding=1&playsinline=1&autoplay=1", - name: "Live stream - ChatGirl86", - }, - { - id: "thermometer", - url: "http://10.9.64.55:8000/index.html", - name: "Tip Me!!! -- Send me cash $$$", - }, - ], - // Game room - https://www.youtube.com/watch?v=Dm2Ma1dOFO4 - '!dYSCwtVljhTdBlgNxq:matrix.org': [ - { - id: "youtube", - url: "https://www.youtube.com/embed/Dm2Ma1dOFO4?controls=0&enablejsapi=1&iv_load_policy=3&modestbranding=1&playsinline=1&autoplay=1", - name: "Live stream - Overwatch Balle Royale", - }, - { - id: "thermometer", - url: "http://10.9.64.55:8000/index.html", - name: "Tip Me!!! -- Send me cash $$$", - }, - ], - // Game room - !BLQjREzUgbtIsgrvRn:matrix.org - '!BLQjREzUgbtIsgrvRn:matrix.org': [ - { - id: "etherpad", - url: "http://10.9.64.55:8000/etherpad.html", - name: "Etherpad", - }, - ], -}; +const dis = require('../../../dispatcher'); module.exports = React.createClass({ displayName: 'AppsDrawer', propTypes: { + room: React.PropTypes.object.isRequired, + }, + + componentWillMount: function() { + MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents); }, componentDidMount: function() { }, - initAppConfig: function(appConfig) { - console.log("App props: ", this.props); - appConfig = appConfig.map( - (app, index, arr) => { - switch(app.id) { - case 'etherpad': - app.url = app.url + '?userName=' + this.props.userId + - '&padId=' + this.props.room.roomId; - break; - } + componentWillUnmount: function() { + if (MatrixClientPeg.get()) { + MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents); + } + }, - return app; - }); - return appConfig; + _initAppConfig: function(appId, app) { + console.log("App props: ", this.props); + app.id = appId; + app.name = app.type; + + switch(app.type) { + case 'etherpad': + app.url = app.url + '?userName=' + this.props.userId + + '&padId=' + this.props.room.roomId; + break; + case 'jitsi': { + const user = MatrixClientPeg.get().getUser(this.props.userId); + app.url = app.url + + '?confId=' + app.data.confId + + '&displayName=' + encodeURIComponent(user.displayName) + + '&avatarUrl=' + encodeURIComponent(MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl)) + + '&email=' + encodeURIComponent(this.props.userId) + + '&isAudioConf=' + app.data.isAudioConf; + + app.name += ' - ' + app.data.confId; + break; + } + } + + return app; }, getInitialState: function() { - for (const key in roomWidgetConfig) { - if(key == this.props.room.roomId) { - return { - apps: this.initAppConfig(roomWidgetConfig[key]), - }; - } - } return { - apps: [], + apps: this._getApps(), }; }, + onRoomStateEvents: function(ev, state) { + if (ev.getRoomId() !== this.props.room.roomId || ev.getType() !== 'im.vector.modular.widgets') { + return; + } + this._updateApps(); + }, + + _getApps: function() { + const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets', ''); + if (!appsStateEvents) { + return []; + } + const appsStateEvent = appsStateEvents.getContent(); + if (Object.keys(appsStateEvent).length < 1) { + return []; + } + + return Object.keys(appsStateEvent).map((appId) => { + return this._initAppConfig(appId, appsStateEvent[appId]); + }); + }, + + _updateApps: function() { + const apps = this._getApps(); + if (apps.length < 1) { + dis.dispatch({ + action: 'appsDrawer', + show: false, + }); + } + this.setState({ + apps: this._getApps(), + }); + }, + onClickAddWidget: function() { Modal.createDialog(AddAppDialog, { onFinished: (proceed, reason) => { @@ -131,7 +124,7 @@ module.exports = React.createClass({ render: function() { const apps = this.state.apps.map( (app, index, arr) => Date: Tue, 13 Jun 2017 15:31:37 +0200 Subject: [PATCH 055/491] AddAppDialog: Support adding apps to room state --- src/components/views/dialogs/AddAppDialog.js | 9 +++- src/components/views/elements/AppIconTile.js | 11 +++- src/components/views/rooms/AppsDrawer.js | 55 ++++++++++++++++++-- 3 files changed, 69 insertions(+), 6 deletions(-) diff --git a/src/components/views/dialogs/AddAppDialog.js b/src/components/views/dialogs/AddAppDialog.js index 6d722365e8..49e16820d6 100644 --- a/src/components/views/dialogs/AddAppDialog.js +++ b/src/components/views/dialogs/AddAppDialog.js @@ -46,10 +46,14 @@ export default React.createClass({ onFormSubmit: function(ev) { ev.preventDefault(); - this.props.onFinished(true, this.state.value); + this.props.onFinished(true, 'custom', this.state.value); return false; }, + onTileClick: function(value) { + this.props.onFinished(true, value, null); + }, + render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const appCards = ModularWidgets.widgetTypes.map((widgetType, index) => @@ -58,7 +62,8 @@ export default React.createClass({ type={widgetType.type} icon={widgetType.icon} name={widgetType.name} - description={widgetType.description}/>, + description={widgetType.description} + onClick={this.onTileClick}/>, ); return ( diff --git a/src/components/views/elements/AppIconTile.js b/src/components/views/elements/AppIconTile.js index 282a33743c..32fcd74111 100644 --- a/src/components/views/elements/AppIconTile.js +++ b/src/components/views/elements/AppIconTile.js @@ -18,6 +18,14 @@ limitations under the License. import React from 'react'; class AppIconTile extends React.Component { + constructor(props) { + super(props); + this._onTileClick = this._onTileClick.bind(this); + } + + _onTileClick(props) { + this.props.onClick(this.props.type); + } render() { const contentClasses = ['mx_AppIconTile']; @@ -26,7 +34,7 @@ class AppIconTile extends React.Component { // } return ( -
+
{this.props.name}
@@ -44,6 +52,7 @@ AppIconTile.propTypes = { icon: React.PropTypes.string.isRequired, name: React.PropTypes.string.isRequired, description: React.PropTypes.string.isRequired, + onClick: React.PropTypes.func.isRequired, }; export default AppIconTile; diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index d2b6cf8b21..1dee5dacf8 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -113,10 +113,59 @@ module.exports = React.createClass({ onClickAddWidget: function() { Modal.createDialog(AddAppDialog, { - onFinished: (proceed, reason) => { - if (!proceed) return; + onFinished: (proceed, type, value) => { + if (!proceed || !type) return; + if (type === 'custom' && !value) return; - this.state.apps.push(); + const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets', ''); + let appsStateEvent = {}; + if (appsStateEvents) { + appsStateEvent = appsStateEvents.getContent(); + } + + if (appsStateEvent[type]) { + return; + } + + switch (type) { + case 'etherpad': + appsStateEvent.etherpad = { + type: type, + url: 'http://localhost:8000/etherpad.html', + }; + break; + case 'grafana': + appsStateEvent.grafana = { + type: type, + url: 'http://localhost:8000/grafana.html', + }; + break; + case 'jitsi': + appsStateEvent.videoConf = { + type: type, + url: 'http://localhost:8000/jitsi.html', + data: { + confId: this.props.room.roomId.replace(/[^A-Za-z0-9]/g, '_') + Date.now(), + }, + }; + break; + case 'custom': + appsStateEvent.custom = { + type: type, + url: value, + }; + break; + default: + console.warn('Unsupported app type:', type); + return; + } + + MatrixClientPeg.get().sendStateEvent( + this.props.room.roomId, + 'im.vector.modular.widgets', + appsStateEvent, + '', + ); }, }); }, From bcb2f8408b80e1e9898e27e80ea77f2d99a51142 Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Tue, 13 Jun 2017 15:32:40 +0200 Subject: [PATCH 056/491] AppTile: Fix typo in property name --- src/components/views/elements/AppTile.js | 2 +- src/components/views/rooms/AppsDrawer.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index a3713d2b96..90208cd548 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -53,7 +53,7 @@ export default React.createClass({ render: function() { return ( -
+
{this.props.name} diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 1dee5dacf8..504221f894 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -177,8 +177,8 @@ module.exports = React.createClass({ id={app.id} url={app.url} name={app.name} - fullWdith={arr.length<2 ? true : false} roomId={this.props.roomId} + fullWidth={arr.length<2 ? true : false} userId={this.props.userId} />); From b63edcb390e6bfafaaa6b1771f7b3ec319807134 Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Tue, 13 Jun 2017 15:33:17 +0200 Subject: [PATCH 057/491] AppTile: Support deletion of apps from room state --- src/components/views/elements/AppTile.js | 20 ++++++++++++++++++++ src/components/views/rooms/AppsDrawer.js | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 90208cd548..3f81ff5067 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -17,6 +17,7 @@ limitations under the License. 'use strict'; const React = require('react'); +const MatrixClientPeg = require('../../../MatrixClientPeg'); export default React.createClass({ displayName: 'AppTile', @@ -25,6 +26,7 @@ export default React.createClass({ id: React.PropTypes.string.isRequired, url: React.PropTypes.string.isRequired, name: React.PropTypes.string.isRequired, + room: React.PropTypes.object.isRequired, }, getDefaultProps: function() { @@ -49,6 +51,24 @@ export default React.createClass({ _onDeleteClick: function() { console.log("Delete widget %s", this.props.id); + const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets', ''); + if (!appsStateEvents) { + return; + } + const appsStateEvent = appsStateEvents.getContent(); + if (appsStateEvent[this.props.id]) { + delete appsStateEvent[this.props.id]; + MatrixClientPeg.get().sendStateEvent( + this.props.room.roomId, + 'im.vector.modular.widgets', + appsStateEvent, + '', + ).then(() => { + console.log('Deleted widget'); + }, (e) => { + console.error('Failed to delete widget', e); + }); + } }, render: function() { diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 504221f894..816b813da1 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -177,8 +177,8 @@ module.exports = React.createClass({ id={app.id} url={app.url} name={app.name} - roomId={this.props.roomId} fullWidth={arr.length<2 ? true : false} + room={this.props.room} userId={this.props.userId} />); From 5d898dd0984663e1ccfff4c983107c76397bb597 Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Tue, 13 Jun 2017 15:34:05 +0200 Subject: [PATCH 058/491] AuxPanel: Add type checking for userId and showApps properties --- src/components/views/rooms/AuxPanel.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/views/rooms/AuxPanel.js b/src/components/views/rooms/AuxPanel.js index efa4da141f..4958de263e 100644 --- a/src/components/views/rooms/AuxPanel.js +++ b/src/components/views/rooms/AuxPanel.js @@ -29,6 +29,8 @@ module.exports = React.createClass({ propTypes: { // js-sdk room object room: React.PropTypes.object.isRequired, + userId: React.PropTypes.string.isRequired, + showApps: React.PropTypes.bool, // Conference Handler implementation conferenceHandler: React.PropTypes.object, From 91eabbba604faed2e062ff777d71c12763c2d9de Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Tue, 13 Jun 2017 15:35:13 +0200 Subject: [PATCH 059/491] MessageComposer: Trigger Jitsi app from call buttons --- src/components/structures/ModularWidgets.js | 6 +++ src/components/views/elements/AppTile.js | 2 +- src/components/views/rooms/MessageComposer.js | 40 ++++++++++++++----- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/components/structures/ModularWidgets.js b/src/components/structures/ModularWidgets.js index 4e59fd9cfd..b459241948 100644 --- a/src/components/structures/ModularWidgets.js +++ b/src/components/structures/ModularWidgets.js @@ -12,6 +12,12 @@ class ModularWidgets { name: 'Grafana', description: 'Graph and monitor all the things!', }, + { + type: 'jitsi', + icon: 'http://localhost:8000/static/jitsi.png', + name: 'jitsi', + description: 'Jitsi video conference', + }, { type: 'custom', icon: 'http://localhost:8000/static/blocks.png', diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 3f81ff5067..3bf99dbddd 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -94,7 +94,7 @@ export default React.createClass({
- +
); diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 63f980304e..24254d989b 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -153,21 +153,41 @@ export default class MessageComposer extends React.Component { }); } - onCallClick(ev) { - console.warn("Call but clicked!"); + _startCallApp(isAudioConf) { dis.dispatch({ - action: 'place_call', - type: ev.shiftKey ? "screensharing" : "video", - room_id: this.props.room.roomId, + action: 'appsDrawer', + show: true, }); + + const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets', ''); + let appsStateEvent = {}; + if (appsStateEvents) { + appsStateEvent = appsStateEvents.getContent(); + } + if (!appsStateEvent.videoConf) { + appsStateEvent.videoConf = { + type: 'jitsi', + url: 'http://localhost:8000/jitsi.html', + data: { + confId: this.props.room.roomId.replace(/[^A-Za-z0-9]/g, '_') + Date.now(), + isAudioConf: isAudioConf, + }, + }; + MatrixClientPeg.get().sendStateEvent( + this.props.room.roomId, + 'im.vector.modular.widgets', + appsStateEvent, + '', + ).then(() => console.log('Sent state'), (e) => console.error(e)); + } + } + + onCallClick(ev) { + this._startCallApp(false); } onVoiceCallClick(ev) { - dis.dispatch({ - action: 'place_call', - type: 'voice', - room_id: this.props.room.roomId, - }); + this._startCallApp(true); } onShowAppsClick(ev) { From 880e7149f345decd0f2c9ded04b3565ccfdfda4e Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Wed, 14 Jun 2017 13:05:43 +0200 Subject: [PATCH 060/491] ModularWidgets: Add a quick VR demo widget --- src/components/structures/ModularWidgets.js | 6 ++++++ src/components/views/rooms/AppsDrawer.js | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/src/components/structures/ModularWidgets.js b/src/components/structures/ModularWidgets.js index b459241948..314d273103 100644 --- a/src/components/structures/ModularWidgets.js +++ b/src/components/structures/ModularWidgets.js @@ -18,6 +18,12 @@ class ModularWidgets { name: 'jitsi', description: 'Jitsi video conference', }, + { + type: 'vrdemo', + icon: 'http://localhost:8000/static/jitsi.png', + name: 'vrdemo', + description: 'Matrix VR Demo', + }, { type: 'custom', icon: 'http://localhost:8000/static/blocks.png', diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 816b813da1..1d738fb9d2 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -65,6 +65,9 @@ module.exports = React.createClass({ app.name += ' - ' + app.data.confId; break; } + case 'vrdemo': + app.name = 'Matrix VR Demo'; + break; } return app; @@ -149,6 +152,12 @@ module.exports = React.createClass({ }, }; break; + case 'vrdemo': + appsStateEvent.vrDemo = { + type: type, + url: 'http://localhost:8000/vrdemo.html', + }; + break; case 'custom': appsStateEvent.custom = { type: type, From 9c8ab2691b1cdc3f3c061bd1b049b208f590fd2b Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Wed, 14 Jun 2017 13:26:43 +0200 Subject: [PATCH 061/491] AppsDrawer: Only append queryParams once --- src/components/views/rooms/AppsDrawer.js | 29 ++++++++++++++---------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 1d738fb9d2..a917dde5f6 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -50,13 +50,12 @@ module.exports = React.createClass({ switch(app.type) { case 'etherpad': - app.url = app.url + '?userName=' + this.props.userId + + app.queryParams = '?userName=' + this.props.userId + '&padId=' + this.props.room.roomId; break; case 'jitsi': { const user = MatrixClientPeg.get().getUser(this.props.userId); - app.url = app.url + - '?confId=' + app.data.confId + + app.queryParams = '?confId=' + app.data.confId + '&displayName=' + encodeURIComponent(user.displayName) + '&avatarUrl=' + encodeURIComponent(MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl)) + '&email=' + encodeURIComponent(this.props.userId) + @@ -181,15 +180,21 @@ module.exports = React.createClass({ render: function() { const apps = this.state.apps.map( - (app, index, arr) => ); + (app, index, arr) => { + let appUrl = app.url; + if (app.queryParams) { + appUrl += app.queryParams; + } + return ; + }); const addWidget = this.state.apps && this.state.apps.length < 2 && (
Date: Wed, 14 Jun 2017 13:27:15 +0200 Subject: [PATCH 062/491] AppsDrawer: Generate room alias for vrdemo --- src/components/views/rooms/AppsDrawer.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index a917dde5f6..c132b395cd 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -65,7 +65,8 @@ module.exports = React.createClass({ break; } case 'vrdemo': - app.name = 'Matrix VR Demo'; + app.name = 'Matrix VR Demo - ' + app.data.roomAlias; + app.queryParams = '?roomAlias=' + encodeURIComponent(app.data.roomAlias); break; } @@ -155,6 +156,9 @@ module.exports = React.createClass({ appsStateEvent.vrDemo = { type: type, url: 'http://localhost:8000/vrdemo.html', + data: { + roomAlias: '#vrvc' + this.props.room.roomId.replace(/[^A-Za-z0-9]/g, '_') + Date.now(), + }, }; break; case 'custom': From 5f020423bc89f1c82a9efb7511827f6c46966d8d Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Wed, 14 Jun 2017 15:05:11 +0200 Subject: [PATCH 063/491] AddAppDialog: Put the submit button inside the form --- src/components/views/dialogs/AddAppDialog.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/dialogs/AddAppDialog.js b/src/components/views/dialogs/AddAppDialog.js index 49e16820d6..512570d9ad 100644 --- a/src/components/views/dialogs/AddAppDialog.js +++ b/src/components/views/dialogs/AddAppDialog.js @@ -80,10 +80,10 @@ export default React.createClass({ autoFocus={true} onChange={this.onValueChange} size="30" className="mx_SetAppURLDialog_input" /> +
+ +
-
- -
); From edb11d805e9f5e5038c12a3cb80f09990af61230 Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Wed, 14 Jun 2017 15:05:29 +0200 Subject: [PATCH 064/491] AppsDrawer: Open add app widget if opening empty drawer This felt much better than having to also click the add app widget button. --- src/components/views/rooms/AppsDrawer.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index c132b395cd..6daa4d98cf 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -35,6 +35,9 @@ module.exports = React.createClass({ }, componentDidMount: function() { + if (this.state.apps && this.state.apps.length < 1) { + this.onClickAddWidget(); + } }, componentWillUnmount: function() { From a0a4fe62d57c314871e5e85f9b8ce705296c9bd2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 15 Jun 2017 19:53:34 +0100 Subject: [PATCH 065/491] make hide joins/parts work for MELS and highlighted/permalink event Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/MessagePanel.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 3c0e486e22..a97041a712 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -241,12 +241,15 @@ module.exports = React.createClass({ // TODO: Implement granular (per-room) hide options _shouldShowEvent: function(mxEv) { - console.log("_shouldShowEvent " + mxEv.getId()); + console.log("_shouldShowEvent ", mxEv.getId(), mxEv); const EventTile = sdk.getComponent('rooms.EventTile'); if (!EventTile.haveTileForEvent(mxEv)) { return false; // no tile = no show } + // Always show highlighted event + if (this.props.highlightedEventId === mxEv.getId()) return true; + const isMemberEvent = mxEv.getType() === "m.room.member" && mxEv.getStateKey() !== undefined; if (!isMemberEvent) { return true; // bail early: all the checks below concern member events only @@ -365,7 +368,7 @@ module.exports = React.createClass({ let collapsedMxEv = this.props.events[i + 1]; // Ignore redacted member events - if (!EventTile.haveTileForEvent(collapsedMxEv)) { + if (!EventTile.haveTileForEvent(collapsedMxEv) || !this._shouldShowEvent(collapsedMxEv)) { continue; } From 4b34a2b169f2c2c3e572c63fbc734a4f4bc60ee0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 15 Jun 2017 19:57:48 +0100 Subject: [PATCH 066/491] modernize imports Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/MessagePanel.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index a97041a712..704ae228be 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -14,13 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -var React = require('react'); -var ReactDOM = require("react-dom"); -var dis = require("../../dispatcher"); -var sdk = require('../../index'); +import React from 'react'; +import ReactDOM from 'react-dom'; +import dis from "../../dispatcher"; +import sdk from '../../index'; -var MatrixClientPeg = require('../../MatrixClientPeg'); -const UserSettingsStore = require('../../UserSettingsStore'); +import MatrixClientPeg from '../../MatrixClientPeg'; +import UserSettingsStore from '../../UserSettingsStore'; const MILLIS_IN_DAY = 86400000; From c53fb084539630881245c0f7de3c0c29d1fbdf8c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 15 Jun 2017 20:02:17 +0100 Subject: [PATCH 067/491] pass user settings from above Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/MessagePanel.js | 6 ++++-- src/components/structures/TimelinePanel.js | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 704ae228be..8b9ada14e7 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -20,7 +20,6 @@ import dis from "../../dispatcher"; import sdk from '../../index'; import MatrixClientPeg from '../../MatrixClientPeg'; -import UserSettingsStore from '../../UserSettingsStore'; const MILLIS_IN_DAY = 86400000; @@ -94,6 +93,9 @@ module.exports = React.createClass({ // hide redacted events as per old behaviour hideRedactions: React.PropTypes.bool, + + // hide membership joins and parts + hideJoinLeaves: React.PropTypes.bool, }, componentWillMount: function() { @@ -273,7 +275,7 @@ module.exports = React.createClass({ // this only applies to joins/leaves not invites/kicks/bans const isJoinOrLeave = membership === "join" || (membership === "leave" && mxEv.getStateKey() === mxEv.getSender()); - const hideJoinLeavesGlobally = UserSettingsStore.getSyncedSetting("hideJoinLeaves", false); + const hideJoinLeavesGlobally = this.props.hideJoinLeaves; if (isJoinOrLeave && hideJoinLeavesGlobally) { return false; } diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 928e2405aa..9642887fd5 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -184,6 +184,9 @@ var TimelinePanel = React.createClass({ // hide redacted events as per old behaviour hideRedactions: syncedSettings.hideRedactions, + + // hide membership joins and leaves + hideJoinLeaves: syncedSettings.hideJoinLeaves, }; }, @@ -1122,6 +1125,7 @@ var TimelinePanel = React.createClass({ return (