From 2db53c228493311ad2fd044936af6f930f570270 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 19 Feb 2017 03:04:42 +0200 Subject: [PATCH 0001/1588] 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 0002/1588] 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 0003/1588] 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 0004/1588] 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 0005/1588] 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 0006/1588] 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 0007/1588] 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 0008/1588] 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 0009/1588] 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 0010/1588] 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 0011/1588] 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 0012/1588] 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 0013/1588] 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 0014/1588] 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 0015/1588] 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 0016/1588] 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 0017/1588] 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 0018/1588] 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 0019/1588] 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 0020/1588] 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 0021/1588] 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 0022/1588] 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 0023/1588] 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 b3fc1844e8537e1c922def161a441cf4e281fd4d Mon Sep 17 00:00:00 2001 From: Lieuwe Rooijakkers Date: Sat, 18 Mar 2017 11:43:35 +0100 Subject: [PATCH 0024/1588] don't show link preview when link is inside of a quote Signed-off-by: Lieuwe Rooijakkers --- src/components/views/messages/TextualBody.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index a625e63062..c493094cbe 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -131,7 +131,8 @@ module.exports = React.createClass({ links.push(node); } } - else if (node.tagName === "PRE" || node.tagName === "CODE") { + else if (node.tagName === "PRE" || node.tagName === "CODE" || + node.tagName === "BLOCKQUOTE") { continue; } else if (node.children && node.children.length) { From 69c3bd7f80ddc987cd45977ae26c66e3c0b9f1f1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 20 Mar 2017 12:13:21 +0000 Subject: [PATCH 0025/1588] Escape closes UserSettings Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/LoggedInView.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index c2243820cd..a8e75c0cdd 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -99,6 +99,17 @@ export default React.createClass({ var handled = false; switch (ev.keyCode) { + case KeyCode.ESCAPE: + + // Implemented this way so possible handling for other pages is neater + switch (this.props.page_type) { + case PageTypes.UserSettings: + this.props.onUserSettingsClose(); + handled = true; + break; + } + + break; case KeyCode.UP: case KeyCode.DOWN: if (ev.altKey) { From 6010350ce5d52b946217c6314a69a87fddef2d4b Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 6 Apr 2017 17:02:35 +0100 Subject: [PATCH 0026/1588] Implement power-level changes in timeline Fixes https://github.com/vector-im/riot-web/issues/266 --- src/TextForEvent.js | 55 +++++++++++++++++++++++++ src/components/views/rooms/EventTile.js | 1 + 2 files changed, 56 insertions(+) diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 3e1659f392..2560264346 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -17,6 +17,13 @@ limitations under the License. var MatrixClientPeg = require("./MatrixClientPeg"); var CallHandler = require("./CallHandler"); +const roles = { + undefined: 'Default', + 0: 'User', + 50: 'Moderator', + 100: 'Admin', +}; + function textForMemberEvent(ev) { // XXX: SYJS-16 "sender is sometimes null for join messages" var senderName = ev.sender ? ev.sender.name : ev.getSender(); @@ -182,6 +189,53 @@ function textForEncryptionEvent(event) { return senderName + " turned on end-to-end encryption (algorithm " + event.getContent().algorithm + ")"; } +function formatPowerLevel(level, roles, userDefault) { + if (roles[level]) { + return roles[level] + (level !== undefined ? ` (${level})` : ` (${userDefault})`); + } else { + return level; + } +} + +// Currently will only display a change if a user's power level is changed +function textForPowerEvent(event) { + const senderName = event.sender ? event.sender.name : event.getSender(); + if (!event.getPrevContent() || !event.getPrevContent().users) { + return ''; + } + const userDefault = event.getContent().users_default || 0; + // Construct set of userIds + let users = []; + Object.keys(event.getContent().users).forEach( + (userId) => { + if (users.indexOf(userId) === -1) users.push(userId); + } + ); + Object.keys(event.getPrevContent().users).forEach( + (userId) => { + if (users.indexOf(userId) === -1) users.push(userId); + } + ); + let diff = []; + users.forEach((userId) => { + // Previous power level + const from = event.getPrevContent().users[userId]; + // Current power level + const to = event.getContent().users[userId]; + if (to !== from) { + diff.push( + userId + + ' from ' + formatPowerLevel(from, roles, userDefault) + + ' to ' + formatPowerLevel(to, roles, userDefault) + ); + } + }); + if (!diff.length) { + return ''; + } + return senderName + ' changed the power level of ' + diff.join(', '); +} + var handlers = { 'm.room.message': textForMessageEvent, 'm.room.name': textForRoomNameEvent, @@ -193,6 +247,7 @@ var handlers = { 'm.room.third_party_invite': textForThreePidInviteEvent, 'm.room.history_visibility': textForHistoryVisibilityEvent, 'm.room.encryption': textForEncryptionEvent, + 'm.room.power_levels': textForPowerEvent, }; module.exports = { diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 8b8e52ae83..9df0499eb2 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -40,6 +40,7 @@ var eventTileTypes = { 'm.room.third_party_invite' : 'messages.TextualEvent', 'm.room.history_visibility' : 'messages.TextualEvent', 'm.room.encryption' : 'messages.TextualEvent', + 'm.room.power_levels' : 'messages.TextualEvent', }; var MAX_READ_AVATARS = 5; From 8b4836b60ef8dfd8852e982df4abd7bc1f715cf0 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 10 Apr 2017 10:09:26 +0100 Subject: [PATCH 0027/1588] Refactor roles into Roles.js So that the mapping between a numerical power level and a "role" are done in one place. PowerSelector.js has been modified to use the same mapping. --- src/Roles.js | 29 +++++++++++++++ src/TextForEvent.js | 19 ++-------- .../views/elements/PowerSelector.js | 37 +++++++++++-------- 3 files changed, 54 insertions(+), 31 deletions(-) create mode 100644 src/Roles.js diff --git a/src/Roles.js b/src/Roles.js new file mode 100644 index 0000000000..cef8670aad --- /dev/null +++ b/src/Roles.js @@ -0,0 +1,29 @@ +/* +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. +*/ +export const LEVEL_ROLE_MAP = { + undefined: 'Default', + 0: 'User', + 50: 'Moderator', + 100: 'Admin', +}; + +export function textualPowerLevel(level, userDefault) { + if (LEVEL_ROLE_MAP[level]) { + return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${userDefault})`); + } else { + return level; + } +} diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 2560264346..40d6a49998 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -17,12 +17,7 @@ limitations under the License. var MatrixClientPeg = require("./MatrixClientPeg"); var CallHandler = require("./CallHandler"); -const roles = { - undefined: 'Default', - 0: 'User', - 50: 'Moderator', - 100: 'Admin', -}; +import * as Roles from './Roles'; function textForMemberEvent(ev) { // XXX: SYJS-16 "sender is sometimes null for join messages" @@ -189,14 +184,6 @@ function textForEncryptionEvent(event) { return senderName + " turned on end-to-end encryption (algorithm " + event.getContent().algorithm + ")"; } -function formatPowerLevel(level, roles, userDefault) { - if (roles[level]) { - return roles[level] + (level !== undefined ? ` (${level})` : ` (${userDefault})`); - } else { - return level; - } -} - // Currently will only display a change if a user's power level is changed function textForPowerEvent(event) { const senderName = event.sender ? event.sender.name : event.getSender(); @@ -225,8 +212,8 @@ function textForPowerEvent(event) { if (to !== from) { diff.push( userId + - ' from ' + formatPowerLevel(from, roles, userDefault) + - ' to ' + formatPowerLevel(to, roles, userDefault) + ' from ' + Roles.textualPowerLevel(from, userDefault) + + ' to ' + Roles.textualPowerLevel(to, userDefault) ); } }); diff --git a/src/components/views/elements/PowerSelector.js b/src/components/views/elements/PowerSelector.js index c7bfd4eec1..5eec464ead 100644 --- a/src/components/views/elements/PowerSelector.js +++ b/src/components/views/elements/PowerSelector.js @@ -16,17 +16,12 @@ limitations under the License. 'use strict'; -var React = require('react'); - -var roles = { - 0: 'User', - 50: 'Moderator', - 100: 'Admin', -}; +import React from 'react'; +import * as Roles from '../../../Roles'; var reverseRoles = {}; -Object.keys(roles).forEach(function(key) { - reverseRoles[roles[key]] = key; +Object.keys(Roles.LEVEL_ROLE_MAP).forEach(function(key) { + reverseRoles[Roles.LEVEL_ROLE_MAP[key]] = key; }); module.exports = React.createClass({ @@ -49,7 +44,7 @@ module.exports = React.createClass({ getInitialState: function() { return { - custom: (roles[this.props.value] === undefined), + custom: (Roles.LEVEL_ROLE_MAP[this.props.value] === undefined), }; }, @@ -99,22 +94,34 @@ module.exports = React.createClass({ selectValue = "Custom"; } else { - selectValue = roles[this.props.value] || "Custom"; + selectValue = Roles.LEVEL_ROLE_MAP[this.props.value] || "Custom"; } var select; if (this.props.disabled) { select = { selectValue }; } else { + // Each level must have a definition in LEVEL_ROLE_MAP + const levels = [0, 50, 100]; + let options = levels.map((level) => { + return { + value: Roles.LEVEL_ROLE_MAP[level], + // Give a userDefault (users_default in the power event) of 0 but + // because level !== undefined, this should never be used. + text: Roles.textualPowerLevel(level, 0), + } + }); + options.push({ value: "Custom", text: "Custom level" }); + options = options.map((op) => { + return ; + }); + select = ; } From 5de71ef504f01c016be02940030de967af8dd84d Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 10 Apr 2017 12:07:39 +0100 Subject: [PATCH 0028/1588] unbreak in-app permalinks correctly --- src/linkify-matrix.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js index e085b1a27a..c8e20316a9 100644 --- a/src/linkify-matrix.js +++ b/src/linkify-matrix.js @@ -122,7 +122,7 @@ var escapeRegExp = function(string) { // anyone else really should be using matrix.to. matrixLinkify.VECTOR_URL_PATTERN = "^(?:https?:\/\/)?(?:" + escapeRegExp(window.location.host + window.location.pathname) + "|" - + "(?:www\\.)?(riot|vector)\\.im/(?:beta|staging|develop)/" + + "(?:www\\.)?(?:riot|vector)\\.im/(?:beta|staging|develop)/" + ")(#.*)"; matrixLinkify.MATRIXTO_URL_PATTERN = "^(?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/((#|@|!).*)"; From 1d836c7d02a6935313bfb05d94fc38ae05439480 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 12 Apr 2017 10:04:25 +0100 Subject: [PATCH 0029/1588] Back to js-sdk develop --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0a0a51fc0b..cb3cdfa63f 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.3", "lodash": "^4.13.1", - "matrix-js-sdk": "0.7.6", + "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop", "optimist": "^0.6.1", "q": "^1.4.1", "react": "^15.4.0", From 424aae6b91283edd1a4e25142cec48dacf3fb6ac Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 12 Apr 2017 15:04:38 +0100 Subject: [PATCH 0030/1588] Prevent the ghost and real RM tile from both appearing --- src/components/structures/MessagePanel.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 0f8d35f525..6ee308a5a7 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -386,9 +386,7 @@ module.exports = React.createClass({ ret.push(this._getReadMarkerTile(visible)); readMarkerVisible = visible; isVisibleReadMarker = visible; - } - - if (eventId == this.currentGhostEventId) { + } else if (eventId == this.currentGhostEventId) { // if we're showing an animation, continue to show it. ret.push(this._getReadMarkerGhostTile()); } else if (!isVisibleReadMarker && From 1c25ed89b01345da3af185bb1900dd7943c388aa Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 12 Apr 2017 15:05:39 +0100 Subject: [PATCH 0031/1588] Initial implementation of using new RM API As detailed here https://docs.google.com/document/d/1UWqdS-e1sdwkLDUY0wA4gZyIkRp-ekjsLZ8k6g_Zvso/edit, the RM state is no longer kept locally, but rather server-side. The client now uses it's locally-calculated RM to update the server and receives server updates via the per-room account data. The sending of the RR has been bundled in to reduce traffic when sending both. In effect, whenever a RR is sent the RM is sent with it but using the new API. This uses a js-sdk change which has set to be finalised and so might change. --- src/components/structures/TimelinePanel.js | 55 +++++++++++++++------- 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 8cd820c284..4fbca4d40a 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -102,9 +102,6 @@ var TimelinePanel = React.createClass({ }, statics: { - // a map from room id to read marker event ID - roomReadMarkerMap: {}, - // a map from room id to read marker event timestamp roomReadMarkerTsMap: {}, }, @@ -121,10 +118,15 @@ var TimelinePanel = React.createClass({ getInitialState: function() { // XXX: we could track RM per TimelineSet rather than per Room. // but for now we just do it per room for simplicity. + let initialReadMarker = null; if (this.props.manageReadMarkers) { - var initialReadMarker = - TimelinePanel.roomReadMarkerMap[this.props.timelineSet.room.roomId] - || this._getCurrentReadReceipt(); + const readmarker = this.props.timelineSet.room.getAccountData('m.read_marker'); + if (readmarker){ + initialReadMarker = readmarker.getContent().marker; + } else { + initialReadMarker = this._getCurrentReadReceipt(); + } + console.info('Read marker initially', initialReadMarker); } return { @@ -180,6 +182,7 @@ var TimelinePanel = React.createClass({ MatrixClientPeg.get().on("Room.redaction", this.onRoomRedaction); MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt); MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated); + MatrixClientPeg.get().on("Room.accountData", this.onAccountData); this._initTimeline(this.props); }, @@ -466,6 +469,21 @@ var TimelinePanel = React.createClass({ this._reloadEvents(); }, + onAccountData: function(ev, room) { + if (this.unmounted) return; + + // ignore events for other rooms + if (room !== this.props.timelineSet.room) return; + + if (ev.getType() !== "m.read_marker") return; + + const markerEventId = ev.getContent().marker; + console.log('TimelinePanel: Read marker received from server', markerEventId); + + this.setState({ + readMarkerEventId: markerEventId, + }, this.props.onReadMarkerUpdated); + }, sendReadReceipt: function() { if (!this.refs.messagePanel) return; @@ -505,13 +523,23 @@ var TimelinePanel = React.createClass({ // we also remember the last read receipt we sent to avoid spamming the // same one at the server repeatedly - if (lastReadEventIndex > currentReadUpToEventIndex - && this.last_rr_sent_event_id != lastReadEvent.getId()) { + if ((lastReadEventIndex > currentReadUpToEventIndex && + this.last_rr_sent_event_id != lastReadEvent.getId()) || + this.last_rm_sent_event_id != this.state.readMarkerEventId) { + this.last_rr_sent_event_id = lastReadEvent.getId(); - MatrixClientPeg.get().sendReadReceipt(lastReadEvent).catch(() => { + this.last_rm_sent_event_id = this.state.readMarkerEventId; + + MatrixClientPeg.get().setRoomReadMarker( + this.props.timelineSet.room.roomId, + this.state.readMarkerEventId, + lastReadEvent + ).catch(() => { // it failed, so allow retries next time the user is active this.last_rr_sent_event_id = undefined; + this.last_rm_sent_event_id = undefined; }); + console.log('TimelinePanel: Read marker sent to the server ', this.state.readMarkerEventId, ); // do a quick-reset of our unreadNotificationCount to avoid having // to wait from the remote echo from the homeserver. @@ -956,16 +984,10 @@ var TimelinePanel = React.createClass({ _setReadMarker: function(eventId, eventTs, inhibitSetState) { var roomId = this.props.timelineSet.room.roomId; - if (TimelinePanel.roomReadMarkerMap[roomId] == eventId) { - // don't update the state (and cause a re-render) if there is - // no change to the RM. + if (eventId === this.state.readMarkerEventId) { return; } - // ideally we'd sync these via the server, but for now just stash them - // in a map. - TimelinePanel.roomReadMarkerMap[roomId] = eventId; - // in order to later figure out if the read marker is // above or below the visible timeline, we stash the timestamp. TimelinePanel.roomReadMarkerTsMap[roomId] = eventTs; @@ -974,6 +996,7 @@ var TimelinePanel = React.createClass({ return; } + // Do the local echo of the RM // run the render cycle before calling the callback, so that // getReadMarkerPosition() returns the right thing. this.setState({ From 249e42747b2435df6d431ee0170dd167133b4479 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 12 Apr 2017 15:09:56 +0100 Subject: [PATCH 0032/1588] Fix bug where `roomId` was expected to be a property on timelineSet --- src/components/structures/TimelinePanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 4fbca4d40a..18f52d1f07 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -734,7 +734,7 @@ var TimelinePanel = React.createClass({ // the messagePanel doesn't know where the read marker is. // if we know the timestamp of the read marker, make a guess based on that. - var rmTs = TimelinePanel.roomReadMarkerTsMap[this.props.timelineSet.roomId]; + const rmTs = TimelinePanel.roomReadMarkerTsMap[this.props.timelineSet.room.roomId]; if (rmTs && this.state.events.length > 0) { if (rmTs < this.state.events[0].getTs()) { return -1; From 9c9dc84f45e0b26df4c444babc42a614749c92c0 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 12 Apr 2017 15:12:37 +0100 Subject: [PATCH 0033/1588] Remove redundant setting of readMarkerEventId --- src/components/structures/TimelinePanel.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 18f52d1f07..9277c3f2b7 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -417,9 +417,10 @@ var TimelinePanel = React.createClass({ } else if(lastEv && this.getReadMarkerPosition() === 0) { // we know we're stuckAtBottom, so we can advance the RM // immediately, to save a later render cycle + + // This call will setState with readMarkerEventId = lastEv.getId() this._setReadMarker(lastEv.getId(), lastEv.getTs(), true); updatedState.readMarkerVisible = false; - updatedState.readMarkerEventId = lastEv.getId(); callback = this.props.onReadMarkerUpdated; } } From 1189368aab8cfb22c1895f8ce6c0d8a8fbe7ca0b Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 15 Apr 2017 00:30:48 +0100 Subject: [PATCH 0034/1588] add a class to remove evil blue outlines --- src/components/views/elements/AccessibleButton.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/views/elements/AccessibleButton.js b/src/components/views/elements/AccessibleButton.js index 2c23c0d208..ce58b6d5cf 100644 --- a/src/components/views/elements/AccessibleButton.js +++ b/src/components/views/elements/AccessibleButton.js @@ -32,6 +32,8 @@ export default function AccessibleButton(props) { }; restProps.tabIndex = restProps.tabIndex || "0"; restProps.role = "button"; + restProps.className = (restProps.className ? restProps.className + " " : "") + + "mx_AccessibleButton"; return React.createElement(element, restProps, children); } From 0a91511f05c2bf3cc7511cffd8f4487a366caa15 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 15 Apr 2017 12:13:29 +0100 Subject: [PATCH 0035/1588] cmd-k for quick search --- src/KeyCode.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/KeyCode.js b/src/KeyCode.js index c9cac01239..f164dbc15c 100644 --- a/src/KeyCode.js +++ b/src/KeyCode.js @@ -32,4 +32,5 @@ module.exports = { DELETE: 46, KEY_D: 68, KEY_E: 69, + KEY_K: 75, }; From 691639d1e06e8c7384dfedcc4514f024a77fc0af Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 15 Apr 2017 13:23:52 +0100 Subject: [PATCH 0036/1588] track RoomTile focus in RoomList, and stop the RoomList from updating during mouseOver --- src/components/views/rooms/RoomList.js | 62 ++++++++++++++++++++++++-- src/components/views/rooms/RoomTile.js | 11 ++++- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 59346d5f4d..0da741df19 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -63,12 +63,15 @@ module.exports = React.createClass({ var s = this.getRoomLists(); this.setState(s); + + this.focusedRoomTileRoomId = null; }, componentDidMount: function() { this.dispatcherRef = dis.register(this.onAction); // Initialise the stickyHeaders when the component is created this._updateStickyHeaders(true); + document.addEventListener('keydown', this._onKeyDown); }, componentDidUpdate: function() { @@ -100,6 +103,8 @@ module.exports = React.createClass({ // Force an update because the notif count state is too deep to cause // an update. This forces the local echo of reading notifs to be // reflected by the RoomTiles. + // + // FIXME: we should surely just be refreshing the right tile... this.forceUpdate(); break; } @@ -120,6 +125,8 @@ module.exports = React.createClass({ } // cancel any pending calls to the rate_limited_funcs this._delayedRefreshRoomList.cancelPendingCall(); + document.removeEventListener('keydown', this._onKeyDown); + }, onRoom: function(room) { @@ -149,6 +156,35 @@ module.exports = React.createClass({ } }, + _onMouseOver: function(ev) { + this._lastMouseOverTs = Date.now(); + }, + + _onKeyDown: function(ev) { + if (!this.focusedRoomTileRoomId) return; + let handled = false; + + switch (ev.keyCode) { + case KeyCode.UP: + this._onMoveFocus(true); + handled = true; + break; + case KeyCode.DOWN: + this._onMoveFocus(false); + handled = true; + break; + } + + if (handled) { + ev.stopPropagation(); + ev.preventDefault(); + } + }, + + _onMoveFocus: function(up) { + + }, + onSubListHeaderClick: function(isHidden, scrollToPosition) { // The scroll area has expanded or contracted, so re-calculate sticky headers positions this._updateStickyHeaders(true, scrollToPosition); @@ -192,7 +228,15 @@ module.exports = React.createClass({ }, _delayedRefreshRoomList: new rate_limited_func(function() { - this.refreshRoomList(); + // if the mouse has been moving over the RoomList in the last 500ms + // then delay the refresh further to avoid bouncing around under the + // cursor + if (Date.now() - this._lastMouseOverTs > 500) { + this.refreshRoomList(); + } + else { + this._delayedRefreshRoomList(); + } }, 500), refreshRoomList: function() { @@ -207,7 +251,8 @@ module.exports = React.createClass({ // us re-rendering all the sublists every time anything changes anywhere // in the state of the client. this.setState(this.getRoomLists()); - this._lastRefreshRoomListTs = Date.now(); + + // this._lastRefreshRoomListTs = Date.now(); }, getRoomLists: function() { @@ -457,6 +502,10 @@ module.exports = React.createClass({ this.refs.gemscroll.forceUpdate(); }, + onRoomTileFocus: function(roomId) { + this.focusedRoomTileRoomId = roomId; + }, + render: function() { var RoomSubList = sdk.getComponent('structures.RoomSubList'); var self = this; @@ -464,7 +513,7 @@ module.exports = React.createClass({ return ( -
+
{ Object.keys(self.state.lists).map(function(tagName) { @@ -529,6 +582,7 @@ module.exports = React.createClass({ collapsed={ self.props.collapsed } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } + onRoomTileFocus={ self.onRoomTileFocus } onShowMoreRooms={ self.onShowMoreRooms } />; } @@ -545,6 +599,7 @@ module.exports = React.createClass({ collapsed={ self.props.collapsed } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } + onRoomTileFocus={ self.onRoomTileFocus } onShowMoreRooms={ self.onShowMoreRooms } />
diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 06b05e9299..cff5c2f623 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -35,6 +35,7 @@ module.exports = React.createClass({ connectDragSource: React.PropTypes.func, connectDropTarget: React.PropTypes.func, onClick: React.PropTypes.func, + onFocus: React.PropTypes.func, isDragging: React.PropTypes.bool, room: React.PropTypes.object.isRequired, @@ -104,6 +105,12 @@ module.exports = React.createClass({ } }, + onFocus: function() { + if (this.props.onFocus) { + this.props.onFocus(this.props.room.roomId); + } + }, + onMouseEnter: function() { this.setState( { hover : true }); this.badgeOnMouseEnter(); @@ -255,7 +262,9 @@ module.exports = React.createClass({ let ret = (
{ /* Only native elements can be wrapped in a DnD object. */} - +
From a0c498e8ba786543fa8efadd8d563af939299b11 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 17 Apr 2017 14:37:24 +0100 Subject: [PATCH 0037/1588] Make Download behaviour consistent with that of E2E (iframed) download butttons (ACTUALLY DOWNLOAD) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/messages/MFileBody.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js index 86aee28269..029a8a9fe4 100644 --- a/src/components/views/messages/MFileBody.js +++ b/src/components/views/messages/MFileBody.js @@ -346,7 +346,7 @@ module.exports = React.createClass({ return (
- + { fileName }
@@ -360,7 +360,7 @@ module.exports = React.createClass({ return (
- + Download {text} From 6f0c3b1c03f92f49a1f90f6edcdcc73283479401 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 17 Apr 2017 14:50:34 +0100 Subject: [PATCH 0038/1588] Pass file name (as name) to the ImageView modal Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/messages/MImageBody.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index ab163297d7..0b4bc6ecb9 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -56,6 +56,7 @@ module.exports = React.createClass({ const ImageView = sdk.getComponent("elements.ImageView"); const params = { src: httpUrl, + name: content.body && content.body.length > 0 ? content.body : 'Attachment', mxEvent: this.props.mxEvent, }; From da569c2c8d3b8ea031566f4b9b5bd4f40e5cc465 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 17 Apr 2017 20:58:43 +0100 Subject: [PATCH 0039/1588] add constantTimeDispatcher and use it for strategic refreshes. constantTimeDispatcher lets you poke a specific react component to do something without having to do any O(N) operations. This is useful if you have thousands of RoomTiles in a RoomSubList and want to just tell one of them to update, without either having to do a full comparison of this.props.list or have each and every RoomTile subscribe to a generic event from flux or node's eventemitter *UNTESTED* --- src/ConstantTimeDispatcher.js | 62 +++++++++++++++ src/components/structures/TimelinePanel.js | 3 + src/components/views/rooms/RoomList.js | 88 ++++++++++++++++------ src/components/views/rooms/RoomTile.js | 7 ++ 4 files changed, 136 insertions(+), 24 deletions(-) create mode 100644 src/ConstantTimeDispatcher.js diff --git a/src/ConstantTimeDispatcher.js b/src/ConstantTimeDispatcher.js new file mode 100644 index 0000000000..265ee11fd4 --- /dev/null +++ b/src/ConstantTimeDispatcher.js @@ -0,0 +1,62 @@ +/* +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. +*/ + +// singleton which dispatches invocations of a given type & argument +// rather than just a type (as per EventEmitter and Flux's dispatcher etc) +// +// This means you can have a single point which listens for an EventEmitter event +// and then dispatches out to one of thousands of RoomTiles (for instance) rather than +// having each RoomTile register for the EventEmitter event and having to +// iterate over all of them. +class ConstantTimeDispatcher { + constructor() { + // type -> arg -> [ listener(arg, params) ] + this.listeners = {}; + } + + register(type, arg, listener) { + if (!this.listeners[type]) this.listeners[type] = {}; + if (!this.listeners[type][arg]) this.listeners[type][arg] = []; + this.listeners[type][arg].push(listener); + } + + unregister(type, arg, listener) { + if (this.listeners[type] && this.listeners[type][arg]) { + var i = this.listeners[type][arg].indexOf(listener); + if (i > -1) { + this.listeners[type][arg].splice(i, 1); + } + } + else { + console.warn("Unregistering unrecognised listener (type=" + type + ", arg=" + arg + ")"); + } + } + + dispatch(type, arg, params) { + if (!this.listeners[type] || !this.listeners[type][arg]) { + console.warn("No registered listeners for dispatch (type=" + type + ", arg=" + arg + ")"); + return; + } + this.listeners[type][arg].forEach(listener=>{ + listener.call(arg, params); + }); + } +} + +if (!global.constantTimeDispatcher) { + global.constantTimeDispatcher = new ConstantTimeDispatcher(); +} +module.exports = global.constantTimeDispatcher; diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 8cd820c284..296565488c 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -523,6 +523,9 @@ var TimelinePanel = React.createClass({ this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0); dis.dispatch({ action: 'on_room_read', + payload: { + room: this.props.timelineSet.room + } }); } } diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 0da741df19..2a70f14724 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -28,6 +28,7 @@ var rate_limited_func = require('../../../ratelimitedfunc'); var Rooms = require('../../../Rooms'); import DMRoomMap from '../../../utils/DMRoomMap'; var Receipt = require('../../../utils/Receipt'); +var constantTimeDispatcher = require('../../../ConstantTimeDispatcher'); var HIDE_CONFERENCE_CHANS = true; @@ -57,13 +58,16 @@ module.exports = React.createClass({ cli.on("Room.name", this.onRoomName); cli.on("Room.tags", this.onRoomTags); cli.on("Room.receipt", this.onRoomReceipt); - cli.on("RoomState.events", this.onRoomStateEvents); + // cli.on("RoomState.events", this.onRoomStateEvents); cli.on("RoomMember.name", this.onRoomMemberName); cli.on("accountData", this.onAccountData); var s = this.getRoomLists(); this.setState(s); + // lookup for which lists a given roomId is currently in. + this.listsForRoomId = {}; + this.focusedRoomTileRoomId = null; }, @@ -100,12 +104,13 @@ module.exports = React.createClass({ } break; case 'on_room_read': - // Force an update because the notif count state is too deep to cause - // an update. This forces the local echo of reading notifs to be - // reflected by the RoomTiles. - // - // FIXME: we should surely just be refreshing the right tile... - this.forceUpdate(); + // poke the right RoomTile to refresh, using the constantTimeDispatcher + // to avoid each and every RoomTile registering to the 'on_room_read' event + // XXX: if we like the constantTimeDispatcher we might want to dispatch + // directly from TimelinePanel rather than needlessly bouncing via here. + constantTimeDispatcher.dispatch( + "RoomTile.refresh", payload.room.roomId, {} + ); break; } }, @@ -119,7 +124,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); MatrixClientPeg.get().removeListener("Room.tags", this.onRoomTags); MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt); - MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents); + // MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents); MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName); MatrixClientPeg.get().removeListener("accountData", this.onAccountData); } @@ -130,10 +135,14 @@ module.exports = React.createClass({ }, onRoom: function(room) { + // XXX: this happens rarely; ideally we should only update the correct + // sublists when it does (e.g. via a constantTimeDispatch to the right sublist) this._delayedRefreshRoomList(); }, onDeleteRoom: function(roomId) { + // XXX: this happens rarely; ideally we should only update the correct + // sublists when it does (e.g. via a constantTimeDispatch to the right sublist) this._delayedRefreshRoomList(); }, @@ -194,35 +203,60 @@ module.exports = React.createClass({ if (toStartOfTimeline) return; if (!room) return; if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return; - this._delayedRefreshRoomList(); + + // rather than regenerate our full roomlists, which is very heavy, we poke the + // correct sublists to just re-sort themselves. This isn't enormously reacty, + // but is much faster than the default react reconciler, or having to do voodoo + // with shouldComponentUpdate and a pleaseRefresh property or similar. + var lists = this.listsByRoomId[room.roomId]; + if (lists) { + lists.forEach(list=>{ + constantTimeDispatcher.dispatch("RoomSubList.sort", list, { room: room }); + }); + } }, onRoomReceipt: function(receiptEvent, room) { // because if we read a notification, it will affect notification count // only bother updating if there's a receipt from us if (Receipt.findReadReceiptFromUserId(receiptEvent, MatrixClientPeg.get().credentials.userId)) { - this._delayedRefreshRoomList(); + var lists = this.listsByRoomId[room.roomId]; + if (lists) { + lists.forEach(list=>{ + constantTimeDispatcher.dispatch( + "RoomSubList.refreshHeader", list, { room: room } + ); + }); + } } }, onRoomName: function(room) { - this._delayedRefreshRoomList(); + constantTimeDispatcher.dispatch( + "RoomTile.refresh", room.roomId, {} + ); }, onRoomTags: function(event, room) { + // XXX: this happens rarely; ideally we should only update the correct + // sublists when it does (e.g. via a constantTimeDispatch to the right sublist) this._delayedRefreshRoomList(); }, - onRoomStateEvents: function(ev, state) { - this._delayedRefreshRoomList(); - }, + // onRoomStateEvents: function(ev, state) { + // this._delayedRefreshRoomList(); + // }, onRoomMemberName: function(ev, member) { - this._delayedRefreshRoomList(); + constantTimeDispatcher.dispatch( + "RoomTile.refresh", member.room.roomId, {} + ); }, onAccountData: function(ev) { if (ev.getType() == 'm.direct') { + // XXX: this happens rarely; ideally we should only update the correct + // sublists when it does (e.g. via a constantTimeDispatch to the right sublist) this._delayedRefreshRoomList(); } }, @@ -244,12 +278,10 @@ module.exports = React.createClass({ // (!this._lastRefreshRoomListTs ? "-" : (Date.now() - this._lastRefreshRoomListTs)) // ); - // TODO: rather than bluntly regenerating and re-sorting everything - // every time we see any kind of room change from the JS SDK - // we could do incremental updates on our copy of the state - // based on the room which has actually changed. This would stop - // us re-rendering all the sublists every time anything changes anywhere - // in the state of the client. + // TODO: ideally we'd calculate this once at start, and then maintain + // any changes to it incrementally, updating the appropriate sublists + // as needed. + // Alternatively we'd do something magical with Immutable.js or similar. this.setState(this.getRoomLists()); // this._lastRefreshRoomListTs = Date.now(); @@ -266,18 +298,19 @@ module.exports = React.createClass({ s.lists["m.lowpriority"] = []; s.lists["im.vector.fake.archived"] = []; + this.listsForRoomId = {}; + const dmRoomMap = new DMRoomMap(MatrixClientPeg.get()); MatrixClientPeg.get().getRooms().forEach(function(room) { const me = room.getMember(MatrixClientPeg.get().credentials.userId); if (!me) return; - // console.log("room = " + room.name + ", me.membership = " + me.membership + // ", sender = " + me.events.member.getSender() + // ", target = " + me.events.member.getStateKey() + // ", prevMembership = " + me.events.member.getPrevContent().membership); - if (me.membership == "invite") { + self.listsForRoomId[room.roomId].push("im.vector.fake.invite"); s.lists["im.vector.fake.invite"].push(room); } else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, self.props.ConferenceHandler)) { @@ -288,23 +321,26 @@ module.exports = React.createClass({ { // Used to split rooms via tags var tagNames = Object.keys(room.tags); - if (tagNames.length) { for (var i = 0; i < tagNames.length; i++) { var tagName = tagNames[i]; s.lists[tagName] = s.lists[tagName] || []; s.lists[tagNames[i]].push(room); + self.listsForRoomId[room.roomId].push(tagNames[i]); } } else if (dmRoomMap.getUserIdForRoomId(room.roomId)) { // "Direct Message" rooms (that we're still in and that aren't otherwise tagged) + self.listsForRoomId[room.roomId].push("im.vector.fake.direct"); s.lists["im.vector.fake.direct"].push(room); } else { + self.listsForRoomId[room.roomId].push("im.vector.fake.recent"); s.lists["im.vector.fake.recent"].push(room); } } else if (me.membership === "leave") { + self.listsForRoomId[room.roomId].push("im.vector.fake.archived"); s.lists["im.vector.fake.archived"].push(room); } else { @@ -325,8 +361,10 @@ module.exports = React.createClass({ const me = room.getMember(MatrixClientPeg.get().credentials.userId); if (me && Rooms.looksLikeDirectMessageRoom(room, me)) { + self.listsForRoomId[room.roomId].push("im.vector.fake.direct"); s.lists["im.vector.fake.direct"].push(room); } else { + self.listsForRoomId[room.roomId].push("im.vector.fake.recent"); s.lists["im.vector.fake.recent"].push(room); } } @@ -343,6 +381,8 @@ module.exports = React.createClass({ newMDirectEvent[otherPerson.userId] = roomList; } + console.warn("Resetting room DM state to be " + JSON.stringify(newMDirectEvent)); + // if this fails, fine, we'll just do the same thing next time we get the room lists MatrixClientPeg.get().setAccountData('m.direct', newMDirectEvent).done(); } diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index cff5c2f623..ac682f710a 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -27,6 +27,7 @@ var RoomNotifs = require('../../../RoomNotifs'); var FormattingUtils = require('../../../utils/FormattingUtils'); import AccessibleButton from '../elements/AccessibleButton'; var UserSettingsStore = require('../../../UserSettingsStore'); +var constantTimeDispatcher = require('../../../ConstantTimeDispatcher'); module.exports = React.createClass({ displayName: 'RoomTile', @@ -89,16 +90,22 @@ module.exports = React.createClass({ }, componentWillMount: function() { + constantTimeDispatcher.register("RoomTile.refresh", this.props.room.roomId, this.onRefresh); MatrixClientPeg.get().on("accountData", this.onAccountData); }, componentWillUnmount: function() { + constantTimeDispatcher.unregister("RoomTile.refresh", this.props.room.roomId, this.onRefresh); var cli = MatrixClientPeg.get(); if (cli) { MatrixClientPeg.get().removeListener("accountData", this.onAccountData); } }, + onRefresh: function() { + this.forceUpdate(); + }, + onClick: function() { if (this.props.onClick) { this.props.onClick(this.props.room.roomId); From 9591ad31e6c95d7748072702c45d10bdb1c4d841 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 18 Apr 2017 02:43:29 +0100 Subject: [PATCH 0040/1588] fix bugs, experiment with focus pulling, make it vaguely work --- src/components/views/rooms/RoomList.js | 143 +++++++++++++++++++++++-- src/components/views/rooms/RoomTile.js | 4 +- 2 files changed, 136 insertions(+), 11 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 2a70f14724..cb692ff253 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -27,6 +27,7 @@ var sdk = require('../../../index'); var rate_limited_func = require('../../../ratelimitedfunc'); var Rooms = require('../../../Rooms'); import DMRoomMap from '../../../utils/DMRoomMap'; +import KeyCode from '../../../KeyCode'; var Receipt = require('../../../utils/Receipt'); var constantTimeDispatcher = require('../../../ConstantTimeDispatcher'); @@ -68,7 +69,13 @@ module.exports = React.createClass({ // lookup for which lists a given roomId is currently in. this.listsForRoomId = {}; - this.focusedRoomTileRoomId = null; + // order of the sublists + this.listOrder = []; + + // this.focusedRoomTileRoomId = null; + this.focusedElement = null; + // this.focusedPosition = null; + // this.focusMoving = false; }, componentDidMount: function() { @@ -170,7 +177,7 @@ module.exports = React.createClass({ }, _onKeyDown: function(ev) { - if (!this.focusedRoomTileRoomId) return; + if (!this.focusedElement) return; let handled = false; switch (ev.keyCode) { @@ -191,7 +198,61 @@ module.exports = React.createClass({ }, _onMoveFocus: function(up) { + // cheat and move focus by faking tab/shift-tab. This lets us do things + // like collapse/uncollapse room headers & truncated lists without having + // to reimplement the entirety of the keyboard navigation logic. + // + // this simply doens't work, as for security apparently you can't inject + // UI events any more - c.f. this note from + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent + // + // Note: manually firing an event does not generate the default action + // associated with that event. For example, manually firing a key event + // does not cause that letter to appear in a focused text input. In the + // case of UI events, this is important for security reasons, as it + // prevents scripts from simulating user actions that interact with the + // browser itself. +/* + var event = document.createEvent('Event'); + event.initEvent('keydown', true, true); + event.keyCode = 9; + event.shiftKey = up ? true : false; + document.dispatchEvent(event); +*/ + // alternatively, this is the beginning of moving the focus through the list, + // navigating the pure datastructure of the list contents, but doesn't let + // you navigate through other things +/* + this.focusMoving = true; + if (this.focusPosition) { + if (up) { + this.focusPosition.index++; + if (this.focusPosition.index > this.listsForRoomId[this.focusPosition.list].length) { + // move to the next sublist + } + } + else { + this.focusPosition.index--; + if (this.focusPosition.index < 0) { + // move to the previous sublist + } + } + } +*/ + // alternatively, we can just try to manually implementing the focus switch at the DOM level. + // ignores tabindex. + var element = this.focusedElement; + if (up) { + element = element.parentElement.previousElementSibling.firstElementChild; + } + else { + element = element.parentElement.nextElementSibling.firstElementChild; + } + + if (element) { + element.focus(); + } }, onSubListHeaderClick: function(isHidden, scrollToPosition) { @@ -208,19 +269,27 @@ module.exports = React.createClass({ // correct sublists to just re-sort themselves. This isn't enormously reacty, // but is much faster than the default react reconciler, or having to do voodoo // with shouldComponentUpdate and a pleaseRefresh property or similar. - var lists = this.listsByRoomId[room.roomId]; + var lists = this.listsForRoomId[room.roomId]; if (lists) { lists.forEach(list=>{ constantTimeDispatcher.dispatch("RoomSubList.sort", list, { room: room }); }); } + +/* + if (this.focusPosition && lists.indexOf(this.focusPosition.list) > -1) { + // if we're reordering the list which currently have focus, recalculate + // our focus offset + this.focusPosition = null; + } +*/ }, onRoomReceipt: function(receiptEvent, room) { // because if we read a notification, it will affect notification count // only bother updating if there's a receipt from us if (Receipt.findReadReceiptFromUserId(receiptEvent, MatrixClientPeg.get().credentials.userId)) { - var lists = this.listsByRoomId[room.roomId]; + var lists = this.listsForRoomId[room.roomId]; if (lists) { lists.forEach(list=>{ constantTimeDispatcher.dispatch( @@ -274,6 +343,12 @@ module.exports = React.createClass({ }, 500), refreshRoomList: function() { +/* + // if we're regenerating the list, then the chances are the contents + // or ordering is changing - forget our cached focus position + this.focusPosition = null; +*/ + // console.log("DEBUG: Refresh room list delta=%s ms", // (!this._lastRefreshRoomListTs ? "-" : (Date.now() - this._lastRefreshRoomListTs)) // ); @@ -299,16 +374,23 @@ module.exports = React.createClass({ s.lists["im.vector.fake.archived"] = []; this.listsForRoomId = {}; + var otherTagNames = {}; const dmRoomMap = new DMRoomMap(MatrixClientPeg.get()); MatrixClientPeg.get().getRooms().forEach(function(room) { const me = room.getMember(MatrixClientPeg.get().credentials.userId); if (!me) return; + // console.log("room = " + room.name + ", me.membership = " + me.membership + // ", sender = " + me.events.member.getSender() + // ", target = " + me.events.member.getStateKey() + // ", prevMembership = " + me.events.member.getPrevContent().membership); + + if (!self.listsForRoomId[room.roomId]) { + self.listsForRoomId[room.roomId] = []; + } + if (me.membership == "invite") { self.listsForRoomId[room.roomId].push("im.vector.fake.invite"); s.lists["im.vector.fake.invite"].push(room); @@ -325,8 +407,9 @@ module.exports = React.createClass({ for (var i = 0; i < tagNames.length; i++) { var tagName = tagNames[i]; s.lists[tagName] = s.lists[tagName] || []; - s.lists[tagNames[i]].push(room); - self.listsForRoomId[room.roomId].push(tagNames[i]); + s.lists[tagName].push(room); + self.listsForRoomId[room.roomId].push(tagName); + otherTagNames[tagName] = 1; } } else if (dmRoomMap.getUserIdForRoomId(room.roomId)) { @@ -391,6 +474,21 @@ module.exports = React.createClass({ // we actually apply the sorting to this when receiving the prop in RoomSubLists. + // we'll need this when we get to iterating through lists programatically - e.g. ctrl-shift-up/down +/* + this.listOrder = [ + "im.vector.fake.invite", + "m.favourite", + "im.vector.fake.recent", + "im.vector.fake.direct", + Object.keys(otherTagNames).filter(tagName=>{ + return (!tagName.match(/^m\.(favourite|lowpriority)$/)); + }).sort(), + "m.lowpriority", + "im.vector.fake.archived" + ]; +*/ + return s; }, @@ -542,8 +640,35 @@ module.exports = React.createClass({ this.refs.gemscroll.forceUpdate(); }, - onRoomTileFocus: function(roomId) { - this.focusedRoomTileRoomId = roomId; + onRoomTileFocus: function(roomId, event) { + // this.focusedRoomTileRoomId = roomId; + this.focusedElement = event ? event.target : null; + + /* + if (roomId && !this.focusPosition) { + var list = this.listsForRoomId[roomId]; + if (list) { + console.warn("Focused to room " + roomId + " not in a list?!"); + } + else { + this.focusPosition = { + list: list, + index: this.state.lists[list].findIndex(room=>{ + return room.roomId == roomId; + }), + }; + } + } + + if (!roomId) { + if (this.focusMoving) { + this.focusMoving = false; + } + else { + this.focusPosition = null; + } + } + */ }, render: function() { @@ -608,7 +733,7 @@ module.exports = React.createClass({ onRoomTileFocus={ self.onRoomTileFocus } onShowMoreRooms={ self.onShowMoreRooms } /> - { Object.keys(self.state.lists).map(function(tagName) { + { Object.keys(self.state.lists).sort().map(function(tagName) { if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { return Date: Tue, 18 Apr 2017 14:44:43 +0100 Subject: [PATCH 0041/1588] m.read_marker -> m.fully_read --- src/components/structures/TimelinePanel.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 9277c3f2b7..162c474a25 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -120,7 +120,7 @@ var TimelinePanel = React.createClass({ // but for now we just do it per room for simplicity. let initialReadMarker = null; if (this.props.manageReadMarkers) { - const readmarker = this.props.timelineSet.room.getAccountData('m.read_marker'); + const readmarker = this.props.timelineSet.room.getAccountData('m.fully_read'); if (readmarker){ initialReadMarker = readmarker.getContent().marker; } else { @@ -476,7 +476,7 @@ var TimelinePanel = React.createClass({ // ignore events for other rooms if (room !== this.props.timelineSet.room) return; - if (ev.getType() !== "m.read_marker") return; + if (ev.getType() !== "m.fully_read") return; const markerEventId = ev.getContent().marker; console.log('TimelinePanel: Read marker received from server', markerEventId); From d33afa99ab25f85b2932f7d9c9621347eabaa40c Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 18 Apr 2017 15:13:05 +0100 Subject: [PATCH 0042/1588] marker -> event_id --- src/components/structures/TimelinePanel.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 162c474a25..74cf549c4d 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -122,7 +122,7 @@ var TimelinePanel = React.createClass({ if (this.props.manageReadMarkers) { const readmarker = this.props.timelineSet.room.getAccountData('m.fully_read'); if (readmarker){ - initialReadMarker = readmarker.getContent().marker; + initialReadMarker = readmarker.getContent().event_id; } else { initialReadMarker = this._getCurrentReadReceipt(); } @@ -478,7 +478,7 @@ var TimelinePanel = React.createClass({ if (ev.getType() !== "m.fully_read") return; - const markerEventId = ev.getContent().marker; + const markerEventId = ev.getContent().event_id; console.log('TimelinePanel: Read marker received from server', markerEventId); this.setState({ @@ -1045,7 +1045,6 @@ var TimelinePanel = React.createClass({ // events when viewing historical messages, we get stuck in a loop // of paginating our way through the entire history of the room. var stickyBottom = !this._timelineWindow.canPaginate(EventTimeline.FORWARDS); - return (
; }, + _showSpoiler: function(event) { + const target = event.target; + const hidden = target.getAttribute('data-spoiler'); + + target.innerHTML = hidden; + + const range = document.createRange(); + range.selectNodeContents(target); + + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + }, + nameForMedium: function(medium) { if (medium == 'msisdn') return 'Phone'; return medium[0].toUpperCase() + medium.slice(1); @@ -958,6 +972,9 @@ module.exports = React.createClass({
Logged in as {this._me}
+
+ Access Token: <click to reveal> +
Homeserver is { MatrixClientPeg.get().getHomeserverUrl() }
From 015a4480e29feb6e7aa9545947b6a03faf7b7ce0 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 18 Apr 2017 22:36:54 +0100 Subject: [PATCH 0049/1588] oops, wire up Room.receipt again, and refresh roomtiles on Room.timeline --- src/components/views/rooms/RoomList.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 739c288598..64871d1c0f 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -56,6 +56,7 @@ module.exports = React.createClass({ cli.on("Room.timeline", this.onRoomTimeline); cli.on("Room.name", this.onRoomName); cli.on("Room.tags", this.onRoomTags); + cli.on("Room.receipt", this.onRoomReceipt); // cli.on("RoomState.events", this.onRoomStateEvents); cli.on("RoomMember.name", this.onRoomMemberName); cli.on("accountData", this.onAccountData); @@ -147,6 +148,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); MatrixClientPeg.get().removeListener("Room.tags", this.onRoomTags); + MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt); // MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents); MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName); MatrixClientPeg.get().removeListener("accountData", this.onAccountData); @@ -212,6 +214,11 @@ module.exports = React.createClass({ constantTimeDispatcher.dispatch("RoomSubList.sort", list, { room: room }); }); } + + // we have to explicitly hit the roomtile which just changed + constantTimeDispatcher.dispatch( + "RoomTile.refresh", room.roomId, {} + ); }, onRoomReceipt: function(receiptEvent, room) { @@ -226,6 +233,11 @@ module.exports = React.createClass({ ); }); } + + // we have to explicitly hit the roomtile which just changed + constantTimeDispatcher.dispatch( + "RoomTile.refresh", room.roomId, {} + ); } }, From 8389a67c758d4b9dc352b816eefdf388bed7d938 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 18 Apr 2017 22:54:30 +0100 Subject: [PATCH 0050/1588] we don't need RoomTile specific focus in the end --- src/components/views/rooms/RoomList.js | 11 ----------- src/components/views/rooms/RoomTile.js | 10 +--------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 64871d1c0f..25e19da770 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -575,10 +575,6 @@ module.exports = React.createClass({ this.refs.gemscroll.forceUpdate(); }, - onRoomTileFocus: function(roomId, event) { - this.focusedElement = event ? event.target : null; - }, - render: function() { var RoomSubList = sdk.getComponent('structures.RoomSubList'); var self = this; @@ -595,7 +591,6 @@ module.exports = React.createClass({ collapsed={ self.props.collapsed } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } - onRoomTileFocus={ self.onRoomTileFocus } onShowMoreRooms={ self.onShowMoreRooms } /> { Object.keys(self.state.lists).sort().map(function(tagName) { @@ -650,7 +642,6 @@ module.exports = React.createClass({ collapsed={ self.props.collapsed } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } - onRoomTileFocus={ self.onRoomTileFocus } onShowMoreRooms={ self.onShowMoreRooms } />; } @@ -666,7 +657,6 @@ module.exports = React.createClass({ collapsed={ self.props.collapsed } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } - onRoomTileFocus={ self.onRoomTileFocus } onShowMoreRooms={ self.onShowMoreRooms } />
diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index db997fff3e..f18df52eee 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -37,7 +37,6 @@ module.exports = React.createClass({ connectDragSource: React.PropTypes.func, connectDropTarget: React.PropTypes.func, onClick: React.PropTypes.func, - onFocus: React.PropTypes.func, isDragging: React.PropTypes.bool, room: React.PropTypes.object.isRequired, @@ -121,12 +120,6 @@ module.exports = React.createClass({ } }, - onFocus: function(event) { - if (this.props.onFocus) { - this.props.onFocus(this.props.room.roomId, event); - } - }, - onMouseEnter: function() { this.setState( { hover : true }); this.badgeOnMouseEnter(); @@ -279,8 +272,7 @@ module.exports = React.createClass({ let ret = (
{ /* Only native elements can be wrapped in a DnD object. */} + onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
From 093b9a0b52a3dfbee682f224e4fe6ca23565a38f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 18 Apr 2017 23:29:28 +0100 Subject: [PATCH 0051/1588] kick the roomtile on RoomState.members --- src/components/views/rooms/RoomList.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 25e19da770..e510de08a4 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -57,7 +57,7 @@ module.exports = React.createClass({ cli.on("Room.name", this.onRoomName); cli.on("Room.tags", this.onRoomTags); cli.on("Room.receipt", this.onRoomReceipt); - // cli.on("RoomState.events", this.onRoomStateEvents); + cli.on("RoomState.members", this.onRoomStateMember); cli.on("RoomMember.name", this.onRoomMemberName); cli.on("accountData", this.onAccountData); @@ -149,7 +149,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); MatrixClientPeg.get().removeListener("Room.tags", this.onRoomTags); MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt); - // MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents); + MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember); MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName); MatrixClientPeg.get().removeListener("accountData", this.onAccountData); } @@ -253,9 +253,11 @@ module.exports = React.createClass({ this._delayedRefreshRoomList(); }, - // onRoomStateEvents: function(ev, state) { - // this._delayedRefreshRoomList(); - // }, + onRoomStateMember: function(ev, state, member) { + constantTimeDispatcher.dispatch( + "RoomTile.refresh", member.roomId, {} + ); + }, onRoomMemberName: function(ev, member) { constantTimeDispatcher.dispatch( From abf2300c0d37a8785ff9813dca3972e9e7a090b5 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 19 Apr 2017 00:09:03 +0100 Subject: [PATCH 0052/1588] highlight invites correctly --- src/components/views/rooms/RoomTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index f18df52eee..31ffdf7e12 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -104,7 +104,7 @@ module.exports = React.createClass({ onRefresh: function(params) { this.setState({ unread: Unread.doesRoomHaveUnreadMessages(this.props.room), - highlight: this.props.room.getUnreadNotificationCount('highlight') > 0 || this.props.label === 'Invites', + highlight: this.props.room.getUnreadNotificationCount('highlight') > 0 || this.props.isInvite, }); }, From 4a9c16868249a6685e2219189fe2a7186d168362 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 19 Apr 2017 00:13:01 +0100 Subject: [PATCH 0053/1588] fix invite highlights --- src/components/views/rooms/RoomTile.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 31ffdf7e12..dc2d9a4b25 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -101,6 +101,10 @@ module.exports = React.createClass({ } }, + componentWillReceiveProps: function(nextProps) { + this.onRefresh(); + }, + onRefresh: function(params) { this.setState({ unread: Unread.doesRoomHaveUnreadMessages(this.props.room), From fb6252a16b6d324a7e7b9cf99c716d8eaf2050b7 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 19 Apr 2017 00:16:17 +0100 Subject: [PATCH 0054/1588] fix invite highlights take 3 --- src/components/views/rooms/RoomTile.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index dc2d9a4b25..1f6063e37c 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -90,6 +90,7 @@ module.exports = React.createClass({ constantTimeDispatcher.register("RoomTile.refresh", this.props.room.roomId, this.onRefresh); constantTimeDispatcher.register("RoomTile.select", this.props.room.roomId, this.onSelect); MatrixClientPeg.get().on("accountData", this.onAccountData); + this.onRefresh(); }, componentWillUnmount: function() { From 566a31524271cc50a76f7af25866468f0eb09911 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 19 Apr 2017 10:08:04 +0100 Subject: [PATCH 0055/1588] Initial commit on riot-web#3524 (login UI update) --- src/components/views/elements/Dropdown.js | 21 ++++--- src/components/views/login/CountryDropdown.js | 2 +- src/components/views/login/PasswordLogin.js | 59 +++++++++++++------ 3 files changed, 55 insertions(+), 27 deletions(-) diff --git a/src/components/views/elements/Dropdown.js b/src/components/views/elements/Dropdown.js index 3b34d3cac1..907d4b0905 100644 --- a/src/components/views/elements/Dropdown.js +++ b/src/components/views/elements/Dropdown.js @@ -249,7 +249,7 @@ export default class Dropdown extends React.Component { ); }); - if (!this.state.searchQuery) { + if (!this.state.searchQuery && this.props.searchEnabled) { options.push(
Type to search... @@ -267,16 +267,20 @@ export default class Dropdown extends React.Component { let menu; if (this.state.expanded) { - currentValue = ; + if (this.props.searchEnabled) { + currentValue = ; + } menu =
{this._getMenuOptions()}
; - } else { + } + + if (!currentValue) { const selectedChild = this.props.getShortOption ? this.props.getShortOption(this.props.value) : this.childrenByKey[this.props.value]; @@ -313,6 +317,7 @@ Dropdown.propTypes = { onOptionChange: React.PropTypes.func.isRequired, // Called when the value of the search field changes onSearchChange: React.PropTypes.func, + searchEnabled: React.PropTypes.boolean, // Function that, given the key of an option, returns // a node representing that option to be displayed in the // box itself as the currently-selected option (ie. as diff --git a/src/components/views/login/CountryDropdown.js b/src/components/views/login/CountryDropdown.js index 9729c9e23f..be1ed51b5e 100644 --- a/src/components/views/login/CountryDropdown.js +++ b/src/components/views/login/CountryDropdown.js @@ -111,7 +111,7 @@ export default class CountryDropdown extends React.Component { return {options} diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 61cb3da652..002de0c2ba 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -60,6 +60,7 @@ module.exports = React.createClass({displayName: 'PasswordLogin', password: this.props.initialPassword, phoneCountry: this.props.initialPhoneCountry, phoneNumber: this.props.initialPhoneNumber, + loginType: "mxid", }; }, @@ -88,6 +89,10 @@ module.exports = React.createClass({displayName: 'PasswordLogin', this.props.onUsernameChanged(ev.target.value); }, + onLoginTypeChange: function(loginType) { + this.setState({loginType: loginType}); + }, + onPhoneCountryChanged: function(country) { this.setState({phoneCountry: country}); this.props.onPhoneCountryChanged(country); @@ -120,28 +125,46 @@ module.exports = React.createClass({displayName: 'PasswordLogin', }); const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); - return ( -
-
+ const Dropdown = sdk.getComponent('elements.Dropdown'); + + const loginType = { + 'email': - or -
- - + placeholder="Email or user name" autoFocus />, + 'mxid': + , + 'phone':
+ + +
+ }[this.state.loginType]; + + return ( +
+ +
+ + + Matrix ID + Email + Phone +
-
+ {loginType} {this._passwordField = e;}} type="password" name="password" value={this.state.password} onChange={this.onPasswordChanged} From 81bdfe2126fe9375b937aa1ec39f7acc61e221b2 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 19 Apr 2017 10:14:57 +0100 Subject: [PATCH 0056/1588] Update to match renamed API --- src/components/structures/TimelinePanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 74cf549c4d..9dc1b2dead 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -531,7 +531,7 @@ var TimelinePanel = React.createClass({ this.last_rr_sent_event_id = lastReadEvent.getId(); this.last_rm_sent_event_id = this.state.readMarkerEventId; - MatrixClientPeg.get().setRoomReadMarker( + MatrixClientPeg.get().setRoomReadMarkers( this.props.timelineSet.room.roomId, this.state.readMarkerEventId, lastReadEvent From 28818b857acbd0505c66a418c7b56e6b95c395ad Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 19 Apr 2017 10:17:44 +0100 Subject: [PATCH 0057/1588] Remove log --- src/components/structures/TimelinePanel.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 9dc1b2dead..34f492c585 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -126,7 +126,6 @@ var TimelinePanel = React.createClass({ } else { initialReadMarker = this._getCurrentReadReceipt(); } - console.info('Read marker initially', initialReadMarker); } return { From e32f153573cc36fac793c2253cbb7d3485858615 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 19 Apr 2017 10:18:25 +0100 Subject: [PATCH 0058/1588] Remove Room.accountData listener on unmount --- src/components/structures/TimelinePanel.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 34f492c585..a9c063b2fa 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -249,6 +249,7 @@ var TimelinePanel = React.createClass({ client.removeListener("Room.redaction", this.onRoomRedaction); client.removeListener("Room.receipt", this.onRoomReceipt); client.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated); + client.removeListener("Room.accountData", this.onAccountData); } }, From 00cf5b59183252a2650de970566a97c83d2bc21d Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 19 Apr 2017 10:20:24 +0100 Subject: [PATCH 0059/1588] Revert change --- src/components/structures/TimelinePanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index a9c063b2fa..4657548a3c 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -418,9 +418,9 @@ var TimelinePanel = React.createClass({ // we know we're stuckAtBottom, so we can advance the RM // immediately, to save a later render cycle - // This call will setState with readMarkerEventId = lastEv.getId() this._setReadMarker(lastEv.getId(), lastEv.getTs(), true); updatedState.readMarkerVisible = false; + updatedState.readMarkerEventId = lastEv.getId(); callback = this.props.onReadMarkerUpdated; } } From a787ee848065adb134d7007ce3cbf15d4f14e35f Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 19 Apr 2017 10:20:53 +0100 Subject: [PATCH 0060/1588] Remove spammy log --- src/components/structures/TimelinePanel.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 4657548a3c..7d202b7a85 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -478,11 +478,8 @@ var TimelinePanel = React.createClass({ if (ev.getType() !== "m.fully_read") return; - const markerEventId = ev.getContent().event_id; - console.log('TimelinePanel: Read marker received from server', markerEventId); - this.setState({ - readMarkerEventId: markerEventId, + readMarkerEventId: ev.getContent().event_id, }, this.props.onReadMarkerUpdated); }, From 81bf2be13b96baff2796690d799f7444bb8262f6 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 19 Apr 2017 10:27:43 +0100 Subject: [PATCH 0061/1588] Make note of inconsistant roomReadMarkerTsMap This will become redundant when there is server support for directionality of the RM --- src/components/structures/TimelinePanel.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 7d202b7a85..5a52d57f17 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -478,6 +478,9 @@ var TimelinePanel = React.createClass({ if (ev.getType() !== "m.fully_read") return; + // XXX: roomReadMarkerTsMap not updated here so it is now inconsistent. Replace + // this mechanism of determining where the RM is relative to the view-port with + // one supported by the server (the client needs more than an event ID). this.setState({ readMarkerEventId: ev.getContent().event_id, }, this.props.onReadMarkerUpdated); From edeaef8c2f163ca8053ab817a37e49c0f142cbec Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 19 Apr 2017 10:28:38 +0100 Subject: [PATCH 0062/1588] Initialise last_rm_sent_event_id --- src/components/structures/TimelinePanel.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 5a52d57f17..92aeb7cc66 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -174,6 +174,7 @@ var TimelinePanel = React.createClass({ debuglog("TimelinePanel: mounting"); this.last_rr_sent_event_id = undefined; + this.last_rm_sent_event_id = undefined; this.dispatcherRef = dis.register(this.onAction); MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); From a4ba5f041c1f80d942984e6ed7fdc25ed022beea Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 19 Apr 2017 10:46:08 +0100 Subject: [PATCH 0063/1588] Remove log, reinstate comment --- src/components/structures/TimelinePanel.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 92aeb7cc66..787638f966 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -541,7 +541,6 @@ var TimelinePanel = React.createClass({ this.last_rr_sent_event_id = undefined; this.last_rm_sent_event_id = undefined; }); - console.log('TimelinePanel: Read marker sent to the server ', this.state.readMarkerEventId, ); // do a quick-reset of our unreadNotificationCount to avoid having // to wait from the remote echo from the homeserver. @@ -986,6 +985,8 @@ var TimelinePanel = React.createClass({ _setReadMarker: function(eventId, eventTs, inhibitSetState) { var roomId = this.props.timelineSet.room.roomId; + // don't update the state (and cause a re-render) if there is + // no change to the RM. if (eventId === this.state.readMarkerEventId) { return; } From 9f99224a1fee849426ca184e72eedba9c3626f32 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 19 Apr 2017 17:59:06 +0100 Subject: [PATCH 0064/1588] fix bugs from PR review --- src/components/views/rooms/RoomList.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index e510de08a4..3916261dda 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -75,6 +75,12 @@ module.exports = React.createClass({ this.dispatcherRef = dis.register(this.onAction); // Initialise the stickyHeaders when the component is created this._updateStickyHeaders(true); + + if (this.props.selectedRoom) { + constantTimeDispatcher.dispatch( + "RoomTile.select", this.props.selectedRoom, { selected: true } + ); + } }, componentWillReceiveProps: function(nextProps) { @@ -155,8 +161,6 @@ module.exports = React.createClass({ } // cancel any pending calls to the rate_limited_funcs this._delayedRefreshRoomList.cancelPendingCall(); - document.removeEventListener('keydown', this._onKeyDown); - }, onRoom: function(room) { From 8da07740d1efb3b3b0389b29eacae1e73c86a344 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 19 Apr 2017 23:34:29 +0100 Subject: [PATCH 0065/1588] bump react-gemini-scrollbar --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cb3cdfa63f..5c96a74f5b 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "react": "^15.4.0", "react-addons-css-transition-group": "15.3.2", "react-dom": "^15.4.0", - "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef", + "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#39d858c", "sanitize-html": "^1.11.1", "text-encoding-utf-8": "^1.0.1", "velocity-vector": "vector-im/velocity#059e3b2", From 90f526bdeba68c201fdd7535ace3fe23fc5034a7 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 20 Apr 2017 00:42:13 +0100 Subject: [PATCH 0066/1588] autofocus doesn't seem to work on this button --- src/components/views/dialogs/QuestionDialog.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/QuestionDialog.js b/src/components/views/dialogs/QuestionDialog.js index 6012541b94..8e20b0d2bc 100644 --- a/src/components/views/dialogs/QuestionDialog.js +++ b/src/components/views/dialogs/QuestionDialog.js @@ -47,6 +47,12 @@ export default React.createClass({ this.props.onFinished(false); }, + componentDidMount: function() { + if (this.props.focus) { + this.refs.button.focus(); + } + }, + render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const cancelButton = this.props.hasCancelButton ? ( @@ -63,7 +69,7 @@ export default React.createClass({ {this.props.description}
- {this.props.extraButtons} From 5a3b4b6a60bcc198e8d15d15c0e8e8499522854f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 20 Apr 2017 01:12:57 +0100 Subject: [PATCH 0067/1588] various bug fixes: don't redraw RoomList when the selectedRoom changes keep passing selectedRoom through to RoomTiles so they have correct initial state handle onAccountData at the RoomList, not RoomTile level Fix some typos --- src/components/views/rooms/RoomList.js | 26 ++++++++++++++++++-------- src/components/views/rooms/RoomTile.js | 21 +++++---------------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 3916261dda..979b14eaaf 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -41,6 +41,12 @@ module.exports = React.createClass({ searchFilter: React.PropTypes.string, }, + shouldComponentUpdate: function(nextProps, nextState) { + if (nextProps.collapsed !== this.props.collapsed) return true; + if (nextProps.searchFilter !== this.props.searchFilter) return true; + return false; + }, + getInitialState: function() { return { isLoadingLeftRooms: false, @@ -75,12 +81,6 @@ module.exports = React.createClass({ this.dispatcherRef = dis.register(this.onAction); // Initialise the stickyHeaders when the component is created this._updateStickyHeaders(true); - - if (this.props.selectedRoom) { - constantTimeDispatcher.dispatch( - "RoomTile.select", this.props.selectedRoom, { selected: true } - ); - } }, componentWillReceiveProps: function(nextProps) { @@ -98,7 +98,7 @@ module.exports = React.createClass({ } }, - componentDidUpdate: function() { + componentDidUpdate: function(prevProps, prevState) { // Reinitialise the stickyHeaders when the component is updated this._updateStickyHeaders(true); this._repositionIncomingCallBox(undefined, false); @@ -265,7 +265,7 @@ module.exports = React.createClass({ onRoomMemberName: function(ev, member) { constantTimeDispatcher.dispatch( - "RoomTile.refresh", member.room.roomId, {} + "RoomTile.refresh", member.roomId, {} ); }, @@ -275,6 +275,9 @@ module.exports = React.createClass({ // sublists when it does (e.g. via a constantTimeDispatch to the right sublist) this._delayedRefreshRoomList(); } + else if (ev.getType() == 'm.push_rules') { + this._delayedRefreshRoomList(); + } }, _delayedRefreshRoomList: new rate_limited_func(function() { @@ -595,6 +598,7 @@ module.exports = React.createClass({ order="recent" incomingCall={ self.state.incomingCall } collapsed={ self.props.collapsed } + selectedRoom={ self.props.selectedRoom } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } onShowMoreRooms={ self.onShowMoreRooms } /> @@ -607,6 +611,7 @@ module.exports = React.createClass({ order="manual" incomingCall={ self.state.incomingCall } collapsed={ self.props.collapsed } + selectedRoom={ self.props.selectedRoom } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } onShowMoreRooms={ self.onShowMoreRooms } /> @@ -619,6 +624,7 @@ module.exports = React.createClass({ order="recent" incomingCall={ self.state.incomingCall } collapsed={ self.props.collapsed } + selectedRoom={ self.props.selectedRoom } alwaysShowHeader={ true } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } @@ -631,6 +637,7 @@ module.exports = React.createClass({ order="recent" incomingCall={ self.state.incomingCall } collapsed={ self.props.collapsed } + selectedRoom={ self.props.selectedRoom } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } onShowMoreRooms={ self.onShowMoreRooms } /> @@ -646,6 +653,7 @@ module.exports = React.createClass({ order="manual" incomingCall={ self.state.incomingCall } collapsed={ self.props.collapsed } + selectedRoom={ self.props.selectedRoom } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } onShowMoreRooms={ self.onShowMoreRooms } />; @@ -661,6 +669,7 @@ module.exports = React.createClass({ order="recent" incomingCall={ self.state.incomingCall } collapsed={ self.props.collapsed } + selectedRoom={ self.props.selectedRoom } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } onShowMoreRooms={ self.onShowMoreRooms } /> @@ -670,6 +679,7 @@ module.exports = React.createClass({ editable={ false } order="recent" collapsed={ self.props.collapsed } + selectedRoom={ self.props.selectedRoom } alwaysShowHeader={ true } startAsHidden={ true } showSpinner={ self.state.isLoadingLeftRooms } diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 1f6063e37c..5d896e8beb 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -38,6 +38,7 @@ module.exports = React.createClass({ connectDropTarget: React.PropTypes.func, onClick: React.PropTypes.func, isDragging: React.PropTypes.bool, + selectedRoom: React.PropTypes.string, room: React.PropTypes.object.isRequired, collapsed: React.PropTypes.bool.isRequired, @@ -53,10 +54,11 @@ module.exports = React.createClass({ getInitialState: function() { return({ - hover : false, - badgeHover : false, + hover: false, + badgeHover: false, menuDisplayed: false, notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId), + selected: this.props.room ? (this.props.selectedRoom === this.props.room.roomId) : false, }); }, @@ -78,28 +80,15 @@ module.exports = React.createClass({ } }, - onAccountData: function(accountDataEvent) { - if (accountDataEvent.getType() == 'm.push_rules') { - this.setState({ - notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId), - }); - } - }, - componentWillMount: function() { constantTimeDispatcher.register("RoomTile.refresh", this.props.room.roomId, this.onRefresh); constantTimeDispatcher.register("RoomTile.select", this.props.room.roomId, this.onSelect); - MatrixClientPeg.get().on("accountData", this.onAccountData); - this.onRefresh(); + this.onRefresh(); }, componentWillUnmount: function() { constantTimeDispatcher.unregister("RoomTile.refresh", this.props.room.roomId, this.onRefresh); constantTimeDispatcher.unregister("RoomTile.select", this.props.room.roomId, this.onSelect); - var cli = MatrixClientPeg.get(); - if (cli) { - MatrixClientPeg.get().removeListener("accountData", this.onAccountData); - } }, componentWillReceiveProps: function(nextProps) { From e69ea68133bb01dfd2093ffc5644edef24fbed70 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 20 Apr 2017 13:53:36 +0100 Subject: [PATCH 0068/1588] unbreak stack overflow which fires on tests due to mocked timers --- src/components/views/rooms/RoomList.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 979b14eaaf..3d80c335ca 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -74,7 +74,11 @@ module.exports = React.createClass({ this.setState(s); // order of the sublists - this.listOrder = []; + //this.listOrder = []; + + // loop count to stop a stack overflow if the user keeps waggling the + // mouse for >30s in a row, or if running under mocha + this._delayedRefreshRoomListLoopCount = 0 }, componentDidMount: function() { @@ -284,10 +288,12 @@ module.exports = React.createClass({ // if the mouse has been moving over the RoomList in the last 500ms // then delay the refresh further to avoid bouncing around under the // cursor - if (Date.now() - this._lastMouseOverTs > 500) { + if (Date.now() - this._lastMouseOverTs > 500 || this._delayedRefreshRoomListLoopCount > 60) { this.refreshRoomList(); + this._delayedRefreshRoomListLoopCount = 0; } else { + this._delayedRefreshRoomListLoopCount++; this._delayedRefreshRoomList(); } }, 500), From 238f59dc87195164f0b7b58e1787fb2fd00b6a38 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 20 Apr 2017 14:16:45 +0100 Subject: [PATCH 0069/1588] return the event from RoomTile's onClick to distinguish clicks from keypresses --- src/components/views/rooms/RoomTile.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 5d896e8beb..3b37d4608f 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -108,9 +108,9 @@ module.exports = React.createClass({ }); }, - onClick: function() { + onClick: function(ev) { if (this.props.onClick) { - this.props.onClick(this.props.room.roomId); + this.props.onClick(this.props.room.roomId, ev); } }, From 67089cb5279c80afabdce4cdb0707f0183a1185a Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 20 Apr 2017 14:34:59 +0100 Subject: [PATCH 0070/1588] If new RR-RM API not implemented, fallback to RR-only API --- src/components/structures/TimelinePanel.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 787638f966..e8774cec62 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -536,9 +536,16 @@ var TimelinePanel = React.createClass({ this.props.timelineSet.room.roomId, this.state.readMarkerEventId, lastReadEvent - ).catch(() => { + ).catch((e) => { + // /read_markers API is not implemented on this HS, fallback to just RR + if (e.errcode === 'M_UNRECOGNIZED') { + return MatrixClientPeg.get().sendReadReceipt( + lastReadEvent + ).catch(() => { + this.last_rr_sent_event_id = undefined; + }); + } // it failed, so allow retries next time the user is active - this.last_rr_sent_event_id = undefined; this.last_rm_sent_event_id = undefined; }); From 0d8d3c67106a3e84fd30de4016b9c852870f99b3 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 20 Apr 2017 15:15:20 +0100 Subject: [PATCH 0071/1588] HOW DID THIS EVER WORK? --- src/components/views/rooms/RoomList.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 3d80c335ca..c2778edc7c 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -600,6 +600,7 @@ module.exports = React.createClass({
Date: Thu, 20 Apr 2017 15:47:59 +0100 Subject: [PATCH 0072/1588] oops, actually refresh roomlist when its state changes! --- src/components/views/rooms/RoomList.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index c2778edc7c..a7dda40c6e 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -44,6 +44,9 @@ module.exports = React.createClass({ shouldComponentUpdate: function(nextProps, nextState) { if (nextProps.collapsed !== this.props.collapsed) return true; if (nextProps.searchFilter !== this.props.searchFilter) return true; + if (nextState.lists !== this.props.lists || + nextState.isLoadingLeftRooms !== this.isLoadingLeftRooms || + nextState.incomingCall !== this.incomingCall) return true; return false; }, From 3d507e98409af91c9b8cadec511f6e5d253bdbed Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Fri, 21 Apr 2017 00:05:52 +0200 Subject: [PATCH 0073/1588] (Room)?Avatar: Request 96x96 avatars on high DPI screens --- src/Avatar.js | 9 +++++---- src/components/views/avatars/RoomAvatar.js | 18 +++++++++++++----- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/Avatar.js b/src/Avatar.js index 76f5e55ff0..cb5e6965e3 100644 --- a/src/Avatar.js +++ b/src/Avatar.js @@ -22,8 +22,8 @@ module.exports = { avatarUrlForMember: function(member, width, height, resizeMethod) { var url = member.getAvatarUrl( MatrixClientPeg.get().getHomeserverUrl(), - width, - height, + window.devicePixelRatio > 1.2 ? 96 : width, + window.devicePixelRatio > 1.2 ? 96 : height, resizeMethod, false, false @@ -40,7 +40,9 @@ module.exports = { avatarUrlForUser: function(user, width, height, resizeMethod) { var url = ContentRepo.getHttpUriForMxc( MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl, - width, height, resizeMethod + window.devicePixelRatio > 1.2 ? 96 : width, + window.devicePixelRatio > 1.2 ? 96 : height, + resizeMethod ); if (!url || url.length === 0) { return null; @@ -57,4 +59,3 @@ module.exports = { return 'img/' + images[total % images.length] + '.png'; } }; - diff --git a/src/components/views/avatars/RoomAvatar.js b/src/components/views/avatars/RoomAvatar.js index bfa7575b0c..7ed7bfa9fa 100644 --- a/src/components/views/avatars/RoomAvatar.js +++ b/src/components/views/avatars/RoomAvatar.js @@ -59,7 +59,9 @@ module.exports = React.createClass({ ContentRepo.getHttpUriForMxc( MatrixClientPeg.get().getHomeserverUrl(), props.oobData.avatarUrl, - props.width, props.height, props.resizeMethod + window.devicePixelRatio > 1.2 ? 96 : props.width, + window.devicePixelRatio > 1.2 ? 96 : props.height, + props.resizeMethod ), // highest priority this.getRoomAvatarUrl(props), this.getOneToOneAvatar(props), @@ -74,7 +76,9 @@ module.exports = React.createClass({ return props.room.getAvatarUrl( MatrixClientPeg.get().getHomeserverUrl(), - props.width, props.height, props.resizeMethod, + window.devicePixelRatio > 1.2 ? 96 : props.width, + window.devicePixelRatio > 1.2 ? 96 : props.height, + props.resizeMethod, false ); }, @@ -103,14 +107,18 @@ module.exports = React.createClass({ } return theOtherGuy.getAvatarUrl( MatrixClientPeg.get().getHomeserverUrl(), - props.width, props.height, props.resizeMethod, + window.devicePixelRatio > 1.2 ? 96 : props.width, + window.devicePixelRatio > 1.2 ? 96 : props.height, + props.resizeMethod, false ); } else if (userIds.length == 1) { return mlist[userIds[0]].getAvatarUrl( MatrixClientPeg.get().getHomeserverUrl(), - props.width, props.height, props.resizeMethod, - false + window.devicePixelRatio > 1.2 ? 96 : props.width, + window.devicePixelRatio > 1.2 ? 96 : props.height, + props.resizeMethod, + false ); } else { return null; From be9b858193fe4dd47b18db4e657aad9dd1a07721 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 Apr 2017 01:06:00 +0100 Subject: [PATCH 0074/1588] focus on composer after jumping to bottom Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomView.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index b22d867acf..b09b101b8a 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1254,6 +1254,7 @@ module.exports = React.createClass({ // jump down to the bottom of this room, where new events are arriving jumpToLiveTimeline: function() { this.refs.messagePanel.jumpToLiveTimeline(); + dis.dispatch({action: 'focus_composer'}); }, // jump up to wherever our read marker is From bbd1f3433683dd64119406898de205b6deaa4762 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 Apr 2017 03:04:34 +0100 Subject: [PATCH 0075/1588] Prepend REACT_SDK_VERSION with a v to match riot-web version output Add simple helper to construct version/commit hash urls var -> let/const and prepend olmVersionString with v for same reason for both matrix-react-sdk and riot-web, if unknown/local don't do anything else try to create a link to the commit hash/tag name Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/UserSettings.js | 26 ++++++++++++++++------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 892865fdf9..881817acab 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -31,10 +31,14 @@ var SdkConfig = require('../../SdkConfig'); import AccessibleButton from '../views/elements/AccessibleButton'; // if this looks like a release, use the 'version' from package.json; else use -// the git sha. -const REACT_SDK_VERSION = - 'dist' in package_json ? package_json.version : package_json.gitHead || ""; +// the git sha. Prepend version with v, to look like riot-web version +const REACT_SDK_VERSION = 'dist' in package_json ? `v${package_json.version}` : package_json.gitHead || ''; +// Simple method to help prettify GH Release Tags and Commit Hashes. +const GHVersionUrl = function(repo, token) { + const uriTail = (token.startsWith('v') && token.includes('.')) ? `releases/tag/${token}` : `commit/${token}`; + return `https://github.com/${repo}/${uriTail}`; +} // Enumerate some simple 'flip a bit' UI settings (if any). // 'id' gives the key name in the im.vector.web.settings account data event @@ -880,12 +884,12 @@ module.exports = React.createClass({
); } - var olmVersion = MatrixClientPeg.get().olmVersion; + const olmVersion = MatrixClientPeg.get().olmVersion; // If the olmVersion is not defined then either crypto is disabled, or // we are using a version old version of olm. We assume the former. - var olmVersionString = ""; + let olmVersionString = ""; if (olmVersion !== undefined) { - olmVersionString = olmVersion[0] + "." + olmVersion[1] + "." + olmVersion[2]; + olmVersionString = `v${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}`; } return ( @@ -965,8 +969,14 @@ module.exports = React.createClass({ Identity Server is { MatrixClientPeg.get().getIdentityServerUrl() }
- matrix-react-sdk version: {REACT_SDK_VERSION}
- riot-web version: {this.state.vectorVersion !== null ? this.state.vectorVersion : 'unknown'}
+ matrix-react-sdk version: {(REACT_SDK_VERSION !== '') + ? {REACT_SDK_VERSION} + : REACT_SDK_VERSION + }
+ riot-web version: {(this.state.vectorVersion !== null) + ? {this.state.vectorVersion} + : 'unknown' + }
olm version: {olmVersionString}
From 9cd7914ea51dbfb60f8b84a80cb800282476d3e4 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 21 Apr 2017 11:37:08 +0100 Subject: [PATCH 0076/1588] Finishing off the first iteration on login UI This makes the following changes: - Improve CountryDropdown by allowing all countries to be displayed at once and using PNGs for performance (trading of quality - the pngs are scaled down from 32px to 25px) - "I want to sign in with" dropdown to select login method - MXID login field that suffixes HS domain (whether custom or matrix.org) and prefixes "@" - Email field which is secretly the same as the username field but with a different placeholder - No more login flickering when changing ServerConfig (!) fixes https://github.com/vector-im/riot-web/issues/1517 This implements most of the design in https://github.com/vector-im/riot-web/issues/3524 but neglects the phone number login: ![login_with_msisdn](https://cloud.githubusercontent.com/assets/1922197/24864469/30a921fc-1dfc-11e7-95d1-76f619da1402.png) This will be updated in another PR to implement desired things: - Country code visible once a country has been selected (propbably but as a prefix to the phone number input box. - Use square flags - Move CountryDropdown above phone input and make it show the full country name when not expanded - Auto-select country based on IP --- src/HtmlUtils.js | 14 +- src/components/structures/login/Login.js | 85 +++---- src/components/views/elements/Dropdown.js | 13 +- src/components/views/login/CountryDropdown.js | 8 +- src/components/views/login/PasswordLogin.js | 210 +++++++++++------- src/components/views/login/ServerConfig.js | 28 ++- 6 files changed, 207 insertions(+), 151 deletions(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index a8e20f5ec1..96934d205e 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -25,6 +25,9 @@ import emojione from 'emojione'; import classNames from 'classnames'; emojione.imagePathSVG = 'emojione/svg/'; +// Store PNG path for displaying many flags at once (for increased performance over SVG) +emojione.imagePathPNG = 'emojione/png/'; +// Use SVGs for emojis emojione.imageType = 'svg'; const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi"); @@ -64,16 +67,23 @@ export function unicodeToImage(str) { * emoji. * * @param alt {string} String to use for the image alt text + * @param useSvg {boolean} Whether to use SVG image src. If False, PNG will be used. * @param unicode {integer} One or more integers representing unicode characters * @returns A img node with the corresponding emoji */ -export function charactersToImageNode(alt, ...unicode) { +export function charactersToImageNode(alt, useSvg, ...unicode) { const fileName = unicode.map((u) => { return u.toString(16); }).join('-'); - return {alt}; + const path = useSvg ? emojione.imagePathSVG : emojione.imagePathPNG; + const fileType = useSvg ? 'svg' : 'png'; + return {alt}; } + export function stripParagraphs(html: string): string { const contentDiv = document.createElement('div'); contentDiv.innerHTML = html; diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 7e1a5f9d35..d9a7039686 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -17,13 +17,11 @@ limitations under the License. 'use strict'; -var React = require('react'); -var ReactDOM = require('react-dom'); -var sdk = require('../../../index'); -var Login = require("../../../Login"); -var PasswordLogin = require("../../views/login/PasswordLogin"); -var CasLogin = require("../../views/login/CasLogin"); -var ServerConfig = require("../../views/login/ServerConfig"); +import React from 'react'; +import ReactDOM from 'react-dom'; +import url from 'url'; +import sdk from '../../../index'; +import Login from '../../../Login'; /** * A wire component which glues together login UI components and Login logic @@ -67,6 +65,7 @@ module.exports = React.createClass({ username: "", phoneCountry: null, phoneNumber: "", + currentFlow: "m.login.password", }; }, @@ -129,23 +128,19 @@ module.exports = React.createClass({ this.setState({ phoneNumber: phoneNumber }); }, - onHsUrlChanged: function(newHsUrl) { + onServerConfigChange: function(config) { var self = this; - this.setState({ - enteredHomeserverUrl: newHsUrl, + let newState = { errorText: null, // reset err messages - }, function() { - self._initLoginLogic(newHsUrl); - }); - }, - - onIsUrlChanged: function(newIsUrl) { - var self = this; - this.setState({ - enteredIdentityServerUrl: newIsUrl, - errorText: null, // reset err messages - }, function() { - self._initLoginLogic(null, newIsUrl); + }; + if (config.hsUrl !== undefined) { + newState.enteredHomeserverUrl = config.hsUrl; + } + if (config.isUrl !== undefined) { + newState.enteredIdentityServerUrl = config.isUrl; + } + this.setState(newState, function() { + self._initLoginLogic(config.hsUrl || null, config.isUrl); }); }, @@ -161,25 +156,28 @@ module.exports = React.createClass({ }); this._loginLogic = loginLogic; - loginLogic.getFlows().then(function(flows) { - // old behaviour was to always use the first flow without presenting - // options. This works in most cases (we don't have a UI for multiple - // logins so let's skip that for now). - loginLogic.chooseFlow(0); - }, function(err) { - self._setStateFromError(err, false); - }).finally(function() { - self.setState({ - busy: false - }); - }); - this.setState({ enteredHomeserverUrl: hsUrl, enteredIdentityServerUrl: isUrl, busy: true, loginIncorrect: false, }); + + loginLogic.getFlows().then(function(flows) { + // old behaviour was to always use the first flow without presenting + // options. This works in most cases (we don't have a UI for multiple + // logins so let's skip that for now). + loginLogic.chooseFlow(0); + self.setState({ + currentFlow: self._getCurrentFlowStep(), + }); + }, function(err) { + self._setStateFromError(err, false); + }).finally(function() { + self.setState({ + busy: false, + }); + }); }, _getCurrentFlowStep: function() { @@ -231,6 +229,7 @@ module.exports = React.createClass({ componentForStep: function(step) { switch (step) { case 'm.login.password': + const PasswordLogin = sdk.getComponent('login.PasswordLogin'); return ( ); case 'm.login.cas': + const CasLogin = sdk.getComponent('login.CasLogin'); return ( ); @@ -262,10 +263,11 @@ module.exports = React.createClass({ }, render: function() { - var Loader = sdk.getComponent("elements.Spinner"); - var LoginHeader = sdk.getComponent("login.LoginHeader"); - var LoginFooter = sdk.getComponent("login.LoginFooter"); - var loader = this.state.busy ?
: null; + const Loader = sdk.getComponent("elements.Spinner"); + const LoginHeader = sdk.getComponent("login.LoginHeader"); + const LoginFooter = sdk.getComponent("login.LoginFooter"); + const ServerConfig = sdk.getComponent("login.ServerConfig"); + const loader = this.state.busy ?
: null; var loginAsGuestJsx; if (this.props.enableGuest) { @@ -291,15 +293,14 @@ module.exports = React.createClass({

Sign in { loader }

- { this.componentForStep(this._getCurrentFlowStep()) } + { this.componentForStep(this.state.currentFlow) }
{ this.state.errorText } diff --git a/src/components/views/elements/Dropdown.js b/src/components/views/elements/Dropdown.js index 907d4b0905..a9ecf5b669 100644 --- a/src/components/views/elements/Dropdown.js +++ b/src/components/views/elements/Dropdown.js @@ -248,13 +248,10 @@ export default class Dropdown extends React.Component { ); }); - - if (!this.state.searchQuery && this.props.searchEnabled) { - options.push( -
- Type to search... -
- ); + if (options.length === 0) { + return [
+ No results +
]; } return options; } @@ -317,7 +314,7 @@ Dropdown.propTypes = { onOptionChange: React.PropTypes.func.isRequired, // Called when the value of the search field changes onSearchChange: React.PropTypes.func, - searchEnabled: React.PropTypes.boolean, + searchEnabled: React.PropTypes.bool, // Function that, given the key of an option, returns // a node representing that option to be displayed in the // box itself as the currently-selected option (ie. as diff --git a/src/components/views/login/CountryDropdown.js b/src/components/views/login/CountryDropdown.js index be1ed51b5e..7f6b21650d 100644 --- a/src/components/views/login/CountryDropdown.js +++ b/src/components/views/login/CountryDropdown.js @@ -33,8 +33,6 @@ function countryMatchesSearchQuery(query, country) { return false; } -const MAX_DISPLAYED_ROWS = 2; - export default class CountryDropdown extends React.Component { constructor(props) { super(props); @@ -64,7 +62,7 @@ export default class CountryDropdown extends React.Component { // Unicode Regional Indicator Symbol letter 'A' const RIS_A = 0x1F1E6; const ASCII_A = 65; - return charactersToImageNode(iso2, + return charactersToImageNode(iso2, true, RIS_A + (iso2.charCodeAt(0) - ASCII_A), RIS_A + (iso2.charCodeAt(1) - ASCII_A), ); @@ -93,10 +91,6 @@ export default class CountryDropdown extends React.Component { displayedCountries = COUNTRIES; } - if (displayedCountries.length > MAX_DISPLAYED_ROWS) { - displayedCountries = displayedCountries.slice(0, MAX_DISPLAYED_ROWS); - } - const options = displayedCountries.map((country) => { return
{this._flagImgForIso2(country.iso2)} diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 002de0c2ba..fc063efbe9 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -25,56 +25,49 @@ import {field_input_incorrect} from '../../../UiEffects'; /** * A pure UI component which displays a username/password form. */ -module.exports = React.createClass({displayName: 'PasswordLogin', - propTypes: { - onSubmit: React.PropTypes.func.isRequired, // fn(username, password) - onForgotPasswordClick: React.PropTypes.func, // fn() - initialUsername: React.PropTypes.string, - initialPhoneCountry: React.PropTypes.string, - initialPhoneNumber: React.PropTypes.string, - initialPassword: React.PropTypes.string, - onUsernameChanged: React.PropTypes.func, - onPhoneCountryChanged: React.PropTypes.func, - onPhoneNumberChanged: React.PropTypes.func, - onPasswordChanged: React.PropTypes.func, - loginIncorrect: React.PropTypes.bool, - }, +class PasswordLogin extends React.Component { + static defaultProps = { + onUsernameChanged: function() {}, + onPasswordChanged: function() {}, + onPhoneCountryChanged: function() {}, + onPhoneNumberChanged: function() {}, + initialUsername: "", + initialPhoneCountry: "", + initialPhoneNumber: "", + initialPassword: "", + loginIncorrect: false, + hsDomain: "", + } - getDefaultProps: function() { - return { - onUsernameChanged: function() {}, - onPasswordChanged: function() {}, - onPhoneCountryChanged: function() {}, - onPhoneNumberChanged: function() {}, - initialUsername: "", - initialPhoneCountry: "", - initialPhoneNumber: "", - initialPassword: "", - loginIncorrect: false, - }; - }, - - getInitialState: function() { - return { + constructor(props) { + super(props); + this.state = { username: this.props.initialUsername, password: this.props.initialPassword, phoneCountry: this.props.initialPhoneCountry, phoneNumber: this.props.initialPhoneNumber, - loginType: "mxid", + loginType: PasswordLogin.LOGIN_FIELD_MXID, }; - }, - componentWillMount: function() { + this.onSubmitForm = this.onSubmitForm.bind(this); + this.onUsernameChanged = this.onUsernameChanged.bind(this); + this.onLoginTypeChange = this.onLoginTypeChange.bind(this); + this.onPhoneCountryChanged = this.onPhoneCountryChanged.bind(this); + this.onPhoneNumberChanged = this.onPhoneNumberChanged.bind(this); + this.onPasswordChanged = this.onPasswordChanged.bind(this); + } + + componentWillMount() { this._passwordField = null; - }, + } - componentWillReceiveProps: function(nextProps) { + componentWillReceiveProps(nextProps) { if (!this.props.loginIncorrect && nextProps.loginIncorrect) { field_input_incorrect(this._passwordField); } - }, + } - onSubmitForm: function(ev) { + onSubmitForm(ev) { ev.preventDefault(); this.props.onSubmit( this.state.username, @@ -82,33 +75,87 @@ module.exports = React.createClass({displayName: 'PasswordLogin', this.state.phoneNumber, this.state.password, ); - }, + } - onUsernameChanged: function(ev) { + onUsernameChanged(ev) { this.setState({username: ev.target.value}); this.props.onUsernameChanged(ev.target.value); - }, + } - onLoginTypeChange: function(loginType) { - this.setState({loginType: loginType}); - }, + onLoginTypeChange(loginType) { + this.setState({ + loginType: loginType, + username: "" // Reset because email and username use the same state + }); + } - onPhoneCountryChanged: function(country) { + onPhoneCountryChanged(country) { this.setState({phoneCountry: country}); this.props.onPhoneCountryChanged(country); - }, + } - onPhoneNumberChanged: function(ev) { + onPhoneNumberChanged(ev) { this.setState({phoneNumber: ev.target.value}); this.props.onPhoneNumberChanged(ev.target.value); - }, + } - onPasswordChanged: function(ev) { + onPasswordChanged(ev) { this.setState({password: ev.target.value}); this.props.onPasswordChanged(ev.target.value); - }, + } - render: function() { + renderLoginField(loginType) { + switch(loginType) { + case PasswordLogin.LOGIN_FIELD_EMAIL: + return ; + case PasswordLogin.LOGIN_FIELD_MXID: + return
+
@
+ +
:{this.props.hsDomain}
+
; + case PasswordLogin.LOGIN_FIELD_PHONE: + const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); + return
+ + +
; + } + } + + render() { var forgotPasswordJsx; if (this.props.onForgotPasswordClick) { @@ -124,47 +171,25 @@ module.exports = React.createClass({displayName: 'PasswordLogin', error: this.props.loginIncorrect, }); - const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); const Dropdown = sdk.getComponent('elements.Dropdown'); - const loginType = { - 'email': - , - 'mxid': - , - 'phone':
- - -
- }[this.state.loginType]; + const loginField = this.renderLoginField(this.state.loginType); return (
- - Matrix ID - Email - Phone + + Matrix ID + Email Address + Phone
- {loginType} + {loginField} {this._passwordField = e;}} type="password" name="password" value={this.state.password} onChange={this.onPasswordChanged} @@ -176,4 +201,25 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
); } -}); +} + +PasswordLogin.LOGIN_FIELD_EMAIL = "login_field_email"; +PasswordLogin.LOGIN_FIELD_MXID = "login_field_mxid"; +PasswordLogin.LOGIN_FIELD_PHONE = "login_field_phone"; + +PasswordLogin.propTypes = { + onSubmit: React.PropTypes.func.isRequired, // fn(username, password) + onForgotPasswordClick: React.PropTypes.func, // fn() + initialUsername: React.PropTypes.string, + initialPhoneCountry: React.PropTypes.string, + initialPhoneNumber: React.PropTypes.string, + initialPassword: React.PropTypes.string, + onUsernameChanged: React.PropTypes.func, + onPhoneCountryChanged: React.PropTypes.func, + onPhoneNumberChanged: React.PropTypes.func, + onPasswordChanged: React.PropTypes.func, + loginIncorrect: React.PropTypes.bool, + hsDomain: React.PropTypes.string, +}; + +module.exports = PasswordLogin; diff --git a/src/components/views/login/ServerConfig.js b/src/components/views/login/ServerConfig.js index 4e6ed12f9e..2853945425 100644 --- a/src/components/views/login/ServerConfig.js +++ b/src/components/views/login/ServerConfig.js @@ -27,8 +27,7 @@ module.exports = React.createClass({ displayName: 'ServerConfig', propTypes: { - onHsUrlChanged: React.PropTypes.func, - onIsUrlChanged: React.PropTypes.func, + onServerConfigChange: React.PropTypes.func, // default URLs are defined in config.json (or the hardcoded defaults) // they are used if the user has not overridden them with a custom URL. @@ -50,8 +49,7 @@ module.exports = React.createClass({ getDefaultProps: function() { return { - onHsUrlChanged: function() {}, - onIsUrlChanged: function() {}, + onServerConfigChange: function() {}, customHsUrl: "", customIsUrl: "", withToggleButton: false, @@ -75,7 +73,10 @@ module.exports = React.createClass({ this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() { var hsUrl = this.state.hs_url.trim().replace(/\/$/, ""); if (hsUrl === "") hsUrl = this.props.defaultHsUrl; - this.props.onHsUrlChanged(hsUrl); + this.props.onServerConfigChange({ + hsUrl : this.state.hs_url, + isUrl : this.state.is_url, + }); }); }); }, @@ -85,7 +86,10 @@ module.exports = React.createClass({ this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, function() { var isUrl = this.state.is_url.trim().replace(/\/$/, ""); if (isUrl === "") isUrl = this.props.defaultIsUrl; - this.props.onIsUrlChanged(isUrl); + this.props.onServerConfigChange({ + hsUrl : this.state.hs_url, + isUrl : this.state.is_url, + }); }); }); }, @@ -102,12 +106,16 @@ module.exports = React.createClass({ configVisible: visible }); if (!visible) { - this.props.onHsUrlChanged(this.props.defaultHsUrl); - this.props.onIsUrlChanged(this.props.defaultIsUrl); + this.props.onServerConfigChange({ + hsUrl : this.props.defaultHsUrl, + isUrl : this.props.defaultIsUrl, + }); } else { - this.props.onHsUrlChanged(this.state.hs_url); - this.props.onIsUrlChanged(this.state.is_url); + this.props.onServerConfigChange({ + hsUrl : this.state.hs_url, + isUrl : this.state.is_url, + }); } }, From 2b9cb999baebc04bc8d62f1159714278dd67711f Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 21 Apr 2017 11:50:19 +0100 Subject: [PATCH 0077/1588] autoFocus PasswordLogin --- src/components/views/login/PasswordLogin.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index fc063efbe9..ffb86636ca 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -150,6 +150,7 @@ class PasswordLogin extends React.Component { onChange={this.onPhoneNumberChanged} placeholder="Mobile phone number" value={this.state.phoneNumber} + autoFocus />
; } From b0288ebd89841ad362a6804b41839ef9d3bdca7f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 21 Apr 2017 12:40:13 +0100 Subject: [PATCH 0078/1588] fix stupid typos in RoomList's shouldComponentUpdate --- src/components/views/rooms/RoomList.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index a7dda40c6e..394de8876b 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -44,9 +44,9 @@ module.exports = React.createClass({ shouldComponentUpdate: function(nextProps, nextState) { if (nextProps.collapsed !== this.props.collapsed) return true; if (nextProps.searchFilter !== this.props.searchFilter) return true; - if (nextState.lists !== this.props.lists || - nextState.isLoadingLeftRooms !== this.isLoadingLeftRooms || - nextState.incomingCall !== this.incomingCall) return true; + if (nextState.lists !== this.state.lists || + nextState.isLoadingLeftRooms !== this.state.isLoadingLeftRooms || + nextState.incomingCall !== this.state.incomingCall) return true; return false; }, From 8308555489448b956236dde3bcbedcd32be3b5d2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 Apr 2017 12:12:37 +0100 Subject: [PATCH 0079/1588] Mark sync param as optional so that my IDE will stop complaining. Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/dispatcher.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dispatcher.js b/src/dispatcher.js index 9864cb3807..f3ebed8357 100644 --- a/src/dispatcher.js +++ b/src/dispatcher.js @@ -16,13 +16,13 @@ limitations under the License. 'use strict'; -var flux = require("flux"); +const flux = require("flux"); class MatrixDispatcher extends flux.Dispatcher { /** * @param {Object} payload Required. The payload to dispatch. * Must contain at least an 'action' key. - * @param {boolean} sync Optional. Pass true to dispatch + * @param {boolean=} sync Optional. Pass true to dispatch * synchronously. This is useful for anything triggering * an operation that the browser requires user interaction * for. From f200e349c9f7b23b28441871abf94bcfe0d08f94 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 Apr 2017 12:50:36 +0100 Subject: [PATCH 0080/1588] Add .idea to .gitignore file so I don't accidentally upload my IDE config Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 5139d614ad..7006c02403 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ npm-debug.log # test reports created by karma /karma-reports + +/.idea From b6ca16fc2f73704536bf93b05db2eeaef040bc98 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 Apr 2017 12:56:59 +0100 Subject: [PATCH 0081/1588] add RoomView state for message being forwarded add RoomView action handler for message forward clear forwardingMessage onCancelClick RoomView change var into const in render RoomView load ForwardMessage from rooms.ForwardMessage if there is a messageForwarding object in state show panel in aux Create ForwardMessage class Modify RoomHeader so that it shows the cancel button more greedily reskindex Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/component-index.js | 2 + src/components/structures/RoomView.js | 48 ++++++---- src/components/views/rooms/ForwardMessage.js | 95 ++++++++++++++++++++ src/components/views/rooms/RoomHeader.js | 3 + 4 files changed, 129 insertions(+), 19 deletions(-) create mode 100644 src/components/views/rooms/ForwardMessage.js diff --git a/src/component-index.js b/src/component-index.js index d6873c6dfd..b9f358467e 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -183,6 +183,8 @@ import views$rooms$EntityTile from './components/views/rooms/EntityTile'; views$rooms$EntityTile && (module.exports.components['views.rooms.EntityTile'] = views$rooms$EntityTile); import views$rooms$EventTile from './components/views/rooms/EventTile'; views$rooms$EventTile && (module.exports.components['views.rooms.EventTile'] = views$rooms$EventTile); +import views$rooms$ForwardMessage from './components/views/rooms/ForwardMessage'; +views$rooms$ForwardMessage && (module.exports.components['views.rooms.ForwardMessage'] = views$rooms$ForwardMessage); import views$rooms$LinkPreviewWidget from './components/views/rooms/LinkPreviewWidget'; views$rooms$LinkPreviewWidget && (module.exports.components['views.rooms.LinkPreviewWidget'] = views$rooms$LinkPreviewWidget); import views$rooms$MemberDeviceInfo from './components/views/rooms/MemberDeviceInfo'; diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index b09b101b8a..ea221e98b7 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -123,6 +123,8 @@ module.exports = React.createClass({ room: null, roomId: null, roomLoading: true, + + forwardingMessage: null, editingRoomSettings: false, uploadingRoomSettings: false, numUnreadMessages: 0, @@ -437,6 +439,11 @@ module.exports = React.createClass({ callState: callState }); + break; + case 'forward_message': + this.setState({ + forwardingMessage: payload.content, + }); break; } }, @@ -1180,7 +1187,10 @@ module.exports = React.createClass({ onCancelClick: function() { console.log("updateTint from onCancelClick"); this.updateTint(); - this.setState({editingRoomSettings: false}); + this.setState({ + editingRoomSettings: false, + forwardingMessage: null, + }); }, onLeaveClick: function() { @@ -1462,16 +1472,17 @@ module.exports = React.createClass({ }, render: function() { - var RoomHeader = sdk.getComponent('rooms.RoomHeader'); - var MessageComposer = sdk.getComponent('rooms.MessageComposer'); - var RoomSettings = sdk.getComponent("rooms.RoomSettings"); - var AuxPanel = sdk.getComponent("rooms.AuxPanel"); - var SearchBar = sdk.getComponent("rooms.SearchBar"); - var ScrollPanel = sdk.getComponent("structures.ScrollPanel"); - var TintableSvg = sdk.getComponent("elements.TintableSvg"); - var RoomPreviewBar = sdk.getComponent("rooms.RoomPreviewBar"); - var Loader = sdk.getComponent("elements.Spinner"); - var TimelinePanel = sdk.getComponent("structures.TimelinePanel"); + const RoomHeader = sdk.getComponent('rooms.RoomHeader'); + const MessageComposer = sdk.getComponent('rooms.MessageComposer'); + const ForwardMessage = sdk.getComponent("rooms.ForwardMessage"); + const RoomSettings = sdk.getComponent("rooms.RoomSettings"); + const AuxPanel = sdk.getComponent("rooms.AuxPanel"); + const SearchBar = sdk.getComponent("rooms.SearchBar"); + const ScrollPanel = sdk.getComponent("structures.ScrollPanel"); + const TintableSvg = sdk.getComponent("elements.TintableSvg"); + const RoomPreviewBar = sdk.getComponent("rooms.RoomPreviewBar"); + const Loader = sdk.getComponent("elements.Spinner"); + const TimelinePanel = sdk.getComponent("structures.TimelinePanel"); if (!this.state.room) { if (this.state.roomLoading) { @@ -1599,17 +1610,16 @@ module.exports = React.createClass({ />; } - var aux = null; - if (this.state.editingRoomSettings) { + let aux = null; + if (this.state.forwardingMessage !== null) { + aux = ; + } else if (this.state.editingRoomSettings) { aux = ; - } - else if (this.state.uploadingRoomSettings) { + } else if (this.state.uploadingRoomSettings) { aux = ; - } - else if (this.state.searching) { + } else if (this.state.searching) { aux = ; - } - else if (!myMember || myMember.membership !== "join") { + } else if (!myMember || myMember.membership !== "join") { // We do have a room object for this room, but we're not currently in it. // We may have a 3rd party invite to it. var inviterName = undefined; diff --git a/src/components/views/rooms/ForwardMessage.js b/src/components/views/rooms/ForwardMessage.js new file mode 100644 index 0000000000..58aac4edd1 --- /dev/null +++ b/src/components/views/rooms/ForwardMessage.js @@ -0,0 +1,95 @@ +/* + Copyright 2017 Vector Creations Ltd + Copyright 2017 Michael Telatynski + + 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 MatrixClientPeg from '../../../MatrixClientPeg'; +import dis from '../../../dispatcher'; +import KeyCode from "../../../KeyCode"; + + +module.exports = React.createClass({ + displayName: 'ForwardMessage', + + propTypes: { + content: React.PropTypes.object.isRequired, + + // true if RightPanel is collapsed + collapsedRhs: React.PropTypes.bool, + onCancelClick: React.PropTypes.func.isRequired, + }, + + componentWillMount: function() { + this._unmounted = false; + + if (!this.props.collapsedRhs) { + dis.dispatch({ + action: 'hide_right_panel', + }); + } + + dis.dispatch({ + action: 'ui_opacity', + sideOpacity: 1.0, + middleOpacity: 0.3, + }); + }, + + componentDidMount: function() { + this.dispatcherRef = dis.register(this.onAction); + document.addEventListener('keydown', this._onKeyDown); + }, + + componentWillUnmount: function() { + this._unmounted = true; + dis.dispatch({ + action: 'show_right_panel', + }); + dis.dispatch({ + action: 'ui_opacity', + sideOpacity: 1.0, + middleOpacity: 1.0, + }); + dis.unregister(this.dispatcherRef); + document.removeEventListener('keydown', this._onKeyDown); + }, + + onAction: function(payload) { + if (payload.action === 'view_room') { + MatrixClientPeg.get().sendMessage(payload.room_id, this.props.content); + } + }, + + _onKeyDown: function(ev) { + switch (ev.keyCode) { + case KeyCode.ESCAPE: + this.props.onCancelClick(); + dis.dispatch({action: 'focus_composer'}); + break; + } + }, + + render: function() { + return ( +
+ +

Select a room to send the message to

+

Use the left sidebar Room List to select forwarding target

+ +
+ ); + }, +}); diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 94f2691f2c..3d2386070c 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -186,6 +186,9 @@ module.exports = React.createClass({ ); save_button = Save; + } + + if (this.props.onCancelClick) { cancel_button = ; } From 64112da25ccb25797dd1cb9a1727fe5ae6c0a468 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 Apr 2017 14:01:04 +0100 Subject: [PATCH 0082/1588] only re-show right panel if it was visible before we were mounted Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/ForwardMessage.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/ForwardMessage.js b/src/components/views/rooms/ForwardMessage.js index 58aac4edd1..efecd1381f 100644 --- a/src/components/views/rooms/ForwardMessage.js +++ b/src/components/views/rooms/ForwardMessage.js @@ -55,9 +55,13 @@ module.exports = React.createClass({ componentWillUnmount: function() { this._unmounted = true; - dis.dispatch({ - action: 'show_right_panel', - }); + + if (!this.props.collapsedRhs) { + dis.dispatch({ + action: 'show_right_panel', + }); + } + dis.dispatch({ action: 'ui_opacity', sideOpacity: 1.0, From 9c4c706120497ad677cff3f1820c31cb628516e1 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 21 Apr 2017 16:09:11 +0100 Subject: [PATCH 0083/1588] Remove :server.name for custom servers Custom servers may not be configured such that their domain name === domain part. --- src/components/structures/login/Login.js | 8 +++++++- src/components/views/login/PasswordLogin.js | 15 +++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index d9a7039686..315a0ea242 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -230,6 +230,12 @@ module.exports = React.createClass({ switch (step) { case 'm.login.password': const PasswordLogin = sdk.getComponent('login.PasswordLogin'); + // HSs that are not matrix.org may not be configured to have their + // domain name === domain part. + let hsDomain = url.parse(this.state.enteredHomeserverUrl).hostname; + if (hsDomain !== 'matrix.org') { + hsDomain = null; + } return ( ); case 'm.login.cas': diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index ffb86636ca..568461817c 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -118,10 +118,21 @@ class PasswordLogin extends React.Component { autoFocus />; case PasswordLogin.LOGIN_FIELD_MXID: + const mxidInputClasses = classNames({ + "mx_Login_field": true, + "mx_Login_username": true, + "mx_Login_field_has_suffix": Boolean(this.props.hsDomain), + }); + let suffix = null; + if (this.props.hsDomain) { + suffix =
+ :{this.props.hsDomain} +
; + } return
@
-
:{this.props.hsDomain}
+ {suffix}
; case PasswordLogin.LOGIN_FIELD_PHONE: const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); From 29c2bd3d18beb22f619f297127ddf28fd3e1e9ab Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 21 Apr 2017 16:46:36 +0100 Subject: [PATCH 0084/1588] reset last_rr_sent on error Indicate that setting the RR was a failure and that hitting the API should be retried (in the case where the errcode !== "M_UNRECOGNISED") --- src/components/structures/TimelinePanel.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index e8774cec62..872d30ac8c 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -546,6 +546,7 @@ var TimelinePanel = React.createClass({ }); } // it failed, so allow retries next time the user is active + this.last_rr_sent_event_id = undefined; this.last_rm_sent_event_id = undefined; }); From fdc26a490ad8b9525fbc0633d192472a4af431d4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 Apr 2017 18:45:28 +0100 Subject: [PATCH 0085/1588] On return to RoomView from auxPanel, send focus back to Composer Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomView.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index b09b101b8a..9d5d50e9b1 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1181,6 +1181,7 @@ module.exports = React.createClass({ console.log("updateTint from onCancelClick"); this.updateTint(); this.setState({editingRoomSettings: false}); + dis.dispatch({action: 'focus_composer'}); }, onLeaveClick: function() { From 589d41e3b9f3e17bb960c67540ac235038ae785c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 Apr 2017 18:50:29 +0100 Subject: [PATCH 0086/1588] DRY the code a little bit in anticipation of #813 which sends a `focus_composer` onCancelClick() Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/ForwardMessage.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/rooms/ForwardMessage.js b/src/components/views/rooms/ForwardMessage.js index efecd1381f..42e2465037 100644 --- a/src/components/views/rooms/ForwardMessage.js +++ b/src/components/views/rooms/ForwardMessage.js @@ -81,7 +81,6 @@ module.exports = React.createClass({ switch (ev.keyCode) { case KeyCode.ESCAPE: this.props.onCancelClick(); - dis.dispatch({action: 'focus_composer'}); break; } }, From 8e9f52e2172e8118ca5767c15729dbf08c16ce4a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 Apr 2017 19:46:19 +0100 Subject: [PATCH 0087/1588] Disable Scalar Integrations if urls passed to it are falsey Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomSettings.js | 76 ++++++++++++---------- 1 file changed, 41 insertions(+), 35 deletions(-) diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 2c7e1d7140..2c29dd433c 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -129,14 +129,17 @@ module.exports = React.createClass({ console.error("Failed to get room visibility: " + err); }); - this.scalarClient = new ScalarAuthClient(); - this.scalarClient.connect().done(() => { - this.forceUpdate(); - }, (err) => { - this.setState({ - scalar_error: err + this.scalarClient = null; + if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) { + this.scalarClient = new ScalarAuthClient(); + this.scalarClient.connect().done(() => { + this.forceUpdate(); + }, (err) => { + this.setState({ + scalar_error: err + }); }); - }); + } dis.dispatch({ action: 'ui_opacity', @@ -490,7 +493,7 @@ module.exports = React.createClass({ ev.preventDefault(); var IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); Modal.createDialog(IntegrationsManager, { - src: this.scalarClient.hasCredentials() ? + src: (this.scalarClient !== null && this.scalarClient.hasCredentials()) ? this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId) : null, onFinished: ()=>{ @@ -765,36 +768,39 @@ module.exports = React.createClass({
; } - var integrationsButton; - var integrationsError; - if (this.state.showIntegrationsError && this.state.scalar_error) { - console.error(this.state.scalar_error); - integrationsError = ( - - Could not connect to the integration server - - ); - } + let integrationsButton; + let integrationsError; - if (this.scalarClient.hasCredentials()) { - integrationsButton = ( + if (this.scalarClient !== null) { + if (this.state.showIntegrationsError && this.state.scalar_error) { + console.error(this.state.scalar_error); + integrationsError = ( + + Could not connect to the integration server + + ); + } + + if (this.scalarClient.hasCredentials()) { + integrationsButton = (
- Manage Integrations -
- ); - } else if (this.state.scalar_error) { - integrationsButton = ( + Manage Integrations +
+ ); + } else if (this.state.scalar_error) { + integrationsButton = (
- Integrations Error - { integrationsError } -
- ); - } else { - integrationsButton = ( -
- Manage Integrations -
- ); + Integrations Error + { integrationsError } +
+ ); + } else { + integrationsButton = ( +
+ Manage Integrations +
+ ); + } } return ( From 2d39b5955616a74d148cb4d797fb8446d4c7b7ad Mon Sep 17 00:00:00 2001 From: turt2live Date: Fri, 21 Apr 2017 13:41:37 -0600 Subject: [PATCH 0088/1588] Change presence status labels to be more clear. As per vector-im/riot-web#3626 the current labels are unclear. Changing the verbage should make it more clear. Signed-off-by: Travis Ralston --- src/components/views/rooms/PresenceLabel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/PresenceLabel.js b/src/components/views/rooms/PresenceLabel.js index 2ece4c771e..52d831fcf6 100644 --- a/src/components/views/rooms/PresenceLabel.js +++ b/src/components/views/rooms/PresenceLabel.js @@ -75,7 +75,7 @@ module.exports = React.createClass({ render: function() { if (this.props.activeAgo >= 0) { - var ago = this.props.currentlyActive ? "now" : (this.getDuration(this.props.activeAgo) + " ago"); + var ago = this.props.currentlyActive ? "" : "for " + (this.getDuration(this.props.activeAgo)); // var ago = this.getDuration(this.props.activeAgo) + " ago"; // if (this.props.currentlyActive) ago += " (now?)"; return ( From e4c4adc5177fc9f33bcd39070ba79cab38cf3055 Mon Sep 17 00:00:00 2001 From: turt2live Date: Fri, 21 Apr 2017 14:28:28 -0600 Subject: [PATCH 0089/1588] Add option to hide other people's read receipts. Addresses vector-im/riot-web#2526 Signed-off-by: Travis Ralston --- src/components/structures/UserSettings.js | 4 ++++ src/components/views/rooms/EventTile.js | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 892865fdf9..b2ee29a1da 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -44,6 +44,10 @@ const SETTINGS_LABELS = [ id: 'autoplayGifsAndVideos', label: 'Autoplay GIFs and videos', }, + { + id: 'hideReadReceipts', + label: 'Hide read receipts' + }, /* { id: 'alwaysShowTimestamps', diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 9df0499eb2..e4234fc0bc 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -23,6 +23,7 @@ var Modal = require('../../../Modal'); var sdk = require('../../../index'); var TextForEvent = require('../../../TextForEvent'); import WithMatrixClient from '../../../wrappers/WithMatrixClient'; +import * as UserSettingsStore from "../../../UserSettingsStore"; var ContextualMenu = require('../../structures/ContextualMenu'); import dis from '../../../dispatcher'; @@ -284,6 +285,11 @@ module.exports = WithMatrixClient(React.createClass({ }, getReadAvatars: function() { + // return early if the user doesn't want any read receipts + if (UserSettingsStore.getSyncedSetting('hideReadReceipts', false)) { + return (); + } + const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker'); const avatars = []; const receiptOffset = 15; From 64e416e11745a9fc5513926f3738d0cac728ac1f Mon Sep 17 00:00:00 2001 From: turt2live Date: Fri, 21 Apr 2017 14:50:26 -0600 Subject: [PATCH 0090/1588] Add option to not send typing notifications Addresses vector-im/riot-web#3220 Fix applies to both the RTE and plain editor. Signed-off-by: Travis Ralston --- src/components/structures/UserSettings.js | 4 ++++ src/components/views/rooms/MessageComposerInput.js | 1 + src/components/views/rooms/MessageComposerInputOld.js | 2 ++ 3 files changed, 7 insertions(+) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 892865fdf9..619d8f32c8 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -44,6 +44,10 @@ const SETTINGS_LABELS = [ id: 'autoplayGifsAndVideos', label: 'Autoplay GIFs and videos', }, + { + id: 'dontSendTypingNotifications', + label: "Don't send typing notifications", + }, /* { id: 'alwaysShowTimestamps', diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 51c9ba881b..a7c20b02b5 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -355,6 +355,7 @@ export default class MessageComposerInput extends React.Component { } sendTyping(isTyping) { + if (UserSettingsStore.getSyncedSetting('dontSendTypingNotifications', false)) return; MatrixClientPeg.get().sendTyping( this.props.room.roomId, this.isTyping, TYPING_SERVER_TIMEOUT diff --git a/src/components/views/rooms/MessageComposerInputOld.js b/src/components/views/rooms/MessageComposerInputOld.js index f0b650eb04..f5366c36ad 100644 --- a/src/components/views/rooms/MessageComposerInputOld.js +++ b/src/components/views/rooms/MessageComposerInputOld.js @@ -20,6 +20,7 @@ var SlashCommands = require("../../../SlashCommands"); var Modal = require("../../../Modal"); var MemberEntry = require("../../../TabCompleteEntries").MemberEntry; var sdk = require('../../../index'); +import UserSettingsStore from "../../../UserSettingsStore"; var dis = require("../../../dispatcher"); var KeyCode = require("../../../KeyCode"); @@ -420,6 +421,7 @@ export default React.createClass({ }, sendTyping: function(isTyping) { + if (UserSettingsStore.getSyncedSetting('dontSendTypingNotifications', false)) return; MatrixClientPeg.get().sendTyping( this.props.room.roomId, this.isTyping, TYPING_SERVER_TIMEOUT From 80b8be64d1692f9546d00b8fa167e7dfffe38695 Mon Sep 17 00:00:00 2001 From: turt2live Date: Fri, 21 Apr 2017 15:09:56 -0600 Subject: [PATCH 0091/1588] Transform h1 and h2 tags to h3 tags Addresses vector-im/riot-web#1772 Signed-off-by: Travis Ralston --- src/HtmlUtils.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 96934d205e..632542ac43 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -165,6 +165,12 @@ var sanitizeHtmlParams = { attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/ return { tagName: tagName, attribs : attribs }; }, + 'h1': function(tagName, attribs) { + return { tagName: 'h3', attribs: attribs }; + }, + 'h2': function(tagName, attribs) { + return { tagName: 'h3', attribs: attribs }; + }, '*': function(tagName, attribs) { // Delete any style previously assigned, style is an allowedTag for font and span // because attributes are stripped after transforming From ec6a1c4c750f959017cdf823402a6c9d86b16fe2 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 22 Apr 2017 01:16:16 +0100 Subject: [PATCH 0092/1588] recalculate roomlist when your invites change --- src/components/views/rooms/RoomList.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 394de8876b..f36078e47d 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -265,9 +265,16 @@ module.exports = React.createClass({ }, onRoomStateMember: function(ev, state, member) { - constantTimeDispatcher.dispatch( - "RoomTile.refresh", member.roomId, {} - ); + if (ev.getStateKey() === MatrixClientPeg.get().credentials.userId && + ev.getPrevContent() && ev.getPrevContent().membership === "invite") + { + this._delayedRefreshRoomList(); + } + else { + constantTimeDispatcher.dispatch( + "RoomTile.refresh", member.roomId, {} + ); + } }, onRoomMemberName: function(ev, member) { From 1faecfd0f7b5cd48bea0166cf73d273fd201d38f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 22 Apr 2017 01:29:48 +0100 Subject: [PATCH 0093/1588] fix sticky headers on resize --- src/components/views/rooms/RoomList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index f36078e47d..5372135f95 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -606,7 +606,7 @@ module.exports = React.createClass({ return ( + autoshow={true} onScroll={ self._whenScrolling } onResize={ self._whenScrolling } ref="gemscroll">
Date: Sat, 22 Apr 2017 04:57:27 +0100 Subject: [PATCH 0094/1588] Remember element that was in focus before rendering dialog restore focus to that element when we unmount also remove some whitespace because ESLint is a big bad bully... Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/dialogs/BaseDialog.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index 0b2ca5225d..d0f34c5fbd 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -47,6 +47,16 @@ export default React.createClass({ children: React.PropTypes.node, }, + componentWillMount: function() { + this.priorActiveElement = document.activeElement; + }, + + componentWillUnmount: function() { + if (this.priorActiveElement !== null) { + this.priorActiveElement.focus(); + } + }, + _onKeyDown: function(e) { if (e.keyCode === KeyCode.ESCAPE) { e.stopPropagation(); @@ -67,7 +77,7 @@ export default React.createClass({ render: function() { const TintableSvg = sdk.getComponent("elements.TintableSvg"); - + return (
Date: Sat, 22 Apr 2017 14:52:20 +0100 Subject: [PATCH 0095/1588] Specify cross platform regexes and add olm to noParse Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- karma.conf.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/karma.conf.js b/karma.conf.js index 6d3047bb3b..3495a981be 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -135,17 +135,24 @@ module.exports = function (config) { }, ], noParse: [ + // for cross platform compatibility use [\\\/] as the path separator + // this ensures that the regex trips on both Windows and *nix + // don't parse the languages within highlight.js. They // cause stack overflows // (https://github.com/webpack/webpack/issues/1721), and // there is no need for webpack to parse them - they can // just be included as-is. - /highlight\.js\/lib\/languages/, + /highlight\.js[\\\/]lib[\\\/]languages/, + + // olm takes ages for webpack to process, and it's already heavily + // optimised, so there is little to gain by us uglifying it. + /olm[\\\/](javascript[\\\/])?olm\.js$/, // also disable parsing for sinon, because it // tries to do voodoo with 'require' which upsets // webpack (https://github.com/webpack/webpack/issues/304) - /sinon\/pkg\/sinon\.js$/, + /sinon[\\\/]pkg[\\\/]sinon\.js$/, ], }, resolve: { From 33e841a786b507a705cc54b735c29ec9427c2ae8 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 22 Apr 2017 15:40:29 +0100 Subject: [PATCH 0096/1588] move user settings outward and use built in read receipts disabling Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomView.js | 3 ++- src/components/views/rooms/EventTile.js | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 9d5d50e9b1..ea8b6e2ae0 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -26,6 +26,7 @@ var q = require("q"); var classNames = require("classnames"); var Matrix = require("matrix-js-sdk"); +var UserSettingsStore = require('../../UserSettingsStore'); var MatrixClientPeg = require("../../MatrixClientPeg"); var ContentMessages = require("../../ContentMessages"); var Modal = require("../../Modal"); @@ -1727,7 +1728,7 @@ module.exports = React.createClass({ var messagePanel = (
, button: "Sign out", extraButtons: [ - From 0e5006b0415b4b42789661eace8aab26b2f0dc48 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 22 Apr 2017 17:28:28 +0100 Subject: [PATCH 0098/1588] typo --- src/components/views/rooms/RoomList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 5372135f95..3810f7d4d6 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -483,7 +483,7 @@ module.exports = React.createClass({ // Use the offset of the top of the scroll area from the window // as this is used to calculate the CSS fixed top position for the stickies var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset; - // Use the offset of the top of the componet from the window + // Use the offset of the top of the component from the window // as this is used to calculate the CSS fixed top position for the stickies var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height; From 34c1a8f3cf7965ac77819b41b61c0702bb28ede4 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 22 Apr 2017 17:28:48 +0100 Subject: [PATCH 0099/1588] make autofocus explicit on errordialog as it autoFocus attr seems unreliable --- src/components/views/dialogs/ErrorDialog.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/ErrorDialog.js b/src/components/views/dialogs/ErrorDialog.js index 937595dfa8..ef6fdbbead 100644 --- a/src/components/views/dialogs/ErrorDialog.js +++ b/src/components/views/dialogs/ErrorDialog.js @@ -50,6 +50,12 @@ export default React.createClass({ }; }, + componentDidMount: function() { + if (this.props.focus) { + this.refs.button.focus(); + } + }, + render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( @@ -59,7 +65,7 @@ export default React.createClass({ {this.props.description}
-
From 6a63c7e50c46ffbe16a4d1df500bac8565b03b90 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 22 Apr 2017 21:06:38 +0100 Subject: [PATCH 0100/1588] fix deep-linking to riot.im/app --- src/linkify-matrix.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js index c8e20316a9..d9b0b78982 100644 --- a/src/linkify-matrix.js +++ b/src/linkify-matrix.js @@ -122,7 +122,7 @@ var escapeRegExp = function(string) { // anyone else really should be using matrix.to. matrixLinkify.VECTOR_URL_PATTERN = "^(?:https?:\/\/)?(?:" + escapeRegExp(window.location.host + window.location.pathname) + "|" - + "(?:www\\.)?(?:riot|vector)\\.im/(?:beta|staging|develop)/" + + "(?:www\\.)?(?:riot|vector)\\.im/(?:app|beta|staging|develop)/" + ")(#.*)"; matrixLinkify.MATRIXTO_URL_PATTERN = "^(?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/((#|@|!).*)"; From fa033e6116e65b5b1e40042eecf537cc2d7a70a0 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 23 Apr 2017 00:49:14 +0100 Subject: [PATCH 0101/1588] limit our keyboard shortcut modifiers correctly fixes https://github.com/vector-im/riot-web/issues/3614 --- src/components/structures/LoggedInView.js | 11 +++++++---- src/components/structures/ScrollPanel.js | 12 ++++++++---- src/components/structures/TimelinePanel.js | 4 +++- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index ef9d8d112a..318a5d7805 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -117,9 +117,10 @@ export default React.createClass({ } break; + case KeyCode.UP: case KeyCode.DOWN: - if (ev.altKey) { + if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) { var action = ev.keyCode == KeyCode.UP ? 'view_prev_room' : 'view_next_room'; dis.dispatch({action: action}); @@ -129,13 +130,15 @@ export default React.createClass({ case KeyCode.PAGE_UP: case KeyCode.PAGE_DOWN: - this._onScrollKeyPressed(ev); - handled = true; + if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { + this._onScrollKeyPressed(ev); + handled = true; + } break; case KeyCode.HOME: case KeyCode.END: - if (ev.ctrlKey) { + if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { this._onScrollKeyPressed(ev); handled = true; } diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 83bec03e9e..d43e22e2f1 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -483,21 +483,25 @@ module.exports = React.createClass({ handleScrollKey: function(ev) { switch (ev.keyCode) { case KeyCode.PAGE_UP: - this.scrollRelative(-1); + if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { + this.scrollRelative(-1); + } break; case KeyCode.PAGE_DOWN: - this.scrollRelative(1); + if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { + this.scrollRelative(1); + } break; case KeyCode.HOME: - if (ev.ctrlKey) { + if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { this.scrollToTop(); } break; case KeyCode.END: - if (ev.ctrlKey) { + if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { this.scrollToBottom(); } break; diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 7325cea2da..8babdaae4a 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -766,7 +766,9 @@ var TimelinePanel = React.createClass({ // jump to the live timeline on ctrl-end, rather than the end of the // timeline window. - if (ev.ctrlKey && ev.keyCode == KeyCode.END) { + if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey && + ev.keyCode == KeyCode.END) + { this.jumpToLiveTimeline(); } else { this.refs.messagePanel.handleScrollKey(ev); From 7854cac61d823f8c98e3a30caec5276e90a98823 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 23 Apr 2017 01:00:44 +0100 Subject: [PATCH 0102/1588] hook up keyb shortcuts for roomdir --- src/components/structures/LoggedInView.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 318a5d7805..4c012b42a8 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -156,6 +156,9 @@ export default React.createClass({ if (this.refs.roomView) { this.refs.roomView.handleScrollKey(ev); } + else if (this.refs.roomDirectory) { + this.refs.roomDirectory.handleScrollKey(ev); + } }, render: function() { @@ -216,6 +219,7 @@ export default React.createClass({ case PageTypes.RoomDirectory: page_element = ; From db996f678c9d0f31c30e44b221250431af89d89c Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 23 Apr 2017 01:32:51 +0100 Subject: [PATCH 0103/1588] show better errors when slash commands fail --- src/components/views/rooms/MessageComposerInput.js | 2 +- src/components/views/rooms/MessageComposerInputOld.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index a7c20b02b5..417d003226 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -510,7 +510,7 @@ export default class MessageComposerInput extends React.Component { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Server error", - description: "Server unavailable, overloaded, or something else went wrong.", + description: ((err && err.message) ? err.message : "Server unavailable, overloaded, or something else went wrong."), }); }); } diff --git a/src/components/views/rooms/MessageComposerInputOld.js b/src/components/views/rooms/MessageComposerInputOld.js index f5366c36ad..378644478c 100644 --- a/src/components/views/rooms/MessageComposerInputOld.js +++ b/src/components/views/rooms/MessageComposerInputOld.js @@ -312,7 +312,7 @@ export default React.createClass({ var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Server error", - description: "Server unavailable, overloaded, or something else went wrong.", + description: ((err && err.message) ? err.message : "Server unavailable, overloaded, or something else went wrong."), }); }); } From a2be764681240644ba5cbfaa7965adc308094dbf Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 23 Apr 2017 01:48:27 +0100 Subject: [PATCH 0104/1588] display err.message to user if available in error msgs --- src/CallHandler.js | 2 +- src/components/structures/MatrixChat.js | 2 +- src/components/structures/RoomView.js | 4 ++-- src/components/structures/UserSettings.js | 18 +++++++++--------- .../views/dialogs/ChatInviteDialog.js | 12 ++++++------ src/components/views/rooms/MemberInfo.js | 4 ++-- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/CallHandler.js b/src/CallHandler.js index 42cc681d08..5199ef0a67 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -313,7 +313,7 @@ function _onAction(payload) { console.error("Conference call failed: " + err); Modal.createDialog(ErrorDialog, { title: "Failed to set up conference call", - description: "Conference call failed.", + description: "Conference call failed. " + ((err && err.message) ? err.message : ""), }); }); } diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index b449ff3094..9b8aa3426a 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -413,7 +413,7 @@ module.exports = React.createClass({ console.error("Failed to leave room " + payload.room_id + " " + err); Modal.createDialog(ErrorDialog, { title: "Failed to leave room", - description: "Server may be unavailable, overloaded, or you hit a bug." + description: (err && err.message ? err.message : "Server may be unavailable, overloaded, or you hit a bug."), }); }); } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index ea8b6e2ae0..c158b87ff3 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -947,7 +947,7 @@ module.exports = React.createClass({ console.error("Failed to upload file " + file + " " + error); Modal.createDialog(ErrorDialog, { title: "Failed to upload file", - description: "Server may be unavailable, overloaded, or the file too big", + description: ((error && error.message) ? error.message : "Server may be unavailable, overloaded, or the file too big"), }); }); }, @@ -1034,7 +1034,7 @@ module.exports = React.createClass({ console.error("Search failed: " + error); Modal.createDialog(ErrorDialog, { title: "Search failed", - description: "Server may be unavailable, overloaded, or search timed out :(" + description: ((error && error.message) ? error.message : "Server may be unavailable, overloaded, or search timed out :("), }); }).finally(function() { self.setState({ diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 3e636c3eb1..ba5d5780b4 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -223,7 +223,7 @@ module.exports = React.createClass({ console.error("Failed to load user settings: " + error); Modal.createDialog(ErrorDialog, { title: "Can't load user settings", - description: "Server may be unavailable or overloaded", + description: ((error && error.message) ? error.message : "Server may be unavailable or overloaded"), }); }); }, @@ -264,8 +264,8 @@ module.exports = React.createClass({ console.error("Failed to set avatar: " + err); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Error", - description: "Failed to set avatar." + title: "Failed to set avatar", + description: ((err && err.message) ? err.message : "Operation failed"), }); }); }, @@ -366,8 +366,8 @@ module.exports = React.createClass({ this.setState({email_add_pending: false}); console.error("Unable to add email address " + email_address + " " + err); Modal.createDialog(ErrorDialog, { - title: "Error", - description: "Unable to add email address" + title: "Unable to add email address", + description: ((err && err.message) ? err.message : "Operation failed"), }); }); ReactDOM.findDOMNode(this.refs.add_email_input).blur(); @@ -391,8 +391,8 @@ module.exports = React.createClass({ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Unable to remove contact information: " + err); Modal.createDialog(ErrorDialog, { - title: "Error", - description: "Unable to remove contact information", + title: "Unable to remove contact information", + description: ((err && err.message) ? err.message : "Operation failed"), }); }).done(); } @@ -432,8 +432,8 @@ module.exports = React.createClass({ var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Unable to verify email address: " + err); Modal.createDialog(ErrorDialog, { - title: "Error", - description: "Unable to verify email address", + title: "Unable to verify email address", + description: ((err && err.message) ? err.message : "Operation failed"), }); } }); diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index 16f756a773..7ba503099a 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -308,8 +308,8 @@ module.exports = React.createClass({ console.error(err.stack); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Error", - description: "Failed to invite", + title: "Failed to invite", + description: ((err && err.message) ? err.message : "Operation failed"), }); return null; }) @@ -321,8 +321,8 @@ module.exports = React.createClass({ console.error(err.stack); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Error", - description: "Failed to invite user", + title: "Failed to invite user", + description: ((err && err.message) ? err.message : "Operation failed"), }); return null; }) @@ -342,8 +342,8 @@ module.exports = React.createClass({ console.error(err.stack); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Error", - description: "Failed to invite", + title: "Failed to invite", + description: ((err && err.message) ? err.message : "Operation failed"), }); return null; }) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 1459ad3eb7..1a9a8d5e0f 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -241,8 +241,8 @@ module.exports = WithMatrixClient(React.createClass({ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Kick error: " + err); Modal.createDialog(ErrorDialog, { - title: "Error", - description: "Failed to kick user", + title: "Failed to kick", + description: ((err && err.message) ? err.message : "Operation failed"), }); } ).finally(()=>{ From 24f2aed45f890a630a9c095e44c7f86d57ffe32e Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 23 Apr 2017 04:05:50 +0100 Subject: [PATCH 0105/1588] summarise profile changes in MELS fixes https://github.com/vector-im/riot-web/issues/3463 --- src/components/structures/MessagePanel.js | 4 +--- .../views/elements/MemberEventListSummary.js | 21 ++++++++++++++++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 8d50789eb0..87f444d607 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -279,9 +279,7 @@ module.exports = React.createClass({ this.currentGhostEventId = null; } - var isMembershipChange = (e) => - e.getType() === 'm.room.member' - && (!e.getPrevContent() || e.getContent().membership !== e.getPrevContent().membership); + var isMembershipChange = (e) => e.getType() === 'm.room.member'; for (i = 0; i < this.props.events.length; i++) { var mxEv = this.props.events[i]; diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index d7f876c16e..8eb81ae5f1 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -221,6 +221,8 @@ module.exports = React.createClass({ "banned": beConjugated + " banned", "unbanned": beConjugated + " unbanned", "kicked": beConjugated + " kicked", + "changed_name": "changed name", + "changed_avatar": "changed avatar", }; if (Object.keys(map).includes(t)) { @@ -289,7 +291,24 @@ module.exports = React.createClass({ switch (e.mxEvent.getContent().membership) { case 'invite': return 'invited'; case 'ban': return 'banned'; - case 'join': return 'joined'; + case 'join': + if (e.mxEvent.getPrevContent().membership === 'join') { + if (e.mxEvent.getContent().displayname !== + e.mxEvent.getPrevContent().displayname) + { + return 'changed_name'; + } + else if (e.mxEvent.getContent().avatar_url !== + e.mxEvent.getPrevContent().avatar_url) + { + return 'changed_avatar'; + } + console.info("MELS ignoring duplicate membership join event"); + return null; + } + else { + return 'joined'; + } case 'leave': if (e.mxEvent.getSender() === e.mxEvent.getStateKey()) { switch (e.mxEvent.getPrevContent().membership) { From 0590ce7faf9680d9d720a43de786545e7da5e7e6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 23 Apr 2017 06:06:23 +0100 Subject: [PATCH 0106/1588] Conform damn you (mostly) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/Notifier.js | 53 ++++++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/src/Notifier.js b/src/Notifier.js index 92770877b7..617135a2c8 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -15,11 +15,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -var MatrixClientPeg = require("./MatrixClientPeg"); -var PlatformPeg = require("./PlatformPeg"); -var TextForEvent = require('./TextForEvent'); -var Avatar = require('./Avatar'); -var dis = require("./dispatcher"); +import MatrixClientPeg from './MatrixClientPeg'; +import PlatformPeg from './PlatformPeg'; +import TextForEvent from './TextForEvent'; +import Avatar from './Avatar'; +import dis from './dispatcher'; /* * Dispatches: @@ -29,7 +29,7 @@ var dis = require("./dispatcher"); * } */ -var Notifier = { +const Notifier = { notifsByRoom: {}, notificationMessageForEvent: function(ev) { @@ -48,16 +48,16 @@ var Notifier = { return; } - var msg = this.notificationMessageForEvent(ev); + let msg = this.notificationMessageForEvent(ev); if (!msg) return; - var title; - if (!ev.sender || room.name == ev.sender.name) { + let title; + if (!ev.sender || room.name === ev.sender.name) { title = room.name; // notificationMessageForEvent includes sender, // but we already have the sender here if (ev.getContent().body) msg = ev.getContent().body; - } else if (ev.getType() == 'm.room.member') { + } else if (ev.getType() === 'm.room.member') { // context is all in the message here, we don't need // to display sender info title = room.name; @@ -68,7 +68,7 @@ var Notifier = { if (ev.getContent().body) msg = ev.getContent().body; } - var avatarUrl = ev.sender ? Avatar.avatarUrlForMember( + const avatarUrl = ev.sender ? Avatar.avatarUrlForMember( ev.sender, 40, 40, 'crop' ) : null; @@ -83,7 +83,7 @@ var Notifier = { }, _playAudioNotification: function(ev, room) { - var e = document.getElementById("messageAudio"); + const e = document.getElementById("messageAudio"); if (e) { e.load(); e.play(); @@ -95,7 +95,7 @@ var Notifier = { this.boundOnSyncStateChange = this.onSyncStateChange.bind(this); this.boundOnRoomReceipt = this.onRoomReceipt.bind(this); MatrixClientPeg.get().on('Room.timeline', this.boundOnRoomTimeline); - MatrixClientPeg.get().on("Room.receipt", this.boundOnRoomReceipt); + MatrixClientPeg.get().on('Room.receipt', this.boundOnRoomReceipt); MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange); this.toolbarHidden = false; this.isSyncing = false; @@ -104,7 +104,7 @@ var Notifier = { stop: function() { if (MatrixClientPeg.get() && this.boundOnRoomTimeline) { MatrixClientPeg.get().removeListener('Room.timeline', this.boundOnRoomTimeline); - MatrixClientPeg.get().removeListener("Room.receipt", this.boundOnRoomReceipt); + MatrixClientPeg.get().removeListener('Room.receipt', this.boundOnRoomReceipt); MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange); } this.isSyncing = false; @@ -121,7 +121,7 @@ var Notifier = { // make sure that we persist the current setting audio_enabled setting // before changing anything if (global.localStorage) { - if(global.localStorage.getItem('audio_notifications_enabled') == null) { + if (global.localStorage.getItem('audio_notifications_enabled') === null) { this.setAudioEnabled(this.isEnabled()); } } @@ -141,7 +141,7 @@ var Notifier = { if (callback) callback(); dis.dispatch({ action: "notifier_enabled", - value: true + value: true, }); }); // clear the notifications_hidden flag, so that if notifications are @@ -152,7 +152,7 @@ var Notifier = { global.localStorage.setItem('notifications_enabled', 'false'); dis.dispatch({ action: "notifier_enabled", - value: false + value: false, }); } }, @@ -165,7 +165,7 @@ var Notifier = { if (!global.localStorage) return true; - var enabled = global.localStorage.getItem('notifications_enabled'); + const enabled = global.localStorage.getItem('notifications_enabled'); if (enabled === null) return true; return enabled === 'true'; }, @@ -173,12 +173,12 @@ var Notifier = { setAudioEnabled: function(enable) { if (!global.localStorage) return; global.localStorage.setItem('audio_notifications_enabled', - enable ? 'true' : 'false'); + enable ? 'true' : 'false'); }, isAudioEnabled: function(enable) { if (!global.localStorage) return true; - var enabled = global.localStorage.getItem( + const enabled = global.localStorage.getItem( 'audio_notifications_enabled'); // default to true if the popups are enabled if (enabled === null) return this.isEnabled(); @@ -192,7 +192,7 @@ var Notifier = { // this is nothing to do with notifier_enabled dis.dispatch({ action: "notifier_enabled", - value: this.isEnabled() + value: this.isEnabled(), }); // update the info to localStorage for persistent settings @@ -215,8 +215,7 @@ var Notifier = { onSyncStateChange: function(state) { if (state === "SYNCING") { this.isSyncing = true; - } - else if (state === "STOPPED" || state === "ERROR") { + } else if (state === "STOPPED" || state === "ERROR") { this.isSyncing = false; } }, @@ -225,10 +224,10 @@ var Notifier = { if (toStartOfTimeline) return; if (!room) return; if (!this.isSyncing) return; // don't alert for any messages initially - if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) return; + if (ev.sender && ev.sender.userId === MatrixClientPeg.get().credentials.userId) return; if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return; - var actions = MatrixClientPeg.get().getPushActionsForEvent(ev); + const actions = MatrixClientPeg.get().getPushActionsForEvent(ev); if (actions && actions.notify) { if (this.isEnabled()) { this._displayPopupNotification(ev, room); @@ -240,7 +239,7 @@ var Notifier = { }, onRoomReceipt: function(ev, room) { - if (room.getUnreadNotificationCount() == 0) { + if (room.getUnreadNotificationCount() === 0) { // ideally we would clear each notification when it was read, // but we have no way, given a read receipt, to know whether // the receipt comes before or after an event, so we can't @@ -255,7 +254,7 @@ var Notifier = { } delete this.notifsByRoom[room.roomId]; } - } + }, }; if (!global.mxNotifier) { From 5e8b43f3edc70cd8d73a920c9a231c0f681c43fb Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 23 Apr 2017 06:16:25 +0100 Subject: [PATCH 0107/1588] if we're not granted, show an ErrorDialog with some text which needs changing Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/Notifier.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Notifier.js b/src/Notifier.js index 617135a2c8..fed2760732 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -20,6 +20,8 @@ import PlatformPeg from './PlatformPeg'; import TextForEvent from './TextForEvent'; import Avatar from './Avatar'; import dis from './dispatcher'; +import sdk from './index'; +import Modal from './Modal'; /* * Dispatches: @@ -131,6 +133,14 @@ const Notifier = { plaf.requestNotificationPermission().done((result) => { if (result !== 'granted') { // The permission request was dismissed or denied + const description = result === 'denied' + ? 'Your browser is not permitting this app to send you notifications.' + : 'It seems you didn\'t accept notifications when your browser asked'; + const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); + Modal.createDialog(ErrorDialog, { + title: 'Unable to enable Notifications', + description, + }); return; } From 6f461f0ebbb084a73f545b8008b34d96c351312b Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 24 Apr 2017 01:09:54 +0100 Subject: [PATCH 0108/1588] add in scrollto button --- src/components/views/rooms/TopUnreadMessagesBar.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/TopUnreadMessagesBar.js b/src/components/views/rooms/TopUnreadMessagesBar.js index 5bef8c0b0a..72b489a406 100644 --- a/src/components/views/rooms/TopUnreadMessagesBar.js +++ b/src/components/views/rooms/TopUnreadMessagesBar.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. @@ -32,7 +33,10 @@ module.exports = React.createClass({
- Jump to first unread message. Mark all read + Scroll to unread messages + Jump to first unread message.
Date: Mon, 24 Apr 2017 12:53:53 +0100 Subject: [PATCH 0109/1588] fix scroll behaviour on macs with no gemini --- src/components/views/rooms/RoomList.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 3810f7d4d6..96ff65498f 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -456,11 +456,10 @@ module.exports = React.createClass({ var panel = ReactDOM.findDOMNode(this); if (!panel) return null; - if (panel.classList.contains('gm-prevented')) { - return panel; - } else { - return panel.children[2]; // XXX: Fragile! - } + // empirically, if we have gm-prevented for some reason, the scroll node + // is still the 3rd child (i.e. the view child). This looks to be due + // to vdh's improved resize updater logic...? + return panel.children[2]; // XXX: Fragile! }, _whenScrolling: function(e) { @@ -506,7 +505,7 @@ module.exports = React.createClass({ // Use the offset of the top of the scroll area from the window // as this is used to calculate the CSS fixed top position for the stickies var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset; - // Use the offset of the top of the componet from the window + // Use the offset of the top of the component from the window // as this is used to calculate the CSS fixed top position for the stickies var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height; From 74e92d6c235e629802184c86d0323587dec9f82f Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 24 Apr 2017 15:44:45 +0100 Subject: [PATCH 0110/1588] Remove DM-guessing code --- src/components/views/rooms/RoomList.js | 53 +++----------------------- 1 file changed, 6 insertions(+), 47 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 3810f7d4d6..5839b66d1c 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -97,7 +97,7 @@ module.exports = React.createClass({ if (this.props.selectedRoom) { constantTimeDispatcher.dispatch( "RoomTile.select", this.props.selectedRoom, {} - ); + ); } constantTimeDispatcher.dispatch( "RoomTile.select", nextProps.selectedRoom, { selected: true } @@ -265,7 +265,7 @@ module.exports = React.createClass({ }, onRoomStateMember: function(ev, state, member) { - if (ev.getStateKey() === MatrixClientPeg.get().credentials.userId && + if (ev.getStateKey() === MatrixClientPeg.get().credentials.userId && ev.getPrevContent() && ev.getPrevContent().membership === "invite") { this._delayedRefreshRoomList(); @@ -290,7 +290,7 @@ module.exports = React.createClass({ this._delayedRefreshRoomList(); } else if (ev.getType() == 'm.push_rules') { - this._delayedRefreshRoomList(); + this._delayedRefreshRoomList(); } }, @@ -318,7 +318,7 @@ module.exports = React.createClass({ // as needed. // Alternatively we'd do something magical with Immutable.js or similar. this.setState(this.getRoomLists()); - + // this._lastRefreshRoomListTs = Date.now(); }, @@ -341,7 +341,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().getRooms().forEach(function(room) { const me = room.getMember(MatrixClientPeg.get().credentials.userId); if (!me) return; - + // console.log("room = " + room.name + ", me.membership = " + me.membership + // ", sender = " + me.events.member.getSender() + // ", target = " + me.events.member.getStateKey() + @@ -391,51 +391,10 @@ module.exports = React.createClass({ } }); - if (s.lists["im.vector.fake.direct"].length == 0 && - MatrixClientPeg.get().getAccountData('m.direct') === undefined && - !MatrixClientPeg.get().isGuest()) - { - // scan through the 'recents' list for any rooms which look like DM rooms - // and make them DM rooms - const oldRecents = s.lists["im.vector.fake.recent"]; - s.lists["im.vector.fake.recent"] = []; - - for (const room of oldRecents) { - const me = room.getMember(MatrixClientPeg.get().credentials.userId); - - if (me && Rooms.looksLikeDirectMessageRoom(room, me)) { - self.listsForRoomId[room.roomId].push("im.vector.fake.direct"); - s.lists["im.vector.fake.direct"].push(room); - } else { - self.listsForRoomId[room.roomId].push("im.vector.fake.recent"); - s.lists["im.vector.fake.recent"].push(room); - } - } - - // save these new guessed DM rooms into the account data - const newMDirectEvent = {}; - for (const room of s.lists["im.vector.fake.direct"]) { - const me = room.getMember(MatrixClientPeg.get().credentials.userId); - const otherPerson = Rooms.getOnlyOtherMember(room, me); - if (!otherPerson) continue; - - const roomList = newMDirectEvent[otherPerson.userId] || []; - roomList.push(room.roomId); - newMDirectEvent[otherPerson.userId] = roomList; - } - - console.warn("Resetting room DM state to be " + JSON.stringify(newMDirectEvent)); - - // if this fails, fine, we'll just do the same thing next time we get the room lists - MatrixClientPeg.get().setAccountData('m.direct', newMDirectEvent).done(); - } - - //console.log("calculated new roomLists; im.vector.fake.recent = " + s.lists["im.vector.fake.recent"]); - // we actually apply the sorting to this when receiving the prop in RoomSubLists. // we'll need this when we get to iterating through lists programatically - e.g. ctrl-shift-up/down -/* +/* this.listOrder = [ "im.vector.fake.invite", "m.favourite", From 3bd77d56db1a2910e3e8872a84876a1533810a0d Mon Sep 17 00:00:00 2001 From: turt2live Date: Mon, 24 Apr 2017 08:43:51 -0600 Subject: [PATCH 0111/1588] Allow h1 and h2 tags again. CSS handled by riot-web Signed-off-by: Travis Ralston --- src/HtmlUtils.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 632542ac43..a31601790f 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -111,8 +111,7 @@ var sanitizeHtmlParams = { allowedTags: [ 'font', // custom to matrix for IRC-style font coloring 'del', // for markdown - // deliberately no h1/h2 to stop people shouting. - 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', + 'h1', 'h2', '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', 'span', ], @@ -165,12 +164,6 @@ var sanitizeHtmlParams = { attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/ return { tagName: tagName, attribs : attribs }; }, - 'h1': function(tagName, attribs) { - return { tagName: 'h3', attribs: attribs }; - }, - 'h2': function(tagName, attribs) { - return { tagName: 'h3', attribs: attribs }; - }, '*': function(tagName, attribs) { // Delete any style previously assigned, style is an allowedTag for font and span // because attributes are stripped after transforming From e6fd380947207a161a9ac8edb336eddd21bc6533 Mon Sep 17 00:00:00 2001 From: turt2live Date: Mon, 24 Apr 2017 12:49:09 -0600 Subject: [PATCH 0112/1588] Change redact -> remove for clarity Addresses vector-im/riot-web#2814 Non-technical users may not understand what 'redact' means and can more easily understand what 'Remove' does. See discussion on vector-im/riot-web#2814 for more information. Signed-off-by: Travis Ralston --- src/components/views/dialogs/ConfirmRedactDialog.js | 8 ++++---- src/components/views/messages/UnknownBody.js | 2 +- src/components/views/rooms/RoomSettings.js | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/views/dialogs/ConfirmRedactDialog.js b/src/components/views/dialogs/ConfirmRedactDialog.js index fc9e55f666..db5197e338 100644 --- a/src/components/views/dialogs/ConfirmRedactDialog.js +++ b/src/components/views/dialogs/ConfirmRedactDialog.js @@ -42,7 +42,7 @@ export default React.createClass({ render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const title = "Confirm Redaction"; + const title = "Confirm Removal"; const confirmButtonClass = classnames({ 'mx_Dialog_primary': true, @@ -55,12 +55,12 @@ export default React.createClass({ title={title} >
- Are you sure you wish to redact (delete) this event? - Note that if you redact a room name or topic change, it could undo the change. + Are you sure you wish to remove (delete) this event? + Note that if you delete a room name or topic change, it could undo the change.
- To redact messages, you must be a + To remove messages, you must be a
From fbca0e0d0d320d970826d6112b2b793b1a093530 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 24 Apr 2017 20:03:07 +0100 Subject: [PATCH 0113/1588] correct cancel appearing when it shouldn't Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- 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 9cd5070ad1..91d5e01f78 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1778,7 +1778,7 @@ module.exports = React.createClass({ onSearchClick={this.onSearchClick} onSettingsClick={this.onSettingsClick} onSaveClick={this.onSettingsSaveClick} - onCancelClick={this.onCancelClick} + onCancelClick={aux ? this.onCancelClick : null} onForgetClick={ (myMember && myMember.membership === "leave") ? this.onForgetClick : null } From ee560a969a17591ea39590a61bc0f3327e9df8ab Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 24 Apr 2017 20:17:29 +0100 Subject: [PATCH 0114/1588] upon forwarding message to current room, explicitly remove clear from aux Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomView.js | 2 +- src/components/views/rooms/ForwardMessage.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 91d5e01f78..e941c6b1a8 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1614,7 +1614,7 @@ module.exports = React.createClass({ let aux = null; if (this.state.forwardingMessage !== null) { - aux = ; + aux = ; } else if (this.state.editingRoomSettings) { aux = ; } else if (this.state.uploadingRoomSettings) { diff --git a/src/components/views/rooms/ForwardMessage.js b/src/components/views/rooms/ForwardMessage.js index 42e2465037..bb401dde69 100644 --- a/src/components/views/rooms/ForwardMessage.js +++ b/src/components/views/rooms/ForwardMessage.js @@ -25,6 +25,7 @@ module.exports = React.createClass({ displayName: 'ForwardMessage', propTypes: { + currentRoomId: React.PropTypes.string.isRequired, content: React.PropTypes.object.isRequired, // true if RightPanel is collapsed @@ -74,6 +75,7 @@ module.exports = React.createClass({ onAction: function(payload) { if (payload.action === 'view_room') { MatrixClientPeg.get().sendMessage(payload.room_id, this.props.content); + if (this.props.currentRoomId === payload.room_id) this.props.onCancelClick(); } }, From 4cf9e0c1ae46d22baef4463376b33c7e577f62cc Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 24 Apr 2017 22:00:59 +0100 Subject: [PATCH 0115/1588] Create a way to restore last state of the rhs panel. Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/MatrixChat.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 9b8aa3426a..6df8d99c3a 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -303,6 +303,7 @@ module.exports = React.createClass({ componentDidUpdate: function() { if (this.focusComposer) { + console.log('is this the shitty duplicate?'); dis.dispatch({action: 'focus_composer'}); this.focusComposer = false; } @@ -547,15 +548,23 @@ module.exports = React.createClass({ }); break; case 'hide_right_panel': + this.was_rhs_collapsed = this.state.collapse_rhs; this.setState({ collapse_rhs: true, }); break; case 'show_right_panel': + this.was_rhs_collapsed = this.state.collapse_rhs; this.setState({ collapse_rhs: false, }); break; + // sets the panel to its state before last show/hide event + case 'restore_right_panel': + this.setState({ + collapse_rhs: this.was_rhs_collapsed, + }); + break; case 'ui_opacity': this.setState({ sideOpacity: payload.sideOpacity, From bfba25f3dac7a0279f19300023065032ad251fc3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 24 Apr 2017 22:10:14 +0100 Subject: [PATCH 0116/1588] we don't care about rhs state anymore as we can just restore it sanely Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- 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 e941c6b1a8..3e43d6984a 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1614,7 +1614,7 @@ module.exports = React.createClass({ let aux = null; if (this.state.forwardingMessage !== null) { - aux = ; + aux = ; } else if (this.state.editingRoomSettings) { aux = ; } else if (this.state.uploadingRoomSettings) { From 28a2266f70ee5aaf18d0eb06333e19a6b9e558cc Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 24 Apr 2017 22:11:20 +0100 Subject: [PATCH 0117/1588] tidy up UDE handler Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/UnknownDeviceErrorHandler.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/UnknownDeviceErrorHandler.js b/src/UnknownDeviceErrorHandler.js index 2aa0573e22..2b1cf23380 100644 --- a/src/UnknownDeviceErrorHandler.js +++ b/src/UnknownDeviceErrorHandler.js @@ -22,7 +22,7 @@ let isDialogOpen = false; const onAction = function(payload) { if (payload.action === 'unknown_device_error' && !isDialogOpen) { - var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog"); + const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog'); isDialogOpen = true; Modal.createDialog(UnknownDeviceDialog, { devices: payload.err.devices, @@ -33,17 +33,17 @@ const onAction = function(payload) { // https://github.com/vector-im/riot-web/issues/3148 console.log('UnknownDeviceDialog closed with '+r); }, - }, "mx_Dialog_unknownDevice"); + }, 'mx_Dialog_unknownDevice'); } -} +}; let ref = null; -export function startListening () { +export function startListening() { ref = dis.register(onAction); } -export function stopListening () { +export function stopListening() { if (ref) { dis.unregister(ref); ref = null; From 9ae9aeea0746e5190865491f6602346af6fb1327 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 24 Apr 2017 22:14:45 +0100 Subject: [PATCH 0118/1588] lets improve forwarding :D ditch double quotes stop caring about rhs state always call hide_right_panel, nop if already hidden use new restore_right_panel to bring it back if it was visible pre-us actually tell things that we sent a message or failed in doing so now the UDE works :D Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/ForwardMessage.js | 32 ++++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/components/views/rooms/ForwardMessage.js b/src/components/views/rooms/ForwardMessage.js index bb401dde69..e5be89b2e0 100644 --- a/src/components/views/rooms/ForwardMessage.js +++ b/src/components/views/rooms/ForwardMessage.js @@ -18,7 +18,7 @@ import React from 'react'; import MatrixClientPeg from '../../../MatrixClientPeg'; import dis from '../../../dispatcher'; -import KeyCode from "../../../KeyCode"; +import KeyCode from '../../../KeyCode'; module.exports = React.createClass({ @@ -28,20 +28,13 @@ module.exports = React.createClass({ currentRoomId: React.PropTypes.string.isRequired, content: React.PropTypes.object.isRequired, - // true if RightPanel is collapsed - collapsedRhs: React.PropTypes.bool, onCancelClick: React.PropTypes.func.isRequired, }, componentWillMount: function() { this._unmounted = false; - if (!this.props.collapsedRhs) { - dis.dispatch({ - action: 'hide_right_panel', - }); - } - + dis.dispatch({action: 'hide_right_panel'}); dis.dispatch({ action: 'ui_opacity', sideOpacity: 1.0, @@ -57,12 +50,7 @@ module.exports = React.createClass({ componentWillUnmount: function() { this._unmounted = true; - if (!this.props.collapsedRhs) { - dis.dispatch({ - action: 'show_right_panel', - }); - } - + dis.dispatch({action: 'restore_right_panel'}); dis.dispatch({ action: 'ui_opacity', sideOpacity: 1.0, @@ -74,7 +62,19 @@ module.exports = React.createClass({ onAction: function(payload) { if (payload.action === 'view_room') { - MatrixClientPeg.get().sendMessage(payload.room_id, this.props.content); + const Client = MatrixClientPeg.get(); + Client.sendMessage(payload.room_id, this.props.content).done(() => { + dis.dispatch({action: 'message_sent'}); + }, (err) => { + if (err.name === "UnknownDeviceError") { + dis.dispatch({ + action: 'unknown_device_error', + err: err, + room: Client.getRoom(payload.room_id), + }); + } + dis.dispatch({action: 'message_send_failed'}); + }); if (this.props.currentRoomId === payload.room_id) this.props.onCancelClick(); } }, From 3997974f0f4036af92187ad1d0d50198ad58710a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 24 Apr 2017 22:16:15 +0100 Subject: [PATCH 0119/1588] remove debug console log (ignore its content pls) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/MatrixChat.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 6df8d99c3a..25ec644787 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -303,7 +303,6 @@ module.exports = React.createClass({ componentDidUpdate: function() { if (this.focusComposer) { - console.log('is this the shitty duplicate?'); dis.dispatch({action: 'focus_composer'}); this.focusComposer = false; } From 63dac026a89954f4487a5a95e697051a1e75b108 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 25 Apr 2017 00:17:46 +0100 Subject: [PATCH 0120/1588] remove spammy log --- src/components/views/elements/MemberEventListSummary.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index 8eb81ae5f1..63bd2a7c39 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -303,7 +303,7 @@ module.exports = React.createClass({ { return 'changed_avatar'; } - console.info("MELS ignoring duplicate membership join event"); + // console.log("MELS ignoring duplicate membership join event"); return null; } else { From e64b647799e57045d25aabc6e3c448f1a7e57cd5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 Apr 2017 09:25:10 +0100 Subject: [PATCH 0121/1588] show the room name in the UDE Dialog especially useful when it appears after you switch rooms Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/dialogs/UnknownDeviceDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/UnknownDeviceDialog.js b/src/components/views/dialogs/UnknownDeviceDialog.js index da9c8e8f65..4f3d4301f9 100644 --- a/src/components/views/dialogs/UnknownDeviceDialog.js +++ b/src/components/views/dialogs/UnknownDeviceDialog.js @@ -149,7 +149,7 @@ export default React.createClass({ >

- This room contains devices that you haven't seen before. + "{this.props.room.name}" contains devices that you haven't seen before.

{ warning } Unknown devices: From 336462366e033a43ac76c133bd74dea1b0222c35 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 25 Apr 2017 11:21:47 +0100 Subject: [PATCH 0122/1588] Improve country dropdown UX and expose +prefix A prefix is now exposed through a change to the API for onOptionChange. This now returns the entire country object which includes iso2, prefix etc. This also shows the prefix in the Registration and Login screens as a prefix to the phone number field. --- src/components/views/login/CountryDropdown.js | 18 +++++++-- src/components/views/login/PasswordLogin.js | 40 +++++++++++-------- .../views/login/RegistrationForm.js | 28 ++++++++----- .../views/settings/AddPhoneNumber.js | 7 ++-- 4 files changed, 61 insertions(+), 32 deletions(-) diff --git a/src/components/views/login/CountryDropdown.js b/src/components/views/login/CountryDropdown.js index 7f6b21650d..da4e770093 100644 --- a/src/components/views/login/CountryDropdown.js +++ b/src/components/views/login/CountryDropdown.js @@ -37,6 +37,7 @@ export default class CountryDropdown extends React.Component { constructor(props) { super(props); this._onSearchChange = this._onSearchChange.bind(this); + this._onOptionChange = this._onOptionChange.bind(this); this.state = { searchQuery: '', @@ -48,7 +49,7 @@ export default class CountryDropdown extends React.Component { // If no value is given, we start with the first // country selected, but our parent component // doesn't know this, therefore we do this. - this.props.onOptionChange(COUNTRIES[0].iso2); + this.props.onOptionChange(COUNTRIES[0]); } } @@ -58,6 +59,10 @@ export default class CountryDropdown extends React.Component { }); } + _onOptionChange(iso2) { + this.props.onOptionChange(COUNTRIES_BY_ISO2[iso2]); + } + _flagImgForIso2(iso2) { // Unicode Regional Indicator Symbol letter 'A' const RIS_A = 0x1F1E6; @@ -68,6 +73,10 @@ export default class CountryDropdown extends React.Component { ); } + getCountryPrefix(iso2) { + return COUNTRIES_BY_ISO2[iso2].prefix; + } + render() { const Dropdown = sdk.getComponent('elements.Dropdown'); @@ -102,9 +111,11 @@ export default class CountryDropdown extends React.Component { // values between mounting and the initial value propgating const value = this.props.value || COUNTRIES[0].iso2; + const getShortOption = this.props.isSmall ? this._flagImgForIso2 : undefined; + return {options} @@ -114,6 +125,7 @@ export default class CountryDropdown extends React.Component { CountryDropdown.propTypes = { className: React.PropTypes.string, + isSmall: React.PropTypes.bool, onOptionChange: React.PropTypes.func.isRequired, value: React.PropTypes.string, }; diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 568461817c..349dd0d139 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -90,8 +90,11 @@ class PasswordLogin extends React.Component { } onPhoneCountryChanged(country) { - this.setState({phoneCountry: country}); - this.props.onPhoneCountryChanged(country); + this.setState({ + phoneCountry: country.iso2, + phonePrefix: country.prefix, + }); + this.props.onPhoneCountryChanged(country.iso2); } onPhoneNumberChanged(ev) { @@ -121,16 +124,17 @@ class PasswordLogin extends React.Component { const mxidInputClasses = classNames({ "mx_Login_field": true, "mx_Login_username": true, + "mx_Login_field_has_prefix": true, "mx_Login_field_has_suffix": Boolean(this.props.hsDomain), }); let suffix = null; if (this.props.hsDomain) { - suffix =
+ suffix =
:{this.props.hsDomain}
; } - return
-
@
+ return
+
@
; case PasswordLogin.LOGIN_FIELD_PHONE: const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); + const prefix = this.state.phonePrefix; return
- +
+
+{prefix}
+ +
; } } diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index 4868c9de63..a0b56e30ac 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -270,7 +270,8 @@ module.exports = React.createClass({ _onPhoneCountryChange(newVal) { this.setState({ - phoneCountry: newVal, + phoneCountry: newVal.iso2, + phonePrefix: newVal.prefix, }); }, @@ -316,15 +317,22 @@ module.exports = React.createClass({ className="mx_Login_phoneCountry" value={this.state.phoneCountry} /> - + +
+
+{this.state.phonePrefix}
+ +
); diff --git a/src/components/views/settings/AddPhoneNumber.js b/src/components/views/settings/AddPhoneNumber.js index 3a348393aa..35dd5548d1 100644 --- a/src/components/views/settings/AddPhoneNumber.js +++ b/src/components/views/settings/AddPhoneNumber.js @@ -50,7 +50,7 @@ export default WithMatrixClient(React.createClass({ }, _onPhoneCountryChange: function(phoneCountry) { - this.setState({ phoneCountry: phoneCountry }); + this.setState({ phoneCountry: phoneCountry.iso2 }); }, _onPhoneNumberChange: function(ev) { @@ -149,10 +149,11 @@ export default WithMatrixClient(React.createClass({
-
+
Date: Tue, 25 Apr 2017 11:25:14 +0100 Subject: [PATCH 0123/1588] Remove redundant API for getting country prefix --- src/components/views/login/CountryDropdown.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/views/login/CountryDropdown.js b/src/components/views/login/CountryDropdown.js index da4e770093..6323b3f558 100644 --- a/src/components/views/login/CountryDropdown.js +++ b/src/components/views/login/CountryDropdown.js @@ -73,10 +73,6 @@ export default class CountryDropdown extends React.Component { ); } - getCountryPrefix(iso2) { - return COUNTRIES_BY_ISO2[iso2].prefix; - } - render() { const Dropdown = sdk.getComponent('elements.Dropdown'); From 0d4ab072500d6ec2187b901f6a7c38f1d046cfa1 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 25 Apr 2017 11:53:14 +0100 Subject: [PATCH 0124/1588] Fix not autoSelecting first item in dropdown Fixes https://github.com/vector-im/riot-web/issues/3686 --- src/components/views/elements/Dropdown.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/Dropdown.js b/src/components/views/elements/Dropdown.js index a9ecf5b669..074853bb92 100644 --- a/src/components/views/elements/Dropdown.js +++ b/src/components/views/elements/Dropdown.js @@ -115,7 +115,7 @@ export default class Dropdown extends React.Component { componentWillReceiveProps(nextProps) { this._reindexChildren(nextProps.children); - const firstChild = React.Children.toArray(nextProps.children)[0]; + const firstChild = nextProps.children[0]; this.setState({ highlightedOption: firstChild ? firstChild.key : null, }); From 1e9a2e80e99cc160fee60c45ad660aff05755ed8 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 25 Apr 2017 11:57:03 +0100 Subject: [PATCH 0125/1588] Remove empty line --- src/components/views/login/RegistrationForm.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index a0b56e30ac..2bc2b8946a 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -317,7 +317,6 @@ module.exports = React.createClass({ className="mx_Login_phoneCountry" value={this.state.phoneCountry} /> -
+{this.state.phonePrefix}
Date: Tue, 25 Apr 2017 17:05:54 +0100 Subject: [PATCH 0126/1588] Guard against no children --- src/components/views/elements/Dropdown.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/views/elements/Dropdown.js b/src/components/views/elements/Dropdown.js index 074853bb92..b4d2545e04 100644 --- a/src/components/views/elements/Dropdown.js +++ b/src/components/views/elements/Dropdown.js @@ -114,6 +114,9 @@ export default class Dropdown extends React.Component { } componentWillReceiveProps(nextProps) { + if (!nextProps.children || nextProps.children.length === 0) { + return; + } this._reindexChildren(nextProps.children); const firstChild = nextProps.children[0]; this.setState({ From 96e7479d8b038a21a94d33107c1617811dc38d48 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 25 Apr 2017 17:19:36 +0100 Subject: [PATCH 0127/1588] Show "jump to message" when message is not paginated --- src/components/structures/RoomView.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index c158b87ff3..8a355a8f6d 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1280,7 +1280,8 @@ module.exports = React.createClass({ // we want to show the bar if the read-marker is off the top of the // screen. - var showBar = (pos < 0); + // If pos is null, the event might not be paginated, so show the unread bar! + var showBar = pos < 0 || pos === null; if (this.state.showTopUnreadMessagesBar != showBar) { this.setState({showTopUnreadMessagesBar: showBar}, From bc045698b959f7fe111fa25f345a95b996a1a416 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 25 Apr 2017 18:01:56 +0100 Subject: [PATCH 0128/1588] Fix for fuse 2.7.2 As of v2.7.2, fuse.js introduces a regression where the second argument to the constructor `Fuse` is assumed to be an object. There was one instance where we were not passing any argument. This fixes that. --- src/autocomplete/EmojiProvider.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index a2d77f02a1..d488ac53ae 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -14,7 +14,7 @@ let instance = null; export default class EmojiProvider extends AutocompleteProvider { constructor() { super(EMOJI_REGEX); - this.fuse = new Fuse(EMOJI_SHORTNAMES); + this.fuse = new Fuse(EMOJI_SHORTNAMES, {}); } async getCompletions(query: string, selection: SelectionRange) { From fa9c2d137330e6e90891132fa3b4a42c4ee03afd Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 25 Apr 2017 19:21:09 +0100 Subject: [PATCH 0129/1588] Fix specifying custom server for registration Broken by https://github.com/matrix-org/matrix-react-sdk/commit/9cd7914ea51dbfb60f8b84a80cb800282476d3e4 (ServerConfig interface changed but Registration not updated) --- .../structures/login/Registration.js | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 4e0d61e716..5501a39b58 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -123,18 +123,17 @@ module.exports = React.createClass({ } }, - onHsUrlChanged: function(newHsUrl) { - this.setState({ - hsUrl: newHsUrl, + onServerConfigChange: function(config) { + let newState = {}; + if (config.hsUrl !== undefined) { + newState.hsUrl = config.hsUrl; + } + if (config.isUrl !== undefined) { + newState.isUrl = config.isUrl; + } + this.setState(newState, function() { + this._replaceClient(); }); - this._replaceClient(); - }, - - onIsUrlChanged: function(newIsUrl) { - this.setState({ - isUrl: newIsUrl, - }); - this._replaceClient(); }, _replaceClient: function() { @@ -390,8 +389,7 @@ module.exports = React.createClass({ customIsUrl={this.props.customIsUrl} defaultHsUrl={this.props.defaultHsUrl} defaultIsUrl={this.props.defaultIsUrl} - onHsUrlChanged={this.onHsUrlChanged} - onIsUrlChanged={this.onIsUrlChanged} + onServerConfigChange={this.onServerConfigChange} delayTimeMs={1000} />
From de89c1f7105232478c879d4828c687e14572d02e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 Apr 2017 22:00:50 +0100 Subject: [PATCH 0130/1588] lets make eslint at least somewhat happy Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/UserSettingsStore.js | 54 ++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index 66a872958c..4d52fa00f2 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -15,9 +15,9 @@ limitations under the License. */ 'use strict'; -var q = require("q"); -var MatrixClientPeg = require("./MatrixClientPeg"); -var Notifier = require("./Notifier"); +import q from 'q'; +import MatrixClientPeg from './MatrixClientPeg'; +import Notifier from './Notifier'; /* * TODO: Make things use this. This is all WIP - see UserSettings.js for usage. @@ -33,7 +33,7 @@ module.exports = { ], loadProfileInfo: function() { - var cli = MatrixClientPeg.get(); + const cli = MatrixClientPeg.get(); return cli.getProfileInfo(cli.credentials.userId); }, @@ -44,7 +44,7 @@ module.exports = { loadThreePids: function() { if (MatrixClientPeg.get().isGuest()) { return q({ - threepids: [] + threepids: [], }); // guests can't poke 3pid endpoint } return MatrixClientPeg.get().getThreePids(); @@ -73,19 +73,19 @@ module.exports = { Notifier.setAudioEnabled(enable); }, - changePassword: function(old_password, new_password) { - var cli = MatrixClientPeg.get(); + changePassword: function(oldPassword, newPassword) { + const cli = MatrixClientPeg.get(); - var authDict = { + const authDict = { type: 'm.login.password', user: cli.credentials.userId, - password: old_password + password: oldPassword, }; - return cli.setPassword(authDict, new_password); + return cli.setPassword(authDict, newPassword); }, - /** + /* * Returns the email pusher (pusher of type 'email') for a given * email address. Email pushers all have the same app ID, so since * pushers are unique over (app ID, pushkey), there will be at most @@ -95,8 +95,8 @@ module.exports = { if (pushers === undefined) { return undefined; } - for (var i = 0; i < pushers.length; ++i) { - if (pushers[i].kind == 'email' && pushers[i].pushkey == address) { + for (let i = 0; i < pushers.length; ++i) { + if (pushers[i].kind === 'email' && pushers[i].pushkey === address) { return pushers[i]; } } @@ -110,7 +110,7 @@ module.exports = { addEmailPusher: function(address, data) { return MatrixClientPeg.get().setPusher({ kind: 'email', - app_id: "m.email", + app_id: 'm.email', pushkey: address, app_display_name: 'Email Notifications', device_display_name: address, @@ -121,46 +121,46 @@ module.exports = { }, getUrlPreviewsDisabled: function() { - var event = MatrixClientPeg.get().getAccountData("org.matrix.preview_urls"); + const event = MatrixClientPeg.get().getAccountData('org.matrix.preview_urls'); return (event && event.getContent().disable); }, setUrlPreviewsDisabled: function(disabled) { // FIXME: handle errors - return MatrixClientPeg.get().setAccountData("org.matrix.preview_urls", { - disable: disabled + return MatrixClientPeg.get().setAccountData('org.matrix.preview_urls', { + disable: disabled, }); }, getSyncedSettings: function() { - var event = MatrixClientPeg.get().getAccountData("im.vector.web.settings"); + const event = MatrixClientPeg.get().getAccountData('im.vector.web.settings'); return event ? event.getContent() : {}; }, getSyncedSetting: function(type, defaultValue = null) { - var settings = this.getSyncedSettings(); + const settings = this.getSyncedSettings(); return settings.hasOwnProperty(type) ? settings[type] : null; }, setSyncedSetting: function(type, value) { - var settings = this.getSyncedSettings(); + const settings = this.getSyncedSettings(); settings[type] = value; // FIXME: handle errors - return MatrixClientPeg.get().setAccountData("im.vector.web.settings", settings); + return MatrixClientPeg.get().setAccountData('im.vector.web.settings', settings); }, getLocalSettings: function() { - var localSettingsString = localStorage.getItem('mx_local_settings') || '{}'; + const localSettingsString = localStorage.getItem('mx_local_settings') || '{}'; return JSON.parse(localSettingsString); }, getLocalSetting: function(type, defaultValue = null) { - var settings = this.getLocalSettings(); + const settings = this.getLocalSettings(); return settings.hasOwnProperty(type) ? settings[type] : null; }, setLocalSetting: function(type, value) { - var settings = this.getLocalSettings(); + const settings = this.getLocalSettings(); settings[type] = value; // FIXME: handle errors localStorage.setItem('mx_local_settings', JSON.stringify(settings)); @@ -171,8 +171,8 @@ module.exports = { if (MatrixClientPeg.get().isGuest()) return false; if (localStorage.getItem(`mx_labs_feature_${feature}`) === null) { - for (var i = 0; i < this.LABS_FEATURES.length; i++) { - var f = this.LABS_FEATURES[i]; + for (let i = 0; i < this.LABS_FEATURES.length; i++) { + const f = this.LABS_FEATURES[i]; if (f.id === feature) { return f.default; } @@ -183,5 +183,5 @@ module.exports = { setFeatureEnabled: function(feature: string, enabled: boolean) { localStorage.setItem(`mx_labs_feature_${feature}`, enabled); - } + }, }; From cc53825b062c59e9fc02f7e7a429571c4067c3ff Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 Apr 2017 22:01:35 +0100 Subject: [PATCH 0131/1588] fix defaultValue on getLocalSetting and getSyncedSetting Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/UserSettingsStore.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index 4d52fa00f2..9de291249f 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -139,7 +139,7 @@ module.exports = { getSyncedSetting: function(type, defaultValue = null) { const 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) { const settings = this.getLocalSettings(); - return settings.hasOwnProperty(type) ? settings[type] : null; + return settings.hasOwnProperty(type) ? settings[type] : defaultValue; }, setLocalSetting: function(type, value) { From 6cbd04045d67197679a318944bdd40eb9d68c697 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 Apr 2017 22:17:25 +0100 Subject: [PATCH 0132/1588] change the now working defaults to what they effectively were when defaultValue was broken (hopefully tests now pass) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/MessageComposer.js | 2 +- src/components/views/rooms/MessageComposerInput.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 8a3b128908..88230062fe 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -50,7 +50,7 @@ export default class MessageComposer extends React.Component { inputState: { style: [], blockType: null, - isRichtextEnabled: UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', true), + isRichtextEnabled: UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false), wordCount: 0, }, showFormatting: UserSettingsStore.getSyncedSetting('MessageComposer.showFormatting', false), diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 417d003226..8efd2fa579 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -94,7 +94,7 @@ export default class MessageComposerInput extends React.Component { this.setDisplayedCompletion = this.setDisplayedCompletion.bind(this); this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this); - const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', true); + const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false); this.state = { // whether we're in rich text or markdown mode From 04f44e920192f8528b6d016a0bef2232d2467043 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 26 Apr 2017 13:48:03 +0100 Subject: [PATCH 0133/1588] Style fixes for LoggedInView PRing this becaise I was going to change LoggedInView, so did some code style updates, but then decided the do the change elsewhere. --- src/components/structures/LoggedInView.js | 25 ++++++++++++----------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 4c012b42a8..9f01b0082b 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -1,5 +1,6 @@ /* Copyright 2015, 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. @@ -162,19 +163,19 @@ export default React.createClass({ }, render: function() { - var LeftPanel = sdk.getComponent('structures.LeftPanel'); - var RightPanel = sdk.getComponent('structures.RightPanel'); - var RoomView = sdk.getComponent('structures.RoomView'); - var UserSettings = sdk.getComponent('structures.UserSettings'); - var CreateRoom = sdk.getComponent('structures.CreateRoom'); - var RoomDirectory = sdk.getComponent('structures.RoomDirectory'); - var HomePage = sdk.getComponent('structures.HomePage'); - var MatrixToolbar = sdk.getComponent('globals.MatrixToolbar'); - var GuestWarningBar = sdk.getComponent('globals.GuestWarningBar'); - var NewVersionBar = sdk.getComponent('globals.NewVersionBar'); + const LeftPanel = sdk.getComponent('structures.LeftPanel'); + const RightPanel = sdk.getComponent('structures.RightPanel'); + const RoomView = sdk.getComponent('structures.RoomView'); + const UserSettings = sdk.getComponent('structures.UserSettings'); + const CreateRoom = sdk.getComponent('structures.CreateRoom'); + const RoomDirectory = sdk.getComponent('structures.RoomDirectory'); + const HomePage = sdk.getComponent('structures.HomePage'); + const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar'); + const GuestWarningBar = sdk.getComponent('globals.GuestWarningBar'); + const NewVersionBar = sdk.getComponent('globals.NewVersionBar'); - var page_element; - var right_panel = ''; + let page_element; + let right_panel = ''; switch (this.props.page_type) { case PageTypes.RoomView: From df283dae4767cbfcb4b073a71a1b384790dae70b Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 26 Apr 2017 14:05:09 +0100 Subject: [PATCH 0134/1588] Show spinner until first sync has completed Shows the 'forward paginating' spinner until the first sync has completed. Fixes https://github.com/vector-im/riot-web/issues/3318 --- src/components/structures/TimelinePanel.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 8babdaae4a..dde86e1ce9 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -1058,11 +1058,18 @@ var TimelinePanel = React.createClass({ // events when viewing historical messages, we get stuck in a loop // of paginating our way through the entire history of the room. var stickyBottom = !this._timelineWindow.canPaginate(EventTimeline.FORWARDS); + + // If the state is PREPARED, we're still waiting for the js-sdk to sync with + // the HS and fetch the latest events, so we are effectively forward paginating. + const forwardPaginating = ( + this.state.forwardPaginating || MatrixClientPeg.get().getSyncState() == 'PREPARED' + ); + return (