From b979a16199fe1a14474dd7d3b9e32ba0eaffd03f Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Wed, 1 Jun 2016 16:54:21 +0530 Subject: [PATCH 001/222] initial version of autocomplete --- package.json | 1 + src/autocomplete/AutocompleteProvider.js | 3 + src/autocomplete/Autocompleter.js | 7 ++ src/autocomplete/CommandProvider.js | 65 ++++++++++++++++++ src/components/views/rooms/Autocomplete.js | 67 +++++++++++++++++++ src/components/views/rooms/MessageComposer.js | 18 ++++- .../views/rooms/MessageComposerInput.js | 4 ++ 7 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 src/autocomplete/AutocompleteProvider.js create mode 100644 src/autocomplete/Autocompleter.js create mode 100644 src/autocomplete/CommandProvider.js create mode 100644 src/components/views/rooms/Autocomplete.js diff --git a/package.json b/package.json index 3f4a862f6f..d46e9b2621 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "favico.js": "^0.3.10", "filesize": "^3.1.2", "flux": "^2.0.3", + "fuse.js": "^2.2.0", "glob": "^5.0.14", "highlight.js": "^8.9.1", "linkifyjs": "^2.0.0-beta.4", diff --git a/src/autocomplete/AutocompleteProvider.js b/src/autocomplete/AutocompleteProvider.js new file mode 100644 index 0000000000..3b2aae920b --- /dev/null +++ b/src/autocomplete/AutocompleteProvider.js @@ -0,0 +1,3 @@ +export default class AutocompleteProvider { + +} diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js new file mode 100644 index 0000000000..e49dbb7ad6 --- /dev/null +++ b/src/autocomplete/Autocompleter.js @@ -0,0 +1,7 @@ +import CommandProvider from './CommandProvider'; + +const COMPLETERS = [CommandProvider].map(completer => new completer()); + +export function getCompletions(query: String) { + return COMPLETERS.map(completer => completer.getCompletions(query)); +} diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js new file mode 100644 index 0000000000..cc95d96fd3 --- /dev/null +++ b/src/autocomplete/CommandProvider.js @@ -0,0 +1,65 @@ +import AutocompleteProvider from './AutocompleteProvider'; +import Q from 'q'; +import Fuse from 'fuse.js'; + +const COMMANDS = [ + { + command: '/me', + args: '', + description: 'Displays action' + }, + { + command: '/ban', + args: ' [reason]', + description: 'Bans user with given id' + }, + { + command: '/deop' + }, + { + command: '/encrypt' + }, + { + command: '/invite' + }, + { + command: '/join', + args: '', + description: 'Joins room with given alias' + }, + { + command: '/kick', + args: ' [reason]', + description: 'Kicks user with given id' + }, + { + command: '/nick', + args: '', + description: 'Changes your display nickname' + } +]; + +export default class CommandProvider extends AutocompleteProvider { + constructor() { + super(); + this.fuse = new Fuse(COMMANDS, { + keys: ['command', 'args', 'description'] + }); + } + + getCompletions(query: String) { + let completions = []; + const matches = query.match(/(^\/\w+)/); + if(!!matches) { + const command = matches[0]; + completions = this.fuse.search(command).map(result => { + return { + title: result.command, + subtitle: result.args, + description: result.description + }; + }); + } + return Q.when(completions); + } +} diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js new file mode 100644 index 0000000000..80208892b0 --- /dev/null +++ b/src/components/views/rooms/Autocomplete.js @@ -0,0 +1,67 @@ +import React from 'react'; + +import {getCompletions} from '../../../autocomplete/Autocompleter'; + +export default class Autocomplete extends React.Component { + constructor(props) { + super(props); + this.state = { + completions: [] + }; + } + + componentWillReceiveProps(props, state) { + getCompletions(props.query)[0].then(completions => { + console.log(completions); + this.setState({ + completions + }); + }); + } + + render() { + const pinElement = document.querySelector(this.props.pinSelector); + if(!pinElement) return null; + + const position = pinElement.getBoundingClientRect(); + + const style = { + position: 'fixed', + border: '1px solid gray', + background: 'white', + borderRadius: '4px' + }; + + this.props.pinTo.forEach(direction => { + console.log(`${direction} = ${position[direction]}`); + style[direction] = position[direction]; + }); + + const renderedCompletions = this.state.completions.map((completion, i) => { + return ( +
+ {completion.title} + {completion.subtitle} + {completion.description} +
+ ); + }); + + return ( +
+ {renderedCompletions} +
+ ); + } +} + +Autocomplete.propTypes = { + // the query string for which to show autocomplete suggestions + query: React.PropTypes.string.isRequired, + + // CSS selector indicating which element to pin the autocomplete to + pinSelector: React.PropTypes.string.isRequired, + + // attributes on which the autocomplete should match the pinElement + pinTo: React.PropTypes.array.isRequired +}; diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 20785c4c70..2d17accd45 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -20,6 +20,7 @@ var MatrixClientPeg = require('../../../MatrixClientPeg'); var Modal = require('../../../Modal'); var sdk = require('../../../index'); var dis = require('../../../dispatcher'); +import Autocomplete from './Autocomplete'; module.exports = React.createClass({ @@ -45,6 +46,12 @@ module.exports = React.createClass({ opacity: React.PropTypes.number, }, + getInitialState: function () { + return { + autocompleteQuery: '' + }; + }, + onUploadClick: function(ev) { this.refs.uploadInput.click(); }, @@ -117,6 +124,12 @@ module.exports = React.createClass({ }); }, + onInputContentChanged(content: String) { + this.setState({ + autocompleteQuery: content + }) + }, + render: function() { var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId); var uploadInputStyle = {display: 'none'}; @@ -170,7 +183,8 @@ module.exports = React.createClass({ controls.push( , + onResize={this.props.onResize} room={this.props.room} + onContentChanged={(content) => this.onInputContentChanged(content) } />, uploadButton, hangupButton, callButton, @@ -191,6 +205,8 @@ module.exports = React.createClass({ {controls} + + ); } diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 733d9e6056..e8db496abf 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -73,6 +73,8 @@ module.exports = React.createClass({ // js-sdk Room object room: React.PropTypes.object.isRequired, + + onContentChanged: React.PropTypes.func }, componentWillMount: function() { @@ -276,6 +278,8 @@ module.exports = React.createClass({ { this.resizeInput(); } + + this.props.onContentChanged && this.props.onContentChanged(this.refs.textarea.value); }, onEnter: function(ev) { From 4bc8ec3e6dac96f92458c1d5186ea20539fb5fcc Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Sun, 12 Jun 2016 17:02:46 +0530 Subject: [PATCH 002/222] room, user, ddg autocomplete providers (wip) --- package.json | 3 +- src/MatrixClientPeg.js | 4 +- src/autocomplete/AutocompleteProvider.js | 4 +- src/autocomplete/Autocompleter.js | 17 ++++++- src/autocomplete/CommandProvider.js | 4 ++ src/autocomplete/DuckDuckGoProvider.js | 35 ++++++++++++++ src/autocomplete/RoomProvider.js | 31 +++++++++++++ src/autocomplete/UserProvider.js | 31 +++++++++++++ src/components/views/rooms/Autocomplete.js | 53 ++++++++++++++++------ 9 files changed, 163 insertions(+), 19 deletions(-) create mode 100644 src/autocomplete/DuckDuckGoProvider.js create mode 100644 src/autocomplete/RoomProvider.js create mode 100644 src/autocomplete/UserProvider.js diff --git a/package.json b/package.json index 5c9a67c734..a736024da6 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,8 @@ "react-dom": "^15.0.1", "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#c3d942e", "sanitize-html": "^1.11.1", - "velocity-vector": "vector-im/velocity#059e3b2" + "velocity-vector": "vector-im/velocity#059e3b2", + "whatwg-fetch": "^1.0.0" }, "//babelversion": [ "brief experiments with babel6 seems to show that it generates source ", diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index cc96503316..c7b77ab88c 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -20,7 +20,7 @@ limitations under the License. var Matrix = require("matrix-js-sdk"); var GuestAccess = require("./GuestAccess"); -var matrixClient = null; +let matrixClient: MatrixClient = null; var localStorage = window.localStorage; @@ -82,7 +82,7 @@ class MatrixClient { this.guestAccess = guestAccess; } - get() { + get(): MatrixClient { return matrixClient; } diff --git a/src/autocomplete/AutocompleteProvider.js b/src/autocomplete/AutocompleteProvider.js index 3b2aae920b..61158d2b56 100644 --- a/src/autocomplete/AutocompleteProvider.js +++ b/src/autocomplete/AutocompleteProvider.js @@ -1,3 +1,5 @@ export default class AutocompleteProvider { - + getName(): string { + return 'Default Provider'; + } } diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index e49dbb7ad6..a8ed2da59a 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -1,7 +1,20 @@ import CommandProvider from './CommandProvider'; +import DuckDuckGoProvider from './DuckDuckGoProvider'; +import RoomProvider from './RoomProvider'; +import UserProvider from './UserProvider'; -const COMPLETERS = [CommandProvider].map(completer => new completer()); +const PROVIDERS = [ + CommandProvider, + DuckDuckGoProvider, + RoomProvider, + UserProvider +].map(completer => new completer()); export function getCompletions(query: String) { - return COMPLETERS.map(completer => completer.getCompletions(query)); + return PROVIDERS.map(provider => { + return { + completions: provider.getCompletions(query), + provider + }; + }); } diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index cc95d96fd3..e2eac47d16 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -62,4 +62,8 @@ export default class CommandProvider extends AutocompleteProvider { } return Q.when(completions); } + + getName() { + return 'Commands'; + } } diff --git a/src/autocomplete/DuckDuckGoProvider.js b/src/autocomplete/DuckDuckGoProvider.js new file mode 100644 index 0000000000..6545b96cbd --- /dev/null +++ b/src/autocomplete/DuckDuckGoProvider.js @@ -0,0 +1,35 @@ +import AutocompleteProvider from './AutocompleteProvider'; +import Q from 'q'; +import 'whatwg-fetch'; + +const DDG_REGEX = /\/ddg\w+(.+)$/; +const REFERER = 'vector'; + +export default class DuckDuckGoProvider extends AutocompleteProvider { + static getQueryUri(query: String) { + return `http://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&t=${encodeURIComponent(REFERER)}`; + } + + getCompletions(query: String) { + if(!query) + return Q.when([]); + + let promise = Q.defer(); + fetch(DuckDuckGoProvider.getQueryUri(query), { + method: 'GET' + }).then(response => { + let results = response.Results.map(result => { + return { + title: result.Text, + description: result.Result + }; + }); + promise.resolve(results); + }); + return promise; + } + + getName() { + return 'Results from DuckDuckGo'; + } +} diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js new file mode 100644 index 0000000000..26dc5733da --- /dev/null +++ b/src/autocomplete/RoomProvider.js @@ -0,0 +1,31 @@ +import AutocompleteProvider from './AutocompleteProvider'; +import Q from 'q'; +import MatrixClientPeg from '../MatrixClientPeg'; + +const ROOM_REGEX = /(?=#)[^\s]*/g; + +export default class RoomProvider extends AutocompleteProvider { + constructor() { + super(); + } + + getCompletions(query: String) { + let client = MatrixClientPeg.get(); + let completions = []; + const matches = query.match(ROOM_REGEX); + if(!!matches) { + const command = matches[0]; + completions = client.getRooms().map(room => { + return { + title: room.name, + subtitle: room.roomId + }; + }); + } + return Q.when(completions); + } + + getName() { + return 'Rooms'; + } +} diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js new file mode 100644 index 0000000000..791dd55a33 --- /dev/null +++ b/src/autocomplete/UserProvider.js @@ -0,0 +1,31 @@ +import AutocompleteProvider from './AutocompleteProvider'; +import Q from 'q'; +import MatrixClientPeg from '../MatrixClientPeg'; + +const ROOM_REGEX = /@[^\s]*/g; + +export default class UserProvider extends AutocompleteProvider { + constructor() { + super(); + } + + getCompletions(query: String) { + let client = MatrixClientPeg.get(); + let completions = []; + const matches = query.match(ROOM_REGEX); + if(!!matches) { + const command = matches[0]; + completions = client.getUsers().map(user => { + return { + title: user.displayName, + description: user.userId + }; + }); + } + return Q.when(completions); + } + + getName() { + return 'Users'; + } +} diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index 80208892b0..4bc4102070 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -11,11 +11,28 @@ export default class Autocomplete extends React.Component { } componentWillReceiveProps(props, state) { - getCompletions(props.query)[0].then(completions => { - console.log(completions); - this.setState({ - completions - }); + getCompletions(props.query).map(completionResult => { + try { + completionResult.completions.then(completions => { + let i = this.state.completions.findIndex( + completion => completion.provider === completionResult.provider + ); + + i = i == -1 ? this.state.completions.length : i; + console.log(completionResult); + let newCompletions = Object.assign([], this.state.completions); + completionResult.completions = completions; + newCompletions[i] = completionResult; + console.log(newCompletions); + this.setState({ + completions: newCompletions + }); + }, err => { + + }); + } catch (e) { + // An error in one provider shouldn't mess up the rest. + } }); } @@ -33,18 +50,28 @@ export default class Autocomplete extends React.Component { }; this.props.pinTo.forEach(direction => { - console.log(`${direction} = ${position[direction]}`); style[direction] = position[direction]; }); - const renderedCompletions = this.state.completions.map((completion, i) => { - return ( -
- {completion.title} - {completion.subtitle} - {completion.description} + const renderedCompletions = this.state.completions.map((completionResult, i) => { + console.log(completionResult); + let completions = completionResult.completions.map((completion, i) => { + return ( +
+ {completion.title} + {completion.subtitle} + {completion.description} +
+ ); + }); + + + return completions.length > 0 ? ( +
+ {completionResult.provider.getName()} + {completions}
- ); + ) : null; }); return ( From b9d7743e5a30f91c71c6d57c5f0e2a0695a4852d Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Fri, 17 Jun 2016 04:58:09 +0530 Subject: [PATCH 003/222] Emoji provider, DDG working, style improvements --- package.json | 2 + src/autocomplete/Autocompleter.js | 4 +- src/autocomplete/Components.js | 13 +++++ src/autocomplete/DuckDuckGoProvider.js | 47 +++++++++++++------ src/autocomplete/EmojiProvider.js | 41 ++++++++++++++++ src/components/views/rooms/Autocomplete.js | 43 +++++++++-------- src/components/views/rooms/MessageComposer.js | 5 +- .../views/rooms/MessageComposerInput.js | 9 +++- 8 files changed, 127 insertions(+), 37 deletions(-) create mode 100644 src/autocomplete/Components.js create mode 100644 src/autocomplete/EmojiProvider.js diff --git a/package.json b/package.json index 586f060b01..82ac307710 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "draft-js-export-html": "^0.2.2", "draft-js-export-markdown": "^0.2.0", "draft-js-import-markdown": "^0.1.6", + "emojione": "^2.2.2", "favico.js": "^0.3.10", "filesize": "^3.1.2", "flux": "^2.0.3", @@ -39,6 +40,7 @@ "optimist": "^0.6.1", "q": "^1.4.1", "react": "^15.0.1", + "react-addons-css-transition-group": "^15.1.0", "react-dom": "^15.0.1", "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#c3d942e", "sanitize-html": "^1.11.1", diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index a8ed2da59a..c8f3134a3b 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -2,12 +2,14 @@ import CommandProvider from './CommandProvider'; import DuckDuckGoProvider from './DuckDuckGoProvider'; import RoomProvider from './RoomProvider'; import UserProvider from './UserProvider'; +import EmojiProvider from './EmojiProvider'; const PROVIDERS = [ CommandProvider, DuckDuckGoProvider, RoomProvider, - UserProvider + UserProvider, + EmojiProvider ].map(completer => new completer()); export function getCompletions(query: String) { diff --git a/src/autocomplete/Components.js b/src/autocomplete/Components.js new file mode 100644 index 0000000000..cb7d56f9bf --- /dev/null +++ b/src/autocomplete/Components.js @@ -0,0 +1,13 @@ +export function TextualCompletion(props: { + title: ?string, + subtitle: ?string, + description: ?string +}) { + return ( +
+ {completion.title} + {completion.subtitle} + {completion.description} +
+ ); +} diff --git a/src/autocomplete/DuckDuckGoProvider.js b/src/autocomplete/DuckDuckGoProvider.js index 6545b96cbd..2acd892498 100644 --- a/src/autocomplete/DuckDuckGoProvider.js +++ b/src/autocomplete/DuckDuckGoProvider.js @@ -2,31 +2,50 @@ import AutocompleteProvider from './AutocompleteProvider'; import Q from 'q'; import 'whatwg-fetch'; -const DDG_REGEX = /\/ddg\w+(.+)$/; +const DDG_REGEX = /\/ddg\s+(.+)$/; const REFERER = 'vector'; export default class DuckDuckGoProvider extends AutocompleteProvider { static getQueryUri(query: String) { - return `http://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&t=${encodeURIComponent(REFERER)}`; + return `http://api.duckduckgo.com/?q=${encodeURIComponent(query)}` + + `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERER)}`; } getCompletions(query: String) { - if(!query) + let match = DDG_REGEX.exec(query); + if(!query || !match) return Q.when([]); - let promise = Q.defer(); - fetch(DuckDuckGoProvider.getQueryUri(query), { + return fetch(DuckDuckGoProvider.getQueryUri(match[1]), { method: 'GET' - }).then(response => { - let results = response.Results.map(result => { - return { - title: result.Text, - description: result.Result - }; + }) + .then(response => response.json()) + .then(json => { + let results = json.Results.map(result => { + return { + title: result.Text, + description: result.Result + }; + }); + if(json.Answer) { + results.unshift({ + title: json.Answer, + description: json.AnswerType + }); + } + if(json.RelatedTopics && json.RelatedTopics.length > 0) { + results.unshift({ + title: json.RelatedTopics[0].Text + }); + } + if(json.AbstractText) { + results.unshift({ + title: json.AbstractText + }); + } + // console.log(results); + return results; }); - promise.resolve(results); - }); - return promise; } getName() { diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js new file mode 100644 index 0000000000..fefd00a7fd --- /dev/null +++ b/src/autocomplete/EmojiProvider.js @@ -0,0 +1,41 @@ +import AutocompleteProvider from './AutocompleteProvider'; +import Q from 'q'; +import {emojioneList, shortnameToImage} from 'emojione'; +import Fuse from 'fuse.js'; + +const EMOJI_REGEX = /:\w*:?/g; +const EMOJI_SHORTNAMES = Object.keys(emojioneList); + +export default class EmojiProvider extends AutocompleteProvider { + constructor() { + super(); + console.log(EMOJI_SHORTNAMES); + this.fuse = new Fuse(EMOJI_SHORTNAMES); + } + + getCompletions(query: String) { + let completions = []; + const matches = query.match(EMOJI_REGEX); + console.log(matches); + if(!!matches) { + const command = matches[0]; + completions = this.fuse.search(command).map(result => { + let shortname = EMOJI_SHORTNAMES[result]; + let imageHTML = shortnameToImage(shortname); + return { + title: shortname, + component: ( +
+ {shortname} +
+ ) + }; + }).slice(0, 4); + } + return Q.when(completions); + } + + getName() { + return 'Emoji'; + } +} diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index 4bc4102070..673cdc5bf5 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -1,4 +1,5 @@ import React from 'react'; +import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; import {getCompletions} from '../../../autocomplete/Autocompleter'; @@ -11,8 +12,11 @@ export default class Autocomplete extends React.Component { } componentWillReceiveProps(props, state) { + if(props.query == this.props.query) return; + getCompletions(props.query).map(completionResult => { try { + console.log(`${completionResult.provider.getName()}: ${JSON.stringify(completionResult.completions)}`); completionResult.completions.then(completions => { let i = this.state.completions.findIndex( completion => completion.provider === completionResult.provider @@ -23,15 +27,16 @@ export default class Autocomplete extends React.Component { let newCompletions = Object.assign([], this.state.completions); completionResult.completions = completions; newCompletions[i] = completionResult; - console.log(newCompletions); + // console.log(newCompletions); this.setState({ completions: newCompletions }); }, err => { - + console.error(err); }); } catch (e) { // An error in one provider shouldn't mess up the rest. + console.error(e); } }); } @@ -42,23 +47,19 @@ export default class Autocomplete extends React.Component { const position = pinElement.getBoundingClientRect(); - const style = { - position: 'fixed', - border: '1px solid gray', - background: 'white', - borderRadius: '4px' - }; - this.props.pinTo.forEach(direction => { - style[direction] = position[direction]; - }); const renderedCompletions = this.state.completions.map((completionResult, i) => { - console.log(completionResult); + // console.log(completionResult); let completions = completionResult.completions.map((completion, i) => { + let Component = completion.component; + if(Component) { + return Component; + } + return ( -
- {completion.title} +
+ {completion.title} {completion.subtitle} {completion.description}
@@ -67,16 +68,20 @@ export default class Autocomplete extends React.Component { return completions.length > 0 ? ( -
- {completionResult.provider.getName()} - {completions} +
+ {completionResult.provider.getName()} + + {completions} +
) : null; }); return ( -
- {renderedCompletions} +
+ + {renderedCompletions} +
); } diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 889415f243..0f9dd86b09 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -212,13 +212,14 @@ module.exports = React.createClass({ return (
+
+ +
{controls}
- -
); } diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 8a0ee7d8a8..a9ee764864 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -352,6 +352,10 @@ export default class MessageComposerInput extends React.Component { } else { this.onFinishedTyping(); } + + if(this.props.onContentChanged) { + this.props.onContentChanged(editorState.getCurrentContent().getPlainText()); + } } enableRichtext(enabled: boolean) { @@ -521,5 +525,8 @@ MessageComposerInput.propTypes = { onResize: React.PropTypes.func, // js-sdk Room object - room: React.PropTypes.object.isRequired + room: React.PropTypes.object.isRequired, + + // called with current plaintext content (as a string) whenever it changes + onContentChanged: React.PropTypes.func }; From 4af983ed902c767b87c0c39890753f0432fef91c Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Mon, 20 Jun 2016 13:52:55 +0530 Subject: [PATCH 004/222] Style changes and improvements in autocomplete --- src/autocomplete/Autocompleter.js | 4 +- src/autocomplete/CommandProvider.js | 11 ++- src/autocomplete/DuckDuckGoProvider.js | 9 ++ src/autocomplete/EmojiProvider.js | 16 ++-- src/autocomplete/RoomProvider.js | 14 ++- src/autocomplete/UserProvider.js | 19 +++- src/components/structures/RoomView.js | 13 ++- src/components/views/rooms/Autocomplete.js | 32 +++---- src/components/views/rooms/MessageComposer.js | 90 ++++++++++--------- .../views/rooms/MessageComposerInput.js | 20 ++--- 10 files changed, 135 insertions(+), 93 deletions(-) diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index c8f3134a3b..95669d5e0f 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -5,12 +5,12 @@ import UserProvider from './UserProvider'; import EmojiProvider from './EmojiProvider'; const PROVIDERS = [ + UserProvider, CommandProvider, DuckDuckGoProvider, RoomProvider, - UserProvider, EmojiProvider -].map(completer => new completer()); +].map(completer => completer.getInstance()); export function getCompletions(query: String) { return PROVIDERS.map(provider => { diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index e2eac47d16..7b950c0ed0 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -39,6 +39,8 @@ const COMMANDS = [ } ]; +let instance = null; + export default class CommandProvider extends AutocompleteProvider { constructor() { super(); @@ -49,7 +51,7 @@ export default class CommandProvider extends AutocompleteProvider { getCompletions(query: String) { let completions = []; - const matches = query.match(/(^\/\w+)/); + const matches = query.match(/(^\/\w*)/); if(!!matches) { const command = matches[0]; completions = this.fuse.search(command).map(result => { @@ -66,4 +68,11 @@ export default class CommandProvider extends AutocompleteProvider { getName() { return 'Commands'; } + + static getInstance(): CommandProvider { + if(instance == null) + instance = new CommandProvider(); + + return instance; + } } diff --git a/src/autocomplete/DuckDuckGoProvider.js b/src/autocomplete/DuckDuckGoProvider.js index 2acd892498..496ce72e46 100644 --- a/src/autocomplete/DuckDuckGoProvider.js +++ b/src/autocomplete/DuckDuckGoProvider.js @@ -5,6 +5,8 @@ import 'whatwg-fetch'; const DDG_REGEX = /\/ddg\s+(.+)$/; const REFERER = 'vector'; +let instance = null; + export default class DuckDuckGoProvider extends AutocompleteProvider { static getQueryUri(query: String) { return `http://api.duckduckgo.com/?q=${encodeURIComponent(query)}` @@ -51,4 +53,11 @@ export default class DuckDuckGoProvider extends AutocompleteProvider { getName() { return 'Results from DuckDuckGo'; } + + static getInstance(): DuckDuckGoProvider { + if(instance == null) + instance = new DuckDuckGoProvider(); + + return instance; + } } diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index fefd00a7fd..684414d72a 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -6,19 +6,19 @@ import Fuse from 'fuse.js'; const EMOJI_REGEX = /:\w*:?/g; const EMOJI_SHORTNAMES = Object.keys(emojioneList); +let instance = null; + export default class EmojiProvider extends AutocompleteProvider { constructor() { super(); - console.log(EMOJI_SHORTNAMES); this.fuse = new Fuse(EMOJI_SHORTNAMES); } getCompletions(query: String) { let completions = []; - const matches = query.match(EMOJI_REGEX); - console.log(matches); - if(!!matches) { - const command = matches[0]; + let matches = query.match(EMOJI_REGEX); + let command = matches && matches[0]; + if(command) { completions = this.fuse.search(command).map(result => { let shortname = EMOJI_SHORTNAMES[result]; let imageHTML = shortnameToImage(shortname); @@ -38,4 +38,10 @@ export default class EmojiProvider extends AutocompleteProvider { getName() { return 'Emoji'; } + + static getInstance() { + if(instance == null) + instance = new EmojiProvider(); + return instance; + } } diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index 26dc5733da..c61541617d 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -1,9 +1,12 @@ import AutocompleteProvider from './AutocompleteProvider'; import Q from 'q'; import MatrixClientPeg from '../MatrixClientPeg'; +import Fuse from 'fuse.js'; const ROOM_REGEX = /(?=#)[^\s]*/g; +let instance = null; + export default class RoomProvider extends AutocompleteProvider { constructor() { super(); @@ -13,8 +16,8 @@ export default class RoomProvider extends AutocompleteProvider { let client = MatrixClientPeg.get(); let completions = []; const matches = query.match(ROOM_REGEX); - if(!!matches) { - const command = matches[0]; + const command = matches && matches[0]; + if(command) { completions = client.getRooms().map(room => { return { title: room.name, @@ -28,4 +31,11 @@ export default class RoomProvider extends AutocompleteProvider { getName() { return 'Rooms'; } + + static getInstance() { + if(instance == null) + instance = new RoomProvider(); + + return instance; + } } diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 791dd55a33..c850cea7d9 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -4,20 +4,22 @@ import MatrixClientPeg from '../MatrixClientPeg'; const ROOM_REGEX = /@[^\s]*/g; +let instance = null; + export default class UserProvider extends AutocompleteProvider { constructor() { super(); + this.users = []; } getCompletions(query: String) { - let client = MatrixClientPeg.get(); let completions = []; const matches = query.match(ROOM_REGEX); if(!!matches) { const command = matches[0]; - completions = client.getUsers().map(user => { + completions = this.users.map(user => { return { - title: user.displayName, + title: user.displayName || user.userId, description: user.userId }; }); @@ -28,4 +30,15 @@ export default class UserProvider extends AutocompleteProvider { getName() { return 'Users'; } + + setUserList(users) { + console.log('setUserList'); + this.users = users; + } + + static getInstance(): UserProvider { + if(instance == null) + instance = new UserProvider(); + return instance; + } } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index e1b4c00175..9d952e611e 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -41,6 +41,8 @@ var rate_limited_func = require('../../ratelimitedfunc'); var ObjectUtils = require('../../ObjectUtils'); var MatrixTools = require('../../MatrixTools'); +import UserProvider from '../../autocomplete/UserProvider'; + var DEBUG = false; if (DEBUG) { @@ -495,21 +497,26 @@ module.exports = React.createClass({ } }, - _updateTabCompleteList: new rate_limited_func(function() { + _updateTabCompleteList: function() { var cli = MatrixClientPeg.get(); + console.log('_updateTabCompleteList'); + console.log(this.state.room); + console.trace(); - if (!this.state.room || !this.tabComplete) { + if (!this.state.room) { return; } var members = this.state.room.getJoinedMembers().filter(function(member) { if (member.userId !== cli.credentials.userId) return true; }); + + UserProvider.getInstance().setUserList(members); this.tabComplete.setCompletionList( MemberEntry.fromMemberList(members).concat( CommandEntry.fromCommands(SlashCommands.getCommandList()) ) ); - }, 500), + }, componentDidUpdate: function() { if (this.refs.roomView) { diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index 673cdc5bf5..0218a88195 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -16,14 +16,14 @@ export default class Autocomplete extends React.Component { getCompletions(props.query).map(completionResult => { try { - console.log(`${completionResult.provider.getName()}: ${JSON.stringify(completionResult.completions)}`); + // console.log(`${completionResult.provider.getName()}: ${JSON.stringify(completionResult.completions)}`); completionResult.completions.then(completions => { let i = this.state.completions.findIndex( completion => completion.provider === completionResult.provider ); i = i == -1 ? this.state.completions.length : i; - console.log(completionResult); + // console.log(completionResult); let newCompletions = Object.assign([], this.state.completions); completionResult.completions = completions; newCompletions[i] = completionResult; @@ -42,13 +42,6 @@ export default class Autocomplete extends React.Component { } render() { - const pinElement = document.querySelector(this.props.pinSelector); - if(!pinElement) return null; - - const position = pinElement.getBoundingClientRect(); - - - const renderedCompletions = this.state.completions.map((completionResult, i) => { // console.log(completionResult); let completions = completionResult.completions.map((completion, i) => { @@ -58,10 +51,11 @@ export default class Autocomplete extends React.Component { } return ( -
- {completion.title} - {completion.subtitle} - {completion.description} +
+ {completion.title} + {completion.subtitle} + + {completion.description}
); }); @@ -70,7 +64,7 @@ export default class Autocomplete extends React.Component { return completions.length > 0 ? (
{completionResult.provider.getName()} - + {completions}
@@ -79,7 +73,7 @@ export default class Autocomplete extends React.Component { return (
- + {renderedCompletions}
@@ -89,11 +83,5 @@ export default class Autocomplete extends React.Component { Autocomplete.propTypes = { // the query string for which to show autocomplete suggestions - query: React.PropTypes.string.isRequired, - - // CSS selector indicating which element to pin the autocomplete to - pinSelector: React.PropTypes.string.isRequired, - - // attributes on which the autocomplete should match the pinElement - pinTo: React.PropTypes.array.isRequired + query: React.PropTypes.string.isRequired }; diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 0f9dd86b09..5373ca4dc8 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -25,36 +25,22 @@ import Autocomplete from './Autocomplete'; import UserSettingsStore from '../../../UserSettingsStore'; -module.exports = React.createClass({ - displayName: 'MessageComposer', +export default class MessageComposer extends React.Component { + constructor(props, context) { + super(props, context); + this.onCallClick = this.onCallClick.bind(this); + this.onHangupClick = this.onHangupClick.bind(this); + this.onUploadClick = this.onUploadClick.bind(this); + this.onUploadFileSelected = this.onUploadFileSelected.bind(this); + this.onVoiceCallClick = this.onVoiceCallClick.bind(this); + this.onInputContentChanged = this.onInputContentChanged.bind(this); - propTypes: { - tabComplete: React.PropTypes.any, - - // a callback which is called when the height of the composer is - // changed due to a change in content. - onResize: React.PropTypes.func, - - // js-sdk Room object - room: React.PropTypes.object.isRequired, - - // string representing the current voip call state - callState: React.PropTypes.string, - - // callback when a file to upload is chosen - uploadFile: React.PropTypes.func.isRequired, - - // opacity for dynamic UI fading effects - opacity: React.PropTypes.number, - }, - - getInitialState: function () { - return { + this.state = { autocompleteQuery: '' }; - }, + } - onUploadClick: function(ev) { + onUploadClick(ev) { if (MatrixClientPeg.get().isGuest()) { var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); Modal.createDialog(NeedToRegisterDialog, { @@ -65,9 +51,9 @@ module.exports = React.createClass({ } this.refs.uploadInput.click(); - }, + } - onUploadFileSelected: function(ev) { + onUploadFileSelected(ev) { var files = ev.target.files; var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); @@ -103,9 +89,9 @@ module.exports = React.createClass({ this.refs.uploadInput.value = null; } }); - }, + } - onHangupClick: function() { + onHangupClick() { var call = CallHandler.getCallForRoom(this.props.room.roomId); //var call = CallHandler.getAnyActiveCall(); if (!call) { @@ -117,31 +103,32 @@ module.exports = React.createClass({ // (e.g. conferences which will hangup the 1:1 room instead) room_id: call.roomId }); - }, + } - onCallClick: function(ev) { + onCallClick(ev) { dis.dispatch({ action: 'place_call', type: ev.shiftKey ? "screensharing" : "video", room_id: this.props.room.roomId }); - }, + } - onVoiceCallClick: function(ev) { + onVoiceCallClick(ev) { dis.dispatch({ action: 'place_call', type: 'voice', room_id: this.props.room.roomId }); - }, + } - onInputContentChanged(content: String) { + onInputContentChanged(content: string) { this.setState({ autocompleteQuery: content - }) - }, + }); + console.log(content); + } - render: function() { + render() { var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId); var uploadInputStyle = {display: 'none'}; var MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); @@ -196,7 +183,7 @@ module.exports = React.createClass({ controls.push( this.onInputContentChanged(content) } />, + onContentChanged={this.onInputContentChanged} />, uploadButton, hangupButton, callButton, @@ -213,7 +200,7 @@ module.exports = React.createClass({ return (
- +
@@ -223,5 +210,24 @@ module.exports = React.createClass({
); } -}); +}; +MessageComposer.propTypes = { + tabComplete: React.PropTypes.any, + + // a callback which is called when the height of the composer is + // changed due to a change in content. + onResize: React.PropTypes.func, + + // js-sdk Room object + room: React.PropTypes.object.isRequired, + + // string representing the current voip call state + callState: React.PropTypes.string, + + // callback when a file to upload is chosen + uploadFile: React.PropTypes.func.isRequired, + + // opacity for dynamic UI fading effects + opacity: React.PropTypes.number +}; diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index a9ee764864..d82e9fb6c7 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -72,7 +72,7 @@ export default class MessageComposerInput extends React.Component { this.onInputClick = this.onInputClick.bind(this); this.handleReturn = this.handleReturn.bind(this); this.handleKeyCommand = this.handleKeyCommand.bind(this); - this.onChange = this.onChange.bind(this); + this.setEditorState = this.setEditorState.bind(this); let isRichtextEnabled = window.localStorage.getItem('mx_editor_rte_enabled'); if(isRichtextEnabled == null) { @@ -207,9 +207,7 @@ export default class MessageComposerInput extends React.Component { let contentJSON = window.sessionStorage.getItem("mx_messagecomposer_input_" + this.roomId); if (contentJSON) { let content = convertFromRaw(JSON.parse(contentJSON)); - component.setState({ - editorState: component.createEditorState(component.state.isRichtextEnabled, content) - }); + component.setEditorState(component.createEditorState(component.state.isRichtextEnabled, content)); } } }; @@ -344,7 +342,7 @@ export default class MessageComposerInput extends React.Component { this.refs.editor.focus(); } - onChange(editorState: EditorState) { + setEditorState(editorState: EditorState) { this.setState({editorState}); if(editorState.getCurrentContent().hasText()) { @@ -361,15 +359,11 @@ export default class MessageComposerInput extends React.Component { enableRichtext(enabled: boolean) { if (enabled) { let html = mdownToHtml(this.state.editorState.getCurrentContent().getPlainText()); - this.setState({ - editorState: this.createEditorState(enabled, RichText.HTMLtoContentState(html)) - }); + this.setEditorState(this.createEditorState(enabled, RichText.HTMLtoContentState(html))); } else { let markdown = stateToMarkdown(this.state.editorState.getCurrentContent()), contentState = ContentState.createFromText(markdown); - this.setState({ - editorState: this.createEditorState(enabled, contentState) - }); + this.setEditorState(this.createEditorState(enabled, contentState)); } window.localStorage.setItem('mx_editor_rte_enabled', enabled); @@ -412,7 +406,7 @@ export default class MessageComposerInput extends React.Component { newState = RichUtils.handleKeyCommand(this.state.editorState, command); if (newState != null) { - this.onChange(newState); + this.setEditorState(newState); return true; } return false; @@ -506,7 +500,7 @@ export default class MessageComposerInput extends React.Component { Date: Mon, 20 Jun 2016 16:30:51 +0100 Subject: [PATCH 005/222] Fix /join to be consistent with the other code Plus a number of other tidyups: * Fix /join to dispatch a view_room for the room alias with the additional auto_join parameter * Make RoomView automatically join the room if the auto_join parameter is true and the user isn't already in it * Tidy up RoomView's peeking code, also fixing https://github.com/vector-im/vector-web/issues/1220 in react-sdk (although it still requires a synapse change to actually fix, but react-sdk does 'the right thing'). * Remove duplication of usage text from /join command * Amalgamate MatrixChat::_viewRoom's many, many parameters into an object and sort out case consistency a little. --- src/SlashCommands.js | 37 ++++------------ src/components/structures/MatrixChat.js | 58 +++++++++++++------------ src/components/structures/RoomView.js | 57 ++++++++++++++---------- 3 files changed, 73 insertions(+), 79 deletions(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index e4c0d5973a..d4e63018d3 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -132,46 +132,25 @@ var commands = { }), // Join a room - join: new Command("join", "", function(room_id, args) { + join: new Command("join", "#alias:domain", function(room_id, args) { if (args) { var matches = args.match(/^(\S+)$/); if (matches) { var room_alias = matches[1]; if (room_alias[0] !== '#') { - return reject("Usage: /join #alias:domain"); + return reject(this.getUsage()); } if (!room_alias.match(/:/)) { room_alias += ':' + MatrixClientPeg.get().getDomain(); } - // Try to find a room with this alias - // XXX: do we need to do this? Doesn't the JS SDK suppress duplicate attempts to join the same room? - var foundRoom = MatrixTools.getRoomForAlias( - MatrixClientPeg.get().getRooms(), - room_alias - ); + dis.dispatch({ + action: 'view_room', + room_alias: room_alias, + auto_join: true, + }); - if (foundRoom) { // we've already joined this room, view it if it's not archived. - var me = foundRoom.getMember(MatrixClientPeg.get().credentials.userId); - if (me && me.membership !== "leave") { - dis.dispatch({ - action: 'view_room', - room_id: foundRoom.roomId - }); - return success(); - } - } - - // otherwise attempt to join this alias. - return success( - MatrixClientPeg.get().joinRoom(room_alias).then( - function(room) { - dis.dispatch({ - action: 'view_room', - room_id: room.roomId - }); - }) - ); + return success(); } } return reject(this.getUsage()); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 1973fedbcd..65cb6a3ef4 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -403,10 +403,7 @@ module.exports = React.createClass({ // known to be in (eg. user clicks on a room in the recents panel), supply the ID // If the user is clicking on a room in the context of the alias being presented // to them, supply the room alias. If both are supplied, the room ID will be ignored. - this._viewRoom( - payload.room_id, payload.room_alias, payload.show_settings, payload.event_id, - payload.third_party_invite, payload.oob_data - ); + this._viewRoom(payload); break; case 'view_prev_room': roomIndexDelta = -1; @@ -423,7 +420,7 @@ module.exports = React.createClass({ } roomIndex = (roomIndex + roomIndexDelta) % allRooms.length; if (roomIndex < 0) roomIndex = allRooms.length - 1; - this._viewRoom(allRooms[roomIndex].roomId); + this._viewRoom({ room_id: allRooms[roomIndex].room_id }); break; case 'view_indexed_room': var allRooms = RoomListSorter.mostRecentActivityFirst( @@ -431,7 +428,7 @@ module.exports = React.createClass({ ); var roomIndex = payload.roomIndex; if (allRooms[roomIndex]) { - this._viewRoom(allRooms[roomIndex].roomId); + this._viewRoom({ room_id: allRooms[roomIndex].roomId }); } break; case 'view_user_settings': @@ -491,39 +488,45 @@ module.exports = React.createClass({ // switch view to the given room // - // eventId is optional and will cause a switch to the context of that - // particular event. - // @param {Object} thirdPartyInvite Object containing data about the third party + // @param {Object} room_info Object containing data about the room to be joined + // @param {string} room_info.room_id ID of the room to join. One of room_id or room_alias must be given. + // @param {string} room_info.room_alias Alias of the room to join. One of room_id or room_alias must be given. + // @param {boolean} room_info.auto_join If true, automatically attempt to join the room if not already a member. + // @param {string} room_info.show_settings ?? + // @param {string} room_info.event_id ID of the event in this room to show: this will cause a switch to the + // context of that particular event. Optional. + // @param {Object} room_info.third_party_invite Object containing data about the third party // we received to join the room, if any. - // @param {string} thirdPartyInvite.inviteSignUrl 3pid invite sign URL - // @param {string} thirdPartyInvite.invitedwithEmail The email address the invite was sent to - // @param {Object} oob_data Object of additional data about the room + // @param {string} room_info.third_party_invite.inviteSignUrl 3pid invite sign URL + // @param {string} room_info.third_party_invite.invitedwithEmail The email address the invite was sent to + // @param {Object} room_info.oob_data Object of additional data about the room // that has been passed out-of-band (eg. // room name and avatar from an invite email) - _viewRoom: function(roomId, roomAlias, showSettings, eventId, thirdPartyInvite, oob_data) { + _viewRoom: function(room_info) { // before we switch room, record the scroll state of the current room this._updateScrollMap(); this.focusComposer = true; var newState = { - initialEventId: eventId, - highlightedEventId: eventId, + initialEventId: room_info.event_id, + highlightedEventId: room_info.event_id, initialEventPixelOffset: undefined, page_type: this.PageTypes.RoomView, - thirdPartyInvite: thirdPartyInvite, - roomOobData: oob_data, - currentRoomAlias: roomAlias, + thirdPartyInvite: room_info.third_party_invite, + roomOobData: room_info.oob_data, + currentRoomAlias: room_info.room_alias, + autoJoin: room_info.auto_join, }; - if (!roomAlias) { - newState.currentRoomId = roomId; + if (!room_info.room_alias) { + newState.currentRoomId = room_info.room_id; } // if we aren't given an explicit event id, look for one in the // scrollStateMap. - if (!eventId) { - var scrollState = this.scrollStateMap[roomId]; + if (!room_info.event_id) { + var scrollState = this.scrollStateMap[room_info.room_id]; if (scrollState) { newState.initialEventId = scrollState.focussedEvent; newState.initialEventPixelOffset = scrollState.pixelOffset; @@ -536,8 +539,8 @@ module.exports = React.createClass({ // the new screen yet (we won't be showing it yet) // The normal case where this happens is navigating // to the room in the URL bar on page load. - var presentedId = roomAlias || roomId; - var room = MatrixClientPeg.get().getRoom(roomId); + var presentedId = room_info.room_alias || room_info.room_id; + var room = MatrixClientPeg.get().getRoom(room_info.room_id); if (room) { var theAlias = MatrixTools.getDisplayAliasForRoom(room); if (theAlias) presentedId = theAlias; @@ -553,15 +556,15 @@ module.exports = React.createClass({ // Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); } - if (eventId) { - presentedId += "/"+eventId; + if (room_info.event_id) { + presentedId += "/"+event_id; } this.notifyNewScreen('room/'+presentedId); newState.ready = true; } this.setState(newState); - if (this.refs.roomView && showSettings) { + if (this.refs.roomView && room_info.showSettings) { this.refs.roomView.showSettings(true); } }, @@ -1030,6 +1033,7 @@ module.exports = React.createClass({ { this.setState({ roomLoading: false, @@ -172,11 +172,11 @@ module.exports = React.createClass({ room: room, roomLoading: !room, hasUnsentMessages: this._hasUnsentMessages(room), - }, this._updatePeeking); + }, this._onHaveRoom); } }, - _updatePeeking: function() { + _onHaveRoom: function() { // if this is an unknown room then we're in one of three states: // - This is a room we can peek into (search engine) (we can /peek) // - This is a room we can publicly join or were invited to. (we can /join) @@ -187,29 +187,40 @@ module.exports = React.createClass({ // Note that peeking works by room ID and room ID only, as opposed to joining // which must be by alias or invite wherever possible (peeking currently does // not work over federation). - if (!this.state.room && this.state.roomId) { - console.log("Attempting to peek into room %s", this.state.roomId); - MatrixClientPeg.get().peekInRoom(this.state.roomId).then((room) => { - this.setState({ - room: room, - roomLoading: false, - }); - this._onRoomLoaded(room); - }, (err) => { - // This won't necessarily be a MatrixError, but we duck-type - // here and say if it's got an 'errcode' key with the right value, - // it means we can't peek. - if (err.errcode == "M_GUEST_ACCESS_FORBIDDEN") { - // This is fine: the room just isn't peekable (we assume). + // NB. We peek if we are not in the room, although if we try to peek into + // a room in which we have a member event (ie. we've left) synapse will just + // send us the same data as we get in the sync (ie. the last events we saw). + var my_member = this.state.room ? this.state.room.getMember(MatrixClientPeg.get().credentials.userId) : null; + var user_is_in_room = my_member ? my_member.membership == 'join' : false; + + if (!user_is_in_room && this.state.roomId) { + if (this.props.autoJoin) { + this.onJoinButtonClicked(); + } else if (this.state.roomId) { + console.log("Attempting to peek into room %s", this.state.roomId); + + MatrixClientPeg.get().peekInRoom(this.state.roomId).then((room) => { this.setState({ + room: room, roomLoading: false, }); - } else { - throw err; - } - }).done(); - } else if (this.state.room) { + this._onRoomLoaded(room); + }, (err) => { + // This won't necessarily be a MatrixError, but we duck-type + // here and say if it's got an 'errcode' key with the right value, + // it means we can't peek. + if (err.errcode == "M_GUEST_ACCESS_FORBIDDEN") { + // This is fine: the room just isn't peekable (we assume). + this.setState({ + roomLoading: false, + }); + } else { + throw err; + } + }).done(); + } + } else if (user_is_in_room) { MatrixClientPeg.get().stopPeeking(); this._onRoomLoaded(this.state.room); } @@ -992,7 +1003,7 @@ module.exports = React.createClass({ this.setState({ rejecting: true }); - MatrixClientPeg.get().leave(this.props.roomAddress).done(function() { + MatrixClientPeg.get().leave(this.props.roomId).done(function() { dis.dispatch({ action: 'view_next_room' }); self.setState({ rejecting: false From 4d5fbfc5b1383a5c0045a5815b245f02cea6beae Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 20 Jun 2016 17:11:46 +0100 Subject: [PATCH 006/222] Remove now unused MatrixTools.GetRoomForAlias --- src/MatrixTools.js | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/src/MatrixTools.js b/src/MatrixTools.js index 372f17f69c..b003d8d2d7 100644 --- a/src/MatrixTools.js +++ b/src/MatrixTools.js @@ -24,30 +24,5 @@ module.exports = { getDisplayAliasForRoom: function(room) { return room.getCanonicalAlias() || room.getAliases()[0]; }, - - /** - * Given a list of room objects, return the room which has the given alias, - * else null. - */ - getRoomForAlias: function(rooms, room_alias) { - var room; - for (var i = 0; i < rooms.length; i++) { - var aliasEvents = rooms[i].currentState.getStateEvents( - "m.room.aliases" - ); - for (var j = 0; j < aliasEvents.length; j++) { - var aliases = aliasEvents[j].getContent().aliases || []; - for (var k = 0; k < aliases.length; k++) { - if (aliases[k] === room_alias) { - room = rooms[i]; - break; - } - } - if (room) { break; } - } - if (room) { break; } - } - return room || null; - } } From 4c214119b2b65e83988e0ef801351cf6cdd875be Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 20 Jun 2016 18:05:58 +0100 Subject: [PATCH 007/222] Fix PR feedback --- src/components/structures/MatrixChat.js | 22 +++++++++++----------- src/components/structures/RoomView.js | 10 +++++++--- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 65cb6a3ef4..8dc70ca2aa 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -489,17 +489,17 @@ module.exports = React.createClass({ // switch view to the given room // // @param {Object} room_info Object containing data about the room to be joined - // @param {string} room_info.room_id ID of the room to join. One of room_id or room_alias must be given. - // @param {string} room_info.room_alias Alias of the room to join. One of room_id or room_alias must be given. - // @param {boolean} room_info.auto_join If true, automatically attempt to join the room if not already a member. - // @param {string} room_info.show_settings ?? - // @param {string} room_info.event_id ID of the event in this room to show: this will cause a switch to the - // context of that particular event. Optional. - // @param {Object} room_info.third_party_invite Object containing data about the third party + // @param {string=} room_info.room_id ID of the room to join. One of room_id or room_alias must be given. + // @param {string=} room_info.room_alias Alias of the room to join. One of room_id or room_alias must be given. + // @param {boolean=} room_info.auto_join If true, automatically attempt to join the room if not already a member. + // @param {boolean=} room_info.show_settings Makes RoomView show the room settings dialog. + // @param {string=} room_info.event_id ID of the event in this room to show: this will cause a switch to the + // context of that particular event. + // @param {Object=} room_info.third_party_invite Object containing data about the third party // we received to join the room, if any. - // @param {string} room_info.third_party_invite.inviteSignUrl 3pid invite sign URL - // @param {string} room_info.third_party_invite.invitedwithEmail The email address the invite was sent to - // @param {Object} room_info.oob_data Object of additional data about the room + // @param {string=} room_info.third_party_invite.inviteSignUrl 3pid invite sign URL + // @param {string=} room_info.third_party_invite.invitedEmail The email address the invite was sent to + // @param {Object=} room_info.oob_data Object of additional data about the room // that has been passed out-of-band (eg. // room name and avatar from an invite email) _viewRoom: function(room_info) { @@ -557,7 +557,7 @@ module.exports = React.createClass({ } if (room_info.event_id) { - presentedId += "/"+event_id; + presentedId += "/"+room_info.event_id; } this.notifyNewScreen('room/'+presentedId); newState.ready = true; diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 8440a56f2d..4219210734 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -191,8 +191,12 @@ module.exports = React.createClass({ // NB. We peek if we are not in the room, although if we try to peek into // a room in which we have a member event (ie. we've left) synapse will just // send us the same data as we get in the sync (ie. the last events we saw). - var my_member = this.state.room ? this.state.room.getMember(MatrixClientPeg.get().credentials.userId) : null; - var user_is_in_room = my_member ? my_member.membership == 'join' : false; + var user_is_in_room = null; + if (this.state.room) { + user_is_in_room = this.state.room.hasMembershipState( + MatrixClientPeg.get().credentials.userId, 'join' + ); + } if (!user_is_in_room && this.state.roomId) { if (this.props.autoJoin) { @@ -1003,7 +1007,7 @@ module.exports = React.createClass({ this.setState({ rejecting: true }); - MatrixClientPeg.get().leave(this.props.roomId).done(function() { + MatrixClientPeg.get().leave(this.state.roomId).done(function() { dis.dispatch({ action: 'view_next_room' }); self.setState({ rejecting: false From fc06ebcc63fa4653269e161b10a34d214c0999a9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 20 Jun 2016 18:43:56 +0100 Subject: [PATCH 008/222] Fix view_next_room --- src/components/structures/MatrixChat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 8dc70ca2aa..37bcbe7cb0 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -420,7 +420,7 @@ module.exports = React.createClass({ } roomIndex = (roomIndex + roomIndexDelta) % allRooms.length; if (roomIndex < 0) roomIndex = allRooms.length - 1; - this._viewRoom({ room_id: allRooms[roomIndex].room_id }); + this._viewRoom({ room_id: allRooms[roomIndex].roomId }); break; case 'view_indexed_room': var allRooms = RoomListSorter.mostRecentActivityFirst( From f6a76edfdfa7b5ec0f5c1ecedc62ce82dbb56465 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Tue, 21 Jun 2016 05:05:23 +0530 Subject: [PATCH 009/222] Fuzzy matching in User and Room providers --- src/autocomplete/RoomProvider.js | 17 ++++++++++++++--- src/autocomplete/UserProvider.js | 17 ++++++++++------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index c61541617d..b1232358b5 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -3,13 +3,16 @@ import Q from 'q'; import MatrixClientPeg from '../MatrixClientPeg'; import Fuse from 'fuse.js'; -const ROOM_REGEX = /(?=#)[^\s]*/g; +const ROOM_REGEX = /(?=#)([^\s]*)/g; let instance = null; export default class RoomProvider extends AutocompleteProvider { constructor() { super(); + this.fuse = new Fuse([], { + keys: ['name', 'roomId', 'aliases'] + }); } getCompletions(query: String) { @@ -18,12 +21,20 @@ export default class RoomProvider extends AutocompleteProvider { const matches = query.match(ROOM_REGEX); const command = matches && matches[0]; if(command) { - completions = client.getRooms().map(room => { + // 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 => { + return { + name: room.name, + roomId: room.roomId, + aliases: room.getAliases() + }; + })); + completions = this.fuse.search(command).map(room => { return { title: room.name, subtitle: room.roomId }; - }); + }).slice(0, 4);; } return Q.when(completions); } diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index c850cea7d9..51a85adaf1 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -1,6 +1,6 @@ import AutocompleteProvider from './AutocompleteProvider'; import Q from 'q'; -import MatrixClientPeg from '../MatrixClientPeg'; +import Fuse from 'fuse.js'; const ROOM_REGEX = /@[^\s]*/g; @@ -10,19 +10,23 @@ export default class UserProvider extends AutocompleteProvider { constructor() { super(); this.users = []; + this.fuse = new Fuse([], { + keys: ['displayName', 'userId'] + }) } getCompletions(query: String) { let completions = []; - const matches = query.match(ROOM_REGEX); - if(!!matches) { - const command = matches[0]; - completions = this.users.map(user => { + let matches = query.match(ROOM_REGEX); + let command = matches && matches[0]; + if(command) { + this.fuse.set(this.users); + completions = this.fuse.search(command).map(user => { return { title: user.displayName || user.userId, description: user.userId }; - }); + }).slice(0, 4); } return Q.when(completions); } @@ -32,7 +36,6 @@ export default class UserProvider extends AutocompleteProvider { } setUserList(users) { - console.log('setUserList'); this.users = users; } From 213e284edfeba1c3e7b5da64f309360fd6506e41 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 21 Jun 2016 11:05:37 +0100 Subject: [PATCH 010/222] Fix https://github.com/vector-im/vector-web/issues/1679 --- src/components/structures/MatrixChat.js | 30 ++++++++++++++++--------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 1973fedbcd..4c59d16474 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -108,10 +108,14 @@ module.exports = React.createClass({ return window.localStorage.getItem("mx_hs_url"); } else { - return this.props.config.default_hs_url || "https://matrix.org"; + return this.getDefaultHsUrl(); } }, + getDefaultHsUrl() { + return this.props.config.default_hs_url || "https://matrix.org"; + }, + getFallbackHsUrl: function() { return this.props.config.fallback_hs_url; }, @@ -126,10 +130,14 @@ module.exports = React.createClass({ return window.localStorage.getItem("mx_is_url"); } else { - return this.props.config.default_is_url || "https://vector.im" + return this.getDefaultIsUrl(); } }, + getDefaultIsUrl() { + return this.props.config.default_is_url || "https://vector.im"; + }, + componentWillMount: function() { this.favicon = new Favico({animation: 'none'}); }, @@ -151,8 +159,8 @@ module.exports = React.createClass({ this.onLoggedIn({ userId: this.props.startingQueryParams.guest_user_id, accessToken: this.props.startingQueryParams.guest_access_token, - homeserverUrl: this.props.config.default_hs_url, - identityServerUrl: this.props.config.default_is_url, + homeserverUrl: this.getDefaultHsUrl(), + identityServerUrl: this.getDefaultIsUrl(), guest: true }); } @@ -1109,8 +1117,8 @@ module.exports = React.createClass({ email={this.props.startingQueryParams.email} username={this.state.upgradeUsername} guestAccessToken={this.state.guestAccessToken} - defaultHsUrl={this.props.config.default_hs_url} - defaultIsUrl={this.props.config.default_is_url} + defaultHsUrl={this.getDefaultHsUrl()} + defaultIsUrl={this.getDefaultIsUrl()} brand={this.props.config.brand} customHsUrl={this.getCurrentHsUrl()} customIsUrl={this.getCurrentIsUrl()} @@ -1124,8 +1132,8 @@ module.exports = React.createClass({ } else if (this.state.screen == 'forgot_password') { return ( ); From fb6eec0f7d25be5714da478ce36aac4faccfbe44 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Tue, 21 Jun 2016 15:46:20 +0530 Subject: [PATCH 011/222] Hide/show autocomplete based on selection state --- src/RichText.js | 28 +++++++++++++- src/autocomplete/AutocompleteProvider.js | 37 +++++++++++++++++++ src/autocomplete/Autocompleter.js | 4 +- src/autocomplete/CommandProvider.js | 13 ++++--- src/autocomplete/DuckDuckGoProvider.js | 14 ++++--- src/autocomplete/EmojiProvider.js | 9 ++--- src/autocomplete/RoomProvider.js | 11 +++--- src/autocomplete/UserProvider.js | 13 ++++--- src/components/views/rooms/Autocomplete.js | 13 +++---- src/components/views/rooms/MessageComposer.js | 11 +++--- .../views/rooms/MessageComposerInput.js | 4 +- 11 files changed, 114 insertions(+), 43 deletions(-) diff --git a/src/RichText.js b/src/RichText.js index 7e749bc24a..678a7de190 100644 --- a/src/RichText.js +++ b/src/RichText.js @@ -5,7 +5,8 @@ import { convertFromHTML, DefaultDraftBlockRenderMap, DefaultDraftInlineStyle, - CompositeDecorator + CompositeDecorator, + SelectionState } from 'draft-js'; import * as sdk from './index'; @@ -168,3 +169,28 @@ export function modifyText(contentState: ContentState, rangeToReplace: Selection return Modifier.replaceText(contentState, rangeToReplace, modifyFn(text), inlineStyle, entityKey); } + +/** + * Computes the plaintext offsets of the given SelectionState. + * Note that this inherently means we make assumptions about what that means (no separator between ContentBlocks, etc) + * Used by autocomplete to show completions when the current selection lies within, or at the edges of a command. + */ +export function getTextSelectionOffsets(selectionState: SelectionState, + contentBlocks: Array): {start: number, end: number} { + let offset = 0, start = 0, end = 0; + for(let block of contentBlocks) { + if (selectionState.getStartKey() == block.getKey()) { + start = offset + selectionState.getStartOffset(); + } + if (selectionState.getEndKey() == block.getKey()) { + end = offset + selectionState.getEndOffset(); + break; + } + offset += block.getLength(); + } + + return { + start, + end + } +} diff --git a/src/autocomplete/AutocompleteProvider.js b/src/autocomplete/AutocompleteProvider.js index 61158d2b56..f741e085b0 100644 --- a/src/autocomplete/AutocompleteProvider.js +++ b/src/autocomplete/AutocompleteProvider.js @@ -1,4 +1,41 @@ +import Q from 'q'; + export default class AutocompleteProvider { + constructor(commandRegex?: RegExp, fuseOpts?: any) { + if(commandRegex) { + if(!commandRegex.global) { + throw new Error('commandRegex must have global flag set'); + } + this.commandRegex = commandRegex; + } + } + + /** + * Of the matched commands in the query, returns the first that contains or is contained by the selection, or null. + */ + getCurrentCommand(query: string, selection: {start: number, end: number}): ?Array { + if(this.commandRegex == null) + return null; + + let match = null; + while((match = this.commandRegex.exec(query)) != null) { + let matchStart = match.index, + matchEnd = matchStart + match[0].length; + + console.log(match); + + if(selection.start <= matchEnd && selection.end >= matchStart) { + return match; + } + } + this.commandRegex.lastIndex = 0; + return null; + } + + getCompletions(query: String, selection: {start: number, end: number}) { + return Q.when([]); + } + getName(): string { return 'Default Provider'; } diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index 95669d5e0f..6b66d2fbdc 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -12,10 +12,10 @@ const PROVIDERS = [ EmojiProvider ].map(completer => completer.getInstance()); -export function getCompletions(query: String) { +export function getCompletions(query: string, selection: {start: number, end: number}) { return PROVIDERS.map(provider => { return { - completions: provider.getCompletions(query), + completions: provider.getCompletions(query, selection), provider }; }); diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index 7b950c0ed0..30b448d7f2 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -39,22 +39,23 @@ const COMMANDS = [ } ]; +let COMMAND_RE = /(^\/\w*)/g; + let instance = null; export default class CommandProvider extends AutocompleteProvider { constructor() { - super(); + super(COMMAND_RE); this.fuse = new Fuse(COMMANDS, { keys: ['command', 'args', 'description'] }); } - getCompletions(query: String) { + getCompletions(query: string, selection: {start: number, end: number}) { let completions = []; - const matches = query.match(/(^\/\w*)/); - if(!!matches) { - const command = matches[0]; - completions = this.fuse.search(command).map(result => { + const command = this.getCurrentCommand(query, selection); + if(command) { + completions = this.fuse.search(command[0]).map(result => { return { title: result.command, subtitle: result.args, diff --git a/src/autocomplete/DuckDuckGoProvider.js b/src/autocomplete/DuckDuckGoProvider.js index 496ce72e46..b2bf27a21a 100644 --- a/src/autocomplete/DuckDuckGoProvider.js +++ b/src/autocomplete/DuckDuckGoProvider.js @@ -2,23 +2,27 @@ import AutocompleteProvider from './AutocompleteProvider'; import Q from 'q'; import 'whatwg-fetch'; -const DDG_REGEX = /\/ddg\s+(.+)$/; +const DDG_REGEX = /\/ddg\s+(.+)$/g; const REFERER = 'vector'; let instance = null; export default class DuckDuckGoProvider extends AutocompleteProvider { + constructor() { + super(DDG_REGEX); + } + static getQueryUri(query: String) { return `http://api.duckduckgo.com/?q=${encodeURIComponent(query)}` + `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERER)}`; } - getCompletions(query: String) { - let match = DDG_REGEX.exec(query); - if(!query || !match) + getCompletions(query: string, selection: {start: number, end: number}) { + let command = this.getCurrentCommand(query, selection); + if(!query || !command) return Q.when([]); - return fetch(DuckDuckGoProvider.getQueryUri(match[1]), { + return fetch(DuckDuckGoProvider.getQueryUri(command[1]), { method: 'GET' }) .then(response => response.json()) diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index 684414d72a..e1b5f3ea38 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -10,16 +10,15 @@ let instance = null; export default class EmojiProvider extends AutocompleteProvider { constructor() { - super(); + super(EMOJI_REGEX); this.fuse = new Fuse(EMOJI_SHORTNAMES); } - getCompletions(query: String) { + getCompletions(query: string, selection: {start: number, end: number}) { let completions = []; - let matches = query.match(EMOJI_REGEX); - let command = matches && matches[0]; + let command = this.getCurrentCommand(query, selection); if(command) { - completions = this.fuse.search(command).map(result => { + completions = this.fuse.search(command[0]).map(result => { let shortname = EMOJI_SHORTNAMES[result]; let imageHTML = shortnameToImage(shortname); return { diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index b1232358b5..8b5650e7a7 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -9,17 +9,18 @@ let instance = null; export default class RoomProvider extends AutocompleteProvider { constructor() { - super(); + super(ROOM_REGEX, { + keys: ['displayName', 'userId'] + }); this.fuse = new Fuse([], { keys: ['name', 'roomId', 'aliases'] }); } - getCompletions(query: String) { + getCompletions(query: string, selection: {start: number, end: number}) { let client = MatrixClientPeg.get(); let completions = []; - const matches = query.match(ROOM_REGEX); - const command = matches && matches[0]; + const command = this.getCurrentCommand(query, selection); 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 => { @@ -29,7 +30,7 @@ export default class RoomProvider extends AutocompleteProvider { aliases: room.getAliases() }; })); - completions = this.fuse.search(command).map(room => { + completions = this.fuse.search(command[0]).map(room => { return { title: room.name, subtitle: room.roomId diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 51a85adaf1..3edb2bf00c 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -2,26 +2,27 @@ import AutocompleteProvider from './AutocompleteProvider'; import Q from 'q'; import Fuse from 'fuse.js'; -const ROOM_REGEX = /@[^\s]*/g; +const USER_REGEX = /@[^\s]*/g; let instance = null; export default class UserProvider extends AutocompleteProvider { constructor() { - super(); + super(USER_REGEX, { + keys: ['displayName', 'userId'] + }); this.users = []; this.fuse = new Fuse([], { keys: ['displayName', 'userId'] }) } - getCompletions(query: String) { + getCompletions(query: string, selection: {start: number, end: number}) { let completions = []; - let matches = query.match(ROOM_REGEX); - let command = matches && matches[0]; + let command = this.getCurrentCommand(query, selection); if(command) { this.fuse.set(this.users); - completions = this.fuse.search(command).map(user => { + completions = this.fuse.search(command[0]).map(user => { return { title: user.displayName || user.userId, description: user.userId diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index 0218a88195..babd349c31 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -7,27 +7,27 @@ export default class Autocomplete extends React.Component { constructor(props) { super(props); this.state = { - completions: [] + completions: [], + + // how far down the completion list we are + selectionOffset: 0 }; } componentWillReceiveProps(props, state) { if(props.query == this.props.query) return; - getCompletions(props.query).map(completionResult => { + getCompletions(props.query, props.selection).map(completionResult => { try { - // console.log(`${completionResult.provider.getName()}: ${JSON.stringify(completionResult.completions)}`); completionResult.completions.then(completions => { let i = this.state.completions.findIndex( completion => completion.provider === completionResult.provider ); i = i == -1 ? this.state.completions.length : i; - // console.log(completionResult); let newCompletions = Object.assign([], this.state.completions); completionResult.completions = completions; newCompletions[i] = completionResult; - // console.log(newCompletions); this.setState({ completions: newCompletions }); @@ -42,8 +42,7 @@ export default class Autocomplete extends React.Component { } render() { - const renderedCompletions = this.state.completions.map((completionResult, i) => { - // console.log(completionResult); + let renderedCompletions = this.state.completions.map((completionResult, i) => { let completions = completionResult.completions.map((completion, i) => { let Component = completion.component; if(Component) { diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 5373ca4dc8..ce1ced2b59 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -36,7 +36,8 @@ export default class MessageComposer extends React.Component { this.onInputContentChanged = this.onInputContentChanged.bind(this); this.state = { - autocompleteQuery: '' + autocompleteQuery: '', + selection: null }; } @@ -121,11 +122,11 @@ export default class MessageComposer extends React.Component { }); } - onInputContentChanged(content: string) { + onInputContentChanged(content: string, selection: {start: number, end: number}) { this.setState({ - autocompleteQuery: content + autocompleteQuery: content, + selection }); - console.log(content); } render() { @@ -200,7 +201,7 @@ export default class MessageComposer extends React.Component { return (
- +
diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index d82e9fb6c7..9b615e7e4e 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -352,7 +352,9 @@ export default class MessageComposerInput extends React.Component { } if(this.props.onContentChanged) { - this.props.onContentChanged(editorState.getCurrentContent().getPlainText()); + this.props.onContentChanged(editorState.getCurrentContent().getPlainText(), + RichText.getTextSelectionOffsets(editorState.getSelection(), + editorState.getCurrentContent().getBlocksAsArray())); } } From a74db3a815879558110fcaa4b2f6f322dc1af783 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Tue, 21 Jun 2016 18:33:39 +0530 Subject: [PATCH 012/222] Get basic keyboard selection working --- src/components/views/rooms/Autocomplete.js | 21 +++++++++- src/components/views/rooms/MessageComposer.js | 31 +++++++++++++-- .../views/rooms/MessageComposerInput.js | 38 ++++++++++++++++++- 3 files changed, 84 insertions(+), 6 deletions(-) diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index babd349c31..ca0c5df701 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -1,5 +1,6 @@ import React from 'react'; import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; +import classNames from 'classnames'; import {getCompletions} from '../../../autocomplete/Autocompleter'; @@ -41,16 +42,34 @@ export default class Autocomplete extends React.Component { }); } + onUpArrow() { + this.setState({selectionOffset: this.state.selectionOffset - 1}); + return true; + } + + onDownArrow() { + this.setState({selectionOffset: this.state.selectionOffset + 1}); + return true; + } + render() { + let position = 0; let renderedCompletions = this.state.completions.map((completionResult, i) => { let completions = completionResult.completions.map((completion, i) => { let Component = completion.component; + let className = classNames('mx_Autocomplete_Completion', { + 'selected': position == this.state.selectionOffset + }); + let componentPosition = position; + position++; if(Component) { return Component; } return ( -
+
this.setState({selectionOffset: componentPosition})}> {completion.title} {completion.subtitle} diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index ce1ced2b59..24d0bd2510 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -34,6 +34,9 @@ export default class MessageComposer extends React.Component { this.onUploadFileSelected = this.onUploadFileSelected.bind(this); this.onVoiceCallClick = this.onVoiceCallClick.bind(this); this.onInputContentChanged = this.onInputContentChanged.bind(this); + this.onUpArrow = this.onUpArrow.bind(this); + this.onDownArrow = this.onDownArrow.bind(this); + this.onTab = this.onTab.bind(this); this.state = { autocompleteQuery: '', @@ -129,6 +132,18 @@ export default class MessageComposer extends React.Component { }); } + onUpArrow() { + return this.refs.autocomplete.onUpArrow(); + } + + onDownArrow() { + return this.refs.autocomplete.onDownArrow(); + } + + onTab() { + return this.refs.autocomplete.onTab(); + } + render() { var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId); var uploadInputStyle = {display: 'none'}; @@ -182,9 +197,14 @@ export default class MessageComposer extends React.Component { ); controls.push( - , + , uploadButton, hangupButton, callButton, @@ -201,7 +221,10 @@ export default class MessageComposer extends React.Component { return (
- +
diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 9b615e7e4e..313216d54c 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -73,6 +73,9 @@ export default class MessageComposerInput extends React.Component { this.handleReturn = this.handleReturn.bind(this); this.handleKeyCommand = this.handleKeyCommand.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); let isRichtextEnabled = window.localStorage.getItem('mx_editor_rte_enabled'); if(isRichtextEnabled == null) { @@ -489,6 +492,30 @@ export default class MessageComposerInput extends React.Component { return true; } + onUpArrow(e) { + if(this.props.onUpArrow) { + if(this.props.onUpArrow()) { + e.preventDefault(); + } + } + } + + onDownArrow(e) { + if(this.props.onDownArrow) { + if(this.props.onDownArrow()) { + e.preventDefault(); + } + } + } + + onTab(e) { + if(this.props.onTab) { + if(this.props.onTab()) { + e.preventDefault(); + } + } + } + render() { let className = "mx_MessageComposer_input"; @@ -507,6 +534,9 @@ export default class MessageComposerInput extends React.Component { handleKeyCommand={this.handleKeyCommand} handleReturn={this.handleReturn} stripPastedStyles={!this.state.isRichtextEnabled} + onTab={this.onTab} + onUpArrow={this.onUpArrow} + onDownArrow={this.onDownArrow} spellCheck={true} />
); @@ -524,5 +554,11 @@ MessageComposerInput.propTypes = { room: React.PropTypes.object.isRequired, // called with current plaintext content (as a string) whenever it changes - onContentChanged: React.PropTypes.func + onContentChanged: React.PropTypes.func, + + onUpArrow: React.PropTypes.func, + + onDownArrow: React.PropTypes.func, + + onTab: React.PropTypes.func }; From d3265ab9707db33a0d8c92d017c100d3cde6a261 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 21 Jun 2016 17:46:55 +0100 Subject: [PATCH 013/222] Redundant getDeafultHs() This now can never be falsey so no point checking it --- src/components/structures/MatrixChat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 4c59d16474..24ebd95f60 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1150,7 +1150,7 @@ module.exports = React.createClass({ customIsUrl={this.getCurrentIsUrl()} fallbackHsUrl={this.getFallbackHsUrl()} onForgotPasswordClick={this.onForgotPasswordClick} - onLoginAsGuestClick={this.props.enableGuest && this.props.config && this.getDefaultHsUrl() ? this._registerAsGuest.bind(this, true) : undefined} + onLoginAsGuestClick={this.props.enableGuest && this.props.config && this._registerAsGuest.bind(this, true)} onCancelClick={ this.state.guestCreds ? this.onReturnToGuestClick : null } /> ); From 5195140ff8e6950fccc95b9bd3239a39aefc822a Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 21 Jun 2016 19:50:03 +0100 Subject: [PATCH 014/222] reposition Login spinner --- src/SdkConfig.js | 47 ++++++++++++++++++++++++ src/components/structures/login/Login.js | 5 ++- 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 src/SdkConfig.js diff --git a/src/SdkConfig.js b/src/SdkConfig.js new file mode 100644 index 0000000000..46c2b818b8 --- /dev/null +++ b/src/SdkConfig.js @@ -0,0 +1,47 @@ +/* +Copyright 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +var DEFAULTS = { + // URL to a page we show in an iframe to configure integrations + //integrations_ui_url: "https://scalar.vector.im/", + integrations_ui_url: "http://127.0.0.1:5051/", + // Base URL to the REST interface of the integrations server + //integrations_rest_url: "https://scalar.vector.im/api", + integrations_rest_url: "http://127.0.0.1:5050", +}; + +class SdkConfig { + + static get() { + return global.mxReactSdkConfig; + } + + static put(cfg) { + var defaultKeys = Object.keys(DEFAULTS); + for (var i = 0; i < defaultKeys.length; ++i) { + if (cfg[defaultKeys[i]] === undefined) { + cfg[defaultKeys[i]] = DEFAULTS[defaultKeys[i]]; + } + } + global.mxReactSdkConfig = cfg; + } + + static unset() { + global.mxReactSdkConfig = undefined; + } +} + +module.exports = SdkConfig; diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index aa0c42dc98..a73ad30f87 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -232,7 +232,9 @@ module.exports = React.createClass({displayName: 'Login',
-

Sign in

+

Sign in + { loader } +

{ this.componentForStep(this._getCurrentFlowStep()) }
- { loader } { this.state.errorText }
From a04f03669c0c24a38d58dc6f631f45a4d53e3dd9 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 23 Jun 2016 10:36:16 +0100 Subject: [PATCH 015/222] RoomSettings: refactor permissions calculations The logic for calculating who had permission for what was impenetrable (and wrong, in parts), so rewrite it to be a bit clearer. --- src/components/views/rooms/RoomSettings.js | 72 ++++++++-------------- 1 file changed, 25 insertions(+), 47 deletions(-) diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 8764700c5a..d6b8b67f25 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -22,6 +22,13 @@ var Modal = require('../../../Modal'); var ObjectUtils = require("../../../ObjectUtils"); var dis = require("../../../dispatcher"); +// parse a string as an integer; if the input is undefined, or cannot be parsed +// as an integer, return a default. +function parseIntWithDefault(val, def) { + var res = parseInt(val); + return isNaN(res) ? def : res; +} + module.exports = React.createClass({ displayName: 'RoomSettings', @@ -251,7 +258,7 @@ module.exports = React.createClass({ power_levels_changed: true }); }, - + _yankValueFromEvent: function(stateEventType, keyName, defaultValue) { // E.g.("m.room.name","name") would yank the "name" content key from "m.room.name" var event = this.props.room.currentState.getStateEvents(stateEventType, ''); @@ -286,7 +293,7 @@ module.exports = React.createClass({ }, }); }, - + _onRoomAccessRadioToggle: function(ev) { // join_rule @@ -368,58 +375,29 @@ module.exports = React.createClass({ var EditableText = sdk.getComponent('elements.EditableText'); var PowerSelector = sdk.getComponent('elements.PowerSelector'); - var power_levels = this.props.room.currentState.getStateEvents('m.room.power_levels', ''); - var events_levels = (power_levels ? power_levels.getContent().events : {}) || {}; var cli = MatrixClientPeg.get(); var roomState = this.props.room.currentState; var user_id = cli.credentials.userId; - if (power_levels) { - power_levels = power_levels.getContent(); + var power_level_event = roomState.getStateEvents('m.room.power_levels', ''); + var power_levels = power_level_event ? power_level_event.getContent() : {}; + var events_levels = power_levels.events || {}; + var user_levels = power_levels.users || {}; - var ban_level = parseInt(power_levels.ban); - var kick_level = parseInt(power_levels.kick); - var redact_level = parseInt(power_levels.redact); - var invite_level = parseInt(power_levels.invite || 0); - var send_level = parseInt(power_levels.events_default || 0); - var state_level = parseInt(power_levels.state_default || 50); - var default_user_level = parseInt(power_levels.users_default || 0); + var ban_level = parseIntWithDefault(power_levels.ban, 50); + var kick_level = parseIntWithDefault(power_levels.kick, 50); + var redact_level = parseIntWithDefault(power_levels.redact, 50); + var invite_level = parseIntWithDefault(power_levels.invite, 50); + var send_level = parseIntWithDefault(power_levels.events_default, 0); + var state_level = power_level_event ? parseIntWithDefault(power_levels.state_default, 50) : 0; + var default_user_level = parseIntWithDefault(power_levels.users_default, 0); - if (power_levels.ban == undefined) ban_level = 50; - if (power_levels.kick == undefined) kick_level = 50; - if (power_levels.redact == undefined) redact_level = 50; - - var user_levels = power_levels.users || {}; - - var current_user_level = user_levels[user_id]; - if (current_user_level == undefined) current_user_level = default_user_level; - - var power_level_level = events_levels["m.room.power_levels"]; - if (power_level_level == undefined) { - power_level_level = state_level; - } - - var can_change_levels = current_user_level >= power_level_level; - } else { - var ban_level = 50; - var kick_level = 50; - var redact_level = 50; - var invite_level = 0; - var send_level = 0; - var state_level = 0; - var default_user_level = 0; - - var user_levels = []; - var events_levels = []; - - var current_user_level = 0; - - var power_level_level = 0; - - var can_change_levels = false; + var current_user_level = user_levels[user_id]; + if (current_user_level === undefined) { + current_user_level = default_user_level; } - var state_default = (parseInt(power_levels ? power_levels.state_default : 0) || 0); + var can_change_levels = roomState.mayClientSendStateEvent("m.room.power_levels", cli); var canSetTag = !cli.isGuest(); @@ -488,7 +466,7 @@ module.exports = React.createClass({ var tagsSection = null; if (canSetTag || self.state.tags) { - var tagsSection = + var tagsSection =
Tagged as: { canSetTag ? (tags.map(function(tag, i) { From 7a7d7c0e023b62e9afd774232ea107cfeaa0d535 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 23 Jun 2016 11:15:55 +0100 Subject: [PATCH 016/222] Fix a pair of warnings from RoomSettings - initialise the 'publish' checkbox correctly so react doesn't grumble about it turning from uncontrolled into controlled - PowerSelector's 'controlled' property isn't really required, so mark it as such. --- src/components/views/elements/PowerSelector.js | 7 ++++++- src/components/views/rooms/RoomSettings.js | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/PowerSelector.js b/src/components/views/elements/PowerSelector.js index 3c65ca707c..993f2b965a 100644 --- a/src/components/views/elements/PowerSelector.js +++ b/src/components/views/elements/PowerSelector.js @@ -34,10 +34,15 @@ module.exports = React.createClass({ propTypes: { value: React.PropTypes.number.isRequired, + // if true, the + Enable encryption (warning: cannot be disabled again!) + + ); + } + + return ( +
+

Encryption

+ + {button} +
+ ); + }, + + render: function() { // TODO: go through greying out things you don't have permission to change // (or turning them into informative stuff) @@ -587,10 +637,6 @@ module.exports = React.createClass({ Members only (since they joined)
-
@@ -655,6 +701,8 @@ module.exports = React.createClass({ { bannedUsersSection } + { this._renderEncryptionSection() } +

Advanced

This room's internal ID is { this.props.room.roomId } From 6283b200fb629c04990f09d58092482fa81f2808 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 23 Jun 2016 13:21:55 +0100 Subject: [PATCH 018/222] Remove /encrypt command Now that we have the room setting to enable encryption, the /encrypt command is not only redundant, but confusing, since it's in conflict with the room setting. --- src/SlashCommands.js | 22 +--------------------- src/encryption.js | 38 -------------------------------------- 2 files changed, 1 insertion(+), 59 deletions(-) delete mode 100644 src/encryption.js diff --git a/src/SlashCommands.js b/src/SlashCommands.js index d4e63018d3..759a95c8ff 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -17,7 +17,6 @@ limitations under the License. var MatrixClientPeg = require("./MatrixClientPeg"); var MatrixTools = require("./MatrixTools"); var dis = require("./dispatcher"); -var encryption = require("./encryption"); var Tinter = require("./Tinter"); @@ -82,32 +81,13 @@ var commands = { return success( MatrixClientPeg.get().setRoomAccountData( room_id, "org.matrix.room.color_scheme", colorScheme - ) + ) ); } } return reject(this.getUsage()); }), - encrypt: new Command("encrypt", "", function(room_id, args) { - if (args == "on") { - var client = MatrixClientPeg.get(); - var members = client.getRoom(room_id).currentState.members; - var user_ids = Object.keys(members); - return success( - encryption.enableEncryption(client, room_id, user_ids) - ); - } - if (args == "off") { - var client = MatrixClientPeg.get(); - return success( - encryption.disableEncryption(client, room_id) - ); - - } - return reject(this.getUsage()); - }), - // Change the room topic topic: new Command("topic", "", function(room_id, args) { if (args) { diff --git a/src/encryption.js b/src/encryption.js deleted file mode 100644 index cbe92d36de..0000000000 --- a/src/encryption.js +++ /dev/null @@ -1,38 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -function enableEncyption(client, roomId, members) { - members = members.slice(0); - members.push(client.credentials.userId); - // TODO: Check the keys actually match what keys the user has. - // TODO: Don't redownload keys each time. - return client.downloadKeys(members, "forceDownload").then(function(res) { - return client.setRoomEncryption(roomId, { - algorithm: "m.olm.v1.curve25519-aes-sha2", - members: members, - }); - }) -} - -function disableEncryption(client, roomId) { - return client.disableRoomEncryption(roomId); -} - - -module.exports = { - enableEncryption: enableEncyption, - disableEncryption: disableEncryption, -} From e046f5359fa39a914aaa8c8fd15af19d6e5cbabd Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 23 Jun 2016 14:08:45 +0100 Subject: [PATCH 019/222] CreateRoom: remove reference to encryption module The CreateRoom structure isn't currently used, but contained a reference to the (now defunct) encryption module; remove the reference for now. --- src/components/structures/CreateRoom.js | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/components/structures/CreateRoom.js b/src/components/structures/CreateRoom.js index f6e2baeaf2..e7585e3640 100644 --- a/src/components/structures/CreateRoom.js +++ b/src/components/structures/CreateRoom.js @@ -24,7 +24,6 @@ var PresetValues = { Custom: "custom", }; var q = require('q'); -var encryption = require("../../encryption"); var sdk = require('../../index'); module.exports = React.createClass({ @@ -108,17 +107,8 @@ module.exports = React.createClass({ var deferred = cli.createRoom(options); - var response; - if (this.state.encrypt) { - deferred = deferred.then(function(res) { - response = res; - return encryption.enableEncryption( - cli, response.room_id, options.invite - ); - }).then(function() { - return q(response) } - ); + // TODO } this.setState({ From 5cda2a6802271d48f9d1b1fef3a46e2d48b00569 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 23 Jun 2016 14:38:08 +0100 Subject: [PATCH 020/222] Disable colour output in jenkins script As it really confuses jenkins --- jenkins.sh | 1 + package.json | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/jenkins.sh b/jenkins.sh index eeb7d7d56e..26d434a504 100755 --- a/jenkins.sh +++ b/jenkins.sh @@ -2,6 +2,7 @@ set -e +export KARMAFLAGS="--no-colors" export NVM_DIR="/home/jenkins/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" nvm use 4 diff --git a/package.json b/package.json index d3021fb1ac..e8c7dd4f97 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,8 @@ "start": "babel src -w -d lib --source-maps", "clean": "rimraf lib", "prepublish": "npm run build && git rev-parse HEAD > git-revision.txt", - "test": "karma start --browsers PhantomJS", - "test-multi": "karma start --single-run=false" + "test": "karma start $KARMAFLAGS --browsers PhantomJS", + "test-multi": "karma start $KARMAFLAGS --single-run=false" }, "dependencies": { "classnames": "^2.1.2", From dc50a0f24a37633c0c8af5452c247ed62ade639f Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 23 Jun 2016 16:20:40 +0100 Subject: [PATCH 021/222] Add logging to TimelinePanel test to see where it fails on jenkins --- test/components/structures/TimelinePanel-test.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/components/structures/TimelinePanel-test.js b/test/components/structures/TimelinePanel-test.js index c201c647c6..045ccd70b7 100644 --- a/test/components/structures/TimelinePanel-test.js +++ b/test/components/structures/TimelinePanel-test.js @@ -220,12 +220,14 @@ describe('TimelinePanel', function() { for (var i = 0; i < N_EVENTS; i++) { timeline.addEvent(mkMessage()); } + console.log("added events to timeline"); var scrollDefer; var panel = ReactDOM.render( {scrollDefer.resolve()}} />, parentDiv ); + console.log("TimelinePanel rendered"); var messagePanel = ReactTestUtils.findRenderedComponentWithType( panel, sdk.getComponent('structures.MessagePanel')); @@ -246,6 +248,7 @@ describe('TimelinePanel', function() { // need to go further return backPaginate(); } + console.log("paginated to end."); // hopefully, we got to the start of the timeline expect(messagePanel.props.backPaginating).toBe(false); @@ -259,6 +262,7 @@ describe('TimelinePanel', function() { expect(messagePanel.props.suppressFirstDateSeparator).toBe(true); // back-paginate until we hit the start + console.log("back paginating..."); return backPaginate(); }).then(() => { expect(messagePanel.props.suppressFirstDateSeparator).toBe(false); From a1dd4274201db740a945ae728fd12cb6dfe390ad Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 23 Jun 2016 17:27:23 +0100 Subject: [PATCH 022/222] Implement device blocking This is the react-sdk part of https://github.com/matrix-org/matrix-js-sdk/pull/146. It adds 'Block'/'Unblock' buttons to the device list, and updates the deviceVerified listeners to listen for deviceVerificationChanged instead. Also adds an extra
to the deviceinfo section to help me with the CSS. --- src/components/views/rooms/EventTile.js | 8 ++- .../views/rooms/MemberDeviceInfo.js | 57 ++++++++++++++++--- src/components/views/rooms/MemberInfo.js | 10 ++-- 3 files changed, 60 insertions(+), 15 deletions(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index ff02139d87..70dfe8ac33 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -139,7 +139,8 @@ module.exports = React.createClass({ componentDidMount: function() { this._suppressReadReceiptAnimation = false; - MatrixClientPeg.get().on("deviceVerified", this.onDeviceVerified); + MatrixClientPeg.get().on("deviceVerificationChanged", + this.onDeviceVerificationChanged); }, componentWillReceiveProps: function (nextProps) { @@ -163,11 +164,12 @@ module.exports = React.createClass({ componentWillUnmount: function() { var client = MatrixClientPeg.get(); if (client) { - client.removeListener("deviceVerified", this.onDeviceVerified); + client.removeListener("deviceVerificationChanged", + this.onDeviceVerificationChanged); } }, - onDeviceVerified: function(userId, device) { + onDeviceVerificationChanged: function(userId, device) { if (userId == this.props.mxEvent.getSender()) { this._verifyEvent(this.props.mxEvent); } diff --git a/src/components/views/rooms/MemberDeviceInfo.js b/src/components/views/rooms/MemberDeviceInfo.js index ebc2ab1c32..b7ddf9b2ce 100644 --- a/src/components/views/rooms/MemberDeviceInfo.js +++ b/src/components/views/rooms/MemberDeviceInfo.js @@ -36,32 +36,73 @@ module.exports = React.createClass({ ); }, + onBlockClick: function() { + MatrixClientPeg.get().setDeviceBlocked( + this.props.userId, this.props.device.id, true + ); + }, + + onUnblockClick: function() { + MatrixClientPeg.get().setDeviceBlocked( + this.props.userId, this.props.device.id, false + ); + }, + render: function() { - var indicator = null, button = null; - if (this.props.device.verified) { - indicator = ( -
+ var indicator = null, blockButton = null, verifyButton = null; + if (this.props.device.blocked) { + blockButton = ( +
+ Unblock +
); - button = ( + } else { + blockButton = ( +
+ Block +
+ ); + } + + if (this.props.device.verified) { + verifyButton = (
Unverify
); } else { - button = ( + verifyButton = (
Verify
); } + + if (this.props.device.blocked) { + indicator = ( +
+ ); + } else if (this.props.device.verified) { + indicator = ( +
+ ); + + } else { + indicator = ( +
?
+ ); + } + return (
{this.props.device.id}
-
{this.props.device.key}
{indicator} - {button} + {verifyButton} + {blockButton}
); }, diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 97cfecc9e1..66501abfa5 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -70,7 +70,7 @@ module.exports = React.createClass({ componentDidMount: function() { this._updateStateForNewMember(this.props.member); - MatrixClientPeg.get().on("deviceVerified", this.onDeviceVerified); + MatrixClientPeg.get().on("deviceVerificationChanged", this.onDeviceVerificationChanged); }, componentWillReceiveProps: function(newProps) { @@ -82,14 +82,14 @@ module.exports = React.createClass({ componentWillUnmount: function() { var client = MatrixClientPeg.get(); if (client) { - client.removeListener("deviceVerified", this.onDeviceVerified); + client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged); } if (this._cancelDeviceList) { this._cancelDeviceList(); } }, - onDeviceVerified: function(userId, device) { + onDeviceVerificationChanged: function(userId, device) { if (userId == this.props.member.userId) { // no need to re-download the whole thing; just update our copy of // the list. @@ -535,7 +535,9 @@ module.exports = React.createClass({ return (

Devices

- {devComponents} +
+ {devComponents} +
); }, From 1c280baddebc21286224302c745eef77f885a4b4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 23 Jun 2016 18:36:44 +0100 Subject: [PATCH 023/222] Increase timeout on TimelinePanel test Since it looks like this timeout sometimes isn't sufficient to scroll all the way up when jenkins is very busy --- test/components/structures/TimelinePanel-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/components/structures/TimelinePanel-test.js b/test/components/structures/TimelinePanel-test.js index 045ccd70b7..de547b1779 100644 --- a/test/components/structures/TimelinePanel-test.js +++ b/test/components/structures/TimelinePanel-test.js @@ -210,7 +210,7 @@ describe('TimelinePanel', function() { var N_EVENTS = 600; // sadly, loading all those events takes a while - this.timeout(N_EVENTS * 20); + this.timeout(N_EVENTS * 40); // client.getRoom is called a /lot/ in this test, so replace // sinon's spy with a fast noop. From 98c03869a7af19932d4d4d7ecc9704ede4471bf8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 24 Jun 2016 15:34:07 +0100 Subject: [PATCH 024/222] Display an error message if room not found Fixes https://github.com/vector-im/vector-web/issues/1012 --- src/components/structures/RoomView.js | 10 +++++--- src/components/views/rooms/RoomPreviewBar.js | 27 +++++++++++++++++--- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 4219210734..78d9351171 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -116,6 +116,7 @@ module.exports = React.createClass({ callState: null, guestsCanJoin: false, canPeek: false, + roomLoadError: null, // this is true if we are fully scrolled-down, and are looking at // the end of the live timeline. It has the effect of hiding the @@ -163,6 +164,7 @@ module.exports = React.createClass({ }, (err) => { this.setState({ roomLoading: false, + roomLoadError: err, }); }); } else { @@ -1282,6 +1284,7 @@ module.exports = React.createClass({ // We have no room object for this room, only the ID. // We've got to this room by following a link, possibly a third party invite. + var room_alias = this.props.roomAddress[0] == '#' ? this.props.roomAddress : null; return (
@@ -1400,7 +1404,7 @@ module.exports = React.createClass({ invitedEmail = this.props.thirdPartyInvite.invitedEmail; } aux = ( - +
+ { error } +
+
+ ); + } + else { + var name = this.props.room ? this.props.room.name : (this.props.room_alias || ""); name = name ? { name } : "a room"; joinBlock = (
From c016eb78c8cb46c317b083275ddb9096c91eca03 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 28 Jun 2016 14:28:05 +0100 Subject: [PATCH 025/222] Fix user links 'Start chat' was broken on the sidebar if the panel was displayed by clicking on a link to a user. This adds null checking for the hack that we use to display users in the member panel. --- src/components/views/rooms/MemberInfo.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 66501abfa5..ddd0d1f6c6 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -358,10 +358,15 @@ module.exports = React.createClass({ ]; var existingRoomId; - var currentRoom = MatrixClientPeg.get().getRoom(this.props.member.roomId); - var currentMembers = currentRoom.getJoinedMembers(); + // roomId can be null here because of a hack in MatrixChat.onUserClick where we + // abuse this to view users rather than room members. + var currentMembers; + if (this.props.member.roomId) { + var currentRoom = MatrixClientPeg.get().getRoom(this.props.member.roomId); + currentMembers = currentRoom.getJoinedMembers(); + } // if we're currently in a 1:1 with this user, start a new chat - if (currentMembers.length === 2 && + if (currentMembers && currentMembers.length === 2 && userIds.indexOf(currentMembers[0].userId) !== -1 && userIds.indexOf(currentMembers[1].userId) !== -1) { From 548c392236078b29121256e5f9c86819bebb0300 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 28 Jun 2016 14:59:45 +0100 Subject: [PATCH 026/222] PR feedback --- src/components/structures/RoomView.js | 3 +++ src/components/views/rooms/RoomPreviewBar.js | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 78d9351171..776461e0e5 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -116,6 +116,9 @@ module.exports = React.createClass({ callState: null, guestsCanJoin: false, canPeek: false, + + // If we failed to load information about the room, + // store the error here. roomLoadError: null, // this is true if we are fully scrolled-down, and are looking at diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index f1b15490d7..2d83b14931 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -41,6 +41,9 @@ module.exports = React.createClass({ canPreview: React.PropTypes.bool, spinner: React.PropTypes.bool, room: React.PropTypes.object, + + // The alias that was used to access this room, if appropriate + roomAlias: React.PropTypes.object, }, getDefaultProps: function() { From e8337b21198ed4d4a1c72ffd571736273a6fce55 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 28 Jun 2016 17:11:47 +0100 Subject: [PATCH 027/222] More PR feedback --- src/components/structures/RoomView.js | 1 + src/components/views/rooms/RoomPreviewBar.js | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 776461e0e5..7946f723e8 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -117,6 +117,7 @@ module.exports = React.createClass({ guestsCanJoin: false, canPeek: false, + // error object, as from the matrix client/server API // If we failed to load information about the room, // store the error here. roomLoadError: null, diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index 2d83b14931..fee3dd8b4f 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -43,6 +43,8 @@ module.exports = React.createClass({ room: React.PropTypes.object, // The alias that was used to access this room, if appropriate + // If given, this will be how the room is referred to (eg. + // in error messages). roomAlias: React.PropTypes.object, }, From 174caceabf1010a1c89ec393af0bafa0f9039709 Mon Sep 17 00:00:00 2001 From: theworldbright Date: Wed, 29 Jun 2016 16:57:59 +0900 Subject: [PATCH 028/222] Use lastActiveAgo to reorder member list --- src/components/views/rooms/MemberList.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index 328f9774c7..da1ef04364 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -445,6 +445,8 @@ module.exports = React.createClass({ // console.log("comparing ts: " + lastActiveTsA + " and " + lastActiveTsB); + var lastActiveTsA = userA && userA.lastActiveAgo ? userA.lastActiveAgo : 0; + var lastActiveTsB = userB && userB.lastActiveAgo ? userB.lastActiveAgo : 0; return lastActiveTsB - lastActiveTsA; }, From 72a1d5a0deea3a1be0a6305f7b244438ddc28afa Mon Sep 17 00:00:00 2001 From: theworldbright Date: Wed, 29 Jun 2016 16:58:08 +0900 Subject: [PATCH 029/222] Remove unused comments --- src/components/views/rooms/MemberList.js | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index da1ef04364..f0b979edd9 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -423,28 +423,6 @@ module.exports = React.createClass({ if (userA.currentlyActive && !userB.currentlyActive) return -1; if (!userA.currentlyActive && userB.currentlyActive) return 1; - // For now, let's just order things by timestamp. It's really annoying - // that a user disappears from sight just because they temporarily go offline - /* - var presenceMap = { - online: 3, - unavailable: 2, - offline: 1 - }; - - var presenceOrdA = userA ? presenceMap[userA.presence] : 0; - var presenceOrdB = userB ? presenceMap[userB.presence] : 0; - - if (presenceOrdA != presenceOrdB) { - return presenceOrdB - presenceOrdA; - } - */ - - var lastActiveTsA = userA && userA.lastActiveTs ? userA.lastActiveTs : 0; - var lastActiveTsB = userB && userB.lastActiveTs ? userB.lastActiveTs : 0; - - // console.log("comparing ts: " + lastActiveTsA + " and " + lastActiveTsB); - var lastActiveTsA = userA && userA.lastActiveAgo ? userA.lastActiveAgo : 0; var lastActiveTsB = userB && userB.lastActiveAgo ? userB.lastActiveAgo : 0; return lastActiveTsB - lastActiveTsA; From 69cb0a8f1c726d62d5ba2dc09b879af4cc8b814e Mon Sep 17 00:00:00 2001 From: theworldbright Date: Wed, 29 Jun 2016 17:08:17 +0900 Subject: [PATCH 030/222] Switch ordering of Idle users --- src/components/views/rooms/MemberList.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index f0b979edd9..9c0fe401db 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -423,9 +423,9 @@ module.exports = React.createClass({ if (userA.currentlyActive && !userB.currentlyActive) return -1; if (!userA.currentlyActive && userB.currentlyActive) return 1; - var lastActiveTsA = userA && userA.lastActiveAgo ? userA.lastActiveAgo : 0; - var lastActiveTsB = userB && userB.lastActiveAgo ? userB.lastActiveAgo : 0; - return lastActiveTsB - lastActiveTsA; + var lastActiveTsA = userA && userA.lastActiveAgo ? userA.lastActiveAgo : 9999999999; + var lastActiveTsB = userB && userB.lastActiveAgo ? userB.lastActiveAgo : 9999999999; + return lastActiveTsA - lastActiveTsB; }, onSearchQueryChanged: function(input) { From b3d871aa6a1bfc1595fb7b4bba65e052fac6a5ad Mon Sep 17 00:00:00 2001 From: theworldbright Date: Wed, 29 Jun 2016 17:33:41 +0900 Subject: [PATCH 031/222] Add back comment --- src/components/views/rooms/MemberList.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index 9c0fe401db..28e6b97942 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -423,6 +423,8 @@ module.exports = React.createClass({ if (userA.currentlyActive && !userB.currentlyActive) return -1; if (!userA.currentlyActive && userB.currentlyActive) return 1; + // For now, let's just order things by timestamp. It's really annoying + // that a user disappears from sight just because they temporarily go offline var lastActiveTsA = userA && userA.lastActiveAgo ? userA.lastActiveAgo : 9999999999; var lastActiveTsB = userB && userB.lastActiveAgo ? userB.lastActiveAgo : 9999999999; return lastActiveTsA - lastActiveTsB; From 7609b9eba8b6711ce806d7eea3693de3d8db86ae Mon Sep 17 00:00:00 2001 From: theworldbright Date: Wed, 29 Jun 2016 17:45:24 +0900 Subject: [PATCH 032/222] Simplify logic for timestamp ordering of memberlist --- src/components/views/rooms/MemberList.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index 28e6b97942..20f60c80a8 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -425,9 +425,7 @@ module.exports = React.createClass({ // For now, let's just order things by timestamp. It's really annoying // that a user disappears from sight just because they temporarily go offline - var lastActiveTsA = userA && userA.lastActiveAgo ? userA.lastActiveAgo : 9999999999; - var lastActiveTsB = userB && userB.lastActiveAgo ? userB.lastActiveAgo : 9999999999; - return lastActiveTsA - lastActiveTsB; + return userB.getLastActiveTs() - userA.getLastActiveTs(); }, onSearchQueryChanged: function(input) { From 1c002866e8fbb0cb225fe0899ca08736c1101951 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Fri, 1 Jul 2016 23:08:51 +0530 Subject: [PATCH 033/222] feat: add and configure eslint --- .eslintrc | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++ .gitignore | 2 + package.json | 6 +++ 3 files changed, 112 insertions(+) create mode 100644 .eslintrc diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000000..761fd2af2b --- /dev/null +++ b/.eslintrc @@ -0,0 +1,104 @@ +{ + "parser": "babel-eslint", + "plugins": [ + "react", + "flowtype" + ], + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module", + "ecmaFeatures": { + "jsx": true, + "impliedStrict": true + } + }, + "env": { + "browser": true, + "amd": true, + "es6": true, + "node": true, + "mocha": true + }, + "extends": ["eslint:recommended", "plugin:react/recommended"], + "rules": { + "no-undef": ["warn"], + "global-strict": ["off"], + "no-extra-semi": ["warn"], + "no-underscore-dangle": ["off"], + "no-console": ["off"], + "no-unused-vars": ["off"], + "no-trailing-spaces": ["warn", { + "skipBlankLines": true + }], + "no-unreachable": ["warn"], + "no-spaced-func": ["warn"], + "no-new-func": ["error"], + "no-new-wrappers": ["error"], + "no-invalid-regexp": ["error"], + "no-extra-bind": ["error"], + "no-magic-numbers": ["error"], + "consistent-return": ["error"], + "valid-jsdoc": ["error"], + "no-use-before-define": ["error"], + "camelcase": ["warn"], + "array-callback-return": ["error"], + "dot-location": ["warn", "property"], + "guard-for-in": ["error"], + "no-useless-call": ["warn"], + "no-useless-escape": ["warn"], + "no-useless-concat": ["warn"], + "brace-style": ["warn", "1tbs"], + "comma-style": ["warn", "last"], + "space-before-function-paren": ["warn", "never"], + "space-before-blocks": ["warn", "always"], + "keyword-spacing": ["warn", { + "before": true, + "after": true + }], + + // dangling commas required, but only for multiline objects/arrays + "comma-dangle": ["warn", "always-multiline"], + // always === instead of ==, unless dealing with null/undefined + "eqeqeq": ["error", "smart"], + // always use curly braces, even with single statements + "curly": ["error", "all"], + // phasing out var in favour of let/const is a good idea + "no-var": ["warn"], + // always require semicolons + "semi": ["error", "always"], + // prefer rest and spread over the Old Ways + "prefer-spread": ["warn"], + "prefer-rest-params": ["warn"], + + /** react **/ + + // bind or arrow function in props causes performance issues + "react/jsx-no-bind": ["error"], + "react/jsx-key": ["error"], + "react/prefer-stateless-function": ["warn"], + "react/sort-comp": ["warn"], + + /** flowtype **/ + "flowtype/require-parameter-type": 1, + "flowtype/require-return-type": [ + 1, + "always", + { + "annotateUndefined": "never" + } + ], + "flowtype/space-after-type-colon": [ + 1, + "always" + ], + "flowtype/space-before-type-colon": [ + 1, + "never" + ] + }, + "settings": { + "flowtype": { + "onlyFilesWithFlowAnnotation": true + } + } +} diff --git a/.gitignore b/.gitignore index 8fdaf5903f..5139d614ad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +npm-debug.log + /node_modules /lib diff --git a/package.json b/package.json index e8c7dd4f97..be5556013b 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "reskindex": "reskindex -h header", "build": "babel src -d lib --source-maps", "start": "babel src -w -d lib --source-maps", + "lint": "eslint", + "lintall": "eslint src/ test/", "clean": "rimraf lib", "prepublish": "npm run build && git rev-parse HEAD > git-revision.txt", "test": "karma start $KARMAFLAGS --browsers PhantomJS", @@ -51,8 +53,12 @@ "devDependencies": { "babel": "^5.8.23", "babel-core": "^5.8.38", + "babel-eslint": "^6.1.0", "babel-loader": "^5.4.0", "babel-polyfill": "^6.5.0", + "eslint": "^2.13.1", + "eslint-plugin-flowtype": "^2.3.0", + "eslint-plugin-react": "^5.2.2", "expect": "^1.16.0", "json-loader": "^0.5.3", "karma": "^0.13.22", From 21cc4cba9a8796f25954c3d85aaec6e12857cfe7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 1 Jul 2016 19:30:53 +0100 Subject: [PATCH 034/222] Correct npm run lint command line --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index be5556013b..6c78c92b93 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "reskindex": "reskindex -h header", "build": "babel src -d lib --source-maps", "start": "babel src -w -d lib --source-maps", - "lint": "eslint", + "lint": "eslint src/", "lintall": "eslint src/ test/", "clean": "rimraf lib", "prepublish": "npm run build && git rev-parse HEAD > git-revision.txt", From 28e620f19b9ba68a55c0c4c90b70c2563da1c802 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 1 Jul 2016 19:38:14 +0100 Subject: [PATCH 035/222] Run eslint in jenkins script --- jenkins.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/jenkins.sh b/jenkins.sh index 26d434a504..b318b586e2 100755 --- a/jenkins.sh +++ b/jenkins.sh @@ -15,6 +15,9 @@ npm install # run the mocha tests npm run test +# run eslint +npm run lint -- -f checkstyle -o eslint.xml || true + # delete the old tarball, if it exists rm -f matrix-react-sdk-*.tgz From 8961c87cf95e1aa7edb349a9c7fdaf6d6c131228 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Sun, 3 Jul 2016 01:11:34 +0530 Subject: [PATCH 036/222] feat: Autocomplete selection wraparound --- .eslintrc | 7 ++- src/RichText.js | 14 ++++++ src/components/views/rooms/Autocomplete.js | 54 ++++++++++++++++------ 3 files changed, 59 insertions(+), 16 deletions(-) diff --git a/.eslintrc b/.eslintrc index 761fd2af2b..3f6c8e6953 100644 --- a/.eslintrc +++ b/.eslintrc @@ -36,7 +36,12 @@ "no-new-wrappers": ["error"], "no-invalid-regexp": ["error"], "no-extra-bind": ["error"], - "no-magic-numbers": ["error"], + "no-magic-numbers": ["error", { + "ignore": [-1, 0, 1], // usually used in array/string indexing + "ignoreArrayIndexes": true, + "enforceConst": true, + "detectObjects": true + }], "consistent-return": ["error"], "valid-jsdoc": ["error"], "no-use-before-define": ["error"], diff --git a/src/RichText.js b/src/RichText.js index 678a7de190..f4fa4883cb 100644 --- a/src/RichText.js +++ b/src/RichText.js @@ -9,6 +9,7 @@ import { SelectionState } from 'draft-js'; import * as sdk from './index'; +import * as emojione from 'emojione'; const BLOCK_RENDER_MAP = DefaultDraftBlockRenderMap.set('unstyled', { element: 'span' @@ -35,6 +36,8 @@ const MARKDOWN_REGEX = { const USERNAME_REGEX = /@\S+:\S+/g; const ROOM_REGEX = /#\S+:\S+/g; +let EMOJI_REGEX = null; +window.EMOJI_REGEX = EMOJI_REGEX = new RegExp(emojione.unicodeRegexp, 'g'); export function contentStateToHTML(contentState: ContentState): string { return contentState.getBlockMap().map((block) => { @@ -89,6 +92,7 @@ export function getScopedRTDecorators(scope: any): CompositeDecorator { return {avatar} {props.children}; } }; + let roomDecorator = { strategy: (contentBlock, callback) => { findWithRegex(ROOM_REGEX, contentBlock, callback); @@ -98,6 +102,16 @@ export function getScopedRTDecorators(scope: any): CompositeDecorator { } }; + // Unused for now, due to https://github.com/facebook/draft-js/issues/414 + let emojiDecorator = { + strategy: (contentBlock, callback) => { + findWithRegex(EMOJI_REGEX, contentBlock, callback); + }, + component: (props) => { + return + } + }; + return [usernameDecorator, roomDecorator]; } diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index ca0c5df701..414b0f1ebb 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -11,26 +11,28 @@ export default class Autocomplete extends React.Component { completions: [], // how far down the completion list we are - selectionOffset: 0 + selectionOffset: 0, }; } componentWillReceiveProps(props, state) { - if(props.query == this.props.query) return; + if (props.query === this.props.query) { + return; + } - getCompletions(props.query, props.selection).map(completionResult => { + getCompletions(props.query, props.selection).forEach(completionResult => { try { completionResult.completions.then(completions => { let i = this.state.completions.findIndex( completion => completion.provider === completionResult.provider ); - i = i == -1 ? this.state.completions.length : i; + i = i === -1 ? this.state.completions.length : i; let newCompletions = Object.assign([], this.state.completions); completionResult.completions = completions; newCompletions[i] = completionResult; this.setState({ - completions: newCompletions + completions: newCompletions, }); }, err => { console.error(err); @@ -42,13 +44,25 @@ export default class Autocomplete extends React.Component { }); } - onUpArrow() { - this.setState({selectionOffset: this.state.selectionOffset - 1}); + countCompletions(): number { + return this.state.completions.map(completionResult => { + return completionResult.completions.length; + }).reduce((l, r) => l + r); + } + + // called from MessageComposerInput + onUpArrow(): boolean { + let completionCount = this.countCompletions(), + selectionOffset = (completionCount + this.state.selectionOffset - 1) % completionCount; + this.setState({selectionOffset}); return true; } - onDownArrow() { - this.setState({selectionOffset: this.state.selectionOffset + 1}); + // called from MessageComposerInput + onDownArrow(): boolean { + let completionCount = this.countCompletions(), + selectionOffset = (this.state.selectionOffset + 1) % completionCount; + this.setState({selectionOffset}); return true; } @@ -58,18 +72,20 @@ export default class Autocomplete extends React.Component { let completions = completionResult.completions.map((completion, i) => { let Component = completion.component; let className = classNames('mx_Autocomplete_Completion', { - 'selected': position == this.state.selectionOffset + 'selected': position === this.state.selectionOffset, }); let componentPosition = position; position++; - if(Component) { + if (Component) { return Component; } + + let onMouseOver = () => this.setState({selectionOffset: componentPosition}); return (
this.setState({selectionOffset: componentPosition})}> + onMouseOver={onMouseOver}> {completion.title} {completion.subtitle} @@ -82,7 +98,11 @@ export default class Autocomplete extends React.Component { return completions.length > 0 ? (
{completionResult.provider.getName()} - + {completions}
@@ -91,7 +111,11 @@ export default class Autocomplete extends React.Component { return (
- + {renderedCompletions}
@@ -101,5 +125,5 @@ export default class Autocomplete extends React.Component { Autocomplete.propTypes = { // the query string for which to show autocomplete suggestions - query: React.PropTypes.string.isRequired + query: React.PropTypes.string.isRequired, }; From cccc58b47f77f0eec644bc2455916496ed468318 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Sun, 3 Jul 2016 22:15:13 +0530 Subject: [PATCH 037/222] feat: implement autocomplete replacement --- package.json | 1 + src/RichText.js | 51 ++++++++++++++--- src/autocomplete/AutocompleteProvider.js | 29 +++++++--- src/autocomplete/Autocompleter.js | 4 +- src/autocomplete/CommandProvider.js | 43 ++++++++------ src/autocomplete/Components.js | 14 +++-- src/autocomplete/DuckDuckGoProvider.js | 57 +++++++++++++------ src/autocomplete/EmojiProvider.js | 14 +++-- src/autocomplete/RoomProvider.js | 24 +++++--- src/autocomplete/UserProvider.js | 23 +++++--- src/components/views/rooms/Autocomplete.js | 51 ++++++++++++----- src/components/views/rooms/MessageComposer.js | 34 ++++++----- .../views/rooms/MessageComposerInput.js | 47 +++++++++++---- 13 files changed, 271 insertions(+), 121 deletions(-) diff --git a/package.json b/package.json index fc3b1c8f24..13cabf32d9 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "glob": "^5.0.14", "highlight.js": "^8.9.1", "linkifyjs": "^2.0.0-beta.4", + "lodash": "^4.13.1", "marked": "^0.3.5", "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop", "optimist": "^0.6.1", diff --git a/src/RichText.js b/src/RichText.js index f4fa4883cb..abbe860863 100644 --- a/src/RichText.js +++ b/src/RichText.js @@ -1,12 +1,14 @@ +import React from 'react'; import { Editor, Modifier, ContentState, + ContentBlock, convertFromHTML, DefaultDraftBlockRenderMap, DefaultDraftInlineStyle, CompositeDecorator, - SelectionState + SelectionState, } from 'draft-js'; import * as sdk from './index'; import * as emojione from 'emojione'; @@ -25,7 +27,7 @@ const STYLES = { CODE: 'code', ITALIC: 'em', STRIKETHROUGH: 's', - UNDERLINE: 'u' + UNDERLINE: 'u', }; const MARKDOWN_REGEX = { @@ -168,7 +170,7 @@ export function modifyText(contentState: ContentState, rangeToReplace: Selection text = ""; - for(let currentKey = startKey; + for (let currentKey = startKey; currentKey && currentKey !== endKey; currentKey = contentState.getKeyAfter(currentKey)) { let blockText = getText(currentKey); @@ -189,14 +191,14 @@ export function modifyText(contentState: ContentState, rangeToReplace: Selection * Note that this inherently means we make assumptions about what that means (no separator between ContentBlocks, etc) * Used by autocomplete to show completions when the current selection lies within, or at the edges of a command. */ -export function getTextSelectionOffsets(selectionState: SelectionState, - contentBlocks: Array): {start: number, end: number} { +export function selectionStateToTextOffsets(selectionState: SelectionState, + contentBlocks: Array): {start: number, end: number} { let offset = 0, start = 0, end = 0; for(let block of contentBlocks) { - if (selectionState.getStartKey() == block.getKey()) { + if (selectionState.getStartKey() === block.getKey()) { start = offset + selectionState.getStartOffset(); } - if (selectionState.getEndKey() == block.getKey()) { + if (selectionState.getEndKey() === block.getKey()) { end = offset + selectionState.getEndOffset(); break; } @@ -205,6 +207,37 @@ export function getTextSelectionOffsets(selectionState: SelectionState, return { start, - end - } + end, + }; +} + +export function textOffsetsToSelectionState({start, end}: {start: number, end: number}, + contentBlocks: Array): SelectionState { + let selectionState = SelectionState.createEmpty(); + + for (let block of contentBlocks) { + let blockLength = block.getLength(); + + if (start !== -1 && start < blockLength) { + selectionState = selectionState.merge({ + anchorKey: block.getKey(), + anchorOffset: start, + }); + start = -1; + } else { + start -= blockLength; + } + + if (end !== -1 && end <= blockLength) { + selectionState = selectionState.merge({ + focusKey: block.getKey(), + focusOffset: end, + }); + end = -1; + } else { + end -= blockLength; + } + } + + return selectionState; } diff --git a/src/autocomplete/AutocompleteProvider.js b/src/autocomplete/AutocompleteProvider.js index f741e085b0..05bbeacfab 100644 --- a/src/autocomplete/AutocompleteProvider.js +++ b/src/autocomplete/AutocompleteProvider.js @@ -14,25 +14,36 @@ export default class AutocompleteProvider { * Of the matched commands in the query, returns the first that contains or is contained by the selection, or null. */ getCurrentCommand(query: string, selection: {start: number, end: number}): ?Array { - if(this.commandRegex == null) + if (this.commandRegex == null) { return null; + } - let match = null; - while((match = this.commandRegex.exec(query)) != null) { + let match; + while ((match = this.commandRegex.exec(query)) != null) { let matchStart = match.index, matchEnd = matchStart + match[0].length; - - console.log(match); - if(selection.start <= matchEnd && selection.end >= matchStart) { - return match; + if (selection.start <= matchEnd && selection.end >= matchStart) { + return { + command: match, + range: { + start: matchStart, + end: matchEnd, + }, + }; } } this.commandRegex.lastIndex = 0; - return null; + return { + command: null, + range: { + start: -1, + end: -1, + }, + }; } - getCompletions(query: String, selection: {start: number, end: number}) { + getCompletions(query: string, selection: {start: number, end: number}) { return Q.when([]); } diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index 6b66d2fbdc..7f32e0ca40 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -9,14 +9,14 @@ const PROVIDERS = [ CommandProvider, DuckDuckGoProvider, RoomProvider, - EmojiProvider + EmojiProvider, ].map(completer => completer.getInstance()); export function getCompletions(query: string, selection: {start: number, end: number}) { return PROVIDERS.map(provider => { return { completions: provider.getCompletions(query, selection), - provider + provider, }; }); } diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index 30b448d7f2..19a366ac63 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -1,42 +1,45 @@ +import React from 'react'; import AutocompleteProvider from './AutocompleteProvider'; import Q from 'q'; import Fuse from 'fuse.js'; +import {TextualCompletion} from './Components'; const COMMANDS = [ { command: '/me', args: '', - description: 'Displays action' + description: 'Displays action', }, { command: '/ban', args: ' [reason]', - description: 'Bans user with given id' + description: 'Bans user with given id', }, { - command: '/deop' + command: '/deop', + args: '', + description: 'Deops user with given id', }, { - command: '/encrypt' - }, - { - command: '/invite' + command: '/invite', + args: '', + description: 'Invites user with given id to current room' }, { command: '/join', args: '', - description: 'Joins room with given alias' + description: 'Joins room with given alias', }, { command: '/kick', args: ' [reason]', - description: 'Kicks user with given id' + description: 'Kicks user with given id', }, { command: '/nick', args: '', - description: 'Changes your display nickname' - } + description: 'Changes your display nickname', + }, ]; let COMMAND_RE = /(^\/\w*)/g; @@ -47,19 +50,23 @@ export default class CommandProvider extends AutocompleteProvider { constructor() { super(COMMAND_RE); this.fuse = new Fuse(COMMANDS, { - keys: ['command', 'args', 'description'] + keys: ['command', 'args', 'description'], }); } getCompletions(query: string, selection: {start: number, end: number}) { let completions = []; - const command = this.getCurrentCommand(query, selection); - if(command) { + let {command, range} = this.getCurrentCommand(query, selection); + if (command) { completions = this.fuse.search(command[0]).map(result => { return { - title: result.command, - subtitle: result.args, - description: result.description + completion: result.command + ' ', + component: (), + range, }; }); } @@ -71,7 +78,7 @@ export default class CommandProvider extends AutocompleteProvider { } static getInstance(): CommandProvider { - if(instance == null) + if (instance == null) instance = new CommandProvider(); return instance; diff --git a/src/autocomplete/Components.js b/src/autocomplete/Components.js index cb7d56f9bf..d9d1c7b3ff 100644 --- a/src/autocomplete/Components.js +++ b/src/autocomplete/Components.js @@ -1,13 +1,19 @@ -export function TextualCompletion(props: { +import React from 'react'; + +export function TextualCompletion({ + title, + subtitle, + description, +}: { title: ?string, subtitle: ?string, description: ?string }) { return (
- {completion.title} - {completion.subtitle} - {completion.description} + {title} + {subtitle} + {description}
); } diff --git a/src/autocomplete/DuckDuckGoProvider.js b/src/autocomplete/DuckDuckGoProvider.js index b2bf27a21a..cfd3cb2ff6 100644 --- a/src/autocomplete/DuckDuckGoProvider.js +++ b/src/autocomplete/DuckDuckGoProvider.js @@ -1,9 +1,12 @@ +import React from 'react'; import AutocompleteProvider from './AutocompleteProvider'; import Q from 'q'; import 'whatwg-fetch'; +import {TextualCompletion} from './Components'; + const DDG_REGEX = /\/ddg\s+(.+)$/g; -const REFERER = 'vector'; +const REFERRER = 'vector'; let instance = null; @@ -14,42 +17,62 @@ export default class DuckDuckGoProvider extends AutocompleteProvider { static getQueryUri(query: String) { return `http://api.duckduckgo.com/?q=${encodeURIComponent(query)}` - + `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERER)}`; + + `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERRER)}`; } getCompletions(query: string, selection: {start: number, end: number}) { - let command = this.getCurrentCommand(query, selection); - if(!query || !command) + let {command, range} = this.getCurrentCommand(query, selection); + if (!query || !command) { return Q.when([]); + } return fetch(DuckDuckGoProvider.getQueryUri(command[1]), { - method: 'GET' + method: 'GET', }) .then(response => response.json()) .then(json => { let results = json.Results.map(result => { return { - title: result.Text, - description: result.Result + completion: result.Text, + component: ( + + ), + range, }; }); - if(json.Answer) { + if (json.Answer) { results.unshift({ - title: json.Answer, - description: json.AnswerType + completion: json.Answer, + component: ( + + ), + range, }); } - if(json.RelatedTopics && json.RelatedTopics.length > 0) { + if (json.RelatedTopics && json.RelatedTopics.length > 0) { results.unshift({ - title: json.RelatedTopics[0].Text + completion: json.RelatedTopics[0].Text, + component: ( + + ), + range, }); } - if(json.AbstractText) { + if (json.AbstractText) { results.unshift({ - title: json.AbstractText + completion: json.AbstractText, + component: ( + + ), + range, }); } - // console.log(results); return results; }); } @@ -59,9 +82,9 @@ export default class DuckDuckGoProvider extends AutocompleteProvider { } static getInstance(): DuckDuckGoProvider { - if(instance == null) + if (instance == null) { instance = new DuckDuckGoProvider(); - + } return instance; } } diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index e1b5f3ea38..574144e95b 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -1,6 +1,7 @@ +import React from 'react'; import AutocompleteProvider from './AutocompleteProvider'; import Q from 'q'; -import {emojioneList, shortnameToImage} from 'emojione'; +import {emojioneList, shortnameToImage, shortnameToUnicode} from 'emojione'; import Fuse from 'fuse.js'; const EMOJI_REGEX = /:\w*:?/g; @@ -16,18 +17,19 @@ export default class EmojiProvider extends AutocompleteProvider { getCompletions(query: string, selection: {start: number, end: number}) { let completions = []; - let command = this.getCurrentCommand(query, selection); - if(command) { + let {command, range} = this.getCurrentCommand(query, selection); + if (command) { completions = this.fuse.search(command[0]).map(result => { let shortname = EMOJI_SHORTNAMES[result]; let imageHTML = shortnameToImage(shortname); return { - title: shortname, + completion: shortnameToUnicode(shortname), component: (
{shortname}
- ) + ), + range, }; }).slice(0, 4); } @@ -39,7 +41,7 @@ export default class EmojiProvider extends AutocompleteProvider { } static getInstance() { - if(instance == null) + if (instance == null) instance = new EmojiProvider(); return instance; } diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index 8b5650e7a7..e38be65987 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -1,7 +1,9 @@ +import React from 'react'; import AutocompleteProvider from './AutocompleteProvider'; import Q from 'q'; import MatrixClientPeg from '../MatrixClientPeg'; import Fuse from 'fuse.js'; +import {TextualCompletion} from './Components'; const ROOM_REGEX = /(?=#)([^\s]*)/g; @@ -10,32 +12,35 @@ let instance = null; export default class RoomProvider extends AutocompleteProvider { constructor() { super(ROOM_REGEX, { - keys: ['displayName', 'userId'] + keys: ['displayName', 'userId'], }); this.fuse = new Fuse([], { - keys: ['name', 'roomId', 'aliases'] + keys: ['name', 'roomId', 'aliases'], }); } getCompletions(query: string, selection: {start: number, end: number}) { let client = MatrixClientPeg.get(); let completions = []; - const command = this.getCurrentCommand(query, selection); - if(command) { + const {command, range} = this.getCurrentCommand(query, selection); + 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 => { return { name: room.name, roomId: room.roomId, - aliases: room.getAliases() + aliases: room.getAliases(), }; })); completions = this.fuse.search(command[0]).map(room => { return { - title: room.name, - subtitle: room.roomId + completion: room.roomId, + component: ( + + ), + range, }; - }).slice(0, 4);; + }).slice(0, 4); } return Q.when(completions); } @@ -45,8 +50,9 @@ export default class RoomProvider extends AutocompleteProvider { } static getInstance() { - if(instance == null) + if (instance == null) { instance = new RoomProvider(); + } return instance; } diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 3edb2bf00c..3e65a65676 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -1,6 +1,8 @@ +import React from 'react'; import AutocompleteProvider from './AutocompleteProvider'; import Q from 'q'; import Fuse from 'fuse.js'; +import {TextualCompletion} from './Components'; const USER_REGEX = /@[^\s]*/g; @@ -9,23 +11,27 @@ let instance = null; export default class UserProvider extends AutocompleteProvider { constructor() { super(USER_REGEX, { - keys: ['displayName', 'userId'] + keys: ['displayName', 'userId'], }); this.users = []; this.fuse = new Fuse([], { - keys: ['displayName', 'userId'] - }) + keys: ['displayName', 'userId'], + }); } getCompletions(query: string, selection: {start: number, end: number}) { let completions = []; - let command = this.getCurrentCommand(query, selection); - if(command) { + let {command, range} = this.getCurrentCommand(query, selection); + if (command) { this.fuse.set(this.users); completions = this.fuse.search(command[0]).map(user => { return { - title: user.displayName || user.userId, - description: user.userId + completion: user.userId, + component: ( + + ), }; }).slice(0, 4); } @@ -41,8 +47,9 @@ export default class UserProvider extends AutocompleteProvider { } static getInstance(): UserProvider { - if(instance == null) + if (instance == null) { instance = new UserProvider(); + } return instance; } } diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index 414b0f1ebb..dfeda96845 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -1,15 +1,23 @@ import React from 'react'; import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; import classNames from 'classnames'; +import _ from 'lodash'; import {getCompletions} from '../../../autocomplete/Autocompleter'; export default class Autocomplete extends React.Component { constructor(props) { super(props); + + this.onConfirm = this.onConfirm.bind(this); + this.state = { + // list of completionResults, each containing completions completions: [], + // array of completions, so we can look up current selection by offset quickly + completionList: [], + // how far down the completion list we are selectionOffset: 0, }; @@ -31,8 +39,10 @@ export default class Autocomplete extends React.Component { let newCompletions = Object.assign([], this.state.completions); completionResult.completions = completions; newCompletions[i] = completionResult; + this.setState({ completions: newCompletions, + completionList: _.flatMap(newCompletions, provider => provider.completions), }); }, err => { console.error(err); @@ -54,7 +64,7 @@ export default class Autocomplete extends React.Component { onUpArrow(): boolean { let completionCount = this.countCompletions(), selectionOffset = (completionCount + this.state.selectionOffset - 1) % completionCount; - this.setState({selectionOffset}); + this.setSelection(selectionOffset); return true; } @@ -62,34 +72,49 @@ export default class Autocomplete extends React.Component { onDownArrow(): boolean { let completionCount = this.countCompletions(), selectionOffset = (this.state.selectionOffset + 1) % completionCount; - this.setState({selectionOffset}); + this.setSelection(selectionOffset); return true; } + /** called from MessageComposerInput + * @returns {boolean} whether confirmation was handled + */ + onConfirm(): boolean { + if (this.countCompletions() === 0) + return false; + + let selectedCompletion = this.state.completionList[this.state.selectionOffset]; + this.props.onConfirm(selectedCompletion.range, selectedCompletion.completion); + + return true; + } + + setSelection(selectionOffset: number) { + this.setState({selectionOffset}); + } + render() { let position = 0; let renderedCompletions = this.state.completions.map((completionResult, i) => { let completions = completionResult.completions.map((completion, i) => { - let Component = completion.component; let className = classNames('mx_Autocomplete_Completion', { 'selected': position === this.state.selectionOffset, }); let componentPosition = position; position++; - if (Component) { - return Component; - } - let onMouseOver = () => this.setState({selectionOffset: componentPosition}); - + let onMouseOver = () => this.setSelection(componentPosition), + onClick = () => { + this.setSelection(componentPosition); + this.onConfirm(); + }; + return (
- {completion.title} - {completion.subtitle} - - {completion.description} + onMouseOver={onMouseOver} + onClick={onClick}> + {completion.component}
); }); diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 24d0bd2510..4dc28e73c5 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -40,16 +40,17 @@ export default class MessageComposer extends React.Component { this.state = { autocompleteQuery: '', - selection: null + selection: null, }; + } onUploadClick(ev) { if (MatrixClientPeg.get().isGuest()) { - var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); + let NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); Modal.createDialog(NeedToRegisterDialog, { title: "Please Register", - description: "Guest users can't upload files. Please register to upload." + description: "Guest users can't upload files. Please register to upload.", }); return; } @@ -58,13 +59,13 @@ export default class MessageComposer extends React.Component { } onUploadFileSelected(ev) { - var files = ev.target.files; + let files = ev.target.files; - var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - var TintableSvg = sdk.getComponent("elements.TintableSvg"); + let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + let TintableSvg = sdk.getComponent("elements.TintableSvg"); - var fileList = []; - for(var i=0; i {files[i].name} ); @@ -91,7 +92,7 @@ export default class MessageComposer extends React.Component { } this.refs.uploadInput.value = null; - } + }, }); } @@ -105,7 +106,7 @@ export default class MessageComposer extends React.Component { action: 'hangup', // hangup the call for this room, which may not be the room in props // (e.g. conferences which will hangup the 1:1 room instead) - room_id: call.roomId + room_id: call.roomId, }); } @@ -113,7 +114,7 @@ export default class MessageComposer extends React.Component { dis.dispatch({ action: 'place_call', type: ev.shiftKey ? "screensharing" : "video", - room_id: this.props.room.roomId + room_id: this.props.room.roomId, }); } @@ -121,14 +122,14 @@ export default class MessageComposer extends React.Component { dis.dispatch({ action: 'place_call', type: 'voice', - room_id: this.props.room.roomId + room_id: this.props.room.roomId, }); } onInputContentChanged(content: string, selection: {start: number, end: number}) { this.setState({ autocompleteQuery: content, - selection + selection, }); } @@ -171,11 +172,11 @@ export default class MessageComposer extends React.Component { callButton =
-
+
; videoCallButton =
-
+
; } var canSendMessages = this.props.room.currentState.maySendMessage( @@ -198,9 +199,11 @@ export default class MessageComposer extends React.Component { controls.push( this.messageComposerInput = c} key="controls_input" onResize={this.props.onResize} room={this.props.room} + tryComplete={this.refs.autocomplete && this.refs.autocomplete.onConfirm} onUpArrow={this.onUpArrow} onDownArrow={this.onDownArrow} onTab={this.onTab} @@ -223,6 +226,7 @@ export default class MessageComposer extends React.Component {
diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 313216d54c..46abc20ed6 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -76,6 +76,7 @@ export default class MessageComposerInput extends React.Component { this.onUpArrow = this.onUpArrow.bind(this); this.onDownArrow = this.onDownArrow.bind(this); this.onTab = this.onTab.bind(this); + this.onConfirmAutocompletion = this.onConfirmAutocompletion.bind(this); let isRichtextEnabled = window.localStorage.getItem('mx_editor_rte_enabled'); if(isRichtextEnabled == null) { @@ -85,7 +86,7 @@ export default class MessageComposerInput extends React.Component { this.state = { isRichtextEnabled: isRichtextEnabled, - editorState: null + editorState: null, }; // bit of a hack, but we need to do this here since createEditorState needs isRichtextEnabled @@ -96,7 +97,7 @@ export default class MessageComposerInput extends React.Component { static getKeyBinding(e: SyntheticKeyboardEvent): string { // C-m => Toggles between rich text and markdown modes - if(e.keyCode == KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) { + if (e.keyCode === KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) { return 'toggle-mode'; } @@ -212,7 +213,7 @@ export default class MessageComposerInput extends React.Component { let content = convertFromRaw(JSON.parse(contentJSON)); component.setEditorState(component.createEditorState(component.state.isRichtextEnabled, content)); } - } + }, }; } @@ -234,7 +235,7 @@ export default class MessageComposerInput extends React.Component { } onAction(payload) { - var editor = this.refs.editor; + let editor = this.refs.editor; switch (payload.action) { case 'focus_composer': @@ -252,7 +253,7 @@ export default class MessageComposerInput extends React.Component { payload.displayname ); this.setState({ - editorState: EditorState.push(this.state.editorState, contentState, 'insert-characters') + editorState: EditorState.push(this.state.editorState, contentState, 'insert-characters'), }); editor.focus(); } @@ -356,7 +357,7 @@ export default class MessageComposerInput extends React.Component { if(this.props.onContentChanged) { this.props.onContentChanged(editorState.getCurrentContent().getPlainText(), - RichText.getTextSelectionOffsets(editorState.getSelection(), + RichText.selectionStateToTextOffsets(editorState.getSelection(), editorState.getCurrentContent().getBlocksAsArray())); } } @@ -418,12 +419,21 @@ export default class MessageComposerInput extends React.Component { } handleReturn(ev) { - if(ev.shiftKey) + if (ev.shiftKey) { return false; + } + + if(this.props.tryComplete) { + if(this.props.tryComplete()) { + return true; + } + } const contentState = this.state.editorState.getCurrentContent(); - if(!contentState.hasText()) + if (!contentState.hasText()) { return true; + } + let contentText = contentState.getPlainText(), contentHTML; @@ -509,17 +519,32 @@ export default class MessageComposerInput extends React.Component { } onTab(e) { - if(this.props.onTab) { - if(this.props.onTab()) { + if (this.props.onTab) { + if (this.props.onTab()) { e.preventDefault(); } } } + onConfirmAutocompletion(range, content: string) { + let contentState = Modifier.replaceText( + this.state.editorState.getCurrentContent(), + RichText.textOffsetsToSelectionState(range, this.state.editorState.getCurrentContent().getBlocksAsArray()), + content + ); + + this.setState({ + editorState: EditorState.push(this.state.editorState, contentState, 'insert-characters'), + }); + + // for some reason, doing this right away does not update the editor :( + setTimeout(() => this.refs.editor.focus(), 50); + } + render() { let className = "mx_MessageComposer_input"; - if(this.state.isRichtextEnabled) { + if (this.state.isRichtextEnabled) { className += " mx_MessageComposer_input_rte"; // placeholder indicator for RTE mode } From 30b7efd5857d6334f0b24e45c5ea8eff46ffa315 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Mon, 4 Jul 2016 21:44:35 +0530 Subject: [PATCH 038/222] fix: code cleanup, fix getCurrentCommand --- src/RichText.js | 5 ++--- src/autocomplete/AutocompleteProvider.js | 3 ++- src/autocomplete/Components.js | 4 ++-- src/components/structures/RoomView.js | 3 --- src/components/views/rooms/Autocomplete.js | 16 ++++++++++------ 5 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/RichText.js b/src/RichText.js index abbe860863..c24a510e05 100644 --- a/src/RichText.js +++ b/src/RichText.js @@ -33,13 +33,12 @@ const STYLES = { const MARKDOWN_REGEX = { LINK: /(?:\[([^\]]+)\]\(([^\)]+)\))|\<(\w+:\/\/[^\>]+)\>/g, ITALIC: /([\*_])([\w\s]+?)\1/g, - BOLD: /([\*_])\1([\w\s]+?)\1\1/g + BOLD: /([\*_])\1([\w\s]+?)\1\1/g, }; const USERNAME_REGEX = /@\S+:\S+/g; const ROOM_REGEX = /#\S+:\S+/g; -let EMOJI_REGEX = null; -window.EMOJI_REGEX = EMOJI_REGEX = new RegExp(emojione.unicodeRegexp, 'g'); +const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp, 'g'); export function contentStateToHTML(contentState: ContentState): string { return contentState.getBlockMap().map((block) => { diff --git a/src/autocomplete/AutocompleteProvider.js b/src/autocomplete/AutocompleteProvider.js index 05bbeacfab..41d5d035d1 100644 --- a/src/autocomplete/AutocompleteProvider.js +++ b/src/autocomplete/AutocompleteProvider.js @@ -18,6 +18,8 @@ export default class AutocompleteProvider { return null; } + this.commandRegex.lastIndex = 0; + let match; while ((match = this.commandRegex.exec(query)) != null) { let matchStart = match.index, @@ -33,7 +35,6 @@ export default class AutocompleteProvider { }; } } - this.commandRegex.lastIndex = 0; return { command: null, range: { diff --git a/src/autocomplete/Components.js b/src/autocomplete/Components.js index d9d1c7b3ff..4a24c79966 100644 --- a/src/autocomplete/Components.js +++ b/src/autocomplete/Components.js @@ -10,10 +10,10 @@ export function TextualCompletion({ description: ?string }) { return ( -
+
{title} {subtitle} - {description} + {description}
); } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index bee038a9e2..dc4b21a300 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -520,9 +520,6 @@ module.exports = React.createClass({ _updateTabCompleteList: function() { var cli = MatrixClientPeg.get(); - console.log('_updateTabCompleteList'); - console.log(this.state.room); - console.trace(); if (!this.state.room) { return; diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index dfeda96845..5ee638c479 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -80,8 +80,9 @@ export default class Autocomplete extends React.Component { * @returns {boolean} whether confirmation was handled */ onConfirm(): boolean { - if (this.countCompletions() === 0) + if (this.countCompletions() === 0) { return false; + } let selectedCompletion = this.state.completionList[this.state.selectionOffset]; this.props.onConfirm(selectedCompletion.range, selectedCompletion.completion); @@ -103,11 +104,11 @@ export default class Autocomplete extends React.Component { let componentPosition = position; position++; - let onMouseOver = () => this.setSelection(componentPosition), - onClick = () => { - this.setSelection(componentPosition); - this.onConfirm(); - }; + let onMouseOver = () => this.setSelection(componentPosition); + let onClick = () => { + this.setSelection(componentPosition); + this.onConfirm(); + }; return (
Date: Mon, 4 Jul 2016 17:15:15 +0100 Subject: [PATCH 039/222] Add a JS code style doc --- code_style.rst | 116 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 code_style.rst diff --git a/code_style.rst b/code_style.rst new file mode 100644 index 0000000000..ff48bc381d --- /dev/null +++ b/code_style.rst @@ -0,0 +1,116 @@ +Matrix JavaScript/ECMAScript Style Guide +======================================== + +The intention of this guide is to make Matrix's JavaScript codebase clean, +consistent with other popular JavaScript styles and consistent with the rest of +the Matrix codebase. For reference, the Matrix Python style guide can be found +at https://github.com/matrix-org/synapse/blob/master/docs/code_style.rst + +General Style +------------- + +- 4 spaces to indent, for consistency with Matrix Python. +- Max line width: 79 chars (with flexibility to overflow by a "few chars" if + the overflowing content is not semantically significant and avoids an + explosion of vertical whitespace). +- No trailing whitespace at end of lines. +- Don't indent empty lines. +- One newline at the end of the file. +- Unix newlines, never `\r` +- Indent similar to our python code: break up long lines at logical boundaries, + more than one argument on a line is OK +- Use semicolons, for consistency with node. +- UpperCamelCase for class and type names +- lowerCamelCase for functions and variables. +- Single line ternary operators are fine. +- UPPER_CAMEL_CASE for constants +- Single quotes for strings, for consistency with most JavaScript styles:: + "bad" // Bad + 'good' // Good +- Use parentheses instead of '\\' for line continuation where ever possible +- Open braces on the same line (consistent with Node):: + if (x) { + console.log("I am a fish"); // Good + } + + if (x) + { + console.log("I am a fish"); // Bad + } +- Spaces after `if`, `for`, `else` etc, no space around the condition:: + if (x) { + console.log("I am a fish"); // Good + } + + if(x) { + console.log("I am a fish"); // Bad + } + + if ( x ) { + console.log("I am a fish"); // Bad + } +- Declare one variable per var statement (consistent with Node). Unless they + are simple and closely related. If you put the next declaration on a new line, + treat yourself to another `var`:: + var key = "foo", + comparator = function(x, y) { + return x - y; + }; // Bad + + var key = "foo"; + var comparator = function(x, y) { + return x - y; + }; // Good + + var x = 0, y = 0; // Fine + + var x = 0; + var y = 0; // Also fine +- A single line `if` is fine, all others have braces. This prevents errors when adding to the code.:: + if (x) return true; // Fine + + if (x) { + return true; // Also fine + } + + if (x) + return true; // Not fine +- Terminate all multi-line lists with commas:: + var mascots = [ + "Patrick", + "Shirley", + "Colin", + "Susan", + "Sir Arthur David" // Bad + ]; + + var mascots = [ + "Patrick", + "Shirley", + "Colin", + "Susan", + "Sir Arthur David", // Good + ]; +- Use `null`, `undefined` etc consistently with node: + Boolean variables and functions should always be either true or false. Don't set it to 0 unless it's supposed to be a number. + When something is intentionally missing or removed, set it to null. + Don't set things to undefined. Reserve that value to mean "not yet set to anything." + Boolean objects are verboten. +- Use JSDoc + +ECMAScript +---------- +- Use `let` for variables and `const` for constants. This sounds obvious, but it isn't: the ES6 `const` keyword + could be used for assign-once variables, however this guide advises against doing so on the grounds that it + confuses them with constants. +- Be careful migrating files to newer syntax. + - Don't mix `require` and `import` in the same file. Either stick to the old style or change them all. + - Likewise, don't mix things like class properties and `MyClass.prototype.MY_CONSTANT = 42;` + - Be careful mixing arrow functions and regular functions, eg. if one function in a promise chain is an + arrow function, they probably all should be. +- Apart from that, newer ES features should be used whenever the author deems them to be appropriate. +- Flow annotations are welcome and encouraged. + +React +----- +- Use ES6 classes, although bear in mind a lot of code uses createClass. From 56c73b68a9abec549b03232be8dd0e3d2e3f1f9a Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 4 Jul 2016 17:23:38 +0100 Subject: [PATCH 040/222] Use markdown because the rst wasn't formatting and we use md for everything else in this repo, and in a document that talks about consistency... --- code_style.rst => code_style.md | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) rename code_style.rst => code_style.md (94%) diff --git a/code_style.rst b/code_style.md similarity index 94% rename from code_style.rst rename to code_style.md index ff48bc381d..eea23aed9c 100644 --- a/code_style.rst +++ b/code_style.md @@ -8,7 +8,6 @@ at https://github.com/matrix-org/synapse/blob/master/docs/code_style.rst General Style ------------- - - 4 spaces to indent, for consistency with Matrix Python. - Max line width: 79 chars (with flexibility to overflow by a "few chars" if the overflowing content is not semantically significant and avoids an @@ -28,7 +27,8 @@ General Style "bad" // Bad 'good' // Good - Use parentheses instead of '\\' for line continuation where ever possible -- Open braces on the same line (consistent with Node):: +- Open braces on the same line (consistent with Node): + ``` if (x) { console.log("I am a fish"); // Good } @@ -37,7 +37,9 @@ General Style { console.log("I am a fish"); // Bad } -- Spaces after `if`, `for`, `else` etc, no space around the condition:: + ``` +- Spaces after `if`, `for`, `else` etc, no space around the condition: + ``` if (x) { console.log("I am a fish"); // Good } @@ -49,9 +51,11 @@ General Style if ( x ) { console.log("I am a fish"); // Bad } + ``` - Declare one variable per var statement (consistent with Node). Unless they are simple and closely related. If you put the next declaration on a new line, - treat yourself to another `var`:: + treat yourself to another `var`: + ``` var key = "foo", comparator = function(x, y) { return x - y; @@ -66,7 +70,9 @@ General Style var x = 0; var y = 0; // Also fine -- A single line `if` is fine, all others have braces. This prevents errors when adding to the code.:: + ``` +- A single line `if` is fine, all others have braces. This prevents errors when adding to the code.: + ``` if (x) return true; // Fine if (x) { @@ -75,7 +81,9 @@ General Style if (x) return true; // Not fine -- Terminate all multi-line lists with commas:: + ``` +- Terminate all multi-line lists with commas: + ``` var mascots = [ "Patrick", "Shirley", @@ -91,6 +99,7 @@ General Style "Susan", "Sir Arthur David", // Good ]; + ``` - Use `null`, `undefined` etc consistently with node: Boolean variables and functions should always be either true or false. Don't set it to 0 unless it's supposed to be a number. When something is intentionally missing or removed, set it to null. From 73b726e5fb8f43cffef2991eecc3ce11813eeca8 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Mon, 4 Jul 2016 21:56:09 +0530 Subject: [PATCH 041/222] feat: import only flatMap from lodash --- src/components/views/rooms/Autocomplete.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index 5ee638c479..95133778ba 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -1,7 +1,7 @@ import React from 'react'; import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; import classNames from 'classnames'; -import _ from 'lodash'; +import flatMap from 'lodash/flatMap'; import {getCompletions} from '../../../autocomplete/Autocompleter'; @@ -42,7 +42,7 @@ export default class Autocomplete extends React.Component { this.setState({ completions: newCompletions, - completionList: _.flatMap(newCompletions, provider => provider.completions), + completionList: flatMap(newCompletions, provider => provider.completions), }); }, err => { console.error(err); From b3d82921137a28edf5b511390c5d6bc34ee402ff Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Mon, 4 Jul 2016 22:34:58 +0530 Subject: [PATCH 042/222] feat: use canonical room alias for room completion --- src/autocomplete/Components.js | 2 +- src/autocomplete/RoomProvider.js | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/autocomplete/Components.js b/src/autocomplete/Components.js index 4a24c79966..168da00c1c 100644 --- a/src/autocomplete/Components.js +++ b/src/autocomplete/Components.js @@ -13,7 +13,7 @@ export function TextualCompletion({
{title} {subtitle} - {description} + {description}
); } diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index e38be65987..b34fdeb59a 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -4,6 +4,7 @@ import Q from 'q'; import MatrixClientPeg from '../MatrixClientPeg'; import Fuse from 'fuse.js'; import {TextualCompletion} from './Components'; +import {getDisplayAliasForRoom} from '../MatrixTools'; const ROOM_REGEX = /(?=#)([^\s]*)/g; @@ -27,16 +28,18 @@ export default class RoomProvider extends AutocompleteProvider { // 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 => { return { + room: room, name: room.name, roomId: room.roomId, aliases: room.getAliases(), }; })); completions = this.fuse.search(command[0]).map(room => { + let displayAlias = getDisplayAliasForRoom(room.room) || room.roomId; return { - completion: room.roomId, + completion: displayAlias, component: ( - + ), range, }; From 22a2593bf2a3691519667e7d8cdc417a17203b5b Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Tue, 5 Jul 2016 01:32:40 +0530 Subject: [PATCH 043/222] fix: unbreak old MessageComposerInput/TabComplete --- src/components/views/rooms/MessageComposer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 4dc28e73c5..f9371385d4 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -207,6 +207,7 @@ export default class MessageComposer extends React.Component { onUpArrow={this.onUpArrow} onDownArrow={this.onDownArrow} onTab={this.onTab} + tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete onContentChanged={this.onInputContentChanged} />, uploadButton, hangupButton, From ed305bd547b3bd9d2ffb04a44d12f2d0f3a55c29 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Tue, 5 Jul 2016 01:37:01 +0530 Subject: [PATCH 044/222] fix: https for DDG, provide range for UserProvider --- src/autocomplete/DuckDuckGoProvider.js | 2 +- src/autocomplete/UserProvider.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/autocomplete/DuckDuckGoProvider.js b/src/autocomplete/DuckDuckGoProvider.js index cfd3cb2ff6..1746ce0aaa 100644 --- a/src/autocomplete/DuckDuckGoProvider.js +++ b/src/autocomplete/DuckDuckGoProvider.js @@ -16,7 +16,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider { } static getQueryUri(query: String) { - return `http://api.duckduckgo.com/?q=${encodeURIComponent(query)}` + return `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}` + `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERRER)}`; } diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 3e65a65676..a583592bae 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -32,6 +32,7 @@ export default class UserProvider extends AutocompleteProvider { title={user.displayName || user.userId} description={user.userId} /> ), + range }; }).slice(0, 4); } From 48f2c4a69680839000e1347a5cb83d3df66057cd Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Tue, 5 Jul 2016 03:13:53 +0530 Subject: [PATCH 045/222] feat: render unicode emoji as emojione images --- src/HtmlUtils.js | 51 +++++++++++++++++++++--------------------------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index dbcb59a20a..629851f9e8 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -20,6 +20,8 @@ var React = require('react'); var sanitizeHtml = require('sanitize-html'); var highlight = require('highlight.js'); var linkifyMatrix = require('./linkify-matrix'); +import escape from 'lodash/escape'; +import {unicodeToImage} from 'emojione'; var sanitizeHtmlParams = { allowedTags: [ @@ -185,40 +187,31 @@ module.exports = { opts = opts || {}; var isHtml = (content.format === "org.matrix.custom.html"); + let body = isHtml ? content.formatted_body : escape(content.body); var safeBody; - if (isHtml) { - // XXX: We sanitize the HTML whilst also highlighting its text nodes, to avoid accidentally trying - // to highlight HTML tags themselves. However, this does mean that we don't highlight textnodes which - // are interrupted by HTML tags (not that we did before) - e.g. foobar won't get highlighted - // by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either - try { - if (highlights && highlights.length > 0) { - var highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink); - var safeHighlights = highlights.map(function(highlight) { - return sanitizeHtml(highlight, sanitizeHtmlParams); - }); - // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeHtmlParams structure. - sanitizeHtmlParams.textFilter = function(safeText) { - return highlighter.applyHighlights(safeText, safeHighlights).join(''); - }; - } - safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams); - } - finally { - delete sanitizeHtmlParams.textFilter; - } - return ; - } else { - safeBody = content.body; + // XXX: We sanitize the HTML whilst also highlighting its text nodes, to avoid accidentally trying + // to highlight HTML tags themselves. However, this does mean that we don't highlight textnodes which + // are interrupted by HTML tags (not that we did before) - e.g. foobar won't get highlighted + // by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either + try { if (highlights && highlights.length > 0) { - var highlighter = new TextHighlighter("mx_EventTile_searchHighlight", opts.highlightLink); - return highlighter.applyHighlights(safeBody, highlights); - } - else { - return safeBody; + var highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink); + var safeHighlights = highlights.map(function(highlight) { + return sanitizeHtml(highlight, sanitizeHtmlParams); + }); + // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeHtmlParams structure. + sanitizeHtmlParams.textFilter = function(safeText) { + return highlighter.applyHighlights(safeText, safeHighlights).join(''); + }; } + safeBody = sanitizeHtml(body, sanitizeHtmlParams); + safeBody = unicodeToImage(safeBody); } + finally { + delete sanitizeHtmlParams.textFilter; + } + return ; }, highlightDom: function(element) { From 4069886cbda08459fa5145fb2771cd77f7e95277 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Tue, 5 Jul 2016 04:04:57 +0530 Subject: [PATCH 046/222] feat: large emoji support --- src/HtmlUtils.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 629851f9e8..91bb063ee2 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -21,7 +21,10 @@ var sanitizeHtml = require('sanitize-html'); var highlight = require('highlight.js'); var linkifyMatrix = require('./linkify-matrix'); import escape from 'lodash/escape'; -import {unicodeToImage} from 'emojione'; +import {unicodeToImage, unicodeRegexp} from 'emojione'; +import classNames from 'classnames'; + +const EMOJI_REGEX = new RegExp(unicodeRegexp+"+", "gi"); var sanitizeHtmlParams = { allowedTags: [ @@ -211,7 +214,15 @@ module.exports = { finally { delete sanitizeHtmlParams.textFilter; } - return ; + + EMOJI_REGEX.lastIndex = 0; + let match = EMOJI_REGEX.exec(body); + let emojiBody = match && match[0] && match[0].length === body.length; + + let className = classNames('markdown-body', { + 'emoji-body': emojiBody, + }); + return ; }, highlightDom: function(element) { From 9c0dc7428920fab3fffac9d576b7cffa4388eb98 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Tue, 5 Jul 2016 09:58:28 +0530 Subject: [PATCH 047/222] feat: use svg emoji --- src/HtmlUtils.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 91bb063ee2..67123fe3dd 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -21,10 +21,10 @@ var sanitizeHtml = require('sanitize-html'); var highlight = require('highlight.js'); var linkifyMatrix = require('./linkify-matrix'); import escape from 'lodash/escape'; -import {unicodeToImage, unicodeRegexp} from 'emojione'; +import emojione from 'emojione'; import classNames from 'classnames'; -const EMOJI_REGEX = new RegExp(unicodeRegexp+"+", "gi"); +const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi"); var sanitizeHtmlParams = { allowedTags: [ @@ -209,7 +209,8 @@ module.exports = { }; } safeBody = sanitizeHtml(body, sanitizeHtmlParams); - safeBody = unicodeToImage(safeBody); + emojione.imageType = 'svg'; + safeBody = emojione.unicodeToImage(safeBody); } finally { delete sanitizeHtmlParams.textFilter; From 020f1f4320ff05fead13aca72d40d0e765c91c9b Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Tue, 5 Jul 2016 10:16:17 +0530 Subject: [PATCH 048/222] feat: emojify ALL THE THINGS! --- src/HtmlUtils.js | 8 +++++++- src/components/views/avatars/BaseAvatar.js | 7 ++++--- src/components/views/messages/TextualEvent.js | 7 ++++--- src/components/views/rooms/EntityTile.js | 7 ++++--- src/components/views/rooms/RoomHeader.js | 5 ++++- src/components/views/rooms/RoomTile.js | 8 ++++++-- 6 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 67123fe3dd..bfc7c5bfb8 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -233,5 +233,11 @@ module.exports = { } }, -} + emojifyText: function(text) { + emojione.imageType = 'svg'; + return { + __html: emojione.unicodeToImage(escape(text)), + }; + }, +}; diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index 121540a8c0..66f8e27b88 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -18,6 +18,7 @@ limitations under the License. var React = require('react'); var AvatarLogic = require("../../../Avatar"); +import {emojifyText} from '../../../HtmlUtils'; module.exports = React.createClass({ displayName: 'BaseAvatar', @@ -137,14 +138,14 @@ module.exports = React.createClass({ var imageUrl = this.state.imageUrls[this.state.urlsIndex]; if (imageUrl === this.state.defaultImageUrl) { - var initialLetter = this._getInitialLetter(this.props.name); + var initialLetter = emojifyText(this._getInitialLetter(this.props.name)); return ( - {TextForEvent.textForEvent(this.props.mxEvent)} +
); }, diff --git a/src/components/views/rooms/EntityTile.js b/src/components/views/rooms/EntityTile.js index acc424b098..91874ed45a 100644 --- a/src/components/views/rooms/EntityTile.js +++ b/src/components/views/rooms/EntityTile.js @@ -20,6 +20,7 @@ var React = require('react'); var MatrixClientPeg = require('../../../MatrixClientPeg'); var sdk = require('../../../index'); +import {emojifyText} from '../../../HtmlUtils'; var PRESENCE_CLASS = { @@ -82,6 +83,7 @@ module.exports = React.createClass({ var mainClassName = "mx_EntityTile "; mainClassName += presenceClass + (this.props.className ? (" " + this.props.className) : ""); var nameEl; + let nameHTML = emojifyText(this.props.name); if (this.state.hover && !this.props.suppressOnHover) { var activeAgo = this.props.presenceLastActiveAgo ? @@ -92,7 +94,7 @@ module.exports = React.createClass({ nameEl = (
-
{ this.props.name }
+
@@ -101,8 +103,7 @@ module.exports = React.createClass({ } else { nameEl = ( -
- { this.props.name } +
); } diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 448a46b84f..0bafc90fe5 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -24,6 +24,7 @@ var Modal = require("../../../Modal"); var linkify = require('linkifyjs'); var linkifyElement = require('linkifyjs/element'); var linkifyMatrix = require('../../../linkify-matrix'); +import {emojifyText} from '../../../HtmlUtils'; linkifyMatrix(linkify); @@ -211,9 +212,11 @@ module.exports = React.createClass({ roomName = this.props.room.name; } + let roomNameHTML = emojifyText(roomName); + name =
-
{ roomName }
+
{ searchStatus }
diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 55971cdd60..da9f97ab65 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -21,6 +21,7 @@ var classNames = require('classnames'); var dis = require("../../../dispatcher"); var MatrixClientPeg = require('../../../MatrixClientPeg'); var sdk = require('../../../index'); +import {emojifyText} from '../../../HtmlUtils'; module.exports = React.createClass({ displayName: 'RoomTile', @@ -104,10 +105,13 @@ module.exports = React.createClass({ var label; if (!this.props.collapsed) { var className = 'mx_RoomTile_name' + (this.props.isInvite ? ' mx_RoomTile_invite' : ''); + let nameHTML = emojifyText(name); if (this.props.selected) { - name = { name }; + name = ; + label =
{ name }
; + } else { + label =
; } - label =
{ name }
; } else if (this.state.hover) { var RoomTooltip = sdk.getComponent("rooms.RoomTooltip"); From 6a133bc034745209e84addc76f2cd52831ee4f29 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Tue, 5 Jul 2016 10:24:18 +0530 Subject: [PATCH 049/222] feat: and emojify name in MemberInfo --- src/components/views/rooms/MemberInfo.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index ddd0d1f6c6..0e14776e82 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -32,6 +32,7 @@ var Modal = require("../../../Modal"); var sdk = require('../../../index'); var UserSettingsStore = require('../../../UserSettingsStore'); var createRoom = require('../../../createRoom'); +import {emojifyText} from '../../../HtmlUtils'; module.exports = React.createClass({ displayName: 'MemberInfo', @@ -601,6 +602,8 @@ module.exports = React.createClass({
} + let memberNameHTML = emojifyText(this.props.member.name); + var MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); var PowerSelector = sdk.getComponent('elements.PowerSelector'); return ( @@ -610,7 +613,7 @@ module.exports = React.createClass({
-

{ this.props.member.name }

+

From a9a3d31b3fabe899fab57fdf9a732a691f71026e Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Tue, 5 Jul 2016 10:43:09 +0530 Subject: [PATCH 050/222] feat: improve emoji-body detection --- src/HtmlUtils.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index bfc7c5bfb8..4c296c95d9 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -217,8 +217,9 @@ module.exports = { } EMOJI_REGEX.lastIndex = 0; - let match = EMOJI_REGEX.exec(body); - let emojiBody = match && match[0] && match[0].length === body.length; + let contentBodyTrimmed = content.body.trim(); + let match = EMOJI_REGEX.exec(contentBodyTrimmed); + let emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length; let className = classNames('markdown-body', { 'emoji-body': emojiBody, From 545d59769e0f6fea9cd6c029cc313a599b128d16 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Tue, 5 Jul 2016 11:13:34 +0530 Subject: [PATCH 051/222] feat: unblacklist img tags with data URIs fixes vector-im/vector-web#1692 --- src/HtmlUtils.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index dbcb59a20a..e022cb2dbb 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -28,7 +28,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: @@ -42,7 +42,9 @@ var sanitizeHtmlParams = { selfClosing: [ 'img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta' ], // URL schemes we permit allowedSchemes: [ 'http', 'https', 'ftp', 'mailto' ], - allowedSchemesByTag: {}, + allowedSchemesByTag: { + img: [ 'data' ], + }, transformTags: { // custom to matrix // add blank targets to all hyperlinks except vector URLs From a2b64798f7504025472123cf5c347746fb1acd0e Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 5 Jul 2016 14:34:33 +0100 Subject: [PATCH 052/222] Fix timelinepanel test timeout The previous fix was adjusting the wrong timeout: it's hitting browserNoActivityTimeout, so revert previous fix & do this instead --- karma.conf.js | 1 + test/components/structures/TimelinePanel-test.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/karma.conf.js b/karma.conf.js index 1ae2494add..45f5dd9998 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -171,5 +171,6 @@ module.exports = function (config) { }, devtool: 'inline-source-map', }, + browserNoActivityTimeout: 15000, }); }; diff --git a/test/components/structures/TimelinePanel-test.js b/test/components/structures/TimelinePanel-test.js index de547b1779..045ccd70b7 100644 --- a/test/components/structures/TimelinePanel-test.js +++ b/test/components/structures/TimelinePanel-test.js @@ -210,7 +210,7 @@ describe('TimelinePanel', function() { var N_EVENTS = 600; // sadly, loading all those events takes a while - this.timeout(N_EVENTS * 40); + this.timeout(N_EVENTS * 20); // client.getRoom is called a /lot/ in this test, so replace // sinon's spy with a fast noop. From e2c473b366e9494d4096a16a52f96df91f9dcc3d Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 6 Jul 2016 15:22:06 +0100 Subject: [PATCH 053/222] Error on registration if email taken Use the new register-specific request token endpoint (https://github.com/matrix-org/matrix-js-sdk/pull/147) and catch the error that it gives if the email is already in use. Also add initial values to the registration form so we can reload it after the error without all the values disappearing, and split out the guest username parameter which was previously called defaultUsername. --- src/Signup.js | 5 +++- src/SignupStages.js | 6 ++--- .../structures/login/Registration.js | 14 ++++++++--- .../views/login/RegistrationForm.js | 24 +++++++++---------- 4 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/Signup.js b/src/Signup.js index 4518955d95..5aadd94701 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -152,7 +152,10 @@ class Register extends Signup { console.log("Active flow => %s", JSON.stringify(flow)); var flowStage = self.firstUncompletedStage(flow); if (flowStage != self.activeStage) { - return self.startStage(flowStage); + return self.startStage(flowStage).catch(function(err) { + self.setStep('START'); + throw err; + }); } } } diff --git a/src/SignupStages.js b/src/SignupStages.js index 1c5c48ddd6..2b0d163a08 100644 --- a/src/SignupStages.js +++ b/src/SignupStages.js @@ -170,7 +170,7 @@ class EmailIdentityStage extends Stage { encodeURIComponent(this.signupInstance.getServerData().session); var self = this; - return this.client.requestEmailToken( + return this.client.requestRegisterEmailToken( this.signupInstance.email, this.clientSecret, 1, // TODO: Multiple send attempts? @@ -186,8 +186,8 @@ class EmailIdentityStage extends Stage { var e = { isFatal: true }; - if (error.errcode == 'THREEPID_IN_USE') { - e.message = "Email in use"; + if (error.errcode == 'M_THREEPID_IN_USE') { + e.message = "This email address is already registered"; } else { e.message = 'Unable to contact the given identity server'; } diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 2f15a3b5df..4615031760 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -54,6 +54,9 @@ module.exports = React.createClass({ return { busy: false, errorText: null, + formVals: { + email: this.props.email, + }, }; }, @@ -108,7 +111,8 @@ module.exports = React.createClass({ var self = this; this.setState({ errorText: "", - busy: true + busy: true, + formVals: formVals, }); if (formVals.username !== this.props.username) { @@ -228,11 +232,15 @@ module.exports = React.createClass({ break; // NOP case "Register.START": case "Register.STEP_m.login.dummy": + // NB. Our 'username' prop is specifically for upgrading + // a guest account registerStep = ( diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index a172d77bb4..17827d5b46 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -37,6 +37,8 @@ module.exports = React.createClass({ propTypes: { defaultEmail: React.PropTypes.string, defaultUsername: React.PropTypes.string, + defaultPassword: React.PropTypes.string, + guestUsername: React.PropTypes.string, showEmail: React.PropTypes.bool, minPasswordLength: React.PropTypes.number, onError: React.PropTypes.func, @@ -55,10 +57,6 @@ module.exports = React.createClass({ getInitialState: function() { return { - email: this.props.defaultEmail, - username: null, - password: null, - passwordConfirm: null, fieldValid: {} }; }, @@ -103,7 +101,7 @@ module.exports = React.createClass({ _doSubmit: function() { var promise = this.props.onRegisterClick({ - username: this.refs.username.value.trim() || this.props.defaultUsername, + username: this.refs.username.value.trim() || this.props.guestUsername, password: this.refs.password.value.trim(), email: this.refs.email.value.trim() }); @@ -144,7 +142,7 @@ module.exports = React.createClass({ break; case FIELD_USERNAME: // XXX: SPEC-1 - var username = this.refs.username.value.trim() || this.props.defaultUsername; + var username = this.refs.username.value.trim() || this.props.guestUsername; if (encodeURIComponent(username) != username) { this.markFieldValid( field_id, @@ -225,7 +223,7 @@ module.exports = React.createClass({ emailSection = ( ); @@ -237,8 +235,8 @@ module.exports = React.createClass({ } var placeholderUserName = "User name"; - if (this.props.defaultUsername) { - placeholderUserName += " (default: " + this.props.defaultUsername + ")" + if (this.props.guestUsername) { + placeholderUserName += " (default: " + this.props.guestUsername + ")" } return ( @@ -247,23 +245,23 @@ module.exports = React.createClass({ {emailSection}

- { this.props.defaultUsername ? + { this.props.guestUsername ?
Setting a user name will create a fresh account
: null } + placeholder="Password" defaultValue={this.props.defaultPassword} />
+ defaultValue={this.props.defaultPassword} />
{registerButton} From 5c879d786e1e8b51ea24b6f8f3a6278d79717800 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 7 Jul 2016 11:23:08 +0100 Subject: [PATCH 054/222] Doc the default params / guestUsername props --- src/components/views/login/RegistrationForm.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index 17827d5b46..ad3526c598 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -35,10 +35,16 @@ module.exports = React.createClass({ displayName: 'RegistrationForm', propTypes: { + // Values pre-filled in the input boxes when the component loads defaultEmail: React.PropTypes.string, defaultUsername: React.PropTypes.string, defaultPassword: React.PropTypes.string, + + // A username that will be used if no username is enetered. + // Specifying this param will also warn the user that enetering + // a different username will cause a fresh account to be generated. guestUsername: React.PropTypes.string, + showEmail: React.PropTypes.bool, minPasswordLength: React.PropTypes.number, onError: React.PropTypes.func, From a6b04c462e26ff32d58dfdfdfc9b103636afe1fa Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 7 Jul 2016 11:26:35 +0100 Subject: [PATCH 055/222] Comment how we're remembering form vals --- src/components/structures/login/Registration.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 4615031760..5126965407 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -54,6 +54,13 @@ module.exports = React.createClass({ return { busy: false, errorText: null, + // We remember the values entered by the user because + // the registration form will be unmounted during the + // course of registration, but if there's an error we + // want to bring back the registration form with the + // values the user enetered still in it. We can keep + // them in this component's state since this component + // persist for the duration of the registration process. formVals: { email: this.props.email, }, From 04728ae03bc041a487bd22b8d943c8a983ff794b Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 7 Jul 2016 12:09:02 +0100 Subject: [PATCH 056/222] PR fixes + more general notes --- code_style.md | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/code_style.md b/code_style.md index eea23aed9c..4a42597b7a 100644 --- a/code_style.md +++ b/code_style.md @@ -6,6 +6,19 @@ consistent with other popular JavaScript styles and consistent with the rest of the Matrix codebase. For reference, the Matrix Python style guide can be found at https://github.com/matrix-org/synapse/blob/master/docs/code_style.rst +This document reflects how we would like Matrix JavaScript code to look, with +acknowledgement that a significant amount of code is written to older +standards. + +Write applications in modern ECMAScript and use a transpiler where necessary to +target older platforms. When writing library code, consider carefully whether +to write in ES5 to allow all JavaScript application to use the code directly or +writing in modern ECMAScript and using a transpile step to generate the file +that applications can then include. There are significant benefits in being +able to use modern ECMAScript, although the tooling for doing so can be awkward +for library code, especially with regard to translating source maps and line +number throgh from the original code to the final application. + General Style ------------- - 4 spaces to indent, for consistency with Matrix Python. @@ -23,10 +36,12 @@ General Style - lowerCamelCase for functions and variables. - Single line ternary operators are fine. - UPPER_CAMEL_CASE for constants -- Single quotes for strings, for consistency with most JavaScript styles:: +- Single quotes for strings by default, for consistency with most JavaScript styles: + ``` "bad" // Bad 'good' // Good -- Use parentheses instead of '\\' for line continuation where ever possible + ``` +- Use parentheses or `\`` instead of '\\' for line continuation where ever possible - Open braces on the same line (consistent with Node): ``` if (x) { @@ -82,7 +97,7 @@ General Style if (x) return true; // Not fine ``` -- Terminate all multi-line lists with commas: +- Terminate all multi-line lists with commas (if using a transpiler): ``` var mascots = [ "Patrick", @@ -103,6 +118,14 @@ General Style - Use `null`, `undefined` etc consistently with node: Boolean variables and functions should always be either true or false. Don't set it to 0 unless it's supposed to be a number. When something is intentionally missing or removed, set it to null. + If returning a boolean, type coerce: + ``` + function hasThings() { + return !!length; // bad + return new Boolean(length); // REALLY bad + return Boolean(length); // good + } + ``` Don't set things to undefined. Reserve that value to mean "not yet set to anything." Boolean objects are verboten. - Use JSDoc @@ -123,3 +146,12 @@ ECMAScript React ----- - Use ES6 classes, although bear in mind a lot of code uses createClass. +- Pull out functions in props to the class, generally as specific event handlers: + ``` + // Bad + {doStuff();}}> // Equally bad + // Better + // Best + ``` +- Think about whether your component really needs state: are you duplicating + information in component state that could be derived from the model? From 345ed04ba9e362d56600776b9bc4595a2ffe2354 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 7 Jul 2016 13:03:27 +0100 Subject: [PATCH 057/222] Less enetering --- src/components/structures/login/Registration.js | 2 +- src/components/views/login/RegistrationForm.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 5126965407..423d62933f 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -58,7 +58,7 @@ module.exports = React.createClass({ // the registration form will be unmounted during the // course of registration, but if there's an error we // want to bring back the registration form with the - // values the user enetered still in it. We can keep + // values the user entered still in it. We can keep // them in this component's state since this component // persist for the duration of the registration process. formVals: { diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index ad3526c598..39c1acc625 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -40,8 +40,8 @@ module.exports = React.createClass({ defaultUsername: React.PropTypes.string, defaultPassword: React.PropTypes.string, - // A username that will be used if no username is enetered. - // Specifying this param will also warn the user that enetering + // A username that will be used if no username is entered. + // Specifying this param will also warn the user that entering // a different username will cause a fresh account to be generated. guestUsername: React.PropTypes.string, From b33452216875ba3ffd09747158bd5e1a12512e96 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Fri, 8 Jul 2016 12:54:28 +0530 Subject: [PATCH 058/222] feat: code cleanup & emoji replacement in composer --- src/RichText.js | 112 +++++++++++++++--- .../views/rooms/MessageComposerInput.js | 71 +++++------ 2 files changed, 135 insertions(+), 48 deletions(-) diff --git a/src/RichText.js b/src/RichText.js index c24a510e05..a5bc554b95 100644 --- a/src/RichText.js +++ b/src/RichText.js @@ -1,6 +1,7 @@ import React from 'react'; import { Editor, + EditorState, Modifier, ContentState, ContentBlock, @@ -9,12 +10,13 @@ import { DefaultDraftInlineStyle, CompositeDecorator, SelectionState, + Entity, } from 'draft-js'; import * as sdk from './index'; import * as emojione from 'emojione'; const BLOCK_RENDER_MAP = DefaultDraftBlockRenderMap.set('unstyled', { - element: 'span' + element: 'span', /* draft uses
by default which we don't really like, so we're using this is probably not a good idea since is not a block level element but @@ -65,7 +67,7 @@ export function contentStateToHTML(contentState: ContentState): string { let result = `<${elem}>${content.join('')}`; // dirty hack because we don't want block level tags by default, but breaks - if(elem === 'span') + if (elem === 'span') result += '
'; return result; }).join(''); @@ -75,6 +77,48 @@ export function HTMLtoContentState(html: string): ContentState { return ContentState.createFromBlockArray(convertFromHTML(html)); } +function unicodeToEmojiUri(str) { + let replaceWith, unicode, alt; + if ((!emojione.unicodeAlt) || (emojione.sprites)) { + // if we are using the shortname as the alt tag then we need a reversed array to map unicode code point to shortnames + let mappedUnicode = emojione.mapUnicodeToShort(); + } + + str = str.replace(emojione.regUnicode, function(unicodeChar) { + if ( (typeof unicodeChar === 'undefined') || (unicodeChar === '') || (!(unicodeChar in emojione.jsEscapeMap)) ) { + // if the unicodeChar doesnt exist just return the entire match + return unicodeChar; + } else { + // get the unicode codepoint from the actual char + unicode = emojione.jsEscapeMap[unicodeChar]; + return emojione.imagePathSVG+unicode+'.svg'+emojione.cacheBustParam; + } + }); + + return str; +} + +// Unused for now, due to https://github.com/facebook/draft-js/issues/414 +let emojiDecorator = { + strategy: (contentBlock, callback) => { + findWithRegex(EMOJI_REGEX, contentBlock, callback); + }, + component: (props) => { + let uri = unicodeToEmojiUri(props.children[0].props.text); + let shortname = emojione.toShort(props.children[0].props.text); + let style = { + display: 'inline-block', + width: '1em', + maxHeight: '1em', + background: `url(${uri})`, + backgroundSize: 'contain', + backgroundPosition: 'center center', + overflow: 'hidden', + }; + return ({props.children}); + }, +}; + /** * Returns a composite decorator which has access to provided scope. */ @@ -90,7 +134,7 @@ export function getScopedRTDecorators(scope: any): CompositeDecorator { // unused until we make these decorators immutable (autocomplete needed) let name = member ? member.name : null; let avatar = member ? : null; - return {avatar} {props.children}; + return {avatar}{props.children}; } }; @@ -103,17 +147,7 @@ export function getScopedRTDecorators(scope: any): CompositeDecorator { } }; - // Unused for now, due to https://github.com/facebook/draft-js/issues/414 - let emojiDecorator = { - strategy: (contentBlock, callback) => { - findWithRegex(EMOJI_REGEX, contentBlock, callback); - }, - component: (props) => { - return - } - }; - - return [usernameDecorator, roomDecorator]; + return [usernameDecorator, roomDecorator, emojiDecorator]; } export function getScopedMDDecorators(scope: any): CompositeDecorator { @@ -139,6 +173,7 @@ export function getScopedMDDecorators(scope: any): CompositeDecorator { ) }); + markdownDecorators.push(emojiDecorator); return markdownDecorators; } @@ -193,7 +228,7 @@ export function modifyText(contentState: ContentState, rangeToReplace: Selection export function selectionStateToTextOffsets(selectionState: SelectionState, contentBlocks: Array): {start: number, end: number} { let offset = 0, start = 0, end = 0; - for(let block of contentBlocks) { + for (let block of contentBlocks) { if (selectionState.getStartKey() === block.getKey()) { start = offset + selectionState.getStartOffset(); } @@ -240,3 +275,50 @@ export function textOffsetsToSelectionState({start, end}: {start: number, end: n return selectionState; } + +// modified version of https://github.com/draft-js-plugins/draft-js-plugins/blob/master/draft-js-emoji-plugin/src/modifiers/attachImmutableEntitiesToEmojis.js +export function attachImmutableEntitiesToEmoji(editorState: EditorState): EditorState { + const contentState = editorState.getCurrentContent(); + const blocks = contentState.getBlockMap(); + let newContentState = contentState; + + blocks.forEach((block) => { + const plainText = block.getText(); + + const addEntityToEmoji = (start, end) => { + const existingEntityKey = block.getEntityAt(start); + if (existingEntityKey) { + // avoid manipulation in case the emoji already has an entity + const entity = Entity.get(existingEntityKey); + if (entity && entity.get('type') === 'emoji') { + return; + } + } + + const selection = SelectionState.createEmpty(block.getKey()) + .set('anchorOffset', start) + .set('focusOffset', end); + const emojiText = plainText.substring(start, end); + const entityKey = Entity.create('emoji', 'IMMUTABLE', { emojiUnicode: emojiText }); + newContentState = Modifier.replaceText( + newContentState, + selection, + emojiText, + null, + entityKey, + ); + }; + + findWithRegex(EMOJI_REGEX, block, addEntityToEmoji); + }); + + if (!newContentState.equals(contentState)) { + return EditorState.push( + editorState, + newContentState, + 'convert-to-immutable-emojis', + ); + } + + return editorState; +} diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 46abc20ed6..fea4e8fea0 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ import React from 'react'; - -var marked = require("marked"); +import type SyntheticKeyboardEvent from 'react/lib/SyntheticKeyboardEvent'; +import marked from 'marked'; marked.setOptions({ renderer: new marked.Renderer(), gfm: true, @@ -24,7 +24,7 @@ marked.setOptions({ pedantic: false, sanitize: true, smartLists: true, - smartypants: false + smartypants: false, }); import {Editor, EditorState, RichUtils, CompositeDecorator, @@ -33,14 +33,14 @@ import {Editor, EditorState, RichUtils, CompositeDecorator, import {stateToMarkdown} from 'draft-js-export-markdown'; -var MatrixClientPeg = require("../../../MatrixClientPeg"); -var SlashCommands = require("../../../SlashCommands"); -var Modal = require("../../../Modal"); -var MemberEntry = require("../../../TabCompleteEntries").MemberEntry; -var sdk = require('../../../index'); +import MatrixClientPeg from '../../../MatrixClientPeg'; +import type {MatrixClient} from 'matrix-js-sdk/lib/matrix'; +import SlashCommands from '../../../SlashCommands'; +import Modal from '../../../Modal'; +import sdk from '../../../index'; -var dis = require("../../../dispatcher"); -var KeyCode = require("../../../KeyCode"); +import dis from '../../../dispatcher'; +import KeyCode from '../../../KeyCode'; import * as RichText from '../../../RichText'; @@ -49,8 +49,8 @@ const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000; const KEY_M = 77; // FIXME Breaks markdown with multiple paragraphs, since it only strips first and last

-function mdownToHtml(mdown) { - var html = marked(mdown) || ""; +function mdownToHtml(mdown: string): string { + let html = marked(mdown) || ""; html = html.trim(); // strip start and end

tags else you get 'orrible spacing if (html.indexOf("

") === 0) { @@ -66,6 +66,17 @@ function mdownToHtml(mdown) { * The textInput part of the MessageComposer */ export default class MessageComposerInput extends React.Component { + static getKeyBinding(e: SyntheticKeyboardEvent): string { + // C-m => Toggles between rich text and markdown modes + if (e.keyCode === KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) { + return 'toggle-mode'; + } + + return getDefaultKeyBinding(e); + } + + client: MatrixClient; + constructor(props, context) { super(props, context); this.onAction = this.onAction.bind(this); @@ -79,7 +90,7 @@ export default class MessageComposerInput extends React.Component { this.onConfirmAutocompletion = this.onConfirmAutocompletion.bind(this); let isRichtextEnabled = window.localStorage.getItem('mx_editor_rte_enabled'); - if(isRichtextEnabled == null) { + if (isRichtextEnabled == null) { isRichtextEnabled = 'true'; } isRichtextEnabled = isRichtextEnabled === 'true'; @@ -95,15 +106,6 @@ export default class MessageComposerInput extends React.Component { this.client = MatrixClientPeg.get(); } - static getKeyBinding(e: SyntheticKeyboardEvent): string { - // C-m => Toggles between rich text and markdown modes - if (e.keyCode === KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) { - return 'toggle-mode'; - } - - return getDefaultKeyBinding(e); - } - /** * "Does the right thing" to create an EditorState, based on: * - whether we've got rich text mode enabled @@ -347,15 +349,16 @@ export default class MessageComposerInput extends React.Component { } setEditorState(editorState: EditorState) { + editorState = RichText.attachImmutableEntitiesToEmoji(editorState); this.setState({editorState}); - if(editorState.getCurrentContent().hasText()) { - this.onTypingActivity() + if (editorState.getCurrentContent().hasText()) { + this.onTypingActivity(); } else { this.onFinishedTyping(); } - if(this.props.onContentChanged) { + if (this.props.onContentChanged) { this.props.onContentChanged(editorState.getCurrentContent().getPlainText(), RichText.selectionStateToTextOffsets(editorState.getSelection(), editorState.getCurrentContent().getBlocksAsArray())); @@ -380,7 +383,7 @@ export default class MessageComposerInput extends React.Component { } handleKeyCommand(command: string): boolean { - if(command === 'toggle-mode') { + if (command === 'toggle-mode') { this.enableRichtext(!this.state.isRichtextEnabled); return true; } @@ -388,7 +391,7 @@ export default class MessageComposerInput extends React.Component { let newState: ?EditorState = null; // Draft handles rich text mode commands by default but we need to do it ourselves for Markdown. - if(!this.state.isRichtextEnabled) { + if (!this.state.isRichtextEnabled) { let contentState = this.state.editorState.getCurrentContent(), selection = this.state.editorState.getSelection(); @@ -396,10 +399,10 @@ export default class MessageComposerInput extends React.Component { bold: text => `**${text}**`, italic: text => `*${text}*`, underline: text => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug* - code: text => `\`${text}\`` + code: text => `\`${text}\``, }[command]; - if(modifyFn) { + if (modifyFn) { newState = EditorState.push( this.state.editorState, RichText.modifyText(contentState, selection, modifyFn), @@ -408,7 +411,7 @@ export default class MessageComposerInput extends React.Component { } } - if(newState == null) + if (newState == null) newState = RichUtils.handleKeyCommand(this.state.editorState, command); if (newState != null) { @@ -533,9 +536,11 @@ export default class MessageComposerInput extends React.Component { content ); - this.setState({ - editorState: EditorState.push(this.state.editorState, contentState, 'insert-characters'), - }); + let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters'); + + editorState = EditorState.forceSelection(editorState, contentState.getSelectionAfter()); + + this.setEditorState(editorState); // for some reason, doing this right away does not update the editor :( setTimeout(() => this.refs.editor.focus(), 50); From 6d403e792b343907dc8069514dddc0baecbf4f96 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 8 Jul 2016 15:29:59 +0100 Subject: [PATCH 059/222] Add --stage 1 to babel cmdline So we can use trailing function commas. The trailing comma proposal is actually at stage 3 now and there's a babel 6 plugin for it, which we should use when we switch back to babel 6. --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 13cabf32d9..cd81ad7c56 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,8 @@ }, "scripts": { "reskindex": "reskindex -h header", - "build": "babel src -d lib --source-maps", - "start": "babel src -w -d lib --source-maps", + "build": "babel src -d lib --source-maps --stage 1", + "start": "babel src -w -d lib --source-maps --stage 1", "lint": "eslint src/", "lintall": "eslint src/ test/", "clean": "rimraf lib", From afa6acc20a75705c4f27fa9a17aa22329fd01d88 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 8 Jul 2016 15:42:42 +0100 Subject: [PATCH 060/222] All the trailing commas --- code_style.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code_style.md b/code_style.md index 4a42597b7a..c46592f244 100644 --- a/code_style.md +++ b/code_style.md @@ -97,7 +97,7 @@ General Style if (x) return true; // Not fine ``` -- Terminate all multi-line lists with commas (if using a transpiler): +- Terminate all multi-line lists, object literals, imports and ideally function calls with commas (if using a transpiler). Note that trailing function commas require explicit configuration in babel at time of writing: ``` var mascots = [ "Patrick", From 1a3bc814e1cb81f2d1d32d73ad9ae11c14b94ccd Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 8 Jul 2016 15:58:18 +0100 Subject: [PATCH 061/222] clarify event handlers --- code_style.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code_style.md b/code_style.md index c46592f244..9078d8cdb2 100644 --- a/code_style.md +++ b/code_style.md @@ -151,7 +151,7 @@ React // Bad {doStuff();}}> // Equally bad // Better - // Best + // Best, if onFooClick would do anything other than directly calling doStuff ``` - Think about whether your component really needs state: are you duplicating information in component state that could be derived from the model? From 0fdc2d817c6c8640048baa3b4b9d6bd71b448e0a Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 8 Jul 2016 15:59:34 +0100 Subject: [PATCH 062/222] mark as jsx --- code_style.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code_style.md b/code_style.md index 9078d8cdb2..0ef16aedf5 100644 --- a/code_style.md +++ b/code_style.md @@ -147,7 +147,7 @@ React ----- - Use ES6 classes, although bear in mind a lot of code uses createClass. - Pull out functions in props to the class, generally as specific event handlers: - ``` + ```jsx // Bad {doStuff();}}> // Equally bad // Better From b78340ff51c40c6b7ec6de6ac9c9614897043c45 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 8 Jul 2016 17:28:04 +0100 Subject: [PATCH 063/222] Use HS proxy API for requestToken on adding email So we report an error if the email is already taken. Also fix a bug where the spinner wouldn't disappear if adding an email failed (and don't include the raw errcode in the user-facing dialog) --- src/AddThreepid.js | 6 ++++-- src/components/structures/UserSettings.js | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/AddThreepid.js b/src/AddThreepid.js index 31805aad11..c32eb3aab1 100644 --- a/src/AddThreepid.js +++ b/src/AddThreepid.js @@ -38,11 +38,13 @@ class AddThreepid { */ addEmailAddress(emailAddress, bind) { this.bind = bind; - return MatrixClientPeg.get().requestEmailToken(emailAddress, this.clientSecret, 1).then((res) => { + return MatrixClientPeg.get().request3pidAddEmailToken(emailAddress, this.clientSecret, 1).then((res) => { this.sessionId = res.sid; return res; }, function(err) { - if (err.httpStatus) { + if (err.errcode == 'M_THREEPID_IN_USE') { + err.message = "This email address is already in use"; + } else if (err.httpStatus) { err.message = err.message + ` (Status ${err.httpStatus})`; } throw err; diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 7fcb81a60c..c54a10c7c0 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -214,9 +214,10 @@ module.exports = React.createClass({ onFinished: this.onEmailDialogFinished, }); }, (err) => { + this.setState({email_add_pending: false}); Modal.createDialog(ErrorDialog, { title: "Unable to add email address", - description: err.toString() + description: err.message }); }); ReactDOM.findDOMNode(this.refs.add_threepid_input).blur(); From 8e8e54a3bfec44b837c86ce4d91d73c8932ed533 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 8 Jul 2016 17:53:06 +0100 Subject: [PATCH 064/222] Update function name as per js-sdk PR feedback --- src/AddThreepid.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AddThreepid.js b/src/AddThreepid.js index c32eb3aab1..5593d46ff7 100644 --- a/src/AddThreepid.js +++ b/src/AddThreepid.js @@ -38,7 +38,7 @@ class AddThreepid { */ addEmailAddress(emailAddress, bind) { this.bind = bind; - return MatrixClientPeg.get().request3pidAddEmailToken(emailAddress, this.clientSecret, 1).then((res) => { + return MatrixClientPeg.get().requestAdd3pidEmailToken(emailAddress, this.clientSecret, 1).then((res) => { this.sessionId = res.sid; return res; }, function(err) { From 30cfc6b605f2a09ad344c438d86dc99e2926857c Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 8 Jul 2016 18:06:50 +0100 Subject: [PATCH 065/222] Error if email already in use when resetting pw Use password reset specific requestToken --- src/PasswordReset.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/PasswordReset.js b/src/PasswordReset.js index bbafa0ef33..a03a565459 100644 --- a/src/PasswordReset.js +++ b/src/PasswordReset.js @@ -48,11 +48,13 @@ class PasswordReset { */ resetPassword(emailAddress, newPassword) { this.password = newPassword; - return this.client.requestEmailToken(emailAddress, this.clientSecret, 1).then((res) => { + return this.client.requestPasswordEmailToken(emailAddress, this.clientSecret, 1).then((res) => { this.sessionId = res.sid; return res; }, function(err) { - if (err.httpStatus) { + if (err.errcode == 'M_THREEPID_NOT_FOUND') { + err.message = "This email address was not found"; + } else if (err.httpStatus) { err.message = err.message + ` (Status ${err.httpStatus})`; } throw err; From ffbe045fcc645bda4fcc0c0292c973774ef1ac1a Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 11 Jul 2016 10:10:51 +0100 Subject: [PATCH 066/222] Change to const-by-default --- code_style.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/code_style.md b/code_style.md index 0ef16aedf5..6ec2597aa2 100644 --- a/code_style.md +++ b/code_style.md @@ -132,9 +132,7 @@ General Style ECMAScript ---------- -- Use `let` for variables and `const` for constants. This sounds obvious, but it isn't: the ES6 `const` keyword - could be used for assign-once variables, however this guide advises against doing so on the grounds that it - confuses them with constants. +- Use `const` unless you need a re-assignable variable. This ensures things you don't want to be re-assigned can't be. - Be careful migrating files to newer syntax. - Don't mix `require` and `import` in the same file. Either stick to the old style or change them all. - Likewise, don't mix things like class properties and `MyClass.prototype.MY_CONSTANT = 42;` From 8d1d37c103bb4dd72eff856ad9cdac5824ca095c Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 11 Jul 2016 16:19:14 +0100 Subject: [PATCH 067/222] Try newline to fix code style formatting --- code_style.md | 1 + 1 file changed, 1 insertion(+) diff --git a/code_style.md b/code_style.md index 6ec2597aa2..70ae36faa7 100644 --- a/code_style.md +++ b/code_style.md @@ -37,6 +37,7 @@ General Style - Single line ternary operators are fine. - UPPER_CAMEL_CASE for constants - Single quotes for strings by default, for consistency with most JavaScript styles: + ``` "bad" // Bad 'good' // Good From a28a335df426dff470e833d5be6cdfbe5851fb76 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 11 Jul 2016 16:21:21 +0100 Subject: [PATCH 068/222] More newlines for formatting --- code_style.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/code_style.md b/code_style.md index 70ae36faa7..6466b512b4 100644 --- a/code_style.md +++ b/code_style.md @@ -44,6 +44,7 @@ General Style ``` - Use parentheses or `\`` instead of '\\' for line continuation where ever possible - Open braces on the same line (consistent with Node): + ``` if (x) { console.log("I am a fish"); // Good @@ -55,6 +56,7 @@ General Style } ``` - Spaces after `if`, `for`, `else` etc, no space around the condition: + ``` if (x) { console.log("I am a fish"); // Good @@ -71,6 +73,7 @@ General Style - Declare one variable per var statement (consistent with Node). Unless they are simple and closely related. If you put the next declaration on a new line, treat yourself to another `var`: + ``` var key = "foo", comparator = function(x, y) { @@ -88,6 +91,7 @@ General Style var y = 0; // Also fine ``` - A single line `if` is fine, all others have braces. This prevents errors when adding to the code.: + ``` if (x) return true; // Fine @@ -99,6 +103,7 @@ General Style return true; // Not fine ``` - Terminate all multi-line lists, object literals, imports and ideally function calls with commas (if using a transpiler). Note that trailing function commas require explicit configuration in babel at time of writing: + ``` var mascots = [ "Patrick", @@ -120,6 +125,7 @@ General Style Boolean variables and functions should always be either true or false. Don't set it to 0 unless it's supposed to be a number. When something is intentionally missing or removed, set it to null. If returning a boolean, type coerce: + ``` function hasThings() { return !!length; // bad @@ -146,6 +152,7 @@ React ----- - Use ES6 classes, although bear in mind a lot of code uses createClass. - Pull out functions in props to the class, generally as specific event handlers: + ```jsx // Bad {doStuff();}}> // Equally bad From 513492e1bf83fc6cf551bc747294c41e0cc11442 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 11 Jul 2016 16:27:17 +0100 Subject: [PATCH 069/222] Change line length as per PR discussion --- code_style.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/code_style.md b/code_style.md index 6466b512b4..15404e1599 100644 --- a/code_style.md +++ b/code_style.md @@ -22,9 +22,8 @@ number throgh from the original code to the final application. General Style ------------- - 4 spaces to indent, for consistency with Matrix Python. -- Max line width: 79 chars (with flexibility to overflow by a "few chars" if - the overflowing content is not semantically significant and avoids an - explosion of vertical whitespace). +- 120 columns per line, but try to keep JavaScript code around the 80 column mark. + Inline JSX in particular can be nicer with more columns per line. - No trailing whitespace at end of lines. - Don't indent empty lines. - One newline at the end of the file. From e00f3d9334e6216685fff74a7e702d58dffec3ea Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 11 Jul 2016 16:28:41 +0100 Subject: [PATCH 070/222] Mark code blocks as javascript --- code_style.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/code_style.md b/code_style.md index 15404e1599..7b272e0656 100644 --- a/code_style.md +++ b/code_style.md @@ -37,14 +37,14 @@ General Style - UPPER_CAMEL_CASE for constants - Single quotes for strings by default, for consistency with most JavaScript styles: - ``` + ```javascript "bad" // Bad 'good' // Good ``` - Use parentheses or `\`` instead of '\\' for line continuation where ever possible - Open braces on the same line (consistent with Node): - ``` + ```javascript if (x) { console.log("I am a fish"); // Good } @@ -56,7 +56,7 @@ General Style ``` - Spaces after `if`, `for`, `else` etc, no space around the condition: - ``` + ```javascript if (x) { console.log("I am a fish"); // Good } @@ -73,7 +73,7 @@ General Style are simple and closely related. If you put the next declaration on a new line, treat yourself to another `var`: - ``` + ```javascript var key = "foo", comparator = function(x, y) { return x - y; @@ -91,7 +91,7 @@ General Style ``` - A single line `if` is fine, all others have braces. This prevents errors when adding to the code.: - ``` + ```javascript if (x) return true; // Fine if (x) { @@ -103,7 +103,7 @@ General Style ``` - Terminate all multi-line lists, object literals, imports and ideally function calls with commas (if using a transpiler). Note that trailing function commas require explicit configuration in babel at time of writing: - ``` + ```javascript var mascots = [ "Patrick", "Shirley", @@ -125,7 +125,7 @@ General Style When something is intentionally missing or removed, set it to null. If returning a boolean, type coerce: - ``` + ```javascript function hasThings() { return !!length; // bad return new Boolean(length); // REALLY bad From a8677b52adb6e30dfc3eb35d48e9eb758425b32e Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 11 Jul 2016 18:26:16 +0100 Subject: [PATCH 071/222] major update to dev guidelines --- README.md | 180 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 113 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index ae1cd17c9a..dfc1a6e6ec 100644 --- a/README.md +++ b/README.md @@ -3,65 +3,85 @@ matrix-react-sdk This is a react-based SDK for inserting a Matrix chat/voip client into a web page. -This package provides the logic and 'controller' parts for the UI components. This -forms one part of a complete matrix client, but it not useable in isolation. It -must be used from a 'skin'. A skin provides: - * The HTML for the UI components (in the form of React `render` methods) - * The CSS for this HTML - * The containing application +This package provides the React components needed to build a Matrix web client +using React. It is not useable in isolation, and instead must must be used from +a 'skin'. A skin provides: + * Customised implementations of presentation components. + * Custom CSS + * The containing application * Zero or more 'modules' containing non-UI functionality -Skins are modules are exported from such a package in the `lib` directory. -`lib/skins` contains one directory per-skin, named after the skin, and the -`modules` directory contains modules as their javascript files. +**WARNING: As of July 2016, the skinning abstraction is broken due to rapid +development of `matrix-react-sdk` to meet the needs of Vector, the first app +to be built on top of the SDK** (https://github.com/vector-im/vector-web). +Right now `matrix-react-sdk` depends on some functionality from `vector-web` +(e.g. CSS), and `matrix-react-sdk` contains some Vector specific behaviour +(grep for 'vector'). This layering will be fixed asap once Vector development +has stabilised, but for now we do not advise trying to create new skins for +matrix-react-sdk until the layers are clearly separated again. -A basic skin is provided in the matrix-react-skin package. This also contains -a minimal application that instantiates the basic skin making a working matrix -client. +In the interim, `vector-im/vector-web` and `matrix-org/matrix-react-sdk` should +be considered as a single project (for instance, matrix-react-sdk bugs +are currently filed against vector-im/vector-web rather than this project). -You can use matrix-react-sdk directly, but to do this you would have to provide -'views' for each UI component. To get started quickly, use matrix-react-skin. +Developer Guide +=============== -How to customise the SDK -======================== +Platform Targets: + * Chrome, Firefox and Safari. + * Edge should also work, but we're not testing it proactively. + * WebRTC features (VoIP and Video calling) are only available in Chrome & Firefox. + * Mobile Web is not currently a target platform - instead please use the native + iOS (https://github.com/matrix-org/matrix-ios-kit) and Android + (https://github.com/matrix-org/matrix-android-sdk) SDKs. -The SDK formerly used the 'atomic' design pattern as seen at http://patternlab.io to -encourage a very modular and reusable architecture, making it easy to -customise and use UI widgets independently of the rest of the SDK and your app. +All code lands on the `develop` branch - `master` is only used for stable releases. +**Please file PRs against `develop`!!** -So unfortunately at the moment this document does not describe how to customize your UI! +Please follow the standard Matrix contributor's guide: +https://github.com/matrix-org/synapse/tree/master/CONTRIBUTING.rst -###This is the old description for the atomic design pattern: +Please follow the Matrix JS/React code style as per: +https://github.com/matrix-org/matrix-react-sdk/tree/master/code_style.rst -In practice this means: +Whilst the layering separation between matrix-react-sdk and Vector is broken +(as of July 2016), code should be committed as follows: + * All new components: https://github.com/matrix-org/matrix-react-sdk/tree/master/src/components + * Vector-specific components: https://github.com/vector-im/vector-web/tree/master/src/components + * In practice, `matrix-react-sdk` is still evolving so fast that the maintenance + burden of customising and overriding these components for Vector can seriously + impede development. So right now, there should be very few (if any) customisations for Vector. + * CSS for Matrix SDK components: https://github.com/vector-im/vector-web/tree/master/src/skins/vector/css/matrix-react-sdk + * CSS for Vector-specific overrides and components: https://github.com/vector-im/vector-web/tree/master/src/skins/vector/css/vector-web - * The UI of the app is strictly split up into a hierarchy of components. - - * Each component has its own: - * View object defined as a React javascript class containing embedded - HTML expressed in React's JSX notation. - * CSS file, which defines the styling specific to that component. - - * Components are loosely grouped into the 5 levels outlined by atomic design: - * atoms: fundamental building blocks (e.g. a timestamp tag) - * molecules: "group of atoms which functions together as a unit" - (e.g. a message in a chat timeline) - * organisms: "groups of molecules (and atoms) which form a distinct section - of a UI" (e.g. a view of a chat room) - * templates: "a reusable configuration of organisms" - used to combine and - style organisms into a well-defined global look and feel - * pages: specific instances of templates. +React components in matrix-react-sdk are come in two different flavours: +'structures' and 'views'. Structures are stateful components which handle the +more complicated business logic of the app, delegating their actual presentation +rendering to stateless 'view' components. For instance, the RoomView component +that orchestrates the act of visualising the contents of a given Matrix chat room +tracks lots of state for its child components which it passes into them for visual +rendering via props. - Good separation between the components is maintained by adopting various best - practices that anyone working with the SDK needs to be be aware of and uphold: +Good separation between the components is maintained by adopting various best +practices that anyone working with the SDK needs to be be aware of and uphold: - * Views are named with upper camel case (e.g. molecules/MessageTile.js) + * Components are named with upper camel case (e.g. views/rooms/EventTile.js) - * The view's CSS file MUST have the same name (e.g. molecules/MessageTile.css) + * They are organised in a typically two-level hierarchy - first whether the + component is a view or a structure, and then a broad functional grouping + (e.g. 'rooms' here) + + * After creating a new component you must run `npm run reskindex` to regenerate + the `component-index.js` for the SDK (used in future for skinning) + + * The view's CSS file MUST have the same name (e.g. view/rooms/MessageTile.css). + CSS for matrix-react-sdk currently resides in + https://github.com/vector-im/vector-web/tree/master/src/skins/vector/css/matrix-react-sdk. * Per-view CSS is optional - it could choose to inherit all its styling from - the context of the rest of the app, although this is unusual for any but - the simplest atoms and molecules. + the context of the rest of the app, although this is unusual for any but + structural components (lacking presentation logic) and the simplest view + components. * The view MUST *only* refer to the CSS rules defined in its own CSS file. 'Stealing' styling information from other components (including parents) @@ -82,9 +102,10 @@ In practice this means: * We deliberately use vanilla CSS 3.0 to avoid adding any more magic dependencies into the mix than we already have. App developers are welcome - to use whatever floats their boat however. + to use whatever floats their boat however. In future we'll start using + css-next to pull in features like CSS variable support. - * The CSS for a component can however override the rules for child components. + * The CSS for a component can override the rules for child components. For instance, .mx_RoomList .mx_RoomTile {} would be the selector to override styles of RoomTiles when viewed in the context of a RoomList view. Overrides *must* be scoped to the View's CSS class - i.e. don't just define @@ -98,30 +119,36 @@ In practice this means: generally not cool and stop the component from being reused easily in different places. - * We don't use the atomify library itself, as React already provides most - of the modularity requirements it brings to the table. +Originally `matrix-react-sdk` followed the Atomic design pattern as per +http://patternlab.io to try to encourage a modular architecture. However, we +found that the grouping of components into atoms/molecules/organisms +made them harder to find relative to a functional split, and didn't emphasise +the distinction between 'structural' and 'view' components, so we backed away +from it. -With all this in mind, here's how you go about skinning the react SDK UI -components to embed a Matrix client into your app: +Github Issues +============= - * Create a new NPM project. Be sure to directly depend on react, (otherwise - you can end up with two copies of react). - * Create an index.js file that sets up react. Add require statements for - React and matrix-react-sdk. Load a skin using the 'loadSkin' method on the - SDK and call Render. This can be a skin provided by a separate package or - a skin in the same package. - * Add a way to build your project: we suggest copying the scripts block - from matrix-react-skin (which uses babel and webpack). You could use - different tools but remember that at least the skins and modules of - your project should end up in plain (ie. non ES6, non JSX) javascript in - the lib directory at the end of the build process, as well as any - packaging that you might do. - * Create an index.html file pulling in your compiled javascript and the - CSS bundle from the skin you use. For now, you'll also need to manually - import CSS from any skins that your skin inherts from. +All issues should be filed under https://github.com/vector-im/vector-web/issues +for now. + +OUTDATED: To Create Your Own Skin +================================= + +**This is ALL LIES currently, as skinning is currently broken - see the WARNING +section at the top of this readme.** + +Skins are modules are exported from such a package in the `lib` directory. +`lib/skins` contains one directory per-skin, named after the skin, and the +`modules` directory contains modules as their javascript files. + +A basic skin is provided in the matrix-react-skin package. This also contains +a minimal application that instantiates the basic skin making a working matrix +client. + +You can use matrix-react-sdk directly, but to do this you would have to provide +'views' for each UI component. To get started quickly, use matrix-react-skin. -To Create Your Own Skin -======================= To actually change the look of a skin, you can create a base skin (which does not use views from any other skin) or you can make a derived skin. Note that derived skins are currently experimental: for example, the CSS @@ -145,3 +172,22 @@ Now you have the basis of a skin, you need to generate a skindex.json file. The you add an npm script to run this, as in matrix-react-skin. For more specific detail on any of these steps, look at matrix-react-skin. + +Alternative instructions: + + * Create a new NPM project. Be sure to directly depend on react, (otherwise + you can end up with two copies of react). + * Create an index.js file that sets up react. Add require statements for + React and matrix-react-sdk. Load a skin using the 'loadSkin' method on the + SDK and call Render. This can be a skin provided by a separate package or + a skin in the same package. + * Add a way to build your project: we suggest copying the scripts block + from matrix-react-skin (which uses babel and webpack). You could use + different tools but remember that at least the skins and modules of + your project should end up in plain (ie. non ES6, non JSX) javascript in + the lib directory at the end of the build process, as well as any + packaging that you might do. + * Create an index.html file pulling in your compiled javascript and the + CSS bundle from the skin you use. For now, you'll also need to manually + import CSS from any skins that your skin inherts from. + From ac58520b3bce6dc32f16b53204f61e6a811b1fef Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 12 Jul 2016 18:30:37 +0100 Subject: [PATCH 072/222] Update tab completion list when we have a room Fixes https://github.com/vector-im/vector-web/issues/1737 --- src/components/structures/RoomView.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index dc4b21a300..cfd359ea01 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -204,6 +204,9 @@ module.exports = React.createClass({ user_is_in_room = this.state.room.hasMembershipState( MatrixClientPeg.get().credentials.userId, 'join' ); + + // update the tab complete list now we have a room + this._updateTabCompleteList(); } if (!user_is_in_room && this.state.roomId) { From 6b03f72474be475ff524531a176c15857c09ecb4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 13 Jul 2016 11:56:01 +0100 Subject: [PATCH 073/222] Fix filtering user list by ID Fixes https://github.com/vector-im/vector-web/issues/1739 --- src/components/views/rooms/MemberList.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index 20f60c80a8..e87d31cdb3 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -442,9 +442,16 @@ module.exports = React.createClass({ var memberList = self.state.members.filter(function(userId) { var m = self.memberDict[userId]; - if (query && m.name.toLowerCase().indexOf(query) === -1) { - return false; + + if (query) { + const matchesName = m.name.toLowerCase().indexOf(query) !== -1; + const matchesId = m.userId.toLowerCase().indexOf(query) !== -1; + + if (!matchesName && !matchesId) { + return false; + } } + return m.membership == membership; }).map(function(userId) { var m = self.memberDict[userId]; From 936a38a9f49672c42a6095055696cf4cce00100f Mon Sep 17 00:00:00 2001 From: wmwragg Date: Wed, 13 Jul 2016 16:02:18 +0100 Subject: [PATCH 074/222] Updated and moved the room header settings icon --- src/components/views/rooms/RoomHeader.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 0bafc90fe5..9effc8fdde 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -218,9 +218,6 @@ module.exports = React.createClass({

{ searchStatus } -
- -
} @@ -266,6 +263,14 @@ module.exports = React.createClass({ ); } + var settings_button; + if (this.props.onSettingsClick) { + settings_button = +
+ +
; + } + var leave_button; if (this.props.onLeaveClick) { leave_button = @@ -291,6 +296,7 @@ module.exports = React.createClass({ if (!this.props.editing) { right_row =
+ { settings_button } { forget_button } { leave_button }
From 7f00053f847ee3922d00317bd5c6e4c36ec3134a Mon Sep 17 00:00:00 2001 From: wmwragg Date: Wed, 13 Jul 2016 17:46:56 +0100 Subject: [PATCH 075/222] Footer upload icon updated to new vesrion --- src/components/views/rooms/MessageComposer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index f9371385d4..da5b504d88 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -189,7 +189,7 @@ export default class MessageComposer extends React.Component { var uploadButton = (
- + Date: Thu, 14 Jul 2016 10:05:40 +0100 Subject: [PATCH 076/222] Listen for the new lastPreseceTs event This will catch all presence events as previously we were only listening for the actual presence string changing --- src/components/views/rooms/MemberList.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index e87d31cdb3..1a3f710063 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -64,7 +64,10 @@ module.exports = React.createClass({ cli.on("RoomMember.name", this.onRoomMemberName); cli.on("RoomState.events", this.onRoomStateEvent); cli.on("Room", this.onRoom); // invites - cli.on("User.presence", this.onUserPresence); + // We listen for changes to the lastPresenceTs which is essentially + // listening for all presence events (we display most of not all of + // the information contained in presence events). + cli.on("User.lastPresenceTs", this.onUserLastPresenceTs); // cli.on("Room.timeline", this.onRoomTimeline); }, @@ -75,7 +78,7 @@ module.exports = React.createClass({ cli.removeListener("RoomMember.name", this.onRoomMemberName); cli.removeListener("RoomState.events", this.onRoomStateEvent); cli.removeListener("Room", this.onRoom); - cli.removeListener("User.presence", this.onUserPresence); + cli.removeListener("User.lastPresenceTs", this.onUserLastPresenceTs); // cli.removeListener("Room.timeline", this.onRoomTimeline); } }, @@ -121,7 +124,7 @@ module.exports = React.createClass({ }, */ - onUserPresence(event, user) { + onUserLastPresenceTs(event, user) { // Attach a SINGLE listener for global presence changes then locate the // member tile and re-render it. This is more efficient than every tile // evar attaching their own listener. From 20210e71040906623648dc8e314708eacfe032c4 Mon Sep 17 00:00:00 2001 From: Kegsay Date: Thu, 14 Jul 2016 10:38:24 +0100 Subject: [PATCH 077/222] Log scrollTop and scrollHeight to try to debug flakey test Sometimes it fails because awaitScroll() on :277 isn't resolving because onScroll isn't firing. We need to know if this is because we aren't changing scrollTop --- test/components/structures/TimelinePanel-test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/components/structures/TimelinePanel-test.js b/test/components/structures/TimelinePanel-test.js index 045ccd70b7..d1c13ff8b8 100644 --- a/test/components/structures/TimelinePanel-test.js +++ b/test/components/structures/TimelinePanel-test.js @@ -271,6 +271,8 @@ describe('TimelinePanel', function() { // we should now be able to scroll down, and paginate in the other // direction. + console.log("scrollingDiv.scrollTop is " + scrollingDiv.scrollTop); + console.log("Going to set it to " + scrollingDiv.scrollHeight); scrollingDiv.scrollTop = scrollingDiv.scrollHeight; return awaitScroll(); }).then(() => { From a5272542ef43d218ab7b5b1b1b7274dc1b79f43b Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jul 2016 10:39:15 +0100 Subject: [PATCH 078/222] Lengthen timelinepanel test timeout again It's now hitting this timeout on the jenkins box instead of the browser disconnect timeout --- test/components/structures/TimelinePanel-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/components/structures/TimelinePanel-test.js b/test/components/structures/TimelinePanel-test.js index d1c13ff8b8..cd9d86cd64 100644 --- a/test/components/structures/TimelinePanel-test.js +++ b/test/components/structures/TimelinePanel-test.js @@ -210,7 +210,7 @@ describe('TimelinePanel', function() { var N_EVENTS = 600; // sadly, loading all those events takes a while - this.timeout(N_EVENTS * 20); + this.timeout(N_EVENTS * 30); // client.getRoom is called a /lot/ in this test, so replace // sinon's spy with a fast noop. From 0dde891d4cc941aa16f8a8f61954e2e0f4e5837b Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jul 2016 11:25:45 +0100 Subject: [PATCH 079/222] Order tab complete by most recently spoke Fixes https://github.com/vector-im/vector-web/issues/1741 --- src/TabCompleteEntries.js | 20 +++++++++++++++++++- src/components/structures/RoomView.js | 2 +- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/TabCompleteEntries.js b/src/TabCompleteEntries.js index a23050063f..488aaa57a6 100644 --- a/src/TabCompleteEntries.js +++ b/src/TabCompleteEntries.js @@ -113,8 +113,26 @@ class MemberEntry extends Entry { } } -MemberEntry.fromMemberList = function(members) { +MemberEntry.fromMemberList = function(room, members) { + // build up a dict of when, in the history we have cached, + // each member last spoke + const lastSpoke = {}; + const timelineEvents = room.getLiveTimeline().getEvents(); + for (var i = timelineEvents.length - 1; i >= 0; --i) { + const ev = timelineEvents[i]; + lastSpoke[ev.sender.userId] = ev.getTs(); + } + return members.sort(function(a, b) { + const lastSpokeA = lastSpoke[a.userId] || 0; + const lastSpokeB = lastSpoke[b.userId] || 0; + + if (lastSpokeA != lastSpokeB) { + // B - A here because the highest value + // is most recent + return lastSpokeB - lastSpokeA; + } + var userA = a.user; var userB = b.user; if (userA && !userB) { diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index cfd359ea01..64a29f9ffc 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -533,7 +533,7 @@ module.exports = React.createClass({ UserProvider.getInstance().setUserList(members); this.tabComplete.setCompletionList( - MemberEntry.fromMemberList(members).concat( + MemberEntry.fromMemberList(this.state.room, members).concat( CommandEntry.fromCommands(SlashCommands.getCommandList()) ) ); From 2ce521fe387428f105013da133c3bf57d291e03c Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jul 2016 11:40:17 +0100 Subject: [PATCH 080/222] Fix null error in TabComplete .sende ris sometimes null: use getSender() which isn't and returns the userId which is what we actually want --- src/TabCompleteEntries.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TabCompleteEntries.js b/src/TabCompleteEntries.js index 488aaa57a6..3cfe07c7c0 100644 --- a/src/TabCompleteEntries.js +++ b/src/TabCompleteEntries.js @@ -120,7 +120,7 @@ MemberEntry.fromMemberList = function(room, members) { const timelineEvents = room.getLiveTimeline().getEvents(); for (var i = timelineEvents.length - 1; i >= 0; --i) { const ev = timelineEvents[i]; - lastSpoke[ev.sender.userId] = ev.getTs(); + lastSpoke[ev.getSender()] = ev.getTs(); } return members.sort(function(a, b) { From f6302fc9905d31520f4ffcf90852d6036bcc0922 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Thu, 14 Jul 2016 11:43:33 +0100 Subject: [PATCH 081/222] Updated the search button, and simplified the leave css --- src/components/views/rooms/RoomHeader.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 9effc8fdde..4298fdb019 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -274,7 +274,7 @@ module.exports = React.createClass({ var leave_button; if (this.props.onLeaveClick) { leave_button = -
+
; } @@ -282,7 +282,7 @@ module.exports = React.createClass({ var forget_button; if (this.props.onForgetClick) { forget_button = -
+
; } @@ -300,7 +300,7 @@ module.exports = React.createClass({ { forget_button } { leave_button }
- +
{ rightPanel_buttons }
; From fd5d28768a0b2447d0aa6c4e9014a8288df6d82a Mon Sep 17 00:00:00 2001 From: wmwragg Date: Thu, 14 Jul 2016 12:05:00 +0100 Subject: [PATCH 082/222] Simplification of the css --- src/components/views/rooms/RoomHeader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 4298fdb019..8c1b2aaff8 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -266,7 +266,7 @@ module.exports = React.createClass({ var settings_button; if (this.props.onSettingsClick) { settings_button = -
+
; } From f1d72296b76e87d28753281ef6d1a8d481ef8a4c Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jul 2016 14:06:31 +0100 Subject: [PATCH 083/222] Fix last-spoke order Turns out this timeline is the other way around, so loop through the other way --- src/TabCompleteEntries.js | 3 +-- src/components/structures/RoomView.js | 4 ++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/TabCompleteEntries.js b/src/TabCompleteEntries.js index 3cfe07c7c0..419b3d7942 100644 --- a/src/TabCompleteEntries.js +++ b/src/TabCompleteEntries.js @@ -118,8 +118,7 @@ MemberEntry.fromMemberList = function(room, members) { // each member last spoke const lastSpoke = {}; const timelineEvents = room.getLiveTimeline().getEvents(); - for (var i = timelineEvents.length - 1; i >= 0; --i) { - const ev = timelineEvents[i]; + for (const ev of room.getLiveTimeline().getEvents()) { lastSpoke[ev.getSender()] = ev.getTs(); } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 64a29f9ffc..71edbf162d 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -360,6 +360,10 @@ module.exports = React.createClass({ }); } } + + // update ther tab complete list as it depends on who most recently spoke, + // and that has probably just changed + this._updateTabCompleteList(); }, // called when state.room is first initialised (either at initial load, From cad057ef348d7029c075644a9dd2a3d611e42788 Mon Sep 17 00:00:00 2001 From: Stefan Pearson Date: Thu, 14 Jul 2016 17:12:17 +0100 Subject: [PATCH 084/222] amends react template and removes opening image in lightbox --- .../views/rooms/LinkPreviewWidget.js | 46 +++++-------------- 1 file changed, 12 insertions(+), 34 deletions(-) diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.js index ba438c1d12..3398b021fb 100644 --- a/src/components/views/rooms/LinkPreviewWidget.js +++ b/src/components/views/rooms/LinkPreviewWidget.js @@ -73,36 +73,13 @@ module.exports = React.createClass({ this.unmounted = true; }, - onImageClick: function(ev) { - var p = this.state.preview; - if (ev.button != 0 || ev.metaKey) return; - ev.preventDefault(); - var ImageView = sdk.getComponent("elements.ImageView"); - - var src = p["og:image"]; - if (src && src.startsWith("mxc://")) { - src = MatrixClientPeg.get().mxcUrlToHttp(src); - } - - var params = { - src: src, - width: p["og:image:width"], - height: p["og:image:height"], - name: p["og:title"] || p["og:description"] || this.props.link, - fileSize: p["matrix:image:size"], - link: this.props.link, - }; - - Modal.createDialog(ImageView, params, "mx_Dialog_lightbox"); - }, - render: function() { var p = this.state.preview; if (!p) return
; // FIXME: do we want to factor out all image displaying between this and MImageBody - especially for lightboxing? var image = p["og:image"]; - var imageMaxWidth = 100, imageMaxHeight = 100; + var imageMaxWidth = 600, imageMaxHeight = 400; if (image && image.startsWith("mxc://")) { image = MatrixClientPeg.get().mxcUrlToHttp(image, imageMaxWidth, imageMaxHeight); } @@ -114,23 +91,24 @@ module.exports = React.createClass({ var img; if (image) { - img =
- -
+ img = ( +
+ ); } return (
- { img }
-
{ p["og:site_name"] ? (" - " + p["og:site_name"]) : null }
-
- { p["og:description"] } -
+
{ p["og:site_name"] ? ("from " + p["og:site_name"]) : null }
- + { img } +
{ p["og:description"] } Read more
+
); } From a49a545161a696009e616c26f96ede57ae849fd8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jul 2016 17:41:07 +0100 Subject: [PATCH 085/222] CSS classes to colour offline users differently So we can use the same 66% opacity as idle tiles for offline-with-last-active-time to reduce the visual jarring --- src/components/views/rooms/EntityTile.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/EntityTile.js b/src/components/views/rooms/EntityTile.js index 91874ed45a..eb351143e9 100644 --- a/src/components/views/rooms/EntityTile.js +++ b/src/components/views/rooms/EntityTile.js @@ -29,6 +29,23 @@ var PRESENCE_CLASS = { "unavailable": "mx_EntityTile_unavailable" }; + +function presence_class_for_member(presence_state, last_active_ago) { + // offline is split into two categories depending on whether we have + // a last_active_ago for them. + if (presence_state == 'offline') { + if (last_active_ago) { + return PRESENCE_CLASS['offline'] + '_beenactive'; + } else { + return PRESENCE_CLASS['offline'] + '_neveractive'; + } + } else if (presence_state) { + return PRESENCE_CLASS[presence_state]; + } else { + return PRESENCE_CLASS['offline']; + } +} + module.exports = React.createClass({ displayName: 'EntityTile', @@ -79,7 +96,10 @@ module.exports = React.createClass({ }, render: function() { - var presenceClass = PRESENCE_CLASS[this.props.presenceState] || "mx_EntityTile_offline"; + const presenceClass = presence_class_for_member( + this.props.presenceState, this.props.presenceLastActiveAgo + ); + var mainClassName = "mx_EntityTile "; mainClassName += presenceClass + (this.props.className ? (" " + this.props.className) : ""); var nameEl; From 7c1b4f4fc965deebde58031bc932fc9e4899b5a8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jul 2016 18:13:15 +0100 Subject: [PATCH 086/222] Obey my own code style --- src/components/views/rooms/EntityTile.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/EntityTile.js b/src/components/views/rooms/EntityTile.js index eb351143e9..b406f4f06f 100644 --- a/src/components/views/rooms/EntityTile.js +++ b/src/components/views/rooms/EntityTile.js @@ -30,7 +30,7 @@ var PRESENCE_CLASS = { }; -function presence_class_for_member(presence_state, last_active_ago) { +function presenceClassForMember(presence_state, last_active_ago) { // offline is split into two categories depending on whether we have // a last_active_ago for them. if (presence_state == 'offline') { @@ -96,7 +96,7 @@ module.exports = React.createClass({ }, render: function() { - const presenceClass = presence_class_for_member( + const presenceClass = presenceClassForMember( this.props.presenceState, this.props.presenceLastActiveAgo ); From 2fd690ea2b14ef1881e203d8ad3db1a3f5d9f9fa Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jul 2016 18:13:54 +0100 Subject: [PATCH 087/222] Oops, removed the pure offline class --- src/components/views/rooms/EntityTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/EntityTile.js b/src/components/views/rooms/EntityTile.js index b406f4f06f..ef460fe74a 100644 --- a/src/components/views/rooms/EntityTile.js +++ b/src/components/views/rooms/EntityTile.js @@ -42,7 +42,7 @@ function presenceClassForMember(presence_state, last_active_ago) { } else if (presence_state) { return PRESENCE_CLASS[presence_state]; } else { - return PRESENCE_CLASS['offline']; + return PRESENCE_CLASS['offline'] + '_neveractive'; } } From 9fd0ea1e328fb066d567a468404920c9fc989286 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jul 2016 18:18:44 +0100 Subject: [PATCH 088/222] More variable case --- src/components/views/rooms/EntityTile.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/EntityTile.js b/src/components/views/rooms/EntityTile.js index ef460fe74a..8a99b4c565 100644 --- a/src/components/views/rooms/EntityTile.js +++ b/src/components/views/rooms/EntityTile.js @@ -30,17 +30,17 @@ var PRESENCE_CLASS = { }; -function presenceClassForMember(presence_state, last_active_ago) { +function presenceClassForMember(presenceState, lastActiveAgo) { // offline is split into two categories depending on whether we have // a last_active_ago for them. - if (presence_state == 'offline') { - if (last_active_ago) { + if (presenceState == 'offline') { + if (lastActiveAgo) { return PRESENCE_CLASS['offline'] + '_beenactive'; } else { return PRESENCE_CLASS['offline'] + '_neveractive'; } - } else if (presence_state) { - return PRESENCE_CLASS[presence_state]; + } else if (presenceState) { + return PRESENCE_CLASS[presenceState]; } else { return PRESENCE_CLASS['offline'] + '_neveractive'; } From 743cb4b7772ede94153a5c9cf0b27d88b02703c1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jul 2016 18:37:57 +0100 Subject: [PATCH 089/222] Remove the member list loading hack Now the memberlist os a truncated list it at-best makes no difference and may be marginally slowing us down. --- src/components/views/rooms/MemberList.js | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index 1a3f710063..dbbc9c6a7e 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -54,7 +54,7 @@ module.exports = React.createClass({ this.memberDict = this.getMemberDict(); - state.members = this.roomMembers(INITIAL_LOAD_NUM_MEMBERS); + state.members = this.roomMembers(); return state; }, @@ -83,19 +83,6 @@ module.exports = React.createClass({ } }, - componentDidMount: function() { - var self = this; - - // Lazy-load in more than the first N members - setTimeout(function() { - if (!self.isMounted()) return; - // lazy load to prevent it blocking the first render - self.setState({ - members: self.roomMembers() - }); - }, 50); - }, - /* onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) { // ignore anything but real-time updates at the end of the room: From 3abdb83d162485a76b84b578ae203e74f0b9433a Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jul 2016 18:40:43 +0100 Subject: [PATCH 090/222] Remove now-unused limit param to roomMembers() --- src/components/views/rooms/MemberList.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index dbbc9c6a7e..01a952f1d7 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -315,7 +315,7 @@ module.exports = React.createClass({ return all_members; }, - roomMembers: function(limit) { + roomMembers: function() { var all_members = this.memberDict || {}; var all_user_ids = Object.keys(all_members); var ConferenceHandler = CallHandler.getConferenceHandler(); @@ -324,7 +324,7 @@ module.exports = React.createClass({ var to_display = []; var count = 0; - for (var i = 0; i < all_user_ids.length && (limit === undefined || count < limit); ++i) { + for (var i = 0; i < all_user_ids.length; ++i) { var user_id = all_user_ids[i]; var m = all_members[user_id]; From febd2110c53afb2f663064d9a1531c474cd04e33 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Fri, 15 Jul 2016 11:23:58 +0100 Subject: [PATCH 091/222] Updated all dialogs to define their primary button, I've selected the one that had been specified for focus, but the primary button isn't dependent on focus, it's a UX feature --- src/components/views/dialogs/ErrorDialog.js | 2 +- src/components/views/dialogs/LogoutPrompt.js | 2 +- src/components/views/dialogs/NeedToRegisterDialog.js | 2 +- src/components/views/dialogs/QuestionDialog.js | 2 +- src/components/views/dialogs/SetDisplayNameDialog.js | 2 +- src/components/views/dialogs/TextInputDialog.js | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/views/dialogs/ErrorDialog.js b/src/components/views/dialogs/ErrorDialog.js index b3278dfcfe..cc400e30a6 100644 --- a/src/components/views/dialogs/ErrorDialog.js +++ b/src/components/views/dialogs/ErrorDialog.js @@ -59,7 +59,7 @@ module.exports = React.createClass({ {this.props.description}
-
diff --git a/src/components/views/dialogs/LogoutPrompt.js b/src/components/views/dialogs/LogoutPrompt.js index 67fedfe840..7c4ba18e82 100644 --- a/src/components/views/dialogs/LogoutPrompt.js +++ b/src/components/views/dialogs/LogoutPrompt.js @@ -46,7 +46,7 @@ module.exports = React.createClass({ Sign out?
- +
diff --git a/src/components/views/dialogs/NeedToRegisterDialog.js b/src/components/views/dialogs/NeedToRegisterDialog.js index d9133ee138..0080e0c643 100644 --- a/src/components/views/dialogs/NeedToRegisterDialog.js +++ b/src/components/views/dialogs/NeedToRegisterDialog.js @@ -63,7 +63,7 @@ module.exports = React.createClass({ {this.props.description}
-
- diff --git a/src/components/views/dialogs/SetDisplayNameDialog.js b/src/components/views/dialogs/SetDisplayNameDialog.js index 81ceb21696..c1041cc218 100644 --- a/src/components/views/dialogs/SetDisplayNameDialog.js +++ b/src/components/views/dialogs/SetDisplayNameDialog.js @@ -76,7 +76,7 @@ module.exports = React.createClass({ />
- +
diff --git a/src/components/views/dialogs/TextInputDialog.js b/src/components/views/dialogs/TextInputDialog.js index fed7ff079a..6245b5786f 100644 --- a/src/components/views/dialogs/TextInputDialog.js +++ b/src/components/views/dialogs/TextInputDialog.js @@ -86,7 +86,7 @@ module.exports = React.createClass({ -
From 41bff38713e4a6f536296a3a5af7d9394fff22b9 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 15 Jul 2016 15:04:19 +0100 Subject: [PATCH 092/222] fix classes used for body spans, and only apply markdown-body to markdown(!) --- src/HtmlUtils.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 64d746f0a4..8b3a368f4d 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -223,8 +223,10 @@ module.exports = { let match = EMOJI_REGEX.exec(contentBodyTrimmed); let emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length; - let className = classNames('markdown-body', { - 'emoji-body': emojiBody, + const className = classNames({ + 'mx_EventTile_body': true, + 'mx_EventTile_bigEmoji': emojiBody, + 'markdown-body': isHtml, }); return ; }, From d5bed78a54926b591422c60fdd3ff3b849c95689 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Jul 2016 16:10:27 +0100 Subject: [PATCH 093/222] Rejig tab complete to make it faster Now do a lot less when people speak. Also move more of the tab completion logic into TabComplete.js and out of RoomView. --- src/TabComplete.js | 77 +++++++++++++++++--- src/TabCompleteEntries.js | 40 +--------- src/components/structures/RoomStatusBar.js | 16 ++-- src/components/structures/RoomView.js | 66 ++++++++--------- src/components/views/rooms/TabCompleteBar.js | 8 +- 5 files changed, 113 insertions(+), 94 deletions(-) diff --git a/src/TabComplete.js b/src/TabComplete.js index 7da8bde76b..5b7be7c286 100644 --- a/src/TabComplete.js +++ b/src/TabComplete.js @@ -13,7 +13,10 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -var Entry = require("./TabCompleteEntries").Entry; + +import { Entry, MemberEntry, CommandEntry } from './TabCompleteEntries'; +import SlashCommands from './SlashCommands'; +import MatrixClientPeg from './MatrixClientPeg'; const DELAY_TIME_MS = 1000; const KEY_TAB = 9; @@ -45,23 +48,34 @@ class TabComplete { this.isFirstWord = false; // true if you tab-complete on the first word this.enterTabCompleteTimerId = null; this.inPassiveMode = false; + this.memberTabOrder = {}; + this.memberOrderSeq = 0; } /** - * @param {Entry[]} completeList + * Call this when a a UI element representing a tab complete entry has been clicked + * @param {entry} The entry that was clicked */ - setCompletionList(completeList) { - this.list = completeList; + onEntryClick(entry) { if (this.opts.onClickCompletes) { - // assign onClick listeners for each entry to complete the text - this.list.forEach((l) => { - l.onClick = () => { - this.completeTo(l); - } - }); + this.completeTo(entry); } } + loadEntries(room) { + this._makeEntries(room); + this._initSorting(room); + this._sortEntries(); + } + + onMemberSpoke(member) { + if (this.memberTabOrder[member.userId] === undefined) { + this.list.push(new MemberEntry(member)); + } + this.memberTabOrder[member.userId] = this.memberOrderSeq++; + this._sortEntries(); + } + /** * @param {DOMElement} */ @@ -307,6 +321,49 @@ class TabComplete { this.opts.onStateChange(this.completing); } } + + _sortEntries() { + // largest comes first + const KIND_ORDER = { + command: 1, + member: 2, + }; + + this.list.sort((a, b) => { + const kindOrderDifference = KIND_ORDER[b.kind] - KIND_ORDER[a.kind]; + if (kindOrderDifference != 0) { + return kindOrderDifference; + } + + if (a.kind == 'member') { + return this.memberTabOrder[b.member.userId] - this.memberTabOrder[a.member.userId]; + } + + // anything else we have no ordering for + return 0; + }); + } + + _makeEntries(room) { + const myUserId = MatrixClientPeg.get().credentials.userId; + + const members = room.getJoinedMembers().filter(function(member) { + if (member.userId !== myUserId) return true; + }); + + this.list = MemberEntry.fromMemberList(members).concat( + CommandEntry.fromCommands(SlashCommands.getCommandList()) + ); + } + + _initSorting(room) { + this.memberTabOrder = {}; + this.memberOrderSeq = 0; + + for (const ev of room.getLiveTimeline().getEvents()) { + this.memberTabOrder[ev.getSender()] = this.memberOrderSeq++; + } + } }; module.exports = TabComplete; diff --git a/src/TabCompleteEntries.js b/src/TabCompleteEntries.js index 419b3d7942..4a28103210 100644 --- a/src/TabCompleteEntries.js +++ b/src/TabCompleteEntries.js @@ -69,6 +69,7 @@ class Entry { class CommandEntry extends Entry { constructor(cmd, cmdWithArgs) { super(cmdWithArgs); + this.kind = 'command'; this.cmd = cmd; } @@ -95,6 +96,7 @@ class MemberEntry extends Entry { constructor(member) { super(member.name || member.userId); this.member = member; + this.kind = 'member'; } getImageJsx() { @@ -113,42 +115,8 @@ class MemberEntry extends Entry { } } -MemberEntry.fromMemberList = function(room, members) { - // build up a dict of when, in the history we have cached, - // each member last spoke - const lastSpoke = {}; - const timelineEvents = room.getLiveTimeline().getEvents(); - for (const ev of room.getLiveTimeline().getEvents()) { - lastSpoke[ev.getSender()] = ev.getTs(); - } - - return members.sort(function(a, b) { - const lastSpokeA = lastSpoke[a.userId] || 0; - const lastSpokeB = lastSpoke[b.userId] || 0; - - if (lastSpokeA != lastSpokeB) { - // B - A here because the highest value - // is most recent - return lastSpokeB - lastSpokeA; - } - - var userA = a.user; - var userB = b.user; - if (userA && !userB) { - return -1; // a comes first - } - else if (!userA && userB) { - return 1; // b comes first - } - else if (!userA && !userB) { - return 0; // don't care - } - else { // both User objects exist - var lastActiveAgoA = userA.lastActiveAgo || Number.MAX_SAFE_INTEGER; - var lastActiveAgoB = userB.lastActiveAgo || Number.MAX_SAFE_INTEGER; - return lastActiveAgoA - lastActiveAgoB; - } - }).map(function(m) { +MemberEntry.fromMemberList = function(members) { + return members.map(function(m) { return new MemberEntry(m); }); } diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 92f50dcb02..4309b1e849 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -26,9 +26,9 @@ module.exports = React.createClass({ propTypes: { // the room this statusbar is representing. room: React.PropTypes.object.isRequired, - - // a list of TabCompleteEntries.Entry objects - tabCompleteEntries: React.PropTypes.array, + + // a TabComplete object + tabComplete: React.PropTypes.object, // the number of messages which have arrived since we've been scrolled up numUnreadMessages: React.PropTypes.number, @@ -208,11 +208,11 @@ module.exports = React.createClass({ ); } - if (this.props.tabCompleteEntries) { + if (this.props.tabComplete.isTabCompleting()) { return (
- + ); - }, + }, }); diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 71edbf162d..f7f7ceb12c 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -31,10 +31,7 @@ var Modal = require("../../Modal"); var sdk = require('../../index'); var CallHandler = require('../../CallHandler'); var TabComplete = require("../../TabComplete"); -var MemberEntry = require("../../TabCompleteEntries").MemberEntry; -var CommandEntry = require("../../TabCompleteEntries").CommandEntry; var Resend = require("../../Resend"); -var SlashCommands = require("../../SlashCommands"); var dis = require("../../dispatcher"); var Tinter = require("../../Tinter"); var rate_limited_func = require('../../ratelimitedfunc'); @@ -136,12 +133,6 @@ module.exports = React.createClass({ }, componentWillMount: function() { - this.dispatcherRef = dis.register(this.onAction); - MatrixClientPeg.get().on("Room", this.onRoom); - MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); - MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData); - MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember); - this.tabComplete = new TabComplete({ allowLooping: false, autoEnterTabComplete: true, @@ -151,6 +142,12 @@ module.exports = React.createClass({ } }); + this.dispatcherRef = dis.register(this.onAction); + MatrixClientPeg.get().on("Room", this.onRoom); + MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); + MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData); + MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember); + if (this.props.roomAddress[0] == '#') { // we always look up the alias from the directory server: // we want the room that the given alias is pointing to @@ -205,8 +202,13 @@ module.exports = React.createClass({ MatrixClientPeg.get().credentials.userId, 'join' ); - // update the tab complete list now we have a room - this._updateTabCompleteList(); + this.tabComplete.loadEntries(this.state.room); + + 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); } if (!user_is_in_room && this.state.roomId) { @@ -363,7 +365,15 @@ module.exports = React.createClass({ // update ther tab complete list as it depends on who most recently spoke, // and that has probably just changed - this._updateTabCompleteList(); + if (ev.sender) { + this.tabComplete.onMemberSpoke(ev.sender); + + 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); + } }, // called when state.room is first initialised (either at initial load, @@ -441,7 +451,13 @@ module.exports = React.createClass({ } // a member state changed in this room, refresh the tab complete list - this._updateTabCompleteList(); + this.tabComplete.loadEntries(this.state.room); + + 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); // 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 @@ -506,8 +522,6 @@ module.exports = React.createClass({ window.addEventListener('resize', this.onResize); this.onResize(); - this._updateTabCompleteList(); - // XXX: EVIL HACK to autofocus inviting on empty rooms. // We use the setTimeout to avoid racing with focus_composer. if (this.state.room && @@ -525,24 +539,6 @@ module.exports = React.createClass({ } }, - _updateTabCompleteList: function() { - var cli = MatrixClientPeg.get(); - - if (!this.state.room) { - return; - } - var members = this.state.room.getJoinedMembers().filter(function(member) { - if (member.userId !== cli.credentials.userId) return true; - }); - - UserProvider.getInstance().setUserList(members); - this.tabComplete.setCompletionList( - MemberEntry.fromMemberList(this.state.room, members).concat( - CommandEntry.fromCommands(SlashCommands.getCommandList()) - ) - ); - }, - componentDidUpdate: function() { if (this.refs.roomView) { var roomView = ReactDOM.findDOMNode(this.refs.roomView); @@ -1380,12 +1376,10 @@ module.exports = React.createClass({ statusBar = } else if (!this.state.searchResults) { var RoomStatusBar = sdk.getComponent('structures.RoomStatusBar'); - var tabEntries = this.tabComplete.isTabCompleting() ? - this.tabComplete.peek(6) : null; statusBar = - {this.props.entries.map(function(entry, i) { + {this.props.tabComplete.peek(6).map((entry, i) => { return (
+ className={ "mx_TabCompleteBar_item " + (entry instanceof CommandEntry ? "mx_TabCompleteBar_command" : "") } + onClick={this.props.tabComplete.onEntryClick.bind(this.props.tabComplete, entry)} > {entry.getImageJsx()} {entry.getText()} From 7d712d06a1086ed0588f04b7c6e281becf9e683d Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Jul 2016 16:14:05 +0100 Subject: [PATCH 094/222] Move code to make diff less confusing --- src/components/structures/RoomView.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index f7f7ceb12c..9c41a993e3 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -133,6 +133,12 @@ module.exports = React.createClass({ }, componentWillMount: function() { + this.dispatcherRef = dis.register(this.onAction); + MatrixClientPeg.get().on("Room", this.onRoom); + MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); + MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData); + MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember); + this.tabComplete = new TabComplete({ allowLooping: false, autoEnterTabComplete: true, @@ -142,12 +148,6 @@ module.exports = React.createClass({ } }); - this.dispatcherRef = dis.register(this.onAction); - MatrixClientPeg.get().on("Room", this.onRoom); - MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); - MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData); - MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember); - if (this.props.roomAddress[0] == '#') { // we always look up the alias from the directory server: // we want the room that the given alias is pointing to From a61168d943099fec0f56bf10e3d31e75269ebe5e Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 15 Jul 2016 16:54:56 +0100 Subject: [PATCH 095/222] Revert "Amends react template and removes opening image in lightbox" --- .../views/rooms/LinkPreviewWidget.js | 46 ++++++++++++++----- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.js index 3398b021fb..ba438c1d12 100644 --- a/src/components/views/rooms/LinkPreviewWidget.js +++ b/src/components/views/rooms/LinkPreviewWidget.js @@ -73,13 +73,36 @@ module.exports = React.createClass({ this.unmounted = true; }, + onImageClick: function(ev) { + var p = this.state.preview; + if (ev.button != 0 || ev.metaKey) return; + ev.preventDefault(); + var ImageView = sdk.getComponent("elements.ImageView"); + + var src = p["og:image"]; + if (src && src.startsWith("mxc://")) { + src = MatrixClientPeg.get().mxcUrlToHttp(src); + } + + var params = { + src: src, + width: p["og:image:width"], + height: p["og:image:height"], + name: p["og:title"] || p["og:description"] || this.props.link, + fileSize: p["matrix:image:size"], + link: this.props.link, + }; + + Modal.createDialog(ImageView, params, "mx_Dialog_lightbox"); + }, + render: function() { var p = this.state.preview; if (!p) return
; // FIXME: do we want to factor out all image displaying between this and MImageBody - especially for lightboxing? var image = p["og:image"]; - var imageMaxWidth = 600, imageMaxHeight = 400; + var imageMaxWidth = 100, imageMaxHeight = 100; if (image && image.startsWith("mxc://")) { image = MatrixClientPeg.get().mxcUrlToHttp(image, imageMaxWidth, imageMaxHeight); } @@ -91,24 +114,23 @@ module.exports = React.createClass({ var img; if (image) { - img = ( - - ); + img =
+ +
} return (
+ { img }
-
{ p["og:site_name"] ? ("from " + p["og:site_name"]) : null }
+
{ p["og:site_name"] ? (" - " + p["og:site_name"]) : null }
+
+ { p["og:description"] } +
- { img } -
{ p["og:description"] } Read more
- +
); } From 327015ba0f271bed0dac88d5a8ff7759c4847344 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Jul 2016 17:03:53 +0100 Subject: [PATCH 096/222] Tidy up autocomplete updating ..into a function --- src/components/structures/RoomView.js | 30 +++++++++++---------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 9c41a993e3..75614471a0 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -202,13 +202,8 @@ module.exports = React.createClass({ MatrixClientPeg.get().credentials.userId, 'join' ); + this._updateAutoComplete(); this.tabComplete.loadEntries(this.state.room); - - 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); } if (!user_is_in_room && this.state.roomId) { @@ -367,12 +362,8 @@ module.exports = React.createClass({ // and that has probably just changed if (ev.sender) { this.tabComplete.onMemberSpoke(ev.sender); - - 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); + // nb. we don't need to update the new autocomplete here since + // its results are currently ordered purely by search score. } }, @@ -452,12 +443,7 @@ module.exports = React.createClass({ // a member state changed in this room, refresh the tab complete list this.tabComplete.loadEntries(this.state.room); - - 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); + this._updateAutoComplete(); // 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 @@ -1263,6 +1249,14 @@ 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 ccf8e269cde30f331804e66d51f8c1daf127b068 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Jul 2016 17:15:51 +0100 Subject: [PATCH 097/222] Comments & required props --- src/TabComplete.js | 5 +++++ src/components/structures/RoomStatusBar.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/TabComplete.js b/src/TabComplete.js index 5b7be7c286..0ec0b77802 100644 --- a/src/TabComplete.js +++ b/src/TabComplete.js @@ -48,7 +48,12 @@ class TabComplete { this.isFirstWord = false; // true if you tab-complete on the first word this.enterTabCompleteTimerId = null; this.inPassiveMode = false; + + // Map tracking ordering of the room members. + // userId: integer, highest comes first. this.memberTabOrder = {}; + + // monotonically increasing counter used for tracking ordering of members this.memberOrderSeq = 0; } diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 4309b1e849..9a0d3dbbdd 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -28,7 +28,7 @@ module.exports = React.createClass({ room: React.PropTypes.object.isRequired, // a TabComplete object - tabComplete: React.PropTypes.object, + tabComplete: React.PropTypes.object.isRequired, // the number of messages which have arrived since we've been scrolled up numUnreadMessages: React.PropTypes.number, From 5c566cae5c01578aa0c164a52903368c7c4460b6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Jul 2016 18:10:56 +0100 Subject: [PATCH 098/222] typo --- 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 75614471a0..abcccc48b8 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -358,7 +358,7 @@ module.exports = React.createClass({ } } - // update ther tab complete list as it depends on who most recently spoke, + // update the tab complete list as it depends on who most recently spoke, // and that has probably just changed if (ev.sender) { this.tabComplete.onMemberSpoke(ev.sender); From bfe50c209c2a44087b4ffdd17a7f32f4c48edc3c Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 16 Jul 2016 22:49:06 +0100 Subject: [PATCH 099/222] fix typo in comment --- src/components/views/rooms/MemberInfo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 0e14776e82..e60a717c3e 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -366,7 +366,7 @@ module.exports = React.createClass({ var currentRoom = MatrixClientPeg.get().getRoom(this.props.member.roomId); currentMembers = currentRoom.getJoinedMembers(); } - // if we're currently in a 1:1 with this user, start a new chat + // if we're currently not in a 1:1 with this user, start a new chat if (currentMembers && currentMembers.length === 2 && userIds.indexOf(currentMembers[0].userId) !== -1 && userIds.indexOf(currentMembers[1].userId) !== -1) From 2bc9dd43077ca10e1821e452241de86d228f86a8 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 17 Jul 2016 18:32:48 +0100 Subject: [PATCH 100/222] hopefully fix vector-im/vector-web#1813 --- .../views/rooms/InviteMemberList.js | 32 +++++++++++++++---- src/createRoom.js | 4 +++ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/components/views/rooms/InviteMemberList.js b/src/components/views/rooms/InviteMemberList.js index 5246e2e54d..c1c83e533b 100644 --- a/src/components/views/rooms/InviteMemberList.js +++ b/src/components/views/rooms/InviteMemberList.js @@ -37,12 +37,31 @@ module.exports = React.createClass({ }, componentWillMount: function() { - this._room = MatrixClientPeg.get().getRoom(this.props.roomId); + var cli = MatrixClientPeg.get(); + cli.on("RoomState.members", this.onRoomStateMember); + this._emailEntity = null; + + // we have to update the list whenever membership changes + // particularly to avoid bug https://github.com/vector-im/vector-web/issues/1813 + this._updateList(); + }, + + componentDidMount: function() { + // initialise the email tile + this.onSearchQueryChanged(''); + }, + + componentWillUnmount: function() { + var cli = MatrixClientPeg.get(); + if (cli) { + cli.removeListener("RoomState.members", this.onRoomStateMember); + } + }, + + _updateList: function() { + this._room = MatrixClientPeg.get().getRoom(this.props.roomId); // Load the complete user list for inviting new users - // TODO: Keep this list bleeding-edge up-to-date. Practically speaking, - // it will do for now not being updated as random new users join different - // rooms as this list will be reloaded every room swap. if (this._room) { this._userList = MatrixClientPeg.get().getUsers().filter((u) => { return !this._room.hasMembershipState(u.userId, "join"); @@ -50,9 +69,8 @@ module.exports = React.createClass({ } }, - componentDidMount: function() { - // initialise the email tile - this.onSearchQueryChanged(''); + onRoomStateMember: function(ev, state, member) { + this._updateList(); }, onInvite: function(ev) { diff --git a/src/createRoom.js b/src/createRoom.js index 658561e78a..3a56dc1be3 100644 --- a/src/createRoom.js +++ b/src/createRoom.js @@ -69,6 +69,10 @@ function createRoom(opts) { return client.createRoom(createOpts).finally(function() { modal.close(); }).then(function(res) { + // NB createRoom doesn't block on the client seeing the echo that the + // room has been created, so we race here with the client knowing that + // the room exists, causing things like + // https://github.com/vector-im/vector-web/issues/1813 dis.dispatch({ action: 'view_room', room_id: res.room_id From 0553d806d71d47a35e77ebd0e87d2c51a36d1b5c Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 17 Jul 2016 18:40:54 +0100 Subject: [PATCH 101/222] stop re-invites --- src/components/views/rooms/InviteMemberList.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/InviteMemberList.js b/src/components/views/rooms/InviteMemberList.js index c1c83e533b..4742597229 100644 --- a/src/components/views/rooms/InviteMemberList.js +++ b/src/components/views/rooms/InviteMemberList.js @@ -64,7 +64,8 @@ module.exports = React.createClass({ // Load the complete user list for inviting new users if (this._room) { this._userList = MatrixClientPeg.get().getUsers().filter((u) => { - return !this._room.hasMembershipState(u.userId, "join"); + return (!this._room.hasMembershipState(u.userId, "join") && + !this._room.hasMembershipState(u.userId, "invite")); }); } }, From 999c52b650abf6ea3d455b5f7cc9993f5353ea52 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 17 Jul 2016 19:00:22 +0100 Subject: [PATCH 102/222] fix comment --- src/components/views/rooms/MemberInfo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index e60a717c3e..63ce78340c 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -366,7 +366,7 @@ module.exports = React.createClass({ var currentRoom = MatrixClientPeg.get().getRoom(this.props.member.roomId); currentMembers = currentRoom.getJoinedMembers(); } - // if we're currently not in a 1:1 with this user, start a new chat + // if our current room is a 1:1 with the target user, start a new chat rather than NOOPing if (currentMembers && currentMembers.length === 2 && userIds.indexOf(currentMembers[0].userId) !== -1 && userIds.indexOf(currentMembers[1].userId) !== -1) From 554a6ff035ddbc66899f1edafd3e52138bb1227c Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 17 Jul 2016 19:41:53 +0100 Subject: [PATCH 103/222] improve wording of MemberInfo's start chat button. Fixes https://github.com/vector-im/vector-web/issues/689 and clarifies https://github.com/matrix-org/matrix-react-sdk/issues/328 --- src/components/views/rooms/MemberInfo.js | 112 +++++++++++++---------- 1 file changed, 66 insertions(+), 46 deletions(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 63ce78340c..c439f8b40c 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -61,12 +61,16 @@ module.exports = React.createClass({ updating: 0, devicesLoading: true, devices: null, + existingOneToOneRoomId: null, } }, - componentWillMount: function() { this._cancelDeviceList = null; + + this.setState({ + existingOneToOneRoomId: this.getExistingOneToOneRoomId() + }); }, componentDidMount: function() { @@ -90,6 +94,44 @@ module.exports = React.createClass({ } }, + getExistingOneToOneRoomId: function() { + var self = this; + var rooms = MatrixClientPeg.get().getRooms(); + var userIds = [ + this.props.member.userId, + MatrixClientPeg.get().credentials.userId + ]; + var existingRoomId; + + // roomId can be null here because of a hack in MatrixChat.onUserClick where we + // abuse this to view users rather than room members. + var currentMembers; + if (this.props.member.roomId) { + var currentRoom = MatrixClientPeg.get().getRoom(this.props.member.roomId); + currentMembers = currentRoom.getJoinedMembers(); + } + + // reuse the first private 1:1 we find + existingRoomId = null; + + for (var i = 0; i < rooms.length; i++) { + // don't try to reuse public 1:1 rooms + var join_rules = rooms[i].currentState.getStateEvents("m.room.join_rules", ''); + if (join_rules && join_rules.getContent().join_rule === 'public') continue; + + var members = rooms[i].getJoinedMembers(); + if (members.length === 2 && + userIds.indexOf(members[0].userId) !== -1 && + userIds.indexOf(members[1].userId) !== -1) + { + existingRoomId = rooms[i].roomId; + break; + } + } + + return existingRoomId; + }, + onDeviceVerificationChanged: function(userId, device) { if (userId == this.props.member.userId) { // no need to re-download the whole thing; just update our copy of @@ -349,54 +391,17 @@ module.exports = React.createClass({ onChatClick: function() { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + + // TODO: keep existingOneToOneRoomId updated if we see any room member changes anywhere + + var useExistingOneToOneRoom = this.state.existingOneToOneRoomId && (this.state.existingOneToOneRoomId !== this.props.member.roomId); + // check if there are any existing rooms with just us and them (1:1) // If so, just view that room. If not, create a private room with them. - var self = this; - var rooms = MatrixClientPeg.get().getRooms(); - var userIds = [ - this.props.member.userId, - MatrixClientPeg.get().credentials.userId - ]; - var existingRoomId; - - // roomId can be null here because of a hack in MatrixChat.onUserClick where we - // abuse this to view users rather than room members. - var currentMembers; - if (this.props.member.roomId) { - var currentRoom = MatrixClientPeg.get().getRoom(this.props.member.roomId); - currentMembers = currentRoom.getJoinedMembers(); - } - // if our current room is a 1:1 with the target user, start a new chat rather than NOOPing - if (currentMembers && currentMembers.length === 2 && - userIds.indexOf(currentMembers[0].userId) !== -1 && - userIds.indexOf(currentMembers[1].userId) !== -1) - { - existingRoomId = null; - } - // otherwise reuse the first private 1:1 we find - else { - existingRoomId = null; - - for (var i = 0; i < rooms.length; i++) { - // don't try to reuse public 1:1 rooms - var join_rules = rooms[i].currentState.getStateEvents("m.room.join_rules", ''); - if (join_rules && join_rules.getContent().join_rule === 'public') continue; - - var members = rooms[i].getJoinedMembers(); - if (members.length === 2 && - userIds.indexOf(members[0].userId) !== -1 && - userIds.indexOf(members[1].userId) !== -1) - { - existingRoomId = rooms[i].roomId; - break; - } - } - } - - if (existingRoomId) { + if (this.state.existingOneToOneRoomId) { dis.dispatch({ action: 'view_room', - room_id: existingRoomId + room_id: this.state.existingOneToOneRoomId, }); this.props.onFinished(); } @@ -553,7 +558,22 @@ module.exports = React.createClass({ if (this.props.member.userId !== MatrixClientPeg.get().credentials.userId) { // FIXME: we're referring to a vector component from react-sdk var BottomLeftMenuTile = sdk.getComponent('rooms.BottomLeftMenuTile'); - startChat = + + var label; + if (this.state.existingOneToOneRoomId) { + if (this.state.existingOneToOneRoomId == this.props.member.roomId) { + label = "Start new direct chat"; + } + else { + label = "Go to direct chat"; + } + } + else { + label = "Start direct chat"; + } + + startChat = } if (this.state.updating) { From 8d749be51f171ee81e6e0ee7eb0608ce9b167d5f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 17 Jul 2016 20:36:53 +0100 Subject: [PATCH 104/222] fix potential bug where canonical alias setting is broken (modulo autoheisting) --- src/components/views/room_settings/AliasSettings.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.js index 34b6083c30..8c8ae659e8 100644 --- a/src/components/views/room_settings/AliasSettings.js +++ b/src/components/views/room_settings/AliasSettings.js @@ -83,13 +83,11 @@ module.exports = React.createClass({ alias: this.state.canonicalAlias }, "" ) - ); + ); } - // save new aliases for m.room.aliases var aliasOperations = this.getAliasOperations(); - var promises = []; for (var i = 0; i < aliasOperations.length; i++) { var alias_operation = aliasOperations[i]; console.log("alias %s %s", alias_operation.place, alias_operation.val); @@ -301,7 +299,7 @@ module.exports = React.createClass({
Add -
+
: "" }
From 1559c69ddf0cb9ca36dccdd3b559971db5382b02 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Sun, 17 Jul 2016 21:21:27 +0100 Subject: [PATCH 105/222] Create room Dialog Spinner fix. --- src/createRoom.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/createRoom.js b/src/createRoom.js index 658561e78a..c00eca0486 100644 --- a/src/createRoom.js +++ b/src/createRoom.js @@ -64,7 +64,7 @@ function createRoom(opts) { } ]; - var modal = Modal.createDialog(Loader); + var modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); return client.createRoom(createOpts).finally(function() { modal.close(); From a385955c6be8b6d33a90ef9ada37fe169979ad2a Mon Sep 17 00:00:00 2001 From: wmwragg Date: Sun, 17 Jul 2016 21:23:52 +0100 Subject: [PATCH 106/222] Fixed MatrixChat Dialog Spinner. Still needs testing, unsure what triggers it. --- src/components/structures/MatrixChat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index dcaa82fc75..dc9ca08e94 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -390,7 +390,7 @@ module.exports = React.createClass({ // FIXME: controller shouldn't be loading a view :( var Loader = sdk.getComponent("elements.Spinner"); - var modal = Modal.createDialog(Loader); + var modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); d.then(function() { modal.close(); From bcd1c7e0997aab09c91f51463ddb7b536938d1d4 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 18 Jul 2016 01:34:26 +0100 Subject: [PATCH 107/222] improve comment --- src/HtmlUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 8b3a368f4d..2ab635081f 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -186,7 +186,7 @@ module.exports = { * * highlights: optional list of words to highlight, ordered by longest word first * - * opts.highlightLink: optional href to add to highlights + * opts.highlightLink: optional href to add to highlighted words */ bodyToHtml: function(content, highlights, opts) { opts = opts || {}; From ebdac4ee50a408b6876a94982475ca2f73466fb3 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 18 Jul 2016 01:35:42 +0100 Subject: [PATCH 108/222] first cut (untested) --- src/UserSettingsStore.js | 28 +++++++++ src/components/structures/MessagePanel.js | 4 ++ src/components/structures/RoomView.js | 46 +++++++++++++- src/components/structures/TimelinePanel.js | 4 ++ src/components/structures/UserSettings.js | 61 ++++++++++++++++++- src/components/views/messages/MessageEvent.js | 4 ++ src/components/views/messages/TextualBody.js | 29 +++++---- .../views/room_settings/ColorSettings.js | 2 +- src/components/views/rooms/EventTile.js | 4 ++ src/components/views/rooms/RoomSettings.js | 3 + 10 files changed, 171 insertions(+), 14 deletions(-) diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index 305994aa0e..990fa8b7a9 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -113,6 +113,34 @@ module.exports = { }); }, + getUrlPreviewsDisabled: function() { + var event = MatrixClientPeg.get().getAccountData("org.matrix.preview_urls"); + return (event && event.disable); + }, + + setUrlPreviewsDisabled: function(disabled) { + // FIXME: handle errors + MatrixClientPeg.get().setAccountData("org.matrix.preview_urls", { + disable: disabled + }); + }, + + getSyncedSettings: function() { + return MatrixClientPeg.get().getAccountData("im.vector.web.settings") || {}; + }, + + getSyncedSetting: function(type) { + var settings = this.getSyncedSettings(); + return settings[type]; + }, + + setSyncedSetting: function(type, value) { + var settings = this.getSyncedSettings(); + settings[type] = value; + // FIXME: handle errors + MatrixClientPeg.get().setAccountData("im.vector.web.settings", settings); + }, + isFeatureEnabled: function(feature: string): boolean { return localStorage.getItem(`mx_labs_feature_${feature}`) === 'true'; }, diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index c8e878118b..53efac6406 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -44,6 +44,9 @@ module.exports = React.createClass({ // ID of an event to highlight. If undefined, no event will be highlighted. highlightedEventId: React.PropTypes.string, + // Should we show URL Previews + showUrlPreview: React.PropTypes.bool, + // event after which we should show a read marker readMarkerEventId: React.PropTypes.string, @@ -365,6 +368,7 @@ module.exports = React.createClass({ onWidgetLoad={this._onWidgetLoad} readReceipts={readReceipts} readReceiptMap={this._readReceiptMap} + showUrlPreview={this.props.showUrlPreview} checkUnmounting={this._isUnmounting} eventSendStatus={mxEv.status} last={last} isSelectedEvent={highlight}/> diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index cfd359ea01..7a27340cc6 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -239,6 +239,8 @@ module.exports = React.createClass({ MatrixClientPeg.get().stopPeeking(); this._onRoomLoaded(this.state.room); } + + _updatePreviewUrlVisibility(this.state.room); }, shouldComponentUpdate: function(nextProps, nextState) { @@ -341,6 +343,10 @@ module.exports = React.createClass({ // ignore events for other rooms if (!this.state.room || room.roomId != this.state.room.roomId) return; + if (event.getType() === "org.matrix.room.preview_urls") { + _updatePreviewUrlVisibility(room); + } + // ignore anything but real-time updates at the end of the room: // updates from pagination will happen when the paginate completes. if (toStartOfTimeline || !data || !data.liveEvent) return; @@ -384,6 +390,40 @@ module.exports = React.createClass({ } }, + _updatePreviewUrlVisibility: function(room) { + // check our per-room overrides + var roomPreviewUrls = room.getAccountData("org.matrix.room.preview_urls"); + if (roomPreviewUrls && roomPreviewUrls.disabled !== undefined) { + this.setState({ + showUrlPreview: !roomPreviewUrls.disabled + }); + return; + } + + // check our global disable override + var userRoomPreviewUrls = MatrixClientPeg().get().getAccountData("org.matrix.preview_urls"); + if (userRoomPreviewUrls && userRoomPreviewUrls.disabled) { + this.setState({ + showUrlPreview: false + }); + return; + } + + // check the room state event + var roomStatePreviewUrls = room.currentState.getStateEvents('org.matrix.room.preview_urls', ''); + if (roomStatePreviewUrls && roomStatePreviewUrls.disabled) { + this.setState({ + showUrlPreview: false; + }); + return; + } + + // otherwise, we assume they're on. + this.setState({ + showUrlPreview: true; + }); + }, + onRoom: function(room) { // This event is fired when the room is 'stored' by the JS SDK, which // means it's now a fully-fledged room object ready to be used, so @@ -416,12 +456,15 @@ module.exports = React.createClass({ onRoomAccountData: function(room, event) { if (room.roomId == this.props.roomId) { - if (event.getType === "org.matrix.room.color_scheme") { + if (event.getType() === "org.matrix.room.color_scheme") { var color_scheme = event.getContent(); // XXX: we should validate the event console.log("Tinter.tint from onRoomAccountData"); Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); } + else if (event.getType() === "org.matrix.room.preview_urls") { + _updatePreviewUrlVisibility(room); + } } }, @@ -1523,6 +1566,7 @@ module.exports = React.createClass({ eventPixelOffset={this.props.eventPixelOffset} onScroll={ this.onMessageListScroll } onReadMarkerUpdated={ this._updateTopUnreadMessagesBar } + showUrlPreview = { this.state.showUrlPreview } opacity={ this.props.opacity } />); diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index d804dfd6b9..52225c7c09 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -71,6 +71,9 @@ var TimelinePanel = React.createClass({ // half way down the viewport. eventPixelOffset: React.PropTypes.number, + // Should we show URL Previews + showUrlPreview: React.PropTypes.bool, + // callback which is called when the panel is scrolled. onScroll: React.PropTypes.func, @@ -934,6 +937,7 @@ var TimelinePanel = React.createClass({ readMarkerEventId={ this.state.readMarkerEventId } readMarkerVisible={ this.state.readMarkerVisible } suppressFirstDateSeparator={ this.state.canBackPaginate } + showUrlPreview = { this.state.showUrlPreview } ourUserId={ MatrixClientPeg.get().credentials.userId } stickyBottom={ stickyBottom } onScroll={ this.onMessageListScroll } diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 7fcb81a60c..41ac03725b 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -261,6 +261,63 @@ module.exports = React.createClass({ }); }, + _renderUserInterfaceSettings: function() { + var client = MatrixClientPeg.get(); + + var settingsLabels = [ + /* + { + id: 'alwaysShowTimestamps', + label: 'Always show message timestamps', + }, + { + id: 'showTwelveHourTimestamps', + label: 'Show timestamps in 12 hour format (e.g. 2:30pm)', + }, + { + id: 'useCompactLayout', + label: 'Use compact timeline layout', + }, + { + id: 'useFixedWidthFont', + label: 'Use fixed width font', + }, + */ + ]; + + var syncedSettings = UserSettingsStore.getSyncedSettings(); + + return ( +
+

User Interface

+
+
+ UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) } + /> + +
+
+ { settingsLabels.forEach( setting => { +
+ UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) } + /> + +
+ })} +
+ ); + }, + _renderDeviceInfo: function() { if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) { return null; @@ -378,7 +435,7 @@ module.exports = React.createClass({ this._renderLabs = function () { let features = LABS_FEATURES.map(feature => ( -
+
; }, }); diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 310da598fa..2268affbe2 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -39,6 +39,9 @@ module.exports = React.createClass({ /* link URL for the highlights */ highlightLink: React.PropTypes.string, + /* should show URL previews for this event */ + showUrlPreview: React.PropTypes.bool, + /* callback for when our widget has loaded */ onWidgetLoad: React.PropTypes.func, }, @@ -57,16 +60,18 @@ module.exports = React.createClass({ componentDidMount: function() { linkifyElement(this.refs.content, linkifyMatrix.options); - var links = this.findLinks(this.refs.content.children); - if (links.length) { - this.setState({ links: links.map((link)=>{ - return link.getAttribute("href"); - })}); + if (this.props.showUrlPreview) { + var links = this.findLinks(this.refs.content.children); + if (links.length) { + this.setState({ links: links.map((link)=>{ + return link.getAttribute("href"); + })}); - // lazy-load the hidden state of the preview widget from localstorage - if (global.localStorage) { - var hidden = global.localStorage.getItem("hide_preview_" + this.props.mxEvent.getId()); - this.setState({ widgetHidden: hidden }); + // lazy-load the hidden state of the preview widget from localstorage + if (global.localStorage) { + var hidden = global.localStorage.getItem("hide_preview_" + this.props.mxEvent.getId()); + this.setState({ widgetHidden: hidden }); + } } } @@ -163,9 +168,11 @@ module.exports = React.createClass({ render: function() { var mxEvent = this.props.mxEvent; var content = mxEvent.getContent(); - var body = HtmlUtils.bodyToHtml(content, this.props.highlights, - {highlightLink: this.props.highlightLink}); + var body = HtmlUtils.bodyToHtml(content, this.props.highlights, {}); + if (this.props.highlightLink) { + body = { body }; + } var widgets; if (this.state.links.length && !this.state.widgetHidden) { diff --git a/src/components/views/room_settings/ColorSettings.js b/src/components/views/room_settings/ColorSettings.js index fff97ea817..6d147b1f63 100644 --- a/src/components/views/room_settings/ColorSettings.js +++ b/src/components/views/room_settings/ColorSettings.js @@ -57,7 +57,7 @@ module.exports = React.createClass({ data.primary_color = scheme.primary_color; data.secondary_color = scheme.secondary_color; data.index = this._getColorIndex(data); - + if (data.index === -1) { // append the unrecognised colours to our palette data.index = ROOM_COLORS.length; diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 70dfe8ac33..a914d513ac 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -101,6 +101,9 @@ module.exports = React.createClass({ /* link URL for the highlights */ highlightLink: React.PropTypes.string, + /* should show URL previews for this event */ + showUrlPreview: React.PropTypes.bool, + /* is this the focused event */ isSelectedEvent: React.PropTypes.bool, @@ -420,6 +423,7 @@ module.exports = React.createClass({
diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 1f50a9241b..ee8b716e02 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -422,6 +422,7 @@ module.exports = React.createClass({ var AliasSettings = sdk.getComponent("room_settings.AliasSettings"); var ColorSettings = sdk.getComponent("room_settings.ColorSettings"); + var UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings"); var EditableText = sdk.getComponent('elements.EditableText'); var PowerSelector = sdk.getComponent('elements.PowerSelector'); @@ -654,6 +655,8 @@ module.exports = React.createClass({ canonicalAliasEvent={this.props.room.currentState.getStateEvents('m.room.canonical_alias', '')} aliasEvents={this.props.room.currentState.getStateEvents('m.room.aliases')} /> + +

Permissions

From f13bb5f6562080250c5947637730e23fdf503879 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 18 Jul 2016 01:39:24 +0100 Subject: [PATCH 109/222] typos --- src/components/structures/RoomView.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 7a27340cc6..35217ab2f4 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -413,14 +413,14 @@ module.exports = React.createClass({ var roomStatePreviewUrls = room.currentState.getStateEvents('org.matrix.room.preview_urls', ''); if (roomStatePreviewUrls && roomStatePreviewUrls.disabled) { this.setState({ - showUrlPreview: false; + showUrlPreview: false }); return; } // otherwise, we assume they're on. this.setState({ - showUrlPreview: true; + showUrlPreview: true }); }, From e92024f7a9ecdfc06d83a05bab0b14ff1693386f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 18 Jul 2016 10:42:18 +0100 Subject: [PATCH 110/222] reskindex --- src/component-index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/component-index.js b/src/component-index.js index 4aa0efe21f..5fadb18b6a 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -74,6 +74,8 @@ module.exports.components['views.messages.TextualEvent'] = require('./components module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody'); module.exports.components['views.room_settings.AliasSettings'] = require('./components/views/room_settings/AliasSettings'); module.exports.components['views.room_settings.ColorSettings'] = require('./components/views/room_settings/ColorSettings'); +module.exports.components['views.room_settings.UrlPreviewSettings'] = require('./components/views/room_settings/UrlPreviewSettings'); +module.exports.components['views.rooms.Autocomplete'] = require('./components/views/rooms/Autocomplete'); module.exports.components['views.rooms.AuxPanel'] = require('./components/views/rooms/AuxPanel'); module.exports.components['views.rooms.EntityTile'] = require('./components/views/rooms/EntityTile'); module.exports.components['views.rooms.EventTile'] = require('./components/views/rooms/EventTile'); From 6bf54992895a0e768e4a453a25b5d5438e9d3317 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 18 Jul 2016 10:47:03 +0100 Subject: [PATCH 111/222] typos --- src/components/structures/RoomView.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 35217ab2f4..ea73193b03 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -240,7 +240,7 @@ module.exports = React.createClass({ this._onRoomLoaded(this.state.room); } - _updatePreviewUrlVisibility(this.state.room); + this._updatePreviewUrlVisibility(this.state.room); }, shouldComponentUpdate: function(nextProps, nextState) { @@ -343,8 +343,8 @@ module.exports = React.createClass({ // ignore events for other rooms if (!this.state.room || room.roomId != this.state.room.roomId) return; - if (event.getType() === "org.matrix.room.preview_urls") { - _updatePreviewUrlVisibility(room); + if (ev.getType() === "org.matrix.room.preview_urls") { + this._updatePreviewUrlVisibility(room); } // ignore anything but real-time updates at the end of the room: From 743f79a9a7475aa1d655adbfcda233e58cb00489 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 18 Jul 2016 15:22:08 +0100 Subject: [PATCH 112/222] Fix unpublishing room in room settings Fixes https://github.com/vector-im/vector-web/issues/1743 --- src/components/views/rooms/RoomSettings.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 1f50a9241b..8f1a2cd141 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -65,7 +65,12 @@ module.exports = React.createClass({ tags_changed: false, tags: tags, areNotifsMuted: areNotifsMuted, - isRoomPublished: false, // loaded async in componentWillMount + // isRoomPublished is loaded async in componentWillMount so when the component + // inits, the saved value will always be undefined, however getInitialState() + // is also called from the saving code so we must return the correct value here + // if we have it (although this could race if the user saves before we load whether + // the room is oublisherd or not. + isRoomPublished: this._originalIsRoomPublished, }; }, From ffaf7f44f3a716e656f93b90285b737e2759d839 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 18 Jul 2016 15:36:19 +0100 Subject: [PATCH 113/222] typos --- src/components/views/rooms/RoomSettings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 8f1a2cd141..df30b63c75 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -69,7 +69,7 @@ module.exports = React.createClass({ // inits, the saved value will always be undefined, however getInitialState() // is also called from the saving code so we must return the correct value here // if we have it (although this could race if the user saves before we load whether - // the room is oublisherd or not. + // the room is published or not). isRoomPublished: this._originalIsRoomPublished, }; }, From 9e4511b8eb60d8a81fd654593dae9a1c4584b697 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Mon, 18 Jul 2016 16:10:07 +0100 Subject: [PATCH 114/222] Now showing three dots when hovering over the badge --- src/components/views/rooms/RoomTile.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index da9f97ab65..d3fbfa0220 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -43,7 +43,10 @@ module.exports = React.createClass({ }, getInitialState: function() { - return( { hover : false }); + return({ + hover : false, + badgeHover : false, + }); }, onClick: function() { @@ -61,6 +64,14 @@ module.exports = React.createClass({ this.setState( { hover : false }); }, + badgeOnMouseEnter: function() { + this.setState( { badgeHover : true } ); + }, + + badgeOnMouseLeave: function() { + this.setState( { badgeHover : false } ); + }, + render: function() { var myUserId = MatrixClientPeg.get().credentials.userId; var me = this.props.room.currentState.members[myUserId]; @@ -83,8 +94,14 @@ module.exports = React.createClass({ name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon var badge; + var badgeContent; if (this.props.highlight || notificationCount > 0) { - badge =
{ notificationCount ? notificationCount : '!' }
; + if (this.state.badgeHover) { + badgeContent = "\u00B7 \u00B7 \u00B7"; + } else { + badgeContent = notificationCount ? notificationCount : '!'; + } + badge =
{ badgeContent }
; } /* if (this.props.highlight) { From 4bd2b93f5909aeb5c4d4e41fd265ce28afda2fbc Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 19 Jul 2016 16:05:15 +0100 Subject: [PATCH 115/222] Add support for sending uploaded content as m.video --- src/ContentMessages.js | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/ContentMessages.js b/src/ContentMessages.js index 56e3499eae..796c1ed58d 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -52,6 +52,36 @@ function infoForImageFile(imageFile) { return deferred.promise; } +function infoForVideoFile(videoFile) { + var deferred = q.defer(); + + // Load the file into an html element + var video = document.createElement("video"); + + var reader = new FileReader(); + reader.onload = function(e) { + video.src = e.target.result; + + // Once ready, returns its size + video.onloadedmetadata = function() { + deferred.resolve({ + w: video.videoWidth, + h: video.videoHeight + }); + }; + video.onerror = function(e) { + deferred.reject(e); + }; + }; + reader.onerror = function(e) { + deferred.reject(e); + }; + reader.readAsDataURL(videoFile); + + return deferred.promise; +} + + class ContentMessages { constructor() { this.inprogress = []; @@ -81,6 +111,12 @@ class ContentMessages { } else if (file.type.indexOf('audio/') == 0) { content.msgtype = 'm.audio'; def.resolve(); + } else if (file.type.indexOf('video/') == 0) { + content.msgtype = 'm.video'; + infoForVideoFile(file).then(function (videoInfo) { + extend(content.info, videoInfo); + def.resolve(); + }); } else { content.msgtype = 'm.file'; def.resolve(); From d6df3682f28af5bb4f1e6430d27615703fa52413 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 20 Jul 2016 11:58:49 +0100 Subject: [PATCH 116/222] Fix enourmous video bug --- src/components/views/messages/MVideoBody.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/messages/MVideoBody.js b/src/components/views/messages/MVideoBody.js index 6cbaf0b151..c8327a71ae 100644 --- a/src/components/views/messages/MVideoBody.js +++ b/src/components/views/messages/MVideoBody.js @@ -34,7 +34,7 @@ module.exports = React.createClass({ } if (fullWidth < thumbWidth && fullHeight < thumbHeight) { // no scaling needs to be applied - return fullHeight; + return 1; } var widthMulti = thumbWidth / fullWidth; var heightMulti = thumbHeight / fullHeight; From 1365f188294b67ed88c01faa43a8ce0640d6bfd4 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 20 Jul 2016 12:03:13 +0100 Subject: [PATCH 117/222] many stupid thinkos and bugfixes; make it work --- src/UserSettingsStore.js | 5 +- src/components/structures/RoomView.js | 23 +-- src/components/structures/TimelinePanel.js | 2 +- src/components/views/messages/TextualBody.js | 41 +++-- .../views/room_settings/UrlPreviewSettings.js | 157 ++++++++++++++++++ src/components/views/rooms/EventTile.js | 2 + src/components/views/rooms/RoomSettings.js | 10 +- 7 files changed, 211 insertions(+), 29 deletions(-) create mode 100644 src/components/views/room_settings/UrlPreviewSettings.js diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index 990fa8b7a9..39f393b242 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -115,7 +115,7 @@ module.exports = { getUrlPreviewsDisabled: function() { var event = MatrixClientPeg.get().getAccountData("org.matrix.preview_urls"); - return (event && event.disable); + return (event && event.getContent().disable); }, setUrlPreviewsDisabled: function(disabled) { @@ -126,7 +126,8 @@ module.exports = { }, getSyncedSettings: function() { - return MatrixClientPeg.get().getAccountData("im.vector.web.settings") || {}; + var event = MatrixClientPeg.get().getAccountData("im.vector.web.settings"); + return event ? event.getContent() : {}; }, getSyncedSetting: function(type) { diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index ea73193b03..7b5b3f7c7f 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -239,8 +239,6 @@ module.exports = React.createClass({ MatrixClientPeg.get().stopPeeking(); this._onRoomLoaded(this.state.room); } - - this._updatePreviewUrlVisibility(this.state.room); }, shouldComponentUpdate: function(nextProps, nextState) { @@ -372,6 +370,7 @@ module.exports = React.createClass({ // after a successful peek, or after we join the room). _onRoomLoaded: function(room) { this._calculatePeekRules(room); + this._updatePreviewUrlVisibility(room); }, _calculatePeekRules: function(room) { @@ -391,18 +390,20 @@ module.exports = React.createClass({ }, _updatePreviewUrlVisibility: function(room) { + console.log("_updatePreviewUrlVisibility"); + // check our per-room overrides var roomPreviewUrls = room.getAccountData("org.matrix.room.preview_urls"); - if (roomPreviewUrls && roomPreviewUrls.disabled !== undefined) { + if (roomPreviewUrls && roomPreviewUrls.getContent().disable !== undefined) { this.setState({ - showUrlPreview: !roomPreviewUrls.disabled + showUrlPreview: !roomPreviewUrls.getContent().disable }); return; } // check our global disable override - var userRoomPreviewUrls = MatrixClientPeg().get().getAccountData("org.matrix.preview_urls"); - if (userRoomPreviewUrls && userRoomPreviewUrls.disabled) { + var userRoomPreviewUrls = MatrixClientPeg.get().getAccountData("org.matrix.preview_urls"); + if (userRoomPreviewUrls && userRoomPreviewUrls.getContent().disable) { this.setState({ showUrlPreview: false }); @@ -411,7 +412,7 @@ module.exports = React.createClass({ // check the room state event var roomStatePreviewUrls = room.currentState.getStateEvents('org.matrix.room.preview_urls', ''); - if (roomStatePreviewUrls && roomStatePreviewUrls.disabled) { + if (roomStatePreviewUrls && roomStatePreviewUrls.getContent().disable) { this.setState({ showUrlPreview: false }); @@ -454,8 +455,8 @@ module.exports = React.createClass({ Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); }, - onRoomAccountData: function(room, event) { - if (room.roomId == this.props.roomId) { + onRoomAccountData: function(event, room) { + if (room.roomId == this.state.roomId) { if (event.getType() === "org.matrix.room.color_scheme") { var color_scheme = event.getContent(); // XXX: we should validate the event @@ -463,7 +464,7 @@ module.exports = React.createClass({ Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); } else if (event.getType() === "org.matrix.room.preview_urls") { - _updatePreviewUrlVisibility(room); + this._updatePreviewUrlVisibility(room); } } }, @@ -1557,6 +1558,8 @@ module.exports = React.createClass({ hideMessagePanel = true; } + console.log("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview); + var messagePanel = ( { @@ -74,19 +98,6 @@ module.exports = React.createClass({ } } } - - if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") - HtmlUtils.highlightDom(ReactDOM.findDOMNode(this)); - }, - - shouldComponentUpdate: function(nextProps, nextState) { - // exploit that events are immutable :) - // ...and that .links is only ever set in componentDidMount and never changes - return (nextProps.mxEvent.getId() !== this.props.mxEvent.getId() || - nextProps.highlights !== this.props.highlights || - nextProps.highlightLink !== this.props.highlightLink || - nextState.links !== this.state.links || - nextState.widgetHidden !== this.state.widgetHidden); }, findLinks: function(nodes) { @@ -175,7 +186,7 @@ module.exports = React.createClass({ } var widgets; - if (this.state.links.length && !this.state.widgetHidden) { + if (this.state.links.length && !this.state.widgetHidden && this.props.showUrlPreview) { var LinkPreviewWidget = sdk.getComponent('rooms.LinkPreviewWidget'); widgets = this.state.links.map((link)=>{ return + + Disable URL previews by default for participants in this room + + } + else { + disableRoomPreviewUrls = + + } + + return ( +
+

URL Previews

+ + + { disableRoomPreviewUrls } + + +
+ ); + + } +}); \ No newline at end of file diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index a914d513ac..77be8226a2 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -362,6 +362,8 @@ module.exports = React.createClass({ var SenderProfile = sdk.getComponent('messages.SenderProfile'); var MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); + //console.log("EventTile showUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview); + var content = this.props.mxEvent.getContent(); var msgtype = content.msgtype; diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index ee8b716e02..fda29a38f0 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -211,10 +211,13 @@ module.exports = React.createClass({ // color scheme promises.push(this.saveColor()); + // url preview settings + promises.push(this.saveUrlPreviewSettings()); + // encryption promises.push(this.saveEncryption()); - console.log("Performing %s operations", promises.length); + console.log("Performing %s operations: %s", promises.length, JSON.stringify(promises)); return q.allSettled(promises); }, @@ -228,6 +231,11 @@ module.exports = React.createClass({ return this.refs.color_settings.saveSettings(); }, + saveUrlPreviewSettings: function() { + if (!this.refs.url_preview_settings) { return q(); } + return this.refs.url_preview_settings.saveSettings(); + }, + saveEncryption: function () { if (!this.refs.encrypt) { return q(); } From 33edb1dea96b9eb3a41dac5f479cce75ee0a2122 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Wed, 20 Jul 2016 12:47:32 +0100 Subject: [PATCH 118/222] Made the badge always exist, but if no unread mesasges available then has a different class --- src/components/views/rooms/RoomTile.js | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index d3fbfa0220..e510c2abed 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -95,14 +95,24 @@ module.exports = React.createClass({ name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon var badge; var badgeContent; - if (this.props.highlight || notificationCount > 0) { - if (this.state.badgeHover) { - badgeContent = "\u00B7 \u00B7 \u00B7"; - } else { - badgeContent = notificationCount ? notificationCount : '!'; - } - badge =
{ badgeContent }
; + var badgeClasses; + + if (this.state.badgeHover) { + badgeContent = "\u00B7 \u00B7 \u00B7"; + } else if (this.props.highlight || notificationCount > 0) { + badgeContent = notificationCount ? notificationCount : '!'; + } else { + badgeContent = '\u200B'; } + + if (this.props.highlight || notificationCount > 0) { + badgeClasses = "mx_RoomTile_badge" + } else { + badgeClasses = "mx_RoomTile_badge mx_RoomTile_badge_no_unread"; + } + + badge =
{ badgeContent }
; + /* if (this.props.highlight) { badge =
!
; From ffa43ebb96039920805dff52e721775af343e14e Mon Sep 17 00:00:00 2001 From: wmwragg Date: Wed, 20 Jul 2016 17:12:41 +0100 Subject: [PATCH 119/222] badge content when hovered is now kerned via the CSS --- 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 e510c2abed..aa83110632 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -98,7 +98,7 @@ module.exports = React.createClass({ var badgeClasses; if (this.state.badgeHover) { - badgeContent = "\u00B7 \u00B7 \u00B7"; + badgeContent = "\u00B7\u00B7\u00B7"; } else if (this.props.highlight || notificationCount > 0) { badgeContent = notificationCount ? notificationCount : '!'; } else { @@ -106,7 +106,7 @@ module.exports = React.createClass({ } if (this.props.highlight || notificationCount > 0) { - badgeClasses = "mx_RoomTile_badge" + badgeClasses = "mx_RoomTile_badge"; } else { badgeClasses = "mx_RoomTile_badge mx_RoomTile_badge_no_unread"; } From d6415aceca539a8c084ba4932864a532e129fe7d Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 20 Jul 2016 18:14:16 +0100 Subject: [PATCH 120/222] handle accountData changes, and errors on toggling URL previews --- src/UserSettingsStore.js | 4 ++-- src/components/structures/RoomView.js | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index 39f393b242..f4eb4f0d83 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -120,7 +120,7 @@ module.exports = { setUrlPreviewsDisabled: function(disabled) { // FIXME: handle errors - MatrixClientPeg.get().setAccountData("org.matrix.preview_urls", { + return MatrixClientPeg.get().setAccountData("org.matrix.preview_urls", { disable: disabled }); }, @@ -139,7 +139,7 @@ module.exports = { var settings = this.getSyncedSettings(); settings[type] = value; // FIXME: handle errors - MatrixClientPeg.get().setAccountData("im.vector.web.settings", settings); + return MatrixClientPeg.get().setAccountData("im.vector.web.settings", settings); }, isFeatureEnabled: function(feature: string): boolean { diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index f73b90ae28..cef85931e1 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -138,6 +138,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData); MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember); + MatrixClientPeg.get().on("accountData", this.onAccountData); this.tabComplete = new TabComplete({ allowLooping: false, @@ -460,6 +461,12 @@ module.exports = React.createClass({ Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); }, + onAccountData: function(event) { + if (event.getType() === "org.matrix.preview_urls" && this.state.room) { + this._updatePreviewUrlVisibility(this.state.room); + } + }, + onRoomAccountData: function(event, room) { if (room.roomId == this.state.roomId) { if (event.getType() === "org.matrix.room.color_scheme") { From bc87fc575b55bb776d17c67431bc1157d9beeeb8 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 20 Jul 2016 18:16:54 +0100 Subject: [PATCH 121/222] remove debug --- 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 cef85931e1..47a81fe18f 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1557,7 +1557,7 @@ module.exports = React.createClass({ hideMessagePanel = true; } - console.log("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview); + // console.log("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview); var messagePanel = ( Date: Wed, 20 Jul 2016 18:17:09 +0100 Subject: [PATCH 122/222] remove debug --- 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 47a81fe18f..8ca3289cb6 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -396,7 +396,7 @@ module.exports = React.createClass({ }, _updatePreviewUrlVisibility: function(room) { - console.log("_updatePreviewUrlVisibility"); + // console.log("_updatePreviewUrlVisibility"); // check our per-room overrides var roomPreviewUrls = room.getAccountData("org.matrix.room.preview_urls"); From e41df245c32072cb8bb81495d4af2c12c7ce2110 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 20 Jul 2016 20:20:10 +0100 Subject: [PATCH 123/222] Set the device_id on pre-login MatrixClient In order that device_id is set when we register a new user (or, for that matter, when we register as a guest), we need to make sure that device_id is set on the temporary MatrixClient which is created before the user is logged in - ie, that created by replaceUsingUrls. In order to achieve this, I've basically removed the distinction between replaceUsingAccessToken and replaceUsingUrls. There is a side-effect in that the temporary MatrixClient now gets an e2e sessionStore, but I don't think that is a bad thing. --- src/MatrixClientPeg.js | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 143b804228..2383a48d0f 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -97,35 +97,20 @@ class MatrixClient { // FIXME, XXX: this all seems very convoluted :( // - // if we replace the singleton using URLs we bypass our createClientForPeg() - // global helper function... but if we replace it using - // an access_token we don't? - // // Why do we have this peg wrapper rather than just MatrixClient.get()? // Why do we name MatrixClient as MatrixClientPeg when we export it? // // -matthew replaceUsingUrls(hs_url, is_url) { - matrixClient = Matrix.createClient({ - baseUrl: hs_url, - idBaseUrl: is_url - }); - - // XXX: factor this out with the localStorage setting in replaceUsingAccessToken - if (localStorage) { - try { - localStorage.setItem("mx_hs_url", hs_url); - localStorage.setItem("mx_is_url", is_url); - } catch (e) { - console.warn("Error using local storage: can't persist HS/IS URLs!"); - } - } else { - console.warn("No local storage available: can't persist HS/IS URLs!"); - } + this.replaceClient(hs_url, is_url); } replaceUsingAccessToken(hs_url, is_url, user_id, access_token, isGuest) { + this.replaceClient(hs_url, is_url, user_id, access_token, isGuest); + } + + replaceClient(hs_url, is_url, user_id, access_token, isGuest) { if (localStorage) { try { localStorage.clear(); From 030652f289a80bb30a7dc485fdb060ee1c88fe5d Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 21 Jul 2016 13:49:31 +0100 Subject: [PATCH 124/222] Add removeLisatener for account data listener --- 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 8ca3289cb6..accf96f349 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -268,6 +268,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().removeListener("Room.accountData", this.onRoomAccountData); MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember); + MatrixClientPeg.get().removeListener("accountData", this.onAccountData); } window.removeEventListener('resize', this.onResize); From 775fc971020772c2ee80e0b291b2d1e00b88840a Mon Sep 17 00:00:00 2001 From: wmwragg Date: Thu, 21 Jul 2016 14:33:54 +0100 Subject: [PATCH 125/222] Slight refactor to better match current code --- src/components/views/rooms/RoomTile.js | 28 +++----------------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index aa83110632..3dccb6ee6a 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -84,6 +84,7 @@ module.exports = React.createClass({ 'mx_RoomTile_selected': this.props.selected, 'mx_RoomTile_unread': this.props.unread, 'mx_RoomTile_unreadNotify': notificationCount > 0, + 'mx_RoomTile_read': !(this.props.highlight || notificationCount > 0), 'mx_RoomTile_highlight': this.props.highlight, 'mx_RoomTile_invited': (me && me.membership == 'invite'), }); @@ -91,11 +92,10 @@ module.exports = React.createClass({ // XXX: We should never display raw room IDs, but sometimes the // room name js sdk gives is undefined (cannot repro this -- k) var name = this.props.room.name || this.props.room.roomId; - name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon + var badge; var badgeContent; - var badgeClasses; if (this.state.badgeHover) { badgeContent = "\u00B7\u00B7\u00B7"; @@ -105,29 +105,7 @@ module.exports = React.createClass({ badgeContent = '\u200B'; } - if (this.props.highlight || notificationCount > 0) { - badgeClasses = "mx_RoomTile_badge"; - } else { - badgeClasses = "mx_RoomTile_badge mx_RoomTile_badge_no_unread"; - } - - badge =
{ badgeContent }
; - - /* - if (this.props.highlight) { - badge =
!
; - } - else if (this.props.unread) { - badge =
1
; - } - var nameCell; - if (badge) { - nameCell =
{name}
{badge}
; - } - else { - nameCell =
{name}
; - } - */ + badge =
{ badgeContent }
; var label; if (!this.props.collapsed) { From 0b0f10ddf6c0488d48837a8796761b6171875c88 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 21 Jul 2016 16:25:51 +0100 Subject: [PATCH 126/222] Fix tab complete order properly Don't return NaN from your sort functions... --- src/TabComplete.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/TabComplete.js b/src/TabComplete.js index 0ec0b77802..65441c9381 100644 --- a/src/TabComplete.js +++ b/src/TabComplete.js @@ -341,7 +341,12 @@ class TabComplete { } if (a.kind == 'member') { - return this.memberTabOrder[b.member.userId] - this.memberTabOrder[a.member.userId]; + let orderA = this.memberTabOrder[a.member.userId]; + let orderB = this.memberTabOrder[b.member.userId]; + if (orderA === undefined) orderA = -1; + if (orderB === undefined) orderB = -1; + + return orderB - orderA; } // anything else we have no ordering for From 4013ea75d0f9eba00a24816213ad1a5f68b4d926 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Thu, 21 Jul 2016 17:44:31 +0100 Subject: [PATCH 127/222] Testing out the context menu --- src/components/views/rooms/RoomTile.js | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index aa83110632..2b8d98d42d 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -21,6 +21,7 @@ var classNames = require('classnames'); var dis = require("../../../dispatcher"); var MatrixClientPeg = require('../../../MatrixClientPeg'); var sdk = require('../../../index'); +var ContextualMenu = require('../../../ContextualMenu'); import {emojifyText} from '../../../HtmlUtils'; module.exports = React.createClass({ @@ -46,6 +47,7 @@ module.exports = React.createClass({ return({ hover : false, badgeHover : false, + menu: false, }); }, @@ -72,6 +74,22 @@ module.exports = React.createClass({ this.setState( { badgeHover : false } ); }, + onBadgeClicked: function(e) { + var Label = sdk.getComponent('elements.Label'); + var elementRect = e.target.getBoundingClientRect(); + var x = elementRect.right; + var y = elementRect.height + (elementRect.height / 2); + var self = this; + ContextualMenu.createMenu(Label, { + left: x, + top: y, + onFinished: function() { + self.setState({menu: false}); + } + }); + this.setState({menu: true}); + }, + render: function() { var myUserId = MatrixClientPeg.get().credentials.userId; var me = this.props.room.currentState.members[myUserId]; @@ -111,7 +129,7 @@ module.exports = React.createClass({ badgeClasses = "mx_RoomTile_badge mx_RoomTile_badge_no_unread"; } - badge =
{ badgeContent }
; + badge =
{ badgeContent }
; /* if (this.props.highlight) { @@ -160,9 +178,9 @@ module.exports = React.createClass({ var connectDropTarget = this.props.connectDropTarget; return connectDragSource(connectDropTarget( -
+
- +
{ label } { badge } From ea5e021d8d09a32e6591f53f63589c8452fbbeab Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 21 Jul 2016 17:57:55 +0100 Subject: [PATCH 128/222] Refactor MatrixClientPeg Should be functionally identical --- src/MatrixClientPeg.js | 138 +++++++++++++++++++++-------------------- 1 file changed, 71 insertions(+), 67 deletions(-) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 7c1c5b34d7..bf1659e25c 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -16,13 +16,10 @@ limitations under the License. 'use strict'; -// A thing that holds your Matrix Client -var Matrix = require("matrix-js-sdk"); -var GuestAccess = require("./GuestAccess"); +import Matrix from 'matrix-js-sdk'; +import GuestAccess from './GuestAccess'; -let matrixClient: MatrixClient = null; - -var localStorage = window.localStorage; +const localStorage = window.localStorage; function deviceId() { // XXX: is Math.random()'s deterministicity a problem here? @@ -35,73 +32,22 @@ function deviceId() { return id; } -function createClientForPeg(hs_url, is_url, user_id, access_token, guestAccess) { - var opts = { - baseUrl: hs_url, - idBaseUrl: is_url, - accessToken: access_token, - userId: user_id, - timelineSupport: true, - }; - - if (localStorage) { - opts.sessionStore = new Matrix.WebStorageSessionStore(localStorage); - opts.deviceId = deviceId(); - } - - matrixClient = Matrix.createClient(opts); - - // we're going to add eventlisteners for each matrix event tile, so the - // potential number of event listeners is quite high. - matrixClient.setMaxListeners(500); - - if (guestAccess) { - console.log("Guest: %s", guestAccess.isGuest()); - matrixClient.setGuest(guestAccess.isGuest()); - var peekedRoomId = guestAccess.getPeekedRoom(); - if (peekedRoomId) { - console.log("Peeking in room %s", peekedRoomId); - matrixClient.peekInRoom(peekedRoomId); - } - } -} - -if (localStorage) { - var hs_url = localStorage.getItem("mx_hs_url"); - var is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org'; - var access_token = localStorage.getItem("mx_access_token"); - var user_id = localStorage.getItem("mx_user_id"); - var guestAccess = new GuestAccess(localStorage); - if (access_token && user_id && hs_url) { - console.log("Restoring session for %s", user_id); - createClientForPeg(hs_url, is_url, user_id, access_token, guestAccess); - } - else { - console.log("Session not found."); - } -} - -class MatrixClient { - +// A thing that holds your Matrix Client +// Also magically works across sessions through the power of localstorage +class MatrixClientPeg { constructor(guestAccess) { + this.matrixClient = null; this.guestAccess = guestAccess; } get(): MatrixClient { - return matrixClient; + return this.matrixClient; } unset() { - matrixClient = null; + this.matrixClient = null; } - // FIXME, XXX: this all seems very convoluted :( - // - // Why do we have this peg wrapper rather than just MatrixClient.get()? - // Why do we name MatrixClient as MatrixClientPeg when we export it? - // - // -matthew - replaceUsingUrls(hs_url, is_url) { this.replaceClient(hs_url, is_url); } @@ -119,7 +65,7 @@ class MatrixClient { } } this.guestAccess.markAsGuest(Boolean(isGuest)); - createClientForPeg(hs_url, is_url, user_id, access_token, this.guestAccess); + this._createClient(hs_url, is_url, user_id, access_token); if (localStorage) { try { localStorage.setItem("mx_hs_url", hs_url); @@ -134,9 +80,67 @@ class MatrixClient { console.warn("No local storage available: can't persist session!"); } } + + getCredentials() { + return [ + this.matrixClient.baseUrl, + this.matrixClient.idBaseUrl, + this.matrixClient.credentials.userId, + this.matrixClient.getAccessToken(), + this.guestAccess.isGuest(), + ]; + } + + tryRestore() { + if (localStorage) { + const hs_url = localStorage.getItem("mx_hs_url"); + const is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org'; + const access_token = localStorage.getItem("mx_access_token"); + const user_id = localStorage.getItem("mx_user_id"); + const guestAccess = new GuestAccess(localStorage); + if (access_token && user_id && hs_url) { + console.log("Restoring session for %s", user_id); + this._createClient(hs_url, is_url, user_id, access_token, guestAccess); + } else { + console.log("Session not found."); + } + } + } + + _createClient(hs_url, is_url, user_id, access_token) { + var opts = { + baseUrl: hs_url, + idBaseUrl: is_url, + accessToken: access_token, + userId: user_id, + timelineSupport: true, + }; + + if (localStorage) { + opts.sessionStore = new Matrix.WebStorageSessionStore(localStorage); + opts.deviceId = deviceId(); + } + + this.matrixClient = Matrix.createClient(opts); + + // we're going to add eventlisteners for each matrix event tile, so the + // potential number of event listeners is quite high. + this.matrixClient.setMaxListeners(500); + + if (this.guestAccess) { + console.log("Guest: %s", this.guestAccess.isGuest()); + this.matrixClient.setGuest(this.guestAccess.isGuest()); + var peekedRoomId = this.guestAccess.getPeekedRoom(); + if (peekedRoomId) { + console.log("Peeking in room %s", peekedRoomId); + this.matrixClient.peekInRoom(peekedRoomId); + } + } + } } -if (!global.mxMatrixClient) { - global.mxMatrixClient = new MatrixClient(new GuestAccess(localStorage)); +if (!global.mxMatrixClientPeg) { + global.mxMatrixClientPeg = new MatrixClientPeg(new GuestAccess(localStorage)); + global.mxMatrixClientPeg.tryRestore(); } -module.exports = global.mxMatrixClient; +module.exports = global.mxMatrixClientPeg; From 762873350a5489f77e8b256f35a29aeff03a6c64 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Thu, 21 Jul 2016 18:20:12 +0100 Subject: [PATCH 129/222] Badge dohickey shown on name hover and badge hover --- src/components/views/rooms/RoomTile.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 3dccb6ee6a..2857a99aa2 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -97,6 +97,11 @@ module.exports = React.createClass({ var badge; var badgeContent; + var badgeClasses = classNames({ + 'mx_RoomTile_badge': true, + 'mx_RoomTile_badgeButton': this.state.badgeHover, + }); + if (this.state.badgeHover) { badgeContent = "\u00B7\u00B7\u00B7"; } else if (this.props.highlight || notificationCount > 0) { @@ -105,7 +110,7 @@ module.exports = React.createClass({ badgeContent = '\u200B'; } - badge =
{ badgeContent }
; + badge =
{ badgeContent }
; var label; if (!this.props.collapsed) { @@ -113,9 +118,9 @@ module.exports = React.createClass({ let nameHTML = emojifyText(name); if (this.props.selected) { name = ; - label =
{ name }
; + label =
{ name }
; } else { - label =
; + label =
; } } else if (this.state.hover) { @@ -138,9 +143,9 @@ module.exports = React.createClass({ var connectDropTarget = this.props.connectDropTarget; return connectDragSource(connectDropTarget( -
+
- +
{ label } { badge } From 922bb0f40204763794c353958632d4ccc794da60 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Thu, 21 Jul 2016 18:50:07 +0100 Subject: [PATCH 130/222] +99 badge when notifications are greater the 99 --- src/components/views/rooms/RoomTile.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 2857a99aa2..0012f66306 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -105,7 +105,8 @@ module.exports = React.createClass({ if (this.state.badgeHover) { badgeContent = "\u00B7\u00B7\u00B7"; } else if (this.props.highlight || notificationCount > 0) { - badgeContent = notificationCount ? notificationCount : '!'; + var limitedCount = (notificationCount > 99) ? '+99' : notificationCount; + badgeContent = notificationCount ? limitedCount : '!'; } else { badgeContent = '\u200B'; } From ad7f8d0a58de9222becf88b08d4b13be5c7f9fc0 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 22 Jul 2016 10:12:37 +0100 Subject: [PATCH 131/222] Bump to latest react-gemini-scrollbar I've updated our forks of the gemini-scrollbar project to latest upstream. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cd81ad7c56..39709e7e2e 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "react": "^15.0.1", "react-addons-css-transition-group": "^15.1.0", "react-dom": "^15.0.1", - "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#c3d942e", + "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#dbf0abf", "sanitize-html": "^1.11.1", "velocity-vector": "vector-im/velocity#059e3b2", "whatwg-fetch": "^1.0.0" From e3cdeed32b706b892e34658556fcea1a08516c84 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 22 Jul 2016 10:43:50 +0100 Subject: [PATCH 132/222] Bump to react 15.2.1 This should also stop npm complaining about invalid peerDependencies. --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 39709e7e2e..0d2f6e1a5b 100644 --- a/package.json +++ b/package.json @@ -42,9 +42,9 @@ "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop", "optimist": "^0.6.1", "q": "^1.4.1", - "react": "^15.0.1", - "react-addons-css-transition-group": "^15.1.0", - "react-dom": "^15.0.1", + "react": "^15.2.1", + "react-addons-css-transition-group": "^15.2.1", + "react-dom": "^15.2.1", "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#dbf0abf", "sanitize-html": "^1.11.1", "velocity-vector": "vector-im/velocity#059e3b2", From 3dd83922f17dbe3d8282d8db89d327e246d52afe Mon Sep 17 00:00:00 2001 From: wmwragg Date: Fri, 22 Jul 2016 11:12:11 +0100 Subject: [PATCH 133/222] Tooltip positioning tweaked --- 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 aff03182a1..7d41b69567 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -270,7 +270,7 @@ module.exports = React.createClass({ _repositionTooltip: function(e) { if (this.tooltip && this.tooltip.parentElement) { var scroll = ReactDOM.findDOMNode(this); - this.tooltip.style.top = (70 + scroll.parentElement.offsetTop + this.tooltip.parentElement.offsetTop - this._getScrollNode().scrollTop) + "px"; + this.tooltip.style.top = (3 + scroll.parentElement.offsetTop + this.tooltip.parentElement.offsetTop - this._getScrollNode().scrollTop) + "px"; } }, From 5d4b03c1f4e9790ce6bb589c899b13ba02e953e5 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Fri, 22 Jul 2016 11:28:11 +0100 Subject: [PATCH 134/222] Put back clicking on the name to acces the room --- 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 2b8d98d42d..f0ac208ef9 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -153,9 +153,9 @@ module.exports = React.createClass({ let nameHTML = emojifyText(name); if (this.props.selected) { name = ; - label =
{ name }
; + label =
{ name }
; } else { - label =
; + label =
; } } else if (this.state.hover) { From ca75d93434ad840cc2f76f3cd3dfc470b0839fbe Mon Sep 17 00:00:00 2001 From: wmwragg Date: Fri, 22 Jul 2016 11:31:26 +0100 Subject: [PATCH 135/222] Tweaked the offset for the tooltip so that it is next to the element it is tipping --- 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 aff03182a1..7d41b69567 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -270,7 +270,7 @@ module.exports = React.createClass({ _repositionTooltip: function(e) { if (this.tooltip && this.tooltip.parentElement) { var scroll = ReactDOM.findDOMNode(this); - this.tooltip.style.top = (70 + scroll.parentElement.offsetTop + this.tooltip.parentElement.offsetTop - this._getScrollNode().scrollTop) + "px"; + this.tooltip.style.top = (3 + scroll.parentElement.offsetTop + this.tooltip.parentElement.offsetTop - this._getScrollNode().scrollTop) + "px"; } }, From b7e95b3883163b4ee1cbeed246b63d5b9b23069e Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 22 Jul 2016 14:00:23 +0100 Subject: [PATCH 136/222] Remove other guestAccess arg --- src/MatrixClientPeg.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index bf1659e25c..77ebbaa2ba 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -100,7 +100,7 @@ class MatrixClientPeg { const guestAccess = new GuestAccess(localStorage); if (access_token && user_id && hs_url) { console.log("Restoring session for %s", user_id); - this._createClient(hs_url, is_url, user_id, access_token, guestAccess); + this._createClient(hs_url, is_url, user_id, access_token); } else { console.log("Session not found."); } From 6984a55b11a9e87100d19b3e6c43f89c973f99d0 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Fri, 22 Jul 2016 14:58:09 +0100 Subject: [PATCH 137/222] The tooltip now appears even when not collapsed when hover over the avater, to allow the full name to be shown --- src/components/views/rooms/RoomTile.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 0012f66306..20d0c9824c 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -114,6 +114,7 @@ module.exports = React.createClass({ badge =
{ badgeContent }
; var label; + var tooltip; if (!this.props.collapsed) { var className = 'mx_RoomTile_name' + (this.props.isInvite ? ' mx_RoomTile_invite' : ''); let nameHTML = emojifyText(name); @@ -123,6 +124,11 @@ module.exports = React.createClass({ } else { label =
; } + + if (this.state.hover) { + var RoomTooltip = sdk.getComponent("rooms.RoomTooltip"); + tooltip = ; + } } else if (this.state.hover) { var RoomTooltip = sdk.getComponent("rooms.RoomTooltip"); @@ -151,6 +157,7 @@ module.exports = React.createClass({ { label } { badge } { incomingCallBox } + { tooltip }
)); } From e4dd6c8dbeced4604dea6d28970f6d0162fac3fd Mon Sep 17 00:00:00 2001 From: wmwragg Date: Fri, 22 Jul 2016 15:15:24 +0100 Subject: [PATCH 138/222] Browser tooltip being used instead of the HTML styled one. --- src/components/views/rooms/RoomTile.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 20d0c9824c..47d61cf95a 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -120,15 +120,17 @@ module.exports = React.createClass({ let nameHTML = emojifyText(name); if (this.props.selected) { name = ; - label =
{ name }
; + label =
{ name }
; } else { - label =
; + label =
; } + /* if (this.state.hover) { var RoomTooltip = sdk.getComponent("rooms.RoomTooltip"); tooltip = ; } + */ } else if (this.state.hover) { var RoomTooltip = sdk.getComponent("rooms.RoomTooltip"); From ddbac8c73a61a3d730e4e6a6414840d3d3e89fd1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 22 Jul 2016 15:47:47 +0100 Subject: [PATCH 139/222] More refactoring of MatrixClientPeg Including getting rid of GuestAccess as it was basically doing nothing apart from remembering if we were a guest which may as well be done in the same place we save/restore everything else --- src/GuestAccess.js | 51 --------------------------------- src/MatrixClientPeg.js | 64 ++++++++++++++++++++++++------------------ 2 files changed, 37 insertions(+), 78 deletions(-) delete mode 100644 src/GuestAccess.js diff --git a/src/GuestAccess.js b/src/GuestAccess.js deleted file mode 100644 index ef48d23ded..0000000000 --- a/src/GuestAccess.js +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2015 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -const IS_GUEST_KEY = "matrix-is-guest"; - -class GuestAccess { - - constructor(localStorage) { - this.localStorage = localStorage; - try { - this._isGuest = localStorage.getItem(IS_GUEST_KEY) === "true"; - } - catch (e) {} // don't care - } - - setPeekedRoom(roomId) { - // we purposefully do not persist this to local storage as peeking is - // entirely transient. - this._peekedRoomId = roomId; - } - - getPeekedRoom() { - return this._peekedRoomId; - } - - isGuest() { - return this._isGuest; - } - - markAsGuest(isGuest) { - try { - this.localStorage.setItem(IS_GUEST_KEY, JSON.stringify(isGuest)); - } catch (e) {} // ignore. If they don't do LS, they'll just get a new account. - this._isGuest = isGuest; - this._peekedRoomId = null; - } -} - -module.exports = GuestAccess; diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 77ebbaa2ba..8d155143fc 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -17,7 +17,6 @@ limitations under the License. 'use strict'; import Matrix from 'matrix-js-sdk'; -import GuestAccess from './GuestAccess'; const localStorage = window.localStorage; @@ -32,12 +31,15 @@ function deviceId() { return id; } -// A thing that holds your Matrix Client -// Also magically works across sessions through the power of localstorage +/** + * Wrapper object for handling the js-sdk Matrix Client object in the react-sdk + * Handles the creation/initialisation of client objects. + * This module provides a singleton instance of this class so the 'current' + * Matrix Client object is available easily. + */ class MatrixClientPeg { - constructor(guestAccess) { + constructor() { this.matrixClient = null; - this.guestAccess = guestAccess; } get(): MatrixClient { @@ -48,15 +50,19 @@ class MatrixClientPeg { this.matrixClient = null; } + /** + * Replace this MatrixClientPeg's client with a client instance that has + * Home Server / Identity Server URLs but no credentials + */ replaceUsingUrls(hs_url, is_url) { - this.replaceClient(hs_url, is_url); + this.replaceUsingAccessToken(hs_url, is_url); } + /** + * Replace this MatrixClientPeg's client with a client instance that has + * Home Server / Identity Server URLs and active credentials + */ replaceUsingAccessToken(hs_url, is_url, user_id, access_token, isGuest) { - this.replaceClient(hs_url, is_url, user_id, access_token, isGuest); - } - - replaceClient(hs_url, is_url, user_id, access_token, isGuest) { if (localStorage) { try { localStorage.clear(); @@ -64,15 +70,20 @@ class MatrixClientPeg { console.warn("Error clearing local storage", e); } } - this.guestAccess.markAsGuest(Boolean(isGuest)); this._createClient(hs_url, is_url, user_id, access_token); + this.matrixClient.setGuest(Boolean(isGuest)); + if (localStorage) { try { localStorage.setItem("mx_hs_url", hs_url); localStorage.setItem("mx_is_url", is_url); - localStorage.setItem("mx_user_id", user_id); - localStorage.setItem("mx_access_token", access_token); - console.log("Session persisted for %s", user_id); + + if (user_id !== undefined && access_token !== undefined) { + localStorage.setItem("mx_user_id", user_id); + localStorage.setItem("mx_access_token", access_token); + localStorage.setItem("mx_is_guest", JSON.stringify(isGuest)); + console.log("Session persisted for %s", user_id); + } } catch (e) { console.warn("Error using local storage: can't persist session!", e); } @@ -87,7 +98,7 @@ class MatrixClientPeg { this.matrixClient.idBaseUrl, this.matrixClient.credentials.userId, this.matrixClient.getAccessToken(), - this.guestAccess.isGuest(), + this.matrixClient.isGuest(), ]; } @@ -97,10 +108,19 @@ class MatrixClientPeg { const is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org'; const access_token = localStorage.getItem("mx_access_token"); const user_id = localStorage.getItem("mx_user_id"); - const guestAccess = new GuestAccess(localStorage); + + let is_guest; + if (localStorage.getItem("mx_is_guest") !== null) { + is_guest = localStorage.getItem("mx_is_guest") === "true"; + } else { + // legacy key name + is_guest = localStorage.getItem("matrix-is-guest") === "true"; + } + if (access_token && user_id && hs_url) { console.log("Restoring session for %s", user_id); this._createClient(hs_url, is_url, user_id, access_token); + this.matrixClient.setGuest(is_guest); } else { console.log("Session not found."); } @@ -126,21 +146,11 @@ class MatrixClientPeg { // we're going to add eventlisteners for each matrix event tile, so the // potential number of event listeners is quite high. this.matrixClient.setMaxListeners(500); - - if (this.guestAccess) { - console.log("Guest: %s", this.guestAccess.isGuest()); - this.matrixClient.setGuest(this.guestAccess.isGuest()); - var peekedRoomId = this.guestAccess.getPeekedRoom(); - if (peekedRoomId) { - console.log("Peeking in room %s", peekedRoomId); - this.matrixClient.peekInRoom(peekedRoomId); - } - } } } if (!global.mxMatrixClientPeg) { - global.mxMatrixClientPeg = new MatrixClientPeg(new GuestAccess(localStorage)); + global.mxMatrixClientPeg = new MatrixClientPeg(); global.mxMatrixClientPeg.tryRestore(); } module.exports = global.mxMatrixClientPeg; From 9b318e8f610af905c6b7b1aab82e98405a011a2e Mon Sep 17 00:00:00 2001 From: wmwragg Date: Fri, 22 Jul 2016 16:12:20 +0100 Subject: [PATCH 140/222] Getting the corrct height for the placing the context menu --- 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 f0ac208ef9..48cb345d91 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -78,7 +78,7 @@ module.exports = React.createClass({ var Label = sdk.getComponent('elements.Label'); var elementRect = e.target.getBoundingClientRect(); var x = elementRect.right; - var y = elementRect.height + (elementRect.height / 2); + var y = elementRect.top + (elementRect.height / 2); var self = this; ContextualMenu.createMenu(Label, { left: x, From b07e50d418c5d2e75f0725e5699cf415c689181e Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 22 Jul 2016 17:30:25 +0100 Subject: [PATCH 141/222] Fix 'start chat' button on MemberInfo this/self fail & related scoping Fixes https://github.com/vector-im/vector-web/issues/1844 --- src/components/views/rooms/MemberInfo.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index c439f8b40c..07a7b9398d 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -406,14 +406,14 @@ module.exports = React.createClass({ this.props.onFinished(); } else { - self.setState({ updating: self.state.updating + 1 }); + this.setState({ updating: this.state.updating + 1 }); createRoom({ createOpts: { invite: [this.props.member.userId], }, - }).finally(function() { - self.props.onFinished(); - self.setState({ updating: self.state.updating - 1 }); + }).finally(() => { + this.props.onFinished(); + this.setState({ updating: this.state.updating - 1 }); }).done(); } }, From c89904bc25136a9520f798b4e6cc61335f18b5f1 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Fri, 22 Jul 2016 17:30:31 +0100 Subject: [PATCH 142/222] Initial unstyled mentions state notifier context menu --- src/components/views/rooms/RoomTile.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 48cb345d91..9c39b50427 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -75,7 +75,7 @@ module.exports = React.createClass({ }, onBadgeClicked: function(e) { - var Label = sdk.getComponent('elements.Label'); + var Label = sdk.getComponent('rooms.NotificationStateContextMenu'); var elementRect = e.target.getBoundingClientRect(); var x = elementRect.right; var y = elementRect.top + (elementRect.height / 2); @@ -83,6 +83,7 @@ module.exports = React.createClass({ ContextualMenu.createMenu(Label, { left: x, top: y, + room: this.props.room, onFinished: function() { self.setState({menu: false}); } From a69107f4f1d5351a6bda37bd5c5608fd9b22a96f Mon Sep 17 00:00:00 2001 From: wmwragg Date: Mon, 25 Jul 2016 10:58:43 +0100 Subject: [PATCH 143/222] Trying to get the context menu dohicky to stick when menu open --- src/components/views/rooms/RoomTile.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 9c39b50427..387cca6775 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -75,20 +75,27 @@ module.exports = React.createClass({ }, onBadgeClicked: function(e) { - var Label = sdk.getComponent('rooms.NotificationStateContextMenu'); + console.log("DEBUG: MENU FALSE"); + var Menu = sdk.getComponent('rooms.NotificationStateContextMenu'); var elementRect = e.target.getBoundingClientRect(); var x = elementRect.right; var y = elementRect.top + (elementRect.height / 2); var self = this; - ContextualMenu.createMenu(Label, { + ContextualMenu.createMenu(Menu, { left: x, top: y, room: this.props.room, onFinished: function() { - self.setState({menu: false}); + self.setState({ + menu: false, + badgeHover: false, + }); } }); - this.setState({menu: true}); + this.setState({ + menu: true, + badgeHover: true, + }); }, render: function() { From 8b8486a8d096b285e2b03032aedf5917afcdfa7d Mon Sep 17 00:00:00 2001 From: wmwragg Date: Mon, 25 Jul 2016 14:39:15 +0100 Subject: [PATCH 144/222] Move from +99 to 99+ for more than 99 messages --- 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 47d61cf95a..b39e80cb54 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -105,7 +105,7 @@ module.exports = React.createClass({ if (this.state.badgeHover) { badgeContent = "\u00B7\u00B7\u00B7"; } else if (this.props.highlight || notificationCount > 0) { - var limitedCount = (notificationCount > 99) ? '+99' : notificationCount; + var limitedCount = (notificationCount > 99) ? '99+' : notificationCount; badgeContent = notificationCount ? limitedCount : '!'; } else { badgeContent = '\u200B'; From 587a86441fad3f7172ae195b9367d2ef80713118 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 25 Jul 2016 16:20:03 +0100 Subject: [PATCH 145/222] This may as wel go in createclient --- src/MatrixClientPeg.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 8d155143fc..b7feca60ef 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -71,7 +71,6 @@ class MatrixClientPeg { } } this._createClient(hs_url, is_url, user_id, access_token); - this.matrixClient.setGuest(Boolean(isGuest)); if (localStorage) { try { @@ -146,6 +145,8 @@ class MatrixClientPeg { // we're going to add eventlisteners for each matrix event tile, so the // potential number of event listeners is quite high. this.matrixClient.setMaxListeners(500); + + this.matrixClient.setGuest(Boolean(isGuest)); } } From cbf10bfff6cc1fe06e044c32436ded090f45a2d2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 25 Jul 2016 16:28:28 +0100 Subject: [PATCH 146/222] PR feedback Reintroduce replaceClient so we're not calling replaceUsingAccessToken without access tokens which is a bit silly. Fix bug from previous commit (pass isGuest through) --- src/MatrixClientPeg.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index b7feca60ef..ce4b5ba743 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -55,7 +55,7 @@ class MatrixClientPeg { * Home Server / Identity Server URLs but no credentials */ replaceUsingUrls(hs_url, is_url) { - this.replaceUsingAccessToken(hs_url, is_url); + this._replaceClient(hs_url, is_url); } /** @@ -63,6 +63,10 @@ class MatrixClientPeg { * Home Server / Identity Server URLs and active credentials */ replaceUsingAccessToken(hs_url, is_url, user_id, access_token, isGuest) { + this._replaceClient(hs_url, is_url, user_id, access_token, isGuest); + } + + _replaceClient(hs_url, is_url, user_id, access_token, isGuest) { if (localStorage) { try { localStorage.clear(); @@ -70,7 +74,7 @@ class MatrixClientPeg { console.warn("Error clearing local storage", e); } } - this._createClient(hs_url, is_url, user_id, access_token); + this._createClient(hs_url, is_url, user_id, access_token, isGuest); if (localStorage) { try { @@ -126,7 +130,7 @@ class MatrixClientPeg { } } - _createClient(hs_url, is_url, user_id, access_token) { + _createClient(hs_url, is_url, user_id, access_token, isGuest) { var opts = { baseUrl: hs_url, idBaseUrl: is_url, From 9c9c5ec4588e56ddd3b3ad56619b452454f84e4b Mon Sep 17 00:00:00 2001 From: wmwragg Date: Mon, 25 Jul 2016 17:18:45 +0100 Subject: [PATCH 147/222] Menu context dohickey now sticks when menu shown --- src/components/views/rooms/RoomTile.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 85a19eeb6e..d373cbc69c 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -86,16 +86,10 @@ module.exports = React.createClass({ top: y, room: this.props.room, onFinished: function() { - self.setState({ - menu: false, - badgeHover: false, - }); + self.setState({ menu: false }); } }); - this.setState({ - menu: true, - badgeHover: true, - }); + this.setState({ menu: true }); }, render: function() { @@ -113,6 +107,7 @@ module.exports = React.createClass({ 'mx_RoomTile_read': !(this.props.highlight || notificationCount > 0), 'mx_RoomTile_highlight': this.props.highlight, 'mx_RoomTile_invited': (me && me.membership == 'invite'), + 'mx_RoomTile_menu': this.state.menu, }); // XXX: We should never display raw room IDs, but sometimes the @@ -128,7 +123,7 @@ module.exports = React.createClass({ 'mx_RoomTile_badgeButton': this.state.badgeHover, }); - if (this.state.badgeHover) { + if (this.state.badgeHover || this.state.menu) { badgeContent = "\u00B7\u00B7\u00B7"; } else if (this.props.highlight || notificationCount > 0) { var limitedCount = (notificationCount > 99) ? '99+' : notificationCount; From 8f3e93214c73531808f1153b735c680c370cefed Mon Sep 17 00:00:00 2001 From: wmwragg Date: Mon, 25 Jul 2016 18:02:30 +0100 Subject: [PATCH 148/222] Drawing cheveron with CSS so it can be styled --- src/ContextualMenu.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ContextualMenu.js b/src/ContextualMenu.js index e720b69eda..65d3e98910 100644 --- a/src/ContextualMenu.js +++ b/src/ContextualMenu.js @@ -54,10 +54,10 @@ module.exports = { var chevron = null; if (props.left) { - chevron = + chevron =
position.left = props.left + 8; } else { - chevron = + chevron =
position.right = props.right + 8; } From 477a17b49f0aeb94f5ed803463502138d116cca6 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Tue, 26 Jul 2016 10:39:34 +0100 Subject: [PATCH 149/222] Removed debug statement --- src/components/views/rooms/RoomTile.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index d373cbc69c..28574fcec5 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -75,7 +75,6 @@ module.exports = React.createClass({ }, onBadgeClicked: function(e) { - console.log("DEBUG: MENU FALSE"); var Menu = sdk.getComponent('rooms.NotificationStateContextMenu'); var elementRect = e.target.getBoundingClientRect(); var x = elementRect.right; From 0eb15085e97c55238089f9f8ae3b76eac66e7052 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Tue, 26 Jul 2016 17:25:16 +0100 Subject: [PATCH 150/222] some code tidyup --- src/ContextualMenu.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/ContextualMenu.js b/src/ContextualMenu.js index 65d3e98910..695b8445ab 100644 --- a/src/ContextualMenu.js +++ b/src/ContextualMenu.js @@ -45,7 +45,9 @@ module.exports = { var closeMenu = function() { ReactDOM.unmountComponentAtNode(self.getOrCreateContainer()); - if (props && props.onFinished) props.onFinished.apply(null, arguments); + if (props && props.onFinished) { + props.onFinished.apply(null, arguments); + } }; var position = { @@ -54,10 +56,10 @@ module.exports = { var chevron = null; if (props.left) { - chevron =
+ chevron =
position.left = props.left + 8; } else { - chevron =
+ chevron =
position.right = props.right + 8; } From 4ecf5f637225c21e01652d1e8f733d4f2edc2939 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 26 Jul 2016 17:58:19 +0100 Subject: [PATCH 151/222] Fix bug where vector freezes on power level event Make rate_limited_function accept functions with args so we can just ratelimit the event handler & be done with it. Fixes https://github.com/vector-im/vector-web/issues/1877 --- src/components/structures/RoomView.js | 6 ++++-- src/ratelimitedfunc.js | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index accf96f349..9aa192d9ca 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -482,7 +482,9 @@ module.exports = React.createClass({ } }, - onRoomStateMember: function(ev, state, member) { + // rate limited because a power level change will emit an event for every + // member in the room. + onRoomStateMember: new rate_limited_func(function(ev, state, member) { // ignore if we don't have a room yet if (!this.state.room) { return; @@ -511,7 +513,7 @@ module.exports = React.createClass({ member.userId === this.props.ConferenceHandler.getConferenceUserIdForRoom(member.roomId)) { this._updateConfCallNotification(); } - }, + }, 500), _hasUnsentMessages: function(room) { return this._getUnsentMessages(room).length > 0; diff --git a/src/ratelimitedfunc.js b/src/ratelimitedfunc.js index 453669b477..94626d5ad9 100644 --- a/src/ratelimitedfunc.js +++ b/src/ratelimitedfunc.js @@ -23,13 +23,13 @@ module.exports = function(f, minIntervalMs) { var now = Date.now(); if (self.lastCall < now - minIntervalMs) { - f.apply(this); + f.apply(this, arguments); self.lastCall = now; } else if (self.scheduledCall === undefined) { self.scheduledCall = setTimeout( () => { self.scheduledCall = undefined; - f.apply(this); + f.apply(this, arguments); self.lastCall = now; }, (self.lastCall + minIntervalMs) - now From 31399254b62c49200f3d7cb04ca7ebb40cb52fda Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 26 Jul 2016 18:15:26 +0100 Subject: [PATCH 152/222] Fix onRoomStateMember debouncing Don't have debounced functions take arsg, because they won't be the same for each invocation. --- src/components/structures/RoomView.js | 18 ++++++++++-------- src/ratelimitedfunc.js | 12 ++++++++++-- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 9aa192d9ca..6d35f76529 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -482,9 +482,7 @@ module.exports = React.createClass({ } }, - // rate limited because a power level change will emit an event for every - // member in the room. - onRoomStateMember: new rate_limited_func(function(ev, state, member) { + onRoomStateMember: function(ev, state, member) { // ignore if we don't have a room yet if (!this.state.room) { return; @@ -495,6 +493,15 @@ module.exports = React.createClass({ return; } + if (this.props.ConferenceHandler && + member.userId === this.props.ConferenceHandler.getConferenceUserIdForRoom(member.roomId)) { + this._updateConfCallNotification(); + } + + this._updateRoomMembers(); + }, + + _updateRoomMembers: new rate_limited_func(function() { // a member state changed in this room, refresh the tab complete list this.tabComplete.loadEntries(this.state.room); this._updateAutoComplete(); @@ -508,11 +515,6 @@ module.exports = React.createClass({ joining: false }); } - - if (this.props.ConferenceHandler && - member.userId === this.props.ConferenceHandler.getConferenceUserIdForRoom(member.roomId)) { - this._updateConfCallNotification(); - } }, 500), _hasUnsentMessages: function(room) { diff --git a/src/ratelimitedfunc.js b/src/ratelimitedfunc.js index 94626d5ad9..ed892f4eac 100644 --- a/src/ratelimitedfunc.js +++ b/src/ratelimitedfunc.js @@ -14,6 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ +/** + * 'debounces' a function to only execute every n milliseconds. + * Useful when react-sdk gets many, many events but only wants + * to update the interface once for all of them. + * + * Note that the function must not take arguments, since the args + * could be different for each invocarion of the function. + */ module.exports = function(f, minIntervalMs) { this.lastCall = 0; this.scheduledCall = undefined; @@ -23,13 +31,13 @@ module.exports = function(f, minIntervalMs) { var now = Date.now(); if (self.lastCall < now - minIntervalMs) { - f.apply(this, arguments); + f.apply(this); self.lastCall = now; } else if (self.scheduledCall === undefined) { self.scheduledCall = setTimeout( () => { self.scheduledCall = undefined; - f.apply(this, arguments); + f.apply(this); self.lastCall = now; }, (self.lastCall + minIntervalMs) - now From 09993cd3bcc737c6b33b2d836bebe7342d879092 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 26 Jul 2016 18:19:25 +0100 Subject: [PATCH 153/222] Add comment back --- src/components/structures/RoomView.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 6d35f76529..9fbdb51f11 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -501,6 +501,8 @@ module.exports = React.createClass({ this._updateRoomMembers(); }, + // rate limited because a power level change will emit an event for every + // member in the room. _updateRoomMembers: new rate_limited_func(function() { // a member state changed in this room, refresh the tab complete list this.tabComplete.loadEntries(this.state.room); From dca4702b7b3df510e9b9fd28524e5e5ac0b57448 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Wed, 27 Jul 2016 09:51:50 +0100 Subject: [PATCH 154/222] Fixed a bug where the contextual menu was being incorrectly positioned when zoom in on the webpage --- src/components/views/rooms/EventTile.js | 6 ++++-- src/components/views/rooms/RoomTile.js | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 77be8226a2..129eb120fa 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -251,8 +251,10 @@ module.exports = React.createClass({ onEditClicked: function(e) { var MessageContextMenu = sdk.getComponent('rooms.MessageContextMenu'); var buttonRect = e.target.getBoundingClientRect() - var x = buttonRect.right; - var y = buttonRect.top + (e.target.height / 2); + + // The window X and Y offsets are to adjust position when zoomed in to page + var x = buttonRect.right + window.pageXOffset; + var y = buttonRect.top + (e.target.height / 2) + window.pageYOffset; var self = this; ContextualMenu.createMenu(MessageContextMenu, { mxEvent: this.props.mxEvent, diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 28574fcec5..ce4888e896 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -77,8 +77,9 @@ module.exports = React.createClass({ onBadgeClicked: function(e) { var Menu = sdk.getComponent('rooms.NotificationStateContextMenu'); var elementRect = e.target.getBoundingClientRect(); - var x = elementRect.right; - var y = elementRect.top + (elementRect.height / 2); + // The window X and Y offsets are to adjust position when zoomed in to page + var x = elementRect.right + window.pageXOffset; + var y = elementRect.top + (elementRect.height / 2) + window.pageYOffset; var self = this; ContextualMenu.createMenu(Menu, { left: x, From 4b26ac58afeff66af403ef4110f32e95c75a4d08 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Wed, 27 Jul 2016 10:41:24 +0100 Subject: [PATCH 155/222] Re-modularised the context menus --- src/{ => components/structures}/ContextualMenu.js | 0 src/components/views/rooms/EventTile.js | 2 +- src/components/views/rooms/RoomTile.js | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename src/{ => components/structures}/ContextualMenu.js (100%) diff --git a/src/ContextualMenu.js b/src/components/structures/ContextualMenu.js similarity index 100% rename from src/ContextualMenu.js rename to src/components/structures/ContextualMenu.js diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 129eb120fa..6006ff3ebd 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -249,7 +249,7 @@ module.exports = React.createClass({ }, onEditClicked: function(e) { - var MessageContextMenu = sdk.getComponent('rooms.MessageContextMenu'); + var MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu'); var buttonRect = e.target.getBoundingClientRect() // The window X and Y offsets are to adjust position when zoomed in to page diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index ce4888e896..654bf268e1 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -75,7 +75,7 @@ module.exports = React.createClass({ }, onBadgeClicked: function(e) { - var Menu = sdk.getComponent('rooms.NotificationStateContextMenu'); + var Menu = sdk.getComponent('context_menus.NotificationStateContextMenu'); var elementRect = e.target.getBoundingClientRect(); // The window X and Y offsets are to adjust position when zoomed in to page var x = elementRect.right + window.pageXOffset; From c8df9148b37136a506333ba968bb7dc227116cb0 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 27 Jul 2016 11:35:48 +0100 Subject: [PATCH 156/222] Remove relayoutOnUpdate prop on gemini-scrollbar The latest gemini-scrollbar makes relayoutOnUpdate redundant, so update to it and remove the properties. --- package.json | 2 +- src/components/structures/ScrollPanel.js | 1 - src/components/structures/UserSettings.js | 1 - src/components/views/rooms/MemberList.js | 1 - src/components/views/rooms/RoomList.js | 1 - src/components/views/rooms/SearchableEntityList.js | 1 - 6 files changed, 1 insertion(+), 6 deletions(-) diff --git a/package.json b/package.json index 0d2f6e1a5b..3ea944a066 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "react": "^15.2.1", "react-addons-css-transition-group": "^15.2.1", "react-dom": "^15.2.1", - "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#dbf0abf", + "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef", "sanitize-html": "^1.11.1", "velocity-vector": "vector-im/velocity#059e3b2", "whatwg-fetch": "^1.0.0" diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 63c78d2a3c..9a4c614720 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -540,7 +540,6 @@ module.exports = React.createClass({ // it's not obvious why we have a separate div and ol anyway. return (
    diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 188e140007..75fe1f0825 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -463,7 +463,6 @@ module.exports = React.createClass({

    Profile

    diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index 01a952f1d7..80ae90d984 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -521,7 +521,6 @@ module.exports = React.createClass({
    {inviteMemberListSection} diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index aff03182a1..0c8ac7ed8d 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -325,7 +325,6 @@ module.exports = React.createClass({ return (
    { list } From 1a600b0674da34ff515c785b8c779779ec248301 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 27 Jul 2016 11:38:04 +0100 Subject: [PATCH 157/222] Stop the Avatar classes setting properties on s React apparently now checks the properties which are set on DOM elements, and grumbles noisily about unexpected ones. Update BaseAvatar and RoomAvatar so that they don't set unrelated properties on the DOM elements. --- src/components/views/avatars/BaseAvatar.js | 26 ++++++++++++---------- src/components/views/avatars/RoomAvatar.js | 8 ++++--- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index 66f8e27b88..08d7965455 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -133,32 +133,34 @@ module.exports = React.createClass({ }, render: function() { - var name = this.props.name; - var imageUrl = this.state.imageUrls[this.state.urlsIndex]; + const {name, idName, title, url, urls, width, height, resizeMethod, + defaultToInitialLetter, + ...otherProps} = this.props; + if (imageUrl === this.state.defaultImageUrl) { - var initialLetter = emojifyText(this._getInitialLetter(this.props.name)); + var initialLetter = emojifyText(this._getInitialLetter(name)); return ( - + + alt="" title={title} onError={this.onError} + width={width} height={height} /> ); } return ( + width={width} height={height} + title={title} alt="" + {...otherProps} /> ); } }); diff --git a/src/components/views/avatars/RoomAvatar.js b/src/components/views/avatars/RoomAvatar.js index 129c68ff1b..dcb25eff61 100644 --- a/src/components/views/avatars/RoomAvatar.js +++ b/src/components/views/avatars/RoomAvatar.js @@ -126,11 +126,13 @@ module.exports = React.createClass({ render: function() { var BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); - var roomName = this.props.room ? this.props.room.name : this.props.oobData.name; + var {room, oobData, ...otherProps} = this.props; + + var roomName = room ? room.name : oobData.name; return ( - ); } From 6d141d1a7ba1929799edc109fb1a8b632349c120 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Wed, 27 Jul 2016 11:58:40 +0100 Subject: [PATCH 158/222] Only allow none guests to access the context menu --- src/components/views/rooms/RoomTile.js | 38 +++++++++++++++----------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 654bf268e1..43d6a31738 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -67,7 +67,10 @@ module.exports = React.createClass({ }, badgeOnMouseEnter: function() { - this.setState( { badgeHover : true } ); + // Only allow none guests to access the context menu + if (!MatrixClientPeg.get().isGuest()) { + this.setState( { badgeHover : true } ); + } }, badgeOnMouseLeave: function() { @@ -75,21 +78,24 @@ module.exports = React.createClass({ }, onBadgeClicked: function(e) { - var Menu = sdk.getComponent('context_menus.NotificationStateContextMenu'); - var elementRect = e.target.getBoundingClientRect(); - // The window X and Y offsets are to adjust position when zoomed in to page - var x = elementRect.right + window.pageXOffset; - var y = elementRect.top + (elementRect.height / 2) + window.pageYOffset; - var self = this; - ContextualMenu.createMenu(Menu, { - left: x, - top: y, - room: this.props.room, - onFinished: function() { - self.setState({ menu: false }); - } - }); - this.setState({ menu: true }); + // Only allow none guests to access the context menu + if (!MatrixClientPeg.get().isGuest()) { + var Menu = sdk.getComponent('context_menus.NotificationStateContextMenu'); + var elementRect = e.target.getBoundingClientRect(); + // The window X and Y offsets are to adjust position when zoomed in to page + var x = elementRect.right + window.pageXOffset; + var y = elementRect.top + (elementRect.height / 2) + window.pageYOffset; + var self = this; + ContextualMenu.createMenu(Menu, { + left: x, + top: y, + room: this.props.room, + onFinished: function() { + self.setState({ menu: false }); + } + }); + this.setState({ menu: true }); + } }, render: function() { From 33088421216f2361fe0a922cee9aaeb902a0a3d6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 27 Jul 2016 13:28:23 +0100 Subject: [PATCH 159/222] Fix up reskindex.js path Since npm does not put our *own* 'binaries' on the path, we need to use the full path to it --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0d2f6e1a5b..60fc45db77 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "reskindex": "./reskindex.js" }, "scripts": { - "reskindex": "reskindex -h header", + "reskindex": "./reskindex.js -h header", "build": "babel src -d lib --source-maps --stage 1", "start": "babel src -w -d lib --source-maps --stage 1", "lint": "eslint src/", From 3df746ef143a02103272081f384280961bc18b81 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Wed, 27 Jul 2016 14:16:17 +0100 Subject: [PATCH 160/222] Revert merge up from develop --- package.json | 10 +- src/GuestAccess.js | 51 ------- src/MatrixClientPeg.js | 171 +++++++++++++---------- src/TabComplete.js | 7 +- src/components/structures/RoomView.js | 18 ++- src/components/views/rooms/MemberInfo.js | 8 +- src/ratelimitedfunc.js | 8 ++ 7 files changed, 130 insertions(+), 143 deletions(-) delete mode 100644 src/GuestAccess.js diff --git a/package.json b/package.json index cd81ad7c56..60fc45db77 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "reskindex": "./reskindex.js" }, "scripts": { - "reskindex": "reskindex -h header", + "reskindex": "./reskindex.js -h header", "build": "babel src -d lib --source-maps --stage 1", "start": "babel src -w -d lib --source-maps --stage 1", "lint": "eslint src/", @@ -42,10 +42,10 @@ "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop", "optimist": "^0.6.1", "q": "^1.4.1", - "react": "^15.0.1", - "react-addons-css-transition-group": "^15.1.0", - "react-dom": "^15.0.1", - "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#c3d942e", + "react": "^15.2.1", + "react-addons-css-transition-group": "^15.2.1", + "react-dom": "^15.2.1", + "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#dbf0abf", "sanitize-html": "^1.11.1", "velocity-vector": "vector-im/velocity#059e3b2", "whatwg-fetch": "^1.0.0" diff --git a/src/GuestAccess.js b/src/GuestAccess.js deleted file mode 100644 index ef48d23ded..0000000000 --- a/src/GuestAccess.js +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2015 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -const IS_GUEST_KEY = "matrix-is-guest"; - -class GuestAccess { - - constructor(localStorage) { - this.localStorage = localStorage; - try { - this._isGuest = localStorage.getItem(IS_GUEST_KEY) === "true"; - } - catch (e) {} // don't care - } - - setPeekedRoom(roomId) { - // we purposefully do not persist this to local storage as peeking is - // entirely transient. - this._peekedRoomId = roomId; - } - - getPeekedRoom() { - return this._peekedRoomId; - } - - isGuest() { - return this._isGuest; - } - - markAsGuest(isGuest) { - try { - this.localStorage.setItem(IS_GUEST_KEY, JSON.stringify(isGuest)); - } catch (e) {} // ignore. If they don't do LS, they'll just get a new account. - this._isGuest = isGuest; - this._peekedRoomId = null; - } -} - -module.exports = GuestAccess; diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 7c1c5b34d7..ce4b5ba743 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -16,13 +16,9 @@ limitations under the License. 'use strict'; -// A thing that holds your Matrix Client -var Matrix = require("matrix-js-sdk"); -var GuestAccess = require("./GuestAccess"); +import Matrix from 'matrix-js-sdk'; -let matrixClient: MatrixClient = null; - -var localStorage = window.localStorage; +const localStorage = window.localStorage; function deviceId() { // XXX: is Math.random()'s deterministicity a problem here? @@ -35,82 +31,42 @@ function deviceId() { return id; } -function createClientForPeg(hs_url, is_url, user_id, access_token, guestAccess) { - var opts = { - baseUrl: hs_url, - idBaseUrl: is_url, - accessToken: access_token, - userId: user_id, - timelineSupport: true, - }; - - if (localStorage) { - opts.sessionStore = new Matrix.WebStorageSessionStore(localStorage); - opts.deviceId = deviceId(); - } - - matrixClient = Matrix.createClient(opts); - - // we're going to add eventlisteners for each matrix event tile, so the - // potential number of event listeners is quite high. - matrixClient.setMaxListeners(500); - - if (guestAccess) { - console.log("Guest: %s", guestAccess.isGuest()); - matrixClient.setGuest(guestAccess.isGuest()); - var peekedRoomId = guestAccess.getPeekedRoom(); - if (peekedRoomId) { - console.log("Peeking in room %s", peekedRoomId); - matrixClient.peekInRoom(peekedRoomId); - } - } -} - -if (localStorage) { - var hs_url = localStorage.getItem("mx_hs_url"); - var is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org'; - var access_token = localStorage.getItem("mx_access_token"); - var user_id = localStorage.getItem("mx_user_id"); - var guestAccess = new GuestAccess(localStorage); - if (access_token && user_id && hs_url) { - console.log("Restoring session for %s", user_id); - createClientForPeg(hs_url, is_url, user_id, access_token, guestAccess); - } - else { - console.log("Session not found."); - } -} - -class MatrixClient { - - constructor(guestAccess) { - this.guestAccess = guestAccess; +/** + * Wrapper object for handling the js-sdk Matrix Client object in the react-sdk + * Handles the creation/initialisation of client objects. + * This module provides a singleton instance of this class so the 'current' + * Matrix Client object is available easily. + */ +class MatrixClientPeg { + constructor() { + this.matrixClient = null; } get(): MatrixClient { - return matrixClient; + return this.matrixClient; } unset() { - matrixClient = null; + this.matrixClient = null; } - // FIXME, XXX: this all seems very convoluted :( - // - // Why do we have this peg wrapper rather than just MatrixClient.get()? - // Why do we name MatrixClient as MatrixClientPeg when we export it? - // - // -matthew - + /** + * Replace this MatrixClientPeg's client with a client instance that has + * Home Server / Identity Server URLs but no credentials + */ replaceUsingUrls(hs_url, is_url) { - this.replaceClient(hs_url, is_url); + this._replaceClient(hs_url, is_url); } + /** + * Replace this MatrixClientPeg's client with a client instance that has + * Home Server / Identity Server URLs and active credentials + */ replaceUsingAccessToken(hs_url, is_url, user_id, access_token, isGuest) { - this.replaceClient(hs_url, is_url, user_id, access_token, isGuest); + this._replaceClient(hs_url, is_url, user_id, access_token, isGuest); } - replaceClient(hs_url, is_url, user_id, access_token, isGuest) { + _replaceClient(hs_url, is_url, user_id, access_token, isGuest) { if (localStorage) { try { localStorage.clear(); @@ -118,15 +74,19 @@ class MatrixClient { console.warn("Error clearing local storage", e); } } - this.guestAccess.markAsGuest(Boolean(isGuest)); - createClientForPeg(hs_url, is_url, user_id, access_token, this.guestAccess); + this._createClient(hs_url, is_url, user_id, access_token, isGuest); + if (localStorage) { try { localStorage.setItem("mx_hs_url", hs_url); localStorage.setItem("mx_is_url", is_url); - localStorage.setItem("mx_user_id", user_id); - localStorage.setItem("mx_access_token", access_token); - console.log("Session persisted for %s", user_id); + + if (user_id !== undefined && access_token !== undefined) { + localStorage.setItem("mx_user_id", user_id); + localStorage.setItem("mx_access_token", access_token); + localStorage.setItem("mx_is_guest", JSON.stringify(isGuest)); + console.log("Session persisted for %s", user_id); + } } catch (e) { console.warn("Error using local storage: can't persist session!", e); } @@ -134,9 +94,68 @@ class MatrixClient { console.warn("No local storage available: can't persist session!"); } } + + getCredentials() { + return [ + this.matrixClient.baseUrl, + this.matrixClient.idBaseUrl, + this.matrixClient.credentials.userId, + this.matrixClient.getAccessToken(), + this.matrixClient.isGuest(), + ]; + } + + tryRestore() { + if (localStorage) { + const hs_url = localStorage.getItem("mx_hs_url"); + const is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org'; + const access_token = localStorage.getItem("mx_access_token"); + const user_id = localStorage.getItem("mx_user_id"); + + let is_guest; + if (localStorage.getItem("mx_is_guest") !== null) { + is_guest = localStorage.getItem("mx_is_guest") === "true"; + } else { + // legacy key name + is_guest = localStorage.getItem("matrix-is-guest") === "true"; + } + + if (access_token && user_id && hs_url) { + console.log("Restoring session for %s", user_id); + this._createClient(hs_url, is_url, user_id, access_token); + this.matrixClient.setGuest(is_guest); + } else { + console.log("Session not found."); + } + } + } + + _createClient(hs_url, is_url, user_id, access_token, isGuest) { + var opts = { + baseUrl: hs_url, + idBaseUrl: is_url, + accessToken: access_token, + userId: user_id, + timelineSupport: true, + }; + + if (localStorage) { + opts.sessionStore = new Matrix.WebStorageSessionStore(localStorage); + opts.deviceId = deviceId(); + } + + this.matrixClient = Matrix.createClient(opts); + + // we're going to add eventlisteners for each matrix event tile, so the + // potential number of event listeners is quite high. + this.matrixClient.setMaxListeners(500); + + this.matrixClient.setGuest(Boolean(isGuest)); + } } -if (!global.mxMatrixClient) { - global.mxMatrixClient = new MatrixClient(new GuestAccess(localStorage)); +if (!global.mxMatrixClientPeg) { + global.mxMatrixClientPeg = new MatrixClientPeg(); + global.mxMatrixClientPeg.tryRestore(); } -module.exports = global.mxMatrixClient; +module.exports = global.mxMatrixClientPeg; diff --git a/src/TabComplete.js b/src/TabComplete.js index 0ec0b77802..65441c9381 100644 --- a/src/TabComplete.js +++ b/src/TabComplete.js @@ -341,7 +341,12 @@ class TabComplete { } if (a.kind == 'member') { - return this.memberTabOrder[b.member.userId] - this.memberTabOrder[a.member.userId]; + let orderA = this.memberTabOrder[a.member.userId]; + let orderB = this.memberTabOrder[b.member.userId]; + if (orderA === undefined) orderA = -1; + if (orderB === undefined) orderB = -1; + + return orderB - orderA; } // anything else we have no ordering for diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index accf96f349..9fbdb51f11 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -493,6 +493,17 @@ module.exports = React.createClass({ return; } + if (this.props.ConferenceHandler && + member.userId === this.props.ConferenceHandler.getConferenceUserIdForRoom(member.roomId)) { + this._updateConfCallNotification(); + } + + this._updateRoomMembers(); + }, + + // rate limited because a power level change will emit an event for every + // member in the room. + _updateRoomMembers: new rate_limited_func(function() { // a member state changed in this room, refresh the tab complete list this.tabComplete.loadEntries(this.state.room); this._updateAutoComplete(); @@ -506,12 +517,7 @@ module.exports = React.createClass({ joining: false }); } - - if (this.props.ConferenceHandler && - member.userId === this.props.ConferenceHandler.getConferenceUserIdForRoom(member.roomId)) { - this._updateConfCallNotification(); - } - }, + }, 500), _hasUnsentMessages: function(room) { return this._getUnsentMessages(room).length > 0; diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index c439f8b40c..07a7b9398d 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -406,14 +406,14 @@ module.exports = React.createClass({ this.props.onFinished(); } else { - self.setState({ updating: self.state.updating + 1 }); + this.setState({ updating: this.state.updating + 1 }); createRoom({ createOpts: { invite: [this.props.member.userId], }, - }).finally(function() { - self.props.onFinished(); - self.setState({ updating: self.state.updating - 1 }); + }).finally(() => { + this.props.onFinished(); + this.setState({ updating: this.state.updating - 1 }); }).done(); } }, diff --git a/src/ratelimitedfunc.js b/src/ratelimitedfunc.js index 453669b477..ed892f4eac 100644 --- a/src/ratelimitedfunc.js +++ b/src/ratelimitedfunc.js @@ -14,6 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ +/** + * 'debounces' a function to only execute every n milliseconds. + * Useful when react-sdk gets many, many events but only wants + * to update the interface once for all of them. + * + * Note that the function must not take arguments, since the args + * could be different for each invocarion of the function. + */ module.exports = function(f, minIntervalMs) { this.lastCall = 0; this.scheduledCall = undefined; From 3cb3dd96a64d6929bc161e458e3d61e67f89ee9d Mon Sep 17 00:00:00 2001 From: wmwragg Date: Wed, 27 Jul 2016 14:23:37 +0100 Subject: [PATCH 161/222] Added the moved ContextualMenu to the components list --- src/component-index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/component-index.js b/src/component-index.js index 5fadb18b6a..7dfdcd6f8b 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -25,6 +25,7 @@ limitations under the License. */ module.exports.components = {}; +module.exports.components['structures.ContextualMenu'] = require('./components/structures/ContextualMenu'); module.exports.components['structures.CreateRoom'] = require('./components/structures/CreateRoom'); module.exports.components['structures.MatrixChat'] = require('./components/structures/MatrixChat'); module.exports.components['structures.MessagePanel'] = require('./components/structures/MessagePanel'); From 0660b9feff800761731d06a60246c7be8e9165ee Mon Sep 17 00:00:00 2001 From: wmwragg Date: Wed, 27 Jul 2016 14:49:10 +0100 Subject: [PATCH 162/222] Fixes to properly point to the moved components --- .DS_Store | Bin 0 -> 6148 bytes src/components/structures/MatrixChat.js | 2 +- src/components/views/rooms/EventTile.js | 2 +- src/components/views/rooms/RoomTile.js | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 Date: Wed, 27 Jul 2016 15:41:24 +0100 Subject: [PATCH 163/222] formatting PR feedback --- src/components/views/avatars/BaseAvatar.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index 08d7965455..f0a36c6608 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -135,9 +135,11 @@ module.exports = React.createClass({ render: function() { var imageUrl = this.state.imageUrls[this.state.urlsIndex]; - const {name, idName, title, url, urls, width, height, resizeMethod, - defaultToInitialLetter, - ...otherProps} = this.props; + const { + name, idName, title, url, urls, width, height, resizeMethod, + defaultToInitialLetter, + ...otherProps + } = this.props; if (imageUrl === this.state.defaultImageUrl) { var initialLetter = emojifyText(this._getInitialLetter(name)); From 6e7e091117ba37f059fc8801e82020cd8eb1be09 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Wed, 27 Jul 2016 16:09:07 +0100 Subject: [PATCH 164/222] Refactor so that chevron and menu can be positioned independantly --- src/components/structures/ContextualMenu.js | 19 +++++++++++++------ src/components/views/rooms/EventTile.js | 3 ++- src/components/views/rooms/RoomTile.js | 3 ++- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/components/structures/ContextualMenu.js b/src/components/structures/ContextualMenu.js index 695b8445ab..c6f8e32b11 100644 --- a/src/components/structures/ContextualMenu.js +++ b/src/components/structures/ContextualMenu.js @@ -17,6 +17,7 @@ limitations under the License. 'use strict'; +var classNames = require('classnames'); var React = require('react'); var ReactDOM = require('react-dom'); @@ -51,25 +52,31 @@ module.exports = { }; var position = { - top: props.top - 20, + top: props.top, }; var chevron = null; if (props.left) { - chevron =
    - position.left = props.left + 8; + chevron =
    + position.left = props.left; } else { chevron =
    - position.right = props.right + 8; + position.right = props.right; } var className = 'mx_ContextualMenu_wrapper'; + var menuClasses = classNames({ + 'mx_ContextualMenu': true, + 'mx_ContextualMenu_left': props.left, + 'mx_ContextualMenu_right': !props.left, + }); + // FIXME: If a menu uses getDefaultProps it clobbers the onFinished // property set here so you can't close the menu from a button click! var menu = ( -
    -
    +
    +
    {chevron}
    diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index a24c597ad4..7945debd1a 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -254,9 +254,10 @@ module.exports = React.createClass({ // The window X and Y offsets are to adjust position when zoomed in to page var x = buttonRect.right + window.pageXOffset; - var y = buttonRect.top + (e.target.height / 2) + window.pageYOffset; + var y = (buttonRect.top + (e.target.height / 2) + window.pageYOffset) - 19; var self = this; ContextualMenu.createMenu(MessageContextMenu, { + chevronOffset: 10, mxEvent: this.props.mxEvent, left: x, top: y, diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index a3ebf3ed45..13b20118e3 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -84,9 +84,10 @@ module.exports = React.createClass({ var elementRect = e.target.getBoundingClientRect(); // The window X and Y offsets are to adjust position when zoomed in to page var x = elementRect.right + window.pageXOffset; - var y = elementRect.top + (elementRect.height / 2) + window.pageYOffset; + var y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset) - 43; var self = this; ContextualMenu.createMenu(Menu, { + chevronOffset: 35, left: x, top: y, room: this.props.room, From 8246d9148cc4d1345d28be54c979021e686773e1 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Wed, 27 Jul 2016 17:43:48 +0100 Subject: [PATCH 165/222] Initial context menu with all it's elements --- src/components/structures/ContextualMenu.js | 11 ++++++++++- src/components/views/rooms/RoomTile.js | 2 ++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/structures/ContextualMenu.js b/src/components/structures/ContextualMenu.js index c6f8e32b11..bf63227283 100644 --- a/src/components/structures/ContextualMenu.js +++ b/src/components/structures/ContextualMenu.js @@ -72,11 +72,20 @@ module.exports = { 'mx_ContextualMenu_right': !props.left, }); + var menuSize = {}; + if (props.menuWidth) { + menuSize.width = props.menuWidth; + } + + if (props.menuHeight) { + menuSize.height = props.menuHeight; + } + // FIXME: If a menu uses getDefaultProps it clobbers the onFinished // property set here so you can't close the menu from a button click! var menu = (
    -
    +
    {chevron}
    diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 13b20118e3..a500f18cb1 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -87,6 +87,8 @@ module.exports = React.createClass({ var y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset) - 43; var self = this; ContextualMenu.createMenu(Menu, { + menuWidth: 188, + menuHeight: 126, chevronOffset: 35, left: x, top: y, From 46a643ac38b5b9b93e368c64c5f9752477650448 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Wed, 27 Jul 2016 18:10:45 +0100 Subject: [PATCH 166/222] Now the text spaceing is correct need to align cheveron --- 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 a500f18cb1..1f77c6a110 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -89,7 +89,7 @@ module.exports = React.createClass({ ContextualMenu.createMenu(Menu, { menuWidth: 188, menuHeight: 126, - chevronOffset: 35, + chevronOffset: 45, left: x, top: y, room: this.props.room, From 867b14d2cacd6db7eb5a1e1404b40081d44eb836 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Wed, 27 Jul 2016 18:14:46 +0100 Subject: [PATCH 167/222] Reposition context menu now that the styling is final --- 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 1f77c6a110..a0af243ea4 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -84,7 +84,7 @@ module.exports = React.createClass({ var elementRect = e.target.getBoundingClientRect(); // The window X and Y offsets are to adjust position when zoomed in to page var x = elementRect.right + window.pageXOffset; - var y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset) - 43; + var y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset) - 53; var self = this; ContextualMenu.createMenu(Menu, { menuWidth: 188, From bc902a9741c4d72e3bf9c8c7079f5564fefab5a3 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Thu, 28 Jul 2016 14:32:59 +0100 Subject: [PATCH 168/222] Tidy up of the contextual menu refactor --- src/components/structures/ContextualMenu.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/structures/ContextualMenu.js b/src/components/structures/ContextualMenu.js index bf63227283..114bdaad1b 100644 --- a/src/components/structures/ContextualMenu.js +++ b/src/components/structures/ContextualMenu.js @@ -28,6 +28,12 @@ var ReactDOM = require('react-dom'); module.exports = { ContextualMenuContainerId: "mx_ContextualMenu_Container", + propTypes: { + menuWidth: React.PropTypes.number, + menuHeight: React.PropTypes.number, + chevronOffset: React.PropTypes.number, + }, + getOrCreateContainer: function() { var container = document.getElementById(this.ContextualMenuContainerId); @@ -55,12 +61,16 @@ module.exports = { top: props.top, }; + var chevronOffset = { + top: props.cheveronOffset, + } + var chevron = null; if (props.left) { - chevron =
    + chevron =
    position.left = props.left; } else { - chevron =
    + chevron =
    position.right = props.right; } From 94350bc780f17bba6b9e050e876c1beb761e3520 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Thu, 28 Jul 2016 15:51:46 +0100 Subject: [PATCH 169/222] Fixed spelling mistake --- src/components/structures/ContextualMenu.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/ContextualMenu.js b/src/components/structures/ContextualMenu.js index 114bdaad1b..fcfc5d5e50 100644 --- a/src/components/structures/ContextualMenu.js +++ b/src/components/structures/ContextualMenu.js @@ -62,7 +62,7 @@ module.exports = { }; var chevronOffset = { - top: props.cheveronOffset, + top: props.chevronOffset, } var chevron = null; From 5889beacf3452883e9c7c1d197f9fd01f4fd651a Mon Sep 17 00:00:00 2001 From: wmwragg Date: Thu, 28 Jul 2016 17:24:58 +0100 Subject: [PATCH 170/222] Mute state now handled correctly --- src/components/views/rooms/RoomTile.js | 68 +++++++++++++++++++++----- 1 file changed, 57 insertions(+), 11 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index a0af243ea4..48b2ea84f4 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -44,17 +44,48 @@ module.exports = React.createClass({ }, getInitialState: function() { + var areNotifsMuted = false; + var cli = MatrixClientPeg.get(); + if (!cli.isGuest()) { + var roomPushRule = cli.getRoomPushRule("global", this.props.room.roomId); + if (roomPushRule) { + if (0 <= roomPushRule.actions.indexOf("dont_notify")) { + areNotifsMuted = true; + } + } + } + return({ hover : false, badgeHover : false, menu: false, + areNotifsMuted: areNotifsMuted, }); }, + onAction: function(payload) { + switch (payload.action) { + case 'notification_change': + // Is the notificaion about this room + if (payload.roomId === this.props.room.roomId) { + this.setState( { areNotifsMuted : payload.isMuted }); + } + break; + } + }, + + componentDidMount: function() { + this.dispatcherRef = dis.register(this.onAction); + }, + + componentWillUnmount: function() { + dis.unregister(this.dispatcherRef); + }, + onClick: function() { dis.dispatch({ action: 'view_room', - room_id: this.props.room.roomId + room_id: this.props.room.roomId, }); }, @@ -119,6 +150,17 @@ module.exports = React.createClass({ 'mx_RoomTile_menu': this.state.menu, }); + var avatarClasses = classNames({ + 'mx_RoomTile_avatar': true, + 'mx_RoomTile_mute': this.state.areNotifsMuted, + }); + + var badgeClasses = classNames({ + 'mx_RoomTile_badge': true, + 'mx_RoomTile_badgeButton': this.state.badgeHover || this.state.menu, + 'mx_RoomTile_badgeMute': this.state.areNotifsMuted, + }); + // XXX: We should never display raw room IDs, but sometimes the // room name js sdk gives is undefined (cannot repro this -- k) var name = this.props.room.name || this.props.room.roomId; @@ -127,11 +169,6 @@ module.exports = React.createClass({ var badge; var badgeContent; - var badgeClasses = classNames({ - 'mx_RoomTile_badge': true, - 'mx_RoomTile_badgeButton': this.state.badgeHover, - }); - if (this.state.badgeHover || this.state.menu) { badgeContent = "\u00B7\u00B7\u00B7"; } else if (this.props.highlight || notificationCount > 0) { @@ -141,19 +178,28 @@ module.exports = React.createClass({ badgeContent = '\u200B'; } - badge =
    { badgeContent }
    ; + if (this.state.areNotifsMuted && !(this.state.badgeHover || this.state.menu)) { + badge =
    ; + } else { + badge =
    { badgeContent }
    ; + } var label; var tooltip; if (!this.props.collapsed) { - var className = 'mx_RoomTile_name' + (this.props.isInvite ? ' mx_RoomTile_invite' : ''); + var nameClasses = classNames({ + 'mx_RoomTile_name': true, + 'mx_RoomTile_invite': this.props.isInvite, + 'mx_RoomTile_mute': this.state.areNotifsMuted, + }); + let nameHTML = emojifyText(name); if (this.props.selected) { name = ; - label =
    { name }
    ; + label =
    { name }
    ; } else { - label =
    ; + label =
    ; } } else if (this.state.hover) { @@ -177,7 +223,7 @@ module.exports = React.createClass({ return connectDragSource(connectDropTarget(
    -
    +
    { label } From 38f504bdcbaf6c57bdc7ab6ab8512154cff76efb Mon Sep 17 00:00:00 2001 From: wmwragg Date: Fri, 29 Jul 2016 11:10:16 +0100 Subject: [PATCH 171/222] Hide tooltip when badge clicked and collapsed --- src/components/views/rooms/RoomTile.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 48b2ea84f4..3f6704c2a2 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -111,6 +111,12 @@ module.exports = React.createClass({ onBadgeClicked: function(e) { // Only allow none guests to access the context menu if (!MatrixClientPeg.get().isGuest()) { + + // If the badge is clicked, then no longer show tooltip + if (this.props.collapsed) { + this.setState({ hover: false }); + } + var Menu = sdk.getComponent('context_menus.NotificationStateContextMenu'); var elementRect = e.target.getBoundingClientRect(); // The window X and Y offsets are to adjust position when zoomed in to page From 3194c5c61d0e55f0964eaf5d16912548fc9b2f96 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 29 Jul 2016 17:35:48 +0100 Subject: [PATCH 172/222] Factor EditableTextContainer out of ChangeDisplayName Take the non-displayname-specific bits out of ChangeDisplayName into a new EditableTextContainer, so that we can reuse the logic elsewhere. --- src/component-index.js | 1 + src/components/views/elements/EditableText.js | 10 +- .../views/elements/EditableTextContainer.js | 147 ++++++++++++++++++ .../views/settings/ChangeDisplayName.js | 91 ++--------- 4 files changed, 169 insertions(+), 80 deletions(-) create mode 100644 src/components/views/elements/EditableTextContainer.js diff --git a/src/component-index.js b/src/component-index.js index 5fadb18b6a..e250838cc4 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -51,6 +51,7 @@ module.exports.components['views.dialogs.QuestionDialog'] = require('./component module.exports.components['views.dialogs.SetDisplayNameDialog'] = require('./components/views/dialogs/SetDisplayNameDialog'); module.exports.components['views.dialogs.TextInputDialog'] = require('./components/views/dialogs/TextInputDialog'); module.exports.components['views.elements.EditableText'] = require('./components/views/elements/EditableText'); +module.exports.components['views.elements.EditableTextContainer'] = require('./components/views/elements/EditableTextContainer'); module.exports.components['views.elements.PowerSelector'] = require('./components/views/elements/PowerSelector'); module.exports.components['views.elements.ProgressBar'] = require('./components/views/elements/ProgressBar'); module.exports.components['views.elements.TintableSvg'] = require('./components/views/elements/TintableSvg'); diff --git a/src/components/views/elements/EditableText.js b/src/components/views/elements/EditableText.js index 9218fe820e..15118f249e 100644 --- a/src/components/views/elements/EditableText.js +++ b/src/components/views/elements/EditableText.js @@ -49,6 +49,8 @@ module.exports = React.createClass({ label: '', placeholder: '', editable: true, + className: "mx_EditableText", + placeholderClassName: "mx_EditableText_placeholder", }; }, @@ -92,7 +94,7 @@ module.exports = React.createClass({ this.refs.editable_div.textContent = this.value; this.refs.editable_div.setAttribute("class", this.props.className); this.placeholder = false; - } + } }, getValue: function() { @@ -101,7 +103,7 @@ module.exports = React.createClass({ setValue: function(value) { this.value = value; - this.showPlaceholder(!this.value); + this.showPlaceholder(!this.value); }, edit: function() { @@ -125,7 +127,7 @@ module.exports = React.createClass({ onKeyDown: function(ev) { // console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder); - + if (this.placeholder) { this.showPlaceholder(false); } @@ -173,7 +175,7 @@ module.exports = React.createClass({ var range = document.createRange(); range.setStart(node, 0); range.setEnd(node, node.length); - + var sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); diff --git a/src/components/views/elements/EditableTextContainer.js b/src/components/views/elements/EditableTextContainer.js new file mode 100644 index 0000000000..9e863629f2 --- /dev/null +++ b/src/components/views/elements/EditableTextContainer.js @@ -0,0 +1,147 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import sdk from '../../../index'; +import q from 'q'; + +/** + * A component which wraps an EditableText, with a spinner while updates take + * place. + * + * Parent components should supply an 'onSubmit' callback which returns a + * promise; a spinner is shown until the promise resolves. + * + * The parent can also supply a 'getIntialValue' callback, which works in a + * similarly asynchronous way. If this is not provided, the initial value is + * taken from the 'initialValue' property. + */ +export default class EditableTextContainer extends React.Component { + constructor(props, context) { + super(props, context); + + this._unmounted = false; + this.state = { + busy: false, + errorString: null, + value: props.initialValue, + }; + this._onValueChanged=this._onValueChanged.bind(this); + } + + componentWillMount() { + if (this.props.getInitialValue === undefined) { + // use whatever was given in the initialValue property. + return; + } + + this.setState({busy: true}); + + this.props.getInitialValue().done( + (result) => { + if (this._unmounted) { return; } + this.setState({ + busy: false, + value: result, + }); + }, + (error) => { + if (this._unmounted) { return; } + this.setState({ + errorString: error.toString(), + busy: false, + }); + } + ); + } + + componentWillUnmount() { + this._unmounted = true; + } + + _onValueChanged(value, shouldSubmit) { + if (!shouldSubmit) { + return; + } + + this.setState({ + busy: true, + errorString: null, + }); + + this.props.onSubmit(value).done( + () => { + if (this._unmounted) { return; } + this.setState({ + busy: false, + value: value, + }); + }, + (error) => { + if (this._unmounted) { return; } + this.setState({ + errorString: error.toString(), + busy: false, + }); + } + ); + } + + render() { + if (this.state.busy) { + var Loader = sdk.getComponent("elements.Spinner"); + return ( + + ); + } else if (this.state.errorString) { + return ( +
    {this.state.errorString}
    + ); + } else { + var EditableText = sdk.getComponent('elements.EditableText'); + return ( + + ); + } + } + +} + +EditableTextContainer.propTypes = { + /* callback to retrieve the initial value. */ + getInitialValue: React.PropTypes.func, + + /* initial value; used if getInitialValue is not given */ + initialValue: React.PropTypes.string, + + /* placeholder text to use when the value is empty (and not being + * edited) */ + placeholder: React.PropTypes.string, + + /* callback to update the value. Called with a single argument: the new + * value. */ + onSubmit: React.PropTypes.func, +}; + + +EditableTextContainer.defaultProps = { + initialValue: "", + placeholder: "", + onSubmit: function(v) {return q(); }, +}; diff --git a/src/components/views/settings/ChangeDisplayName.js b/src/components/views/settings/ChangeDisplayName.js index 799a8b9634..26b6c2f830 100644 --- a/src/components/views/settings/ChangeDisplayName.js +++ b/src/components/views/settings/ChangeDisplayName.js @@ -21,29 +21,10 @@ var MatrixClientPeg = require("../../../MatrixClientPeg"); module.exports = React.createClass({ displayName: 'ChangeDisplayName', - propTypes: { - onFinished: React.PropTypes.func - }, - getDefaultProps: function() { - return { - onFinished: function() {}, - }; - }, - - getInitialState: function() { - return { - busy: false, - errorString: null - } - }, - - componentWillMount: function() { + _getDisplayName: function() { var cli = MatrixClientPeg.get(); - this.setState({busy: true}); - var self = this; - cli.getProfileInfo(cli.credentials.userId).done(function(result) { - + return cli.getProfileInfo(cli.credentials.userId).then(function(result) { var displayname = result.displayname; if (!displayname) { if (MatrixClientPeg.get().isGuest()) { @@ -53,68 +34,26 @@ module.exports = React.createClass({ displayname = MatrixClientPeg.get().getUserIdLocalpart(); } } - - self.setState({ - displayName: displayname, - busy: false - }); + return displayname; }, function(error) { - self.setState({ - errorString: "Failed to fetch display name", - busy: false - }); + throw new Error("Failed to fetch display name"); }); }, - changeDisplayname: function(new_displayname) { - this.setState({ - busy: true, - errorString: null, - }) - - var self = this; - MatrixClientPeg.get().setDisplayName(new_displayname).then(function() { - self.setState({ - busy: false, - displayName: new_displayname - }); - }, function(error) { - self.setState({ - busy: false, - errorString: "Failed to set display name" - }); + _changeDisplayName: function(new_displayname) { + var cli = MatrixClientPeg.get(); + return cli.setDisplayName(new_displayname).catch(function(e) { + throw new Error("Failed to set display name"); }); }, - edit: function() { - this.refs.displayname_edit.edit() - }, - - onValueChanged: function(new_value, shouldSubmit) { - if (shouldSubmit) { - this.changeDisplayname(new_value); - } - }, - render: function() { - if (this.state.busy) { - var Loader = sdk.getComponent("elements.Spinner"); - return ( - - ); - } else if (this.state.errorString) { - return ( -
    {this.state.errorString}
    - ); - } else { - var EditableText = sdk.getComponent('elements.EditableText'); - return ( - - ); - } + var EditableTextContainer = sdk.getComponent('elements.EditableTextContainer'); + return ( + + ); } }); From 229664a624eac2768c6522511431188e00fc81d0 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Fri, 29 Jul 2016 17:49:42 +0100 Subject: [PATCH 173/222] first pass on css re-write --- src/components/views/rooms/RoomTile.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 3f6704c2a2..e74647f717 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -229,11 +229,15 @@ module.exports = React.createClass({ return connectDragSource(connectDropTarget(
    -
    - +
    +
    + +
    { label } - { badge } +
    + { badge } +
    { incomingCallBox } { tooltip }
    From 1487c600ee5510ba8dd9bd3a2d6166d9d98224bb Mon Sep 17 00:00:00 2001 From: wmwragg Date: Fri, 29 Jul 2016 17:53:18 +0100 Subject: [PATCH 174/222] Revert so that the --- src/components/views/rooms/RoomTile.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index e74647f717..3f6704c2a2 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -229,15 +229,11 @@ module.exports = React.createClass({ return connectDragSource(connectDropTarget(
    -
    -
    - -
    +
    +
    { label } -
    - { badge } -
    + { badge } { incomingCallBox } { tooltip }
    From 398e56c9f9eec489be6b80dd9f208e5185742485 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Sat, 30 Jul 2016 12:28:10 +0100 Subject: [PATCH 175/222] Fixed bug where the long hover tooltip for a room name show an object rather than the name --- 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 3f6704c2a2..e31514e015 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -201,9 +201,9 @@ module.exports = React.createClass({ let nameHTML = emojifyText(name); if (this.props.selected) { - name = ; + let nameSelected = ; - label =
    { name }
    ; + label =
    { nameSelected }
    ; } else { label =
    ; } From d16aa276e2a29ea0ece9f5e39e0ef7afe7ed4879 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Sat, 30 Jul 2016 12:52:39 +0100 Subject: [PATCH 176/222] Positional tweaks for the name, badge and context menu, to better match design in both normal and collapsed states --- 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 e31514e015..d6b33b9409 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -120,7 +120,7 @@ module.exports = React.createClass({ var Menu = sdk.getComponent('context_menus.NotificationStateContextMenu'); var elementRect = e.target.getBoundingClientRect(); // The window X and Y offsets are to adjust position when zoomed in to page - var x = elementRect.right + window.pageXOffset; + var x = elementRect.right + window.pageXOffset + 3; var y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset) - 53; var self = this; ContextualMenu.createMenu(Menu, { From 9333793a7c14ef7a57839d86d71652dbda45d803 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 1 Aug 2016 10:49:51 +0100 Subject: [PATCH 177/222] Fix code style --- src/components/views/elements/EditableTextContainer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/EditableTextContainer.js b/src/components/views/elements/EditableTextContainer.js index 9e863629f2..b17f1b417d 100644 --- a/src/components/views/elements/EditableTextContainer.js +++ b/src/components/views/elements/EditableTextContainer.js @@ -39,7 +39,7 @@ export default class EditableTextContainer extends React.Component { errorString: null, value: props.initialValue, }; - this._onValueChanged=this._onValueChanged.bind(this); + this._onValueChanged = this._onValueChanged.bind(this); } componentWillMount() { From c071bc54f9c78ff9d093318cb30ee5da0db9444e Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 1 Aug 2016 13:42:29 +0100 Subject: [PATCH 178/222] Implement device management UI View/rename/delete. --- src/component-index.js | 2 + src/components/structures/UserSettings.js | 71 +++++---- src/components/views/settings/DevicesPanel.js | 138 ++++++++++++++++++ .../views/settings/DevicesPanelEntry.js | 134 +++++++++++++++++ 4 files changed, 317 insertions(+), 28 deletions(-) create mode 100644 src/components/views/settings/DevicesPanel.js create mode 100644 src/components/views/settings/DevicesPanelEntry.js diff --git a/src/component-index.js b/src/component-index.js index e250838cc4..19a016aec8 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -107,6 +107,8 @@ module.exports.components['views.rooms.UserTile'] = require('./components/views/ module.exports.components['views.settings.ChangeAvatar'] = require('./components/views/settings/ChangeAvatar'); module.exports.components['views.settings.ChangeDisplayName'] = require('./components/views/settings/ChangeDisplayName'); module.exports.components['views.settings.ChangePassword'] = require('./components/views/settings/ChangePassword'); +module.exports.components['views.settings.DevicesPanel'] = require('./components/views/settings/DevicesPanel'); +module.exports.components['views.settings.DevicesPanelEntry'] = require('./components/views/settings/DevicesPanelEntry'); module.exports.components['views.settings.EnableNotificationsButton'] = require('./components/views/settings/EnableNotificationsButton'); module.exports.components['views.voip.CallView'] = require('./components/views/voip/CallView'); module.exports.components['views.voip.IncomingCallBox'] = require('./components/views/voip/IncomingCallBox'); diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 75fe1f0825..6555668ff4 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -319,7 +319,7 @@ module.exports = React.createClass({ ); }, - _renderDeviceInfo: function() { + _renderCryptoInfo: function() { if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) { return null; } @@ -340,6 +340,45 @@ module.exports = React.createClass({ ); }, + _renderDevicesPanel: function() { + if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) { + return null; + } + var DevicesPanel = sdk.getComponent('settings.DevicesPanel'); + return ( +
    +

    Devices

    + +
    + ); + }, + + _renderLabs: function () { + let features = LABS_FEATURES.map(feature => ( +
    + { + UserSettingsStore.setFeatureEnabled(feature.id, e.target.checked); + this.forceUpdate(); + }}/> + +
    + )); + return ( +
    +

    Labs

    +
    +

    These are experimental features that may break in unexpected ways. Use with caution.

    + {features} +
    +
    + ) + }, + render: function() { var self = this; var Loader = sdk.getComponent("elements.Spinner"); @@ -360,6 +399,7 @@ module.exports = React.createClass({ var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar'); var Notifications = sdk.getComponent("settings.Notifications"); var EditableText = sdk.getComponent('elements.EditableText'); + var avatarUrl = ( this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null ); @@ -434,30 +474,6 @@ module.exports = React.createClass({
    ); } - this._renderLabs = function () { - let features = LABS_FEATURES.map(feature => ( -
    - UserSettingsStore.setFeatureEnabled(feature.id, e.target.checked)} /> - -
    - )); - return ( -
    -

    Labs

    - -
    -

    These are experimental features that may break in unexpected ways. Use with caution.

    - {features} -
    -
    - ) - }; - return (
    @@ -510,10 +526,9 @@ module.exports = React.createClass({ {notification_area} {this._renderUserInterfaceSettings()} - - {this._renderDeviceInfo()} - {this._renderLabs()} + {this._renderDevicesPanel()} + {this._renderCryptoInfo()}

    Advanced

    diff --git a/src/components/views/settings/DevicesPanel.js b/src/components/views/settings/DevicesPanel.js new file mode 100644 index 0000000000..8dd6bb9230 --- /dev/null +++ b/src/components/views/settings/DevicesPanel.js @@ -0,0 +1,138 @@ +/* +Copyright 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import classNames from 'classnames'; + +import sdk from '../../../index'; +import MatrixClientPeg from '../../../MatrixClientPeg'; + + +export default class DevicesPanel extends React.Component { + constructor(props, context) { + super(props, context); + + this.state = { + devices: undefined, + deviceLoadError: undefined, + }; + + this._unmounted = false; + + this._renderDevice = this._renderDevice.bind(this); + } + + componentDidMount() { + this._loadDevices(); + } + + componentWillUnmount() { + this._unmounted = true; + } + + _loadDevices() { + MatrixClientPeg.get().getDevices().done( + (resp) => { + if (this._unmounted) { return; } + this.setState({devices: resp.devices || []}); + }, + (error) => { + if (this._unmounted) { return; } + var errtxt; + if (err.httpStatus == 404) { + // 404 probably means the HS doesn't yet support the API. + errtxt = "Your home server does not support device management."; + } else { + console.error("Error loading devices:", error); + errtxt = "Unable to load device list."; + } + this.setState({deviceLoadError: errtxt}); + } + ); + } + + + /** + * compare two devices, sorting from most-recently-seen to least-recently-seen + * (and then, for stability, by device id) + */ + _deviceCompare(a, b) { + // return < 0 if a comes before b, > 0 if a comes after b. + const lastSeenDelta = + (b.last_seen_ts || 0) - (a.last_seen_ts || 0); + + if (lastSeenDelta !== 0) { return lastSeenDelta; } + + const idA = a.device_id; + const idB = b.device_id; + return (idA < idB) ? -1 : (idA > idB) ? 1 : 0; + } + + _onDeviceDeleted(device) { + if (this._unmounted) { return; } + + // delete the removed device from our list. + const removed_id = device.device_id; + this.setState((state, props) => { + const newDevices = state.devices.filter( + d => { return d.device_id != removed_id } + ); + return { devices: newDevices }; + }); + } + + _renderDevice(device) { + var DevicesPanelEntry = sdk.getComponent('settings.DevicesPanelEntry'); + return ( + {this._onDeviceDeleted(device)}} /> + ); + } + + render() { + const Spinner = sdk.getComponent("elements.Spinner"); + + if (this.state.deviceLoadError !== undefined) { + const classes = classNames(this.props.className, "error"); + return ( +
    + {this.state.deviceLoadError} +
    + ); + } + + const devices = this.state.devices; + if (devices === undefined) { + // still loading + const classes = this.props.className; + return ; + } + + devices.sort(this._deviceCompare); + + const classes = classNames(this.props.className, "mx_DevicesPanel"); + return ( +
    +
    +
    Name
    +
    Last seen
    +
    +
    + {devices.map(this._renderDevice)} +
    + ); + } +} diff --git a/src/components/views/settings/DevicesPanelEntry.js b/src/components/views/settings/DevicesPanelEntry.js new file mode 100644 index 0000000000..6858e62102 --- /dev/null +++ b/src/components/views/settings/DevicesPanelEntry.js @@ -0,0 +1,134 @@ +/* +Copyright 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import classNames from 'classnames'; +import q from 'q'; + +import sdk from '../../../index'; +import MatrixClientPeg from '../../../MatrixClientPeg'; +import DateUtils from '../../../DateUtils'; + +export default class DevicesPanelEntry extends React.Component { + constructor(props, context) { + super(props, context); + + this.state = { + deleting: false, + deleteError: undefined, + }; + + this._unmounted = false; + + this._onDeleteClick = this._onDeleteClick.bind(this); + this._onDisplayNameChanged = this._onDisplayNameChanged.bind(this); + } + + componentWillUnmount() { + this._unmounted = true; + } + + _onDisplayNameChanged(value) { + const device = this.props.device; + return MatrixClientPeg.get().setDeviceDetails(device.device_id, { + display_name: value, + }).catch((e) => { + console.error("Error setting device display name", e); + throw new Error("Failed to set display name"); + }); + } + + _onDeleteClick() { + const device = this.props.device; + this.setState({deleting: true}); + + MatrixClientPeg.get().deleteDevice(device.device_id).done( + () => { + this.props.onDeleted(); + if (this._unmounted) { return; } + this.setState({ deleting: false }); + }, + (e) => { + console.error("Error deleting device", e); + if (this._unmounted) { return; } + this.setState({ + deleting: false, + deleteError: "Failed to delete device", + }); + } + ); + } + + render() { + const EditableTextContainer = sdk.getComponent('elements.EditableTextContainer'); + + const device = this.props.device; + + if (this.state.deleting) { + const Spinner = sdk.getComponent("elements.Spinner"); + + return ( +
    + +
    + ); + } + + let lastSeen = ""; + if (device.last_seen_ts) { + // todo: format the timestamp as "5 minutes ago" or whatever. + const lastSeenDate = new Date(device.last_seen_ts); + lastSeen = device.last_seen_ip + " @ " + + lastSeenDate.toLocaleString(); + } + + let deleteButton; + if (this.state.deleteError) { + deleteButton =
    {this.state.deleteError}
    + } else { + deleteButton = ( +
    + Delete +
    + ); + } + + return ( +
    +
    + +
    +
    + {lastSeen} +
    +
    + {deleteButton} +
    +
    + ); + } +} + +DevicesPanelEntry.propTypes = { + device: React.PropTypes.object.isRequired, + onDeleted: React.PropTypes.func, +}; + +DevicesPanelEntry.defaultProps = { + onDeleted: function() {}, +}; From 84583b25e6a3cbe96e7d4e8aa82612c06ace4a5c Mon Sep 17 00:00:00 2001 From: wmwragg Date: Mon, 1 Aug 2016 16:45:27 +0100 Subject: [PATCH 179/222] New voice and video call buttons --- src/components/views/rooms/MessageComposer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index da5b504d88..c27a61c33a 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -171,11 +171,11 @@ export default class MessageComposer extends React.Component { else { callButton =
    - +
    ; videoCallButton =
    - +
    ; } From 94a44bfec3a24a403c091b9ca574ddab3dc79b4f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 1 Aug 2016 16:56:25 +0100 Subject: [PATCH 180/222] Fix warnings emanating from Velociraptor elements We are no longer allowed to stick random properties on child properties, and the Velociraptor animations were causing some React warnings. Move the startStyles and enterTransitionOpts properties up to the Velociraptor node, and avoid setting arbitrary props on the created children. This is less flexible, as it assumes that all children will have the same start style; however, we weren't using the flexibility, and we can always replace the array with a map or a function or something if we need it in the future. --- src/Velociraptor.js | 78 +++++++++++++------ .../views/rooms/ReadReceiptMarker.js | 6 +- 2 files changed, 56 insertions(+), 28 deletions(-) diff --git a/src/Velociraptor.js b/src/Velociraptor.js index f45925867f..d9b6b3d5dc 100644 --- a/src/Velociraptor.js +++ b/src/Velociraptor.js @@ -18,6 +18,19 @@ module.exports = React.createClass({ // optional transition information for changing existing children transition: React.PropTypes.object, + + // a list of state objects to apply to each child node in turn + startStyles: React.PropTypes.array, + + // a list of transition options from the corresponding startStyle + enterTransitionOpts: React.PropTypes.array, + }, + + getDefaultProps: function() { + return { + startStyles: [], + enterTransitionOpts: [], + }; }, componentWillMount: function() { @@ -56,56 +69,71 @@ module.exports = React.createClass({ } self.children[c.key] = old; } else { - // new element. If it has a startStyle, use that as the style and go through + // new element. If we have a startStyle, use that as the style and go through // the enter animations - var newProps = { - ref: self.collectNode.bind(self, c.key) - }; - if (c.props.startStyle && Object.keys(c.props.startStyle).length) { - var startStyle = c.props.startStyle; - if (Array.isArray(startStyle)) { - startStyle = startStyle[0]; - } - newProps._restingStyle = c.props.style; + var newProps = {}; + var restingStyle = c.props.style; + + var startStyles = self.props.startStyles; + if (startStyles.length > 0) { + var startStyle = startStyles[0] newProps.style = startStyle; - //console.log("mounted@startstyle0: "+JSON.stringify(startStyle)); - // apply the enter animations once it's mounted + // console.log("mounted@startstyle0: "+JSON.stringify(startStyle)); } + + newProps.ref = (n => self._collectNode( + c.key, n, restingStyle + )); + self.children[c.key] = React.cloneElement(c, newProps); } }); }, - collectNode: function(k, node) { + /** + * called when a child element is mounted/unmounted + * + * @param {string} k key of the child + * @param {null|Object} node On mount: React node. On unmount: null + * @param {Object} restingStyle final style + */ + _collectNode: function(k, node, restingStyle) { if ( node && this.nodes[k] === undefined && - node.props.startStyle && - Object.keys(node.props.startStyle).length + this.props.startStyles.length > 0 ) { + var startStyles = this.props.startStyles; + var transitionOpts = this.props.enterTransitionOpts; var domNode = ReactDom.findDOMNode(node); - var startStyles = node.props.startStyle; - var transitionOpts = node.props.enterTransitionOpts; - if (!Array.isArray(startStyles)) { - startStyles = [ startStyles ]; - transitionOpts = [ transitionOpts ]; - } // start from startStyle 1: 0 is the one we gave it // to start with, so now we animate 1 etc. for (var i = 1; i < startStyles.length; ++i) { Velocity(domNode, startStyles[i], transitionOpts[i-1]); - //console.log("start: "+JSON.stringify(startStyles[i])); + /* + console.log("start:", + JSON.stringify(transitionOpts[i-1]), + "->", + JSON.stringify(startStyles[i]), + ); + */ } + // and then we animate to the resting state - Velocity(domNode, node.props._restingStyle, + Velocity(domNode, restingStyle, transitionOpts[i-1]) .then(() => { // once we've reached the resting state, hide the element if // appropriate - domNode.style.visibility = node.props._restingStyle.visibility; + domNode.style.visibility = restingStyle.visibility; }); - //console.log("enter: "+JSON.stringify(node.props._restingStyle)); + /* + console.log("enter:", + JSON.stringify(transitionOpts[i-1]), + "->", + JSON.stringify(restingStyle)); + */ } else if (node === null) { // Velocity stores data on elements using the jQuery .data() // method, and assumes you'll be using jQuery's .remove() to diff --git a/src/components/views/rooms/ReadReceiptMarker.js b/src/components/views/rooms/ReadReceiptMarker.js index d40b2f5f8d..91ba201683 100644 --- a/src/components/views/rooms/ReadReceiptMarker.js +++ b/src/components/views/rooms/ReadReceiptMarker.js @@ -163,13 +163,13 @@ module.exports = React.createClass({ }; return ( - + From 124a8167519dccf839957244178c4849cb938de1 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 1 Aug 2016 17:10:46 +0100 Subject: [PATCH 181/222] Silence some more react warnings Remove some spurious props which were causing react warnings --- src/components/views/avatars/MemberAvatar.js | 7 +++++-- src/components/views/rooms/MemberList.js | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/views/avatars/MemberAvatar.js b/src/components/views/avatars/MemberAvatar.js index 5e2dbbb23a..654f801afc 100644 --- a/src/components/views/avatars/MemberAvatar.js +++ b/src/components/views/avatars/MemberAvatar.js @@ -59,9 +59,12 @@ module.exports = React.createClass({ render: function() { var BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); + + var {member, ...otherProps} = this.props; + return ( - + ); } }); diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index 80ae90d984..5536aeddd6 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -492,7 +492,7 @@ module.exports = React.createClass({ invitedSection = (

    Invited

    -
    +
    {invitedMemberTiles}
    From e63a32dc105daf49646d0b5ce150e69b6987b5c3 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 2 Aug 2016 11:00:00 +0100 Subject: [PATCH 182/222] Fix warnings from MessageComposer Fix a couple of warnings from React about required properties not being set. --- src/components/views/rooms/MessageComposer.js | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index c27a61c33a..282f7f013f 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -37,6 +37,8 @@ export default class MessageComposer extends React.Component { this.onUpArrow = this.onUpArrow.bind(this); this.onDownArrow = this.onDownArrow.bind(this); this.onTab = this.onTab.bind(this); + this._tryComplete = this._tryComplete.bind(this); + this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this); this.state = { autocompleteQuery: '', @@ -142,7 +144,22 @@ export default class MessageComposer extends React.Component { } onTab() { - return this.refs.autocomplete.onTab(); + // FIXME Autocomplete doesn't have an onTab - what is this supposed to do? + // return this.refs.autocomplete.onTab(); + return false; + } + + _tryComplete(): boolean { + if (this.refs.autocomplete) { + return this.refs.autocomplete.onConfirm(); + } + return false; + } + + _onAutocompleteConfirm(range, completion) { + if (this.messageComposerInput) { + this.messageComposerInput.onConfirmAutocompletion(range, completion); + } } render() { @@ -203,7 +220,7 @@ export default class MessageComposer extends React.Component { key="controls_input" onResize={this.props.onResize} room={this.props.room} - tryComplete={this.refs.autocomplete && this.refs.autocomplete.onConfirm} + tryComplete={this._tryComplete} onUpArrow={this.onUpArrow} onDownArrow={this.onDownArrow} onTab={this.onTab} @@ -227,7 +244,7 @@ export default class MessageComposer extends React.Component {
    From db9750a7e322757e5bc9318bc33e756e12827a64 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 2 Aug 2016 14:04:20 +0100 Subject: [PATCH 183/222] Call the logout API when we log out Also try to refactor some of the login/logout code out of MatrixChat and into a separate Lifecycle.js. This still isn't great, but it at least gets some code out of MatrixClient. --- src/CallHandler.js | 7 +- src/Lifecycle.js | 98 +++++++++++++++++++ src/MatrixClientPeg.js | 22 +++-- src/components/structures/MatrixChat.js | 121 ++++++++++++------------ 4 files changed, 176 insertions(+), 72 deletions(-) create mode 100644 src/Lifecycle.js diff --git a/src/CallHandler.js b/src/CallHandler.js index c459d12e31..5bd2d20ae8 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -181,11 +181,11 @@ function _onAction(payload) { console.error("Unknown conf call type: %s", payload.type); } } - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); switch (payload.action) { case 'place_call': if (module.exports.getAnyActiveCall()) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Existing Call", description: "You are already in a call." @@ -195,6 +195,7 @@ function _onAction(payload) { // if the runtime env doesn't do VoIP, whine. if (!MatrixClientPeg.get().supportsVoip()) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "VoIP is unsupported", description: "You cannot place VoIP calls in this browser." @@ -210,7 +211,7 @@ function _onAction(payload) { var members = room.getJoinedMembers(); if (members.length <= 1) { - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { description: "You cannot place a call with yourself." }); @@ -236,11 +237,13 @@ function _onAction(payload) { case 'place_conference_call': console.log("Place conference call in %s", payload.room_id); if (!ConferenceHandler) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { description: "Conference calls are not supported in this client" }); } else if (!MatrixClientPeg.get().supportsVoip()) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "VoIP is unsupported", description: "You cannot place VoIP calls in this browser." diff --git a/src/Lifecycle.js b/src/Lifecycle.js new file mode 100644 index 0000000000..86fa39cb51 --- /dev/null +++ b/src/Lifecycle.js @@ -0,0 +1,98 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import MatrixClientPeg from './MatrixClientPeg'; +import Notifier from './Notifier' +import UserActivity from './UserActivity'; +import Presence from './Presence'; +import dis from './dispatcher'; + +function login(credentials, options) { + credentials.guest = Boolean(credentials.guest); + console.log("onLoggedIn => %s (guest=%s)", credentials.userId, credentials.guest); + MatrixClientPeg.replaceUsingAccessToken( + credentials.homeserverUrl, credentials.identityServerUrl, + credentials.userId, credentials.accessToken, credentials.guest + ); + + dis.dispatch({action: 'on_logged_in'}); + + startMatrixClient(options); +} + +function logout() { + if (MatrixClientPeg.get().isGuest()) { + // logout doesn't work for guest sessions + // Also we sometimes want to re-log in a guest session + // if we abort the login + _onLoggedOut(); + return; + } + + return MatrixClientPeg.get().logout().then(_onLoggedOut, + // Just throwing an error here is going to be very unhelpful + // if you're trying to log out because your server's down and + // you want to log into a different server, so just forget the + // access token. It's annoying that this will leave the access + // token still valid, but we should fix this by having access + // tokens expire (and if you really think you've been compromised, + // change your password). + _onLoggedOut + ); +} + +function startMatrixClient(options) { + // dispatch this before starting the matrix client: it's used + // to add listeners for the 'sync' event so otherwise we'd have + // a race condition (and we need to dispatch synchronously for this + // to work). + dis.dispatch({action: 'will_start_client'}, true); + + Notifier.start(); + UserActivity.start(); + Presence.start(); + MatrixClientPeg.get().startClient(MatrixClientPeg.opts); +} + +function _onLoggedOut() { + if (window.localStorage) { + const hsUrl = window.localStorage.getItem("mx_hs_url"); + const isUrl = window.localStorage.getItem("mx_is_url"); + window.localStorage.clear(); + // preserve our HS & IS URLs for convenience + // N.B. we cache them in hsUrl/isUrl and can't really inline them + // as getCurrentHsUrl() may call through to localStorage. + if (hsUrl) window.localStorage.setItem("mx_hs_url", hsUrl); + if (isUrl) window.localStorage.setItem("mx_is_url", isUrl); + } + _stopMatrixClient(); + + dis.dispatch({action: 'on_logged_out'}); +} + +// stop all the background processes related to the current client +function _stopMatrixClient() { + Notifier.stop(); + UserActivity.stop(); + Presence.stop(); + MatrixClientPeg.get().stopClient(); + MatrixClientPeg.get().removeAllListeners(); + MatrixClientPeg.unset(); +} + +module.exports = { + login, logout, startMatrixClient +}; diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index ce4b5ba743..96eb95de64 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -40,6 +40,14 @@ function deviceId() { class MatrixClientPeg { constructor() { this.matrixClient = null; + + // These are the default options used when Lifecycle.js + // starts the client. These can be altered when the + // 'will_start_client' event is dispatched. + this.opts = { + pendingEventOrdering: "detached", + initialSyncLimit: 20, + }; } get(): MatrixClient { @@ -96,13 +104,13 @@ class MatrixClientPeg { } getCredentials() { - return [ - this.matrixClient.baseUrl, - this.matrixClient.idBaseUrl, - this.matrixClient.credentials.userId, - this.matrixClient.getAccessToken(), - this.matrixClient.isGuest(), - ]; + return { + homeserverUrl: this.matrixClient.baseUrl, + identityServerUrl: this.matrixClient.idBaseUrl, + userId: this.matrixClient.credentials.userId, + accessToken: this.matrixClient.getAccessToken(), + guest: this.matrixClient.isGuest(), + }; } tryRestore() { diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index dc9ca08e94..a4f024efef 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -36,6 +36,7 @@ var sdk = require('../../index'); var MatrixTools = require('../../MatrixTools'); var linkifyMatrix = require("../../linkify-matrix"); var KeyCode = require('../../KeyCode'); +var Lifecycle = require('../../Lifecycle'); var createRoom = require("../../createRoom"); @@ -140,6 +141,7 @@ module.exports = React.createClass({ componentWillMount: function() { this.favicon = new Favico({animation: 'none'}); + this.guestCreds = null; }, componentDidMount: function() { @@ -156,7 +158,7 @@ module.exports = React.createClass({ this.props.startingQueryParams.guest_access_token) { this._autoRegisterAsGuest = false; - this.onLoggedIn({ + this._onHaveCredentials({ userId: this.props.startingQueryParams.guest_user_id, accessToken: this.props.startingQueryParams.guest_access_token, homeserverUrl: this.getDefaultHsUrl(), @@ -174,7 +176,7 @@ module.exports = React.createClass({ // Don't auto-register as a guest. This applies if you refresh the page on a // logged in client THEN hit the Sign Out button. this._autoRegisterAsGuest = false; - this.startMatrixClient(); + Lifecycle.startMatrixClient(); } this.focusComposer = false; // scrollStateMap is a map from room id to the scroll state returned by @@ -229,7 +231,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().registerGuest().done(function(creds) { console.log("Registered as guest: %s", creds.user_id); self._setAutoRegisterAsGuest(false); - self.onLoggedIn({ + self._onHaveCredentials({ userId: creds.user_id, accessToken: creds.access_token, homeserverUrl: hsUrl, @@ -260,34 +262,10 @@ module.exports = React.createClass({ var self = this; switch (payload.action) { case 'logout': - var guestCreds; if (MatrixClientPeg.get().isGuest()) { - guestCreds = { // stash our guest creds so we can backout if needed - userId: MatrixClientPeg.get().credentials.userId, - accessToken: MatrixClientPeg.get().getAccessToken(), - homeserverUrl: MatrixClientPeg.get().getHomeserverUrl(), - identityServerUrl: MatrixClientPeg.get().getIdentityServerUrl(), - guest: true - } + this.guestCreds = MatrixClientPeg.getCredentials(); } - - if (window.localStorage) { - var hsUrl = this.getCurrentHsUrl(); - var isUrl = this.getCurrentIsUrl(); - window.localStorage.clear(); - // preserve our HS & IS URLs for convenience - // N.B. we cache them in hsUrl/isUrl and can't really inline them - // as getCurrentHsUrl() may call through to localStorage. - window.localStorage.setItem("mx_hs_url", hsUrl); - window.localStorage.setItem("mx_is_url", isUrl); - } - this._stopMatrixClient(); - this.notifyNewScreen('login'); - this.replaceState({ - logged_in: false, - ready: false, - guestCreds: guestCreds, - }); + Lifecycle.logout(); break; case 'start_registration': var newState = payload.params || {}; @@ -313,7 +291,6 @@ module.exports = React.createClass({ if (this.state.logged_in) return; this.replaceState({ screen: 'login', - guestCreds: this.state.guestCreds, }); this.notifyNewScreen('login'); break; @@ -323,17 +300,14 @@ module.exports = React.createClass({ }); break; case 'start_upgrade_registration': + // stash our guest creds so we can backout if needed + if (MatrixClientPeg.get().isGuest()) { + this.guestCreds = MatrixClientPeg.getCredentials(); + } this.replaceState({ screen: "register", upgradeUsername: MatrixClientPeg.get().getUserIdLocalpart(), guestAccessToken: MatrixClientPeg.get().getAccessToken(), - guestCreds: { // stash our guest creds so we can backout if needed - userId: MatrixClientPeg.get().credentials.userId, - accessToken: MatrixClientPeg.get().getAccessToken(), - homeserverUrl: MatrixClientPeg.get().getHomeserverUrl(), - identityServerUrl: MatrixClientPeg.get().getIdentityServerUrl(), - guest: true - } }); this.notifyNewScreen('register'); break; @@ -482,6 +456,15 @@ module.exports = React.createClass({ middleOpacity: payload.middleOpacity, }); break; + case 'on_logged_in': + this._onLoggedIn(); + break; + case 'on_logged_out': + this._onLoggedOut(); + break; + case 'will_start_client': + this._onWillStartClient(); + break; } }, @@ -592,23 +575,40 @@ module.exports = React.createClass({ this.scrollStateMap[roomId] = state; }, - onLoggedIn: function(credentials) { - credentials.guest = Boolean(credentials.guest); - console.log("onLoggedIn => %s (guest=%s)", credentials.userId, credentials.guest); - MatrixClientPeg.replaceUsingAccessToken( - credentials.homeserverUrl, credentials.identityServerUrl, - credentials.userId, credentials.accessToken, credentials.guest - ); - this.setState({ - screen: undefined, - logged_in: true + _doLogin(creds) { + Lifecycle.login(creds, { + syncTimelineLimit: this.props.config.sync_timeline_limit, }); - this.startMatrixClient(); - this.notifyNewScreen(''); }, - startMatrixClient: function() { + _onHaveCredentials: function(credentials) { + credentials.guest = Boolean(credentials.guest); + Lifecycle.login(credentials); + }, + + _onLoggedIn: function(credentials) { + this.guestCreds = null; + this.setState({ + screen: undefined, + logged_in: true, + }); + }, + + _onLoggedOut: function() { + this.notifyNewScreen('login'); + this.replaceState({ + logged_in: false, + ready: false, + }); + }, + + _onWillStartClient() { var cli = MatrixClientPeg.get(); + + if (this.props.config.sync_timeline_limit) { + MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit; + } + var self = this; cli.on('sync', function(state, prevState) { self.updateFavicon(state, prevState); @@ -675,13 +675,6 @@ module.exports = React.createClass({ action: 'logout' }); }); - Notifier.start(); - UserActivity.start(); - Presence.start(); - cli.startClient({ - pendingEventOrdering: "detached", - initialSyncLimit: this.props.config.sync_timeline_limit || 20, - }); }, // stop all the background processes related to the current client @@ -919,12 +912,14 @@ module.exports = React.createClass({ onReturnToGuestClick: function() { // reanimate our guest login - this.onLoggedIn(this.state.guestCreds); - this.setState({ guestCreds: null }); + if (this.guestCreds) { + this._onHaveCredentials(this.guestCreds); + this.guestCreds = null; + } }, onRegistered: function(credentials) { - this.onLoggedIn(credentials); + this._onHaveCredentials(credentials); // do post-registration stuff // This now goes straight to user settings // We use _setPage since if we wait for @@ -1130,7 +1125,7 @@ module.exports = React.createClass({ onLoggedIn={this.onRegistered} onLoginClick={this.onLoginClick} onRegisterClick={this.onRegisterClick} - onCancelClick={ this.state.guestCreds ? this.onReturnToGuestClick : null } + onCancelClick={this.guestCreds ? this.onReturnToGuestClick : null} /> ); } else if (this.state.screen == 'forgot_password') { @@ -1146,7 +1141,7 @@ module.exports = React.createClass({ } else { return ( ); } From 2c2f689361d1fa12352acd8555d2db831d30ad61 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Tue, 2 Aug 2016 14:46:47 +0100 Subject: [PATCH 184/222] New design for long names --- src/components/views/rooms/RoomList.js | 6 ++++-- src/components/views/rooms/RoomTile.js | 18 +++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index e2bc25653c..8e57ceab9b 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -268,9 +268,11 @@ module.exports = React.createClass({ }, _repositionTooltip: function(e) { - if (this.tooltip && this.tooltip.parentElement) { + // We access the parent of the parent, as the tooltip is inside a container + // Needs refactoring into a better multipurpose tooltip + if (this.tooltip && this.tooltip.parentElement && this.tooltip.parentElement.parentElement) { var scroll = ReactDOM.findDOMNode(this); - this.tooltip.style.top = (3 + scroll.parentElement.offsetTop + this.tooltip.parentElement.offsetTop - this._getScrollNode().scrollTop) + "px"; + this.tooltip.style.top = (3 + scroll.parentElement.offsetTop + this.tooltip.parentElement.parentElement.offsetTop - this._getScrollNode().scrollTop) + "px"; } }, diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index d6b33b9409..5e367ffd80 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -99,7 +99,8 @@ module.exports = React.createClass({ badgeOnMouseEnter: function() { // Only allow none guests to access the context menu - if (!MatrixClientPeg.get().isGuest()) { + // and only change it if it needs to change + if (!MatrixClientPeg.get().isGuest() && !this.state.badgeHover) { this.setState( { badgeHover : true } ); } }, @@ -185,9 +186,9 @@ module.exports = React.createClass({ } if (this.state.areNotifsMuted && !(this.state.badgeHover || this.state.menu)) { - badge =
    ; + badge =
    ; } else { - badge =
    { badgeContent }
    ; + badge =
    { badgeContent }
    ; } var label; @@ -197,15 +198,16 @@ module.exports = React.createClass({ 'mx_RoomTile_name': true, 'mx_RoomTile_invite': this.props.isInvite, 'mx_RoomTile_mute': this.state.areNotifsMuted, + 'mx_RoomTile_badgeShown': this.props.highlight || notificationCount > 0 || this.state.badgeHover || this.state.menu || this.state.areNotifsMuted, }); let nameHTML = emojifyText(name); if (this.props.selected) { let nameSelected = ; - label =
    { nameSelected }
    ; + label =
    { nameSelected }
    ; } else { - label =
    ; + label =
    ; } } else if (this.state.hover) { @@ -232,8 +234,10 @@ module.exports = React.createClass({
    - { label } - { badge } +
    + { label } + { badge } +
    { incomingCallBox } { tooltip }
    From 05f0d4d166d7fe53090affad0eb546dbe4f48b2c Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 2 Aug 2016 14:55:29 +0100 Subject: [PATCH 185/222] Fix 'start new direct chat' --- src/components/views/rooms/MemberInfo.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 07a7b9398d..eed5e953d3 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -394,11 +394,11 @@ module.exports = React.createClass({ // TODO: keep existingOneToOneRoomId updated if we see any room member changes anywhere - var useExistingOneToOneRoom = this.state.existingOneToOneRoomId && (this.state.existingOneToOneRoomId !== this.props.member.roomId); + const useExistingOneToOneRoom = this.state.existingOneToOneRoomId && (this.state.existingOneToOneRoomId !== this.props.member.roomId); // check if there are any existing rooms with just us and them (1:1) // If so, just view that room. If not, create a private room with them. - if (this.state.existingOneToOneRoomId) { + if (useExistingOneToOneRoom) { dis.dispatch({ action: 'view_room', room_id: this.state.existingOneToOneRoomId, From 61e0258b6f3d76d8abfbccf34ba462c82c23ff35 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 2 Aug 2016 15:55:18 +0100 Subject: [PATCH 186/222] Include rooms with 1 person invited If we can't find one with both people joined. --- src/components/views/rooms/MemberInfo.js | 33 +++++++++++++++++------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index eed5e953d3..f366f3209e 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -95,31 +95,31 @@ module.exports = React.createClass({ }, getExistingOneToOneRoomId: function() { - var self = this; - var rooms = MatrixClientPeg.get().getRooms(); - var userIds = [ + const rooms = MatrixClientPeg.get().getRooms(); + const userIds = [ this.props.member.userId, MatrixClientPeg.get().credentials.userId ]; - var existingRoomId; + let existingRoomId = null; + let invitedRoomId = null; // roomId can be null here because of a hack in MatrixChat.onUserClick where we // abuse this to view users rather than room members. - var currentMembers; + let currentMembers; if (this.props.member.roomId) { - var currentRoom = MatrixClientPeg.get().getRoom(this.props.member.roomId); + const currentRoom = MatrixClientPeg.get().getRoom(this.props.member.roomId); currentMembers = currentRoom.getJoinedMembers(); } // reuse the first private 1:1 we find existingRoomId = null; - for (var i = 0; i < rooms.length; i++) { + for (let i = 0; i < rooms.length; i++) { // don't try to reuse public 1:1 rooms - var join_rules = rooms[i].currentState.getStateEvents("m.room.join_rules", ''); + const join_rules = rooms[i].currentState.getStateEvents("m.room.join_rules", ''); if (join_rules && join_rules.getContent().join_rule === 'public') continue; - var members = rooms[i].getJoinedMembers(); + const members = rooms[i].getJoinedMembers(); if (members.length === 2 && userIds.indexOf(members[0].userId) !== -1 && userIds.indexOf(members[1].userId) !== -1) @@ -127,6 +127,21 @@ module.exports = React.createClass({ existingRoomId = rooms[i].roomId; break; } + + const invited = rooms[i].getMembersWithMembership('invite'); + if (members.length === 1 && + invited.length === 1 && + userIds.indexOf(members[0].userId) !== -1 && + userIds.indexOf(invited[0].userId) !== -1) && + invitedRoomId === null + { + invitedRoomId = rooms[i].roomId; + // keep looking: we'll use this one if there's nothing better + } + } + + if (existingRoomId === null) { + existingRoomId = invitedRoomId; } return existingRoomId; From 3000fae5d89423b44ac9b1dd74413d9cf96da76c Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 2 Aug 2016 15:58:21 +0100 Subject: [PATCH 187/222] Oops, uncommit 61e0258 --- src/components/views/rooms/MemberInfo.js | 33 +++++++----------------- 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index f366f3209e..eed5e953d3 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -95,31 +95,31 @@ module.exports = React.createClass({ }, getExistingOneToOneRoomId: function() { - const rooms = MatrixClientPeg.get().getRooms(); - const userIds = [ + var self = this; + var rooms = MatrixClientPeg.get().getRooms(); + var userIds = [ this.props.member.userId, MatrixClientPeg.get().credentials.userId ]; - let existingRoomId = null; - let invitedRoomId = null; + var existingRoomId; // roomId can be null here because of a hack in MatrixChat.onUserClick where we // abuse this to view users rather than room members. - let currentMembers; + var currentMembers; if (this.props.member.roomId) { - const currentRoom = MatrixClientPeg.get().getRoom(this.props.member.roomId); + var currentRoom = MatrixClientPeg.get().getRoom(this.props.member.roomId); currentMembers = currentRoom.getJoinedMembers(); } // reuse the first private 1:1 we find existingRoomId = null; - for (let i = 0; i < rooms.length; i++) { + for (var i = 0; i < rooms.length; i++) { // don't try to reuse public 1:1 rooms - const join_rules = rooms[i].currentState.getStateEvents("m.room.join_rules", ''); + var join_rules = rooms[i].currentState.getStateEvents("m.room.join_rules", ''); if (join_rules && join_rules.getContent().join_rule === 'public') continue; - const members = rooms[i].getJoinedMembers(); + var members = rooms[i].getJoinedMembers(); if (members.length === 2 && userIds.indexOf(members[0].userId) !== -1 && userIds.indexOf(members[1].userId) !== -1) @@ -127,21 +127,6 @@ module.exports = React.createClass({ existingRoomId = rooms[i].roomId; break; } - - const invited = rooms[i].getMembersWithMembership('invite'); - if (members.length === 1 && - invited.length === 1 && - userIds.indexOf(members[0].userId) !== -1 && - userIds.indexOf(invited[0].userId) !== -1) && - invitedRoomId === null - { - invitedRoomId = rooms[i].roomId; - // keep looking: we'll use this one if there's nothing better - } - } - - if (existingRoomId === null) { - existingRoomId = invitedRoomId; } return existingRoomId; From 10f6ecfe5f7050710938d8420d8210cde7759bd4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 2 Aug 2016 15:59:28 +0100 Subject: [PATCH 188/222] Include rooms with 1 person invited If we can't find one with both people joined. --- src/components/views/rooms/MemberInfo.js | 33 +++++++++++++++++------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index eed5e953d3..f366f3209e 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -95,31 +95,31 @@ module.exports = React.createClass({ }, getExistingOneToOneRoomId: function() { - var self = this; - var rooms = MatrixClientPeg.get().getRooms(); - var userIds = [ + const rooms = MatrixClientPeg.get().getRooms(); + const userIds = [ this.props.member.userId, MatrixClientPeg.get().credentials.userId ]; - var existingRoomId; + let existingRoomId = null; + let invitedRoomId = null; // roomId can be null here because of a hack in MatrixChat.onUserClick where we // abuse this to view users rather than room members. - var currentMembers; + let currentMembers; if (this.props.member.roomId) { - var currentRoom = MatrixClientPeg.get().getRoom(this.props.member.roomId); + const currentRoom = MatrixClientPeg.get().getRoom(this.props.member.roomId); currentMembers = currentRoom.getJoinedMembers(); } // reuse the first private 1:1 we find existingRoomId = null; - for (var i = 0; i < rooms.length; i++) { + for (let i = 0; i < rooms.length; i++) { // don't try to reuse public 1:1 rooms - var join_rules = rooms[i].currentState.getStateEvents("m.room.join_rules", ''); + const join_rules = rooms[i].currentState.getStateEvents("m.room.join_rules", ''); if (join_rules && join_rules.getContent().join_rule === 'public') continue; - var members = rooms[i].getJoinedMembers(); + const members = rooms[i].getJoinedMembers(); if (members.length === 2 && userIds.indexOf(members[0].userId) !== -1 && userIds.indexOf(members[1].userId) !== -1) @@ -127,6 +127,21 @@ module.exports = React.createClass({ existingRoomId = rooms[i].roomId; break; } + + const invited = rooms[i].getMembersWithMembership('invite'); + if (members.length === 1 && + invited.length === 1 && + userIds.indexOf(members[0].userId) !== -1 && + userIds.indexOf(invited[0].userId) !== -1) && + invitedRoomId === null + { + invitedRoomId = rooms[i].roomId; + // keep looking: we'll use this one if there's nothing better + } + } + + if (existingRoomId === null) { + existingRoomId = invitedRoomId; } return existingRoomId; From a0b973ffadb87082f2b67127b15b370e9d273396 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 2 Aug 2016 16:07:07 +0100 Subject: [PATCH 189/222] Syntax fail --- src/components/views/rooms/MemberInfo.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index f366f3209e..c087e7dc71 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -132,8 +132,8 @@ module.exports = React.createClass({ if (members.length === 1 && invited.length === 1 && userIds.indexOf(members[0].userId) !== -1 && - userIds.indexOf(invited[0].userId) !== -1) && - invitedRoomId === null + userIds.indexOf(invited[0].userId) !== -1 && + invitedRoomId === null) { invitedRoomId = rooms[i].roomId; // keep looking: we'll use this one if there's nothing better From 3818a89ca3a0e9308dd23eaca8bbc94c85e606cb Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 2 Aug 2016 18:46:43 +0100 Subject: [PATCH 190/222] Comment guestCreds --- src/components/structures/MatrixChat.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index a4f024efef..d0ff91fa3b 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -141,6 +141,10 @@ module.exports = React.createClass({ componentWillMount: function() { this.favicon = new Favico({animation: 'none'}); + + // Stashed guest credentials if the user logs out + // whilst logged in as a guest user (so they can change + // their mind & log back in) this.guestCreds = null; }, From 58bbb3509618e951e39ce0e6343b6e3097e9f53b Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 2 Aug 2016 18:48:27 +0100 Subject: [PATCH 191/222] s/login/setLoggedIn/ --- src/Lifecycle.js | 4 ++-- src/components/structures/MatrixChat.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 86fa39cb51..53180b24dd 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -20,7 +20,7 @@ import UserActivity from './UserActivity'; import Presence from './Presence'; import dis from './dispatcher'; -function login(credentials, options) { +function setLoggedIn(credentials, options) { credentials.guest = Boolean(credentials.guest); console.log("onLoggedIn => %s (guest=%s)", credentials.userId, credentials.guest); MatrixClientPeg.replaceUsingAccessToken( @@ -94,5 +94,5 @@ function _stopMatrixClient() { } module.exports = { - login, logout, startMatrixClient + setLoggedIn, logout, startMatrixClient }; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index d0ff91fa3b..2783f2ee82 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -580,14 +580,14 @@ module.exports = React.createClass({ }, _doLogin(creds) { - Lifecycle.login(creds, { + Lifecycle.setLoggedIn(creds, { syncTimelineLimit: this.props.config.sync_timeline_limit, }); }, _onHaveCredentials: function(credentials) { credentials.guest = Boolean(credentials.guest); - Lifecycle.login(credentials); + Lifecycle.setLoggedIn(credentials); }, _onLoggedIn: function(credentials) { From b9a5f7902b0d1c23f2cbe699e06efd5fa1cbb0a2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 2 Aug 2016 18:52:56 +0100 Subject: [PATCH 192/222] Doc setLoggedIn & remove redundant opts --- src/Lifecycle.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 53180b24dd..821cbd189d 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -20,7 +20,15 @@ import UserActivity from './UserActivity'; import Presence from './Presence'; import dis from './dispatcher'; -function setLoggedIn(credentials, options) { +/** + * Transitions to a logged-in state using the given credentials + * @param {string} credentials.homeserverUrl The base HS URL + * @param {string} credentials.identityServerUrl The base IS URL + * @param {string} credentials.userId The full Matrix User ID + * @param {string} credentials.accessToken The session access token + * @param {boolean} credentials.guest True if the session is a guest session + */ +function setLoggedIn(credentials) { credentials.guest = Boolean(credentials.guest); console.log("onLoggedIn => %s (guest=%s)", credentials.userId, credentials.guest); MatrixClientPeg.replaceUsingAccessToken( @@ -30,7 +38,7 @@ function setLoggedIn(credentials, options) { dis.dispatch({action: 'on_logged_in'}); - startMatrixClient(options); + startMatrixClient(); } function logout() { @@ -54,7 +62,7 @@ function logout() { ); } -function startMatrixClient(options) { +function startMatrixClient() { // dispatch this before starting the matrix client: it's used // to add listeners for the 'sync' event so otherwise we'd have // a race condition (and we need to dispatch synchronously for this From c2c548ef5af57a63b86de10539b39a2844934b23 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 2 Aug 2016 18:55:13 +0100 Subject: [PATCH 193/222] Comment logout --- src/Lifecycle.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 821cbd189d..9c31f1a20e 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -41,6 +41,9 @@ function setLoggedIn(credentials) { startMatrixClient(); } +/** + * Logs the current session out and transitions to the logged-out state + */ function logout() { if (MatrixClientPeg.get().isGuest()) { // logout doesn't work for guest sessions From 77a5384bf8419af01d183811cb3917d1c3c92d12 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 2 Aug 2016 18:56:12 +0100 Subject: [PATCH 194/222] Comment startMatrixClient --- src/Lifecycle.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 9c31f1a20e..157af4aa4c 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -65,6 +65,10 @@ function logout() { ); } +/** + * Starts the matrix client and all other react-sdk services that + * listen for events while a session is logged in. + */ function startMatrixClient() { // dispatch this before starting the matrix client: it's used // to add listeners for the 'sync' event so otherwise we'd have From 40834d188e86fd795aaf687abefd8adc8ee30300 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 2 Aug 2016 18:58:18 +0100 Subject: [PATCH 195/222] Don't let pendingEventOrdering be changed --- src/Lifecycle.js | 5 +++++ src/MatrixClientPeg.js | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 157af4aa4c..be09551c88 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -79,6 +79,11 @@ function startMatrixClient() { Notifier.start(); UserActivity.start(); Presence.start(); + + // the react sdk doesn't work without this, so don't allow + // it to be overridden (and modify the global object so at + // at least the app can see we've changed it) + MatrixClientPeg.opts.pendingEventOrdering = "detached"; MatrixClientPeg.get().startClient(MatrixClientPeg.opts); } diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 96eb95de64..49326cc22f 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -45,7 +45,6 @@ class MatrixClientPeg { // starts the client. These can be altered when the // 'will_start_client' event is dispatched. this.opts = { - pendingEventOrdering: "detached", initialSyncLimit: 20, }; } From 65865f879ffcec54574444b75c158dfad5de2e63 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 2 Aug 2016 18:59:09 +0100 Subject: [PATCH 196/222] We already know we're a guest here --- src/components/structures/MatrixChat.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 2783f2ee82..81a2924fb7 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -305,9 +305,7 @@ module.exports = React.createClass({ break; case 'start_upgrade_registration': // stash our guest creds so we can backout if needed - if (MatrixClientPeg.get().isGuest()) { - this.guestCreds = MatrixClientPeg.getCredentials(); - } + this.guestCreds = MatrixClientPeg.getCredentials(); this.replaceState({ screen: "register", upgradeUsername: MatrixClientPeg.get().getUserIdLocalpart(), From d74a8e405d1b040a42597d2d8d1615b66ea1d682 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 2 Aug 2016 19:00:01 +0100 Subject: [PATCH 197/222] Remove unused function --- src/components/structures/MatrixChat.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 81a2924fb7..d0442ae235 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -577,12 +577,6 @@ module.exports = React.createClass({ this.scrollStateMap[roomId] = state; }, - _doLogin(creds) { - Lifecycle.setLoggedIn(creds, { - syncTimelineLimit: this.props.config.sync_timeline_limit, - }); - }, - _onHaveCredentials: function(credentials) { credentials.guest = Boolean(credentials.guest); Lifecycle.setLoggedIn(credentials); From 0c61c52480331410bbb8adf8fd284e00e0954015 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 2 Aug 2016 19:02:07 +0100 Subject: [PATCH 198/222] Just use Lifecycle.setLoggedIn --- src/components/structures/MatrixChat.js | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index d0442ae235..1861366a96 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -162,7 +162,7 @@ module.exports = React.createClass({ this.props.startingQueryParams.guest_access_token) { this._autoRegisterAsGuest = false; - this._onHaveCredentials({ + Lifecycle.setLoggedIn({ userId: this.props.startingQueryParams.guest_user_id, accessToken: this.props.startingQueryParams.guest_access_token, homeserverUrl: this.getDefaultHsUrl(), @@ -235,7 +235,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().registerGuest().done(function(creds) { console.log("Registered as guest: %s", creds.user_id); self._setAutoRegisterAsGuest(false); - self._onHaveCredentials({ + Lifecycle.setLoggedIn({ userId: creds.user_id, accessToken: creds.access_token, homeserverUrl: hsUrl, @@ -577,11 +577,6 @@ module.exports = React.createClass({ this.scrollStateMap[roomId] = state; }, - _onHaveCredentials: function(credentials) { - credentials.guest = Boolean(credentials.guest); - Lifecycle.setLoggedIn(credentials); - }, - _onLoggedIn: function(credentials) { this.guestCreds = null; this.setState({ @@ -909,13 +904,13 @@ module.exports = React.createClass({ onReturnToGuestClick: function() { // reanimate our guest login if (this.guestCreds) { - this._onHaveCredentials(this.guestCreds); + Lifecycle.setLoggedIn(this.guestCreds); this.guestCreds = null; } }, onRegistered: function(credentials) { - this._onHaveCredentials(credentials); + Lifecycle.setLoggedIn(credentials); // do post-registration stuff // This now goes straight to user settings // We use _setPage since if we wait for @@ -1137,7 +1132,7 @@ module.exports = React.createClass({ } else { return ( Date: Tue, 2 Aug 2016 19:05:05 +0100 Subject: [PATCH 199/222] Comment functions --- src/components/structures/MatrixChat.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 1861366a96..ca7437754b 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -577,6 +577,9 @@ module.exports = React.createClass({ this.scrollStateMap[roomId] = state; }, + /** + * Called when a new logged in session has started + */ _onLoggedIn: function(credentials) { this.guestCreds = null; this.setState({ @@ -585,6 +588,9 @@ module.exports = React.createClass({ }); }, + /** + * Called when the session is logged out + */ _onLoggedOut: function() { this.notifyNewScreen('login'); this.replaceState({ @@ -593,6 +599,10 @@ module.exports = React.createClass({ }); }, + /** + * Called just before the matrix client is started + * (useful for setting options and listeners) + */ _onWillStartClient() { var cli = MatrixClientPeg.get(); From 4825ab8fe78b3ddab42f73250932833d5ae61c3b Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 3 Aug 2016 09:53:02 +0100 Subject: [PATCH 200/222] No need to set options in WillStartClient We can set them any time up to that point --- src/MatrixClientPeg.js | 5 +++-- src/components/structures/MatrixChat.js | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 49326cc22f..54e4cbb646 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -42,8 +42,9 @@ class MatrixClientPeg { this.matrixClient = null; // These are the default options used when Lifecycle.js - // starts the client. These can be altered when the - // 'will_start_client' event is dispatched. + // starts the client. These can be altered at any + // time up to after the 'will_start_client' event is + // finished processing. this.opts = { initialSyncLimit: 20, }; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index ca7437754b..47cbde9af5 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -146,6 +146,10 @@ module.exports = React.createClass({ // whilst logged in as a guest user (so they can change // their mind & log back in) this.guestCreds = null; + + if (this.props.config.sync_timeline_limit) { + MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit; + } }, componentDidMount: function() { @@ -601,15 +605,11 @@ module.exports = React.createClass({ /** * Called just before the matrix client is started - * (useful for setting options and listeners) + * (useful for setting listeners) */ _onWillStartClient() { var cli = MatrixClientPeg.get(); - if (this.props.config.sync_timeline_limit) { - MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit; - } - var self = this; cli.on('sync', function(state, prevState) { self.updateFavicon(state, prevState); From e3a5776eaed5f46ab56e6abe5e023a79c42b7ff2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 3 Aug 2016 09:57:12 +0100 Subject: [PATCH 201/222] We should clear /login off the URL after login --- src/components/structures/MatrixChat.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 47cbde9af5..d7fd90b537 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -586,6 +586,7 @@ module.exports = React.createClass({ */ _onLoggedIn: function(credentials) { this.guestCreds = null; + this.notifyNewScreen(''); this.setState({ screen: undefined, logged_in: true, From 1f17b78371048451856c33a35f8b9030e42514e6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 3 Aug 2016 10:01:23 +0100 Subject: [PATCH 202/222] log if we can't log out --- src/Lifecycle.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index be09551c88..24073fe7a6 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -54,14 +54,17 @@ function logout() { } return MatrixClientPeg.get().logout().then(_onLoggedOut, - // Just throwing an error here is going to be very unhelpful - // if you're trying to log out because your server's down and - // you want to log into a different server, so just forget the - // access token. It's annoying that this will leave the access - // token still valid, but we should fix this by having access - // tokens expire (and if you really think you've been compromised, - // change your password). - _onLoggedOut + (err) => { + // Just throwing an error here is going to be very unhelpful + // if you're trying to log out because your server's down and + // you want to log into a different server, so just forget the + // access token. It's annoying that this will leave the access + // token still valid, but we should fix this by having access + // tokens expire (and if you really think you've been compromised, + // change your password). + console.log("Failed to call logout API: token will not be invalidated"); + _onLoggedOut(); + } ); } From cf7e7d65c8e7122f07e32eb7b7d8e9679d5f1e25 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 3 Aug 2016 10:15:50 +0100 Subject: [PATCH 203/222] Don't start the client twice if we have a guest_access_token --- src/components/structures/MatrixChat.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index d7fd90b537..415c736707 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -153,6 +153,8 @@ module.exports = React.createClass({ }, componentDidMount: function() { + let clientStarted = false; + this._autoRegisterAsGuest = false; if (this.props.enableGuest) { if (!this.getCurrentHsUrl()) { @@ -173,6 +175,7 @@ module.exports = React.createClass({ identityServerUrl: this.getDefaultIsUrl(), guest: true }); + clientStarted = true; } else { this._autoRegisterAsGuest = true; @@ -184,7 +187,9 @@ module.exports = React.createClass({ // Don't auto-register as a guest. This applies if you refresh the page on a // logged in client THEN hit the Sign Out button. this._autoRegisterAsGuest = false; - Lifecycle.startMatrixClient(); + if (!clientStarted) { + Lifecycle.startMatrixClient(); + } } this.focusComposer = false; // scrollStateMap is a map from room id to the scroll state returned by From d9a7d50a03167d6fbf820796ab2296bc468aab69 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 3 Aug 2016 10:46:42 +0100 Subject: [PATCH 204/222] Add an interface for MatrixClientCreds and make MatrixClientPeg functions use it consistently --- src/Lifecycle.js | 5 +---- src/MatrixClientPeg.js | 20 +++++++++++++++++--- src/components/structures/MatrixChat.js | 11 +++++++---- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 24073fe7a6..163e6e9463 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -31,10 +31,7 @@ import dis from './dispatcher'; function setLoggedIn(credentials) { credentials.guest = Boolean(credentials.guest); console.log("onLoggedIn => %s (guest=%s)", credentials.userId, credentials.guest); - MatrixClientPeg.replaceUsingAccessToken( - credentials.homeserverUrl, credentials.identityServerUrl, - credentials.userId, credentials.accessToken, credentials.guest - ); + MatrixClientPeg.replaceUsingCreds(credentials); dis.dispatch({action: 'on_logged_in'}); diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 54e4cbb646..c8b015f99f 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -31,6 +31,14 @@ function deviceId() { return id; } +interface MatrixClientCreds { + homeserverUrl: string, + identityServerUrl: string, + userId: string, + accessToken: string, + guest: boolean, +} + /** * Wrapper object for handling the js-sdk Matrix Client object in the react-sdk * Handles the creation/initialisation of client objects. @@ -70,8 +78,14 @@ class MatrixClientPeg { * Replace this MatrixClientPeg's client with a client instance that has * Home Server / Identity Server URLs and active credentials */ - replaceUsingAccessToken(hs_url, is_url, user_id, access_token, isGuest) { - this._replaceClient(hs_url, is_url, user_id, access_token, isGuest); + replaceUsingCreds(creds: MatrixClientCreds) { + this._replaceClient( + creds.homeserverUrl, + creds.identityServerUrl, + creds.userId, + creds.accessToken, + creds.guest, + ); } _replaceClient(hs_url, is_url, user_id, access_token, isGuest) { @@ -103,7 +117,7 @@ class MatrixClientPeg { } } - getCredentials() { + getCredentials(): MatrixClientCreds { return { homeserverUrl: this.matrixClient.baseUrl, identityServerUrl: this.matrixClient.idBaseUrl, diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 415c736707..b712445dc2 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -340,10 +340,13 @@ module.exports = React.createClass({ var client = MatrixClientPeg.get(); client.loginWithToken(payload.params.loginToken).done(function(data) { - MatrixClientPeg.replaceUsingAccessToken( - client.getHomeserverUrl(), client.getIdentityServerUrl(), - data.user_id, data.access_token - ); + MatrixClientPeg.replaceUsingCreds({ + homeserverUrl: client.getHomeserverUrl(), + identityServerUrl: client.getIdentityServerUrl(), + userId: data.user_id, + accessToken: data.access_token, + guest: false, + }); self.setState({ screen: undefined, logged_in: true From da03af6c1cd415471417d82463545cb510f27c94 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 3 Aug 2016 10:51:58 +0100 Subject: [PATCH 205/222] Fix tests --- test/test-utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test-utils.js b/test/test-utils.js index fc3aaace9f..e2ff5e8c10 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -51,7 +51,7 @@ module.exports.stubClient = function() { // 'sandbox.restore()' doesn't work correctly on inherited methods, // so we do this for each method var methods = ['get', 'unset', 'replaceUsingUrls', - 'replaceUsingAccessToken']; + 'replaceUsingCreds']; for (var i = 0; i < methods.length; i++) { sandbox.stub(peg, methods[i]); } From 2cddf18461851c851d75eea55b21e830325754a5 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Tue, 2 Aug 2016 10:00:12 +0530 Subject: [PATCH 206/222] strip (IRC) displayname suffix from autocomplete Fixes vector-im/vector-web#574 --- src/autocomplete/UserProvider.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index a583592bae..8828f8cb70 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -11,11 +11,11 @@ let instance = null; export default class UserProvider extends AutocompleteProvider { constructor() { super(USER_REGEX, { - keys: ['displayName', 'userId'], + keys: ['name', 'userId'], }); this.users = []; this.fuse = new Fuse([], { - keys: ['displayName', 'userId'], + keys: ['name', 'userId'], }); } @@ -25,11 +25,12 @@ export default class UserProvider extends AutocompleteProvider { if (command) { this.fuse.set(this.users); completions = this.fuse.search(command[0]).map(user => { + const displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done return { completion: user.userId, component: ( ), range From 1f9a396fa53b5da3fd8f6e9e6ea6669d13a92d00 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Wed, 3 Aug 2016 17:51:40 +0530 Subject: [PATCH 207/222] fix: autocomplete to use tab instead of return --- src/components/views/rooms/MessageComposer.js | 8 -------- src/components/views/rooms/MessageComposerInput.js | 13 ++++--------- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 282f7f013f..edd8ed7b9a 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -36,7 +36,6 @@ export default class MessageComposer extends React.Component { this.onInputContentChanged = this.onInputContentChanged.bind(this); this.onUpArrow = this.onUpArrow.bind(this); this.onDownArrow = this.onDownArrow.bind(this); - this.onTab = this.onTab.bind(this); this._tryComplete = this._tryComplete.bind(this); this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this); @@ -143,12 +142,6 @@ export default class MessageComposer extends React.Component { return this.refs.autocomplete.onDownArrow(); } - onTab() { - // FIXME Autocomplete doesn't have an onTab - what is this supposed to do? - // return this.refs.autocomplete.onTab(); - return false; - } - _tryComplete(): boolean { if (this.refs.autocomplete) { return this.refs.autocomplete.onConfirm(); @@ -223,7 +216,6 @@ export default class MessageComposer extends React.Component { tryComplete={this._tryComplete} onUpArrow={this.onUpArrow} onDownArrow={this.onDownArrow} - onTab={this.onTab} tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete onContentChanged={this.onInputContentChanged} />, uploadButton, diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 46abc20ed6..690da28c01 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -422,12 +422,6 @@ export default class MessageComposerInput extends React.Component { if (ev.shiftKey) { return false; } - - if(this.props.tryComplete) { - if(this.props.tryComplete()) { - return true; - } - } const contentState = this.state.editorState.getCurrentContent(); if (!contentState.hasText()) { @@ -519,8 +513,8 @@ export default class MessageComposerInput extends React.Component { } onTab(e) { - if (this.props.onTab) { - if (this.props.onTab()) { + if (this.props.tryComplete) { + if (this.props.tryComplete()) { e.preventDefault(); } } @@ -585,5 +579,6 @@ MessageComposerInput.propTypes = { onDownArrow: React.PropTypes.func, - onTab: React.PropTypes.func + // attempts to confirm currently selected completion, returns whether actually confirmed + tryComplete: React.PropTypes.func, }; From 569b6057c3c35b778728bf237a26e2f0f34a92b8 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 3 Aug 2016 13:27:06 +0100 Subject: [PATCH 208/222] fix upload for video or image files where sniffing the content.info fails --- src/ContentMessages.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/ContentMessages.js b/src/ContentMessages.js index 796c1ed58d..fd18b22d30 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -104,19 +104,25 @@ class ContentMessages { var def = q.defer(); if (file.type.indexOf('image/') == 0) { content.msgtype = 'm.image'; - infoForImageFile(file).then(function (imageInfo) { + infoForImageFile(file).then(imageInfo=>{ extend(content.info, imageInfo); def.resolve(); + }, error=>{ + content.msgtype = 'm.file'; + def.resolve(); }); } else if (file.type.indexOf('audio/') == 0) { content.msgtype = 'm.audio'; def.resolve(); } else if (file.type.indexOf('video/') == 0) { - content.msgtype = 'm.video'; - infoForVideoFile(file).then(function (videoInfo) { - extend(content.info, videoInfo); - def.resolve(); - }); + content.msgtype = 'm.video'; + infoForVideoFile(file).then(videoInfo=>{ + extend(content.info, videoInfo); + def.resolve(); + }, error=>{ + content.msgtype = 'm.file'; + def.resolve(); + }); } else { content.msgtype = 'm.file'; def.resolve(); From a2d64f51197109c3a85d18fbf81704147362b07e Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Wed, 3 Aug 2016 18:04:52 +0530 Subject: [PATCH 209/222] fix: allow up/down normally for no completions Autocomplete current eats up up/down key events by unconditionally returning true for onUpArrow and onDownArrow. Instead, only do that if there are completions actually visible. --- src/components/views/rooms/Autocomplete.js | 6 ++++++ src/components/views/rooms/MessageComposerInput.js | 12 ++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index 95133778ba..32e568e2ba 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -64,6 +64,9 @@ export default class Autocomplete extends React.Component { onUpArrow(): boolean { let completionCount = this.countCompletions(), selectionOffset = (completionCount + this.state.selectionOffset - 1) % completionCount; + if (!completionCount) { + return false; + } this.setSelection(selectionOffset); return true; } @@ -72,6 +75,9 @@ export default class Autocomplete extends React.Component { onDownArrow(): boolean { let completionCount = this.countCompletions(), selectionOffset = (this.state.selectionOffset + 1) % completionCount; + if (!completionCount) { + return false; + } this.setSelection(selectionOffset); return true; } diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 46abc20ed6..1c81a69f16 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -503,18 +503,14 @@ export default class MessageComposerInput extends React.Component { } onUpArrow(e) { - if(this.props.onUpArrow) { - if(this.props.onUpArrow()) { - e.preventDefault(); - } + if (this.props.onUpArrow && this.props.onUpArrow()) { + e.preventDefault(); } } onDownArrow(e) { - if(this.props.onDownArrow) { - if(this.props.onDownArrow()) { - e.preventDefault(); - } + if (this.props.onDownArrow && this.props.onDownArrow()) { + e.preventDefault(); } } From 8e66e6dfdd7c82c626bb90e1c7432d3693255ec1 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Wed, 3 Aug 2016 18:27:49 +0530 Subject: [PATCH 210/222] fix: Switch to opacity: 0 for composer emoji. This seems to be the best option for displaying emoji in the composer. While it means selected emoji don't actually have the selection colour applied, it's the most functional of all the options. Facebook uses the same approach. --- src/RichText.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/RichText.js b/src/RichText.js index a5bc554b95..7cd78a14c9 100644 --- a/src/RichText.js +++ b/src/RichText.js @@ -98,7 +98,7 @@ function unicodeToEmojiUri(str) { return str; } -// Unused for now, due to https://github.com/facebook/draft-js/issues/414 +// Workaround for https://github.com/facebook/draft-js/issues/414 let emojiDecorator = { strategy: (contentBlock, callback) => { findWithRegex(EMOJI_REGEX, contentBlock, callback); @@ -115,7 +115,7 @@ let emojiDecorator = { backgroundPosition: 'center center', overflow: 'hidden', }; - return ({props.children}); + return ({props.children}); }, }; From 8e19532e5a231c3331bc91e5dadcafc8209ebb46 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Wed, 3 Aug 2016 14:09:10 +0100 Subject: [PATCH 211/222] Hover state for badges, now only on the badges themselves --- src/components/views/rooms/RoomTile.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 5e367ffd80..30245f61ee 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -186,9 +186,9 @@ module.exports = React.createClass({ } if (this.state.areNotifsMuted && !(this.state.badgeHover || this.state.menu)) { - badge =
    ; + badge =
    ; } else { - badge =
    { badgeContent }
    ; + badge =
    { badgeContent }
    ; } var label; @@ -234,7 +234,7 @@ module.exports = React.createClass({
    -
    +
    { label } { badge }
    From 55f4d23625af7d71a25eb2c7c79dacbde63a59e0 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 3 Aug 2016 14:18:09 +0100 Subject: [PATCH 212/222] MemberDeviceInfo: Use the device name, where available we now have device name for e2e devices: use it! --- src/components/views/rooms/MemberDeviceInfo.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/MemberDeviceInfo.js b/src/components/views/rooms/MemberDeviceInfo.js index b7ddf9b2ce..7e684c89a2 100644 --- a/src/components/views/rooms/MemberDeviceInfo.js +++ b/src/components/views/rooms/MemberDeviceInfo.js @@ -97,9 +97,11 @@ module.exports = React.createClass({ ); } + var deviceName = this.props.device.display_name || this.props.device.id; + return (
    -
    {this.props.device.id}
    +
    {deviceName}
    {indicator} {verifyButton} {blockButton} From f4d41b78473200be4c9e19f433e3e97dbdb2413d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 3 Aug 2016 14:19:54 +0100 Subject: [PATCH 213/222] DevicesPanel: use device_id as a placeholder A device may have no display_name set, in which case we probably want to use the device_id as a placeholder. --- src/components/views/settings/DevicesPanelEntry.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/DevicesPanelEntry.js b/src/components/views/settings/DevicesPanelEntry.js index 6858e62102..b660f196c8 100644 --- a/src/components/views/settings/DevicesPanelEntry.js +++ b/src/components/views/settings/DevicesPanelEntry.js @@ -111,7 +111,9 @@ export default class DevicesPanelEntry extends React.Component {
    + onSubmit={this._onDisplayNameChanged} + placeholder={device.device_id} + />
    {lastSeen} From c1cfbd6b59ed9b4a7b1eaea7706b11b50aea00fe Mon Sep 17 00:00:00 2001 From: wmwragg Date: Wed, 3 Aug 2016 14:47:53 +0100 Subject: [PATCH 214/222] Corrected comment typo --- 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 30245f61ee..602ed4ee04 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -66,7 +66,7 @@ module.exports = React.createClass({ onAction: function(payload) { switch (payload.action) { case 'notification_change': - // Is the notificaion about this room + // Is the notification about this room? if (payload.roomId === this.props.room.roomId) { this.setState( { areNotifsMuted : payload.isMuted }); } From 8a57881618d30bcb2469791268b8786d55a5651c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 3 Aug 2016 15:23:12 +0100 Subject: [PATCH 215/222] Add more logging to TimelinePanel-test In an attempt to figure out why this is timing out sometimes, add even more debugging. --- .../structures/TimelinePanel-test.js | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/test/components/structures/TimelinePanel-test.js b/test/components/structures/TimelinePanel-test.js index cd9d86cd64..7a603d138f 100644 --- a/test/components/structures/TimelinePanel-test.js +++ b/test/components/structures/TimelinePanel-test.js @@ -224,7 +224,7 @@ describe('TimelinePanel', function() { var scrollDefer; var panel = ReactDOM.render( - {scrollDefer.resolve()}} />, + {scrollDefer.resolve()}} />, parentDiv ); console.log("TimelinePanel rendered"); @@ -238,17 +238,29 @@ describe('TimelinePanel', function() { // the TimelinePanel fires a scroll event var awaitScroll = function() { scrollDefer = q.defer(); - return scrollDefer.promise; + return scrollDefer.promise.then(() => { + console.log("got scroll event; scrollTop now " + + scrollingDiv.scrollTop); + }); }; + function setScrollTop(scrollTop) { + const before = scrollingDiv.scrollTop; + scrollingDiv.scrollTop = scrollTop; + console.log("setScrollTop: before update: " + before + + "; assigned: " + scrollTop + + "; after update: " + scrollingDiv.scrollTop); + } + function backPaginate() { - scrollingDiv.scrollTop = 0; + console.log("back paginating..."); + setScrollTop(0); return awaitScroll().then(() => { if(scrollingDiv.scrollTop > 0) { // need to go further return backPaginate(); } - console.log("paginated to end."); + console.log("paginated to start."); // hopefully, we got to the start of the timeline expect(messagePanel.props.backPaginating).toBe(false); @@ -262,7 +274,6 @@ describe('TimelinePanel', function() { expect(messagePanel.props.suppressFirstDateSeparator).toBe(true); // back-paginate until we hit the start - console.log("back paginating..."); return backPaginate(); }).then(() => { expect(messagePanel.props.suppressFirstDateSeparator).toBe(false); @@ -271,8 +282,7 @@ describe('TimelinePanel', function() { // we should now be able to scroll down, and paginate in the other // direction. - console.log("scrollingDiv.scrollTop is " + scrollingDiv.scrollTop); - console.log("Going to set it to " + scrollingDiv.scrollHeight); + setScrollTop(scrollingDiv.scrollHeight); scrollingDiv.scrollTop = scrollingDiv.scrollHeight; return awaitScroll(); }).then(() => { From a5384d32e21ff2b32af59cb8de5968e7d844bb9e Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 3 Aug 2016 16:28:37 +0100 Subject: [PATCH 216/222] Copy opts to set pendingEventOrdering --- src/Lifecycle.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 163e6e9463..d91b49ad7c 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -19,6 +19,7 @@ import Notifier from './Notifier' import UserActivity from './UserActivity'; import Presence from './Presence'; import dis from './dispatcher'; +import utils from 'matrix-js-sdk/lib/utils'; /** * Transitions to a logged-in state using the given credentials @@ -80,11 +81,9 @@ function startMatrixClient() { UserActivity.start(); Presence.start(); - // the react sdk doesn't work without this, so don't allow - // it to be overridden (and modify the global object so at - // at least the app can see we've changed it) - MatrixClientPeg.opts.pendingEventOrdering = "detached"; - MatrixClientPeg.get().startClient(MatrixClientPeg.opts); + let opts = utils.deepCopy(MatrixClientPeg.opts); + opts.pendingEventOrdering = "detached"; + MatrixClientPeg.get().startClient(opts); } function _onLoggedOut() { From b95a1c4a4b388bef4600e1593260444b11cff819 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 3 Aug 2016 16:31:42 +0100 Subject: [PATCH 217/222] Just doc with the MatrixClientCreds object --- src/Lifecycle.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index d91b49ad7c..3bf52b6cf2 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -23,11 +23,7 @@ import utils from 'matrix-js-sdk/lib/utils'; /** * Transitions to a logged-in state using the given credentials - * @param {string} credentials.homeserverUrl The base HS URL - * @param {string} credentials.identityServerUrl The base IS URL - * @param {string} credentials.userId The full Matrix User ID - * @param {string} credentials.accessToken The session access token - * @param {boolean} credentials.guest True if the session is a guest session + * @param {MatrixClientCreds} credentials The credentials to use */ function setLoggedIn(credentials) { credentials.guest = Boolean(credentials.guest); From 9bf45fb556ace7fee6bd02cfb8ce12eb7e5279fb Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 3 Aug 2016 16:39:47 +0100 Subject: [PATCH 218/222] Add start wrapper in MatrixClientPeg to handle the opts dictionary --- src/Lifecycle.js | 4 +--- src/MatrixClientPeg.js | 6 ++++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 3bf52b6cf2..f6aead4786 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -77,9 +77,7 @@ function startMatrixClient() { UserActivity.start(); Presence.start(); - let opts = utils.deepCopy(MatrixClientPeg.opts); - opts.pendingEventOrdering = "detached"; - MatrixClientPeg.get().startClient(opts); + MatrixClientPeg.start(); } function _onLoggedOut() { diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index c8b015f99f..0d5af6ccf4 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -88,6 +88,12 @@ class MatrixClientPeg { ); } + start() { + const opts = utils.deepCopy(MatrixClientPeg.opts); + opts.pendingEventOrdering = "detached"; + this.get().startClient(opts); + }, + _replaceClient(hs_url, is_url, user_id, access_token, isGuest) { if (localStorage) { try { From 009c768b72a77fc6a0564b0461d27bdc0e2a2638 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 3 Aug 2016 16:41:22 +0100 Subject: [PATCH 219/222] Comma fail --- src/MatrixClientPeg.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 0d5af6ccf4..63032fb0bd 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -92,7 +92,7 @@ class MatrixClientPeg { const opts = utils.deepCopy(MatrixClientPeg.opts); opts.pendingEventOrdering = "detached"; this.get().startClient(opts); - }, + } _replaceClient(hs_url, is_url, user_id, access_token, isGuest) { if (localStorage) { From 0919e4146907ea8672d857f5d5a75e3ed8754ae8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 3 Aug 2016 16:45:23 +0100 Subject: [PATCH 220/222] Fix MatrixClientPeg.start() Move import & use `this` --- src/Lifecycle.js | 1 - src/MatrixClientPeg.js | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index f6aead4786..7c507c2c50 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -19,7 +19,6 @@ import Notifier from './Notifier' import UserActivity from './UserActivity'; import Presence from './Presence'; import dis from './dispatcher'; -import utils from 'matrix-js-sdk/lib/utils'; /** * Transitions to a logged-in state using the given credentials diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 63032fb0bd..3599c55f13 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -17,6 +17,7 @@ limitations under the License. 'use strict'; import Matrix from 'matrix-js-sdk'; +import utils from 'matrix-js-sdk/lib/utils'; const localStorage = window.localStorage; @@ -89,7 +90,7 @@ class MatrixClientPeg { } start() { - const opts = utils.deepCopy(MatrixClientPeg.opts); + const opts = utils.deepCopy(this.opts); opts.pendingEventOrdering = "detached"; this.get().startClient(opts); } From b32a19a0f1828125c96e1cb3890b80374281a955 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 3 Aug 2016 17:23:09 +0100 Subject: [PATCH 221/222] Comments --- src/MatrixClientPeg.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 3599c55f13..e6d0e7f3f7 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -50,10 +50,10 @@ class MatrixClientPeg { constructor() { this.matrixClient = null; - // These are the default options used when Lifecycle.js - // starts the client. These can be altered at any - // time up to after the 'will_start_client' event is - // finished processing. + // These are the default options used when when the + // client is started in 'start'. These can be altered + // at any time up to after the 'will_start_client' + // event is finished processing. this.opts = { initialSyncLimit: 20, }; @@ -91,6 +91,7 @@ class MatrixClientPeg { start() { const opts = utils.deepCopy(this.opts); + // the react sdk doesn't work without this, so don't allow opts.pendingEventOrdering = "detached"; this.get().startClient(opts); } From 6632322029b467109388f6e71374664d4c5c2401 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 3 Aug 2016 19:56:04 +0100 Subject: [PATCH 222/222] spell out that conference calling is hit and miss --- src/CallHandler.js | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/CallHandler.js b/src/CallHandler.js index 5bd2d20ae8..9118ee1973 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -250,12 +250,24 @@ function _onAction(payload) { }); } else { - ConferenceHandler.createNewMatrixCall( - MatrixClientPeg.get(), payload.room_id - ).done(function(call) { - placeCall(call); - }, function(err) { - console.error("Failed to setup conference call: %s", err); + var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + Modal.createDialog(QuestionDialog, { + title: "Warning!", + description: "Conference calling in Vector is in development and may not be reliable.", + onFinished: confirm=>{ + if (confirm) { + ConferenceHandler.createNewMatrixCall( + MatrixClientPeg.get(), payload.room_id + ).done(function(call) { + placeCall(call); + }, function(err) { + Modal.createDialog(ErrorDialog, { + title: "Failed to set up conference call", + description: "Conference call failed: " + err, + }); + }); + } + }, }); } break;