From a967ddd1cb4ef0afd95189fa19cc1a6bc90e26e4 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 13 Nov 2016 14:10:46 +0000 Subject: [PATCH 001/260] quick and dirty support for custom welcome pages, with an example for geektime techfest --- .DS_Store | Bin 6148 -> 6148 bytes src/PageTypes.js | 1 + src/components/structures/LoggedInView.js | 10 ++++++++++ src/components/structures/MatrixChat.js | 22 +++++++++++++++++++-- src/components/views/avatars/BaseAvatar.js | 2 +- 5 files changed, 32 insertions(+), 3 deletions(-) diff --git a/.DS_Store b/.DS_Store index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..d2a053831af5212a0eeadcbe98a90fb570842166 100644 GIT binary patch delta 128 zcmZoMXfc=|&e%3FQEZ}~q6iZM0|O%ig8)Nua#DVN4v@#dJTXy29VE`oki(G4kd&5! zB*#$9P{fc76Jp$$7|K4`K!jy8HwO;~W822W@640=MHEFr+7*B}0f>RdGaLY7hRu#5 G` if (!this.props.collapse_rhs) right_panel = break; + + case PageTypes.HomePage: + page_element = + if (!this.props.collapse_rhs) right_panel = + break; + case PageTypes.UserView: page_element = null; // deliberately null for now right_panel = diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 945088106b..9a3ff8f95c 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -410,6 +410,10 @@ module.exports = React.createClass({ this._setPage(PageTypes.RoomDirectory); this.notifyNewScreen('directory'); break; + case 'view_home_page': + this._setPage(PageTypes.HomePage); + this.notifyNewScreen('home'); + break; case 'view_create_chat': this._createChat(); break; @@ -629,7 +633,12 @@ module.exports = React.createClass({ )[0].roomId; self.setState({ready: true, currentRoomId: firstRoom, page_type: PageTypes.RoomView}); } else { - self.setState({ready: true, page_type: PageTypes.RoomDirectory}); + if (self.props.config.home_page) { + self.setState({ready: true, page_type: PageTypes.HomePage}); + } + else { + self.setState({ready: true, page_type: PageTypes.RoomDirectory}); + } } } else { self.setState({ready: true, page_type: PageTypes.RoomView}); @@ -649,7 +658,12 @@ module.exports = React.createClass({ } else { // There is no information on presentedId // so point user to fallback like /directory - self.notifyNewScreen('directory'); + if (self.props.config.home_page) { + self.notifyNewScreen('home'); + } + else { + self.notifyNewScreen('directory'); + } } dis.dispatch({action: 'focus_composer'}); @@ -703,6 +717,10 @@ module.exports = React.createClass({ dis.dispatch({ action: 'view_user_settings', }); + } else if (screen == 'home') { + dis.dispatch({ + action: 'view_home_page', + }); } else if (screen == 'directory') { dis.dispatch({ action: 'view_room_directory', diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index 47f0a76891..4025859478 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -138,7 +138,7 @@ module.exports = React.createClass({ const { name, idName, title, url, urls, width, height, resizeMethod, - defaultToInitialLetter, + defaultToInitialLetter, viewUserOnClick, ...otherProps } = this.props; From 69f6393ed9fbb19c7460ac9fac3a61af851e3794 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 13 Nov 2016 14:13:21 +0000 Subject: [PATCH 002/260] try to make joining rooms more obvious --- src/components/views/rooms/RoomPreviewBar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index baeae4807d..7c03ca89a4 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -146,7 +146,7 @@ module.exports = React.createClass({
You are trying to access { name }.
- Would you like to join in order to participate in the discussion? + Click here to join the discussion!
); From 8a5678efdd74f1997d65533352ec800d0482cc67 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 19 Nov 2016 01:20:09 +0200 Subject: [PATCH 003/260] boldify the preview bar click --- src/components/views/rooms/RoomPreviewBar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index 7c03ca89a4..d41fed04b4 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -146,7 +146,7 @@ module.exports = React.createClass({
You are trying to access { name }.
- Click here to join the discussion! + Click here to join the discussion!
); From 4d2926485b914843b3b462403c5825f6fa21adf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20L=C3=B6thberg?= Date: Tue, 29 Nov 2016 20:56:48 +0100 Subject: [PATCH 004/260] Replace marked with commonmark MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Marked has some annoying bugs, and the author is inactive, so replace it with commonmark.js, which is the reference JavaScript implementation of CommonMark. CommonMark is also preferable since it has a specification, and a conformance test suite to make sure that parsers are correct. Signed-off-by: Johannes Löthberg --- package.json | 2 +- src/Markdown.js | 92 +++++++++++++------------------------------------ 2 files changed, 24 insertions(+), 70 deletions(-) diff --git a/package.json b/package.json index a25260e35d..e93aadf8d1 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.3", "lodash": "^4.13.1", - "marked": "^0.3.5", + "commonmark": "^0.27.0", "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop", "optimist": "^0.6.1", "q": "^1.4.1", diff --git a/src/Markdown.js b/src/Markdown.js index a7b267b110..80805144e2 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -14,20 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import marked from 'marked'; - -// marked only applies the default options on the high -// level marked() interface, so we do it here. -const marked_options = Object.assign({}, marked.defaults, { - gfm: true, - tables: true, - breaks: true, - pedantic: false, - sanitize: true, - smartLists: true, - smartypants: false, - xhtml: true, // return self closing tags (ie.
not
) -}); +import commonmark from 'commonmark'; /** * Class that wraps marked, adding the ability to see whether @@ -36,16 +23,7 @@ const marked_options = Object.assign({}, marked.defaults, { */ export default class Markdown { constructor(input) { - const lexer = new marked.Lexer(marked_options); - this.tokens = lexer.lex(input); - } - - _copyTokens() { - // copy tokens (the parser modifies its input arg) - const tokens_copy = this.tokens.slice(); - // it also has a 'links' property, because this is javascript - // and why wouldn't you have an array that also has properties? - return Object.assign(tokens_copy, this.tokens); + this.input = input } isPlainText() { @@ -64,65 +42,41 @@ export default class Markdown { is_plain = false; } - const dummy_renderer = {}; - for (const k of Object.keys(marked.Renderer.prototype)) { + const dummy_renderer = new commonmark.HtmlRenderer(); + for (const k of Object.keys(commonmark.HtmlRenderer.prototype)) { dummy_renderer[k] = setNotPlain; } // text and paragraph are just text - dummy_renderer.text = function(t){return t;} - dummy_renderer.paragraph = function(t){return t;} + dummy_renderer.text = function(t) { return t; } + dummy_renderer.paragraph = function(t) { return t; } - // ignore links where text is just the url: - // this ignores plain URLs that markdown has - // detected whilst preserving markdown syntax links - dummy_renderer.link = function(href, title, text) { - if (text != href) { - is_plain = false; - } - } - - const dummy_options = Object.assign({}, marked_options, { - renderer: dummy_renderer, - }); - const dummy_parser = new marked.Parser(dummy_options); - dummy_parser.parse(this._copyTokens()); + const dummy_parser = new commonmark.Parser(); + dummy_renderer.render(dummy_parser.parse(this.input)); return is_plain; } toHTML() { - const real_renderer = new marked.Renderer(); - real_renderer.link = function(href, title, text) { - // prevent marked from turning plain URLs - // into links, because its algorithm is fairly - // poor. Let's send plain URLs rather than - // badly linkified ones (the linkifier Vector - // uses on message display is way better, eg. - // handles URLs with closing parens at the end). - if (text == href) { - return href; - } - return marked.Renderer.prototype.link.apply(this, arguments); - } + const parser = new commonmark.Parser(); - real_renderer.paragraph = (text) => { - // The tokens at the top level are the 'blocks', so if we - // have more than one, there are multiple 'paragraphs'. - // If there is only one top level token, just return the + const renderer = new commonmark.HtmlRenderer({safe: true}); + const real_paragraph = renderer.paragraph; + renderer.paragraph = function(node, entering) { + // If there is only one top level node, just return the // bare text: it's a single line of text and so should be - // 'inline', rather than necessarily wrapped in its own - // p tag. If, however, we have multiple tokens, each gets + // 'inline', rather than unnecessarily wrapped in its own + // p tag. If, however, we have multiple nodes, each gets // its own p tag to keep them as separate paragraphs. - if (this.tokens.length == 1) { - return text; + var par = node; + while (par.parent) { + par = par.parent + } + if (par.firstChild != par.lastChild) { + real_paragraph.bind(this)(node, entering); } - return '

' + text + '

'; } - const real_options = Object.assign({}, marked_options, { - renderer: real_renderer, - }); - const real_parser = new marked.Parser(real_options); - return real_parser.parse(this._copyTokens()); + var parsed = parser.parse(this.input); + return renderer.render(parsed); } } From 5f160d2e7f86f435db73b7d17799a8a35bb83514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20L=C3=B6thberg?= Date: Wed, 30 Nov 2016 01:03:05 +0100 Subject: [PATCH 005/260] Markdown: Use .call instead of .bind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johannes Löthberg --- src/Markdown.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markdown.js b/src/Markdown.js index 80805144e2..18c888b541 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -72,7 +72,7 @@ export default class Markdown { par = par.parent } if (par.firstChild != par.lastChild) { - real_paragraph.bind(this)(node, entering); + real_paragraph.call(this, node, entering); } } From 6e2a75bbee54540d69c0d03057d20e850556e200 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 11 Jan 2017 11:27:07 +0000 Subject: [PATCH 006/260] Fix redacted member events being visible This was due to the `MemberEventListSummary` not ignoring redacted joins/leaves. This was also causing bug #2765. --- src/components/structures/MessagePanel.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index affa33a3e8..c04bec4b35 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -295,8 +295,8 @@ module.exports = React.createClass({ var last = (i == lastShownEventIndex); - // Wrap consecutive member events in a ListSummary - if (isMembershipChange(mxEv)) { + // Wrap consecutive member events in a ListSummary, ignore if redacted + if (isMembershipChange(mxEv) && EventTile.haveTileForEvent(mxEv)) { let ts1 = mxEv.getTs(); // Ensure that the key of the MemberEventListSummary does not change with new // member events. This will prevent it from being re-created unnecessarily, and @@ -317,6 +317,11 @@ module.exports = React.createClass({ for (;i + 1 < this.props.events.length; i++) { let collapsedMxEv = this.props.events[i + 1]; + // Ignore redacted member events + if (!EventTile.haveTileForEvent(collapsedMxEv)) { + continue; + } + if (!isMembershipChange(collapsedMxEv) || this._wantsDateSeparator(this.props.events[i], collapsedMxEv.getDate())) { break; From baaf827c48c5c0eda422006b4b964ed628fc205b Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 11 Jan 2017 17:03:14 +0000 Subject: [PATCH 007/260] Use `getStateKey` instead of `getSender` This makes sure that the kickee is not the user used for the MemberEventListSummary --- src/components/views/elements/MemberEventListSummary.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index bff2ce3d05..518439b1c7 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -181,7 +181,7 @@ module.exports = React.createClass({ }; eventsToRender.forEach((e) => { - const userId = e.getSender(); + const userId = e.getStateKey(); // Initialise a user's events if (!userEvents[userId]) { userEvents[userId] = {first: null, last: null}; From a4aa74e85ae2d029fee211042af39d5cd8df1e62 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 11 Jan 2017 17:18:37 +0000 Subject: [PATCH 008/260] Build the js-sdk in the CI script now it's transpiled --- jenkins.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/jenkins.sh b/jenkins.sh index b318b586e2..1e281fa016 100755 --- a/jenkins.sh +++ b/jenkins.sh @@ -12,6 +12,9 @@ set -x # install the other dependencies npm install +# we may be using a dev branch of js-sdk in which case we need to build it +(cd node_modules/matrix-js-sdk && npm run build) + # run the mocha tests npm run test From 5142cb63c85b7e2bef41276adf40d48fc00d4423 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 11 Jan 2017 18:18:46 +0000 Subject: [PATCH 009/260] s/build/compile/ for js sdk --- jenkins.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jenkins.sh b/jenkins.sh index 1e281fa016..788f5f03d8 100755 --- a/jenkins.sh +++ b/jenkins.sh @@ -13,7 +13,7 @@ set -x npm install # we may be using a dev branch of js-sdk in which case we need to build it -(cd node_modules/matrix-js-sdk && npm run build) +(cd node_modules/matrix-js-sdk && npm run compile) # run the mocha tests npm run test From fcc40ee37275b8a76a7d3c5ed7a92324a689a0a8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 11 Jan 2017 19:03:20 +0000 Subject: [PATCH 010/260] It's now just build --- jenkins.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jenkins.sh b/jenkins.sh index 788f5f03d8..1e281fa016 100755 --- a/jenkins.sh +++ b/jenkins.sh @@ -13,7 +13,7 @@ set -x npm install # we may be using a dev branch of js-sdk in which case we need to build it -(cd node_modules/matrix-js-sdk && npm run compile) +(cd node_modules/matrix-js-sdk && npm run build) # run the mocha tests npm run test From cc8b3d4ba400e49ba5b95575b9becab5cf664174 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 12 Jan 2017 13:46:12 +0000 Subject: [PATCH 011/260] spell out email invites better --- src/components/views/dialogs/ChatInviteDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index aa694f6838..e9a041357f 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -48,7 +48,7 @@ module.exports = React.createClass({ title: "Start a chat", description: "Who would you like to communicate with?", value: "", - placeholder: "User ID, Name or email", + placeholder: "Email, name or matrix ID", button: "Start Chat", focus: true }; From 2708859b34b588ef335b60bdce439f830fdc0808 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 12 Jan 2017 16:38:48 +0000 Subject: [PATCH 013/260] npm install the js-sdk otherwise we don't have uglifyjs --- jenkins.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jenkins.sh b/jenkins.sh index 1e281fa016..3b4e31fd7f 100755 --- a/jenkins.sh +++ b/jenkins.sh @@ -13,7 +13,7 @@ set -x npm install # we may be using a dev branch of js-sdk in which case we need to build it -(cd node_modules/matrix-js-sdk && npm run build) +(cd node_modules/matrix-js-sdk && npm install) # run the mocha tests npm run test From 4ca4441f77b331f06f6098b79a702793c5d2a7e1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 12 Jan 2017 17:09:49 +0000 Subject: [PATCH 015/260] Build js-sdk in travis too --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 9d6a114391..2221b7295d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,6 @@ language: node_js node_js: - node # Latest stable version of nodejs. +install: + - npm install + - (cd node_modules/matrix-react-sdk && npm install) From 3706d6e32ad4e17861804c2e2b92d80ffc9991ef Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 12 Jan 2017 17:14:51 +0000 Subject: [PATCH 016/260] js-sdk, not react-sdk --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2221b7295d..6d68b66a1c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,4 +3,4 @@ node_js: - node # Latest stable version of nodejs. install: - npm install - - (cd node_modules/matrix-react-sdk && npm install) + - (cd node_modules/matrix-js-sdk && npm install) From d2d78919cefda89c37c3abb8d20700793bde3cd3 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 12 Jan 2017 18:55:53 +0000 Subject: [PATCH 017/260] Overhaul MELS to deal with causality, kicks, etc. The MELS can now deal with arbitrary sequences of transitions per user, where a transition is a change in membership. A transition can be joined, left, invite_reject, invite_withdrawal, invited, banned, unbanned or kicked. Repeated segments (modulo 1 and 2), such as joined,left,joined,left,joined will be handled and will be rendered as " ... and 10 others joined and left 2 times and then joined". The repeated segments are assumed to be at the beginning of the sequence. This could be improved to handle arbitrary repeated sequences. --- src/components/structures/MessagePanel.js | 1 - .../views/elements/MemberEventListSummary.js | 247 +++++++++++------- 2 files changed, 150 insertions(+), 98 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index c04bec4b35..6cbf708252 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -281,7 +281,6 @@ module.exports = React.createClass({ var isMembershipChange = (e) => e.getType() === 'm.room.member' - && ['join', 'leave'].indexOf(e.getContent().membership) !== -1 && (!e.getPrevContent() || e.getContent().membership !== e.getPrevContent().membership); for (i = 0; i < this.props.events.length; i++) { diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index 518439b1c7..ab4a89eb69 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -24,7 +24,7 @@ module.exports = React.createClass({ events: React.PropTypes.array.isRequired, // An array of EventTiles to render when expanded children: React.PropTypes.array.isRequired, - // The maximum number of names to show in either the join or leave summaries + // The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left" summaryLength: React.PropTypes.number, // The maximum number of avatars to display in the summary avatarsMaxLength: React.PropTypes.number, @@ -40,7 +40,7 @@ module.exports = React.createClass({ getDefaultProps: function() { return { - summaryLength: 3, + summaryLength: 1, threshold: 3, avatarsMaxLength: 5, }; @@ -52,88 +52,122 @@ module.exports = React.createClass({ }); }, - _getEventSenderName: function(ev) { - if (!ev) { - return 'undefined'; - } - return ev.sender.name || ev.event.content.displayname || ev.getSender(); - }, - - _renderNameList: function(events) { - if (events.length === 0) { + _renderNameList: function(users) { + if (users.length === 0) { return null; } - let originalNumber = events.length; - events = events.slice(0, this.props.summaryLength); - let lastEvent = events.pop(); + let originalNumber = users.length; - let names = events.map((ev) => { - return this._getEventSenderName(ev); - }).join(', '); - - let lastName = this._getEventSenderName(lastEvent); - if (names.length === 0) { - // special-case for a single event - return lastName; - } + users = users.slice(0, this.props.summaryLength); let remaining = originalNumber - this.props.summaryLength; - if (remaining > 0) { - // name1, name2, name3, and 100 others - return names + ', ' + lastName + ', and ' + remaining + ' others'; + if (remaining < 0) { + remaining = 0; + } + let other = " other" + (remaining > 1 ? "s" : ""); + + return this._renderCommaSeparatedList(users, remaining) + (remaining ? ' and ' + remaining + other : ''); + }, + + // Test whether the first n items repeat for the duration + // e.g. [1,2,3,4,1,2,3] would resolve true for n = 4 + _isRepeatedSequence: function(transitions, n) { + let count = 0; + for (let i = 0; i < transitions.length; i++) { + if (transitions[i % n] !== transitions[i]) { + return null; + } + } + return true; + }, + + _renderCommaSeparatedList(items, disableAnd) { + if (disableAnd) { + return items.join(', '); + } + if (items.length === 0) { + return ""; + } else if (items.length === 1) { + return items[0]; } else { - // name1, name2 and name3 - return names + ' and ' + lastName; + let last = items.pop(); + return items.join(', ') + ' and ' + last; } }, - _renderSummary: function(joinEvents, leaveEvents) { - let joiners = this._renderNameList(joinEvents); - let leavers = this._renderNameList(leaveEvents); + _getDescriptionForTransition(t, plural) { + let beConjugated = plural ? "were" : "was"; + let invitation = plural ? "invitations" : "an invitation"; - let joinSummary = null; - if (joiners) { - joinSummary = ( - - {joiners} joined the room - - ); - } - let leaveSummary = null; - if (leavers) { - leaveSummary = ( - - {leavers} left the room - - ); + switch (t) { + case 'joined': return "joined"; + case 'left': return "left"; + case 'invite_reject': return "rejected " + invitation; + case 'invite_withdrawal': return "withdrew " + invitation; + case 'invited': return beConjugated + " invited"; + case 'banned': return beConjugated + " banned"; + case 'unbanned': return beConjugated + " unbanned"; + case 'kicked': return beConjugated + " kicked"; } - // The joinEvents and leaveEvents are representative of the net movement - // per-user, and so it is possible that the total net movement is nil, - // whilst there are some events in the expanded list. If the total net - // movement is nil, then neither joinSummary nor leaveSummary will be - // truthy, so return null. - if (!joinSummary && !leaveSummary) { + return null; + }, + + _renderSummary: function(eventAggregates) { + let summaries = Object.keys(eventAggregates).map((transitions) => { + let nameList = this._renderNameList(eventAggregates[transitions]); + + let repeats = 1; + let repeatExtra = 0; + + let splitTransitions = transitions.split(','); + let describedTransitions = splitTransitions; + let plural = eventAggregates[transitions].length > 1; + + for (let modulus = 1; modulus <= 2; modulus++) { + // Sequences that are repeating through modulus transitions will be truncated + if (this._isRepeatedSequence(describedTransitions, modulus)) { + // Extra repeating sequence on the end that should be treated separately + // so as to avoid j,l,j,l,j => "... joined and left 2.5 times" + repeatExtra = describedTransitions.length % modulus; + + repeats = (describedTransitions.length - repeatExtra) / modulus; + describedTransitions = describedTransitions.slice(0, modulus); + break; + } + } + + let numberOfTimes = repeats > 1 ? " " + repeats + " times" : ""; + + let descs = describedTransitions.map((t) => { + return this._getDescriptionForTransition(t, plural); + }); + + let afterRepeatDescs = splitTransitions.slice(splitTransitions.length - repeatExtra).map((t) => { + return this._getDescriptionForTransition(t, plural); + }); + + let desc = this._renderCommaSeparatedList(descs); + let afterRepeatDesc = this._renderCommaSeparatedList(afterRepeatDescs); + + return nameList + " " + desc + numberOfTimes + (afterRepeatDesc ? " and then " + afterRepeatDesc : ""); + }); + + if (!summaries) { return null; } return ( - {joinSummary}{joinSummary && leaveSummary?'; ':''} - {leaveSummary}.  + {summaries.join(", ")} ); }, - _renderAvatars: function(events) { - let avatars = events.slice(0, this.props.avatarsMaxLength).map((e) => { + _renderAvatars: function(roomMembers) { + let avatars = roomMembers.slice(0, this.props.avatarsMaxLength).map((m) => { return ( - + ); }); @@ -157,6 +191,32 @@ module.exports = React.createClass({ ); }, + _getTransition: function(e) { + switch (e.getContent().membership) { + case 'invite': return 'invited'; + case 'ban': return 'banned'; + case 'join': return 'joined'; + case 'leave': + if (e.getSender() === e.getStateKey()) { + switch (e.getPrevContent().membership) { + case 'invite': return 'invite_reject'; + default: return 'left'; + } + } + switch (e.getPrevContent().membership) { + case 'invite': return 'invite_withdrawal'; + case 'ban': return 'unbanned'; + case 'join': return 'kicked'; + default: return 'left'; + } + default: return null; + } + }, + + _getTransitionSequence: function(events) { + return events.map(this._getTransition); + }, + render: function() { let eventsToRender = this.props.events; let fewEvents = eventsToRender.length < this.props.threshold; @@ -175,61 +235,54 @@ module.exports = React.createClass({ ); } - // Map user IDs to the first and last member events in eventsToRender for each user + // Map user IDs to all of the user's member events in eventsToRender let userEvents = { - // $userId : {first : e0, last : e1} + // $userId : [] }; eventsToRender.forEach((e) => { const userId = e.getStateKey(); // Initialise a user's events if (!userEvents[userId]) { - userEvents[userId] = {first: null, last: null}; + userEvents[userId] = []; } - if (!userEvents[userId].first) { - userEvents[userId].first = e; - } - userEvents[userId].last = e; + userEvents[userId].push(e); }); - // Populate the join/leave event arrays with events that represent what happened - // overall to a user's membership. If no events are added to either array for a - // particular user, they will be considered a user that "joined and left". - let joinEvents = []; - let leaveEvents = []; - let joinedAndLeft = 0; - let senders = Object.keys(userEvents); - senders.forEach( + // A map of agregate type to arrays of display names. Each aggregate type + // is a comma-delimited string of transitions, e.g. "joined,left,kicked". + // The array of display names is the array of users who went through that + // sequence during eventsToRender. + let aggregate = { + // $aggregateType : []:string + }; + let avatarMembers = []; + + let users = Object.keys(userEvents); + users.forEach( (userId) => { - let firstEvent = userEvents[userId].first; - let lastEvent = userEvents[userId].last; + let displayName = userEvents[userId][0].getContent().displayname || userId; - // Membership BEFORE eventsToRender - let previousMembership = firstEvent.getPrevContent().membership || "leave"; - - // If the last membership event differs from previousMembership, use that. - if (previousMembership !== lastEvent.getContent().membership) { - if (lastEvent.event.content.membership === 'join') { - joinEvents.push(lastEvent); - } else if (lastEvent.event.content.membership === 'leave') { - leaveEvents.push(lastEvent); - } - } else { - // Increment the number of users whose membership change was nil overall - joinedAndLeft++; + let seq = this._getTransitionSequence(userEvents[userId]); + if (!aggregate[seq]) { + aggregate[seq] = []; } + + // Assumes display names are unique + if (aggregate[seq].indexOf(displayName) === -1) { + aggregate[seq].push(displayName); + } + avatarMembers.push(userEvents[userId][0].target); } ); - let avatars = this._renderAvatars(joinEvents.concat(leaveEvents)); - let summary = this._renderSummary(joinEvents, leaveEvents); + let avatars = this._renderAvatars(avatarMembers); + let summary = this._renderSummary(aggregate); let toggleButton = ( {expanded ? 'collapse' : 'expand'} ); - let plural = (joinEvents.length + leaveEvents.length > 0) ? 'others' : 'users'; - let noun = (joinedAndLeft === 1 ? 'user' : plural); let summaryContainer = (
@@ -238,7 +291,7 @@ module.exports = React.createClass({ {avatars} - {summary}{joinedAndLeft ? joinedAndLeft + ' ' + noun + ' joined and left' : ''} + {summary}   {toggleButton}
From 57652c06e3a0ead99d4feaebfa32f91d418667a3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 13 Jan 2017 11:13:17 +0000 Subject: [PATCH 018/260] released js-sdk --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1eaee39c41..1f40e4ed34 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "linkifyjs": "^2.1.3", "lodash": "^4.13.1", "marked": "^0.3.5", - "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "0.7.4-rc.1", "optimist": "^0.6.1", "q": "^1.4.1", "react": "^15.4.0", From 08020d1c394a69ab4d53dda3b545ae72e4b28a0e Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 13 Jan 2017 11:14:25 +0000 Subject: [PATCH 019/260] Prepare changelog for v0.8.5-rc.1 --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9673c3e820..3c7d0930b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +Changes in [0.8.5-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.5-rc.1) (2017-01-13) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.4...v0.8.5-rc.1) + + * Build the js-sdk in the CI script + [\#612](https://github.com/matrix-org/matrix-react-sdk/pull/612) + * Fix redacted member events being visible + [\#609](https://github.com/matrix-org/matrix-react-sdk/pull/609) + * Use `getStateKey` instead of `getSender` + [\#611](https://github.com/matrix-org/matrix-react-sdk/pull/611) + * Move screen sharing error check into platform + [\#608](https://github.com/matrix-org/matrix-react-sdk/pull/608) + * Fix 'create account' link in 'forgot password' + [\#606](https://github.com/matrix-org/matrix-react-sdk/pull/606) + * Let electron users complete captchas in a web browser + [\#601](https://github.com/matrix-org/matrix-react-sdk/pull/601) + * Add support for deleting threepids + [\#597](https://github.com/matrix-org/matrix-react-sdk/pull/597) + * Display msisdn threepids as 'Phone' + [\#598](https://github.com/matrix-org/matrix-react-sdk/pull/598) + Changes in [0.8.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.4) (2016-12-24) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.3...v0.8.4) From 3ac9ed7f8fea807ad764af2064040e54ceec44f6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 13 Jan 2017 11:14:26 +0000 Subject: [PATCH 020/260] v0.8.5-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1f40e4ed34..491dcb30b9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.8.4", + "version": "0.8.5-rc.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From e196cfaf910a9faff96c53a7ac84eb8fe33c2b00 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 13 Jan 2017 14:51:01 +0000 Subject: [PATCH 021/260] Show the error dialog when requests to PUT power levels fail This is why I dislike Q. --- src/components/views/rooms/MemberInfo.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index d57bf4bce1..5ccb633d8a 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -376,6 +376,7 @@ module.exports = WithMatrixClient(React.createClass({ // get out of sync if we force setState here! console.log("Power change success"); }, function(err) { + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Failure to change power level", description: err.message From dcd0acb2b5f0f6ed50bb96fd750482d6b59ccf8d Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 13 Jan 2017 15:17:34 +0000 Subject: [PATCH 022/260] Review comments --- 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 5ccb633d8a..1f4d392461 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -376,7 +376,7 @@ module.exports = WithMatrixClient(React.createClass({ // get out of sync if we force setState here! console.log("Power change success"); }, function(err) { - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Failure to change power level", description: err.message @@ -384,7 +384,7 @@ module.exports = WithMatrixClient(React.createClass({ } ).finally(()=>{ this.setState({ updating: this.state.updating - 1 }); - }); + }).done(); this.props.onFinished(); }, From 77ae04140746779b5654662575fdaaf393e92d3d Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 13 Jan 2017 16:40:33 +0000 Subject: [PATCH 023/260] Order names by order of first events for users --- src/components/views/elements/MemberEventListSummary.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index ab4a89eb69..89c7835671 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -239,12 +239,15 @@ module.exports = React.createClass({ let userEvents = { // $userId : [] }; + // Array of userIds ordered by the same ordering as the first event of each user + let users = []; eventsToRender.forEach((e) => { const userId = e.getStateKey(); // Initialise a user's events if (!userEvents[userId]) { userEvents[userId] = []; + users.push(userId); } userEvents[userId].push(e); }); @@ -258,7 +261,6 @@ module.exports = React.createClass({ }; let avatarMembers = []; - let users = Object.keys(userEvents); users.forEach( (userId) => { let displayName = userEvents[userId][0].getContent().displayname || userId; From ad072cc1792e82759a9b40d0d4c32c31e1f7bb4a Mon Sep 17 00:00:00 2001 From: Jani Mustonen Date: Fri, 6 Jan 2017 01:37:27 +0200 Subject: [PATCH 024/260] Turned buttons from divs to links. Makes it possible for screen readers and hotkeys to recognize the buttons. --- src/components/structures/UserSettings.js | 4 ++-- .../views/dialogs/ChatInviteDialog.js | 4 ++-- src/components/views/rooms/RoomHeader.js | 20 +++++++++---------- src/components/views/rooms/RoomTile.js | 4 ++-- .../views/rooms/SimpleRoomHeader.js | 2 +- .../views/settings/ChangePassword.js | 4 ++-- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index a41eab3a76..cd07a91475 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -663,9 +663,9 @@ module.exports = React.createClass({
- + {accountJsx}
diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index e9a041357f..5c6c627d58 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -409,9 +409,9 @@ module.exports = React.createClass({
{this.props.title}
-
+ -
+
diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index db3c7bb3d9..bfcaa6b172 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -182,8 +182,8 @@ module.exports = React.createClass({ 'm.room.name', user_id ); - save_button =
Save
- cancel_button =
Cancel
+ save_button = Save + cancel_button = Cancel } if (this.props.saving) { @@ -275,9 +275,9 @@ module.exports = React.createClass({ var settings_button; if (this.props.onSettingsClick) { settings_button = -
+ -
; + ; } // var leave_button; @@ -291,17 +291,17 @@ module.exports = React.createClass({ var forget_button; if (this.props.onForgetClick) { forget_button = -
+ -
; + ; } var rightPanel_buttons; if (this.props.collapsedRhs) { rightPanel_buttons = -
+ -
+ } var right_row; @@ -310,9 +310,9 @@ module.exports = React.createClass({
{ settings_button } { forget_button } -
+ -
+ { rightPanel_buttons }
; } diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 84916f8ab8..fce2868d50 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -287,7 +287,7 @@ module.exports = React.createClass({ var connectDropTarget = this.props.connectDropTarget; let ret = ( -
+
@@ -302,7 +302,7 @@ module.exports = React.createClass({
{/* { incomingCallBox } */} { tooltip } -
+
); if (connectDropTarget) ret = connectDropTarget(ret); diff --git a/src/components/views/rooms/SimpleRoomHeader.js b/src/components/views/rooms/SimpleRoomHeader.js index 7f2bb0048a..84c6802b3d 100644 --- a/src/components/views/rooms/SimpleRoomHeader.js +++ b/src/components/views/rooms/SimpleRoomHeader.js @@ -44,7 +44,7 @@ module.exports = React.createClass({ var cancelButton; if (this.props.onCancelClick) { - cancelButton =
Cancel
+ cancelButton = Cancel } var showRhsButton; diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 1ef3eff205..74658a09e5 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -136,9 +136,9 @@ module.exports = React.createClass({
- + ); case this.Phases.Uploading: From 8d79716421253002ae3a640ad7f6dcf77d885e3e Mon Sep 17 00:00:00 2001 From: Jani Mustonen Date: Fri, 6 Jan 2017 16:41:35 +0200 Subject: [PATCH 025/260] Turned the links to buttons to comply with MDN's recommendations --- src/components/structures/UserSettings.js | 4 ++-- .../views/dialogs/ChatInviteDialog.js | 4 ++-- src/components/views/rooms/RoomHeader.js | 20 +++++++++---------- src/components/views/rooms/RoomTile.js | 4 ++-- .../views/rooms/SimpleRoomHeader.js | 2 +- .../views/settings/ChangePassword.js | 4 ++-- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index cd07a91475..b104352096 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -663,9 +663,9 @@ module.exports = React.createClass({
- + {accountJsx}
diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index 5c6c627d58..fe33ea6d1c 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -409,9 +409,9 @@ module.exports = React.createClass({
{this.props.title}
- +
diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index bfcaa6b172..92cc6c64fd 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -182,8 +182,8 @@ module.exports = React.createClass({ 'm.room.name', user_id ); - save_button = Save - cancel_button = Cancel + save_button = + cancel_button = } if (this.props.saving) { @@ -275,9 +275,9 @@ module.exports = React.createClass({ var settings_button; if (this.props.onSettingsClick) { settings_button = - + ; } // var leave_button; @@ -291,17 +291,17 @@ module.exports = React.createClass({ var forget_button; if (this.props.onForgetClick) { forget_button = - + ; } var rightPanel_buttons; if (this.props.collapsedRhs) { rightPanel_buttons = - + } var right_row; @@ -310,9 +310,9 @@ module.exports = React.createClass({
{ settings_button } { forget_button } - + { rightPanel_buttons }
; } diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index fce2868d50..6cd9795bdc 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -287,7 +287,7 @@ module.exports = React.createClass({ var connectDropTarget = this.props.connectDropTarget; let ret = ( - + ); if (connectDropTarget) ret = connectDropTarget(ret); diff --git a/src/components/views/rooms/SimpleRoomHeader.js b/src/components/views/rooms/SimpleRoomHeader.js index 84c6802b3d..3c08fac821 100644 --- a/src/components/views/rooms/SimpleRoomHeader.js +++ b/src/components/views/rooms/SimpleRoomHeader.js @@ -44,7 +44,7 @@ module.exports = React.createClass({ var cancelButton; if (this.props.onCancelClick) { - cancelButton = Cancel + cancelButton = } var showRhsButton; diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 74658a09e5..84f049fdad 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -136,9 +136,9 @@ module.exports = React.createClass({ - + ); case this.Phases.Uploading: From d2ff2715ce276f4dd477a545a594698d4068422d Mon Sep 17 00:00:00 2001 From: Jani Mustonen Date: Fri, 6 Jan 2017 22:01:37 +0200 Subject: [PATCH 026/260] Buttonified almost everything. Stylesheet is broken. --- src/components/views/avatars/BaseAvatar.js | 28 +++++++++++++++------- src/components/views/rooms/EntityTile.js | 4 ++-- src/components/views/rooms/MemberInfo.js | 22 ++++++++--------- 3 files changed, 33 insertions(+), 21 deletions(-) diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index 47f0a76891..38a700eb7e 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -138,7 +138,7 @@ module.exports = React.createClass({ const { name, idName, title, url, urls, width, height, resizeMethod, - defaultToInitialLetter, + defaultToInitialLetter, onClick, ...otherProps } = this.props; @@ -156,12 +156,24 @@ module.exports = React.createClass({ ); } - return ( - - ); + if (onClick != null) { + return ( + + ); + } else { + return ( + + ); + } } }); diff --git a/src/components/views/rooms/EntityTile.js b/src/components/views/rooms/EntityTile.js index d29137ffc2..058359706e 100644 --- a/src/components/views/rooms/EntityTile.js +++ b/src/components/views/rooms/EntityTile.js @@ -152,7 +152,7 @@ module.exports = React.createClass({ var av = this.props.avatarJsx || ; return ( -
@@ -161,7 +161,7 @@ module.exports = React.createClass({
{ nameEl } { inviteButton } -
+ ); } }); diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 1f4d392461..40f85c9e63 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -612,7 +612,7 @@ module.exports = WithMatrixClient(React.createClass({ mx_MemberInfo_createRoom_label: true, mx_RoomTile_name: true, }); - const startNewChat =
@@ -620,7 +620,7 @@ module.exports = WithMatrixClient(React.createClass({
Start new chat
- + startChat =

Direct chats

@@ -635,26 +635,26 @@ module.exports = WithMatrixClient(React.createClass({ } if (this.state.can.kick) { - kickButton =
+ kickButton =
; + ; } if (this.state.can.ban) { - banButton =
+ banButton =
; + ; } if (this.state.can.mute) { var muteLabel = this.state.muted ? "Unmute" : "Mute"; - muteButton =
+ muteButton =
; + ; } if (this.state.can.toggleMod) { var giveOpLabel = this.state.isTargetMod ? "Revoke Moderator" : "Make Moderator"; - giveModButton =
+ giveModButton =
+ } // TODO: we should have an invite button if this MemberInfo is showing a user who isn't actually in the current room yet @@ -682,7 +682,7 @@ module.exports = WithMatrixClient(React.createClass({ const EmojiText = sdk.getComponent('elements.EmojiText'); return (
- +
From 041196d7298d72381b66c7ce2aefa4ed924324ec Mon Sep 17 00:00:00 2001 From: Jani Mustonen Date: Sat, 7 Jan 2017 17:53:45 +0200 Subject: [PATCH 027/260] Added quick search functionality --- src/components/views/elements/TintableSvg.js | 1 + src/components/views/rooms/RoomTile.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/TintableSvg.js b/src/components/views/elements/TintableSvg.js index 0157131506..401a11c1cb 100644 --- a/src/components/views/elements/TintableSvg.js +++ b/src/components/views/elements/TintableSvg.js @@ -69,6 +69,7 @@ var TintableSvg = React.createClass({ width={ this.props.width } height={ this.props.height } onLoad={ this.onLoad } + tabIndex="-1" /> ); } diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 6cd9795bdc..9f592868b4 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -287,7 +287,7 @@ module.exports = React.createClass({ var connectDropTarget = this.props.connectDropTarget; let ret = ( - +
; }, @@ -492,10 +493,10 @@ module.exports = React.createClass({ // bind() the invited rooms so any new invites that may come in as this button is clicked // don't inadvertently get rejected as well. reject = ( - + ); } @@ -663,9 +664,9 @@ module.exports = React.createClass({
- + {accountJsx}
diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index 38a700eb7e..906325268f 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -19,6 +19,7 @@ limitations under the License. var React = require('react'); var AvatarLogic = require("../../../Avatar"); import sdk from '../../../index'; +var AccessibleButton = require('../elements/AccessibleButton'); module.exports = React.createClass({ displayName: 'BaseAvatar', @@ -158,13 +159,13 @@ module.exports = React.createClass({ } if (onClick != null) { return ( - + ); } else { return ( diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index fe33ea6d1c..a54651b2db 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -24,6 +24,7 @@ var DMRoomMap = require('../../../utils/DMRoomMap'); var rate_limited_func = require("../../../ratelimitedfunc"); var dis = require("../../../dispatcher"); var Modal = require('../../../Modal'); +var AccessibleButton = require('../elements/AccessibleButton'); const TRUNCATE_QUERY_LIST = 40; @@ -409,9 +410,9 @@ module.exports = React.createClass({
{this.props.title}
- +
diff --git a/src/components/views/rooms/EntityTile.js b/src/components/views/rooms/EntityTile.js index 058359706e..64de431d9d 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'); +var AccessibleButton = require('../elements/AccessibleButton'); var PRESENCE_CLASS = { @@ -152,7 +153,7 @@ module.exports = React.createClass({ var av = this.props.avatarJsx || ; return ( - + ); } }); diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 40f85c9e63..4863bad5ed 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -35,6 +35,7 @@ var DMRoomMap = require('../../../utils/DMRoomMap'); var Unread = require('../../../Unread'); var Receipt = require('../../../utils/Receipt'); var WithMatrixClient = require('../../../wrappers/WithMatrixClient'); +var AccessibleButton = require('../elements/AccessibleButton'); module.exports = WithMatrixClient(React.createClass({ displayName: 'MemberInfo', @@ -612,7 +613,7 @@ module.exports = WithMatrixClient(React.createClass({ mx_MemberInfo_createRoom_label: true, mx_RoomTile_name: true, }); - const startNewChat = + startChat =

Direct chats

@@ -635,26 +636,26 @@ module.exports = WithMatrixClient(React.createClass({ } if (this.state.can.kick) { - kickButton = ; + ; } if (this.state.can.ban) { - banButton = ; + ; } if (this.state.can.mute) { var muteLabel = this.state.muted ? "Unmute" : "Mute"; - muteButton = ; + ; } if (this.state.can.toggleMod) { var giveOpLabel = this.state.isTargetMod ? "Revoke Moderator" : "Make Moderator"; - giveModButton = + } // TODO: we should have an invite button if this MemberInfo is showing a user who isn't actually in the current room yet @@ -682,7 +683,7 @@ module.exports = WithMatrixClient(React.createClass({ const EmojiText = sdk.getComponent('elements.EmojiText'); return (
- +
diff --git a/src/components/views/rooms/ReadReceiptMarker.js b/src/components/views/rooms/ReadReceiptMarker.js index 47875bd7fb..1618e4440d 100644 --- a/src/components/views/rooms/ReadReceiptMarker.js +++ b/src/components/views/rooms/ReadReceiptMarker.js @@ -192,9 +192,9 @@ module.exports = React.createClass({ width={14} height={14} resizeMethod="crop" style={style} title={title} - onClick={this.props.onClick} /> ); + /* onClick={this.props.onClick} */ }, }); diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 92cc6c64fd..b67acefc52 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -26,6 +26,7 @@ var rate_limited_func = require('../../../ratelimitedfunc'); var linkify = require('linkifyjs'); var linkifyElement = require('linkifyjs/element'); var linkifyMatrix = require('../../../linkify-matrix'); +var AccessibleButton = require('../elements/AccessibleButton'); linkifyMatrix(linkify); @@ -182,8 +183,8 @@ module.exports = React.createClass({ 'm.room.name', user_id ); - save_button = - cancel_button = + save_button = Save + cancel_button = Cancel } if (this.props.saving) { @@ -275,9 +276,9 @@ module.exports = React.createClass({ var settings_button; if (this.props.onSettingsClick) { settings_button = - ; + ; } // var leave_button; @@ -291,17 +292,17 @@ module.exports = React.createClass({ var forget_button; if (this.props.onForgetClick) { forget_button = - ; + ; } var rightPanel_buttons; if (this.props.collapsedRhs) { rightPanel_buttons = - + } var right_row; @@ -310,9 +311,9 @@ module.exports = React.createClass({
{ settings_button } { forget_button } - + { rightPanel_buttons }
; } diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 9f592868b4..07790181c5 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -26,6 +26,7 @@ var sdk = require('../../../index'); var ContextualMenu = require('../../structures/ContextualMenu'); var RoomNotifs = require('../../../RoomNotifs'); var FormattingUtils = require('../../../utils/FormattingUtils'); +var AccessibleButton = require('../elements/AccessibleButton'); module.exports = React.createClass({ displayName: 'RoomTile', @@ -286,8 +287,10 @@ module.exports = React.createClass({ var connectDragSource = this.props.connectDragSource; var connectDropTarget = this.props.connectDropTarget; + let ret = ( - + +
); if (connectDropTarget) ret = connectDropTarget(ret); diff --git a/src/components/views/rooms/SimpleRoomHeader.js b/src/components/views/rooms/SimpleRoomHeader.js index 3c08fac821..dbb27deb73 100644 --- a/src/components/views/rooms/SimpleRoomHeader.js +++ b/src/components/views/rooms/SimpleRoomHeader.js @@ -19,6 +19,7 @@ limitations under the License. var React = require('react'); var sdk = require('../../../index'); var dis = require("../../../dispatcher"); +var AccessibleButton = require('../elements/AccessibleButton'); /* * A stripped-down room header used for things like the user settings @@ -44,7 +45,7 @@ module.exports = React.createClass({ var cancelButton; if (this.props.onCancelClick) { - cancelButton = + cancelButton = Cancel } var showRhsButton; diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 84f049fdad..8882c1e048 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -19,6 +19,7 @@ limitations under the License. var React = require('react'); var MatrixClientPeg = require("../../../MatrixClientPeg"); var sdk = require("../../../index"); +var AccessibleButton = require('../elements/AccessibleButton'); module.exports = React.createClass({ displayName: 'ChangePassword', @@ -136,9 +137,9 @@ module.exports = React.createClass({
- + ); case this.Phases.Uploading: From 5e013860eee729c7131d5dcde47e4feff5b8037a Mon Sep 17 00:00:00 2001 From: Jani Mustonen Date: Fri, 13 Jan 2017 18:26:59 +0200 Subject: [PATCH 029/260] Definition for AccessibleButton --- .../views/elements/AccessibleButton.js | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/components/views/elements/AccessibleButton.js diff --git a/src/components/views/elements/AccessibleButton.js b/src/components/views/elements/AccessibleButton.js new file mode 100644 index 0000000000..91a9b99333 --- /dev/null +++ b/src/components/views/elements/AccessibleButton.js @@ -0,0 +1,44 @@ +/* + Copyright 2016 Aviral Dasgupta + + 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'; + +export default function AccessibleButton(props) { + const {element, onClick, children, ...restProps} = props; + restProps.onClick = onClick; + restProps.onKeyDown = function(e) { + if (e.keyCode == 13 || e.keyCode == 32) return onClick(); + }; + restProps.tabIndex = restProps.tabIndex || "0"; + restProps.role = "button"; + if (Array.isArray(children)) { + return React.createElement(element, restProps, ...children); + } else { + return React.createElement(element, restProps, children); + } +} + +AccessibleButton.propTypes = { + children: React.PropTypes.node, + element: React.PropTypes.string, + onClick: React.PropTypes.func.isRequired, +}; + +AccessibleButton.defaultProps = { + element: 'div' +}; + +AccessibleButton.displayName = "AccessibleButton"; From b323551f2210dea5842647a213b3486f34342e73 Mon Sep 17 00:00:00 2001 From: Jani Mustonen Date: Fri, 13 Jan 2017 19:34:53 +0200 Subject: [PATCH 030/260] Adhered to code review --- .../views/elements/AccessibleButton.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/components/views/elements/AccessibleButton.js b/src/components/views/elements/AccessibleButton.js index 91a9b99333..3ff5d7a38a 100644 --- a/src/components/views/elements/AccessibleButton.js +++ b/src/components/views/elements/AccessibleButton.js @@ -1,5 +1,5 @@ /* - Copyright 2016 Aviral Dasgupta + Copyright 2016 Jani Mustonen Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,6 +16,10 @@ import React from 'react'; +/** + * AccessibleButton is a generic wrapper for any element that should be treated as a button. + * Identifies the element as a button, setting proper tab indexing and keyboard activation behavior. + */ export default function AccessibleButton(props) { const {element, onClick, children, ...restProps} = props; restProps.onClick = onClick; @@ -24,13 +28,15 @@ export default function AccessibleButton(props) { }; restProps.tabIndex = restProps.tabIndex || "0"; restProps.role = "button"; - if (Array.isArray(children)) { - return React.createElement(element, restProps, ...children); - } else { - return React.createElement(element, restProps, children); - } + return React.createElement(element, restProps, children); } +/** + * children: React's magic prop. Represents all children given to the element. + * element: (optional) The base element type. "div" by default. + * onClick: (required) Event handler for button activation. Should be + * implemented exactly like a normal onClick handler. + */ AccessibleButton.propTypes = { children: React.PropTypes.node, element: React.PropTypes.string, From 1d5d44d63d2c07ac51f9886eb426c8ee8f9a22ab Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 12 Jan 2017 11:45:47 +0000 Subject: [PATCH 031/260] TextEncoder polyfill Apparently Safari doesn't sport a TextEncoder, so here's a polyfill for it. --- src/utils/TextDecoderPolyfill.js | 131 +++++++++++++++++++++++++ src/utils/TextEncoderPolyfill.js | 78 +++++++++++++++ test/utils/TextDecoderPolyfill-test.js | 85 ++++++++++++++++ test/utils/TextEncoderPolyfill-test.js | 39 ++++++++ 4 files changed, 333 insertions(+) create mode 100644 src/utils/TextDecoderPolyfill.js create mode 100644 src/utils/TextEncoderPolyfill.js create mode 100644 test/utils/TextDecoderPolyfill-test.js create mode 100644 test/utils/TextEncoderPolyfill-test.js diff --git a/src/utils/TextDecoderPolyfill.js b/src/utils/TextDecoderPolyfill.js new file mode 100644 index 0000000000..e203676bb7 --- /dev/null +++ b/src/utils/TextDecoderPolyfill.js @@ -0,0 +1,131 @@ +/* +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +"use strict"; + +// Polyfill for TextDecoder. + +const REPLACEMENT_CHAR = '\uFFFD'; + +export default class TextDecoder { + /** + * Decode a UTF-8 byte array as a javascript string + * + * @param {Uint8Array} u8Array UTF-8-encoded onput + * @return {str} + */ + decode(u8Array) { + let u0, u1, u2, u3; + + let str = ''; + let idx = 0; + while (idx < u8Array.length) { + u0 = u8Array[idx++]; + if (!(u0 & 0x80)) { + str += String.fromCharCode(u0); + continue; + } + + if ((u0 & 0xC0) != 0xC0) { + // continuation byte where we expect a leading byte + str += REPLACEMENT_CHAR; + continue; + } + + if (u0 > 0xF4) { + // this would imply a 5-byte or longer encoding, which is + // invalid and unsupported here. + str += REPLACEMENT_CHAR; + continue; + } + + u1 = u8Array[idx++]; + if (u1 === undefined) { + str += REPLACEMENT_CHAR; + continue; + } + + if ((u1 & 0xC0) != 0x80) { + // leading byte where we expect a continuation byte + str += REPLACEMENT_CHAR.repeat(2); + continue; + } + u1 &= 0x3F; + if (!(u0 & 0x20)) { + const u = ((u0 & 0x1F) << 6) | u1; + if (u < 0x80) { + // over-long + str += REPLACEMENT_CHAR.repeat(2); + } else { + str += String.fromCharCode(u); + } + continue; + } + + u2 = u8Array[idx++]; + if (u2 === undefined) { + str += REPLACEMENT_CHAR.repeat(2); + continue; + } + if ((u2 & 0xC0) != 0x80) { + // leading byte where we expect a continuation byte + str += REPLACEMENT_CHAR.repeat(3); + continue; + } + u2 &= 0x3F; + if (!(u0 & 0x10)) { + const u = ((u0 & 0x0F) << 12) | (u1 << 6) | u2; + if (u < 0x800) { + // over-long + str += REPLACEMENT_CHAR.repeat(3); + } else if (u == 0xFEFF && idx == 3) { + // byte-order mark: do not add to output + } else { + str += String.fromCharCode(u); + } + continue; + } + + u3 = u8Array[idx++]; + if (u3 === undefined) { + str += REPLACEMENT_CHAR.repeat(3); + continue; + } + if ((u3 & 0xC0) != 0x80) { + // leading byte where we expect a continuation byte + str += REPLACEMENT_CHAR.repeat(4); + continue; + } + u3 &= 0x3F; + const u = ((u0 & 7) << 18) | (u1 << 12) | (u2 << 6) | u3; + if (u < 0x10000) { + // over-long + str += REPLACEMENT_CHAR.repeat(4); + continue; + } + if (u > 0x1FFFF) { + // unicode stops here. + str += REPLACEMENT_CHAR.repeat(4); + continue; + } + + // encode as utf-16 + const v = u - 0x10000; + str += String.fromCharCode(0xD800 | (v >> 10), 0xDC00 | (v & 0x3FF)); + } + return str; + } +} diff --git a/src/utils/TextEncoderPolyfill.js b/src/utils/TextEncoderPolyfill.js new file mode 100644 index 0000000000..2da09710f2 --- /dev/null +++ b/src/utils/TextEncoderPolyfill.js @@ -0,0 +1,78 @@ +/* +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +"use strict"; + +// Polyfill for TextEncoder. Based on emscripten's stringToUTF8Array. + +function utf8len(str) { + var len = 0; + for (var i = 0; i < str.length; ++i) { + var u = str.charCodeAt(i); + if (u >= 0xD800 && u <= 0xDFFF && i < str.length-1) { + // lead surrogate - combine with next surrogate + u = 0x10000 + ((u & 0x3FF) << 10) | (str.charCodeAt(++i) & 0x3FF); + } + + if (u <= 0x7F) { + ++len; + } else if (u <= 0x7FF) { + len += 2; + } else if (u <= 0xFFFF) { + len += 3; + } else { + len += 4; + } + } + return len; +} + +export default class TextEncoder { + /** + * Encode a javascript string as utf-8 + * + * @param {String} str String to encode + * @return {Uint8Array} UTF-8-encoded output + */ + encode(str) { + const outU8Array = new Uint8Array(utf8len(str)); + var outIdx = 0; + for (var i = 0; i < str.length; ++i) { + var u = str.charCodeAt(i); + if (u >= 0xD800 && u <= 0xDFFF && i < str.length-1) { + // lead surrogate - combine with next surrogate + u = 0x10000 + ((u & 0x3FF) << 10) | (str.charCodeAt(++i) & 0x3FF); + } + + if (u <= 0x7F) { + outU8Array[outIdx++] = u; + } else if (u <= 0x7FF) { + outU8Array[outIdx++] = 0xC0 | (u >> 6); + outU8Array[outIdx++] = 0x80 | (u & 63); + } else if (u <= 0xFFFF) { + outU8Array[outIdx++] = 0xE0 | (u >> 12); + outU8Array[outIdx++] = 0x80 | ((u >> 6) & 63); + outU8Array[outIdx++] = 0x80 | (u & 63); + } else { + outU8Array[outIdx++] = 0xF0 | (u >> 18); + outU8Array[outIdx++] = 0x80 | ((u >> 12) & 63); + outU8Array[outIdx++] = 0x80 | ((u >> 6) & 63); + outU8Array[outIdx++] = 0x80 | (u & 63); + } + } + return outU8Array; + } +} diff --git a/test/utils/TextDecoderPolyfill-test.js b/test/utils/TextDecoderPolyfill-test.js new file mode 100644 index 0000000000..84f5edf187 --- /dev/null +++ b/test/utils/TextDecoderPolyfill-test.js @@ -0,0 +1,85 @@ +/* +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +"use strict"; + +import TextDecoderPolyfill from 'utils/TextDecoderPolyfill'; + +import * as testUtils from '../test-utils'; +import expect from 'expect'; + +describe('textDecoderPolyfill', function() { + beforeEach(function() { + testUtils.beforeEach(this); + }); + + it('should correctly decode a range of strings', function() { + const decoder = new TextDecoderPolyfill(); + + expect(decoder.decode(Uint8Array.of(65, 66, 67))).toEqual('ABC'); + expect(decoder.decode(Uint8Array.of(0xC3, 0xA6))).toEqual('æ'); + expect(decoder.decode(Uint8Array.of(0xE2, 0x82, 0xAC))).toEqual('€'); + expect(decoder.decode(Uint8Array.of(0xF0, 0x9F, 0x92, 0xA9))).toEqual('\uD83D\uDCA9'); + }); + + it('should ignore byte-order marks', function() { + const decoder = new TextDecoderPolyfill(); + expect(decoder.decode(Uint8Array.of(0xEF, 0xBB, 0xBF, 65))) + .toEqual('A'); + }); + + it('should not ignore byte-order marks in the middle of the array', function() { + const decoder = new TextDecoderPolyfill(); + expect(decoder.decode(Uint8Array.of(65, 0xEF, 0xBB, 0xBF, 66))) + .toEqual('A\uFEFFB'); + }); + + it('should reject overlong encodings', function() { + const decoder = new TextDecoderPolyfill(); + + // euro, as 4 bytes + expect(decoder.decode(Uint8Array.of(65, 0xF0, 0x82, 0x82, 0xAC, 67))) + .toEqual('A\uFFFD\uFFFD\uFFFD\uFFFDC'); + }); + + it('should reject 5 and 6-byte encodings', function() { + const decoder = new TextDecoderPolyfill(); + + expect(decoder.decode(Uint8Array.of(65, 0xF8, 0x82, 0x82, 0x82, 0x82, 67))) + .toEqual('A\uFFFD\uFFFD\uFFFD\uFFFD\uFFFDC'); + }); + + it('should reject code points beyond 0x10000', function() { + const decoder = new TextDecoderPolyfill(); + + expect(decoder.decode(Uint8Array.of(0xF4, 0xA0, 0x80, 0x80))) + .toEqual('\uFFFD\uFFFD\uFFFD\uFFFD'); + }); + + it('should cope with end-of-string', function() { + const decoder = new TextDecoderPolyfill(); + + expect(decoder.decode(Uint8Array.of(65, 0xC3))) + .toEqual('A\uFFFD'); + + expect(decoder.decode(Uint8Array.of(65, 0xE2, 0x82))) + .toEqual('A\uFFFD\uFFFD'); + + expect(decoder.decode(Uint8Array.of(65, 0xF0, 0x9F, 0x92))) + .toEqual('A\uFFFD\uFFFD\uFFFD'); + }); + +}); diff --git a/test/utils/TextEncoderPolyfill-test.js b/test/utils/TextEncoderPolyfill-test.js new file mode 100644 index 0000000000..4f422ec375 --- /dev/null +++ b/test/utils/TextEncoderPolyfill-test.js @@ -0,0 +1,39 @@ +/* +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +"use strict"; + +import TextEncoderPolyfill from 'utils/TextEncoderPolyfill'; + +import * as testUtils from '../test-utils'; +import expect from 'expect'; + +describe('textEncoderPolyfill', function() { + beforeEach(function() { + testUtils.beforeEach(this); + }); + + it('should correctly encode a range of strings', function() { + const encoder = new TextEncoderPolyfill(); + + expect(encoder.encode('ABC')).toEqual(Uint8Array.of(65, 66, 67)); + expect(encoder.encode('æ')).toEqual(Uint8Array.of(0xC3, 0xA6)); + expect(encoder.encode('€')).toEqual(Uint8Array.of(0xE2, 0x82, 0xAC)); + + // PILE OF POO (💩) + expect(encoder.encode('\uD83D\uDCA9')).toEqual(Uint8Array.of(0xF0, 0x9F, 0x92, 0xA9)); + }); +}); From f8e56778ea5b0577d0d594f4416305b79ee6d281 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 11 Jan 2017 22:22:11 +0000 Subject: [PATCH 032/260] Encryption and decryption for megolm backups --- src/utils/MegolmExportEncryption.js | 312 +++++++++++++++++++++ test/utils/MegolmExportEncryption-test.js | 115 ++++++++ test/utils/generate-megolm-test-vectors.py | 117 ++++++++ 3 files changed, 544 insertions(+) create mode 100644 src/utils/MegolmExportEncryption.js create mode 100644 test/utils/MegolmExportEncryption-test.js create mode 100755 test/utils/generate-megolm-test-vectors.py diff --git a/src/utils/MegolmExportEncryption.js b/src/utils/MegolmExportEncryption.js new file mode 100644 index 0000000000..5b2e16ef29 --- /dev/null +++ b/src/utils/MegolmExportEncryption.js @@ -0,0 +1,312 @@ +/* +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +"use strict"; + +// polyfill textencoder if necessary +let TextEncoder = window.TextEncoder; +if (!TextEncoder) { + TextEncoder = require('./TextEncoderPolyfill'); +} +let TextDecoder = window.TextDecoder; +if (TextDecoder) { + TextDecoder = require('./TextDecoderPolyfill'); +} + +const subtleCrypto = window.crypto.subtle || window.crypto.webkitSubtle; + +/** + * Decrypt a megolm key file + * + * @param {ArrayBuffer} file + * @param {String} password + * @return {Promise} promise for decrypted output + */ +export function decryptMegolmKeyFile(data, password) { + const body = unpackMegolmKeyFile(data); + + // check we have a version byte + if (body.length < 1) { + throw new Error('Invalid file: too short'); + } + + const version = body[0]; + if (version !== 1) { + throw new Error('Unsupported version'); + } + + const ciphertextLength = body.length-(1+16+16+4+32); + if (body.length < 0) { + throw new Error('Invalid file: too short'); + } + + const salt = body.subarray(1, 1+16); + const iv = body.subarray(17, 17+16); + const iterations = body[33] << 24 | body[34] << 16 | body[35] << 8 | body[36]; + const ciphertext = body.subarray(37, 37+ciphertextLength); + const hmac = body.subarray(-32); + + return deriveKeys(salt, iterations, password).then((keys) => { + const [aes_key, sha_key] = keys; + + const toVerify = body.subarray(0, -32); + return subtleCrypto.verify( + {name: 'HMAC'}, + sha_key, + hmac, + toVerify, + ).then((isValid) => { + if (!isValid) { + throw new Error('Authentication check failed: incorrect password?') + } + + return subtleCrypto.decrypt( + { + name: "AES-CTR", + counter: iv, + length: 64, + }, + aes_key, + ciphertext, + ); + }); + }).then((plaintext) => { + return new TextDecoder().decode(new Uint8Array(plaintext)); + }); +} + + +/** + * Encrypt a megolm key file + * + * @param {String} data + * @param {String} password + * @param {Object=} options + * @param {Nunber=} options.kdf_rounds Number of iterations to perform of the + * key-derivation function. + * @return {Promise} promise for encrypted output + */ +export function encryptMegolmKeyFile(data, password, options) { + options = options || {}; + const kdf_rounds = options.kdf_rounds || 100000; + + const salt = new Uint8Array(16); + window.crypto.getRandomValues(salt); + const iv = new Uint8Array(16); + window.crypto.getRandomValues(iv); + + return deriveKeys(salt, kdf_rounds, password).then((keys) => { + const [aes_key, sha_key] = keys; + + return subtleCrypto.encrypt( + { + name: "AES-CTR", + counter: iv, + length: 64, + }, + aes_key, + new TextEncoder().encode(data), + ).then((ciphertext) => { + const cipherArray = new Uint8Array(ciphertext); + const bodyLength = (1+salt.length+iv.length+4+cipherArray.length+32); + const resultBuffer = new Uint8Array(bodyLength); + let idx = 0; + resultBuffer[idx++] = 1; // version + resultBuffer.set(salt, idx); idx += salt.length; + resultBuffer.set(iv, idx); idx += iv.length; + resultBuffer[idx++] = kdf_rounds >> 24; + resultBuffer[idx++] = (kdf_rounds >> 16) & 0xff; + resultBuffer[idx++] = (kdf_rounds >> 8) & 0xff; + resultBuffer[idx++] = kdf_rounds & 0xff; + resultBuffer.set(cipherArray, idx); idx += cipherArray.length; + + const toSign = resultBuffer.subarray(0, idx); + + return subtleCrypto.sign( + {name: 'HMAC'}, + sha_key, + toSign, + ).then((hmac) => { + hmac = new Uint8Array(hmac); + resultBuffer.set(hmac, idx); + return packMegolmKeyFile(resultBuffer); + }); + }); + }); +} + +/** + * Derive the AES and SHA keys for the file + * + * @param {Unit8Array} salt salt for pbkdf + * @param {Number} iterations number of pbkdf iterations + * @param {String} password password + * @return {Promise<[CryptoKey, CryptoKey]>} promise for [aes key, sha key] + */ +function deriveKeys(salt, iterations, password) { + return subtleCrypto.importKey( + 'raw', + new TextEncoder().encode(password), + {name: 'PBKDF2'}, + false, + ['deriveBits'] + ).then((key) => { + return subtleCrypto.deriveBits( + { + name: 'PBKDF2', + salt: salt, + iterations: iterations, + hash: 'SHA-512', + }, + key, + 512 + ); + }).then((keybits) => { + const aes_key = keybits.slice(0, 32); + const sha_key = keybits.slice(32); + + const aes_prom = subtleCrypto.importKey( + 'raw', + aes_key, + {name: 'AES-CTR'}, + false, + ['encrypt', 'decrypt'] + ); + const sha_prom = subtleCrypto.importKey( + 'raw', + sha_key, + { + name: 'HMAC', + hash: {name: 'SHA-256'}, + }, + false, + ['sign', 'verify'] + ); + return Promise.all([aes_prom, sha_prom]); + }); +} + +const HEADER_LINE = '-----BEGIN MEGOLM SESSION DATA-----'; +const TRAILER_LINE = '-----END MEGOLM SESSION DATA-----'; + +/** + * Unbase64 an ascii-armoured megolm key file + * + * Strips the header and trailer lines, and unbase64s the content + * + * @param {ArrayBuffer} data input file + * @return {Uint8Array} unbase64ed content + */ +function unpackMegolmKeyFile(data) { + // parse the file as a great big String. This should be safe, because there + // should be no non-ASCII characters, and it means that we can do string + // comparisons to find the header and footer, and feed it into window.atob. + const fileStr = new TextDecoder().decode(new Uint8Array(data)); + + // look for the start line + let lineStart = 0; + while (1) { + const lineEnd = fileStr.indexOf('\n', lineStart); + if (lineEnd < 0) { + throw new Error('Header line not found'); + } + const line = fileStr.slice(lineStart, lineEnd).trim(); + + // start the next line after the newline + lineStart = lineEnd+1; + + if (line === HEADER_LINE) { + break; + } + } + + const dataStart = lineStart; + + // look for the end line + while (1) { + const lineEnd = fileStr.indexOf('\n', lineStart); + const line = fileStr.slice(lineStart, lineEnd < 0 ? undefined : lineEnd) + .trim(); + if (line === TRAILER_LINE) { + break; + } + + if (lineEnd < 0) { + throw new Error('Trailer line not found'); + } + + // start the next line after the newline + lineStart = lineEnd+1; + } + + const dataEnd = lineStart; + return decodeBase64(fileStr.slice(dataStart, dataEnd)); +} + +/** + * ascii-armour a megolm key file + * + * base64s the content, and adds header and trailer lines + * + * @param {Uint8Array} data raw data + * @return {ArrayBuffer} formatted file + */ +function packMegolmKeyFile(data) { + // we split into lines before base64ing, because encodeBase64 doesn't deal + // terribly well with large arrays. + const LINE_LENGTH = (72 * 4 / 3); + const nLines = Math.ceil(data.length / LINE_LENGTH); + const lines = new Array(nLines + 3); + lines[0] = HEADER_LINE; + let o = 0; + let i; + for (i = 1; i <= nLines; i++) { + lines[i] = encodeBase64(data.subarray(o, o+LINE_LENGTH)); + o += LINE_LENGTH; + } + lines[i++] = TRAILER_LINE; + lines[i] = ''; + return (new TextEncoder().encode(lines.join('\n'))).buffer; +} + +/** + * Encode a typed array of uint8 as base64. + * @param {Uint8Array} uint8Array The data to encode. + * @return {string} The base64. + */ +function encodeBase64(uint8Array) { + // Misinterpt the Uint8Array as Latin-1. + // window.btoa expects a unicode string with codepoints in the range 0-255. + var latin1String = String.fromCharCode.apply(null, uint8Array); + // Use the builtin base64 encoder. + return window.btoa(latin1String); +} + +/** + * Decode a base64 string to a typed array of uint8. + * @param {string} base64 The base64 to decode. + * @return {Uint8Array} The decoded data. + */ +function decodeBase64(base64) { + // window.atob returns a unicode string with codepoints in the range 0-255. + var latin1String = window.atob(base64); + // Encode the string as a Uint8Array + var uint8Array = new Uint8Array(latin1String.length); + for (var i = 0; i < latin1String.length; i++) { + uint8Array[i] = latin1String.charCodeAt(i); + } + return uint8Array; +} diff --git a/test/utils/MegolmExportEncryption-test.js b/test/utils/MegolmExportEncryption-test.js new file mode 100644 index 0000000000..fa51d83c6d --- /dev/null +++ b/test/utils/MegolmExportEncryption-test.js @@ -0,0 +1,115 @@ +/* +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +"use strict"; + +import * as MegolmExportEncryption from 'utils/MegolmExportEncryption'; + +import * as testUtils from '../test-utils'; +import expect from 'expect'; + +// polyfill textencoder if necessary +let TextEncoder = window.TextEncoder; +if (!TextEncoder) { + TextEncoder = require('utils/TextEncoderPolyfill'); +} + +const TEST_VECTORS=[ + [ + "plain", + "password", + "-----BEGIN MEGOLM SESSION DATA-----\nAXNhbHRzYWx0c2FsdHNhbHSIiIiIiIiIiIiIiIiIiIiIAAAACmIRUW2OjZ3L2l6j9h0lHlV3M2dx\ncissyYBxjsfsAndErh065A8=\n-----END MEGOLM SESSION DATA-----" + ], + [ + "Hello, World", + "betterpassword", + "-----BEGIN MEGOLM SESSION DATA-----\nAW1vcmVzYWx0bW9yZXNhbHT//////////wAAAAAAAAAAAAAD6KyBpe1Niv5M5NPm4ZATsJo5nghk\nKYu63a0YQ5DRhUWEKk7CcMkrKnAUiZny\n-----END MEGOLM SESSION DATA-----" + ], + [ + "alphanumericallyalphanumericallyalphanumericallyalphanumerically", + "SWORDFISH", + "-----BEGIN MEGOLM SESSION DATA-----\nAXllc3NhbHR5Z29vZG5lc3P//////////wAAAAAAAAAAAAAD6OIW+Je7gwvjd4kYrb+49gKCfExw\nMgJBMD4mrhLkmgAngwR1pHjbWXaoGybtiAYr0moQ93GrBQsCzPbvl82rZhaXO3iH5uHo/RCEpOqp\nPgg29363BGR+/Ripq/VCLKGNbw==\n-----END MEGOLM SESSION DATA-----" + ], + [ + "alphanumericallyalphanumericallyalphanumericallyalphanumerically", + "passwordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpassword", + "-----BEGIN MEGOLM SESSION DATA-----\nAf//////////////////////////////////////////AAAD6IAZJy7IQ7Y0idqSw/bmpngEEVVh\ngsH+8ptgqxw6ZVWQnohr8JsuwH9SwGtiebZuBu5smPCO+RFVWH2cQYslZijXv/BEH/txvhUrrtCd\nbWnSXS9oymiqwUIGs08sXI33ZA==\n-----END MEGOLM SESSION DATA-----" + ] +] +; + +function stringToArray(s) { + return new TextEncoder().encode(s).buffer; +} + +describe('MegolmExportEncryption', function() { + beforeEach(function() { + testUtils.beforeEach(this); + }); + + describe('decrypt', function() { + it('should handle missing header', function() { + const input=stringToArray(`-----`); + expect(()=>{MegolmExportEncryption.decryptMegolmKeyFile(input, '')}) + .toThrow('Header line not found'); + }); + + it('should handle missing trailer', function() { + const input=stringToArray(`-----BEGIN MEGOLM SESSION DATA----- +-----`); + expect(()=>{MegolmExportEncryption.decryptMegolmKeyFile(input, '')}) + .toThrow('Trailer line not found'); + }); + + it('should decrypt a range of inputs', function(done) { + function next(i) { + if (i >= TEST_VECTORS.length) { + done(); + return; + } + + const [plain, password, input] = TEST_VECTORS[i]; + return MegolmExportEncryption.decryptMegolmKeyFile( + stringToArray(input), password + ).then((decrypted) => { + expect(decrypted).toEqual(plain); + return next(i+1); + }) + }; + return next(0).catch(done); + }); + }); + + describe('encrypt', function() { + it('should round-trip', function(done) { + const input = + 'words words many words in plain text here'.repeat(100); + + const password = 'my super secret passphrase'; + + return MegolmExportEncryption.encryptMegolmKeyFile( + input, password, {kdf_rounds: 1000}, + ).then((ciphertext) => { + return MegolmExportEncryption.decryptMegolmKeyFile( + ciphertext, password + ); + }).then((plaintext) => { + expect(plaintext).toEqual(input); + done(); + }).catch(done); + }); + }); +}); diff --git a/test/utils/generate-megolm-test-vectors.py b/test/utils/generate-megolm-test-vectors.py new file mode 100755 index 0000000000..0ce5f5e4b3 --- /dev/null +++ b/test/utils/generate-megolm-test-vectors.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python + +from __future__ import print_function + +import base64 +import json +import struct + +from cryptography.hazmat import backends +from cryptography.hazmat.primitives import ciphers, hashes, hmac +from cryptography.hazmat.primitives.kdf import pbkdf2 +from cryptography.hazmat.primitives.ciphers import algorithms, modes + +backend = backends.default_backend() + +def parse_u128(s): + a, b = struct.unpack(">QQ", s) + return (a << 64) | b + +def encrypt_ctr(key, iv, plaintext, counter_bits=64): + alg = algorithms.AES(key) + + # Some AES-CTR implementations treat some parts of the IV as a nonce (which + # remains constant throughought encryption), and some as a counter (which + # increments every block, ie 16 bytes, and wraps after a while). Different + # implmententations use different amounts of the IV for each part. + # + # The python cryptography library uses the whole IV as a counter; to make + # it match other implementations with a given counter size, we manually + # implement wrapping the counter. + + # number of AES blocks between each counter wrap + limit = 1 << counter_bits + + # parse IV as a 128-bit int + parsed_iv = parse_u128(iv) + + # split IV into counter and nonce + counter = parsed_iv & (limit - 1) + nonce = parsed_iv & ~(limit - 1) + + # encrypt up to the first counter wraparound + size = 16 * (limit - counter) + encryptor = ciphers.Cipher( + alg, + modes.CTR(iv), + backend=backend + ).encryptor() + input = plaintext[:size] + result = encryptor.update(input) + encryptor.finalize() + offset = size + + # do remaining data starting with a counter of zero + iv = struct.pack(">QQ", nonce >> 64, nonce & ((1 << 64) - 1)) + size = 16 * limit + + while offset < len(plaintext): + encryptor = ciphers.Cipher( + alg, + modes.CTR(iv), + backend=backend + ).encryptor() + input = plaintext[offset:offset+size] + result += encryptor.update(input) + encryptor.finalize() + offset += size + + return result + +def hmac_sha256(key, message): + h = hmac.HMAC(key, hashes.SHA256(), backend=backend) + h.update(message) + return h.finalize() + +def encrypt(key, iv, salt, plaintext, iterations=1000): + """ + Returns: + (bytes) ciphertext + """ + if len(salt) != 16: + raise Exception("Expected 128 bits of salt - got %i bits" % len((salt) * 8)) + if len(iv) != 16: + raise Exception("Expected 128 bits of IV - got %i bits" % (len(iv) * 8)) + + sha = hashes.SHA512() + kdf = pbkdf2.PBKDF2HMAC(sha, 64, salt, iterations, backend) + k = kdf.derive(key) + + aes_key = k[0:32] + sha_key = k[32:] + + packed_file = ( + b"\x01" # version + + salt + + iv + + struct.pack(">L", iterations) + + encrypt_ctr(aes_key, iv, plaintext) + ) + packed_file += hmac_sha256(sha_key, packed_file) + + return ( + b"-----BEGIN MEGOLM SESSION DATA-----\n" + + base64.encodestring(packed_file) + + b"-----END MEGOLM SESSION DATA-----" + ) + +def gen(password, iv, salt, plaintext, iterations=1000): + ciphertext = encrypt( + password.encode('utf-8'), iv, salt, plaintext.encode('utf-8'), iterations + ) + return (plaintext, password, ciphertext.decode('utf-8')) + +print (json.dumps([ + gen("password", b"\x88"*16, b"saltsaltsaltsalt", "plain", 10), + gen("betterpassword", b"\xFF"*8 + b"\x00"*8, b"moresaltmoresalt", "Hello, World"), + gen("SWORDFISH", b"\xFF"*8 + b"\x00"*8, b"yessaltygoodness", "alphanumerically" * 4), + gen("password"*32, b"\xFF"*16, b"\xFF"*16, "alphanumerically" * 4), +], indent=4)) From d63f7e83599937a8b9e5dbff638b0a93efa7fafa Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Sat, 14 Jan 2017 01:21:26 +0000 Subject: [PATCH 033/260] Expose megolm import/export via the devtools --- src/index.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/index.js b/src/index.js index 4b920d95d4..5d4145a39b 100644 --- a/src/index.js +++ b/src/index.js @@ -28,3 +28,27 @@ module.exports.getComponent = function(componentName) { return Skinner.getComponent(componentName); }; + +/* hacky functions for megolm import/export until we give it a UI */ +import * as MegolmExportEncryption from './utils/MegolmExportEncryption'; +import MatrixClientPeg from './MatrixClientPeg'; + +window.exportKeys = function(password) { + return MatrixClientPeg.get().exportRoomKeys().then((k) => { + return MegolmExportEncryption.encryptMegolmKeyFile( + JSON.stringify(k), password + ); + }).then((f) => { + console.log(new TextDecoder().decode(new Uint8Array(f))); + }).done(); +}; + +window.importKeys = function(password, data) { + const arrayBuffer = new TextEncoder().encode(data).buffer; + return MegolmExportEncryption.decryptMegolmKeyFile( + arrayBuffer, password + ).then((j) => { + const k = JSON.parse(j); + return MatrixClientPeg.get().importRoomKeys(k); + }); +}; From e37bf6b7be824caade5019da1bc1a4b92c9d6114 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Sat, 14 Jan 2017 01:41:48 +0000 Subject: [PATCH 034/260] Skip crypto tests on PhantomJS --- test/utils/MegolmExportEncryption-test.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/utils/MegolmExportEncryption-test.js b/test/utils/MegolmExportEncryption-test.js index fa51d83c6d..db38a931ed 100644 --- a/test/utils/MegolmExportEncryption-test.js +++ b/test/utils/MegolmExportEncryption-test.js @@ -56,6 +56,13 @@ function stringToArray(s) { } describe('MegolmExportEncryption', function() { + before(function() { + // if we don't have subtlecrypto, go home now + if (!window.crypto.subtle && !window.crypto.webkitSubtle) { + this.skip(); + } + }) + beforeEach(function() { testUtils.beforeEach(this); }); From fb68fff536a9dec35d8cf14c786b93e2d8f471ff Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Mon, 16 Jan 2017 13:45:42 +0100 Subject: [PATCH 035/260] Refactor renderCommaSeparated for reuse --- .../views/elements/MemberEventListSummary.js | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index ab4a89eb69..975e2aebf3 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -56,17 +56,8 @@ module.exports = React.createClass({ if (users.length === 0) { return null; } - let originalNumber = users.length; - users = users.slice(0, this.props.summaryLength); - - let remaining = originalNumber - this.props.summaryLength; - if (remaining < 0) { - remaining = 0; - } - let other = " other" + (remaining > 1 ? "s" : ""); - - return this._renderCommaSeparatedList(users, remaining) + (remaining ? ' and ' + remaining + other : ''); + return this._renderCommaSeparatedList(users, this.props.summaryLength); }, // Test whether the first n items repeat for the duration @@ -81,14 +72,16 @@ module.exports = React.createClass({ return true; }, - _renderCommaSeparatedList(items, disableAnd) { - if (disableAnd) { - return items.join(', '); - } + _renderCommaSeparatedList(items, itemLimit) { + const remaining = itemLimit === undefined ? 0 : Math.max(items.length - itemLimit, 0); if (items.length === 0) { return ""; } else if (items.length === 1) { return items[0]; + } else if (remaining) { + items = items.slice(0, itemLimit); + const other = " other" + (remaining > 1 ? "s" : ""); + return items.join(', ') + ' and ' + remaining + other; } else { let last = items.pop(); return items.join(', ') + ' and ' + last; From 3679e470028137da21782b8d248cb8af78d2a846 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 16 Jan 2017 13:19:24 +0000 Subject: [PATCH 036/260] js-sdk 0.7.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 491dcb30b9..c30cf0da7a 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "linkifyjs": "^2.1.3", "lodash": "^4.13.1", "marked": "^0.3.5", - "matrix-js-sdk": "0.7.4-rc.1", + "matrix-js-sdk": "0.7.4", "optimist": "^0.6.1", "q": "^1.4.1", "react": "^15.4.0", From 834c8d0890524a8ea2d40d3ed2f4b71f169adbfb Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 16 Jan 2017 13:21:34 +0000 Subject: [PATCH 037/260] Prepare changelog for v0.8.5 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c7d0930b7..4f1e33e61c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +Changes in [0.8.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.5) (2017-01-16) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.5-rc.1...v0.8.5) + + * Pull in newer matrix-js-sdk for video calling fix + Changes in [0.8.5-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.5-rc.1) (2017-01-13) ============================================================================================================= [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.4...v0.8.5-rc.1) From a9256b0ab1f546fe9f60c791439e2b81f1bb0f60 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 16 Jan 2017 13:21:34 +0000 Subject: [PATCH 038/260] v0.8.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c30cf0da7a..34e22d7c70 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.8.5-rc.1", + "version": "0.8.5", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 1d5112db5d6cc568713f5d81456dd4a0af399e60 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 16 Jan 2017 13:22:57 +0000 Subject: [PATCH 039/260] Back to develop js-sdk --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 34e22d7c70..47d6c9aced 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "linkifyjs": "^2.1.3", "lodash": "^4.13.1", "marked": "^0.3.5", - "matrix-js-sdk": "0.7.4", + "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop", "optimist": "^0.6.1", "q": "^1.4.1", "react": "^15.4.0", From 82d6805a718f4e7f1616c7552dae85e968091c74 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Mon, 16 Jan 2017 14:49:07 +0100 Subject: [PATCH 040/260] Canonicalise certain transition pairs, handle arbitrary consecutive transitions Transition pairs joined,left and left,joined are now transformed into single meta-transitions "joined_and_left" and "left_and_joined" respectively. These are described as "joined and left", "left and rejoined". Treat consecutive sequences of transitions as repetitions, and handle any arbitrary repetitions of transitions: ...,joined,left,joined,left,joined,left,... is canonicalised into ...,joined_and_left, joined_and_left, joined_and_left,... which is truncated and described as ... , joined and left 3 times, ... This also works if there are multiple consecutive sequences separated by other transitions: ..., banned, banned, banned, joined, unbanned, unbanned, unbanned,... becomes ... was banned 3 times, joined, was unbanned 3 times ... --- .../views/elements/MemberEventListSummary.js | 113 ++++++++++++------ 1 file changed, 78 insertions(+), 35 deletions(-) diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index 975e2aebf3..dc16127017 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -88,62 +88,105 @@ module.exports = React.createClass({ } }, - _getDescriptionForTransition(t, plural) { + _getDescriptionForTransition(t, plural, repeats) { let beConjugated = plural ? "were" : "was"; let invitation = plural ? "invitations" : "an invitation"; - switch (t) { - case 'joined': return "joined"; - case 'left': return "left"; - case 'invite_reject': return "rejected " + invitation; - case 'invite_withdrawal': return "withdrew " + invitation; - case 'invited': return beConjugated + " invited"; - case 'banned': return beConjugated + " banned"; - case 'unbanned': return beConjugated + " unbanned"; - case 'kicked': return beConjugated + " kicked"; + let res = null; + let map = { + "joined": "joined", + "left": "left", + "joined_and_left": "joined and left", + "left_and_joined": "left and rejoined", + "invite_reject": "rejected " + invitation, + "invite_withdrawal": "withdrew " + invitation, + "invited": beConjugated + " invited", + "banned": beConjugated + " banned", + "unbanned": beConjugated + " unbanned", + "kicked": beConjugated + " kicked", + }; + + if (Object.keys(map).includes(t)) { + res = map[t] + (repeats > 1 ? " " + repeats + " times" : "" ); } - return null; + return res; + }, + + _getCanonicalTransitions: function(transitions) { + let modMap = { + 'joined' : { + 'after' : 'left', + 'newTransition' : 'joined_and_left', + }, + 'left' : { + 'after' : 'joined', + 'newTransition' : 'left_and_joined', + }, + // $currentTransition : { + // 'after' : $nextTransition, + // 'newTransition' : 'new_transition_type', + // }, + }; + const res = []; + + for (let i = 0; i < transitions.length; i++) { + let t = transitions[i]; + let t2 = transitions[i + 1]; + + let transition = t; + + if (i < transitions.length - 1 && modMap[t] && modMap[t].after === t2) { + transition = modMap[t].newTransition; + i++; + } + + res.push(transition); + } + return res; + }, + + _getTruncatedTransitions: function(transitions) { + let res = []; + for (let i = 0; i < transitions.length; i++) { + if (res.length > 0 && res[res.length - 1].transitionType === transitions[i]) { + res[res.length - 1].repeats += 1; + } else { + res.push({ + transitionType: transitions[i], + repeats: 1, + }); + } + } + // returns [{ + // transitionType: "joined_and_left" + // repeats: 123 + // }, ... ] + return res; }, _renderSummary: function(eventAggregates) { let summaries = Object.keys(eventAggregates).map((transitions) => { let nameList = this._renderNameList(eventAggregates[transitions]); + let plural = eventAggregates[transitions].length > 1; let repeats = 1; let repeatExtra = 0; let splitTransitions = transitions.split(','); - let describedTransitions = splitTransitions; - let plural = eventAggregates[transitions].length > 1; - for (let modulus = 1; modulus <= 2; modulus++) { - // Sequences that are repeating through modulus transitions will be truncated - if (this._isRepeatedSequence(describedTransitions, modulus)) { - // Extra repeating sequence on the end that should be treated separately - // so as to avoid j,l,j,l,j => "... joined and left 2.5 times" - repeatExtra = describedTransitions.length % modulus; + // Some pairs of transitions are common and are repeated a lot, so canonicalise them into "pair" transitions + let canonicalTransitions = this._getCanonicalTransitions(splitTransitions); + // Remove consecutive repetitions of the same transition (like 5 consecutive 'join_and_leave's) + let truncatedTransitions = this._getTruncatedTransitions(canonicalTransitions); - repeats = (describedTransitions.length - repeatExtra) / modulus; - describedTransitions = describedTransitions.slice(0, modulus); - break; - } - } - - let numberOfTimes = repeats > 1 ? " " + repeats + " times" : ""; - - let descs = describedTransitions.map((t) => { - return this._getDescriptionForTransition(t, plural); - }); - - let afterRepeatDescs = splitTransitions.slice(splitTransitions.length - repeatExtra).map((t) => { - return this._getDescriptionForTransition(t, plural); + let descs = truncatedTransitions.map((t) => { + return this._getDescriptionForTransition(t.transitionType, plural, t.repeats); }); let desc = this._renderCommaSeparatedList(descs); - let afterRepeatDesc = this._renderCommaSeparatedList(afterRepeatDescs); - return nameList + " " + desc + numberOfTimes + (afterRepeatDesc ? " and then " + afterRepeatDesc : ""); + return nameList + " " + desc; }); if (!summaries) { From 4be444d52482883b4b87d6fc5cac851626b4cb50 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Mon, 16 Jan 2017 15:12:00 +0100 Subject: [PATCH 041/260] Move shouldComponentUpdate --- .../views/elements/MemberEventListSummary.js | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index dc16127017..5474865117 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -46,6 +46,19 @@ module.exports = React.createClass({ }; }, + shouldComponentUpdate: function(nextProps, nextState) { + // Update if + // - The number of summarised events has changed + // - or if the summary is currently expanded + // - or if the summary is about to toggle to become collapsed + // - or if there are fewEvents, meaning the child eventTiles are shown as-is + return ( + nextProps.events.length !== this.props.events.length || + this.state.expanded || nextState.expanded || + nextProps.events.length < this.props.threshold + ); + }, + _toggleSummary: function() { this.setState({ expanded: !this.state.expanded, @@ -214,19 +227,6 @@ module.exports = React.createClass({ ); }, - shouldComponentUpdate: function(nextProps, nextState) { - // Update if - // - The number of summarised events has changed - // - or if the summary is currently expanded - // - or if the summary is about to toggle to become collapsed - // - or if there are fewEvents, meaning the child eventTiles are shown as-is - return ( - nextProps.events.length !== this.props.events.length || - this.state.expanded || nextState.expanded || - nextProps.events.length < this.props.threshold - ); - }, - _getTransition: function(e) { switch (e.getContent().membership) { case 'invite': return 'invited'; From ac22803ba004b519d88a16a37487efb833575ce9 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 16 Jan 2017 17:01:26 +0000 Subject: [PATCH 042/260] Allow Modal to be used with async-loaded components Add Modal.createDialogAsync, which can be used to display asynchronously-loaded React components. Also make EncryptedEventDialog use it as a handy demonstration. --- src/Modal.js | 73 ++++++++++++++++++- .../views/dialogs/EncryptedEventDialog.js | 0 src/component-index.js | 2 - src/components/views/rooms/EventTile.js | 5 +- 4 files changed, 74 insertions(+), 6 deletions(-) rename src/{components => async-components}/views/dialogs/EncryptedEventDialog.js (100%) diff --git a/src/Modal.js b/src/Modal.js index 44072b9278..c2ce04c4e8 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -19,6 +19,53 @@ limitations under the License. var React = require('react'); var ReactDOM = require('react-dom'); +import sdk from './index'; + +/** + * Wrap an asynchronous loader function with a react component which shows a + * spinner until the real component loads. + */ +const AsyncWrapper = React.createClass({ + propTypes: { + /** A function which takes a 'callback' argument which it will call + * with the real component once it loads. + */ + loader: React.PropTypes.func.isRequired, + }, + + getInitialState: function() { + return { + component: null, + } + }, + + componentWillMount: function() { + this._unmounted = false; + this.props.loader((e) => { + if (this._unmounted) { + return; + } + this.setState({component: e}); + }); + }, + + componentWillUnmount: function() { + this._unmounted = true; + }, + + render: function() { + const {loader, ...otherProps} = this.props; + + if (this.state.component) { + const Component = this.state.component; + return ; + } else { + // show a spinner until the component is loaded. + const Spinner = sdk.getComponent("elements.Spinner"); + return ; + } + }, +}); module.exports = { DialogContainerId: "mx_Dialog_Container", @@ -36,8 +83,30 @@ module.exports = { }, createDialog: function (Element, props, className) { - var self = this; + return this.createDialogAsync((cb) => {cb(Element)}, props, className); + }, + /** + * Open a modal view. + * + * This can be used to display a react component which is loaded as an asynchronous + * webpack component. To do this, set 'loader' as: + * + * (cb) => { + * require([''], cb); + * } + * + * @param {Function} loader a function which takes a 'callback' argument, + * which it should call with a React component which will be displayed as + * the modal view. + * + * @param {Object} props properties to pass to the displayed + * component. (We will also pass an 'onFinished' property.) + * + * @param {String} className CSS class to apply to the modal wrapper + */ + createDialogAsync: function (loader, props, className) { + var self = this; // never call this via modal.close() from onFinished() otherwise it will loop var closeDialog = function() { if (props && props.onFinished) props.onFinished.apply(null, arguments); @@ -49,7 +118,7 @@ module.exports = { var dialog = (
- +
diff --git a/src/components/views/dialogs/EncryptedEventDialog.js b/src/async-components/views/dialogs/EncryptedEventDialog.js similarity index 100% rename from src/components/views/dialogs/EncryptedEventDialog.js rename to src/async-components/views/dialogs/EncryptedEventDialog.js diff --git a/src/component-index.js b/src/component-index.js index bc3d698cac..e83de8739d 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -75,8 +75,6 @@ import views$dialogs$ChatInviteDialog from './components/views/dialogs/ChatInvit views$dialogs$ChatInviteDialog && (module.exports.components['views.dialogs.ChatInviteDialog'] = views$dialogs$ChatInviteDialog); import views$dialogs$DeactivateAccountDialog from './components/views/dialogs/DeactivateAccountDialog'; views$dialogs$DeactivateAccountDialog && (module.exports.components['views.dialogs.DeactivateAccountDialog'] = views$dialogs$DeactivateAccountDialog); -import views$dialogs$EncryptedEventDialog from './components/views/dialogs/EncryptedEventDialog'; -views$dialogs$EncryptedEventDialog && (module.exports.components['views.dialogs.EncryptedEventDialog'] = views$dialogs$EncryptedEventDialog); import views$dialogs$ErrorDialog from './components/views/dialogs/ErrorDialog'; views$dialogs$ErrorDialog && (module.exports.components['views.dialogs.ErrorDialog'] = views$dialogs$ErrorDialog); import views$dialogs$InteractiveAuthDialog from './components/views/dialogs/InteractiveAuthDialog'; diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 42dbe78630..d5a2acfdd6 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -366,10 +366,11 @@ module.exports = WithMatrixClient(React.createClass({ }, onCryptoClicked: function(e) { - var EncryptedEventDialog = sdk.getComponent("dialogs.EncryptedEventDialog"); var event = this.props.mxEvent; - Modal.createDialog(EncryptedEventDialog, { + Modal.createDialogAsync((cb) => { + require(['../../../async-components/views/dialogs/EncryptedEventDialog'], cb) + }, { event: event, }); }, From f168f9cd063c2597878fb11d7e3c75c8d389d574 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Mon, 16 Jan 2017 17:25:44 +0000 Subject: [PATCH 043/260] Fix vector-im/riot-web#2833 : Fail nicely when people try to register numeric user IDs --- src/Signup.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Signup.js b/src/Signup.js index f148ac2419..5d1c7062d5 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -203,7 +203,19 @@ class Register extends Signup { } else if (error.errcode == 'M_INVALID_USERNAME') { throw new Error("User names may only contain alphanumeric characters, underscores or dots!"); } else if (error.httpStatus >= 400 && error.httpStatus < 500) { - throw new Error(`Registration failed! (${error.httpStatus})`); + let msg = null; + if (error.message) { + msg = error.message; + } + else if (error.errcode) { + msg = error.errcode; + } + if (msg) { + throw new Error(`Registration failed! (${error.httpStatus}) - ${msg}`); + } + else { + throw new Error(`Registration failed! (${error.httpStatus}) - That's all we know.`); + } } else if (error.httpStatus >= 500 && error.httpStatus < 600) { throw new Error( `Server error during registration! (${error.httpStatus})` From a79dc886ba5a146633aaf478587efb6ea35d1c90 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Mon, 16 Jan 2017 18:46:17 +0100 Subject: [PATCH 044/260] Order sequences by occurance of the first event in each sequence --- .../views/elements/MemberEventListSummary.js | 44 ++++++++++++++----- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index 5474865117..7adc4c2f85 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -178,8 +178,8 @@ module.exports = React.createClass({ return res; }, - _renderSummary: function(eventAggregates) { - let summaries = Object.keys(eventAggregates).map((transitions) => { + _renderSummary: function(eventAggregates, orderedTransitionSequences) { + let summaries = orderedTransitionSequences.map((transitions) => { let nameList = this._renderNameList(eventAggregates[transitions]); let plural = eventAggregates[transitions].length > 1; @@ -228,18 +228,18 @@ module.exports = React.createClass({ }, _getTransition: function(e) { - switch (e.getContent().membership) { + switch (e.mxEvent.getContent().membership) { case 'invite': return 'invited'; case 'ban': return 'banned'; case 'join': return 'joined'; case 'leave': - if (e.getSender() === e.getStateKey()) { - switch (e.getPrevContent().membership) { + if (e.mxEvent.getSender() === e.mxEvent.getStateKey()) { + switch (e.mxEvent.getPrevContent().membership) { case 'invite': return 'invite_reject'; default: return 'left'; } } - switch (e.getPrevContent().membership) { + switch (e.mxEvent.getPrevContent().membership) { case 'invite': return 'invite_withdrawal'; case 'ban': return 'unbanned'; case 'join': return 'kicked'; @@ -276,44 +276,64 @@ module.exports = React.createClass({ // $userId : [] }; - eventsToRender.forEach((e) => { + eventsToRender.forEach((e, index) => { const userId = e.getStateKey(); // Initialise a user's events if (!userEvents[userId]) { userEvents[userId] = []; } - userEvents[userId].push(e); + userEvents[userId].push({ + mxEvent: e, + displayName: e.getContent().displayname || userId, + index: index, + }); }); - // A map of agregate type to arrays of display names. Each aggregate type + // A map of aggregate type to arrays of display names. Each aggregate type // is a comma-delimited string of transitions, e.g. "joined,left,kicked". // The array of display names is the array of users who went through that // sequence during eventsToRender. let aggregate = { // $aggregateType : []:string }; + // A map of aggregate types to the indices that order them (the index of + // the first event for a given transition sequence) + let aggregateIndices = { + // $aggregateType : int + }; + let avatarMembers = []; let users = Object.keys(userEvents); users.forEach( (userId) => { - let displayName = userEvents[userId][0].getContent().displayname || userId; + let firstEvent = userEvents[userId][0]; + let displayName = firstEvent.displayName; let seq = this._getTransitionSequence(userEvents[userId]); if (!aggregate[seq]) { aggregate[seq] = []; + aggregateIndices[seq] = -1; } // Assumes display names are unique if (aggregate[seq].indexOf(displayName) === -1) { aggregate[seq].push(displayName); } - avatarMembers.push(userEvents[userId][0].target); + + if (aggregateIndices[seq] === -1 || firstEvent.index < aggregateIndices[seq]) { + aggregateIndices[seq] = firstEvent.index; + } + + avatarMembers.push(firstEvent.mxEvent.target); } ); + // Sort types by order of lowest event index within sequence + let orderedTransitionSequences = Object.keys(aggregate).sort((seq1, seq2) => aggregateIndices[seq1] > aggregateIndices[seq2]); + let avatars = this._renderAvatars(avatarMembers); - let summary = this._renderSummary(aggregate); + let summary = this._renderSummary(aggregate, orderedTransitionSequences); let toggleButton = ( {expanded ? 'collapse' : 'expand'} From 5ab287fa1ad1f2f5456bf160b988a15b4d882a4c Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Mon, 16 Jan 2017 18:57:49 +0100 Subject: [PATCH 045/260] Use pre-calculated displaynames to handle dupes --- src/components/views/elements/MemberEventListSummary.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index 7adc4c2f85..425404dab9 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -284,7 +284,7 @@ module.exports = React.createClass({ } userEvents[userId].push({ mxEvent: e, - displayName: e.getContent().displayname || userId, + displayName: e.target.name || userId, index: index, }); }); From aa6e168505ef139c67d66270bf69cf3750a7e85e Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Mon, 16 Jan 2017 18:58:53 +0100 Subject: [PATCH 046/260] Remove comment --- src/components/views/elements/MemberEventListSummary.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index 425404dab9..e9ec793953 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -316,7 +316,6 @@ module.exports = React.createClass({ aggregateIndices[seq] = -1; } - // Assumes display names are unique if (aggregate[seq].indexOf(displayName) === -1) { aggregate[seq].push(displayName); } From 09ce74cc767a5f98021f5980ce7f4e2b2aece7c4 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 16 Jan 2017 18:44:46 +0000 Subject: [PATCH 047/260] Fix a couple of minor review comments --- src/utils/MegolmExportEncryption.js | 20 ++++++++++---------- src/utils/TextEncoderPolyfill.js | 12 ++++++------ 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/utils/MegolmExportEncryption.js b/src/utils/MegolmExportEncryption.js index 5b2e16ef29..351f58aaa6 100644 --- a/src/utils/MegolmExportEncryption.js +++ b/src/utils/MegolmExportEncryption.js @@ -60,12 +60,12 @@ export function decryptMegolmKeyFile(data, password) { const hmac = body.subarray(-32); return deriveKeys(salt, iterations, password).then((keys) => { - const [aes_key, sha_key] = keys; + const [aes_key, hmac_key] = keys; const toVerify = body.subarray(0, -32); return subtleCrypto.verify( {name: 'HMAC'}, - sha_key, + hmac_key, hmac, toVerify, ).then((isValid) => { @@ -109,7 +109,7 @@ export function encryptMegolmKeyFile(data, password, options) { window.crypto.getRandomValues(iv); return deriveKeys(salt, kdf_rounds, password).then((keys) => { - const [aes_key, sha_key] = keys; + const [aes_key, hmac_key] = keys; return subtleCrypto.encrypt( { @@ -137,7 +137,7 @@ export function encryptMegolmKeyFile(data, password, options) { return subtleCrypto.sign( {name: 'HMAC'}, - sha_key, + hmac_key, toSign, ).then((hmac) => { hmac = new Uint8Array(hmac); @@ -149,12 +149,12 @@ export function encryptMegolmKeyFile(data, password, options) { } /** - * Derive the AES and SHA keys for the file + * Derive the AES and HMAC-SHA-256 keys for the file * * @param {Unit8Array} salt salt for pbkdf * @param {Number} iterations number of pbkdf iterations * @param {String} password password - * @return {Promise<[CryptoKey, CryptoKey]>} promise for [aes key, sha key] + * @return {Promise<[CryptoKey, CryptoKey]>} promise for [aes key, hmac key] */ function deriveKeys(salt, iterations, password) { return subtleCrypto.importKey( @@ -176,7 +176,7 @@ function deriveKeys(salt, iterations, password) { ); }).then((keybits) => { const aes_key = keybits.slice(0, 32); - const sha_key = keybits.slice(32); + const hmac_key = keybits.slice(32); const aes_prom = subtleCrypto.importKey( 'raw', @@ -185,9 +185,9 @@ function deriveKeys(salt, iterations, password) { false, ['encrypt', 'decrypt'] ); - const sha_prom = subtleCrypto.importKey( + const hmac_prom = subtleCrypto.importKey( 'raw', - sha_key, + hmac_key, { name: 'HMAC', hash: {name: 'SHA-256'}, @@ -195,7 +195,7 @@ function deriveKeys(salt, iterations, password) { false, ['sign', 'verify'] ); - return Promise.all([aes_prom, sha_prom]); + return Promise.all([aes_prom, hmac_prom]); }); } diff --git a/src/utils/TextEncoderPolyfill.js b/src/utils/TextEncoderPolyfill.js index 2da09710f2..41ee4782a9 100644 --- a/src/utils/TextEncoderPolyfill.js +++ b/src/utils/TextEncoderPolyfill.js @@ -61,16 +61,16 @@ export default class TextEncoder { outU8Array[outIdx++] = u; } else if (u <= 0x7FF) { outU8Array[outIdx++] = 0xC0 | (u >> 6); - outU8Array[outIdx++] = 0x80 | (u & 63); + outU8Array[outIdx++] = 0x80 | (u & 0x3F); } else if (u <= 0xFFFF) { outU8Array[outIdx++] = 0xE0 | (u >> 12); - outU8Array[outIdx++] = 0x80 | ((u >> 6) & 63); - outU8Array[outIdx++] = 0x80 | (u & 63); + outU8Array[outIdx++] = 0x80 | ((u >> 6) & 0x3F); + outU8Array[outIdx++] = 0x80 | (u & 0x3F); } else { outU8Array[outIdx++] = 0xF0 | (u >> 18); - outU8Array[outIdx++] = 0x80 | ((u >> 12) & 63); - outU8Array[outIdx++] = 0x80 | ((u >> 6) & 63); - outU8Array[outIdx++] = 0x80 | (u & 63); + outU8Array[outIdx++] = 0x80 | ((u >> 12) & 0x3F); + outU8Array[outIdx++] = 0x80 | ((u >> 6) & 0x3F); + outU8Array[outIdx++] = 0x80 | (u & 0x3F); } } return outU8Array; From 4f860b4c6dc42f7783c0e61611eb76462850ccd4 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 17 Jan 2017 10:50:44 +0000 Subject: [PATCH 048/260] Review comments: If-statement style --- src/Signup.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Signup.js b/src/Signup.js index 5d1c7062d5..d3643bd749 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -206,14 +206,12 @@ class Register extends Signup { let msg = null; if (error.message) { msg = error.message; - } - else if (error.errcode) { + } else if (error.errcode) { msg = error.errcode; } if (msg) { throw new Error(`Registration failed! (${error.httpStatus}) - ${msg}`); - } - else { + } else { throw new Error(`Registration failed! (${error.httpStatus}) - That's all we know.`); } } else if (error.httpStatus >= 500 && error.httpStatus < 600) { From 45655f4de3611ce9391175abc22988d719cbf8ec Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Tue, 17 Jan 2017 12:01:19 +0100 Subject: [PATCH 049/260] Modified desc for invitation rejections, withdrawals --- src/components/views/elements/MemberEventListSummary.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index e9ec793953..1f05ba000e 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -103,7 +103,7 @@ module.exports = React.createClass({ _getDescriptionForTransition(t, plural, repeats) { let beConjugated = plural ? "were" : "was"; - let invitation = plural ? "invitations" : "an invitation"; + let invitation = "their invitation" + (plural || (repeats > 1) ? "s" : ""); let res = null; let map = { @@ -112,7 +112,7 @@ module.exports = React.createClass({ "joined_and_left": "joined and left", "left_and_joined": "left and rejoined", "invite_reject": "rejected " + invitation, - "invite_withdrawal": "withdrew " + invitation, + "invite_withdrawal": "had " + invitation + " withdrawn", "invited": beConjugated + " invited", "banned": beConjugated + " banned", "unbanned": beConjugated + " unbanned", From ade7c65617cb43bf654f3421a64a75afbc0f393e Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Tue, 17 Jan 2017 12:01:54 +0100 Subject: [PATCH 050/260] Add test for MemberEventListSummary --- .../elements/MemberEventListSummary-test.js | 556 ++++++++++++++++++ 1 file changed, 556 insertions(+) create mode 100644 test/components/views/elements/MemberEventListSummary-test.js diff --git a/test/components/views/elements/MemberEventListSummary-test.js b/test/components/views/elements/MemberEventListSummary-test.js new file mode 100644 index 0000000000..af4d92fa61 --- /dev/null +++ b/test/components/views/elements/MemberEventListSummary-test.js @@ -0,0 +1,556 @@ +const expect = require('expect'); +const React = require('react'); +const ReactDOM = require("react-dom"); +const ReactTestUtils = require('react-addons-test-utils'); +const sdk = require('matrix-react-sdk'); +const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary'); + +const testUtils = require('../../../test-utils'); +describe.only('MemberEventListSummary', function() { + let sandbox; + let parentDiv; + + const generateTiles = (events) => { + return events.map((e) => { + return ( +
+ Expanded membership +
+ ); + }); + }; + + const generateMembershipEvent = (eventId, parameters) => { + let membership = parameters.membership; + let userId = parameters.userId; + let prevMembership = parameters.prevMembership; + let senderId = parameters.senderId; + return { + content: { + membership: membership, + }, + target: { + name: userId.match(/@([^:]*):/)[1], + getAvatarUrl: () => { + return "avatar.jpeg"; + }, + userId: userId, + }, + getId: () => { + return eventId; + }, + getContent: function() { + return this.content; + }, + getPrevContent: function() { + return { + membership: prevMembership ? prevMembership : this.content, + }; + }, + getSender: () => { + return senderId || userId; + }, + getStateKey: () => { + return userId; + }, + }; + }; + + const generateEvents = (parameters) => { + const res = []; + for (let i = 0; i < parameters.length; i++) { + res.push(generateMembershipEvent(`event${i}`, parameters[i])); + } + return res; + }; + + const generateEventsForUsers = (userIdTemplate, n, events) => { + let eventsForUsers = []; + let userId = ""; + for (let i = 0; i < n; i++) { + userId = userIdTemplate.replace('$', i); + events.forEach((e) => { + e.userId = userId; + return e; + }); + eventsForUsers = eventsForUsers.concat(generateEvents(events)); + } + return eventsForUsers; + }; + + beforeEach(function() { + testUtils.beforeEach(this); + sandbox = testUtils.stubClient(); + parentDiv = document.createElement('div'); + document.body.appendChild(parentDiv); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('renders expanded events if there are less than props.threshold', function(done) { + const events = generateEvents([ + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, + ]); + const props = { + events : events, + children : generateTiles(events), + summaryLength : 1, + avatarsMaxLength : 5, + threshold : 3, + }; + + const renderer = ReactTestUtils.createRenderer(); + renderer.render(); + const result = renderer.getRenderOutput(); + + expect(result.type).toBe('div'); + expect(result.props.children).toEqual([ +
Expanded membership
, + ]); + done(); + }); + + it('renders expanded events if there are less than props.threshold', function(done) { + const events = generateEvents([ + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, + ]); + const props = { + events : events, + children : generateTiles(events), + summaryLength : 1, + avatarsMaxLength : 5, + threshold : 3, + }; + + const renderer = ReactTestUtils.createRenderer(); + renderer.render(); + const result = renderer.getRenderOutput(); + + expect(result.type).toBe('div'); + expect(result.props.children).toEqual([ +
Expanded membership
, +
Expanded membership
, + ]); + done(); + }); + + it('renders collapsed events if events.length = props.threshold', function(done) { + const events = generateEvents([ + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, + ]); + const props = { + events : events, + children : generateTiles(events), + summaryLength : 1, + avatarsMaxLength : 5, + threshold : 3, + }; + + const instance = ReactDOM.render(, parentDiv); + const summary = ReactTestUtils.findRenderedDOMComponentWithClass(instance, "mx_MemberEventListSummary_summary"); + const summaryText = summary.innerText; + + expect(summaryText).toBe("user_1 joined and left and joined"); + + done(); + }); + + it('truncates long join,leave repetitions', function(done) { + const events = generateEvents([ + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, + ]); + const props = { + events : events, + children : generateTiles(events), + summaryLength : 1, + avatarsMaxLength : 5, + threshold : 3, + }; + + const instance = ReactDOM.render(, parentDiv); + const summary = ReactTestUtils.findRenderedDOMComponentWithClass(instance, "mx_MemberEventListSummary_summary"); + const summaryText = summary.innerText; + + expect(summaryText).toBe("user_1 joined and left 7 times"); + + done(); + }); + + it('truncates long join,leave repetitions inbetween other events', function(done) { + const events = generateEvents([ + {userId : "@user_1:some.domain", prevMembership: "ban", membership: "leave", senderId: "@some_other_user:some.domain"}, + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "invite", senderId: "@some_other_user:some.domain"}, + ]); + const props = { + events : events, + children : generateTiles(events), + summaryLength : 1, + avatarsMaxLength : 5, + threshold : 3, + }; + + const instance = ReactDOM.render(, parentDiv); + const summary = ReactTestUtils.findRenderedDOMComponentWithClass(instance, "mx_MemberEventListSummary_summary"); + const summaryText = summary.innerText; + + expect(summaryText).toBe("user_1 was unbanned, joined and left 7 times and was invited"); + + done(); + }); + + it('truncates multiple sequences of repetitions with other events inbetween', function(done) { + const events = generateEvents([ + {userId : "@user_1:some.domain", prevMembership: "ban", membership: "leave", senderId: "@some_other_user:some.domain"}, + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "ban", senderId: "@some_other_user:some.domain"}, + {userId : "@user_1:some.domain", prevMembership: "ban", membership: "join"}, + {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "invite", senderId: "@some_other_user:some.domain"}, + ]); + const props = { + events : events, + children : generateTiles(events), + summaryLength : 1, + avatarsMaxLength : 5, + threshold : 3, + }; + + const instance = ReactDOM.render(, parentDiv); + const summary = ReactTestUtils.findRenderedDOMComponentWithClass(instance, "mx_MemberEventListSummary_summary"); + const summaryText = summary.innerText; + + expect(summaryText).toBe("user_1 was unbanned, joined and left 2 times, was banned, joined and left 3 times and was invited"); + + done(); + }); + + it('handles multple users following the same sequence of memberships', function(done) { + const events = generateEvents([ + // user_1 + {userId : "@user_1:some.domain", prevMembership: "ban", membership: "leave", senderId: "@some_other_user:some.domain"}, + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "ban", senderId: "@some_other_user:some.domain"}, + // user_2 + {userId : "@user_2:some.domain", prevMembership: "ban", membership: "leave", senderId: "@some_other_user:some.domain"}, + {userId : "@user_2:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_2:some.domain", prevMembership: "join", membership: "leave"}, + {userId : "@user_2:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_2:some.domain", prevMembership: "join", membership: "leave"}, + {userId : "@user_2:some.domain", prevMembership: "leave", membership: "ban", senderId: "@some_other_user:some.domain"}, + ]); + const props = { + events : events, + children : generateTiles(events), + summaryLength : 1, + avatarsMaxLength : 5, + threshold : 3, + }; + + const instance = ReactDOM.render(, parentDiv); + const summary = ReactTestUtils.findRenderedDOMComponentWithClass(instance, "mx_MemberEventListSummary_summary"); + const summaryText = summary.innerText; + + expect(summaryText).toBe("user_1 and 1 other were unbanned, joined and left 2 times and were banned"); + + done(); + }); + + it('handles multple users following the same sequence of memberships', function(done) { + const events = generateEvents([ + // user_1 + {userId : "@user_1:some.domain", prevMembership: "ban", membership: "leave", senderId: "@some_other_user:some.domain"}, + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "ban", senderId: "@some_other_user:some.domain"}, + // user_2 + {userId : "@user_2:some.domain", prevMembership: "ban", membership: "leave", senderId: "@some_other_user:some.domain"}, + {userId : "@user_2:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_2:some.domain", prevMembership: "join", membership: "leave"}, + {userId : "@user_2:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_2:some.domain", prevMembership: "join", membership: "leave"}, + {userId : "@user_2:some.domain", prevMembership: "leave", membership: "ban", senderId: "@some_other_user:some.domain"}, + ]); + const props = { + events : events, + children : generateTiles(events), + summaryLength : 1, + avatarsMaxLength : 5, + threshold : 3, + }; + + const instance = ReactDOM.render(, parentDiv); + const summary = ReactTestUtils.findRenderedDOMComponentWithClass(instance, "mx_MemberEventListSummary_summary"); + const summaryText = summary.innerText; + + expect(summaryText).toBe("user_1 and 1 other were unbanned, joined and left 2 times and were banned"); + + done(); + }); + + it('handles many users following the same sequence of memberships', function(done) { + const events = generateEventsForUsers("@user_$:some.domain", 20, [ + {prevMembership: "ban", membership: "leave", senderId: "@some_other_user:some.domain"}, + {prevMembership: "leave", membership: "join"}, + {prevMembership: "join", membership: "leave"}, + {prevMembership: "leave", membership: "join"}, + {prevMembership: "join", membership: "leave"}, + {prevMembership: "leave", membership: "ban", senderId: "@some_other_user:some.domain"}, + ]); + const props = { + events : events, + children : generateTiles(events), + summaryLength : 1, + avatarsMaxLength : 5, + threshold : 3, + }; + + const instance = ReactDOM.render(, parentDiv); + const summary = ReactTestUtils.findRenderedDOMComponentWithClass(instance, "mx_MemberEventListSummary_summary"); + const summaryText = summary.innerText; + + expect(summaryText).toBe("user_0 and 19 others were unbanned, joined and left 2 times and were banned"); + + done(); + }); + + it('correctly orders sequences of transitions by the order of their first event', function(done) { + const events = generateEvents([ + {userId : "@user_2:some.domain", prevMembership: "ban", membership: "leave", senderId: "@some_other_user:some.domain"}, + {userId : "@user_1:some.domain", prevMembership: "ban", membership: "leave", senderId: "@some_other_user:some.domain"}, + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, + {userId : "@user_1:some.domain", prevMembership: "leave", membership: "ban", senderId: "@some_other_user:some.domain"}, + {userId : "@user_2:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_2:some.domain", prevMembership: "join", membership: "leave"}, + {userId : "@user_2:some.domain", prevMembership: "leave", membership: "join"}, + {userId : "@user_2:some.domain", prevMembership: "join", membership: "leave"}, + ]); + const props = { + events : events, + children : generateTiles(events), + summaryLength : 1, + avatarsMaxLength : 5, + threshold : 3, + }; + + const instance = ReactDOM.render(, parentDiv); + const summary = ReactTestUtils.findRenderedDOMComponentWithClass(instance, "mx_MemberEventListSummary_summary"); + const summaryText = summary.innerText; + + expect(summaryText).toBe( + "user_2 was unbanned and joined and left 2 times, user_1 was unbanned, joined and left 2 times and was banned" + ); + + done(); + }); + + it('correctly identifies transitions', function(done) { + const events = generateEvents([ + // invited + {userId : "@user_1:some.domain", membership: "invite"}, + // banned + {userId : "@user_1:some.domain", membership: "ban"}, + // joined + {userId : "@user_1:some.domain", membership: "join"}, + // invite_reject + {userId : "@user_1:some.domain", prevMembership: "invite", membership: "leave"}, + // left + {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, + // invite_withdrawal + {userId : "@user_1:some.domain", prevMembership: "invite", membership: "leave", senderId: "@some_other_user:some.domain"}, + // unbanned + {userId : "@user_1:some.domain", prevMembership: "ban", membership: "leave", senderId: "@some_other_user:some.domain"}, + // kicked + {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave", senderId: "@some_other_user:some.domain"}, + // default = left + {userId : "@user_1:some.domain", prevMembership: "????", membership: "leave", senderId: "@some_other_user:some.domain"}, + ]); + const props = { + events : events, + children : generateTiles(events), + summaryLength : 1, + avatarsMaxLength : 5, + threshold : 3, + }; + + const instance = ReactDOM.render(, parentDiv); + const summary = ReactTestUtils.findRenderedDOMComponentWithClass(instance, "mx_MemberEventListSummary_summary"); + const summaryText = summary.innerText; + + expect(summaryText).toBe( + "user_1 was invited, was banned, joined, rejected their invitation, left, had their invitation withdrawn, was unbanned, was kicked and left" + ); + + done(); + }); + + it('handles invitation plurals correctly when there are multiple users', function(done) { + const events = generateEvents([ + {userId : "@user_1:some.domain", prevMembership: "invite", membership: "leave"}, + {userId : "@user_1:some.domain", prevMembership: "invite", membership: "leave", senderId: "@some_other_user:some.domain"}, + {userId : "@user_2:some.domain", prevMembership: "invite", membership: "leave"}, + {userId : "@user_2:some.domain", prevMembership: "invite", membership: "leave", senderId: "@some_other_user:some.domain"}, + ]); + const props = { + events : events, + children : generateTiles(events), + summaryLength : 1, + avatarsMaxLength : 5, + threshold : 3, + }; + + const instance = ReactDOM.render(, parentDiv); + const summary = ReactTestUtils.findRenderedDOMComponentWithClass(instance, "mx_MemberEventListSummary_summary"); + const summaryText = summary.innerText; + + expect(summaryText).toBe( + "user_1 and 1 other rejected their invitations and had their invitations withdrawn" + ); + + done(); + }); + + it('handles invitation plurals correctly when there are multiple invites', function(done) { + const events = generateEvents([ + {userId : "@user_1:some.domain", prevMembership: "invite", membership: "leave"}, + {userId : "@user_1:some.domain", prevMembership: "invite", membership: "leave"}, + ]); + const props = { + events : events, + children : generateTiles(events), + summaryLength : 1, + avatarsMaxLength : 5, + threshold : 1, // threshold = 1 to force collapse + }; + + const instance = ReactDOM.render(, parentDiv); + const summary = ReactTestUtils.findRenderedDOMComponentWithClass(instance, "mx_MemberEventListSummary_summary"); + const summaryText = summary.innerText; + + expect(summaryText).toBe( + "user_1 rejected their invitations 2 times" + ); + + done(); + }); + + it('handles a summary length = 2, with no "others"', function(done) { + const events = generateEvents([ + {userId : "@user_1:some.domain", membership: "join"}, + {userId : "@user_1:some.domain", membership: "join"}, + {userId : "@user_2:some.domain", membership: "join"}, + {userId : "@user_2:some.domain", membership: "join"}, + ]); + const props = { + events : events, + children : generateTiles(events), + summaryLength : 2, + avatarsMaxLength : 5, + threshold : 3, + }; + + const instance = ReactDOM.render(, parentDiv); + const summary = ReactTestUtils.findRenderedDOMComponentWithClass(instance, "mx_MemberEventListSummary_summary"); + const summaryText = summary.innerText; + + expect(summaryText).toBe( + "user_1 and user_2 joined 2 times" + ); + + done(); + }); + + it('handles a summary length = 2, with 1 "other"', function(done) { + const events = generateEvents([ + {userId : "@user_1:some.domain", membership: "join"}, + {userId : "@user_2:some.domain", membership: "join"}, + {userId : "@user_3:some.domain", membership: "join"}, + ]); + const props = { + events : events, + children : generateTiles(events), + summaryLength : 2, + avatarsMaxLength : 5, + threshold : 3, + }; + + const instance = ReactDOM.render(, parentDiv); + const summary = ReactTestUtils.findRenderedDOMComponentWithClass(instance, "mx_MemberEventListSummary_summary"); + const summaryText = summary.innerText; + + expect(summaryText).toBe( + "user_1, user_2 and 1 other joined" + ); + + done(); + }); + + it('handles a summary length = 2, with many "others"', function(done) { + const events = generateEventsForUsers("@user_$:some.domain", 20, [ + {membership: "join"}, + ]); + const props = { + events : events, + children : generateTiles(events), + summaryLength : 2, + avatarsMaxLength : 5, + threshold : 3, + }; + + const instance = ReactDOM.render(, parentDiv); + const summary = ReactTestUtils.findRenderedDOMComponentWithClass(instance, "mx_MemberEventListSummary_summary"); + const summaryText = summary.innerText; + + expect(summaryText).toBe( + "user_0, user_1 and 18 others joined" + ); + + done(); + }); +}); \ No newline at end of file From 203172c886b326782e66bf53d8dbd8d00104ff3c Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 17 Jan 2017 14:11:01 +0000 Subject: [PATCH 051/260] typos --- 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 fecb2a1841..da419897dc 100644 --- a/src/components/structures/ContextualMenu.js +++ b/src/components/structures/ContextualMenu.js @@ -67,7 +67,7 @@ module.exports = { chevronOffset.top = props.chevronOffset; } - // To overide the deafult chevron colour, if it's been set + // To override the default chevron colour, if it's been set var chevronCSS = ""; if (props.menuColour) { chevronCSS = ` From a18d94099e76232454ed3feefa837cfe8da532ab Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 17 Jan 2017 14:11:15 +0000 Subject: [PATCH 052/260] switch to using for edit button --- src/components/views/rooms/EventTile.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 42dbe78630..ef578d47db 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -263,7 +263,7 @@ module.exports = WithMatrixClient(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) - 19; + var y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19; var self = this; ContextualMenu.createMenu(MessageContextMenu, { chevronOffset: 10, @@ -465,7 +465,7 @@ module.exports = WithMatrixClient(React.createClass({ } var editButton = ( - Options + ); var e2e; From 0b67fd5b4ef834355dfaf9f8f6c4ac2dc62a5a01 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 17 Jan 2017 14:48:50 +0000 Subject: [PATCH 053/260] Add 'searching known users' to the user picker So it's more obvious it's only finding people you've already seen Fixes https://github.com/vector-im/riot-web/issues/2931 --- src/components/views/dialogs/ChatInviteDialog.js | 7 ++++++- src/components/views/elements/AddressSelector.js | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index e9a041357f..7fee741e47 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -396,11 +396,16 @@ module.exports = React.createClass({ if (this.state.error) { error =
You have entered an invalid contact. Try using their Matrix ID or email address.
} else { + const addressSelectorHeader =
+ Searching known users +
; addressSelector = ( {this.addressSelector = ref}} addressList={ this.state.queryList } onSelected={ this.onSelected } - truncateAt={ TRUNCATE_QUERY_LIST } /> + truncateAt={ TRUNCATE_QUERY_LIST } + header={ addressSelectorHeader } + /> ); } diff --git a/src/components/views/elements/AddressSelector.js b/src/components/views/elements/AddressSelector.js index 2c2d7e2d61..8b2855e99d 100644 --- a/src/components/views/elements/AddressSelector.js +++ b/src/components/views/elements/AddressSelector.js @@ -28,6 +28,9 @@ module.exports = React.createClass({ addressList: React.PropTypes.array.isRequired, truncateAt: React.PropTypes.number.isRequired, selected: React.PropTypes.number, + + // Element to put as a header on top of the list + header: React.PropTypes.node, }, getInitialState: function() { @@ -147,6 +150,7 @@ module.exports = React.createClass({ return (
{this.scrollElement = ref}}> + { this.props.header } { this.createAddressListTiles() }
); From 893a5c971fd43186637f50cb3725eefa09b6d365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20L=C3=B6thberg?= Date: Fri, 2 Dec 2016 19:58:35 +0100 Subject: [PATCH 054/260] Fix escaping markdown by rendering plaintext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We still need to parse "plaintext" messages through the markdown renderer so that escappes are rendered properly. Fixes vector-im/riot-web#2870. Signed-off-by: Johannes Löthberg --- src/Markdown.js | 32 ++++++++++++------- .../views/rooms/MessageComposerInput.js | 8 +++-- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/Markdown.js b/src/Markdown.js index 18c888b541..2eb84b9041 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -56,23 +56,31 @@ export default class Markdown { return is_plain; } - toHTML() { + render(html) { const parser = new commonmark.Parser(); const renderer = new commonmark.HtmlRenderer({safe: true}); const real_paragraph = renderer.paragraph; - renderer.paragraph = function(node, entering) { - // If there is only one top level node, just return the - // bare text: it's a single line of text and so should be - // 'inline', rather than unnecessarily wrapped in its own - // p tag. If, however, we have multiple nodes, each gets - // its own p tag to keep them as separate paragraphs. - var par = node; - while (par.parent) { - par = par.parent + if (html) { + renderer.paragraph = function(node, entering) { + // If there is only one top level node, just return the + // bare text: it's a single line of text and so should be + // 'inline', rather than unnecessarily wrapped in its own + // p tag. If, however, we have multiple nodes, each gets + // its own p tag to keep them as separate paragraphs. + var par = node; + while (par.parent) { + par = par.parent + } + if (par.firstChild != par.lastChild) { + real_paragraph.call(this, node, entering); + } } - if (par.firstChild != par.lastChild) { - real_paragraph.call(this, node, entering); + } else { + renderer.paragraph = function(node, entering) { + if (entering) { + this.lit('\n\n'); + } } } diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 37d937d6f5..5e8df592da 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -401,7 +401,7 @@ export default class MessageComposerInput extends React.Component { let contentState = null; if (enabled) { const md = new Markdown(this.state.editorState.getCurrentContent().getPlainText()); - contentState = RichText.HTMLtoContentState(md.toHTML()); + contentState = RichText.HTMLtoContentState(md.render(true)); } else { let markdown = stateToMarkdown(this.state.editorState.getCurrentContent()); if (markdown[markdown.length - 1] === '\n') { @@ -523,8 +523,10 @@ export default class MessageComposerInput extends React.Component { ); } else { const md = new Markdown(contentText); - if (!md.isPlainText()) { - contentHTML = md.toHTML(); + if (md.isPlainText()) { + contentText = md.render(false); + } else { + contentHTML = md.render(true); } } From 49f2b9df88c33187020900174e1ced591d66e035 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Tue, 17 Jan 2017 18:53:38 +0100 Subject: [PATCH 055/260] Remove duplicate test --- .../elements/MemberEventListSummary-test.js | 36 +------------------ 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/test/components/views/elements/MemberEventListSummary-test.js b/test/components/views/elements/MemberEventListSummary-test.js index af4d92fa61..39a79eaf6c 100644 --- a/test/components/views/elements/MemberEventListSummary-test.js +++ b/test/components/views/elements/MemberEventListSummary-test.js @@ -263,41 +263,7 @@ describe.only('MemberEventListSummary', function() { done(); }); - it('handles multple users following the same sequence of memberships', function(done) { - const events = generateEvents([ - // user_1 - {userId : "@user_1:some.domain", prevMembership: "ban", membership: "leave", senderId: "@some_other_user:some.domain"}, - {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, - {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, - {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, - {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, - {userId : "@user_1:some.domain", prevMembership: "leave", membership: "ban", senderId: "@some_other_user:some.domain"}, - // user_2 - {userId : "@user_2:some.domain", prevMembership: "ban", membership: "leave", senderId: "@some_other_user:some.domain"}, - {userId : "@user_2:some.domain", prevMembership: "leave", membership: "join"}, - {userId : "@user_2:some.domain", prevMembership: "join", membership: "leave"}, - {userId : "@user_2:some.domain", prevMembership: "leave", membership: "join"}, - {userId : "@user_2:some.domain", prevMembership: "join", membership: "leave"}, - {userId : "@user_2:some.domain", prevMembership: "leave", membership: "ban", senderId: "@some_other_user:some.domain"}, - ]); - const props = { - events : events, - children : generateTiles(events), - summaryLength : 1, - avatarsMaxLength : 5, - threshold : 3, - }; - - const instance = ReactDOM.render(, parentDiv); - const summary = ReactTestUtils.findRenderedDOMComponentWithClass(instance, "mx_MemberEventListSummary_summary"); - const summaryText = summary.innerText; - - expect(summaryText).toBe("user_1 and 1 other were unbanned, joined and left 2 times and were banned"); - - done(); - }); - - it('handles multple users following the same sequence of memberships', function(done) { + it('handles multiple users following the same sequence of memberships', function(done) { const events = generateEvents([ // user_1 {userId : "@user_1:some.domain", prevMembership: "ban", membership: "leave", senderId: "@some_other_user:some.domain"}, From 9574a0b663f9e4e6e4e013e6fe73707f2d41d831 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Tue, 17 Jan 2017 18:56:57 +0100 Subject: [PATCH 056/260] Remove pointless length guard --- src/components/views/elements/MemberEventListSummary.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index 1f05ba000e..945a5c28fd 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -66,10 +66,6 @@ module.exports = React.createClass({ }, _renderNameList: function(users) { - if (users.length === 0) { - return null; - } - return this._renderCommaSeparatedList(users, this.props.summaryLength); }, From 3ba9f5087320122f4af807b123b00224d4d23e0a Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Tue, 17 Jan 2017 19:07:45 +0100 Subject: [PATCH 057/260] Move functions around, remove redundancies, add docs --- .../views/elements/MemberEventListSummary.js | 159 +++++++++--------- .../elements/MemberEventListSummary-test.js | 2 +- 2 files changed, 81 insertions(+), 80 deletions(-) diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index 945a5c28fd..0c2861a023 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -69,57 +69,44 @@ module.exports = React.createClass({ return this._renderCommaSeparatedList(users, this.props.summaryLength); }, - // Test whether the first n items repeat for the duration - // e.g. [1,2,3,4,1,2,3] would resolve true for n = 4 - _isRepeatedSequence: function(transitions, n) { - let count = 0; - for (let i = 0; i < transitions.length; i++) { - if (transitions[i % n] !== transitions[i]) { - return null; - } + _renderSummary: function(eventAggregates, orderedTransitionSequences) { + let summaries = orderedTransitionSequences.map((transitions) => { + let nameList = this._renderNameList(eventAggregates[transitions]); + let plural = eventAggregates[transitions].length > 1; + + let splitTransitions = transitions.split(','); + + // Some pairs of transitions are common and are repeated a lot, so canonicalise them into "pair" transitions + let canonicalTransitions = this._getCanonicalTransitions(splitTransitions); + // Remove consecutive repetitions of the same transition (like 5 consecutive 'join_and_leave's) + let truncatedTransitions = this._getTruncatedTransitions(canonicalTransitions); + + let descs = truncatedTransitions.map((t) => { + return this._getDescriptionForTransition(t.transitionType, plural, t.repeats); + }); + + let desc = this._renderCommaSeparatedList(descs); + + return nameList + " " + desc; + }); + + if (!summaries) { + return null; } - return true; + + return ( + + {summaries.join(", ")} + + ); }, - _renderCommaSeparatedList(items, itemLimit) { - const remaining = itemLimit === undefined ? 0 : Math.max(items.length - itemLimit, 0); - if (items.length === 0) { - return ""; - } else if (items.length === 1) { - return items[0]; - } else if (remaining) { - items = items.slice(0, itemLimit); - const other = " other" + (remaining > 1 ? "s" : ""); - return items.join(', ') + ' and ' + remaining + other; - } else { - let last = items.pop(); - return items.join(', ') + ' and ' + last; - } - }, - - _getDescriptionForTransition(t, plural, repeats) { - let beConjugated = plural ? "were" : "was"; - let invitation = "their invitation" + (plural || (repeats > 1) ? "s" : ""); - - let res = null; - let map = { - "joined": "joined", - "left": "left", - "joined_and_left": "joined and left", - "left_and_joined": "left and rejoined", - "invite_reject": "rejected " + invitation, - "invite_withdrawal": "had " + invitation + " withdrawn", - "invited": beConjugated + " invited", - "banned": beConjugated + " banned", - "unbanned": beConjugated + " unbanned", - "kicked": beConjugated + " kicked", - }; - - if (Object.keys(map).includes(t)) { - res = map[t] + (repeats > 1 ? " " + repeats + " times" : "" ); + _renderNameList: function(users) { + if (users.length === 0) { + return null; } - return res; + return this._renderCommaSeparatedList(users, this.props.summaryLength); }, _getCanonicalTransitions: function(transitions) { @@ -174,39 +161,53 @@ module.exports = React.createClass({ return res; }, - _renderSummary: function(eventAggregates, orderedTransitionSequences) { - let summaries = orderedTransitionSequences.map((transitions) => { - let nameList = this._renderNameList(eventAggregates[transitions]); - let plural = eventAggregates[transitions].length > 1; + /** + * For a certain transition, t, describe what happened to the users that + * underwent the transition. + * @param {string} t the transition type + * @param {boolean} plural whether there were multiple users undergoing the same transition + * @param {number} repeats the number of times the transition was repeated in a row + * @returns {string} the spoken English equivalent of the transition + */ + _getDescriptionForTransition(t, plural, repeats) { + let beConjugated = plural ? "were" : "was"; + let invitation = "their invitation" + (plural || (repeats > 1) ? "s" : ""); - let repeats = 1; - let repeatExtra = 0; + let res = null; + let map = { + "joined": "joined", + "left": "left", + "joined_and_left": "joined and left", + "left_and_joined": "left and rejoined", + "invite_reject": "rejected " + invitation, + "invite_withdrawal": "had " + invitation + " withdrawn", + "invited": beConjugated + " invited", + "banned": beConjugated + " banned", + "unbanned": beConjugated + " unbanned", + "kicked": beConjugated + " kicked", + }; - let splitTransitions = transitions.split(','); - - // Some pairs of transitions are common and are repeated a lot, so canonicalise them into "pair" transitions - let canonicalTransitions = this._getCanonicalTransitions(splitTransitions); - // Remove consecutive repetitions of the same transition (like 5 consecutive 'join_and_leave's) - let truncatedTransitions = this._getTruncatedTransitions(canonicalTransitions); - - let descs = truncatedTransitions.map((t) => { - return this._getDescriptionForTransition(t.transitionType, plural, t.repeats); - }); - - let desc = this._renderCommaSeparatedList(descs); - - return nameList + " " + desc; - }); - - if (!summaries) { - return null; + if (Object.keys(map).includes(t)) { + res = map[t] + (repeats > 1 ? " " + repeats + " times" : "" ); } - return ( - - {summaries.join(", ")} - - ); + return res; + }, + + _renderCommaSeparatedList(items, itemLimit) { + const remaining = itemLimit === undefined ? 0 : Math.max(items.length - itemLimit, 0); + if (items.length === 0) { + return ""; + } else if (items.length === 1) { + return items[0]; + } else if (remaining) { + items = items.slice(0, itemLimit); + const other = " other" + (remaining > 1 ? "s" : ""); + return items.join(', ') + ' and ' + remaining + other; + } else { + let last = items.pop(); + return items.join(', ') + ' and ' + last; + } }, _renderAvatars: function(roomMembers) { @@ -223,6 +224,10 @@ module.exports = React.createClass({ ); }, + _getTransitionSequence: function(events) { + return events.map(this._getTransition); + }, + _getTransition: function(e) { switch (e.mxEvent.getContent().membership) { case 'invite': return 'invited'; @@ -245,10 +250,6 @@ module.exports = React.createClass({ } }, - _getTransitionSequence: function(events) { - return events.map(this._getTransition); - }, - render: function() { let eventsToRender = this.props.events; let fewEvents = eventsToRender.length < this.props.threshold; diff --git a/test/components/views/elements/MemberEventListSummary-test.js b/test/components/views/elements/MemberEventListSummary-test.js index 39a79eaf6c..61e0f9627b 100644 --- a/test/components/views/elements/MemberEventListSummary-test.js +++ b/test/components/views/elements/MemberEventListSummary-test.js @@ -6,7 +6,7 @@ const sdk = require('matrix-react-sdk'); const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary'); const testUtils = require('../../../test-utils'); -describe.only('MemberEventListSummary', function() { +describe('MemberEventListSummary', function() { let sandbox; let parentDiv; From a87e7d66170bcae5c28075aef6aae3de696703ae Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 17 Jan 2017 18:17:51 +0000 Subject: [PATCH 058/260] Make user search do a bit better on word boundary --- .../views/dialogs/ChatInviteDialog.js | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index e9a041357f..59e95d1538 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -27,6 +27,10 @@ var Modal = require('../../../Modal'); const TRUNCATE_QUERY_LIST = 40; +function escapeRegExp(str) { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); +} + module.exports = React.createClass({ displayName: "ChatInviteDialog", propTypes: { @@ -315,13 +319,18 @@ module.exports = React.createClass({ return true; } - // split spaces in name and try matching constituent parts - var parts = name.split(" "); - for (var i = 0; i < parts.length; i++) { - if (parts[i].indexOf(query) === 0) { - return true; - } + // Try to find the query following a "word boundary", except that + // this does avoids using \b because it only considers letters from + // the roman alphabet to be word characters. + // Instead, we look for the query following either: + // * The start of the string + // * Whitespace, or + // * A fixed number of punctuation characters + let expr = new RegExp("(?:^|[\\s\\('\",\.-])" + escapeRegExp(query)); + if (expr.test(name)) { + return true; } + return false; }, From c2cdb626bdf3b8156168a6594ba1c8462d482c2f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 17 Jan 2017 19:13:04 +0000 Subject: [PATCH 059/260] let the tinter handle 'white' highlights of SVGs too --- src/Tinter.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Tinter.js b/src/Tinter.js index 534a1d810b..c18d3068a7 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -42,6 +42,7 @@ var keyHex = [ "#76CFA6", // Vector Green "#EAF5F0", // Vector Light Green "#D3EFE1", // BottomLeftMenu overlay (20% Vector Green overlaid on Vector Light Green) + "#FFFFFF", // white highlights of the SVGs (for switching to dark theme) ]; // cache of our replacement colours @@ -50,6 +51,7 @@ var colors = [ keyHex[0], keyHex[1], keyHex[2], + keyHex[3], ]; var cssFixups = [ @@ -172,7 +174,7 @@ module.exports = { tintables.push(tintable); }, - tint: function(primaryColor, secondaryColor, tertiaryColor) { + tint: function(primaryColor, secondaryColor, tertiaryColor, whiteColor) { if (!cached) { calcCssFixups(); @@ -203,14 +205,19 @@ module.exports = { tertiaryColor = rgbToHex(rgb1); } + if (!whiteColor) { + whiteColor = colors[3]; + } + if (colors[0] === primaryColor && colors[1] === secondaryColor && - colors[2] === tertiaryColor) + colors[2] === tertiaryColor && + colors[3] === whiteColor) { return; } - colors = [primaryColor, secondaryColor, tertiaryColor]; + colors = [primaryColor, secondaryColor, tertiaryColor, whiteColor]; if (DEBUG) console.log("Tinter.tint"); From 8288eb730c0ddd7239dbb542031dd6b56de4eeac Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 17 Jan 2017 19:13:23 +0000 Subject: [PATCH 060/260] a rather hacky implementation of theme switching --- src/components/structures/MatrixChat.js | 42 +++++++++++++++ src/components/structures/UserSettings.js | 63 ++++++++++++++++++----- 2 files changed, 93 insertions(+), 12 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index c47109db94..79e93bb990 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -25,6 +25,7 @@ var SdkConfig = require("../../SdkConfig"); var ContextualMenu = require("./ContextualMenu"); var RoomListSorter = require("../../RoomListSorter"); var UserActivity = require("../../UserActivity"); +var UserSettingsStore = require('../../UserSettingsStore'); var Presence = require("../../Presence"); var dis = require("../../dispatcher"); @@ -456,6 +457,9 @@ module.exports = React.createClass({ middleOpacity: payload.middleOpacity, }); break; + case 'set_theme': + this._onSetTheme(payload.value); + break; case 'on_logged_in': this._onLoggedIn(); break; @@ -584,6 +588,44 @@ module.exports = React.createClass({ _onLoadCompleted: function() { this.props.onLoadCompleted(); this.setState({loading: false}); + + // set up the right theme. + // XXX: this will temporarily flicker the wrong CSS. + dis.dispatch({ + action: 'set_theme', + value: UserSettingsStore.getSyncedSetting('theme') + }); + }, + + /** + * Called whenever someone changes the theme + */ + _onSetTheme: function(theme) { + if (!theme) { + theme = 'light'; + } + + var i, a; + for (i = 0; (a = document.getElementsByTagName("link")[i]); i++) { + var href = a.getAttribute("href"); + if (href.startsWith("theme-")) { + if (href.startsWith("theme-" + theme + ".")) { + a.disabled = false; + } + else { + a.disabled = true; + } + } + } + + if (theme === 'dark') { + // abuse the tinter to change all the SVG's #fff to #2d2d2d + // XXX: obviously this shouldn't be hardcoded here. + Tinter.tint(undefined, undefined, undefined, '#2d2d2d'); + } + else { + Tinter.tint(undefined, undefined, undefined, '#ffffff'); + } }, /** diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index a41eab3a76..ebfd6dc0a7 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -363,7 +363,23 @@ module.exports = React.createClass({ */ ]; + var themes = [ + { + id: 'theme', + label: 'Light theme', + value: 'light', + }, + { + id: 'theme', + label: 'Dark theme', + value: 'dark', + } + ]; + var syncedSettings = UserSettingsStore.getSyncedSettings(); + if (!syncedSettings.theme) { + syncedSettings.theme = 'light'; + } return (
@@ -379,19 +395,42 @@ module.exports = React.createClass({ Disable inline URL previews by default
+ { settingsLabels.map( setting => { + return
+ UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) } + /> + +
+ })} + { themes.map( setting => { + return
+ { + if (e.target.checked) { + UserSettingsStore.setSyncedSetting(setting.id, setting.value) + } + dis.dispatch({ + action: 'set_theme', + value: setting.value, + }); + } + } + /> + +
+ })} - { settingsLabels.forEach( setting => { -
- UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) } - /> - -
- })} ); }, From 35d70f0b35aeefa4af43379078b63922f0f70afe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20L=C3=B6thberg?= Date: Tue, 17 Jan 2017 20:32:06 +0100 Subject: [PATCH 061/260] markdown: Only add \n\n on multiple paragraphs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johannes Löthberg --- src/Markdown.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Markdown.js b/src/Markdown.js index 2eb84b9041..17723e42f8 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -78,8 +78,20 @@ export default class Markdown { } } else { renderer.paragraph = function(node, entering) { - if (entering) { - this.lit('\n\n'); + // If there is only one top level node, just return the + // bare text: it's a single line of text and so should be + // 'inline', rather than unnecessarily wrapped in its own + // p tag. If, however, we have multiple nodes, each gets + // its own p tag to keep them as separate paragraphs. + var par = node; + while (par.parent) { + node = par; + par = par.parent; + } + if (node != par.lastChild) { + if (!entering) { + this.lit('\n\n'); + } } } } From c819b433a247ca502daf600c3bc763f138262f5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20L=C3=B6thberg?= Date: Tue, 17 Jan 2017 20:37:27 +0100 Subject: [PATCH 062/260] Make old message composer use new markdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johannes Löthberg --- src/components/views/rooms/MessageComposerInputOld.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/MessageComposerInputOld.js b/src/components/views/rooms/MessageComposerInputOld.js index 28e3186c50..3b0100278b 100644 --- a/src/components/views/rooms/MessageComposerInputOld.js +++ b/src/components/views/rooms/MessageComposerInputOld.js @@ -325,12 +325,13 @@ module.exports = React.createClass({ } if (send_markdown) { - const htmlText = mdown.toHTML(); + const htmlText = mdown.render(true); sendMessagePromise = isEmote ? MatrixClientPeg.get().sendHtmlEmote(this.props.room.roomId, contentText, htmlText) : MatrixClientPeg.get().sendHtmlMessage(this.props.room.roomId, contentText, htmlText); } else { + const contentText = mdown.render(false); sendMessagePromise = isEmote ? MatrixClientPeg.get().sendEmoteMessage(this.props.room.roomId, contentText) : MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText); From 49d60ff879aa677e5befb10c831b0d89e9952d0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20L=C3=B6thberg?= Date: Tue, 17 Jan 2017 21:04:12 +0100 Subject: [PATCH 063/260] Markdown: softbreak is not HTML MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johannes Löthberg --- src/Markdown.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Markdown.js b/src/Markdown.js index 17723e42f8..ad7ec5ef0c 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -48,6 +48,7 @@ export default class Markdown { } // text and paragraph are just text dummy_renderer.text = function(t) { return t; } + dummy_renderer.softbreak = function(t) { return t; } dummy_renderer.paragraph = function(t) { return t; } const dummy_parser = new commonmark.Parser(); From 2e3bdcf5c697009b0d7b6b82ab5f902ec49115de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20L=C3=B6thberg?= Date: Tue, 17 Jan 2017 22:20:05 +0100 Subject: [PATCH 064/260] Markdown: Don't XML escape the output when not HTML MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johannes Löthberg --- src/Markdown.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Markdown.js b/src/Markdown.js index ad7ec5ef0c..e6f5f59f01 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -78,6 +78,10 @@ export default class Markdown { } } } else { + renderer.out = function(s) { + this.lit(s); + } + renderer.paragraph = function(node, entering) { // If there is only one top level node, just return the // bare text: it's a single line of text and so should be From 484549e50be36266c581d4583c84c9c55db1e5cf Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Wed, 18 Jan 2017 10:26:25 +0100 Subject: [PATCH 065/260] Refactor a few things and document everything --- .../views/elements/MemberEventListSummary.js | 108 +++++++++++++----- 1 file changed, 82 insertions(+), 26 deletions(-) diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index 0c2861a023..41307969d6 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -65,20 +65,27 @@ module.exports = React.createClass({ }); }, - _renderNameList: function(users) { - return this._renderCommaSeparatedList(users, this.props.summaryLength); - }, - + /** + * Render the JSX for users aggregated by their transition sequences (`eventAggregates`) where + * the sequences are ordered by `orderedTransitionSequences`. + * @param {object[]} eventAggregates a map of transition sequence to array of user display names + * or user IDs. + * @param {string[]} orderedTransitionSequences an array which is some ordering of + * `Object.keys(eventAggregates)`. + * @returns {ReactElement} a single containing the textual summary of the aggregated + * events that occurred. + */ _renderSummary: function(eventAggregates, orderedTransitionSequences) { let summaries = orderedTransitionSequences.map((transitions) => { - let nameList = this._renderNameList(eventAggregates[transitions]); - let plural = eventAggregates[transitions].length > 1; + let userNames = eventAggregates[transitions]; + let nameList = this._renderNameList(userNames); + let plural = userNames.length > 1; let splitTransitions = transitions.split(','); - // Some pairs of transitions are common and are repeated a lot, so canonicalise them into "pair" transitions + // Some neighbouring transitions are common, so canonicalise some into "pair" transitions let canonicalTransitions = this._getCanonicalTransitions(splitTransitions); - // Remove consecutive repetitions of the same transition (like 5 consecutive 'join_and_leave's) + // Transform into consecutive repetitions of the same transition (like 5 consecutive 'joined_and_left's) let truncatedTransitions = this._getTruncatedTransitions(canonicalTransitions); let descs = truncatedTransitions.map((t) => { @@ -101,14 +108,30 @@ module.exports = React.createClass({ ); }, + /** + * @param {string[]} users an array of user display names or user IDs. + * @returns {string} a comma-separated list that ends with "and [n] others" if there are + * more items in `users` than `this.props.summaryLength`, which is the number of names + * included before "and [n] others". + */ _renderNameList: function(users) { - if (users.length === 0) { - return null; - } - return this._renderCommaSeparatedList(users, this.props.summaryLength); }, + /** + * Canonicalise an array of transitions into an array of transitions and how many times + * they are repeated consecutively. + * + * An array of 123 "joined_and_left" transitions, would result in: + * ``` + * [{ + * transitionType: "joined_and_left" + * repeats: 123 + * }, ... ] + * ``` + * @param {string[]} transitions the array of transitions to transform. + * @returns {object[]} an array of truncated transitions. + */ _getCanonicalTransitions: function(transitions) { let modMap = { 'joined' : { @@ -142,6 +165,20 @@ module.exports = React.createClass({ return res; }, + /** + * Transform an array of transitions into an array of transitions and how many times + * they are repeated consecutively. + * + * An array of 123 "joined_and_left" transitions, would result in: + * ``` + * [{ + * transitionType: "joined_and_left" + * repeats: 123 + * }, ... ] + * ``` + * @param {string[]} transitions the array of transitions to transform. + * @returns {object[]} an array of truncated transitions. + */ _getTruncatedTransitions: function(transitions) { let res = []; for (let i = 0; i < transitions.length; i++) { @@ -154,20 +191,16 @@ module.exports = React.createClass({ }); } } - // returns [{ - // transitionType: "joined_and_left" - // repeats: 123 - // }, ... ] return res; }, /** * For a certain transition, t, describe what happened to the users that * underwent the transition. - * @param {string} t the transition type - * @param {boolean} plural whether there were multiple users undergoing the same transition - * @param {number} repeats the number of times the transition was repeated in a row - * @returns {string} the spoken English equivalent of the transition + * @param {string} t the transition type. + * @param {boolean} plural whether there were multiple users undergoing the same transition. + * @param {number} repeats the number of times the transition was repeated in a row. + * @returns {string} the written English equivalent of the transition. */ _getDescriptionForTransition(t, plural, repeats) { let beConjugated = plural ? "were" : "was"; @@ -194,6 +227,16 @@ module.exports = React.createClass({ return res; }, + /** + * Constructs a written English string representing `items`, with an optional limit on the number + * of items included in the result. If specified and if the length of `items` is greater than the + * limit, the string "and n others" will be appended onto the result. + * If `items` is empty, returns the empty string. If there is only one item, return it. + * @param {string[]} items the items to construct a string from. + * @param {number?} itemLimit the number by which to limit the list. + * @returns {string} a string constructed by joining `items` with a comma between each + * item, but with the last item appended as " and [lastItem]". + */ _renderCommaSeparatedList(items, itemLimit) { const remaining = itemLimit === undefined ? 0 : Math.max(items.length - itemLimit, 0); if (items.length === 0) { @@ -228,6 +271,14 @@ module.exports = React.createClass({ return events.map(this._getTransition); }, + /** + * Enumerate a given membership event, `e`, where `getContent().membership` has + * changed for each transition allowed by the Matrix protocol. This attempts to + * enumerate the membership changes that occur in `../../../TextForEvent.js`. + * @param {MatrixEvent} e the membership change event to enumerate. + * @returns {string?} the transition type given to this event. This defaults to `null` + * if a transition is not recognised. + */ _getTransition: function(e) { switch (e.mxEvent.getContent().membership) { case 'invite': return 'invited'; @@ -268,16 +319,25 @@ module.exports = React.createClass({ ); } - // Map user IDs to all of the user's member events in eventsToRender + // Map user IDs to an array of objects: let userEvents = { - // $userId : [] + // $userId : [{ + // // The original event + // mxEvent: e, + // // The display name of the user (if not, then user ID) + // displayName: e.target.name || userId, + // // The original index of the event in this.props.events + // index: index, + // }] }; + let avatarMembers = []; eventsToRender.forEach((e, index) => { const userId = e.getStateKey(); // Initialise a user's events if (!userEvents[userId]) { userEvents[userId] = []; + avatarMembers.push(e.target); } userEvents[userId].push({ mxEvent: e, @@ -299,8 +359,6 @@ module.exports = React.createClass({ // $aggregateType : int }; - let avatarMembers = []; - let users = Object.keys(userEvents); users.forEach( (userId) => { @@ -320,8 +378,6 @@ module.exports = React.createClass({ if (aggregateIndices[seq] === -1 || firstEvent.index < aggregateIndices[seq]) { aggregateIndices[seq] = firstEvent.index; } - - avatarMembers.push(firstEvent.mxEvent.target); } ); From 5dd1512ff27e7299d89d604a91ebfbedff441780 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Wed, 18 Jan 2017 10:59:19 +0100 Subject: [PATCH 066/260] Move aggregation code to dedicated function --- .../views/elements/MemberEventListSummary.js | 79 ++++++++++--------- .../elements/MemberEventListSummary-test.js | 2 +- 2 files changed, 44 insertions(+), 37 deletions(-) diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index 41307969d6..57686344ed 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -301,6 +301,46 @@ module.exports = React.createClass({ } }, + _getAggregate: function(userEvents) { + // A map of aggregate type to arrays of display names. Each aggregate type + // is a comma-delimited string of transitions, e.g. "joined,left,kicked". + // The array of display names is the array of users who went through that + // sequence during eventsToRender. + let aggregate = { + // $aggregateType : []:string + }; + // A map of aggregate types to the indices that order them (the index of + // the first event for a given transition sequence) + let aggregateIndices = { + // $aggregateType : int + }; + + let users = Object.keys(userEvents); + users.forEach( + (userId) => { + let firstEvent = userEvents[userId][0]; + let displayName = firstEvent.displayName; + + let seq = this._getTransitionSequence(userEvents[userId]); + if (!aggregate[seq]) { + aggregate[seq] = []; + aggregateIndices[seq] = -1; + } + + aggregate[seq].push(displayName); + + if (aggregateIndices[seq] === -1 || firstEvent.index < aggregateIndices[seq]) { + aggregateIndices[seq] = firstEvent.index; + } + } + ); + + return { + names: aggregate, + indices: aggregateIndices, + }; + }, + render: function() { let eventsToRender = this.props.events; let fewEvents = eventsToRender.length < this.props.threshold; @@ -346,46 +386,13 @@ module.exports = React.createClass({ }); }); - // A map of aggregate type to arrays of display names. Each aggregate type - // is a comma-delimited string of transitions, e.g. "joined,left,kicked". - // The array of display names is the array of users who went through that - // sequence during eventsToRender. - let aggregate = { - // $aggregateType : []:string - }; - // A map of aggregate types to the indices that order them (the index of - // the first event for a given transition sequence) - let aggregateIndices = { - // $aggregateType : int - }; - - let users = Object.keys(userEvents); - users.forEach( - (userId) => { - let firstEvent = userEvents[userId][0]; - let displayName = firstEvent.displayName; - - let seq = this._getTransitionSequence(userEvents[userId]); - if (!aggregate[seq]) { - aggregate[seq] = []; - aggregateIndices[seq] = -1; - } - - if (aggregate[seq].indexOf(displayName) === -1) { - aggregate[seq].push(displayName); - } - - if (aggregateIndices[seq] === -1 || firstEvent.index < aggregateIndices[seq]) { - aggregateIndices[seq] = firstEvent.index; - } - } - ); + let aggregate = this._getAggregate(userEvents); // Sort types by order of lowest event index within sequence - let orderedTransitionSequences = Object.keys(aggregate).sort((seq1, seq2) => aggregateIndices[seq1] > aggregateIndices[seq2]); + let orderedTransitionSequences = Object.keys(aggregate.names).sort((seq1, seq2) => aggregate.indices[seq1] > aggregate.indices[seq2]); let avatars = this._renderAvatars(avatarMembers); - let summary = this._renderSummary(aggregate, orderedTransitionSequences); + let summary = this._renderSummary(aggregate.names, orderedTransitionSequences); let toggleButton = (
{expanded ? 'collapse' : 'expand'} diff --git a/test/components/views/elements/MemberEventListSummary-test.js b/test/components/views/elements/MemberEventListSummary-test.js index 61e0f9627b..39a79eaf6c 100644 --- a/test/components/views/elements/MemberEventListSummary-test.js +++ b/test/components/views/elements/MemberEventListSummary-test.js @@ -6,7 +6,7 @@ const sdk = require('matrix-react-sdk'); const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary'); const testUtils = require('../../../test-utils'); -describe('MemberEventListSummary', function() { +describe.only('MemberEventListSummary', function() { let sandbox; let parentDiv; From 78e2c787e08b7fef7d50cf02cf9aceb907d8670a Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Wed, 18 Jan 2017 11:53:17 +0100 Subject: [PATCH 067/260] Refactor and document test helpers. --- .../elements/MemberEventListSummary-test.js | 64 ++++++++++--------- test/test-utils.js | 12 +++- 2 files changed, 45 insertions(+), 31 deletions(-) diff --git a/test/components/views/elements/MemberEventListSummary-test.js b/test/components/views/elements/MemberEventListSummary-test.js index 39a79eaf6c..93c085f88c 100644 --- a/test/components/views/elements/MemberEventListSummary-test.js +++ b/test/components/views/elements/MemberEventListSummary-test.js @@ -4,12 +4,15 @@ const ReactDOM = require("react-dom"); const ReactTestUtils = require('react-addons-test-utils'); const sdk = require('matrix-react-sdk'); const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary'); +var jssdk = require('matrix-js-sdk'); +var MatrixEvent = jssdk.MatrixEvent; const testUtils = require('../../../test-utils'); describe.only('MemberEventListSummary', function() { let sandbox; let parentDiv; + // Generate dummy event tiles for use in simulating an expanded MELS const generateTiles = (events) => { return events.map((e) => { return ( @@ -20,42 +23,41 @@ describe.only('MemberEventListSummary', function() { }); }; + /** + * Generates a membership event with the target of the event set as a mocked RoomMember based + * on `parameters.userId`. + * @param {string} eventId the ID of the event. + * @param {object} parameters the parameters to use to create the event. + * @param {string} parameters.membership the membership to assign to `content.membership` + * @param {string} parameters.userId the state key and target userId of the event. If + * `parameters.senderId` is not specified, this is also used as the event sender. + * @param {string} parameters.prevMembership the membership to assign to + * `prev_content.membership`. + * @param {string} parameters.senderId the user ID of the sender of the event. Optional. + * Defaults to `parameters.userId`. + * @returns {MatrixEvent} the event created. + */ const generateMembershipEvent = (eventId, parameters) => { - let membership = parameters.membership; - let userId = parameters.userId; - let prevMembership = parameters.prevMembership; - let senderId = parameters.senderId; - return { - content: { - membership: membership, - }, - target: { - name: userId.match(/@([^:]*):/)[1], + let e = testUtils.mkMembership({ + event: true, + user: parameters.senderId || parameters.userId, + skey: parameters.userId, + mship: parameters.membership, + prevMship: parameters.prevMembership, + target : { + name: parameters.userId.match(/@([^:]*):/)[1], // Use localpart as display name + userId: parameters.userId, getAvatarUrl: () => { return "avatar.jpeg"; }, - userId: userId, }, - getId: () => { - return eventId; - }, - getContent: function() { - return this.content; - }, - getPrevContent: function() { - return { - membership: prevMembership ? prevMembership : this.content, - }; - }, - getSender: () => { - return senderId || userId; - }, - getStateKey: () => { - return userId; - }, - }; + }); + // Override random event ID + e.event.event_id = eventId; + return e; }; + // Generate mock MatrixEvents from the array of parameters const generateEvents = (parameters) => { const res = []; for (let i = 0; i < parameters.length; i++) { @@ -64,6 +66,9 @@ describe.only('MemberEventListSummary', function() { return res; }; + // Generate the same sequence of `events` for `n` users, where each user ID + // is created by replacing the first "$" in userIdTemplate with `i` for + // `i = 0 .. n`. const generateEventsForUsers = (userIdTemplate, n, events) => { let eventsForUsers = []; let userId = ""; @@ -71,7 +76,6 @@ describe.only('MemberEventListSummary', function() { userId = userIdTemplate.replace('$', i); events.forEach((e) => { e.userId = userId; - return e; }); eventsForUsers = eventsForUsers.concat(generateEvents(events)); } diff --git a/test/test-utils.js b/test/test-utils.js index db405c2e1a..cdfae4421c 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -108,6 +108,7 @@ export function mkEvent(opts) { room_id: opts.room, sender: opts.user, content: opts.content, + prev_content: opts.prev_content, event_id: "$" + Math.random() + "-" + Math.random(), origin_server_ts: opts.ts, }; @@ -150,7 +151,9 @@ export function mkPresence(opts) { * @param {Object} opts Values for the membership. * @param {string} opts.room The room ID for the event. * @param {string} opts.mship The content.membership for the event. + * @param {string} opts.prevMship The prev_content.membership for the event. * @param {string} opts.user The user ID for the event. + * @param {RoomMember} opts.target The target of the event. * @param {string} opts.skey The other user ID for the event if applicable * e.g. for invites/bans. * @param {string} opts.name The content.displayname for the event. @@ -169,9 +172,16 @@ export function mkMembership(opts) { opts.content = { membership: opts.mship }; + if (opts.prevMship) { + opts.prev_content = { membership: opts.prevMship }; + } if (opts.name) { opts.content.displayname = opts.name; } if (opts.url) { opts.content.avatar_url = opts.url; } - return mkEvent(opts); + let e = mkEvent(opts); + if (opts.target) { + e.target = opts.target; + } + return e; }; /** From 867a532e5e5aa5bce40e2eec68c79d7c6c5b5352 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Wed, 18 Jan 2017 11:58:54 +0100 Subject: [PATCH 068/260] Remove parentDiv from tests --- .../elements/MemberEventListSummary-test.js | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/test/components/views/elements/MemberEventListSummary-test.js b/test/components/views/elements/MemberEventListSummary-test.js index 93c085f88c..325b1d6b17 100644 --- a/test/components/views/elements/MemberEventListSummary-test.js +++ b/test/components/views/elements/MemberEventListSummary-test.js @@ -10,7 +10,6 @@ var MatrixEvent = jssdk.MatrixEvent; const testUtils = require('../../../test-utils'); describe.only('MemberEventListSummary', function() { let sandbox; - let parentDiv; // Generate dummy event tiles for use in simulating an expanded MELS const generateTiles = (events) => { @@ -85,8 +84,6 @@ describe.only('MemberEventListSummary', function() { beforeEach(function() { testUtils.beforeEach(this); sandbox = testUtils.stubClient(); - parentDiv = document.createElement('div'); - document.body.appendChild(parentDiv); }); afterEach(function() { @@ -155,7 +152,7 @@ describe.only('MemberEventListSummary', function() { threshold : 3, }; - const instance = ReactDOM.render(, parentDiv); + const instance = ReactTestUtils.renderIntoDocument(); const summary = ReactTestUtils.findRenderedDOMComponentWithClass(instance, "mx_MemberEventListSummary_summary"); const summaryText = summary.innerText; @@ -189,7 +186,7 @@ describe.only('MemberEventListSummary', function() { threshold : 3, }; - const instance = ReactDOM.render(, parentDiv); + const instance = ReactTestUtils.renderIntoDocument(); const summary = ReactTestUtils.findRenderedDOMComponentWithClass(instance, "mx_MemberEventListSummary_summary"); const summaryText = summary.innerText; @@ -225,7 +222,7 @@ describe.only('MemberEventListSummary', function() { threshold : 3, }; - const instance = ReactDOM.render(, parentDiv); + const instance = ReactTestUtils.renderIntoDocument(); const summary = ReactTestUtils.findRenderedDOMComponentWithClass(instance, "mx_MemberEventListSummary_summary"); const summaryText = summary.innerText; @@ -258,7 +255,7 @@ describe.only('MemberEventListSummary', function() { threshold : 3, }; - const instance = ReactDOM.render(, parentDiv); + const instance = ReactTestUtils.renderIntoDocument(); const summary = ReactTestUtils.findRenderedDOMComponentWithClass(instance, "mx_MemberEventListSummary_summary"); const summaryText = summary.innerText; @@ -292,7 +289,7 @@ describe.only('MemberEventListSummary', function() { threshold : 3, }; - const instance = ReactDOM.render(, parentDiv); + const instance = ReactTestUtils.renderIntoDocument(); const summary = ReactTestUtils.findRenderedDOMComponentWithClass(instance, "mx_MemberEventListSummary_summary"); const summaryText = summary.innerText; @@ -318,7 +315,7 @@ describe.only('MemberEventListSummary', function() { threshold : 3, }; - const instance = ReactDOM.render(, parentDiv); + const instance = ReactTestUtils.renderIntoDocument(); const summary = ReactTestUtils.findRenderedDOMComponentWithClass(instance, "mx_MemberEventListSummary_summary"); const summaryText = summary.innerText; @@ -349,7 +346,7 @@ describe.only('MemberEventListSummary', function() { threshold : 3, }; - const instance = ReactDOM.render(, parentDiv); + const instance = ReactTestUtils.renderIntoDocument(); const summary = ReactTestUtils.findRenderedDOMComponentWithClass(instance, "mx_MemberEventListSummary_summary"); const summaryText = summary.innerText; @@ -389,7 +386,7 @@ describe.only('MemberEventListSummary', function() { threshold : 3, }; - const instance = ReactDOM.render(, parentDiv); + const instance = ReactTestUtils.renderIntoDocument(); const summary = ReactTestUtils.findRenderedDOMComponentWithClass(instance, "mx_MemberEventListSummary_summary"); const summaryText = summary.innerText; @@ -415,7 +412,7 @@ describe.only('MemberEventListSummary', function() { threshold : 3, }; - const instance = ReactDOM.render(, parentDiv); + const instance = ReactTestUtils.renderIntoDocument(); const summary = ReactTestUtils.findRenderedDOMComponentWithClass(instance, "mx_MemberEventListSummary_summary"); const summaryText = summary.innerText; @@ -439,7 +436,7 @@ describe.only('MemberEventListSummary', function() { threshold : 1, // threshold = 1 to force collapse }; - const instance = ReactDOM.render(, parentDiv); + const instance = ReactTestUtils.renderIntoDocument(); const summary = ReactTestUtils.findRenderedDOMComponentWithClass(instance, "mx_MemberEventListSummary_summary"); const summaryText = summary.innerText; @@ -465,7 +462,7 @@ describe.only('MemberEventListSummary', function() { threshold : 3, }; - const instance = ReactDOM.render(, parentDiv); + const instance = ReactTestUtils.renderIntoDocument(); const summary = ReactTestUtils.findRenderedDOMComponentWithClass(instance, "mx_MemberEventListSummary_summary"); const summaryText = summary.innerText; @@ -490,7 +487,7 @@ describe.only('MemberEventListSummary', function() { threshold : 3, }; - const instance = ReactDOM.render(, parentDiv); + const instance = ReactTestUtils.renderIntoDocument(); const summary = ReactTestUtils.findRenderedDOMComponentWithClass(instance, "mx_MemberEventListSummary_summary"); const summaryText = summary.innerText; @@ -513,7 +510,7 @@ describe.only('MemberEventListSummary', function() { threshold : 3, }; - const instance = ReactDOM.render(, parentDiv); + const instance = ReactTestUtils.renderIntoDocument(); const summary = ReactTestUtils.findRenderedDOMComponentWithClass(instance, "mx_MemberEventListSummary_summary"); const summaryText = summary.innerText; From 41d2697e28719bd45ff041ca33c45d6867588ee8 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Wed, 18 Jan 2017 12:03:38 +0100 Subject: [PATCH 069/260] Remove `done`, const instead of var for `requier`s --- .../elements/MemberEventListSummary-test.js | 62 +++++-------------- 1 file changed, 17 insertions(+), 45 deletions(-) diff --git a/test/components/views/elements/MemberEventListSummary-test.js b/test/components/views/elements/MemberEventListSummary-test.js index 325b1d6b17..7094520f7b 100644 --- a/test/components/views/elements/MemberEventListSummary-test.js +++ b/test/components/views/elements/MemberEventListSummary-test.js @@ -4,8 +4,8 @@ const ReactDOM = require("react-dom"); const ReactTestUtils = require('react-addons-test-utils'); const sdk = require('matrix-react-sdk'); const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary'); -var jssdk = require('matrix-js-sdk'); -var MatrixEvent = jssdk.MatrixEvent; +const jssdk = require('matrix-js-sdk'); +const MatrixEvent = jssdk.MatrixEvent; const testUtils = require('../../../test-utils'); describe.only('MemberEventListSummary', function() { @@ -90,7 +90,7 @@ describe.only('MemberEventListSummary', function() { sandbox.restore(); }); - it('renders expanded events if there are less than props.threshold', function(done) { + it('renders expanded events if there are less than props.threshold', function() { const events = generateEvents([ {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, ]); @@ -110,10 +110,9 @@ describe.only('MemberEventListSummary', function() { expect(result.props.children).toEqual([
Expanded membership
, ]); - done(); }); - it('renders expanded events if there are less than props.threshold', function(done) { + it('renders expanded events if there are less than props.threshold', function() { const events = generateEvents([ {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, @@ -135,10 +134,9 @@ describe.only('MemberEventListSummary', function() {
Expanded membership
,
Expanded membership
, ]); - done(); }); - it('renders collapsed events if events.length = props.threshold', function(done) { + it('renders collapsed events if events.length = props.threshold', function() { const events = generateEvents([ {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, @@ -157,11 +155,9 @@ describe.only('MemberEventListSummary', function() { const summaryText = summary.innerText; expect(summaryText).toBe("user_1 joined and left and joined"); - - done(); }); - it('truncates long join,leave repetitions', function(done) { + it('truncates long join,leave repetitions', function() { const events = generateEvents([ {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, {userId : "@user_1:some.domain", prevMembership: "join", membership: "leave"}, @@ -191,11 +187,9 @@ describe.only('MemberEventListSummary', function() { const summaryText = summary.innerText; expect(summaryText).toBe("user_1 joined and left 7 times"); - - done(); }); - it('truncates long join,leave repetitions inbetween other events', function(done) { + it('truncates long join,leave repetitions inbetween other events', function() { const events = generateEvents([ {userId : "@user_1:some.domain", prevMembership: "ban", membership: "leave", senderId: "@some_other_user:some.domain"}, {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, @@ -227,11 +221,9 @@ describe.only('MemberEventListSummary', function() { const summaryText = summary.innerText; expect(summaryText).toBe("user_1 was unbanned, joined and left 7 times and was invited"); - - done(); }); - it('truncates multiple sequences of repetitions with other events inbetween', function(done) { + it('truncates multiple sequences of repetitions with other events inbetween', function() { const events = generateEvents([ {userId : "@user_1:some.domain", prevMembership: "ban", membership: "leave", senderId: "@some_other_user:some.domain"}, {userId : "@user_1:some.domain", prevMembership: "leave", membership: "join"}, @@ -260,11 +252,9 @@ describe.only('MemberEventListSummary', function() { const summaryText = summary.innerText; expect(summaryText).toBe("user_1 was unbanned, joined and left 2 times, was banned, joined and left 3 times and was invited"); - - done(); }); - it('handles multiple users following the same sequence of memberships', function(done) { + it('handles multiple users following the same sequence of memberships', function() { const events = generateEvents([ // user_1 {userId : "@user_1:some.domain", prevMembership: "ban", membership: "leave", senderId: "@some_other_user:some.domain"}, @@ -294,11 +284,9 @@ describe.only('MemberEventListSummary', function() { const summaryText = summary.innerText; expect(summaryText).toBe("user_1 and 1 other were unbanned, joined and left 2 times and were banned"); - - done(); }); - it('handles many users following the same sequence of memberships', function(done) { + it('handles many users following the same sequence of memberships', function() { const events = generateEventsForUsers("@user_$:some.domain", 20, [ {prevMembership: "ban", membership: "leave", senderId: "@some_other_user:some.domain"}, {prevMembership: "leave", membership: "join"}, @@ -320,11 +308,9 @@ describe.only('MemberEventListSummary', function() { const summaryText = summary.innerText; expect(summaryText).toBe("user_0 and 19 others were unbanned, joined and left 2 times and were banned"); - - done(); }); - it('correctly orders sequences of transitions by the order of their first event', function(done) { + it('correctly orders sequences of transitions by the order of their first event', function() { const events = generateEvents([ {userId : "@user_2:some.domain", prevMembership: "ban", membership: "leave", senderId: "@some_other_user:some.domain"}, {userId : "@user_1:some.domain", prevMembership: "ban", membership: "leave", senderId: "@some_other_user:some.domain"}, @@ -353,11 +339,9 @@ describe.only('MemberEventListSummary', function() { expect(summaryText).toBe( "user_2 was unbanned and joined and left 2 times, user_1 was unbanned, joined and left 2 times and was banned" ); - - done(); }); - it('correctly identifies transitions', function(done) { + it('correctly identifies transitions', function() { const events = generateEvents([ // invited {userId : "@user_1:some.domain", membership: "invite"}, @@ -393,11 +377,9 @@ describe.only('MemberEventListSummary', function() { expect(summaryText).toBe( "user_1 was invited, was banned, joined, rejected their invitation, left, had their invitation withdrawn, was unbanned, was kicked and left" ); - - done(); }); - it('handles invitation plurals correctly when there are multiple users', function(done) { + it('handles invitation plurals correctly when there are multiple users', function() { const events = generateEvents([ {userId : "@user_1:some.domain", prevMembership: "invite", membership: "leave"}, {userId : "@user_1:some.domain", prevMembership: "invite", membership: "leave", senderId: "@some_other_user:some.domain"}, @@ -419,11 +401,9 @@ describe.only('MemberEventListSummary', function() { expect(summaryText).toBe( "user_1 and 1 other rejected their invitations and had their invitations withdrawn" ); - - done(); }); - it('handles invitation plurals correctly when there are multiple invites', function(done) { + it('handles invitation plurals correctly when there are multiple invites', function() { const events = generateEvents([ {userId : "@user_1:some.domain", prevMembership: "invite", membership: "leave"}, {userId : "@user_1:some.domain", prevMembership: "invite", membership: "leave"}, @@ -443,11 +423,9 @@ describe.only('MemberEventListSummary', function() { expect(summaryText).toBe( "user_1 rejected their invitations 2 times" ); - - done(); }); - it('handles a summary length = 2, with no "others"', function(done) { + it('handles a summary length = 2, with no "others"', function() { const events = generateEvents([ {userId : "@user_1:some.domain", membership: "join"}, {userId : "@user_1:some.domain", membership: "join"}, @@ -469,11 +447,9 @@ describe.only('MemberEventListSummary', function() { expect(summaryText).toBe( "user_1 and user_2 joined 2 times" ); - - done(); }); - it('handles a summary length = 2, with 1 "other"', function(done) { + it('handles a summary length = 2, with 1 "other"', function() { const events = generateEvents([ {userId : "@user_1:some.domain", membership: "join"}, {userId : "@user_2:some.domain", membership: "join"}, @@ -494,11 +470,9 @@ describe.only('MemberEventListSummary', function() { expect(summaryText).toBe( "user_1, user_2 and 1 other joined" ); - - done(); }); - it('handles a summary length = 2, with many "others"', function(done) { + it('handles a summary length = 2, with many "others"', function() { const events = generateEventsForUsers("@user_$:some.domain", 20, [ {membership: "join"}, ]); @@ -517,7 +491,5 @@ describe.only('MemberEventListSummary', function() { expect(summaryText).toBe( "user_0, user_1 and 18 others joined" ); - - done(); }); }); \ No newline at end of file From 31df78f946ef64f73007c6183d17e1c4dc326448 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 18 Jan 2017 11:39:44 +0000 Subject: [PATCH 070/260] Use text-encoding-utf-8 as a TextEncoder polyfill Somebody else seems to have done a good job of polyfilling TextEncoder, so let's use that. --- package.json | 1 + .../views/dialogs/ExportE2eKeysDialog.js | 84 +++++++++++ src/utils/MegolmExportEncryption.js | 7 +- src/utils/TextDecoderPolyfill.js | 131 ------------------ src/utils/TextEncoderPolyfill.js | 78 ----------- test/utils/TextDecoderPolyfill-test.js | 85 ------------ test/utils/TextEncoderPolyfill-test.js | 39 ------ 7 files changed, 89 insertions(+), 336 deletions(-) create mode 100644 src/async-components/views/dialogs/ExportE2eKeysDialog.js delete mode 100644 src/utils/TextDecoderPolyfill.js delete mode 100644 src/utils/TextEncoderPolyfill.js delete mode 100644 test/utils/TextDecoderPolyfill-test.js delete mode 100644 test/utils/TextEncoderPolyfill-test.js diff --git a/package.json b/package.json index 1eaee39c41..e0cfb72148 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "react-dom": "^15.4.0", "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef", "sanitize-html": "^1.11.1", + "text-encoding-utf-8": "^1.0.1", "velocity-vector": "vector-im/velocity#059e3b2", "whatwg-fetch": "^1.0.0" }, diff --git a/src/async-components/views/dialogs/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/ExportE2eKeysDialog.js new file mode 100644 index 0000000000..284d299f4b --- /dev/null +++ b/src/async-components/views/dialogs/ExportE2eKeysDialog.js @@ -0,0 +1,84 @@ +/* +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; + +import sdk from '../../../index'; + +import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption'; + +export default React.createClass({ + displayName: 'ExportE2eKeysDialog', + + getInitialState: function() { + return { + collectedPassword: false, + }; + }, + + _onPassphraseFormSubmit: function(ev) { + ev.preventDefault(); + console.log(this.refs.passphrase1.value); + return false; + }, + + render: function() { + let content; + if (!this.state.collectedPassword) { + content = ( +
+

+ This process will allow you to export the keys for messages + you have received in encrypted rooms to a local file. You + will then be able to import the file into another Matrix + client in the future, so that client will also be able to + decrypt these messages. +

+

+ The exported file will allow anyone who can read it to decrypt + any encrypted messages that you can see, so you should be + careful to keep it secure. To help with this, you should enter + a passphrase below, which will be used to encrypt the exported + data. It will only be possible to import the data by using the + same passphrase. +

+
+
+ +
+
+ +
+
+ +
+
+
+ ); + } + + return ( +
+
+ Export room keys +
+ {content} +
+ ); + }, +}); diff --git a/src/utils/MegolmExportEncryption.js b/src/utils/MegolmExportEncryption.js index 351f58aaa6..e3ca7e68f2 100644 --- a/src/utils/MegolmExportEncryption.js +++ b/src/utils/MegolmExportEncryption.js @@ -17,13 +17,14 @@ limitations under the License. "use strict"; // polyfill textencoder if necessary +import * as TextEncodingUtf8 from 'text-encoding-utf-8'; let TextEncoder = window.TextEncoder; if (!TextEncoder) { - TextEncoder = require('./TextEncoderPolyfill'); + TextEncoder = TextEncodingUtf8.TextEncoder; } let TextDecoder = window.TextDecoder; -if (TextDecoder) { - TextDecoder = require('./TextDecoderPolyfill'); +if (!TextDecoder) { + TextDecoder = TextEncodingUtf8.TextDecoder; } const subtleCrypto = window.crypto.subtle || window.crypto.webkitSubtle; diff --git a/src/utils/TextDecoderPolyfill.js b/src/utils/TextDecoderPolyfill.js deleted file mode 100644 index e203676bb7..0000000000 --- a/src/utils/TextDecoderPolyfill.js +++ /dev/null @@ -1,131 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -"use strict"; - -// Polyfill for TextDecoder. - -const REPLACEMENT_CHAR = '\uFFFD'; - -export default class TextDecoder { - /** - * Decode a UTF-8 byte array as a javascript string - * - * @param {Uint8Array} u8Array UTF-8-encoded onput - * @return {str} - */ - decode(u8Array) { - let u0, u1, u2, u3; - - let str = ''; - let idx = 0; - while (idx < u8Array.length) { - u0 = u8Array[idx++]; - if (!(u0 & 0x80)) { - str += String.fromCharCode(u0); - continue; - } - - if ((u0 & 0xC0) != 0xC0) { - // continuation byte where we expect a leading byte - str += REPLACEMENT_CHAR; - continue; - } - - if (u0 > 0xF4) { - // this would imply a 5-byte or longer encoding, which is - // invalid and unsupported here. - str += REPLACEMENT_CHAR; - continue; - } - - u1 = u8Array[idx++]; - if (u1 === undefined) { - str += REPLACEMENT_CHAR; - continue; - } - - if ((u1 & 0xC0) != 0x80) { - // leading byte where we expect a continuation byte - str += REPLACEMENT_CHAR.repeat(2); - continue; - } - u1 &= 0x3F; - if (!(u0 & 0x20)) { - const u = ((u0 & 0x1F) << 6) | u1; - if (u < 0x80) { - // over-long - str += REPLACEMENT_CHAR.repeat(2); - } else { - str += String.fromCharCode(u); - } - continue; - } - - u2 = u8Array[idx++]; - if (u2 === undefined) { - str += REPLACEMENT_CHAR.repeat(2); - continue; - } - if ((u2 & 0xC0) != 0x80) { - // leading byte where we expect a continuation byte - str += REPLACEMENT_CHAR.repeat(3); - continue; - } - u2 &= 0x3F; - if (!(u0 & 0x10)) { - const u = ((u0 & 0x0F) << 12) | (u1 << 6) | u2; - if (u < 0x800) { - // over-long - str += REPLACEMENT_CHAR.repeat(3); - } else if (u == 0xFEFF && idx == 3) { - // byte-order mark: do not add to output - } else { - str += String.fromCharCode(u); - } - continue; - } - - u3 = u8Array[idx++]; - if (u3 === undefined) { - str += REPLACEMENT_CHAR.repeat(3); - continue; - } - if ((u3 & 0xC0) != 0x80) { - // leading byte where we expect a continuation byte - str += REPLACEMENT_CHAR.repeat(4); - continue; - } - u3 &= 0x3F; - const u = ((u0 & 7) << 18) | (u1 << 12) | (u2 << 6) | u3; - if (u < 0x10000) { - // over-long - str += REPLACEMENT_CHAR.repeat(4); - continue; - } - if (u > 0x1FFFF) { - // unicode stops here. - str += REPLACEMENT_CHAR.repeat(4); - continue; - } - - // encode as utf-16 - const v = u - 0x10000; - str += String.fromCharCode(0xD800 | (v >> 10), 0xDC00 | (v & 0x3FF)); - } - return str; - } -} diff --git a/src/utils/TextEncoderPolyfill.js b/src/utils/TextEncoderPolyfill.js deleted file mode 100644 index 41ee4782a9..0000000000 --- a/src/utils/TextEncoderPolyfill.js +++ /dev/null @@ -1,78 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -"use strict"; - -// Polyfill for TextEncoder. Based on emscripten's stringToUTF8Array. - -function utf8len(str) { - var len = 0; - for (var i = 0; i < str.length; ++i) { - var u = str.charCodeAt(i); - if (u >= 0xD800 && u <= 0xDFFF && i < str.length-1) { - // lead surrogate - combine with next surrogate - u = 0x10000 + ((u & 0x3FF) << 10) | (str.charCodeAt(++i) & 0x3FF); - } - - if (u <= 0x7F) { - ++len; - } else if (u <= 0x7FF) { - len += 2; - } else if (u <= 0xFFFF) { - len += 3; - } else { - len += 4; - } - } - return len; -} - -export default class TextEncoder { - /** - * Encode a javascript string as utf-8 - * - * @param {String} str String to encode - * @return {Uint8Array} UTF-8-encoded output - */ - encode(str) { - const outU8Array = new Uint8Array(utf8len(str)); - var outIdx = 0; - for (var i = 0; i < str.length; ++i) { - var u = str.charCodeAt(i); - if (u >= 0xD800 && u <= 0xDFFF && i < str.length-1) { - // lead surrogate - combine with next surrogate - u = 0x10000 + ((u & 0x3FF) << 10) | (str.charCodeAt(++i) & 0x3FF); - } - - if (u <= 0x7F) { - outU8Array[outIdx++] = u; - } else if (u <= 0x7FF) { - outU8Array[outIdx++] = 0xC0 | (u >> 6); - outU8Array[outIdx++] = 0x80 | (u & 0x3F); - } else if (u <= 0xFFFF) { - outU8Array[outIdx++] = 0xE0 | (u >> 12); - outU8Array[outIdx++] = 0x80 | ((u >> 6) & 0x3F); - outU8Array[outIdx++] = 0x80 | (u & 0x3F); - } else { - outU8Array[outIdx++] = 0xF0 | (u >> 18); - outU8Array[outIdx++] = 0x80 | ((u >> 12) & 0x3F); - outU8Array[outIdx++] = 0x80 | ((u >> 6) & 0x3F); - outU8Array[outIdx++] = 0x80 | (u & 0x3F); - } - } - return outU8Array; - } -} diff --git a/test/utils/TextDecoderPolyfill-test.js b/test/utils/TextDecoderPolyfill-test.js deleted file mode 100644 index 84f5edf187..0000000000 --- a/test/utils/TextDecoderPolyfill-test.js +++ /dev/null @@ -1,85 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -"use strict"; - -import TextDecoderPolyfill from 'utils/TextDecoderPolyfill'; - -import * as testUtils from '../test-utils'; -import expect from 'expect'; - -describe('textDecoderPolyfill', function() { - beforeEach(function() { - testUtils.beforeEach(this); - }); - - it('should correctly decode a range of strings', function() { - const decoder = new TextDecoderPolyfill(); - - expect(decoder.decode(Uint8Array.of(65, 66, 67))).toEqual('ABC'); - expect(decoder.decode(Uint8Array.of(0xC3, 0xA6))).toEqual('æ'); - expect(decoder.decode(Uint8Array.of(0xE2, 0x82, 0xAC))).toEqual('€'); - expect(decoder.decode(Uint8Array.of(0xF0, 0x9F, 0x92, 0xA9))).toEqual('\uD83D\uDCA9'); - }); - - it('should ignore byte-order marks', function() { - const decoder = new TextDecoderPolyfill(); - expect(decoder.decode(Uint8Array.of(0xEF, 0xBB, 0xBF, 65))) - .toEqual('A'); - }); - - it('should not ignore byte-order marks in the middle of the array', function() { - const decoder = new TextDecoderPolyfill(); - expect(decoder.decode(Uint8Array.of(65, 0xEF, 0xBB, 0xBF, 66))) - .toEqual('A\uFEFFB'); - }); - - it('should reject overlong encodings', function() { - const decoder = new TextDecoderPolyfill(); - - // euro, as 4 bytes - expect(decoder.decode(Uint8Array.of(65, 0xF0, 0x82, 0x82, 0xAC, 67))) - .toEqual('A\uFFFD\uFFFD\uFFFD\uFFFDC'); - }); - - it('should reject 5 and 6-byte encodings', function() { - const decoder = new TextDecoderPolyfill(); - - expect(decoder.decode(Uint8Array.of(65, 0xF8, 0x82, 0x82, 0x82, 0x82, 67))) - .toEqual('A\uFFFD\uFFFD\uFFFD\uFFFD\uFFFDC'); - }); - - it('should reject code points beyond 0x10000', function() { - const decoder = new TextDecoderPolyfill(); - - expect(decoder.decode(Uint8Array.of(0xF4, 0xA0, 0x80, 0x80))) - .toEqual('\uFFFD\uFFFD\uFFFD\uFFFD'); - }); - - it('should cope with end-of-string', function() { - const decoder = new TextDecoderPolyfill(); - - expect(decoder.decode(Uint8Array.of(65, 0xC3))) - .toEqual('A\uFFFD'); - - expect(decoder.decode(Uint8Array.of(65, 0xE2, 0x82))) - .toEqual('A\uFFFD\uFFFD'); - - expect(decoder.decode(Uint8Array.of(65, 0xF0, 0x9F, 0x92))) - .toEqual('A\uFFFD\uFFFD\uFFFD'); - }); - -}); diff --git a/test/utils/TextEncoderPolyfill-test.js b/test/utils/TextEncoderPolyfill-test.js deleted file mode 100644 index 4f422ec375..0000000000 --- a/test/utils/TextEncoderPolyfill-test.js +++ /dev/null @@ -1,39 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -"use strict"; - -import TextEncoderPolyfill from 'utils/TextEncoderPolyfill'; - -import * as testUtils from '../test-utils'; -import expect from 'expect'; - -describe('textEncoderPolyfill', function() { - beforeEach(function() { - testUtils.beforeEach(this); - }); - - it('should correctly encode a range of strings', function() { - const encoder = new TextEncoderPolyfill(); - - expect(encoder.encode('ABC')).toEqual(Uint8Array.of(65, 66, 67)); - expect(encoder.encode('æ')).toEqual(Uint8Array.of(0xC3, 0xA6)); - expect(encoder.encode('€')).toEqual(Uint8Array.of(0xE2, 0x82, 0xAC)); - - // PILE OF POO (💩) - expect(encoder.encode('\uD83D\uDCA9')).toEqual(Uint8Array.of(0xF0, 0x9F, 0x92, 0xA9)); - }); -}); From 5ef5204c8c974b8ada599e573515031e5dd2fc47 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 18 Jan 2017 12:48:28 +0100 Subject: [PATCH 071/260] Implement simple team-based registration (#620) * Implement simple team-based registration Config required goes in the `teams` top-level property in config.json. This consists of an array of team objects: ```json { "name": "University of Bath", "emailSuffix": "bath.ac.uk" } ``` These can be selected on registration and require a user to have a certain email address in order to register as part of a team. This is for vector-im/riot-web#2940. The next step would be sending users with emails matching the emailSuffix of a team to the correct welcome page as in vector-im/riot-web#2430. --- src/components/structures/MatrixChat.js | 1 + .../structures/login/Registration.js | 11 ++ .../views/login/RegistrationForm.js | 108 ++++++++++++++++-- 3 files changed, 108 insertions(+), 12 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index c47109db94..4d98e3f09e 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1003,6 +1003,7 @@ module.exports = React.createClass({ defaultHsUrl={this.getDefaultHsUrl()} defaultIsUrl={this.getDefaultIsUrl()} brand={this.props.config.brand} + teamsConfig={this.props.config.teamsConfig} customHsUrl={this.getCurrentHsUrl()} customIsUrl={this.getCurrentIsUrl()} registrationUrl={this.props.registrationUrl} diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 269aabed9b..fb24b61504 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -49,6 +49,16 @@ module.exports = React.createClass({ email: React.PropTypes.string, username: React.PropTypes.string, guestAccessToken: React.PropTypes.string, + teamsConfig: React.PropTypes.shape({ + // Email address to request new teams + supportEmail: React.PropTypes.string, + teams: React.PropTypes.arrayOf(React.PropTypes.shape({ + // The displayed name of the team + "name": React.PropTypes.string, + // The suffix with which every team email address ends + "emailSuffix": React.PropTypes.string, + })).required, + }), defaultDeviceDisplayName: React.PropTypes.string, @@ -254,6 +264,7 @@ module.exports = React.createClass({ defaultUsername={this.state.formVals.username} defaultEmail={this.state.formVals.email} defaultPassword={this.state.formVals.password} + teamsConfig={this.props.teamsConfig} guestUsername={this.props.username} minPasswordLength={MIN_PASSWORD_LENGTH} onError={this.onFormValidationFailed} diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index 33809fbfd6..3e07302a91 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -38,6 +38,16 @@ module.exports = React.createClass({ defaultEmail: React.PropTypes.string, defaultUsername: React.PropTypes.string, defaultPassword: React.PropTypes.string, + teamsConfig: React.PropTypes.shape({ + // Email address to request new teams + supportEmail: React.PropTypes.string, + teams: React.PropTypes.arrayOf(React.PropTypes.shape({ + // The displayed name of the team + "name": React.PropTypes.string, + // The suffix with which every team email address ends + "emailSuffix": React.PropTypes.string, + })).required, + }), // A username that will be used if no username is entered. // Specifying this param will also warn the user that entering @@ -62,7 +72,8 @@ module.exports = React.createClass({ getInitialState: function() { return { - fieldValid: {} + fieldValid: {}, + selectedTeam: null, }; }, @@ -119,6 +130,25 @@ module.exports = React.createClass({ } }, + onSelectTeam: function(teamIndex) { + let team = this._getSelectedTeam(teamIndex); + if (team) { + this.refs.email.value = this.refs.email.value.split("@")[0]; + } + this.setState({ + selectedTeam: team, + showSupportEmail: teamIndex === "other", + }); + }, + + _getSelectedTeam: function(teamIndex) { + if (this.props.teamsConfig && + this.props.teamsConfig.teams[teamIndex]) { + return this.props.teamsConfig.teams[teamIndex]; + } + return null; + }, + /** * Returns true if all fields were valid last time * they were validated. @@ -139,11 +169,15 @@ module.exports = React.createClass({ switch (field_id) { case FIELD_EMAIL: - this.markFieldValid( - field_id, - this.refs.email.value == '' || Email.looksValid(this.refs.email.value), - "RegistrationForm.ERR_EMAIL_INVALID" - ); + let email = this.refs.email.value; + if (this.props.teamsConfig) { + let team = this.state.selectedTeam; + if (team) { + email = email + "@" + team.emailSuffix; + } + } + let valid = email === '' || Email.looksValid(email); + this.markFieldValid(field_id, valid, "RegistrationForm.ERR_EMAIL_INVALID"); break; case FIELD_USERNAME: // XXX: SPEC-1 @@ -222,17 +256,64 @@ module.exports = React.createClass({ return cls; }, + _renderEmailInputSuffix: function() { + let suffix = null; + if (!this.state.selectedTeam) { + return suffix; + } + let team = this.state.selectedTeam; + if (team) { + suffix = "@" + team.emailSuffix; + } + return suffix; + }, + render: function() { var self = this; - var emailSection, registerButton; + var emailSection, teamSection, teamAdditionSupport, registerButton; if (this.props.showEmail) { + let emailSuffix = this._renderEmailInputSuffix(); emailSection = ( - +
+ + {emailSuffix ? : null } +
); + if (this.props.teamsConfig) { + teamSection = ( + + ); + if (this.props.teamsConfig.supportEmail && this.state.showSupportEmail) { + teamAdditionSupport = ( + + If your team is not listed, email  +
+ {this.props.teamsConfig.supportEmail} + + + ); + } + } } if (this.props.onRegisterClick) { registerButton = ( @@ -248,6 +329,9 @@ module.exports = React.createClass({ return (
+ {teamSection} + {teamAdditionSupport} +
{emailSection}
Date: Wed, 18 Jan 2017 14:25:11 +0100 Subject: [PATCH 072/260] Markdown: Add comment about out function override MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johannes Löthberg --- src/Markdown.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Markdown.js b/src/Markdown.js index e6f5f59f01..35ae42f770 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -78,6 +78,9 @@ export default class Markdown { } } } else { + // The default `out` function only sends the input through an XML + // escaping function, which causes messages to be entity encoded, + // which we don't want in this case. renderer.out = function(s) { this.lit(s); } From 3d30553b7fa21f5884abd26c27212626f38d2ce2 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 18 Jan 2017 14:06:47 +0000 Subject: [PATCH 073/260] review fixes, plus unbreak to work with new webpack layout --- src/Tinter.js | 26 ++- src/components/structures/MatrixChat.js | 9 +- src/components/structures/UserSettings.js | 184 ++++++++++++---------- 3 files changed, 123 insertions(+), 96 deletions(-) diff --git a/src/Tinter.js b/src/Tinter.js index c18d3068a7..4a5e4e453c 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -174,7 +174,7 @@ module.exports = { tintables.push(tintable); }, - tint: function(primaryColor, secondaryColor, tertiaryColor, whiteColor) { + tint: function(primaryColor, secondaryColor, tertiaryColor) { if (!cached) { calcCssFixups(); @@ -205,19 +205,16 @@ module.exports = { tertiaryColor = rgbToHex(rgb1); } - if (!whiteColor) { - whiteColor = colors[3]; - } - if (colors[0] === primaryColor && colors[1] === secondaryColor && - colors[2] === tertiaryColor && - colors[3] === whiteColor) + colors[2] === tertiaryColor) { return; } - colors = [primaryColor, secondaryColor, tertiaryColor, whiteColor]; + colors[0] = primaryColor; + colors[1] = secondaryColor; + colors[2] = tertiaryColor; if (DEBUG) console.log("Tinter.tint"); @@ -231,6 +228,19 @@ module.exports = { }); }, + tintSvgWhite: function(whiteColor) { + if (!whiteColor) { + whiteColor = colors[3]; + } + if (colors[3] === whiteColor) { + return; + } + colors[3] = whiteColor; + tintables.forEach(function(tintable) { + tintable(); + }); + }, + // XXX: we could just move this all into TintableSvg, but as it's so similar // to the CSS fixup stuff in Tinter (just that the fixups are stored in TintableSvg) // keeping it here for now. diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 7d74e2ee02..8917f0535e 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -608,8 +608,9 @@ module.exports = React.createClass({ var i, a; for (i = 0; (a = document.getElementsByTagName("link")[i]); i++) { var href = a.getAttribute("href"); - if (href.startsWith("theme-")) { - if (href.startsWith("theme-" + theme + ".")) { + var match = href.match(/^bundles\/.*\/theme-(.*)\.css$/); + if (match) { + if (match[1] === theme) { a.disabled = false; } else { @@ -621,10 +622,10 @@ module.exports = React.createClass({ if (theme === 'dark') { // abuse the tinter to change all the SVG's #fff to #2d2d2d // XXX: obviously this shouldn't be hardcoded here. - Tinter.tint(undefined, undefined, undefined, '#2d2d2d'); + Tinter.tintSvgWhite('#2d2d2d'); } else { - Tinter.tint(undefined, undefined, undefined, '#ffffff'); + Tinter.tintSvgWhite('#ffffff'); } }, diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index ebfd6dc0a7..a120d365d1 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -32,6 +32,47 @@ var AddThreepid = require('../../AddThreepid'); const REACT_SDK_VERSION = 'dist' in package_json ? package_json.version : package_json.gitHead || ""; + +// Enumerate some simple 'flip a bit' UI settings (if any) +const SETTINGS_LABELS = [ +/* + { + 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', + }, +*/ +]; + +// Enumerate the available themes, with a nice human text label. +// XXX: Ideally we would have a theme manifest or something and they'd be nicely +// packaged up in a single directory, and/or located at the application layer. +// But for now for expedience we just hardcode them here. +const THEMES = [ + { + id: 'theme', + label: 'Light theme', + value: 'light', + }, + { + id: 'theme', + label: 'Dark theme', + value: 'dark', + } +]; + + module.exports = React.createClass({ displayName: 'UserSettings', @@ -93,6 +134,12 @@ module.exports = React.createClass({ middleOpacity: 0.3, }); this._refreshFromServer(); + + var syncedSettings = UserSettingsStore.getSyncedSettings(); + if (!syncedSettings.theme) { + syncedSettings.theme = 'light'; + } + this._syncedSettings = syncedSettings; }, componentDidMount: function() { @@ -342,99 +389,68 @@ 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 themes = [ - { - id: 'theme', - label: 'Light theme', - value: 'light', - }, - { - id: 'theme', - label: 'Dark theme', - value: 'dark', - } - ]; - - var syncedSettings = UserSettingsStore.getSyncedSettings(); - if (!syncedSettings.theme) { - syncedSettings.theme = 'light'; - } - return (

User Interface

-
- UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) } - /> - -
- { settingsLabels.map( setting => { - return
- UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) } - /> - -
- })} - { themes.map( setting => { - return
- { - if (e.target.checked) { - UserSettingsStore.setSyncedSetting(setting.id, setting.value) - } - dis.dispatch({ - action: 'set_theme', - value: setting.value, - }); - } - } - /> - -
- })} + { this._renderUrlPreviewSelector() } + { SETTINGS_LABELS.map( this._renderSyncedSetting ) } + { THEMES.map( this._renderThemeSelector ) }
); }, + _renderUrlPreviewSelector: function() { + return
+ UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) } + /> + +
+ }, + + _renderSyncedSetting: function(setting) { + return
+ UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) } + /> + +
+ }, + + _renderThemeSelector: function(setting) { + return
+ { + if (e.target.checked) { + UserSettingsStore.setSyncedSetting(setting.id, setting.value) + } + dis.dispatch({ + action: 'set_theme', + value: setting.value, + }); + } + } + /> + +
+ }, + _renderCryptoInfo: function() { const client = MatrixClientPeg.get(); const deviceId = client.deviceId; From de621902fc3acc6075d48b57e8123753ac26422a Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 18 Jan 2017 15:21:50 +0000 Subject: [PATCH 074/260] Better feedback in invite dialog Show feedback if you enter a valid but unknown email address or mxid Fixes https://github.com/vector-im/riot-web/issues/2933 --- src/Invite.js | 7 +++++-- .../views/dialogs/ChatInviteDialog.js | 18 +++++++++++++++--- .../views/elements/AddressSelector.js | 4 +++- src/components/views/elements/AddressTile.js | 4 +++- 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/Invite.js b/src/Invite.js index 6422812734..6cb04b1b19 100644 --- a/src/Invite.js +++ b/src/Invite.js @@ -19,9 +19,12 @@ import MultiInviter from './utils/MultiInviter'; const emailRegex = /^\S+@\S+\.\S+$/; +// We allow localhost for mxids to avoid confusion +const mxidRegex = /^@\S+:(?:\S+\.\S+|localhost)$/ + export function getAddressType(inputText) { - const isEmailAddress = /^\S+@\S+\.\S+$/.test(inputText); - const isMatrixId = inputText[0] === '@' && inputText.indexOf(":") > 0; + const isEmailAddress = emailRegex.test(inputText); + const isMatrixId = mxidRegex.test(inputText); // sanity check the input for user IDs if (isEmailAddress) { diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index e9a041357f..f6d7c17898 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -154,14 +154,26 @@ module.exports = React.createClass({ }, onQueryChanged: function(ev) { - var query = ev.target.value; - var queryList = []; + const query = ev.target.value; + let queryList = []; // Only do search if there is something to search - if (query.length > 0) { + if (query.length > 0 && query != '@') { + // filter the known users list queryList = this._userList.filter((user) => { return this._matches(query, user); + }).map((user) => { + return user.userId; }); + + // If the query isn't a user we know about, but is a + // valid address, add an entry for that + if (queryList.length == 0) { + const addrType = Invite.getAddressType(query); + if (addrType !== null) { + queryList.push(query); + } + } } this.setState({ diff --git a/src/components/views/elements/AddressSelector.js b/src/components/views/elements/AddressSelector.js index 2c2d7e2d61..853b8db144 100644 --- a/src/components/views/elements/AddressSelector.js +++ b/src/components/views/elements/AddressSelector.js @@ -25,6 +25,8 @@ module.exports = React.createClass({ propTypes: { onSelected: React.PropTypes.func.isRequired, + + // List of strings: the addresses to display addressList: React.PropTypes.array.isRequired, truncateAt: React.PropTypes.number.isRequired, selected: React.PropTypes.number, @@ -125,7 +127,7 @@ module.exports = React.createClass({ // method, how far to scroll when using the arrow keys addressList.push(
{ this.addressListElement = ref; }} > - +
); } diff --git a/src/components/views/elements/AddressTile.js b/src/components/views/elements/AddressTile.js index 2799f10a41..b49a84cedd 100644 --- a/src/components/views/elements/AddressTile.js +++ b/src/components/views/elements/AddressTile.js @@ -71,6 +71,8 @@ module.exports = React.createClass({ imgUrl = "img/avatar-error.svg"; } + // Removing networks for now as they're not really supported + /* var network; if (this.props.networkUrl !== "") { network = ( @@ -79,6 +81,7 @@ module.exports = React.createClass({
); } + */ var info; var error = false; @@ -145,7 +148,6 @@ module.exports = React.createClass({ return (
- { network }
From fc630672378bb292dcde1188255deb830a486596 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 18 Jan 2017 16:36:27 +0000 Subject: [PATCH 075/260] improve commentary --- src/components/structures/UserSettings.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index a120d365d1..5ce9ab1a15 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -33,7 +33,9 @@ const REACT_SDK_VERSION = 'dist' in package_json ? package_json.version : package_json.gitHead || ""; -// Enumerate some simple 'flip a bit' UI settings (if any) +// Enumerate some simple 'flip a bit' UI settings (if any). +// 'id' gives the key name in the im.vector.web.settings account data event +// 'label' is how we describe it in the UI. const SETTINGS_LABELS = [ /* { @@ -56,6 +58,10 @@ const SETTINGS_LABELS = [ ]; // Enumerate the available themes, with a nice human text label. +// 'id' gives the key name in the im.vector.web.settings account data event +// 'value' is the value for that key in the event +// 'label' is how we describe it in the UI. +// // XXX: Ideally we would have a theme manifest or something and they'd be nicely // packaged up in a single directory, and/or located at the application layer. // But for now for expedience we just hardcode them here. From f105ec279451cf8866fc1a202a546458fb5cde50 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 18 Jan 2017 17:51:39 +0000 Subject: [PATCH 076/260] Attempt to sanitize ChatInviteDialog a bit * Use binds rather than onFoo functions which aren't actually handler functions themselves but return them * Rename onKeyUp to moveSelectionDown etc,, reserving onKeyUp for "a key has been released" rather than, "the up arrow key has been pressed" --- .../views/dialogs/ChatInviteDialog.js | 6 ++--- .../views/elements/AddressSelector.js | 24 +++++++------------ 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index e9a041357f..9ca3ff635d 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -119,15 +119,15 @@ module.exports = React.createClass({ } else if (e.keyCode === 38) { // up arrow e.stopPropagation(); e.preventDefault(); - this.addressSelector.onKeyUp(); + this.addressSelector.moveSelectionUp(); } else if (e.keyCode === 40) { // down arrow e.stopPropagation(); e.preventDefault(); - this.addressSelector.onKeyDown(); + this.addressSelector.moveSelectionDown(); } else if (this.state.queryList.length > 0 && (e.keyCode === 188, e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab e.stopPropagation(); e.preventDefault(); - this.addressSelector.onKeySelect(); + this.addressSelector.chooseSelection(); } else if (this.refs.textinput.value.length === 0 && this.state.inviteList.length && e.keyCode === 8) { // backspace e.stopPropagation(); e.preventDefault(); diff --git a/src/components/views/elements/AddressSelector.js b/src/components/views/elements/AddressSelector.js index 2c2d7e2d61..c477b8e7eb 100644 --- a/src/components/views/elements/AddressSelector.js +++ b/src/components/views/elements/AddressSelector.js @@ -55,7 +55,7 @@ module.exports = React.createClass({ } }, - onKeyUp: function() { + moveSelectionUp: function() { if (this.state.selected > 0) { this.setState({ selected: this.state.selected - 1, @@ -64,7 +64,7 @@ module.exports = React.createClass({ } }, - onKeyDown: function() { + moveSelectionDown: function() { if (this.state.selected < this._maxSelected(this.props.addressList)) { this.setState({ selected: this.state.selected + 1, @@ -73,25 +73,19 @@ module.exports = React.createClass({ } }, - onKeySelect: function() { + chooseSelection: function() { this.selectAddress(this.state.selected); }, onClick: function(index) { - var self = this; - return function() { - self.selectAddress(index); - }; + this.selectAddress(index); }, onMouseEnter: function(index) { - var self = this; - return function() { - self.setState({ - selected: index, - hover: true, - }); - }; + this.setState({ + selected: index, + hover: true, + }); }, onMouseLeave: function() { @@ -124,7 +118,7 @@ module.exports = React.createClass({ // Saving the addressListElement so we can use it to work out, in the componentDidUpdate // method, how far to scroll when using the arrow keys addressList.push( -
{ this.addressListElement = ref; }} > +
{ this.addressListElement = ref; }} >
); From 30bd01cdf2a2548cf536c7edc72a8950d1238175 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20L=C3=B6thberg?= Date: Wed, 18 Jan 2017 19:29:11 +0100 Subject: [PATCH 077/260] Markdown: Split up render function into toHTML/toPlaintext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johannes Löthberg --- src/Markdown.js | 98 +++++++++++-------- .../views/rooms/MessageComposerInput.js | 6 +- .../views/rooms/MessageComposerInputOld.js | 4 +- 3 files changed, 61 insertions(+), 47 deletions(-) diff --git a/src/Markdown.js b/src/Markdown.js index 35ae42f770..80d1aa4335 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -23,7 +23,9 @@ import commonmark from 'commonmark'; */ export default class Markdown { constructor(input) { - this.input = input + this.input = input; + this.parser = new commonmark.Parser(); + this.renderer = new commonmark.HtmlRenderer({safe: false}); } isPlainText() { @@ -57,54 +59,66 @@ export default class Markdown { return is_plain; } - render(html) { - const parser = new commonmark.Parser(); + toHTML(html) { + const real_paragraph = this.renderer.paragraph; - const renderer = new commonmark.HtmlRenderer({safe: true}); - const real_paragraph = renderer.paragraph; - if (html) { - renderer.paragraph = function(node, entering) { - // If there is only one top level node, just return the - // bare text: it's a single line of text and so should be - // 'inline', rather than unnecessarily wrapped in its own - // p tag. If, however, we have multiple nodes, each gets - // its own p tag to keep them as separate paragraphs. - var par = node; - while (par.parent) { - par = par.parent - } - if (par.firstChild != par.lastChild) { - real_paragraph.call(this, node, entering); - } + this.renderer.paragraph = function(node, entering) { + // If there is only one top level node, just return the + // bare text: it's a single line of text and so should be + // 'inline', rather than unnecessarily wrapped in its own + // p tag. If, however, we have multiple nodes, each gets + // its own p tag to keep them as separate paragraphs. + var par = node; + while (par.parent) { + par = par.parent } - } else { - // The default `out` function only sends the input through an XML - // escaping function, which causes messages to be entity encoded, - // which we don't want in this case. - renderer.out = function(s) { - this.lit(s); + if (par.firstChild != par.lastChild) { + real_paragraph.call(this, node, entering); } + } - renderer.paragraph = function(node, entering) { - // If there is only one top level node, just return the - // bare text: it's a single line of text and so should be - // 'inline', rather than unnecessarily wrapped in its own - // p tag. If, however, we have multiple nodes, each gets - // its own p tag to keep them as separate paragraphs. - var par = node; - while (par.parent) { - node = par; - par = par.parent; - } - if (node != par.lastChild) { - if (!entering) { - this.lit('\n\n'); - } + var parsed = this.parser.parse(this.input); + var rendered = this.renderer.render(parsed); + + this.renderer.paragraph = real_paragraph; + + return rendered; + } + + toPlaintext() { + const real_paragraph = this.renderer.paragraph; + + // The default `out` function only sends the input through an XML + // escaping function, which causes messages to be entity encoded, + // which we don't want in this case. + this.renderer.out = function(s) { + // The `lit` function adds a string literal to the output buffer. + this.lit(s); + } + + this.renderer.paragraph = function(node, entering) { + // If there is only one top level node, just return the + // bare text: it's a single line of text and so should be + // 'inline', rather than unnecessarily wrapped in its own + // p tag. If, however, we have multiple nodes, each gets + // its own p tag to keep them as separate paragraphs. + var par = node; + while (par.parent) { + node = par; + par = par.parent; + } + if (node != par.lastChild) { + if (!entering) { + this.lit('\n\n'); } } } - var parsed = parser.parse(this.input); - return renderer.render(parsed); + var parsed = this.parser.parse(this.input); + var rendered = this.renderer.render(parsed); + + this.renderer.paragraph = real_paragraph; + + return rendered; } } diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 5e8df592da..41b27c1394 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -401,7 +401,7 @@ export default class MessageComposerInput extends React.Component { let contentState = null; if (enabled) { const md = new Markdown(this.state.editorState.getCurrentContent().getPlainText()); - contentState = RichText.HTMLtoContentState(md.render(true)); + contentState = RichText.HTMLtoContentState(md.toHTML()); } else { let markdown = stateToMarkdown(this.state.editorState.getCurrentContent()); if (markdown[markdown.length - 1] === '\n') { @@ -524,9 +524,9 @@ export default class MessageComposerInput extends React.Component { } else { const md = new Markdown(contentText); if (md.isPlainText()) { - contentText = md.render(false); + contentText = md.toPlaintext(); } else { - contentHTML = md.render(true); + contentHTML = md.toHTML(true); } } diff --git a/src/components/views/rooms/MessageComposerInputOld.js b/src/components/views/rooms/MessageComposerInputOld.js index 3b0100278b..91abd5a2a8 100644 --- a/src/components/views/rooms/MessageComposerInputOld.js +++ b/src/components/views/rooms/MessageComposerInputOld.js @@ -325,13 +325,13 @@ module.exports = React.createClass({ } if (send_markdown) { - const htmlText = mdown.render(true); + const htmlText = mdown.toHTML(); sendMessagePromise = isEmote ? MatrixClientPeg.get().sendHtmlEmote(this.props.room.roomId, contentText, htmlText) : MatrixClientPeg.get().sendHtmlMessage(this.props.room.roomId, contentText, htmlText); } else { - const contentText = mdown.render(false); + const contentText = mdown.toPlaintext(false); sendMessagePromise = isEmote ? MatrixClientPeg.get().sendEmoteMessage(this.props.room.roomId, contentText) : MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText); From 7b7728c93abec435c44ae66f218fa23ee23e35ef Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 18 Jan 2017 18:32:38 +0000 Subject: [PATCH 078/260] Make behaviour of ChatInviteDialog more consistent * Pressing enter now always adds whatever was in the input box to the invite list, if it's a valid address (previously it added it to the list of it was a search result but submitted the form straight away if there were no results). * Remove isValidAddress as it was only used in the context of testing whether its return value was true or null (where null meant "unsure") so just use getAddressType instead. --- src/Invite.js | 22 ----------------- .../views/dialogs/ChatInviteDialog.js | 24 +++++++++++++++---- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/src/Invite.js b/src/Invite.js index 6422812734..6bfc646977 100644 --- a/src/Invite.js +++ b/src/Invite.js @@ -59,25 +59,3 @@ export function inviteMultipleToRoom(roomId, addrs) { return this.inviter.invite(addrs); } -/** - * Checks is the supplied address is valid - * - * @param {addr} The mx userId or email address to check - * @returns true, false, or null for unsure - */ -export function isValidAddress(addr) { - // Check if the addr is a valid type - var addrType = this.getAddressType(addr); - if (addrType === "mx") { - let user = MatrixClientPeg.get().getUser(addr); - if (user) { - return true; - } else { - return null; - } - } else if (addrType === "email") { - return true; - } else { - return false; - } -} diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index 9ca3ff635d..767620d93f 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -74,8 +74,8 @@ module.exports = React.createClass({ var inviteList = this.state.inviteList.slice(); // Check the text input field to see if user has an unconverted address // If there is and it's valid add it to the local inviteList - var check = Invite.isValidAddress(this.refs.textinput.value); - if (check === true || check === null) { + const addrType = Invite.getAddressType(this.refs.textinput.value); + if (addrType !== null) { inviteList.push(this.refs.textinput.value); } else if (this.refs.textinput.value.length > 0) { this.setState({ error: true }); @@ -135,12 +135,26 @@ module.exports = React.createClass({ } else if (e.keyCode === 13) { // enter e.stopPropagation(); e.preventDefault(); - this.onButtonClick(); + if (this.state.queryList.length > 0) { + this.addressSelector.chooseSelection(); + } else { + const addrType = Invite.getAddressType(this.refs.textinput.value); + if (addrType !== null) { + const inviteList = this.state.inviteList.slice(); + inviteList.push(this.refs.textinput.value.trim()); + this.setState({ + inviteList: inviteList, + queryList: [], + }); + } else { + this.setState({ error: true }); + } + } } else if (e.keyCode === 188 || e.keyCode === 9) { // comma or tab e.stopPropagation(); e.preventDefault(); - var check = Invite.isValidAddress(this.refs.textinput.value); - if (check === true || check === null) { + const addrType = Invite.getAddressType(this.refs.textinput.value); + if (addrType !== null) { var inviteList = this.state.inviteList.slice(); inviteList.push(this.refs.textinput.value.trim()); this.setState({ From 14ead373e2e8c6d1ed612000ddbee668b84f8989 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20L=C3=B6thberg?= Date: Wed, 18 Jan 2017 20:54:34 +0100 Subject: [PATCH 079/260] Add markdown test-cases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johannes Löthberg --- .../views/rooms/MessageComposerInput-test.js | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/test/components/views/rooms/MessageComposerInput-test.js b/test/components/views/rooms/MessageComposerInput-test.js index 8d33e0ead3..ca2bbba2eb 100644 --- a/test/components/views/rooms/MessageComposerInput-test.js +++ b/test/components/views/rooms/MessageComposerInput-test.js @@ -158,4 +158,85 @@ describe('MessageComposerInput', () => { expect(['__', '**']).toContain(spy.args[0][1]); }); + it('should not entity-encode " in Markdown mode', () => { + const spy = sinon.spy(client, 'sendTextMessage'); + mci.enableRichtext(false); + addTextToDraft('"'); + mci.handleReturn(sinon.stub()); + + expect(spy.calledOnce).toEqual(true); + expect(spy.args[0][1]).toEqual('"'); + }); + + it('should escape characters without other markup in Markdown mode', () => { + const spy = sinon.spy(client, 'sendTextMessage'); + mci.enableRichtext(false); + addTextToDraft('\\*escaped\\*'); + mci.handleReturn(sinon.stub()); + + expect(spy.calledOnce).toEqual(true); + expect(spy.args[0][1]).toEqual('*escaped*'); + }); + + it('should escape characters with other markup in Markdown mode', () => { + const spy = sinon.spy(client, 'sendHtmlMessage'); + mci.enableRichtext(false); + addTextToDraft('\\*escaped\\* *italic*'); + mci.handleReturn(sinon.stub()); + + expect(spy.calledOnce).toEqual(true); + expect(spy.args[0][1]).toEqual('\\*escaped\\* *italic*'); + expect(spy.args[0][2]).toEqual('*escaped* italic'); + }); + + it('should not convert -_- into a horizontal rule in Markdown mode', () => { + const spy = sinon.spy(client, 'sendTextMessage'); + mci.enableRichtext(false); + addTextToDraft('-_-'); + mci.handleReturn(sinon.stub()); + + expect(spy.calledOnce).toEqual(true); + expect(spy.args[0][1]).toEqual('-_-'); + }); + + it('should not strip tags in Markdown mode', () => { + const spy = sinon.spy(client, 'sendHtmlMessage'); + mci.enableRichtext(false); + addTextToDraft('striked-out'); + mci.handleReturn(sinon.stub()); + + expect(spy.calledOnce).toEqual(true); + expect(spy.args[0][1]).toEqual('striked-out'); + expect(spy.args[0][2]).toEqual('striked-out'); + }); + + it('should not strike-through ~~~ in Markdown mode', () => { + const spy = sinon.spy(client, 'sendTextMessage'); + mci.enableRichtext(false); + addTextToDraft('~~~striked-out~~~'); + mci.handleReturn(sinon.stub()); + + expect(spy.calledOnce).toEqual(true); + expect(spy.args[0][1]).toEqual('~~~striked-out~~~'); + }); + + it('should not mark single unmarkedup paragraphs as HTML in Markdown mode', () => { + const spy = sinon.spy(client, 'sendTextMessage'); + mci.enableRichtext(false); + addTextToDraft('Lorem ipsum dolor sit amet, consectetur adipiscing elit.'); + mci.handleReturn(sinon.stub()); + + expect(spy.calledOnce).toEqual(true); + expect(spy.args[0][1]).toEqual('Lorem ipsum dolor sit amet, consectetur adipiscing elit.'); + }); + + it('should not mark two unmarkedup paragraphs as HTML in Markdown mode', () => { + const spy = sinon.spy(client, 'sendTextMessage'); + mci.enableRichtext(false); + addTextToDraft('Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n\nFusce congue sapien sed neque molestie volutpat.'); + mci.handleReturn(sinon.stub()); + + expect(spy.calledOnce).toEqual(true); + expect(spy.args[0][1]).toEqual('Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n\nFusce congue sapien sed neque molestie volutpat.'); + }); }); From 4df968ecdf36334313f30d27aff409d40cdbe859 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 18 Jan 2017 20:06:44 +0000 Subject: [PATCH 080/260] fix css snafu --- src/components/structures/UserSettings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 5ce9ab1a15..498acc1917 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -605,7 +605,7 @@ module.exports = React.createClass({
- +
Remove From 4e5689082de009c3e6214e578831804942c120bd Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 18 Jan 2017 20:06:54 +0000 Subject: [PATCH 081/260] correctly load synced themes without NPE --- src/components/structures/MatrixChat.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 8917f0535e..0336dc99d5 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -588,13 +588,6 @@ module.exports = React.createClass({ _onLoadCompleted: function() { this.props.onLoadCompleted(); this.setState({loading: false}); - - // set up the right theme. - // XXX: this will temporarily flicker the wrong CSS. - dis.dispatch({ - action: 'set_theme', - value: UserSettingsStore.getSyncedSetting('theme') - }); }, /** @@ -730,6 +723,16 @@ module.exports = React.createClass({ action: 'logout' }); }); + cli.on("accountData", function(ev) { + if (ev.getType() === 'im.vector.web.settings') { + if (ev.getContent() && ev.getContent().theme) { + dis.dispatch({ + action: 'set_theme', + value: ev.getContent().theme, + }); + } + } + }); }, onFocus: function(ev) { From e06dd6e34ae5b70723d7f83603e0a100565fd641 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Thu, 19 Jan 2017 10:51:40 +0100 Subject: [PATCH 082/260] Implement auto-join rooms on registration Also: This fixes registration with a team: only the email localpart was being used to register. When a registration is successful, the user will be joined to rooms specified in the config.json teamsConfig: "teamsConfig" : { "supportEmail": "support@riot.im", "teams": [ { "name" : "matrix", "emailSuffix" : "matrix.org", "rooms" : [ { "id" : "#irc_matrix:matrix.org", "autoJoin" : true } ] } ] } autoJoin can of course be set to false if the room should only be displayed on the (forthcoming) welcome page for each team, and not auto-joined. --- src/components/structures/login/Registration.js | 17 +++++++++++++++++ src/components/views/login/RegistrationForm.js | 6 +++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index fb24b61504..f89b627e8d 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -179,6 +179,23 @@ module.exports = React.createClass({ accessToken: response.access_token }); + // Auto-join rooms + if (self.props.teamsConfig) { + for (let i = 0; i < self.props.teamsConfig.teams.length; i++) { + let team = self.props.teamsConfig.teams[i]; + if (self.state.formVals.email.endsWith(team.emailSuffix)) { + console.log("User successfully registered with team " + team.name); + team.rooms.forEach((room) => { + if (room.autoJoin) { + console.log("Auto-joining " + room.id); + MatrixClientPeg.get().joinRoom(room.id); + } + }); + break; + } + } + } + if (self.props.brand) { MatrixClientPeg.get().getPushers().done((resp)=>{ var pushers = resp.pushers; diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index 3e07302a91..4be40bc53a 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -116,10 +116,14 @@ module.exports = React.createClass({ }, _doSubmit: function() { + let email = this.refs.email.value.trim(); + if (this.state.selectedTeam) { + email += "@" + this.state.selectedTeam.emailSuffix; + } var promise = this.props.onRegisterClick({ username: this.refs.username.value.trim() || this.props.guestUsername, password: this.refs.password.value.trim(), - email: this.refs.email.value.trim() + email: email, }); if (promise) { From e9eb38fd74cb13e85a0d31cd359894a1eea5d535 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Thu, 19 Jan 2017 11:05:08 +0100 Subject: [PATCH 083/260] Update propTypes and do null checks --- src/components/structures/login/Registration.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index f89b627e8d..b092e0a9fb 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -57,6 +57,11 @@ module.exports = React.createClass({ "name": React.PropTypes.string, // The suffix with which every team email address ends "emailSuffix": React.PropTypes.string, + // The rooms to use during auto-join + "rooms": React.PropTypes.arrayOf(React.PropTypes.shape({ + "id": React.PropTypes.string, + "autoJoin": React.PropTypes.bool, + })), })).required, }), @@ -180,11 +185,14 @@ module.exports = React.createClass({ }); // Auto-join rooms - if (self.props.teamsConfig) { + if (self.props.teamsConfig && self.props.teamsConfig.teams) { for (let i = 0; i < self.props.teamsConfig.teams.length; i++) { let team = self.props.teamsConfig.teams[i]; if (self.state.formVals.email.endsWith(team.emailSuffix)) { console.log("User successfully registered with team " + team.name); + if (!team.rooms) { + break; + } team.rooms.forEach((room) => { if (room.autoJoin) { console.log("Auto-joining " + room.id); From 242f5e03016e6aad73890c18914c0d2eaafcc8d4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Jan 2017 10:24:21 +0000 Subject: [PATCH 084/260] PR feedback * Doc & properly indent escapeRegExp * Add close bracket to the list of punctuation chars we search after --- src/components/views/dialogs/ChatInviteDialog.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index 59e95d1538..dfeb2a3978 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -27,8 +27,13 @@ var Modal = require('../../../Modal'); const TRUNCATE_QUERY_LIST = 40; +/* + * Escapes a string so it can be used in a RegExp + * Basically just replaces: \ ^ $ * + ? . ( ) | { } [ ] + * From http://stackoverflow.com/a/6969486 + */ function escapeRegExp(str) { - return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); } module.exports = React.createClass({ @@ -326,7 +331,7 @@ module.exports = React.createClass({ // * The start of the string // * Whitespace, or // * A fixed number of punctuation characters - let expr = new RegExp("(?:^|[\\s\\('\",\.-])" + escapeRegExp(query)); + let expr = new RegExp("(?:^|[\\s\\(\)'\",\.-])" + escapeRegExp(query)); if (expr.test(name)) { return true; } From 8b60cb9df0594bb032a560428dfa1f67f068c08c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 19 Jan 2017 10:43:41 +0000 Subject: [PATCH 085/260] Megolm export: Clear bit 63 of the salt --- src/utils/MegolmExportEncryption.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/utils/MegolmExportEncryption.js b/src/utils/MegolmExportEncryption.js index e3ca7e68f2..983ec2c75f 100644 --- a/src/utils/MegolmExportEncryption.js +++ b/src/utils/MegolmExportEncryption.js @@ -106,6 +106,12 @@ export function encryptMegolmKeyFile(data, password, options) { const salt = new Uint8Array(16); window.crypto.getRandomValues(salt); + + // clear bit 63 of the salt to stop us hitting the 64-bit counter boundary + // (which would mean we wouldn't be able to decrypt on Android). The loss + // of a single bit of salt is a price we have to pay. + salt[9] &= 0x7f; + const iv = new Uint8Array(16); window.crypto.getRandomValues(iv); From fdc213cbb80f84a202c7d55fa79a7c45a66af4d0 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 19 Jan 2017 10:44:01 +0000 Subject: [PATCH 086/260] Megolm export: fix test --- test/utils/MegolmExportEncryption-test.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/utils/MegolmExportEncryption-test.js b/test/utils/MegolmExportEncryption-test.js index db38a931ed..28752ae529 100644 --- a/test/utils/MegolmExportEncryption-test.js +++ b/test/utils/MegolmExportEncryption-test.js @@ -21,12 +21,6 @@ import * as MegolmExportEncryption from 'utils/MegolmExportEncryption'; import * as testUtils from '../test-utils'; import expect from 'expect'; -// polyfill textencoder if necessary -let TextEncoder = window.TextEncoder; -if (!TextEncoder) { - TextEncoder = require('utils/TextEncoderPolyfill'); -} - const TEST_VECTORS=[ [ "plain", From b58a67f6b19fec62731a57656c7a1f27246d9fc4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Jan 2017 10:51:43 +0000 Subject: [PATCH 087/260] Add more punctuation. Also s/let/const/ --- src/components/views/dialogs/ChatInviteDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index dfeb2a3978..b5222d77d0 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -331,7 +331,7 @@ module.exports = React.createClass({ // * The start of the string // * Whitespace, or // * A fixed number of punctuation characters - let expr = new RegExp("(?:^|[\\s\\(\)'\",\.-])" + escapeRegExp(query)); + const expr = new RegExp("(?:^|[\\s\\(\)'\",\.-_@\?;:{}\\[\\]\\#~`\\*\\&\\$])" + escapeRegExp(query)); if (expr.test(name)) { return true; } From 9c1c657a1edf157ec6039bf12b0f8cc848bca5f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20L=C3=B6thberg?= Date: Thu, 19 Jan 2017 11:55:36 +0100 Subject: [PATCH 088/260] Markdown: delete remaining pre-split relics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johannes Löthberg --- src/Markdown.js | 2 +- src/components/views/rooms/MessageComposerInput.js | 2 +- src/components/views/rooms/MessageComposerInputOld.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Markdown.js b/src/Markdown.js index 80d1aa4335..3506e3cb59 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -59,7 +59,7 @@ export default class Markdown { return is_plain; } - toHTML(html) { + toHTML() { const real_paragraph = this.renderer.paragraph; this.renderer.paragraph = function(node, entering) { diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 41b27c1394..b6af5a9f09 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -526,7 +526,7 @@ export default class MessageComposerInput extends React.Component { if (md.isPlainText()) { contentText = md.toPlaintext(); } else { - contentHTML = md.toHTML(true); + contentHTML = md.toHTML(); } } diff --git a/src/components/views/rooms/MessageComposerInputOld.js b/src/components/views/rooms/MessageComposerInputOld.js index 91abd5a2a8..ed4533737f 100644 --- a/src/components/views/rooms/MessageComposerInputOld.js +++ b/src/components/views/rooms/MessageComposerInputOld.js @@ -331,7 +331,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().sendHtmlMessage(this.props.room.roomId, contentText, htmlText); } else { - const contentText = mdown.toPlaintext(false); + const contentText = mdown.toPlaintext(); sendMessagePromise = isEmote ? MatrixClientPeg.get().sendEmoteMessage(this.props.room.roomId, contentText) : MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText); From 1b6735d7292e7e0ee6a4331f687818b1f5fb4506 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 19 Jan 2017 12:51:20 +0000 Subject: [PATCH 089/260] Configure travis to test riot-web after building --- .travis-test-riot.sh | 15 +++++++++++++++ .travis.yml | 3 +++ 2 files changed, 18 insertions(+) create mode 100755 .travis-test-riot.sh diff --git a/.travis-test-riot.sh b/.travis-test-riot.sh new file mode 100755 index 0000000000..b488045df1 --- /dev/null +++ b/.travis-test-riot.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# +# script which is run by the travis build (after `npm run test`). +# +# clones riot-web develop and runs the tests against our version of react-sdk. + +set -ev + +git clone --depth=1 https://github.com/vector-im/riot-web.git riot-web +cd riot-web +mkdir node_modules +ln -s ../.. node_modules/matrix-react-sdk +npm install +(cd node_modules/matrix-js-sdk && npm install) +npm run test diff --git a/.travis.yml b/.travis.yml index 6d68b66a1c..9a8f804644 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,3 +4,6 @@ node_js: install: - npm install - (cd node_modules/matrix-js-sdk && npm install) +script: + - npm run test + - ./.travis-test-riot.sh From a88f9fdd73095c03423f2c5d649ea4930aa55b0a Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 19 Jan 2017 15:36:57 +0000 Subject: [PATCH 090/260] (hopefully) fix theming on Chrome Jump through some hoops to make Chrome behave sensibly on non-default themes. --- src/components/structures/MatrixChat.js | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 0336dc99d5..61a02057ea 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -598,20 +598,32 @@ module.exports = React.createClass({ theme = 'light'; } + // look for the stylesheet elements. + // styleElements is a map from style name to HTMLLinkElement. + var styleElements = Object.create(null); var i, a; for (i = 0; (a = document.getElementsByTagName("link")[i]); i++) { var href = a.getAttribute("href"); + // shouldn't we be using the 'title' tag rather than the href? var match = href.match(/^bundles\/.*\/theme-(.*)\.css$/); if (match) { - if (match[1] === theme) { - a.disabled = false; - } - else { - a.disabled = true; - } + styleElements[match[1]] = a; } } + if (!(theme in styleElements)) { + throw new Error("Unknown theme " + theme); + } + + // disable all of them first, then enable the one we want. Chrome only + // bothers to do an update on a true->false transition, so this ensures + // that we get exactly one update, at the right time. + + Object.values(styleElements).forEach((a) => { + a.disabled = true; + }); + styleElements[theme].disabled = false; + if (theme === 'dark') { // abuse the tinter to change all the SVG's #fff to #2d2d2d // XXX: obviously this shouldn't be hardcoded here. From 2a08abaa955a9e60a06584ef7203e07743d70928 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Jan 2017 16:35:40 +0000 Subject: [PATCH 091/260] Keep old behaviour of submitting on enter if input is empty --- src/components/views/dialogs/ChatInviteDialog.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index 767620d93f..c117944482 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -135,7 +135,10 @@ module.exports = React.createClass({ } else if (e.keyCode === 13) { // enter e.stopPropagation(); e.preventDefault(); - if (this.state.queryList.length > 0) { + if (this.refs.textinput.value == '') { + // if there's nothing in the input box, submit the form + this.onButtonClick(); + } else if (this.state.queryList.length > 0) { this.addressSelector.chooseSelection(); } else { const addrType = Invite.getAddressType(this.refs.textinput.value); From ee1f6c772e164d24e0896c88be200f75d8e371aa Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Jan 2017 16:50:09 +0000 Subject: [PATCH 092/260] Remove duplicate case handled above And fix typo where it was handled --- src/components/views/dialogs/ChatInviteDialog.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index c117944482..5cbb0d4503 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -124,7 +124,7 @@ module.exports = React.createClass({ e.stopPropagation(); e.preventDefault(); this.addressSelector.moveSelectionDown(); - } else if (this.state.queryList.length > 0 && (e.keyCode === 188, e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab + } else if (this.state.queryList.length > 0 && (e.keyCode === 188 || e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab e.stopPropagation(); e.preventDefault(); this.addressSelector.chooseSelection(); @@ -138,8 +138,6 @@ module.exports = React.createClass({ if (this.refs.textinput.value == '') { // if there's nothing in the input box, submit the form this.onButtonClick(); - } else if (this.state.queryList.length > 0) { - this.addressSelector.chooseSelection(); } else { const addrType = Invite.getAddressType(this.refs.textinput.value); if (addrType !== null) { From a2ff1cd8e6578d237720fb0ac77885677c4e4775 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Jan 2017 17:03:16 +0000 Subject: [PATCH 093/260] Factor out adding the input field to the list --- .../views/dialogs/ChatInviteDialog.js | 51 ++++++++----------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index 5cbb0d4503..9928030d7f 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -71,15 +71,12 @@ module.exports = React.createClass({ }, onButtonClick: function() { - var inviteList = this.state.inviteList.slice(); + let inviteList = this.state.inviteList.slice(); // Check the text input field to see if user has an unconverted address // If there is and it's valid add it to the local inviteList - const addrType = Invite.getAddressType(this.refs.textinput.value); - if (addrType !== null) { - inviteList.push(this.refs.textinput.value); - } else if (this.refs.textinput.value.length > 0) { - this.setState({ error: true }); - return; + if (this.refs.textinput.value !== '') { + inviteList = this._addInputToList(); + if (inviteList === false) return; } if (inviteList.length > 0) { @@ -139,32 +136,12 @@ module.exports = React.createClass({ // if there's nothing in the input box, submit the form this.onButtonClick(); } else { - const addrType = Invite.getAddressType(this.refs.textinput.value); - if (addrType !== null) { - const inviteList = this.state.inviteList.slice(); - inviteList.push(this.refs.textinput.value.trim()); - this.setState({ - inviteList: inviteList, - queryList: [], - }); - } else { - this.setState({ error: true }); - } + this._addInputToList(); } } else if (e.keyCode === 188 || e.keyCode === 9) { // comma or tab e.stopPropagation(); e.preventDefault(); - const addrType = Invite.getAddressType(this.refs.textinput.value); - if (addrType !== null) { - var inviteList = this.state.inviteList.slice(); - inviteList.push(this.refs.textinput.value.trim()); - this.setState({ - inviteList: inviteList, - queryList: [], - }); - } else { - this.setState({ error: true }); - } + this._addInputToList(); } }, @@ -376,6 +353,22 @@ module.exports = React.createClass({ return addrs; }, + _addInputToList: function() { + const addrType = Invite.getAddressType(this.refs.textinput.value); + if (addrType !== null) { + const inviteList = this.state.inviteList.slice(); + inviteList.push(this.refs.textinput.value.trim()); + this.setState({ + inviteList: inviteList, + queryList: [], + }); + return inviteList; + } else { + this.setState({ error: true }); + return false; + } + }, + render: function() { var TintableSvg = sdk.getComponent("elements.TintableSvg"); var AddressSelector = sdk.getComponent("elements.AddressSelector"); From dee3495d51b850f7765da766cc063f52284f0c1d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 19 Jan 2017 17:21:03 +0000 Subject: [PATCH 094/260] Run riot-web develop, not master --- .travis-test-riot.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis-test-riot.sh b/.travis-test-riot.sh index b488045df1..c483ea7b4e 100755 --- a/.travis-test-riot.sh +++ b/.travis-test-riot.sh @@ -6,7 +6,7 @@ set -ev -git clone --depth=1 https://github.com/vector-im/riot-web.git riot-web +git clone --depth=1 --branch develop https://github.com/vector-im/riot-web.git riot-web cd riot-web mkdir node_modules ln -s ../.. node_modules/matrix-react-sdk From af4ef1da8bb99e1b4cb2ac0cd22bac39351b2ce9 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 19 Jan 2017 17:41:58 +0000 Subject: [PATCH 095/260] Install source-map-loader before building riot-web --- .travis-test-riot.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.travis-test-riot.sh b/.travis-test-riot.sh index c483ea7b4e..482b4a21c1 100755 --- a/.travis-test-riot.sh +++ b/.travis-test-riot.sh @@ -11,5 +11,10 @@ cd riot-web mkdir node_modules ln -s ../.. node_modules/matrix-react-sdk npm install + (cd node_modules/matrix-js-sdk && npm install) + +# https://github.com/webpack/webpack/issues/1472 workaround +(cd node_modules/matrix-react-sdk && npm install source-map-loader) + npm run test From afa384c4f39e466c22dc6b0552ad9552a80c28bd Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Jan 2017 18:13:27 +0000 Subject: [PATCH 096/260] Use null instead of false --- src/components/views/dialogs/ChatInviteDialog.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index 9928030d7f..18f4ce36ba 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -76,7 +76,7 @@ module.exports = React.createClass({ // If there is and it's valid add it to the local inviteList if (this.refs.textinput.value !== '') { inviteList = this._addInputToList(); - if (inviteList === false) return; + if (inviteList === null) return; } if (inviteList.length > 0) { @@ -365,7 +365,7 @@ module.exports = React.createClass({ return inviteList; } else { this.setState({ error: true }); - return false; + return null; } }, From 33d741e83d826b8540277f4869e84b08b010449d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 19 Jan 2017 18:23:34 +0000 Subject: [PATCH 097/260] Do riot-web setup in a different order ... in an effort to stop npm cocking it up. --- .travis-test-riot.sh | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.travis-test-riot.sh b/.travis-test-riot.sh index 482b4a21c1..c280044246 100755 --- a/.travis-test-riot.sh +++ b/.travis-test-riot.sh @@ -6,15 +6,20 @@ set -ev -git clone --depth=1 --branch develop https://github.com/vector-im/riot-web.git riot-web -cd riot-web +RIOT_WEB_DIR=riot-web +REACT_SDK_DIR=`pwd` + +git clone --depth=1 --branch develop https://github.com/vector-im/riot-web.git \ + "$RIOT_WEB_DIR" + +cd "$RIOT_WEB_DIR" + mkdir node_modules -ln -s ../.. node_modules/matrix-react-sdk npm install (cd node_modules/matrix-js-sdk && npm install) -# https://github.com/webpack/webpack/issues/1472 workaround -(cd node_modules/matrix-react-sdk && npm install source-map-loader) +rm -r node_modules/matrix-react-sdk +ln -s "$REACT_SDK_DIR" node_modules/matrix-react-sdk npm run test From b76b0f755d0d5698383e0bbe9ecbf2e9d090e86a Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 20 Jan 2017 02:47:47 +0000 Subject: [PATCH 098/260] fix context menu bg --- src/components/structures/MatrixChat.js | 1 - src/components/views/rooms/RoomTile.js | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 61a02057ea..170fa6ff84 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -25,7 +25,6 @@ var SdkConfig = require("../../SdkConfig"); var ContextualMenu = require("./ContextualMenu"); var RoomListSorter = require("../../RoomListSorter"); var UserActivity = require("../../UserActivity"); -var UserSettingsStore = require('../../UserSettingsStore'); var Presence = require("../../Presence"); var dis = require("../../dispatcher"); diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 84916f8ab8..834a06d8e6 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -26,6 +26,7 @@ var sdk = require('../../../index'); var ContextualMenu = require('../../structures/ContextualMenu'); var RoomNotifs = require('../../../RoomNotifs'); var FormattingUtils = require('../../../utils/FormattingUtils'); +var UserSettingsStore = require('../../../UserSettingsStore'); module.exports = React.createClass({ displayName: 'RoomTile', @@ -176,7 +177,8 @@ module.exports = React.createClass({ var self = this; ContextualMenu.createMenu(RoomTagMenu, { chevronOffset: 10, - menuColour: "#FFFFFF", + // XXX: fix horrid hardcoding + menuColour: UserSettingsStore.getSyncedSettings().theme === 'dark' ? "#2d2d2d" : "#FFFFFF", left: x, top: y, room: this.props.room, From 2d060c8d2e91669ebc7a17ba0745e99d72c196dc Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 20 Jan 2017 11:04:09 +0000 Subject: [PATCH 099/260] Fix error display in account deactivate dialog --- src/components/views/dialogs/DeactivateAccountDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/DeactivateAccountDialog.js b/src/components/views/dialogs/DeactivateAccountDialog.js index 926e4059d2..9a15841061 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.js +++ b/src/components/views/dialogs/DeactivateAccountDialog.js @@ -80,7 +80,7 @@ export default class DeactivateAccountDialog extends React.Component { let error = null; if (this.state.errStr) { error =
- {this.state.err_str} + {this.state.errStr}
passwordBoxClass = 'error'; } From a16aeeef2a0f16efedf7e6616cdf3c2c8752a077 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 18 Jan 2017 17:22:41 +0000 Subject: [PATCH 100/260] Temporarily add more logging, to try to catch failures on travis --- src/components/structures/MatrixChat.js | 6 ++++-- src/dispatcher.js | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 170fa6ff84..ceca3591c5 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -259,6 +259,8 @@ module.exports = React.createClass({ }, onAction: function(payload) { + console.log("onAction: "+payload.action); + var roomIndexDelta = 1; var self = this; @@ -1006,8 +1008,8 @@ module.exports = React.createClass({ var ForgotPassword = sdk.getComponent('structures.login.ForgotPassword'); var LoggedInView = sdk.getComponent('structures.LoggedInView'); - // console.log("rendering; loading="+this.state.loading+"; screen="+this.state.screen + - // "; logged_in="+this.state.logged_in+"; ready="+this.state.ready); + console.log("rendering; loading="+this.state.loading+"; screen="+this.state.screen + + "; logged_in="+this.state.logged_in+"; ready="+this.state.ready); if (this.state.loading) { var Spinner = sdk.getComponent('elements.Spinner'); diff --git a/src/dispatcher.js b/src/dispatcher.js index f35639c3be..22ef712b6c 100644 --- a/src/dispatcher.js +++ b/src/dispatcher.js @@ -28,6 +28,7 @@ class MatrixDispatcher extends flux.Dispatcher { * for. */ dispatch(payload, sync) { + console.log("Dispatch: "+payload.action); if (sync) { super.dispatch(payload); } else { From 4ba224aac3cccf26537b5bb339dc438239b6b741 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 20 Jan 2017 14:08:14 +0000 Subject: [PATCH 101/260] Use eslint config from the js sdk Extend the js sdk's eslint config to give as consistent a code style as possible. Add react/jsx/flow stuff that we use here. --- .eslintrc | 117 --------------------------------------------------- .eslintrc.js | 62 +++++++++++++++++++++++++++ package.json | 9 ++-- 3 files changed, 67 insertions(+), 121 deletions(-) delete mode 100644 .eslintrc create mode 100644 .eslintrc.js diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index e2baaed5a6..0000000000 --- a/.eslintrc +++ /dev/null @@ -1,117 +0,0 @@ -{ - "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", { - "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"], - "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", { - "ignoreRefs": true - }], - "react/jsx-key": ["error"], - "react/prefer-stateless-function": ["warn"], - - /** flowtype **/ - "flowtype/require-parameter-type": [ - 1, - { - "excludeArrowFunctions": true - } - ], - "flowtype/define-flow-type": 1, - "flowtype/require-return-type": [ - 1, - "always", - { - "annotateUndefined": "never", - "excludeArrowFunctions": true - } - ], - "flowtype/space-after-type-colon": [ - 1, - "always" - ], - "flowtype/space-before-type-colon": [ - 1, - "never" - ] - }, - "settings": { - "flowtype": { - "onlyFilesWithFlowAnnotation": true - } - } -} diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000000..e41106d695 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,62 @@ +module.exports = { + parser: "babel-eslint", + extends: ["./node_modules/matrix-js-sdk/.eslintrc.js"], + plugins: [ + "react", + "flowtype", + ], + env: { + es6: true, + }, + parserOptions: { + ecmaFeatures: { + jsx: true, + } + }, + rules: { + /** react **/ + // This just uses the react plugin to help eslint known when + // variables have been used in JSX + "react/jsx-uses-vars": "error", + + // bind or arrow function in props causes performance issues + "react/jsx-no-bind": ["error", { + "ignoreRefs": true, + }], + "react/jsx-key": ["error"], + + /** flowtype **/ + "flowtype/require-parameter-type": ["warn", { + "excludeArrowFunctions": true, + }], + "flowtype/define-flow-type": "warn", + "flowtype/require-return-type": ["warn", + "always", + { + "annotateUndefined": "never", + "excludeArrowFunctions": true, + } + ], + "flowtype/space-after-type-colon": ["warn", "always"], + "flowtype/space-before-type-colon": ["warn", "never"], + + /* + * things that are errors in the js-sdk config that the current + * code does not adhere to, turned down to warn + */ + "max-len": ["warn"], + "valid-jsdoc": ["warn"], + "new-cap": ["warn"], + "key-spacing": ["warn"], + "arrow-parens": ["warn"], + "prefer-const": ["warn"], + + // crashes currently: https://github.com/eslint/eslint/issues/6274 + "generator-star-spacing": "off", + }, + settings: { + flowtype: { + onlyFilesWithFlowAnnotation: true + }, + }, +}; diff --git a/package.json b/package.json index e2a8fe2c35..8e1ead4b9e 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "devDependencies": { "babel-cli": "^6.5.2", "babel-core": "^6.14.0", - "babel-eslint": "^6.1.0", + "babel-eslint": "^6.1.2", "babel-loader": "^6.2.5", "babel-plugin-add-module-exports": "^0.2.1", "babel-plugin-transform-async-to-generator": "^6.16.0", @@ -86,9 +86,10 @@ "babel-preset-es2016": "^6.11.3", "babel-preset-es2017": "^6.14.0", "babel-preset-react": "^6.11.1", - "eslint": "^2.13.1", - "eslint-plugin-flowtype": "^2.17.0", - "eslint-plugin-react": "^6.2.1", + "eslint": "^3.13.1", + "eslint-config-google": "^0.7.1", + "eslint-plugin-flowtype": "^2.30.0", + "eslint-plugin-react": "^6.9.0", "expect": "^1.16.0", "json-loader": "^0.5.3", "karma": "^0.13.22", From 18d4d3392a13b56d683ff0a536e723d7ead76921 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 20 Jan 2017 14:22:27 +0000 Subject: [PATCH 102/260] Fix a bunch of linting errors eslint --fix and a few manual ones --- src/AddThreepid.js | 2 +- src/Avatar.js | 4 +-- src/BasePlatform.js | 6 ++-- src/CallHandler.js | 4 +-- src/ContentMessages.js | 2 +- src/DateUtils.js | 2 +- src/Entities.js | 2 +- src/HtmlUtils.js | 10 +++--- src/ImageUtils.js | 2 +- src/Invite.js | 4 +-- src/Lifecycle.js | 4 +-- src/Markdown.js | 14 ++++---- src/Modal.js | 8 ++--- src/Notifier.js | 4 +-- src/ObjectUtils.js | 2 +- src/Presence.js | 2 +- src/RichText.js | 4 +-- src/RoomListSorter.js | 2 +- src/RoomNotifs.js | 2 +- src/Rooms.js | 2 +- src/ScalarMessaging.js | 2 +- src/SlashCommands.js | 12 +++---- src/TabComplete.js | 6 ++-- src/TabCompleteEntries.js | 5 ++- src/TextForEvent.js | 3 +- src/Tinter.js | 9 ++--- src/Velociraptor.js | 6 ++-- src/VelocityBounce.js | 6 ++-- src/WhoIsTyping.js | 2 +- src/autocomplete/AutocompleteProvider.js | 2 +- src/autocomplete/CommandProvider.js | 2 +- src/autocomplete/DuckDuckGoProvider.js | 2 +- src/autocomplete/EmojiProvider.js | 2 +- src/autocomplete/RoomProvider.js | 4 +-- src/components/structures/ContextualMenu.js | 8 ++--- src/components/structures/CreateRoom.js | 4 +-- src/components/structures/FilePanel.js | 2 +- src/components/structures/LoggedInView.js | 18 +++++----- src/components/structures/MatrixChat.js | 4 +-- src/components/structures/MessagePanel.js | 6 ++-- src/components/structures/RoomView.js | 34 ++++++++++--------- src/components/structures/ScrollPanel.js | 4 +-- src/components/structures/TimelinePanel.js | 20 ++++++----- src/components/structures/UploadBar.js | 6 ++-- src/components/structures/UserSettings.js | 20 +++++------ .../structures/login/ForgotPassword.js | 6 ++-- src/components/structures/login/Login.js | 8 ++--- .../structures/login/Registration.js | 2 +- src/components/views/avatars/BaseAvatar.js | 2 +- src/components/views/avatars/MemberAvatar.js | 6 ++-- src/components/views/avatars/RoomAvatar.js | 4 +-- src/components/views/create_room/Presets.js | 2 +- .../views/dialogs/ChatInviteDialog.js | 6 ++-- .../views/dialogs/DeactivateAccountDialog.js | 4 +-- .../views/elements/AddressSelector.js | 4 +-- src/components/views/elements/AddressTile.js | 2 +- .../views/elements/DeviceVerifyButtons.js | 4 +-- src/components/views/elements/EditableText.js | 8 ++--- .../views/elements/PowerSelector.js | 8 ++--- src/components/views/elements/ProgressBar.js | 2 +- .../views/elements/TruncatedList.js | 2 +- src/components/views/elements/UserSelector.js | 2 +- src/components/views/login/CaptchaForm.js | 4 +-- .../login/InteractiveAuthEntryComponents.js | 4 +-- .../views/login/RegistrationForm.js | 16 ++++----- src/components/views/login/ServerConfig.js | 4 +-- src/components/views/messages/MAudioBody.js | 2 +- src/components/views/messages/MFileBody.js | 4 +-- src/components/views/messages/MImageBody.js | 2 +- src/components/views/messages/MVideoBody.js | 2 +- src/components/views/messages/TextualBody.js | 6 ++-- .../views/room_settings/AliasSettings.js | 18 +++++----- .../views/room_settings/ColorSettings.js | 2 +- .../views/room_settings/UrlPreviewSettings.js | 6 ++-- src/components/views/rooms/AuxPanel.js | 4 +-- src/components/views/rooms/EventTile.js | 10 +++--- .../views/rooms/LinkPreviewWidget.js | 8 +++-- .../views/rooms/MemberDeviceInfo.js | 2 +- src/components/views/rooms/MemberInfo.js | 12 +++---- src/components/views/rooms/MemberList.js | 8 ++--- src/components/views/rooms/MemberTile.js | 2 +- src/components/views/rooms/MessageComposer.js | 2 +- .../views/rooms/MessageComposerInput.js | 19 ++++++----- .../views/rooms/MessageComposerInputOld.js | 2 +- .../views/rooms/ReadReceiptMarker.js | 4 +-- src/components/views/rooms/RoomHeader.js | 10 +++--- src/components/views/rooms/RoomList.js | 8 ++--- src/components/views/rooms/RoomPreviewBar.js | 6 ++-- src/components/views/rooms/RoomSettings.js | 14 ++++---- src/components/views/rooms/RoomTile.js | 2 +- .../views/rooms/SearchableEntityList.js | 6 ++-- .../views/rooms/SimpleRoomHeader.js | 2 +- src/components/views/rooms/UserTile.js | 2 +- src/components/views/settings/ChangeAvatar.js | 6 ++-- .../views/settings/ChangePassword.js | 4 +-- src/components/views/settings/DevicesPanel.js | 4 +-- .../views/settings/DevicesPanelEntry.js | 7 ++-- .../settings/EnableNotificationsButton.js | 1 - src/components/views/voip/CallView.js | 2 +- src/components/views/voip/VideoView.js | 2 +- src/createRoom.js | 4 +-- src/dispatcher.js | 2 +- src/extend.js | 2 +- src/linkify-matrix.js | 4 +-- src/utils/FormattingUtils.js | 2 +- src/utils/MegolmExportEncryption.js | 2 +- src/wrappers/WithMatrixClient.js | 2 +- 107 files changed, 290 insertions(+), 290 deletions(-) diff --git a/src/AddThreepid.js b/src/AddThreepid.js index 5593d46ff7..d6a1d58aa0 100644 --- a/src/AddThreepid.js +++ b/src/AddThreepid.js @@ -21,7 +21,7 @@ var MatrixClientPeg = require("./MatrixClientPeg"); * optionally, the identity servers. * * This involves getting an email token from the identity server to "prove" that - * the client owns the given email address, which is then passed to the + * the client owns the given email address, which is then passed to the * add threepid API on the homeserver. */ class AddThreepid { diff --git a/src/Avatar.js b/src/Avatar.js index 0ef6c8d07b..76f5e55ff0 100644 --- a/src/Avatar.js +++ b/src/Avatar.js @@ -49,12 +49,12 @@ module.exports = { }, defaultAvatarUrlForString: function(s) { - var images = [ '76cfa6', '50e2c2', 'f4c371' ]; + var images = ['76cfa6', '50e2c2', 'f4c371']; var total = 0; for (var i = 0; i < s.length; ++i) { total += s.charCodeAt(i); } return 'img/' + images[total % images.length] + '.png'; } -} +}; diff --git a/src/BasePlatform.js b/src/BasePlatform.js index 815133c334..8bdf7d0391 100644 --- a/src/BasePlatform.js +++ b/src/BasePlatform.js @@ -41,7 +41,7 @@ export default class BasePlatform { * Returns true if the platform supports displaying * notifications, otherwise false. */ - supportsNotifications() : boolean { + supportsNotifications(): boolean { return false; } @@ -49,7 +49,7 @@ export default class BasePlatform { * Returns true if the application currently has permission * to display notifications. Otherwise false. */ - maySendNotifications() : boolean { + maySendNotifications(): boolean { return false; } @@ -60,7 +60,7 @@ export default class BasePlatform { * that is 'granted' if the user allowed the request or * 'denied' otherwise. */ - requestNotificationPermission() : Promise { + requestNotificationPermission(): Promise { } displayNotification(title: string, msg: string, avatarUrl: string, room: Object) { diff --git a/src/CallHandler.js b/src/CallHandler.js index 31b52b65a3..268a599d8e 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -159,10 +159,10 @@ function _setCallState(call, roomId, status) { calls[roomId] = call; if (status === "ringing") { - play("ringAudio") + play("ringAudio"); } else if (call && call.call_state === "ringing") { - pause("ringAudio") + pause("ringAudio"); } if (call) { diff --git a/src/ContentMessages.js b/src/ContentMessages.js index c169ce64b5..17c8155c1b 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -256,7 +256,7 @@ function uploadFile(matrixClient, roomId, file) { }); }); } else { - const basePromise = matrixClient.uploadContent(file); + const basePromise = matrixClient.uploadContent(file); const promise1 = basePromise.then(function(url) { // If the attachment isn't encrypted then include the URL directly. return {"url": url}; diff --git a/src/DateUtils.js b/src/DateUtils.js index 2b51c5903f..07bab4ae7b 100644 --- a/src/DateUtils.js +++ b/src/DateUtils.js @@ -48,5 +48,5 @@ module.exports = { //return pad(date.getHours()) + ':' + pad(date.getMinutes()); return ('00' + date.getHours()).slice(-2) + ':' + ('00' + date.getMinutes()).slice(-2); } -} +}; diff --git a/src/Entities.js b/src/Entities.js index ac3c976797..7c3909f36f 100644 --- a/src/Entities.js +++ b/src/Entities.js @@ -136,6 +136,6 @@ module.exports = { fromUsers: function(users, showInviteButton, inviteFn) { return users.map(function(u) { return new UserEntity(u, showInviteButton, inviteFn); - }) + }); } }; diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index fc1630b6fb..c7b13bc071 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -91,16 +91,16 @@ var sanitizeHtmlParams = { ], allowedAttributes: { // custom ones first: - font: [ 'color' ], // custom to matrix - a: [ 'href', 'name', 'target', 'rel' ], // remote target: custom to matrix + font: ['color'], // custom to matrix + a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix // We don't currently allow img itself by default, but this // would make sense if we did - img: [ 'src' ], + img: ['src'], }, // Lots of these won't come up by default because we don't allow them - selfClosing: [ 'img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta' ], + selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'], // URL schemes we permit - allowedSchemes: [ 'http', 'https', 'ftp', 'mailto' ], + allowedSchemes: ['http', 'https', 'ftp', 'mailto'], // DO NOT USE. sanitize-html allows all URL starting with '//' // so this will always allow links to whatever scheme the diff --git a/src/ImageUtils.js b/src/ImageUtils.js index fdb12c7608..3744241874 100644 --- a/src/ImageUtils.js +++ b/src/ImageUtils.js @@ -53,5 +53,5 @@ module.exports = { return Math.floor(heightMulti * fullHeight); } }, -} +}; diff --git a/src/Invite.js b/src/Invite.js index 6bfc646977..6ad929e33b 100644 --- a/src/Invite.js +++ b/src/Invite.js @@ -55,7 +55,7 @@ export function inviteToRoom(roomId, addr) { * @returns Promise */ export function inviteMultipleToRoom(roomId, addrs) { - this.inviter = new MultiInviter(roomId); - return this.inviter.invite(addrs); + const inviter = new MultiInviter(roomId); + return inviter.invite(addrs); } diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 0a61dc6105..493bbf12aa 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -18,7 +18,7 @@ import q from 'q'; import Matrix from 'matrix-js-sdk'; import MatrixClientPeg from './MatrixClientPeg'; -import Notifier from './Notifier' +import Notifier from './Notifier'; import UserActivity from './UserActivity'; import Presence from './Presence'; import dis from './dispatcher'; @@ -140,7 +140,7 @@ function _loginWithToken(queryParams, defaultDeviceDisplayName) { homeserverUrl: queryParams.homeserver, identityServerUrl: queryParams.identityServer, guest: false, - }) + }); }, (err) => { console.error("Failed to log in with login token: " + err + " " + err.data); diff --git a/src/Markdown.js b/src/Markdown.js index 3506e3cb59..2f278183a3 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -49,9 +49,9 @@ export default class Markdown { dummy_renderer[k] = setNotPlain; } // text and paragraph are just text - dummy_renderer.text = function(t) { return t; } - dummy_renderer.softbreak = function(t) { return t; } - dummy_renderer.paragraph = function(t) { return t; } + dummy_renderer.text = function(t) { return t; }; + dummy_renderer.softbreak = function(t) { return t; }; + dummy_renderer.paragraph = function(t) { return t; }; const dummy_parser = new commonmark.Parser(); dummy_renderer.render(dummy_parser.parse(this.input)); @@ -70,12 +70,12 @@ export default class Markdown { // its own p tag to keep them as separate paragraphs. var par = node; while (par.parent) { - par = par.parent + par = par.parent; } if (par.firstChild != par.lastChild) { real_paragraph.call(this, node, entering); } - } + }; var parsed = this.parser.parse(this.input); var rendered = this.renderer.render(parsed); @@ -94,7 +94,7 @@ export default class Markdown { this.renderer.out = function(s) { // The `lit` function adds a string literal to the output buffer. this.lit(s); - } + }; this.renderer.paragraph = function(node, entering) { // If there is only one top level node, just return the @@ -112,7 +112,7 @@ export default class Markdown { this.lit('\n\n'); } } - } + }; var parsed = this.parser.parse(this.input); var rendered = this.renderer.render(parsed); diff --git a/src/Modal.js b/src/Modal.js index c2ce04c4e8..862e4befc5 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -36,7 +36,7 @@ const AsyncWrapper = React.createClass({ getInitialState: function() { return { component: null, - } + }; }, componentWillMount: function() { @@ -82,8 +82,8 @@ module.exports = { return container; }, - createDialog: function (Element, props, className) { - return this.createDialogAsync((cb) => {cb(Element)}, props, className); + createDialog: function(Element, props, className) { + return this.createDialogAsync((cb) => {cb(Element);}, props, className); }, /** @@ -105,7 +105,7 @@ module.exports = { * * @param {String} className CSS class to apply to the modal wrapper */ - createDialogAsync: function (loader, props, className) { + createDialogAsync: function(loader, props, className) { var self = this; // never call this via modal.close() from onFinished() otherwise it will loop var closeDialog = function() { diff --git a/src/Notifier.js b/src/Notifier.js index a58fc0132f..67642e734a 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -53,7 +53,7 @@ var Notifier = { if (!msg) return; var title; - if (!ev.sender || room.name == ev.sender.name) { + if (!ev.sender || room.name == ev.sender.name) { title = room.name; // notificationMessageForEvent includes sender, // but we already have the sender here @@ -88,7 +88,7 @@ var Notifier = { if (e) { e.load(); e.play(); - }; + } }, start: function() { diff --git a/src/ObjectUtils.js b/src/ObjectUtils.js index 07a16df501..5fac588a4f 100644 --- a/src/ObjectUtils.js +++ b/src/ObjectUtils.js @@ -64,7 +64,7 @@ module.exports.getKeyValueArrayDiffs = function(before, after) { } else if (itemDelta[item] === -1) { results.push({ place: "del", key: muxedKey, val: item }); } else { - // itemDelta of 0 means it was unchanged between before/after + // itemDelta of 0 means it was unchanged between before/after } }); break; diff --git a/src/Presence.js b/src/Presence.js index 4152d7a487..c45d571217 100644 --- a/src/Presence.js +++ b/src/Presence.js @@ -111,7 +111,7 @@ class Presence { this.timer = setTimeout(function() { self._onUnavailableTimerFire(); }, UNAVAILABLE_TIME_MS); - } + } } module.exports = new Presence(); diff --git a/src/RichText.js b/src/RichText.js index 5fe920fe50..b1793d0ddf 100644 --- a/src/RichText.js +++ b/src/RichText.js @@ -12,7 +12,7 @@ import { SelectionState, Entity, } from 'draft-js'; -import * as sdk from './index'; +import * as sdk from './index'; import * as emojione from 'emojione'; import {stateToHTML} from 'draft-js-export-html'; import {SelectionRange} from "./autocomplete/Autocompleter"; @@ -109,7 +109,7 @@ export function getScopedRTDecorators(scope: any): CompositeDecorator { return {avatar}{props.children}; } }; - + let roomDecorator = { strategy: (contentBlock, callback) => { findWithRegex(ROOM_REGEX, contentBlock, callback); diff --git a/src/RoomListSorter.js b/src/RoomListSorter.js index 09f178dd3f..7a43c1891e 100644 --- a/src/RoomListSorter.js +++ b/src/RoomListSorter.js @@ -26,7 +26,7 @@ function tsOfNewestEvent(room) { } function mostRecentActivityFirst(roomList) { - return roomList.sort(function(a,b) { + return roomList.sort(function(a, b) { return tsOfNewestEvent(b) - tsOfNewestEvent(a); }); } diff --git a/src/RoomNotifs.js b/src/RoomNotifs.js index d0cdd6ead7..7cb7d4b9de 100644 --- a/src/RoomNotifs.js +++ b/src/RoomNotifs.js @@ -146,7 +146,7 @@ function isRuleForRoom(roomId, rule) { } const cond = rule.conditions[0]; if ( - cond.kind == 'event_match' && + cond.kind == 'event_match' && cond.key == 'room_id' && cond.pattern == roomId ) { diff --git a/src/Rooms.js b/src/Rooms.js index cf62f2dda0..fbcc843ad2 100644 --- a/src/Rooms.js +++ b/src/Rooms.js @@ -37,7 +37,7 @@ export function getOnlyOtherMember(room, me) { if (joinedMembers.length === 2) { return joinedMembers.filter(function(m) { - return m.userId !== me.userId + return m.userId !== me.userId; })[0]; } diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index 75062daaa2..dbb7e405df 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -371,7 +371,7 @@ const onMessage = function(event) { }, (err) => { console.error(err); sendError(event, "Failed to lookup current room."); - }) + }); }; module.exports = { diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 523d1d8f3c..1ddcf4832d 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -41,7 +41,7 @@ class Command { } getUsage() { - return "Usage: " + this.getCommandWithArgs() + return "Usage: " + this.getCommandWithArgs(); } } @@ -84,7 +84,7 @@ var commands = { var matches = args.match(/^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))( +(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})))?$/); if (matches) { Tinter.tint(matches[1], matches[4]); - var colorScheme = {} + var colorScheme = {}; colorScheme.primary_color = matches[1]; if (matches[4]) { colorScheme.secondary_color = matches[4]; @@ -288,7 +288,7 @@ var commands = { // helpful aliases var aliases = { j: "join" -} +}; module.exports = { /** @@ -331,9 +331,9 @@ module.exports = { // Return all the commands plus /me and /markdown which aren't handled like normal commands var cmds = Object.keys(commands).sort().map(function(cmdKey) { return commands[cmdKey]; - }) - cmds.push(new Command("me", "", function(){})); - cmds.push(new Command("markdown", "", function(){})); + }); + cmds.push(new Command("me", "", function() {})); + cmds.push(new Command("markdown", "", function() {})); return cmds; } diff --git a/src/TabComplete.js b/src/TabComplete.js index a0380f36c4..59ecc2ae20 100644 --- a/src/TabComplete.js +++ b/src/TabComplete.js @@ -112,7 +112,7 @@ class TabComplete { return; } // ES6 destructuring; ignore first element (the complete match) - var [ , boundaryGroup, partialGroup] = res; + var [, boundaryGroup, partialGroup] = res; if (partialGroup.length === 0 && passive) { return; @@ -254,7 +254,7 @@ class TabComplete { if (ev.ctrlKey || ev.metaKey || ev.altKey) return; // tab key has been pressed at this point - this.handleTabPress(false, ev.shiftKey) + this.handleTabPress(false, ev.shiftKey); // prevent the default TAB operation (typically focus shifting) ev.preventDefault(); @@ -386,6 +386,6 @@ class TabComplete { this.memberTabOrder[ev.getSender()] = this.memberOrderSeq++; } } -}; +} module.exports = TabComplete; diff --git a/src/TabCompleteEntries.js b/src/TabCompleteEntries.js index 2a8c7b383a..e6adec0d7d 100644 --- a/src/TabCompleteEntries.js +++ b/src/TabCompleteEntries.js @@ -13,7 +13,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -var React = require("react"); var sdk = require("./index"); class Entry { @@ -90,7 +89,7 @@ CommandEntry.fromCommands = function(commandArray) { return commandArray.map(function(cmd) { return new CommandEntry(cmd.getCommand(), cmd.getCommandWithArgs()); }); -} +}; class MemberEntry extends Entry { constructor(member) { @@ -119,7 +118,7 @@ MemberEntry.fromMemberList = function(members) { return members.map(function(m) { return new MemberEntry(m); }); -} +}; module.exports.Entry = Entry; module.exports.MemberEntry = MemberEntry; diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 2ffd33167f..3f772e9cfb 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -75,7 +75,6 @@ function textForMemberEvent(ev) { return targetName + " joined the room."; } } - return ''; case 'leave': if (ev.getSender() === ev.getStateKey()) { if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) { @@ -203,4 +202,4 @@ module.exports = { if (!hdlr) return ""; return hdlr(ev); } -} +}; diff --git a/src/Tinter.js b/src/Tinter.js index 4a5e4e453c..5bf13e6d4a 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -14,9 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -var dis = require("./dispatcher"); -var sdk = require("./index"); - // FIXME: these vars should be bundled up and attached to // module.exports otherwise this will break when included by both // react-sdk and apps layered on top. @@ -152,7 +149,7 @@ function hexToRgb(color) { function rgbToHex(rgb) { var val = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2]; - return '#' + (0x1000000 + val).toString(16).slice(1) + return '#' + (0x1000000 + val).toString(16).slice(1); } // List of functions to call when the tint changes. @@ -187,7 +184,7 @@ module.exports = { } if (!secondaryColor) { - var x = 0.16; // average weighting factor calculated from vector green & light green + const x = 0.16; // average weighting factor calculated from vector green & light green var rgb = hexToRgb(primaryColor); rgb[0] = x * rgb[0] + (1 - x) * 255; rgb[1] = x * rgb[1] + (1 - x) * 255; @@ -196,7 +193,7 @@ module.exports = { } if (!tertiaryColor) { - var x = 0.19; + const x = 0.19; var rgb1 = hexToRgb(primaryColor); var rgb2 = hexToRgb(secondaryColor); rgb1[0] = x * rgb1[0] + (1 - x) * rgb2[0]; diff --git a/src/Velociraptor.js b/src/Velociraptor.js index d9b6b3d5dc..006dbcb0ac 100644 --- a/src/Velociraptor.js +++ b/src/Velociraptor.js @@ -76,7 +76,7 @@ module.exports = React.createClass({ var startStyles = self.props.startStyles; if (startStyles.length > 0) { - var startStyle = startStyles[0] + var startStyle = startStyles[0]; newProps.style = startStyle; // console.log("mounted@startstyle0: "+JSON.stringify(startStyle)); } @@ -105,7 +105,7 @@ module.exports = React.createClass({ ) { var startStyles = this.props.startStyles; var transitionOpts = this.props.enterTransitionOpts; - var domNode = ReactDom.findDOMNode(node); + const domNode = ReactDom.findDOMNode(node); // 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) { @@ -145,7 +145,7 @@ module.exports = React.createClass({ // and the FAQ entry, "Preventing memory leaks when // creating/destroying large numbers of elements" // (https://github.com/julianshapiro/velocity/issues/47) - var domNode = ReactDom.findDOMNode(this.nodes[k]); + const domNode = ReactDom.findDOMNode(this.nodes[k]); Velocity.Utilities.removeData(domNode); } this.nodes[k] = node; diff --git a/src/VelocityBounce.js b/src/VelocityBounce.js index 168b0b14af..3ad7d207a9 100644 --- a/src/VelocityBounce.js +++ b/src/VelocityBounce.js @@ -6,10 +6,12 @@ function bounce( p ) { var pow2, bounce = 4; - while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {} + while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) { + // just sets pow2 + } return 1 / Math.pow( 4, 3 - bounce ) - 7.5625 * Math.pow( ( pow2 * 3 - 2 ) / 22 - p, 2 ); } Velocity.Easings.easeOutBounce = function(p) { return 1 - bounce(1 - p); -} +}; diff --git a/src/WhoIsTyping.js b/src/WhoIsTyping.js index 4fb5399027..8c3838d615 100644 --- a/src/WhoIsTyping.js +++ b/src/WhoIsTyping.js @@ -46,4 +46,4 @@ module.exports = { return names.join(', ') + ' and ' + lastPerson + ' are typing'; } } -} +}; diff --git a/src/autocomplete/AutocompleteProvider.js b/src/autocomplete/AutocompleteProvider.js index 9cdb774cac..5c90990295 100644 --- a/src/autocomplete/AutocompleteProvider.js +++ b/src/autocomplete/AutocompleteProvider.js @@ -26,7 +26,7 @@ export default class AutocompleteProvider { } commandRegex.lastIndex = 0; - + let match; while ((match = commandRegex.exec(query)) != null) { let matchStart = match.index, diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index 7d032006db..60171bc72f 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -83,7 +83,7 @@ export default class CommandProvider extends AutocompleteProvider { static getInstance(): CommandProvider { if (instance == null) - instance = new CommandProvider(); + {instance = new CommandProvider();} return instance; } diff --git a/src/autocomplete/DuckDuckGoProvider.js b/src/autocomplete/DuckDuckGoProvider.js index 46aa4b0f03..bffd924976 100644 --- a/src/autocomplete/DuckDuckGoProvider.js +++ b/src/autocomplete/DuckDuckGoProvider.js @@ -13,7 +13,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider { constructor() { super(DDG_REGEX); } - + static getQueryUri(query: String) { return `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}` + `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERRER)}`; diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index 4c8bf60b83..a2d77f02a1 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -44,7 +44,7 @@ export default class EmojiProvider extends AutocompleteProvider { static getInstance() { if (instance == null) - instance = new EmojiProvider(); + {instance = new EmojiProvider();} return instance; } diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index f3401cf1bb..8d1e555e56 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -52,12 +52,12 @@ export default class RoomProvider extends AutocompleteProvider { getName() { return '💬 Rooms'; } - + static getInstance() { if (instance == null) { instance = new RoomProvider(); } - + return instance; } diff --git a/src/components/structures/ContextualMenu.js b/src/components/structures/ContextualMenu.js index da419897dc..e5a62b8345 100644 --- a/src/components/structures/ContextualMenu.js +++ b/src/components/structures/ContextualMenu.js @@ -47,7 +47,7 @@ module.exports = { return container; }, - createMenu: function (Element, props) { + createMenu: function(Element, props) { var self = this; var closeMenu = function() { @@ -78,15 +78,15 @@ module.exports = { .mx_ContextualMenu_chevron_right:after { border-left-color: ${props.menuColour}; } - ` + `; } var chevron = null; if (props.left) { - chevron =
+ chevron =
; position.left = props.left; } else { - chevron =
+ chevron =
; position.right = props.right; } diff --git a/src/components/structures/CreateRoom.js b/src/components/structures/CreateRoom.js index ce4c0916d4..24ebfea07f 100644 --- a/src/components/structures/CreateRoom.js +++ b/src/components/structures/CreateRoom.js @@ -118,7 +118,7 @@ module.exports = React.createClass({ var self = this; - deferred.then(function (resp) { + deferred.then(function(resp) { self.setState({ phase: self.phases.CREATED, }); @@ -210,7 +210,7 @@ module.exports = React.createClass({ onAliasChanged: function(alias) { this.setState({ alias: alias - }) + }); }, onEncryptChanged: function(ev) { diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index 0dd16a7e99..5166619d48 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -35,7 +35,7 @@ var FilePanel = React.createClass({ getInitialState: function() { return { timelineSet: null, - } + }; }, componentWillMount: function() { diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 7c0fe14edd..57a4d4c721 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -160,8 +160,8 @@ export default React.createClass({ collapsedRhs={this.props.collapse_rhs} ConferenceHandler={this.props.ConferenceHandler} scrollStateMap={this._scrollStateMap} - /> - if (!this.props.collapse_rhs) right_panel = + />; + if (!this.props.collapse_rhs) right_panel = ; break; case PageTypes.UserSettings: @@ -170,28 +170,28 @@ export default React.createClass({ brand={this.props.config.brand} collapsedRhs={this.props.collapse_rhs} enableLabs={this.props.config.enableLabs} - /> - if (!this.props.collapse_rhs) right_panel = + />; + if (!this.props.collapse_rhs) right_panel = ; break; case PageTypes.CreateRoom: page_element = - if (!this.props.collapse_rhs) right_panel = + />; + if (!this.props.collapse_rhs) right_panel = ; break; case PageTypes.RoomDirectory: page_element = - if (!this.props.collapse_rhs) right_panel = + />; + if (!this.props.collapse_rhs) right_panel = ; break; case PageTypes.UserView: page_element = null; // deliberately null for now - right_panel = + right_panel = ; break; } diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index ceca3591c5..20d59e22ec 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -77,7 +77,7 @@ module.exports = React.createClass({ getChildContext: function() { return { appConfig: this.props.config, - } + }; }, getInitialState: function() { @@ -1038,7 +1038,7 @@ module.exports = React.createClass({ {...this.props} {...this.state} /> - ) + ); } else if (this.state.logged_in) { // we think we are logged in, but are still waiting for the /sync to complete var Spinner = sdk.getComponent('elements.Spinner'); diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index c04bec4b35..64b0a8e875 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -19,7 +19,7 @@ var ReactDOM = require("react-dom"); var dis = require("../../dispatcher"); var sdk = require('../../index'); -var MatrixClientPeg = require('../../MatrixClientPeg') +var MatrixClientPeg = require('../../MatrixClientPeg'); const MILLIS_IN_DAY = 86400000; @@ -282,7 +282,7 @@ module.exports = React.createClass({ var isMembershipChange = (e) => e.getType() === 'm.room.member' && ['join', 'leave'].indexOf(e.getContent().membership) !== -1 - && (!e.getPrevContent() || e.getContent().membership !== e.getPrevContent().membership); + && (!e.getPrevContent() || e.getContent().membership !== e.getPrevContent().membership); for (i = 0; i < this.props.events.length; i++) { var mxEv = this.props.events[i]; @@ -340,7 +340,7 @@ module.exports = React.createClass({ prevEvent = e; return ret; } - ).reduce((a,b) => a.concat(b)); + ).reduce((a, b) => a.concat(b)); if (eventTiles.length === 0) { eventTiles = null; diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 8059cd9372..1f35e41817 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -48,7 +48,7 @@ if (DEBUG) { // using bind means that we get to keep useful line numbers in the console var debuglog = console.log.bind(console); } else { - var debuglog = function () {}; + var debuglog = function() {}; } module.exports = React.createClass({ @@ -146,7 +146,7 @@ module.exports = React.createClass({ showTopUnreadMessagesBar: false, auxPanelMaxHeight: undefined, - } + }; }, componentWillMount: function() { @@ -674,8 +674,9 @@ module.exports = React.createClass({ }, onSearchResultsFillRequest: function(backwards) { - if (!backwards) + if (!backwards) { return q(false); + } if (this.state.searchResults.next_batch) { debuglog("requesting more search results"); @@ -758,7 +759,7 @@ module.exports = React.createClass({ }).then(() => { var sign_url = this.props.thirdPartyInvite ? this.props.thirdPartyInvite.inviteSignUrl : undefined; return MatrixClientPeg.get().joinRoom(this.props.roomAddress, - { inviteSignUrl: sign_url } ) + { inviteSignUrl: sign_url } ); }).then(function(resp) { var roomId = resp.roomId; @@ -962,7 +963,7 @@ module.exports = React.createClass({ // For overlapping highlights, // favour longer (more specific) terms first highlights = highlights.sort(function(a, b) { - return b.length - a.length }); + return b.length - a.length; }); self.setState({ searchHighlights: highlights, @@ -1025,7 +1026,7 @@ module.exports = React.createClass({ if (scrollPanel) { scrollPanel.checkScroll(); } - } + }; var lastRoomId; @@ -1090,7 +1091,7 @@ module.exports = React.createClass({ } this.refs.room_settings.save().then((results) => { - var fails = results.filter(function(result) { return result.state !== "fulfilled" }); + var fails = results.filter(function(result) { return result.state !== "fulfilled"; }); console.log("Settings saved with %s errors", fails.length); if (fails.length) { fails.forEach(function(result) { @@ -1099,7 +1100,7 @@ module.exports = React.createClass({ var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Failed to save settings", - description: fails.map(function(result) { return result.reason }).join("\n"), + description: fails.map(function(result) { return result.reason; }).join("\n"), }); // still editing room settings } @@ -1183,7 +1184,7 @@ module.exports = React.createClass({ this.setState({ searching: true }); }, - onCancelSearchClick: function () { + onCancelSearchClick: function() { this.setState({ searching: false, searchResults: null, @@ -1208,8 +1209,9 @@ module.exports = React.createClass({ // decide whether or not the top 'unread messages' bar should be shown _updateTopUnreadMessagesBar: function() { - if (!this.refs.messagePanel) + if (!this.refs.messagePanel) { return; + } var pos = this.refs.messagePanel.getReadMarkerPosition(); @@ -1498,7 +1500,7 @@ module.exports = React.createClass({ if (ContentMessages.getCurrentUploads().length > 0) { var UploadBar = sdk.getComponent('structures.UploadBar'); - statusBar = + statusBar = ; } else if (!this.state.searchResults) { var RoomStatusBar = sdk.getComponent('structures.RoomStatusBar'); @@ -1513,7 +1515,7 @@ module.exports = React.createClass({ onCancelAllClick={this.onCancelAllClick} onScrollToBottomClick={this.jumpToLiveTimeline} onResize={this.onChildResize} - /> + />; } var aux = null; @@ -1569,7 +1571,7 @@ module.exports = React.createClass({ messageComposer = + callState={this.state.callState} tabComplete={this.tabComplete} opacity={ this.props.opacity }/>; } // TODO: Why aren't we storing the term/scope/count in this format @@ -1597,14 +1599,14 @@ module.exports = React.createClass({ {call.isLocalVideoMuted() -
+
; } voiceMuteButton =
{call.isMicrophoneMuted() -
+ ; // wrap the existing status bar into a 'callStatusBar' which adds more knobs. statusBar = @@ -1614,7 +1616,7 @@ module.exports = React.createClass({ { zoomButton } { statusBar } - + ; } // if we have search results, we keep the messagepanel (so that it preserves its diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index a9e16d364c..1391d2b740 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -34,7 +34,7 @@ if (DEBUG_SCROLL) { // using bind means that we get to keep useful line numbers in the console var debuglog = console.log.bind(console); } else { - var debuglog = function () {}; + var debuglog = function() {}; } /* This component implements an intelligent scrolling list. @@ -600,7 +600,7 @@ module.exports = React.createClass({ stuckAtBottom: false, trackedScrollToken: node.dataset.scrollToken, pixelOffset: wrapperRect.bottom - boundingRect.bottom, - } + }; debuglog("Saved scroll state", this.scrollState); return; } diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index bf31f44315..490b83f2bf 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -38,7 +38,7 @@ if (DEBUG) { // using bind means that we get to keep useful line numbers in the console var debuglog = console.log.bind(console); } else { - var debuglog = function () {}; + var debuglog = function() {}; } /* @@ -322,7 +322,7 @@ var TimelinePanel = React.createClass({ }); }, - onMessageListScroll: function () { + onMessageListScroll: function() { if (this.props.onScroll) { this.props.onScroll(); } @@ -387,7 +387,7 @@ var TimelinePanel = React.createClass({ // if we're at the end of the live timeline, append the pending events if (this.props.timelineSet.room && !this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) { - events.push(... this.props.timelineSet.room.getPendingEvents()); + events.push(...this.props.timelineSet.room.getPendingEvents()); } var updatedState = {events: events}; @@ -564,8 +564,9 @@ var TimelinePanel = React.createClass({ // first find where the current RM is for (var i = 0; i < events.length; i++) { - if (events[i].getId() == this.state.readMarkerEventId) + if (events[i].getId() == this.state.readMarkerEventId) { break; + } } if (i >= events.length) { return; @@ -644,7 +645,7 @@ var TimelinePanel = React.createClass({ var tl = this.props.timelineSet.getTimelineForEvent(rmId); var rmTs; if (tl) { - var event = tl.getEvents().find((e) => { return e.getId() == rmId }); + var event = tl.getEvents().find((e) => { return e.getId() == rmId; }); if (event) { rmTs = event.getTs(); } @@ -821,7 +822,7 @@ var TimelinePanel = React.createClass({ description: message, onFinished: onFinished, }); - } + }; var prom = this._timelineWindow.load(eventId, INITIAL_SIZE); @@ -843,7 +844,7 @@ var TimelinePanel = React.createClass({ timelineLoading: true, }); - prom = prom.then(onLoaded, onError) + prom = prom.then(onLoaded, onError); } prom.done(); @@ -868,7 +869,7 @@ var TimelinePanel = React.createClass({ // if we're at the end of the live timeline, append the pending events if (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) { - events.push(... this.props.timelineSet.getPendingEvents()); + events.push(...this.props.timelineSet.getPendingEvents()); } return events; @@ -930,8 +931,9 @@ var TimelinePanel = React.createClass({ _getCurrentReadReceipt: function(ignoreSynthesized) { var client = MatrixClientPeg.get(); // the client can be null on logout - if (client == null) + if (client == null) { return null; + } var myUserId = client.credentials.userId; return this.props.timelineSet.room.getEventReadUpTo(myUserId, ignoreSynthesized); diff --git a/src/components/structures/UploadBar.js b/src/components/structures/UploadBar.js index 794fcffec7..e91e558cb2 100644 --- a/src/components/structures/UploadBar.js +++ b/src/components/structures/UploadBar.js @@ -57,7 +57,7 @@ module.exports = React.createClass({displayName: 'UploadBar', // }]; if (uploads.length == 0) { - return
+ return
; } var upload; @@ -68,7 +68,7 @@ module.exports = React.createClass({displayName: 'UploadBar', } } if (!upload) { - return
+ return
; } var innerProgressStyle = { @@ -76,7 +76,7 @@ module.exports = React.createClass({displayName: 'UploadBar', }; var uploadedSize = filesize(upload.loaded); var totalSize = filesize(upload.total); - if (uploadedSize.replace(/^.* /,'') === totalSize.replace(/^.* /,'')) { + if (uploadedSize.replace(/^.* /, '') === totalSize.replace(/^.* /, '')) { uploadedSize = uploadedSize.replace(/ .*/, ''); } diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 498acc1917..8f6473a181 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -346,8 +346,8 @@ module.exports = React.createClass({ this.setState({email_add_pending: false}); if (err.errcode == 'M_THREEPID_AUTH_FAILED') { var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - var message = "Unable to verify email address. " - message += "Please check your email and click on the link it contains. Once this is done, click continue." + var message = "Unable to verify email address. "; + message += "Please check your email and click on the link it contains. Once this is done, click continue."; Modal.createDialog(QuestionDialog, { title: "Verification Pending", description: message, @@ -417,7 +417,7 @@ module.exports = React.createClass({ -
+
; }, _renderSyncedSetting: function(setting) { @@ -430,7 +430,7 @@ module.exports = React.createClass({ -
+
; }, _renderThemeSelector: function(setting) { @@ -442,7 +442,7 @@ module.exports = React.createClass({ defaultChecked={ this._syncedSettings[setting.id] === setting.value } onChange={ e => { if (e.target.checked) { - UserSettingsStore.setSyncedSetting(setting.id, setting.value) + UserSettingsStore.setSyncedSetting(setting.id, setting.value); } dis.dispatch({ action: 'set_theme', @@ -454,7 +454,7 @@ module.exports = React.createClass({ - + ; }, _renderCryptoInfo: function() { @@ -467,8 +467,8 @@ module.exports = React.createClass({

Cryptography

    -
  • {deviceId}
  • -
  • {identityKey}
  • +
  • {deviceId}
  • +
  • {identityKey}
@@ -485,7 +485,7 @@ module.exports = React.createClass({ ); }, - _renderLabs: function () { + _renderLabs: function() { // default to enabled if undefined if (this.props.enableLabs === false) return null; @@ -521,7 +521,7 @@ module.exports = React.createClass({ {features} - ) + ); }, _renderDeactivateAccount: function() { diff --git a/src/components/structures/login/ForgotPassword.js b/src/components/structures/login/ForgotPassword.js index 1868c2ee73..5037136b1d 100644 --- a/src/components/structures/login/ForgotPassword.js +++ b/src/components/structures/login/ForgotPassword.js @@ -58,7 +58,7 @@ module.exports = React.createClass({ this.setState({ progress: null }); - }) + }); }, onVerify: function(ev) { @@ -71,7 +71,7 @@ module.exports = React.createClass({ this.setState({ progress: "complete" }); }, (err) => { this.showErrorDialog(err.message); - }) + }); }, onSubmitForm: function(ev) { @@ -129,7 +129,7 @@ module.exports = React.createClass({ var resetPasswordJsx; if (this.state.progress === "sending_email") { - resetPasswordJsx = + resetPasswordJsx = ; } else if (this.state.progress === "sent_email") { resetPasswordJsx = ( diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index c0d0c08d2c..fe9b544751 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -173,7 +173,7 @@ module.exports = React.createClass({ }, _getCurrentFlowStep: function() { - return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null + return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null; }, _setStateFromError: function(err, isLoginAttempt) { @@ -195,7 +195,7 @@ module.exports = React.createClass({ } let errorText = "Error: Problem communicating with the given homeserver " + - (errCode ? "(" + errCode + ")" : "") + (errCode ? "(" + errCode + ")" : ""); if (err.cors === 'rejected') { if (window.location.protocol === 'https:' && @@ -258,7 +258,7 @@ module.exports = React.createClass({ loginAsGuestJsx = Login as guest - + ; } var returnToAppJsx; @@ -266,7 +266,7 @@ module.exports = React.createClass({ returnToAppJsx = Return to app - + ; } return ( diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index b092e0a9fb..90140b3280 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -333,7 +333,7 @@ module.exports = React.createClass({ returnToAppJsx = Return to app - + ; } return ( diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index 47f0a76891..363f340fad 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -41,7 +41,7 @@ module.exports = React.createClass({ height: 40, resizeMethod: 'crop', defaultToInitialLetter: true - } + }; }, getInitialState: function() { diff --git a/src/components/views/avatars/MemberAvatar.js b/src/components/views/avatars/MemberAvatar.js index c8a9abb4fe..9fb522a5f1 100644 --- a/src/components/views/avatars/MemberAvatar.js +++ b/src/components/views/avatars/MemberAvatar.js @@ -42,7 +42,7 @@ module.exports = React.createClass({ height: 40, resizeMethod: 'crop', viewUserOnClick: false, - } + }; }, getInitialState: function() { @@ -64,7 +64,7 @@ module.exports = React.createClass({ props.width, props.height, props.resizeMethod) - } + }; }, render: function() { @@ -78,7 +78,7 @@ module.exports = React.createClass({ action: 'view_user', member: this.props.member, }); - } + }; } return ( diff --git a/src/components/views/avatars/RoomAvatar.js b/src/components/views/avatars/RoomAvatar.js index dcb25eff61..bfa7575b0c 100644 --- a/src/components/views/avatars/RoomAvatar.js +++ b/src/components/views/avatars/RoomAvatar.js @@ -39,7 +39,7 @@ module.exports = React.createClass({ height: 36, resizeMethod: 'crop', oobData: {}, - } + }; }, getInitialState: function() { @@ -51,7 +51,7 @@ module.exports = React.createClass({ componentWillReceiveProps: function(newProps) { this.setState({ urls: this.getImageUrls(newProps) - }) + }); }, getImageUrls: function(props) { diff --git a/src/components/views/create_room/Presets.js b/src/components/views/create_room/Presets.js index 0cce4a6644..6d40be9d32 100644 --- a/src/components/views/create_room/Presets.js +++ b/src/components/views/create_room/Presets.js @@ -40,7 +40,7 @@ module.exports = React.createClass({ }, onValueChanged: function(ev) { - this.props.onChange(ev.target.value) + this.props.onChange(ev.target.value); }, render: function() { diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index 18f4ce36ba..64f90c1a30 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -171,7 +171,7 @@ module.exports = React.createClass({ inviteList: inviteList, queryList: [], }); - } + }; }, onClick: function(index) { @@ -402,10 +402,10 @@ module.exports = React.createClass({ var error; var addressSelector; if (this.state.error) { - error =
You have entered an invalid contact. Try using their Matrix ID or email address.
+ error =
You have entered an invalid contact. Try using their Matrix ID or email address.
; } else { addressSelector = ( - {this.addressSelector = ref}} + {this.addressSelector = ref;}} addressList={ this.state.queryList } onSelected={ this.onSelected } truncateAt={ TRUNCATE_QUERY_LIST } /> diff --git a/src/components/views/dialogs/DeactivateAccountDialog.js b/src/components/views/dialogs/DeactivateAccountDialog.js index 9a15841061..54a4e99424 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.js +++ b/src/components/views/dialogs/DeactivateAccountDialog.js @@ -81,7 +81,7 @@ export default class DeactivateAccountDialog extends React.Component { if (this.state.errStr) { error =
{this.state.errStr} -
+ ; passwordBoxClass = 'error'; } @@ -92,7 +92,7 @@ export default class DeactivateAccountDialog extends React.Component { if (!this.state.busy) { cancelButton = + ; } return ( diff --git a/src/components/views/elements/AddressSelector.js b/src/components/views/elements/AddressSelector.js index c477b8e7eb..d42637c9e5 100644 --- a/src/components/views/elements/AddressSelector.js +++ b/src/components/views/elements/AddressSelector.js @@ -129,7 +129,7 @@ module.exports = React.createClass({ _maxSelected: function(list) { var listSize = list.length === 0 ? 0 : list.length - 1; - var maxSelected = listSize > (this.props.truncateAt - 1) ? (this.props.truncateAt - 1) : listSize + var maxSelected = listSize > (this.props.truncateAt - 1) ? (this.props.truncateAt - 1) : listSize; return maxSelected; }, @@ -140,7 +140,7 @@ module.exports = React.createClass({ }); return ( -
{this.scrollElement = ref}}> +
{this.scrollElement = ref;}}> { this.createAddressListTiles() }
); diff --git a/src/components/views/elements/AddressTile.js b/src/components/views/elements/AddressTile.js index 2799f10a41..9fc45d582c 100644 --- a/src/components/views/elements/AddressTile.js +++ b/src/components/views/elements/AddressTile.js @@ -108,7 +108,7 @@ module.exports = React.createClass({ info = (
{ this.props.address }
); - } else if (email) { + } else if (email) { var emailClasses = classNames({ "mx_AddressTile_email": true, "mx_AddressTile_justified": this.props.justified, diff --git a/src/components/views/elements/DeviceVerifyButtons.js b/src/components/views/elements/DeviceVerifyButtons.js index aeb93e866c..da3975e4db 100644 --- a/src/components/views/elements/DeviceVerifyButtons.js +++ b/src/components/views/elements/DeviceVerifyButtons.js @@ -42,8 +42,8 @@ export default React.createClass({
  • { this.props.device.getDisplayName() }
  • -
  • { this.props.device.deviceId}
  • -
  • { this.props.device.getFingerprint() }
  • +
  • { this.props.device.deviceId}
  • +
  • { this.props.device.getFingerprint() }

diff --git a/src/components/views/elements/EditableText.js b/src/components/views/elements/EditableText.js index 15118f249e..2c74567698 100644 --- a/src/components/views/elements/EditableText.js +++ b/src/components/views/elements/EditableText.js @@ -57,7 +57,7 @@ module.exports = React.createClass({ getInitialState: function() { return { phase: this.Phases.Display, - } + }; }, componentWillReceiveProps: function(nextProps) { @@ -164,7 +164,7 @@ module.exports = React.createClass({ this.setState({ phase: this.Phases.Edit, - }) + }); }, onFocus: function(ev) { @@ -197,9 +197,9 @@ module.exports = React.createClass({ sel.removeAllRanges(); if (this.props.blurToCancel) - this.cancelEdit(); + {this.cancelEdit();} else - this.onFinish(ev); + {this.onFinish(ev);} this.showPlaceholder(!this.value); }, diff --git a/src/components/views/elements/PowerSelector.js b/src/components/views/elements/PowerSelector.js index 993f2b965a..c7bfd4eec1 100644 --- a/src/components/views/elements/PowerSelector.js +++ b/src/components/views/elements/PowerSelector.js @@ -73,7 +73,7 @@ module.exports = React.createClass({ getValue: function() { var value; if (this.refs.select) { - value = reverseRoles[ this.refs.select.value ]; + value = reverseRoles[this.refs.select.value]; if (this.refs.custom) { if (value === undefined) value = parseInt( this.refs.custom.value ); } @@ -86,10 +86,10 @@ module.exports = React.createClass({ if (this.state.custom) { var input; if (this.props.disabled) { - input = { this.props.value } + input = { this.props.value }; } else { - input = + input = ; } customPicker = of { input }; } @@ -115,7 +115,7 @@ module.exports = React.createClass({ - + ; } return ( diff --git a/src/components/views/elements/ProgressBar.js b/src/components/views/elements/ProgressBar.js index 12b34480f1..a39e8e48f9 100644 --- a/src/components/views/elements/ProgressBar.js +++ b/src/components/views/elements/ProgressBar.js @@ -35,4 +35,4 @@ module.exports = React.createClass({

); } -}); \ No newline at end of file +}); diff --git a/src/components/views/elements/TruncatedList.js b/src/components/views/elements/TruncatedList.js index 3e174848d3..0ec2c15f0a 100644 --- a/src/components/views/elements/TruncatedList.js +++ b/src/components/views/elements/TruncatedList.js @@ -55,7 +55,7 @@ module.exports = React.createClass({ overflowJsx = this.props.createOverflowElement( overflowCount, childCount ); - + // cut out the overflow elements childArray.splice(childCount - overflowCount, overflowCount); childsJsx = childArray; // use what is left diff --git a/src/components/views/elements/UserSelector.js b/src/components/views/elements/UserSelector.js index 5f176a3e54..266e10154f 100644 --- a/src/components/views/elements/UserSelector.js +++ b/src/components/views/elements/UserSelector.js @@ -56,7 +56,7 @@ module.exports = React.createClass({
    {this.props.selected_users.map(function(user_id, i) { - return
  • {user_id} - X
  • + return
  • {user_id} - X
  • ; })}
diff --git a/src/components/views/login/CaptchaForm.js b/src/components/views/login/CaptchaForm.js index d50e0dee26..0977f947aa 100644 --- a/src/components/views/login/CaptchaForm.js +++ b/src/components/views/login/CaptchaForm.js @@ -52,7 +52,7 @@ module.exports = React.createClass({ this._onCaptchaLoaded(); } else { console.log("Loading recaptcha script..."); - window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded()}; + window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded();}; var protocol = global.location.protocol; if (protocol === "file:") { var warning = document.createElement('div'); @@ -101,7 +101,7 @@ module.exports = React.createClass({ } catch (e) { this.setState({ errorText: e.toString(), - }) + }); } }, diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js index 23e2b442ef..ec184ca09f 100644 --- a/src/components/views/login/InteractiveAuthEntryComponents.js +++ b/src/components/views/login/InteractiveAuthEntryComponents.js @@ -70,7 +70,7 @@ export const PasswordAuthEntry = React.createClass({ }); }, - _onPasswordFieldChange: function (ev) { + _onPasswordFieldChange: function(ev) { // enable the submit button iff the password is non-empty this.props.setSubmitButtonEnabled(Boolean(ev.target.value)); }, @@ -209,4 +209,4 @@ export function getEntryComponentForLoginType(loginType) { } } return FallbackAuthEntry; -}; +} diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index 4be40bc53a..f8a0863f70 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -169,7 +169,7 @@ module.exports = React.createClass({ validateField: function(field_id) { var pwd1 = this.refs.password.value.trim(); - var pwd2 = this.refs.passwordConfirm.value.trim() + var pwd2 = this.refs.passwordConfirm.value.trim(); switch (field_id) { case FIELD_EMAIL: @@ -283,7 +283,7 @@ module.exports = React.createClass({ autoFocus={true} placeholder="Email address (optional)" defaultValue={this.props.defaultEmail} className={this._classForField(FIELD_EMAIL, 'mx_Login_field')} - onBlur={function() {self.validateField(FIELD_EMAIL)}} + onBlur={function() {self.validateField(FIELD_EMAIL);}} value={self.state.email}/> {emailSuffix ? : null }
@@ -293,8 +293,8 @@ module.exports = React.createClass({ + onBlur={function() {self.validateField(FIELD_USERNAME);}} />
{ this.props.guestUsername ?
Setting a user name will create a fresh account
: null }

{registerButton} diff --git a/src/components/views/login/ServerConfig.js b/src/components/views/login/ServerConfig.js index a18cfbbcef..4e6ed12f9e 100644 --- a/src/components/views/login/ServerConfig.js +++ b/src/components/views/login/ServerConfig.js @@ -64,10 +64,10 @@ module.exports = React.createClass({ hs_url: this.props.customHsUrl, is_url: this.props.customIsUrl, // if withToggleButton is false, then show the config all the time given we have no way otherwise of making it visible - configVisible: !this.props.withToggleButton || + configVisible: !this.props.withToggleButton || (this.props.customHsUrl !== this.props.defaultHsUrl) || (this.props.customIsUrl !== this.props.defaultIsUrl) - } + }; }, onHomeserverChanged: function(ev) { diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js index 7e338e8466..73b9bdb200 100644 --- a/src/components/views/messages/MAudioBody.js +++ b/src/components/views/messages/MAudioBody.js @@ -31,7 +31,7 @@ export default class MAudioBody extends React.Component { decryptedUrl: null, decryptedBlob: null, error: null, - } + }; } onPlayToggle() { this.setState({ diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js index 4f5ca2d3be..86aee28269 100644 --- a/src/components/views/messages/MFileBody.js +++ b/src/components/views/messages/MFileBody.js @@ -281,7 +281,7 @@ module.exports = React.createClass({ decryptedBlob: blob, }); }).catch((err) => { - console.warn("Unable to decrypt attachment: ", err) + console.warn("Unable to decrypt attachment: ", err); Modal.createDialog(ErrorDialog, { description: "Error decrypting attachment" }); @@ -372,7 +372,7 @@ module.exports = React.createClass({ var extra = text ? (': ' + text) : ''; return Invalid file{extra} - + ; } }, }); diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 5e7cb6e800..10941e0f7f 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -119,7 +119,7 @@ module.exports = React.createClass({ if (content.info.thumbnail_file) { thumbnailPromise = decryptFile( content.info.thumbnail_file - ).then(function (blob) { + ).then(function(blob) { return readBlobAsDataUri(blob); }); } diff --git a/src/components/views/messages/MVideoBody.js b/src/components/views/messages/MVideoBody.js index 18552a973d..e2d4af9e69 100644 --- a/src/components/views/messages/MVideoBody.js +++ b/src/components/views/messages/MVideoBody.js @@ -111,7 +111,7 @@ module.exports = React.createClass({ this.props.onWidgetLoad(); }); }).catch((err) => { - console.warn("Unable to decrypt attachment: ", err) + console.warn("Unable to decrypt attachment: ", err); // Set a placeholder image when we can't decrypt the image. this.setState({ error: err, diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index d005ef0cca..fd26ae58da 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -200,7 +200,7 @@ module.exports = React.createClass({ global.localStorage.removeItem("hide_preview_" + self.props.mxEvent.getId()); } }, - } + }; }, onStarterLinkClick: function(starterLink, ev) { @@ -230,8 +230,8 @@ module.exports = React.createClass({ if (!confirmed) { return; } - let width = window.screen.width > 1024 ? 1024 : window.screen.width; - let height = window.screen.height > 800 ? 800 : window.screen.height; + let width = window.screen.width > 1024 ? 1024 : window.screen.width; + let height = window.screen.height > 800 ? 800 : window.screen.height; let left = (window.screen.width - width) / 2; let top = (window.screen.height - height) / 2; window.open(completeUrl, '_blank', `height=${height}, width=${width}, top=${top}, left=${left},`); diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.js index 96618b613a..5935298634 100644 --- a/src/components/views/room_settings/AliasSettings.js +++ b/src/components/views/room_settings/AliasSettings.js @@ -103,13 +103,13 @@ module.exports = React.createClass({ } if (oldCanonicalAlias !== this.state.canonicalAlias) { console.log("AliasSettings: Updating canonical alias"); - promises = [ q.all(promises).then( + promises = [q.all(promises).then( MatrixClientPeg.get().sendStateEvent( this.props.roomId, "m.room.canonical_alias", { alias: this.state.canonicalAlias }, "" ) - ) ]; + )]; } return promises; @@ -144,7 +144,7 @@ module.exports = React.createClass({ // XXX: do we need to deep copy aliases before editing it? this.state.domainToAliases[domain] = this.state.domainToAliases[domain] || []; this.state.domainToAliases[domain].push(alias); - this.setState({ + this.setState({ domainToAliases: this.state.domainToAliases }); @@ -152,9 +152,9 @@ module.exports = React.createClass({ this.refs.add_alias.setValue(''); // FIXME } else { - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Invalid alias format", + title: "Invalid alias format", description: "'" + alias + "' is not a valid format for an alias", }); } @@ -168,9 +168,9 @@ module.exports = React.createClass({ this.state.domainToAliases[domain][index] = alias; } else { - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Invalid address format", + title: "Invalid address format", description: "'" + alias + "' is not a valid format for an address", }); } @@ -183,7 +183,7 @@ module.exports = React.createClass({ // would be to arbitrarily deepcopy to a temp variable and then setState // that, but why bother when we can cut this corner. var alias = this.state.domainToAliases[domain].splice(index, 1); - this.setState({ + this.setState({ domainToAliases: this.state.domainToAliases }); }, @@ -310,4 +310,4 @@ module.exports = React.createClass({
); } -}); \ No newline at end of file +}); diff --git a/src/components/views/room_settings/ColorSettings.js b/src/components/views/room_settings/ColorSettings.js index 6d147b1f63..6a455d9c3c 100644 --- a/src/components/views/room_settings/ColorSettings.js +++ b/src/components/views/room_settings/ColorSettings.js @@ -135,7 +135,7 @@ module.exports = React.createClass({ ); } - var boundClick = this._onColorSchemeChanged.bind(this, i) + var boundClick = this._onColorSchemeChanged.bind(this, i); return (
Disable URL previews by default for participants in this room - + ; } else { disableRoomPreviewUrls = + ; } return ( @@ -154,4 +154,4 @@ module.exports = React.createClass({ ); } -}); \ No newline at end of file +}); diff --git a/src/components/views/rooms/AuxPanel.js b/src/components/views/rooms/AuxPanel.js index f7c3052ea8..365cc18f99 100644 --- a/src/components/views/rooms/AuxPanel.js +++ b/src/components/views/rooms/AuxPanel.js @@ -93,8 +93,8 @@ module.exports = React.createClass({ } else { joinText = ( - Join as { this.onConferenceNotificationClick(event, 'voice')}} - href="#">voice or { this.onConferenceNotificationClick(event, 'video') }} + Join as { this.onConferenceNotificationClick(event, 'voice');}} + href="#">voice or { this.onConferenceNotificationClick(event, 'video'); }} href="#">video. ); diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 42393ad87b..c6a766509a 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -149,13 +149,13 @@ module.exports = WithMatrixClient(React.createClass({ this.props.mxEvent.on("Event.decrypted", this._onDecrypted); }, - componentWillReceiveProps: function (nextProps) { + componentWillReceiveProps: function(nextProps) { if (nextProps.mxEvent !== this.props.mxEvent) { this._verifyEvent(nextProps.mxEvent); } }, - shouldComponentUpdate: function (nextProps, nextState) { + shouldComponentUpdate: function(nextProps, nextState) { if (!ObjectUtils.shallowEqual(this.state, nextState)) { return true; } @@ -259,7 +259,7 @@ module.exports = WithMatrixClient(React.createClass({ onEditClicked: function(e) { var MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu'); - var buttonRect = e.target.getBoundingClientRect() + var buttonRect = e.target.getBoundingClientRect(); // The window X and Y offsets are to adjust position when zoomed in to page var x = buttonRect.right + window.pageXOffset; @@ -293,7 +293,7 @@ module.exports = WithMatrixClient(React.createClass({ // If it is, we want to display the complete date along with the HH:MM:SS, // rather than just HH:MM:SS. let dayAfterEvent = new Date(this.props.mxEvent.getTs()); - dayAfterEvent.setDate(dayAfterEvent.getDate() + 1) + dayAfterEvent.setDate(dayAfterEvent.getDate() + 1); dayAfterEvent.setHours(0); dayAfterEvent.setMinutes(0); dayAfterEvent.setSeconds(0); @@ -369,7 +369,7 @@ module.exports = WithMatrixClient(React.createClass({ var event = this.props.mxEvent; Modal.createDialogAsync((cb) => { - require(['../../../async-components/views/dialogs/EncryptedEventDialog'], cb) + require(['../../../async-components/views/dialogs/EncryptedEventDialog'], cb); }, { event: event, }); diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.js index 60f4f8abc0..ef8fb29cbc 100644 --- a/src/components/views/rooms/LinkPreviewWidget.js +++ b/src/components/views/rooms/LinkPreviewWidget.js @@ -60,13 +60,15 @@ module.exports = React.createClass({ }, componentDidMount: function() { - if (this.refs.description) + if (this.refs.description) { linkifyElement(this.refs.description, linkifyMatrix.options); + } }, componentDidUpdate: function() { - if (this.refs.description) + if (this.refs.description) { linkifyElement(this.refs.description, linkifyMatrix.options); + } }, componentWillUnmount: function() { @@ -116,7 +118,7 @@ module.exports = React.createClass({ if (image) { img =
-
+
; } return ( diff --git a/src/components/views/rooms/MemberDeviceInfo.js b/src/components/views/rooms/MemberDeviceInfo.js index 1e7850ab44..d4c00dda76 100644 --- a/src/components/views/rooms/MemberDeviceInfo.js +++ b/src/components/views/rooms/MemberDeviceInfo.js @@ -60,7 +60,7 @@ export default class MemberDeviceInfo extends React.Component { ); } -}; +} MemberDeviceInfo.displayName = 'MemberDeviceInfo'; MemberDeviceInfo.propTypes = { diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 1f4d392461..16a047f72d 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -64,7 +64,7 @@ module.exports = WithMatrixClient(React.createClass({ updating: 0, devicesLoading: true, devices: null, - } + }; }, componentWillMount: function() { @@ -202,7 +202,7 @@ module.exports = WithMatrixClient(React.createClass({ } var cancelled = false; - this._cancelDeviceList = function() { cancelled = true; } + this._cancelDeviceList = function() { cancelled = true; }; var client = this.props.matrixClient; var self = this; @@ -529,7 +529,7 @@ module.exports = WithMatrixClient(React.createClass({ }); }, - onMemberAvatarClick: function () { + onMemberAvatarClick: function() { var avatarUrl = this.props.member.user ? this.props.member.user.avatarUrl : this.props.member.events.member.getContent().avatar_url; if(!avatarUrl) return; @@ -620,7 +620,7 @@ module.exports = WithMatrixClient(React.createClass({
Start new chat
- + ; startChat =

Direct chats

@@ -654,7 +654,7 @@ module.exports = WithMatrixClient(React.createClass({ var giveOpLabel = this.state.isTargetMod ? "Revoke Moderator" : "Make Moderator"; giveModButton =
{giveOpLabel} -
+
; } // TODO: we should have an invite button if this MemberInfo is showing a user who isn't actually in the current room yet @@ -672,7 +672,7 @@ module.exports = WithMatrixClient(React.createClass({ {banButton} {giveModButton} - + ; } const memberName = this.props.member.name; diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index deedded4fa..bd386ed1bb 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -32,7 +32,7 @@ var SHARE_HISTORY_WARNING = Newly invited users will see the history of this room.
If you'd prefer invited users not to see messages that were sent before they joined,
turn off, 'Share message history with new users' in the settings for this room. - + ; module.exports = React.createClass({ displayName: 'MemberList', @@ -207,7 +207,7 @@ module.exports = React.createClass({ // For now we'll pretend this is any entity. It should probably be a separate tile. var EntityTile = sdk.getComponent("rooms.EntityTile"); var BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); - var text = "and " + overflowCount + " other" + (overflowCount > 1 ? "s" : "") + "..."; + var text = "and " + overflowCount + " other" + (overflowCount > 1 ? "s" : "") + "..."; return ( @@ -338,8 +338,8 @@ module.exports = React.createClass({ } memberList.push( - ) - }) + ); + }); } } diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js index cf79394228..5becef9ede 100644 --- a/src/components/views/rooms/MemberTile.js +++ b/src/components/views/rooms/MemberTile.js @@ -46,7 +46,7 @@ module.exports = React.createClass({ (this.user_last_modified_time === undefined || this.user_last_modified_time < nextProps.member.user.getLastModifiedTime()) ) { - return true + return true; } return false; }, diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index ee9c49d52a..4dfb95f5d0 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -367,7 +367,7 @@ export default class MessageComposer extends React.Component { ); } -}; +} MessageComposer.propTypes = { tabComplete: React.PropTypes.any, diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index b6af5a9f09..634939331f 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -443,12 +443,12 @@ export default class MessageComposerInput extends React.Component { selection = this.state.editorState.getSelection(); let modifyFn = { - bold: text => `**${text}**`, - italic: text => `*${text}*`, - underline: text => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug* - strike: text => `~~${text}~~`, - code: text => `\`${text}\``, - blockquote: text => text.split('\n').map(line => `> ${line}\n`).join(''), + 'bold': text => `**${text}**`, + 'italic': text => `*${text}*`, + 'underline': text => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug* + 'strike': text => `~~${text}~~`, + 'code': text => `\`${text}\``, + 'blockquote': text => text.split('\n').map(line => `> ${line}\n`).join(''), 'unordered-list-item': text => text.split('\n').map(line => `- ${line}\n`).join(''), 'ordered-list-item': text => text.split('\n').map((line, i) => `${i+1}. ${line}\n`).join(''), }[command]; @@ -462,8 +462,9 @@ export default class MessageComposerInput extends React.Component { } } - if (newState == null) + if (newState == null) { newState = RichUtils.handleKeyCommand(this.state.editorState, command); + } if (newState != null) { this.setEditorState(newState); @@ -665,7 +666,7 @@ export default class MessageComposerInput extends React.Component { const blockName = { 'code-block': 'code', - blockquote: 'quote', + 'blockquote': 'quote', 'unordered-list-item': 'bullet', 'ordered-list-item': 'numbullet', }; @@ -740,7 +741,7 @@ export default class MessageComposerInput extends React.Component { ); } -}; +} MessageComposerInput.propTypes = { tabComplete: React.PropTypes.any, diff --git a/src/components/views/rooms/MessageComposerInputOld.js b/src/components/views/rooms/MessageComposerInputOld.js index ed4533737f..c5d5f083c1 100644 --- a/src/components/views/rooms/MessageComposerInputOld.js +++ b/src/components/views/rooms/MessageComposerInputOld.js @@ -192,7 +192,7 @@ module.exports = React.createClass({ } }, - onKeyDown: function (ev) { + onKeyDown: function(ev) { if (ev.keyCode === KeyCode.ENTER && !ev.shiftKey) { var input = this.refs.textarea.value; if (input.length === 0) { diff --git a/src/components/views/rooms/ReadReceiptMarker.js b/src/components/views/rooms/ReadReceiptMarker.js index 47875bd7fb..c1fe4431bf 100644 --- a/src/components/views/rooms/ReadReceiptMarker.js +++ b/src/components/views/rooms/ReadReceiptMarker.js @@ -71,7 +71,7 @@ module.exports = React.createClass({ getDefaultProps: function() { return { leftOffset: 0, - } + }; }, getInitialState: function() { @@ -81,7 +81,7 @@ module.exports = React.createClass({ // position. return { suppressDisplay: !this.props.suppressAnimation, - } + }; }, componentWillUnmount: function() { diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index db3c7bb3d9..a066bfd337 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -182,8 +182,8 @@ module.exports = React.createClass({ 'm.room.name', user_id ); - save_button =
Save
- cancel_button =
Cancel
+ save_button =
Save
; + cancel_button =
Cancel
; } if (this.props.saving) { @@ -193,7 +193,7 @@ module.exports = React.createClass({ if (can_set_room_name) { var RoomNameEditor = sdk.getComponent("rooms.RoomNameEditor"); - name = + name = ; } else { var searchStatus; @@ -232,7 +232,7 @@ module.exports = React.createClass({ if (can_set_room_topic) { var RoomTopicEditor = sdk.getComponent("rooms.RoomTopicEditor"); - topic_el = + topic_el = ; } else { var topic; if (this.props.room) { @@ -301,7 +301,7 @@ module.exports = React.createClass({ rightPanel_buttons =
-
+ ; } var right_row; diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 3ced4102d4..c3ee5f1730 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -46,7 +46,7 @@ module.exports = React.createClass({ isLoadingLeftRooms: false, lists: {}, incomingCall: null, - } + }; }, componentWillMount: function() { @@ -338,7 +338,7 @@ module.exports = React.createClass({ // as this is used to calculate the CSS fixed top position for the stickies var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height; - var top = (incomingCallBox.parentElement.getBoundingClientRect().top + window.pageYOffset) + var top = (incomingCallBox.parentElement.getBoundingClientRect().top + window.pageYOffset); // Make sure we don't go too far up, if the headers aren't sticky top = (top < scrollAreaOffset) ? scrollAreaOffset : top; // make sure we don't go too far down, if the headers aren't sticky @@ -401,7 +401,7 @@ module.exports = React.createClass({ var stickyHeight = sticky.dataset.originalHeight; var stickyHeader = sticky.childNodes[0]; var topStuckHeight = stickyHeight * i; - var bottomStuckHeight = stickyHeight * (stickyWrappers.length - i) + var bottomStuckHeight = stickyHeight * (stickyWrappers.length - i); if (self.scrollAreaSufficient && stickyPosition < (scrollArea.scrollTop + topStuckHeight)) { // Top stickies @@ -520,7 +520,7 @@ module.exports = React.createClass({ collapsed={ self.props.collapsed } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } - onShowMoreRooms={ self.onShowMoreRooms } /> + onShowMoreRooms={ self.onShowMoreRooms } />; } }) } diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index b9912b6fcc..218bac48aa 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -58,7 +58,7 @@ module.exports = React.createClass({ getInitialState: function() { return { busy: false - } + }; }, componentWillMount: function() { @@ -96,7 +96,7 @@ module.exports = React.createClass({ emailMatchBlock =
Unable to ascertain that the address this invite was sent to matches one associated with your account. -
+ ; } else if (this.state.invitedEmailMxid != MatrixClientPeg.get().credentials.userId) { emailMatchBlock =
@@ -107,7 +107,7 @@ module.exports = React.createClass({ This invitation was sent to {this.props.invitedEmail}, which is not associated with this account.
You may wish to login with a different account, or add this email to this account.
- + ; } } joinBlock = ( diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 04ea05843d..e14a929ebe 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -252,7 +252,7 @@ module.exports = React.createClass({ return this.refs.url_preview_settings.saveSettings(); }, - saveEncryption: function () { + saveEncryption: function() { if (!this.refs.encrypt) { return q(); } var encrypt = this.refs.encrypt.checked; @@ -404,7 +404,7 @@ module.exports = React.createClass({ var cli = MatrixClientPeg.get(); var roomState = this.props.room.currentState; return (roomState.mayClientSendStateEvent("m.room.join_rules", cli) && - roomState.mayClientSendStateEvent("m.room.guest_access", cli)) + roomState.mayClientSendStateEvent("m.room.guest_access", cli)); }, onManageIntegrations(ev) { @@ -510,7 +510,7 @@ module.exports = React.createClass({ var UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings"); var EditableText = sdk.getComponent('elements.EditableText'); var PowerSelector = sdk.getComponent('elements.PowerSelector'); - var Loader = sdk.getComponent("elements.Spinner") + var Loader = sdk.getComponent("elements.Spinner"); var cli = MatrixClientPeg.get(); var roomState = this.props.room.currentState; @@ -557,7 +557,7 @@ module.exports = React.createClass({ ; } else { - userLevelsSection =
No users have specific privileges in this room.
+ userLevelsSection =
No users have specific privileges in this room.
; } var banned = this.props.room.getMembersWithMembership("ban"); @@ -635,7 +635,7 @@ module.exports = React.createClass({ ); })) : (self.state.tags && self.state.tags.join) ? self.state.tags.join(", ") : "" } - + ; } // If there is no history_visibility, it is assumed to be 'shared'. @@ -653,7 +653,7 @@ module.exports = React.createClass({ addressWarning =
To link to a room it must have an address. -
+ ; } var inviteGuestWarning; @@ -664,7 +664,7 @@ module.exports = React.createClass({ this.setState({ join_rule: "invite", guest_access: "can_join" }); e.preventDefault(); }}>Click here to fix. - + ; } var integrationsButton; diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 834a06d8e6..83b9cf3c6f 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -221,7 +221,7 @@ module.exports = React.createClass({ var avatarContainerClasses = classNames({ 'mx_RoomTile_avatar_container': true, 'mx_RoomTile_avatar_roomTagMenu': this.state.roomTagMenu, - }) + }); var badgeClasses = classNames({ 'mx_RoomTile_badge': true, diff --git a/src/components/views/rooms/SearchableEntityList.js b/src/components/views/rooms/SearchableEntityList.js index ccf733b985..50169edad5 100644 --- a/src/components/views/rooms/SearchableEntityList.js +++ b/src/components/views/rooms/SearchableEntityList.js @@ -118,7 +118,7 @@ var SearchableEntityList = React.createClass({ _createOverflowEntity: function(overflowCount, totalCount) { var EntityTile = sdk.getComponent("rooms.EntityTile"); var BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); - var text = "and " + overflowCount + " other" + (overflowCount > 1 ? "s" : "") + "..."; + var text = "and " + overflowCount + " other" + (overflowCount > 1 ? "s" : "") + "..."; return ( @@ -135,8 +135,8 @@ var SearchableEntityList = React.createClass({ { this.setState({ focused: true }) }} - onBlur= {() => { this.setState({ focused: false }) }} + onFocus= {() => { this.setState({ focused: true }); }} + onBlur= {() => { this.setState({ focused: false }); }} placeholder={this.props.searchPlaceholderText} /> ); diff --git a/src/components/views/rooms/SimpleRoomHeader.js b/src/components/views/rooms/SimpleRoomHeader.js index 7f2bb0048a..4c63be5b99 100644 --- a/src/components/views/rooms/SimpleRoomHeader.js +++ b/src/components/views/rooms/SimpleRoomHeader.js @@ -44,7 +44,7 @@ module.exports = React.createClass({ var cancelButton; if (this.props.onCancelClick) { - cancelButton =
Cancel
+ cancelButton =
Cancel
; } var showRhsButton; diff --git a/src/components/views/rooms/UserTile.js b/src/components/views/rooms/UserTile.js index 9608247d5e..66d736f3bb 100644 --- a/src/components/views/rooms/UserTile.js +++ b/src/components/views/rooms/UserTile.js @@ -38,7 +38,7 @@ module.exports = React.createClass({ var active = -1; // FIXME: make presence data update whenever User.presence changes... - active = user.lastActiveAgo ? + active = user.lastActiveAgo ? (Date.now() - (user.lastPresenceTs - user.lastActiveAgo)) : -1; var BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); diff --git a/src/components/views/settings/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js index 9b03aba1a3..de30b51f1b 100644 --- a/src/components/views/settings/ChangeAvatar.js +++ b/src/components/views/settings/ChangeAvatar.js @@ -49,7 +49,7 @@ module.exports = React.createClass({ return { avatarUrl: this.props.initialAvatarUrl, phase: this.Phases.Display, - } + }; }, componentWillReceiveProps: function(newProps) { @@ -120,7 +120,7 @@ module.exports = React.createClass({ var BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); // XXX: FIXME: once we track in the JS what our own displayname is(!) then use it here rather than ? avatarImg = + name='?' idName={ MatrixClientPeg.get().getUserIdLocalpart() } url={this.state.avatarUrl} />; } var uploadSection; @@ -130,7 +130,7 @@ module.exports = React.createClass({ Upload new: {this.state.errorText} - + ); } diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 1ef3eff205..a011d5262e 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -59,7 +59,7 @@ module.exports = React.createClass({ getInitialState: function() { return { phase: this.Phases.Edit - } + }; }, changePassword: function(old_password, new_password) { @@ -105,7 +105,7 @@ module.exports = React.createClass({ render: function() { var rowClassName = this.props.rowClassName; var rowLabelClassName = this.props.rowLabelClassName; - var rowInputClassName = this.props.rowInputClassName + var rowInputClassName = this.props.rowInputClassName; var buttonClassName = this.props.buttonClassName; switch (this.state.phase) { diff --git a/src/components/views/settings/DevicesPanel.js b/src/components/views/settings/DevicesPanel.js index f48d4bec85..28eee55527 100644 --- a/src/components/views/settings/DevicesPanel.js +++ b/src/components/views/settings/DevicesPanel.js @@ -88,7 +88,7 @@ export default class DevicesPanel extends React.Component { const removed_id = device.device_id; this.setState((state, props) => { const newDevices = state.devices.filter( - d => { return d.device_id != removed_id } + d => { return d.device_id != removed_id; } ); return { devices: newDevices }; }); @@ -98,7 +98,7 @@ export default class DevicesPanel extends React.Component { var DevicesPanelEntry = sdk.getComponent('settings.DevicesPanelEntry'); return ( {this._onDeviceDeleted(device)}} /> + onDeleted={()=>{this._onDeviceDeleted(device);}} /> ); } diff --git a/src/components/views/settings/DevicesPanelEntry.js b/src/components/views/settings/DevicesPanelEntry.js index f9f0e49a5e..4fa7d961ac 100644 --- a/src/components/views/settings/DevicesPanelEntry.js +++ b/src/components/views/settings/DevicesPanelEntry.js @@ -15,12 +15,9 @@ 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'; import Modal from '../../../Modal'; export default class DevicesPanelEntry extends React.Component { @@ -61,7 +58,7 @@ export default class DevicesPanelEntry extends React.Component { if (this._unmounted) { return; } if (error.httpStatus !== 401 || !error.data || !error.data.flows) { // doesn't look like an interactive-auth failure - throw e; + throw error; } // pop up an interactive auth dialog @@ -121,7 +118,7 @@ export default class DevicesPanelEntry extends React.Component { let deleteButton; if (this.state.deleteError) { - deleteButton =
{this.state.deleteError}
+ deleteButton =
{this.state.deleteError}
; } else { deleteButton = (
-
+
diff --git a/src/createRoom.js b/src/createRoom.js index a1512e23f6..2a23fb0787 100644 --- a/src/createRoom.js +++ b/src/createRoom.js @@ -45,7 +45,7 @@ function createRoom(opts) { Modal.createDialog(NeedToRegisterDialog, { title: "Please Register", description: "Guest users can't create new rooms. Please register to create room and start a chat." - }) + }); }, 0); return q(null); } @@ -78,7 +78,7 @@ function createRoom(opts) { let modal; setTimeout(()=>{ - modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner') + modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); }, 0); let roomId; diff --git a/src/dispatcher.js b/src/dispatcher.js index 22ef712b6c..ed0350fe54 100644 --- a/src/dispatcher.js +++ b/src/dispatcher.js @@ -40,7 +40,7 @@ class MatrixDispatcher extends flux.Dispatcher { setTimeout(super.dispatch.bind(this, payload), 0); } } -}; +} if (global.mxDispatcher === undefined) { global.mxDispatcher = new MatrixDispatcher(); diff --git a/src/extend.js b/src/extend.js index e39e9e9be9..cc3c33b2e7 100644 --- a/src/extend.js +++ b/src/extend.js @@ -23,4 +23,4 @@ module.exports = function(dest, src) { } } return dest; -} +}; diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js index e7d798c9ff..68f7a66bda 100644 --- a/src/linkify-matrix.js +++ b/src/linkify-matrix.js @@ -129,7 +129,7 @@ matrixLinkify.MATRIXTO_URL_PATTERN = "^(?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/ matrixLinkify.MATRIXTO_BASE_URL= "https://matrix.to"; matrixLinkify.options = { - events: function (href, type) { + events: function(href, type) { switch (type) { case "userid": return { @@ -146,7 +146,7 @@ matrixLinkify.options = { } }, - formatHref: function (href, type) { + formatHref: function(href, type) { switch (type) { case 'roomalias': case 'userid': diff --git a/src/utils/FormattingUtils.js b/src/utils/FormattingUtils.js index 44dcb2aa22..414784d101 100644 --- a/src/utils/FormattingUtils.js +++ b/src/utils/FormattingUtils.js @@ -25,4 +25,4 @@ export function formatCount(count) { if (count < 10000000) return (count / 1000000).toFixed(1) + "M"; if (count < 100000000) return (count / 1000000).toFixed(0) + "M"; return (count / 1000000000).toFixed(1) + "B"; // 10B is enough for anyone, right? :S -} \ No newline at end of file +} diff --git a/src/utils/MegolmExportEncryption.js b/src/utils/MegolmExportEncryption.js index 983ec2c75f..abae81e5ad 100644 --- a/src/utils/MegolmExportEncryption.js +++ b/src/utils/MegolmExportEncryption.js @@ -71,7 +71,7 @@ export function decryptMegolmKeyFile(data, password) { toVerify, ).then((isValid) => { if (!isValid) { - throw new Error('Authentication check failed: incorrect password?') + throw new Error('Authentication check failed: incorrect password?'); } return subtleCrypto.decrypt( diff --git a/src/wrappers/WithMatrixClient.js b/src/wrappers/WithMatrixClient.js index c9c9e7adb7..8e56d17dff 100644 --- a/src/wrappers/WithMatrixClient.js +++ b/src/wrappers/WithMatrixClient.js @@ -36,4 +36,4 @@ export default function(WrappedComponent) { return ; }, }); -}; +} From 0c5762b91da9f13402018f7a914e83b14c16678c Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 20 Jan 2017 17:51:35 +0100 Subject: [PATCH 103/260] Implement "someone is typing" avatars (#631) When users are typing, their avatars can be seen instead of "..." in the RoomView StatusBar --- src/components/structures/RoomStatusBar.js | 39 +++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index c6f2d6500b..618989a75c 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -19,6 +19,9 @@ var sdk = require('../../index'); var dis = require("../../dispatcher"); var WhoIsTyping = require("../../WhoIsTyping"); var MatrixClientPeg = require("../../MatrixClientPeg"); +const MemberAvatar = require("../views/avatars/MemberAvatar"); + +const TYPING_AVATARS_LIMIT = 2; module.exports = React.createClass({ displayName: 'RoomStatusBar', @@ -173,10 +176,8 @@ module.exports = React.createClass({ if (wantPlaceholder) { return ( -
- . - . - . +
+ {this._renderTypingIndicatorAvatars(TYPING_AVATARS_LIMIT)}
); } @@ -184,6 +185,36 @@ module.exports = React.createClass({ return null; }, + _renderTypingIndicatorAvatars: function(limit) { + let users = WhoIsTyping.usersTyping(this.props.room); + + let othersCount = Math.max(users.length - limit, 0); + users = users.slice(0, limit); + + let avatars = users.map((u, index) => { + let showInitial = othersCount === 0 && index === users.length - 1; + return ( + + ); + }); + + if (othersCount > 0) { + avatars.push( + + +{othersCount} + + ); + } + + return avatars; + }, // return suitable content for the main (text) part of the status bar. _getContent: function() { From 937f13d578c0b63b911d4f68728dd7e28cab60d5 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 20 Jan 2017 21:00:22 +0000 Subject: [PATCH 104/260] fix a bunch of dark-theme buttons --- src/components/structures/UserSettings.js | 8 +++---- .../views/room_settings/AliasSettings.js | 4 ++-- src/components/views/rooms/MessageComposer.js | 21 +++++++++++-------- .../views/rooms/MessageComposerInput.js | 2 +- src/components/views/rooms/RoomHeader.js | 2 +- 5 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 8f6473a181..4a1332be8c 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -607,8 +607,8 @@ module.exports = React.createClass({
-
- Remove +
+ Remove
); @@ -630,7 +630,7 @@ module.exports = React.createClass({ blurToCancel={ false } onValueChanged={ this.onAddThreepidClicked } />
-
+
Add
@@ -711,7 +711,7 @@ module.exports = React.createClass({
diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.js index 5935298634..6543f2a17d 100644 --- a/src/components/views/room_settings/AliasSettings.js +++ b/src/components/views/room_settings/AliasSettings.js @@ -281,7 +281,7 @@ module.exports = React.createClass({ onValueChanged={ self.onAliasChanged.bind(self, localDomain, i) } editable={ self.props.canSetAliases } initialValue={ alias } /> -
+
{ deleteButton }
@@ -297,7 +297,7 @@ module.exports = React.createClass({ placeholder={ "New address (e.g. #foo:" + localDomain + ")" } blurToCancel={ false } onValueChanged={ self.onAliasAdded } /> -
+
Add
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 4dfb95f5d0..113224666d 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -222,20 +222,22 @@ export default class MessageComposer extends React.Component {
); - let e2eimg, e2etitle; + let e2eImg, e2eTitle, e2eClass; if (MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId)) { // FIXME: show a /!\ if there are untrusted devices in the room... - e2eimg = 'img/e2e-verified.svg'; - e2etitle = 'Encrypted room'; + e2eImg = 'img/e2e-verified.svg'; + e2eTitle = 'Encrypted room'; + e2eClass = 'mx_MessageComposer_e2eIcon'; } else { - e2eimg = 'img/e2e-unencrypted.svg'; - e2etitle = 'Unencrypted room'; + e2eImg = 'img/e2e-unencrypted.svg'; + e2eTitle = 'Unencrypted room'; + e2eClass = 'mx_MessageComposer_e2eIcon mx_filterFlipColor'; } controls.push( - {e2etitle} ); var callButton, videoCallButton, hangupButton; @@ -331,6 +333,7 @@ export default class MessageComposer extends React.Component { const disabled = !this.state.inputState.isRichtextEnabled && 'underline' === name; const className = classNames("mx_MessageComposer_format_button", { mx_MessageComposer_format_button_disabled: disabled, + mx_filterFlipColor: true, }); return
: null diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 634939331f..bf936a2c13 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -719,7 +719,7 @@ export default class MessageComposerInput extends React.Component { selection={selection} />
- diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index a066bfd337..e345918f07 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -183,7 +183,7 @@ module.exports = React.createClass({ ); save_button =
Save
; - cancel_button =
Cancel
; + cancel_button =
Cancel
; } if (this.props.saving) { From 2e15e8f9b458c01612d60eeaa73fdd0a5308b9b5 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 21 Jan 2017 05:13:36 +0000 Subject: [PATCH 105/260] very barebones support for warning users when rooms contain unknown devices --- src/Resend.js | 13 +++- src/component-index.js | 2 + .../views/dialogs/UnknownDeviceDialog.js | 73 +++++++++++++++++++ .../views/rooms/MessageComposerInput.js | 10 ++- .../views/rooms/MessageComposerInputOld.js | 11 ++- .../views/rooms/SimpleRoomHeader.js | 2 +- 6 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 src/components/views/dialogs/UnknownDeviceDialog.js diff --git a/src/Resend.js b/src/Resend.js index ecf504e780..e67c812b7c 100644 --- a/src/Resend.js +++ b/src/Resend.js @@ -16,17 +16,26 @@ limitations under the License. var MatrixClientPeg = require('./MatrixClientPeg'); var dis = require('./dispatcher'); +var sdk = require('./index'); +var Modal = require('./Modal'); module.exports = { resend: function(event) { MatrixClientPeg.get().resendEvent( event, MatrixClientPeg.get().getRoom(event.getRoomId()) - ).done(function() { + ).done(function(res) { dis.dispatch({ action: 'message_sent', event: event }); - }, function() { + }, function(err) { + if (err.name === "UnknownDeviceError") { + var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog"); + Modal.createDialog(UnknownDeviceDialog, { + devices: err.devices + }); + } + dis.dispatch({ action: 'message_send_failed', event: event diff --git a/src/component-index.js b/src/component-index.js index e83de8739d..dcf96a6e56 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -89,6 +89,8 @@ import views$dialogs$SetDisplayNameDialog from './components/views/dialogs/SetDi views$dialogs$SetDisplayNameDialog && (module.exports.components['views.dialogs.SetDisplayNameDialog'] = views$dialogs$SetDisplayNameDialog); import views$dialogs$TextInputDialog from './components/views/dialogs/TextInputDialog'; views$dialogs$TextInputDialog && (module.exports.components['views.dialogs.TextInputDialog'] = views$dialogs$TextInputDialog); +import views$dialogs$UnknownDeviceDialog from './components/views/dialogs/UnknownDeviceDialog'; +views$dialogs$UnknownDeviceDialog && (module.exports.components['views.dialogs.UnknownDeviceDialog'] = views$dialogs$UnknownDeviceDialog); import views$elements$AddressSelector from './components/views/elements/AddressSelector'; views$elements$AddressSelector && (module.exports.components['views.elements.AddressSelector'] = views$elements$AddressSelector); import views$elements$AddressTile from './components/views/elements/AddressTile'; diff --git a/src/components/views/dialogs/UnknownDeviceDialog.js b/src/components/views/dialogs/UnknownDeviceDialog.js new file mode 100644 index 0000000000..aad310c855 --- /dev/null +++ b/src/components/views/dialogs/UnknownDeviceDialog.js @@ -0,0 +1,73 @@ +/* +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. +*/ + +var React = require("react"); +var sdk = require('../../../index'); +var MatrixClientPeg = require("../../../MatrixClientPeg"); + +module.exports = React.createClass({ + displayName: 'UnknownEventDialog', + + propTypes: { + devices: React.PropTypes.object.isRequired, + onFinished: React.PropTypes.func.isRequired, + }, + + onKeyDown: function(e) { + if (e.keyCode === 27) { // escape + e.stopPropagation(); + e.preventDefault(); + this.props.onFinished(false); + } + }, + + render: function() { + return ( +
+
+ Room contains unknown devices +
+
+

This room contains unknown devices which have not been verified.

+

We strongly recommend you verify them before continuing.

+

Unknown devices: +

    { + Object.keys(this.props.devices).map(userId=>{ + return
  • +

    { userId }:

    +
      + { + Object.keys(this.props.devices[userId]).map(deviceId=>{ + return
    • + { deviceId } ( { this.props.devices[userId][deviceId].getDisplayName() } ) +
    • + }) + } +
    +
  • + }) + }
+

+
+
+ +
+
+ ); + } +}); diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index bf936a2c13..4a6e36b854 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -553,11 +553,17 @@ export default class MessageComposerInput extends React.Component { sendMessagePromise = sendTextFn.call(this.client, this.props.room.roomId, contentText); } - sendMessagePromise.then(() => { + sendMessagePromise.then((res) => { dis.dispatch({ action: 'message_sent', }); - }, () => { + }, (err) => { + if (err.name === "UnknownDeviceError") { + var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog"); + Modal.createDialog(UnknownDeviceDialog, { + devices: err.devices + }); + } dis.dispatch({ action: 'message_send_failed', }); diff --git a/src/components/views/rooms/MessageComposerInputOld.js b/src/components/views/rooms/MessageComposerInputOld.js index c5d5f083c1..a5b1f6b786 100644 --- a/src/components/views/rooms/MessageComposerInputOld.js +++ b/src/components/views/rooms/MessageComposerInputOld.js @@ -337,11 +337,18 @@ module.exports = React.createClass({ MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText); } - sendMessagePromise.done(function() { + sendMessagePromise.done(function(res) { dis.dispatch({ action: 'message_sent' }); - }, function() { + }, function(err) { + if (err.name === "UnknownDeviceError") { + var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog"); + Modal.createDialog(UnknownDeviceDialog, { + devices: err.devices + }); + } + dis.dispatch({ action: 'message_send_failed' }); diff --git a/src/components/views/rooms/SimpleRoomHeader.js b/src/components/views/rooms/SimpleRoomHeader.js index 4c63be5b99..c6d09f49ee 100644 --- a/src/components/views/rooms/SimpleRoomHeader.js +++ b/src/components/views/rooms/SimpleRoomHeader.js @@ -44,7 +44,7 @@ module.exports = React.createClass({ var cancelButton; if (this.props.onCancelClick) { - cancelButton =
Cancel
; + cancelButton =
Cancel
; } var showRhsButton; From 3071fc0ddcdf6065cf4e64b647c21ade53722173 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 21 Jan 2017 17:39:31 +0000 Subject: [PATCH 106/260] UI for blacklisting unverified devices per-room & globally (written blind; untested as yet) --- src/UserSettingsStore.js | 16 +++++++ src/components/structures/UserSettings.js | 36 ++++++++++++++++ .../views/dialogs/UnknownDeviceDialog.js | 2 + src/components/views/rooms/RoomSettings.js | 43 +++++++++++++++++-- 4 files changed, 94 insertions(+), 3 deletions(-) diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index e5dba62ee7..f2f99603d6 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -149,6 +149,22 @@ module.exports = { return MatrixClientPeg.get().setAccountData("im.vector.web.settings", settings); }, + getLocalSettings: function() { + return localStorage.getItem('mx_local_settings'); + }, + + getLocalSetting: function(type, defaultValue = null) { + var settings = this.getLocalSettings(); + return settings.hasOwnProperty(type) ? settings[type] : null; + }, + + setLocalSetting: function(type, value) { + var settings = this.getLocalSettings(); + settings[type] = value; + // FIXME: handle errors + localStorage.setItem('mx_local_settings', settings); + }, + isFeatureEnabled: function(feature: string): boolean { // Disable labs for guests. if (MatrixClientPeg.get().isGuest()) return false; diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 4a1332be8c..a262431f2b 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -57,6 +57,18 @@ const SETTINGS_LABELS = [ */ ]; +const CRYPTO_SETTINGS_LABELS = [ + { + id: 'blacklistUnverifiedDevices', + label: 'Never send encrypted messages to unverified devices', + }, + // XXX: this is here for documentation; the actual setting is managed via RoomSettings + // { + // id: 'blacklistUnverifiedDevicesPerRoom' + // label: 'Never send encrypted messages to unverified devices in this room', + // } +]; + // Enumerate the available themes, with a nice human text label. // 'id' gives the key name in the im.vector.web.settings account data event // 'value' is the value for that key in the event @@ -146,6 +158,8 @@ module.exports = React.createClass({ syncedSettings.theme = 'light'; } this._syncedSettings = syncedSettings; + + this._localSettings = UserSettingsStore.getLocalSettings(); }, componentDidMount: function() { @@ -471,10 +485,32 @@ module.exports = React.createClass({
  • {identityKey}
  • + { CRYPTO_SETTINGS_LABELS.map( this._renderLocalSetting ) } ); }, + _renderLocalSetting: function(setting) { + const client = MatrixClientPeg.get(); + return
    + { + UserSettingsStore.setLocalSetting(setting.id, e.target.checked) + if (setting.id === 'blacklistUnverifiedDevices') { // XXX: this is a bit ugly + client.setGlobalBlacklistUnverifiedDevices(e.target.checked); + } + } + } + /> + +
    ; + }, + _renderDevicesPanel: function() { var DevicesPanel = sdk.getComponent('settings.DevicesPanel'); return ( diff --git a/src/components/views/dialogs/UnknownDeviceDialog.js b/src/components/views/dialogs/UnknownDeviceDialog.js index aad310c855..8e04414b9c 100644 --- a/src/components/views/dialogs/UnknownDeviceDialog.js +++ b/src/components/views/dialogs/UnknownDeviceDialog.js @@ -69,5 +69,7 @@ module.exports = React.createClass({ ); + // XXX: do we want to give the user the option to enable blacklistUnverifiedDevices for this room (or globally) at this point? + // It feels like confused users will likely turn it on and then disappear in a cloud of UISIs... } }); diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index e14a929ebe..7283201858 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -228,11 +228,13 @@ module.exports = React.createClass({ } // encryption - p = this.saveEncryption(); + p = this.saveEnableEncryption(); if (!q.isFulfilled(p)) { promises.push(p); } + this.saveBlacklistUnverifiedDevicesPerRoom(); + console.log("Performing %s operations: %s", promises.length, JSON.stringify(promises)); return promises; }, @@ -252,11 +254,11 @@ module.exports = React.createClass({ return this.refs.url_preview_settings.saveSettings(); }, - saveEncryption: function() { + saveEnableEncryption: function() { if (!this.refs.encrypt) { return q(); } var encrypt = this.refs.encrypt.checked; - if (!encrypt) { return q(); } + if (encrypt) { return q(); } var roomId = this.props.room.roomId; return MatrixClientPeg.get().sendStateEvent( @@ -265,6 +267,29 @@ module.exports = React.createClass({ ); }, + saveBlacklistUnverifiedDevicesPerRoom: function() { + if (!this.refs.blacklistUnverified) return; + if (this._isRoomBlacklistUnverified() !== this.refs.blacklistUnverified.checked) { + this._setRoomBlacklistUnverified(this.refs.blacklistUnverified.checked); + } + }, + + _isRoomBlacklistUnverified: function() { + var blacklistUnverifiedDevicesPerRoom = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevicesPerRoom; + if (blacklistUnverifiedDevicesPerRoom) { + return blacklistUnverifiedDevicesPerRoom[this.props.room.roomId]; + } + return false; + }, + + _setRoomBlacklistUnverified: function(value) { + var blacklistUnverifiedDevicesPerRoom = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevicesPerRoom; + blacklistUnverifiedDevicesPerRoom[this.props.room.roomId] = value; + UserSettingsStore.setLocalSettings('blacklistUnverifiedDevicesPerRoom', blacklistUnverifiedDevicesPerRoom); + + this.props.room.setBlacklistUnverifiedDevices(value); + }, + _hasDiff: function(strA, strB) { // treat undefined as an empty string because other components may blindly // call setName("") when there has been no diff made to the name! @@ -477,6 +502,16 @@ module.exports = React.createClass({ var cli = MatrixClientPeg.get(); var roomState = this.props.room.currentState; var isEncrypted = cli.isRoomEncrypted(this.props.room.roomId); + var isGlobalBlacklistUnverified = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevices; + var isRoomBlacklistUnverified = this._isRoomBlacklistUnverified(); + + var settings = + ; if (!isEncrypted && roomState.mayClientSendStateEvent("m.room.encryption", cli)) { @@ -486,6 +521,7 @@ module.exports = React.createClass({ Enable encryption (warning: cannot be disabled again!) + { settings } ); } else { @@ -497,6 +533,7 @@ module.exports = React.createClass({ } Encryption is { isEncrypted ? "" : "not " } enabled in this room. + { settings } ); } }, From c618880af96c560e0434be0d842e2ef47d306887 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 21 Jan 2017 17:43:46 +0000 Subject: [PATCH 107/260] oops --- src/components/views/rooms/RoomSettings.js | 32 ++++++++++++---------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 7283201858..a5782dad7b 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -516,24 +516,28 @@ module.exports = React.createClass({ if (!isEncrypted && roomState.mayClientSendStateEvent("m.room.encryption", cli)) { return ( - - { settings } +
    + + { settings } +
    ); } else { return ( - - { settings } +
    + + { settings } +
    ); } }, From 071e364be29cfafab94bf0442370fa0a955f3197 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 21 Jan 2017 17:56:48 +0000 Subject: [PATCH 108/260] improve the verify warning if blacklisting is unabled --- src/Resend.js | 3 ++- src/components/views/dialogs/UnknownDeviceDialog.js | 12 +++++++++++- src/components/views/rooms/MessageComposerInput.js | 3 ++- .../views/rooms/MessageComposerInputOld.js | 3 ++- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Resend.js b/src/Resend.js index e67c812b7c..21da1c173b 100644 --- a/src/Resend.js +++ b/src/Resend.js @@ -32,7 +32,8 @@ module.exports = { if (err.name === "UnknownDeviceError") { var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog"); Modal.createDialog(UnknownDeviceDialog, { - devices: err.devices + devices: err.devices, + room: MatrixClientPeg.get().getRoom(event.getRoomId()), }); } diff --git a/src/components/views/dialogs/UnknownDeviceDialog.js b/src/components/views/dialogs/UnknownDeviceDialog.js index 8e04414b9c..bd23427d2b 100644 --- a/src/components/views/dialogs/UnknownDeviceDialog.js +++ b/src/components/views/dialogs/UnknownDeviceDialog.js @@ -22,6 +22,7 @@ module.exports = React.createClass({ displayName: 'UnknownEventDialog', propTypes: { + room: React.PropTypes.object.isRequired, devices: React.PropTypes.object.isRequired, onFinished: React.PropTypes.func.isRequired, }, @@ -35,6 +36,15 @@ module.exports = React.createClass({ }, render: function() { + var client = MatrixClientPeg.get(); + var warning; + if (client.getGlobalBlacklistUnverifiedDevices() || room.getBlacklistUnverifiedDevices()) { + warning =

    You are currently blacklisting unverified devices; to send messages to these devices you must verify them.

    ; + } + else { + warning =

    We strongly recommend you verify them before continuing.

    ; + } + return (
    @@ -42,7 +52,7 @@ module.exports = React.createClass({

    This room contains unknown devices which have not been verified.

    -

    We strongly recommend you verify them before continuing.

    + { warning }

    Unknown devices:

      { Object.keys(this.props.devices).map(userId=>{ diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 4a6e36b854..54e4894adf 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -561,7 +561,8 @@ export default class MessageComposerInput extends React.Component { if (err.name === "UnknownDeviceError") { var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog"); Modal.createDialog(UnknownDeviceDialog, { - devices: err.devices + devices: err.devices, + room: this.props.room, }); } dis.dispatch({ diff --git a/src/components/views/rooms/MessageComposerInputOld.js b/src/components/views/rooms/MessageComposerInputOld.js index a5b1f6b786..7fd3e0b427 100644 --- a/src/components/views/rooms/MessageComposerInputOld.js +++ b/src/components/views/rooms/MessageComposerInputOld.js @@ -345,7 +345,8 @@ module.exports = React.createClass({ if (err.name === "UnknownDeviceError") { var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog"); Modal.createDialog(UnknownDeviceDialog, { - devices: err.devices + devices: err.devices, + room: this.props.room, }); } From 532f4e59c904afe4967ec80dbff3d57b6be0e88a Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 21 Jan 2017 19:06:15 +0100 Subject: [PATCH 109/260] literally blindly add verification buttons --- src/components/views/dialogs/UnknownDeviceDialog.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/UnknownDeviceDialog.js b/src/components/views/dialogs/UnknownDeviceDialog.js index bd23427d2b..a2e7bd13fb 100644 --- a/src/components/views/dialogs/UnknownDeviceDialog.js +++ b/src/components/views/dialogs/UnknownDeviceDialog.js @@ -37,8 +37,9 @@ module.exports = React.createClass({ render: function() { var client = MatrixClientPeg.get(); + var blacklistUnverified = (client.getGlobalBlacklistUnverifiedDevices() || room.getBlacklistUnverifiedDevices()); var warning; - if (client.getGlobalBlacklistUnverifiedDevices() || room.getBlacklistUnverifiedDevices()) { + if (blacklistUnverified) { warning =

      You are currently blacklisting unverified devices; to send messages to these devices you must verify them.

      ; } else { @@ -61,8 +62,11 @@ module.exports = React.createClass({
        { Object.keys(this.props.devices[userId]).map(deviceId=>{ + var DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons'); + var device = this.props.devices[userId][deviceId]; + var buttons = return
      • - { deviceId } ( { this.props.devices[userId][deviceId].getDisplayName() } ) + { deviceId } ( { device.getDisplayName() } ) { buttons }
      • }) } From 39c122fe4f09d048b5237dd539c66b09855cad76 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 21 Jan 2017 22:27:55 +0100 Subject: [PATCH 110/260] fix local storage idiocy --- src/UserSettingsStore.js | 5 +++-- src/components/structures/UserSettings.js | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index f2f99603d6..d7d3e7bc7a 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -150,7 +150,8 @@ module.exports = { }, getLocalSettings: function() { - return localStorage.getItem('mx_local_settings'); + var localSettingsString = localStorage.getItem('mx_local_settings') || '{}'; + return JSON.parse(localSettingsString); }, getLocalSetting: function(type, defaultValue = null) { @@ -162,7 +163,7 @@ module.exports = { var settings = this.getLocalSettings(); settings[type] = value; // FIXME: handle errors - localStorage.setItem('mx_local_settings', settings); + localStorage.setItem('mx_local_settings', JSON.stringify(settings)); }, isFeatureEnabled: function(feature: string): boolean { diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index a262431f2b..1dd30b679c 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -485,7 +485,9 @@ module.exports = React.createClass({
      • {identityKey}

    - { CRYPTO_SETTINGS_LABELS.map( this._renderLocalSetting ) } +
    + { CRYPTO_SETTINGS_LABELS.map( this._renderLocalSetting ) } +
    ); }, From d9c0513ee28f007353b934562733b17a269e527f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 21 Jan 2017 22:49:29 +0100 Subject: [PATCH 111/260] make it work --- src/components/views/rooms/RoomSettings.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index a5782dad7b..548809e9ad 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -24,6 +24,8 @@ var ObjectUtils = require("../../../ObjectUtils"); var dis = require("../../../dispatcher"); var ScalarAuthClient = require("../../../ScalarAuthClient"); var ScalarMessaging = require('../../../ScalarMessaging'); +var UserSettingsStore = require('../../../UserSettingsStore'); + // parse a string as an integer; if the input is undefined, or cannot be parsed // as an integer, return a default. @@ -283,9 +285,9 @@ module.exports = React.createClass({ }, _setRoomBlacklistUnverified: function(value) { - var blacklistUnverifiedDevicesPerRoom = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevicesPerRoom; + var blacklistUnverifiedDevicesPerRoom = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevicesPerRoom || {}; blacklistUnverifiedDevicesPerRoom[this.props.room.roomId] = value; - UserSettingsStore.setLocalSettings('blacklistUnverifiedDevicesPerRoom', blacklistUnverifiedDevicesPerRoom); + UserSettingsStore.setLocalSetting('blacklistUnverifiedDevicesPerRoom', blacklistUnverifiedDevicesPerRoom); this.props.room.setBlacklistUnverifiedDevices(value); }, @@ -508,8 +510,8 @@ module.exports = React.createClass({ var settings = ; From 7bc3fc86961bf11e1840ff5f9978c17dc7eb03d7 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 22 Jan 2017 01:28:43 +0100 Subject: [PATCH 112/260] make UnknownDeviceDialog work again, other than the mess of vector-im/vector-web#3020 --- src/components/structures/UserSettings.js | 2 +- .../views/dialogs/UnknownDeviceDialog.js | 49 ++++++++++--------- .../views/elements/DeviceVerifyButtons.js | 4 +- .../views/rooms/MessageComposerInput.js | 3 +- .../views/rooms/MessageComposerInputOld.js | 3 +- src/components/views/rooms/RoomSettings.js | 2 +- 6 files changed, 33 insertions(+), 30 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 1dd30b679c..00219e3bb2 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -60,7 +60,7 @@ const SETTINGS_LABELS = [ const CRYPTO_SETTINGS_LABELS = [ { id: 'blacklistUnverifiedDevices', - label: 'Never send encrypted messages to unverified devices', + label: 'Never send encrypted messages to unverified devices from this device', }, // XXX: this is here for documentation; the actual setting is managed via RoomSettings // { diff --git a/src/components/views/dialogs/UnknownDeviceDialog.js b/src/components/views/dialogs/UnknownDeviceDialog.js index a2e7bd13fb..656f831ba1 100644 --- a/src/components/views/dialogs/UnknownDeviceDialog.js +++ b/src/components/views/dialogs/UnknownDeviceDialog.js @@ -36,14 +36,15 @@ module.exports = React.createClass({ }, render: function() { + var DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons'); var client = MatrixClientPeg.get(); - var blacklistUnverified = (client.getGlobalBlacklistUnverifiedDevices() || room.getBlacklistUnverifiedDevices()); + var blacklistUnverified = client.getGlobalBlacklistUnverifiedDevices() || this.props.room.getBlacklistUnverifiedDevices(); var warning; if (blacklistUnverified) { - warning =

    You are currently blacklisting unverified devices; to send messages to these devices you must verify them.

    ; + warning =

    You are currently blacklisting unverified devices; to send messages to these devices you must verify them.

    } else { - warning =

    We strongly recommend you verify them before continuing.

    ; + warning =

    We strongly recommend you verify them before continuing.

    } return ( @@ -54,27 +55,27 @@ module.exports = React.createClass({

    This room contains unknown devices which have not been verified.

    { warning } -

    Unknown devices: -

      { - Object.keys(this.props.devices).map(userId=>{ - return
    • -

      { userId }:

      -
        - { - Object.keys(this.props.devices[userId]).map(deviceId=>{ - var DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons'); - var device = this.props.devices[userId][deviceId]; - var buttons = - return
      • - { deviceId } ( { device.getDisplayName() } ) { buttons } -
      • - }) - } -
      -
    • - }) - }
    -

    + Unknown devices: +
      { + Object.keys(this.props.devices).map(userId=>{ + return
    • +

      { userId }:

      +
        + { + Object.keys(this.props.devices[userId]).map(deviceId=>{ + var device = this.props.devices[userId][deviceId]; + var buttons = + return
      • + { buttons } + { deviceId }
        + { device.getDisplayName() } +
      • + }) + } +
      +
    • + }) + }
    ); } + let statusBarAreaClass = "mx_RoomView_statusArea mx_fadable"; + if (this.state.statusBarVisible) { + statusBarAreaClass += " mx_RoomView_statusArea_expanded"; + } return (
    @@ -1691,7 +1711,7 @@ module.exports = React.createClass({ { topUnreadMessagesBar } { messagePanel } { searchResultsPanel } -
    +
    { statusBar } From 94fdd2bcd286760613d66e2675031b1a57bd3c3a Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 23 Jan 2017 15:41:33 +0000 Subject: [PATCH 117/260] lint all --- jenkins.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jenkins.sh b/jenkins.sh index 3b4e31fd7f..c1fba19e94 100755 --- a/jenkins.sh +++ b/jenkins.sh @@ -19,7 +19,7 @@ npm install npm run test # run eslint -npm run lint -- -f checkstyle -o eslint.xml || true +npm run lintall -- -f checkstyle -o eslint.xml || true # delete the old tarball, if it exists rm -f matrix-react-sdk-*.tgz From 1e77b2eba33f8b084dd43030d1929c50cb870cc0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 23 Jan 2017 16:19:29 +0000 Subject: [PATCH 118/260] Bundle eslint config --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 8e1ead4b9e..0a8c09f984 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "license": "Apache-2.0", "main": "lib/index.js", "files": [ + ".eslintrc.js", "CHANGELOG.md", "CONTRIBUTING.rst", "LICENSE", From 905508f1ceeda9466da1ea23d9318117c68f1502 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 23 Jan 2017 17:33:09 +0000 Subject: [PATCH 119/260] Correctly get the path of the js-sdk .eslintrc.js So we work correctly when we're included in another module --- .eslintrc.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index e41106d695..d5684e21a7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,15 @@ +const path = require('path'); + +// get the path of the js-sdk so we can extend the config +// eslint supports loading extended configs by module, +// but only if they come from a module that starts with eslint-config- +// So we load the filename directly (and it could be in node_modules/ +// or or ../node_modules/ etc) +const matrixJsSdkPath = path.dirname(require.resolve('matrix-js-sdk')); + module.exports = { parser: "babel-eslint", - extends: ["./node_modules/matrix-js-sdk/.eslintrc.js"], + extends: [matrixJsSdkPath + "/.eslintrc.js"], plugins: [ "react", "flowtype", From c0de0870ed3f3262b55e6d2057c934ef867cded8 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 24 Jan 2017 13:31:52 +0000 Subject: [PATCH 120/260] Some sarcastic comments --- src/dispatcher.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/dispatcher.js b/src/dispatcher.js index ed0350fe54..11c79f58ee 100644 --- a/src/dispatcher.js +++ b/src/dispatcher.js @@ -42,6 +42,9 @@ class MatrixDispatcher extends flux.Dispatcher { } } +// XXX this is a big anti-pattern, and makes testing hard. Because dispatches +// happen asynchronously, it is possible for actions dispatched in one thread +// to arrive in another, with *hilarious* consequences. if (global.mxDispatcher === undefined) { global.mxDispatcher = new MatrixDispatcher(); } From 5f24fc3e5da798b945adf245a21ae22df0d8b0ed Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 24 Jan 2017 13:56:22 +0000 Subject: [PATCH 121/260] Fix merge fail --- src/components/views/elements/AddressSelector.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/AddressSelector.js b/src/components/views/elements/AddressSelector.js index f9d1c2e28d..a14a8ffdff 100644 --- a/src/components/views/elements/AddressSelector.js +++ b/src/components/views/elements/AddressSelector.js @@ -123,7 +123,7 @@ module.exports = React.createClass({ // Saving the addressListElement so we can use it to work out, in the componentDidUpdate // method, how far to scroll when using the arrow keys addressList.push( -
    { this.addressListElement = ref; }} > +
    { this.addressListElement = ref; }} >
    ); From 5091bab657f8c043b96634fb7a236b795a06894a Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 24 Jan 2017 13:59:02 +0000 Subject: [PATCH 122/260] Fix failed merge #2 --- src/components/views/elements/AddressSelector.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/AddressSelector.js b/src/components/views/elements/AddressSelector.js index a14a8ffdff..361705c6d5 100644 --- a/src/components/views/elements/AddressSelector.js +++ b/src/components/views/elements/AddressSelector.js @@ -124,7 +124,7 @@ module.exports = React.createClass({ // method, how far to scroll when using the arrow keys addressList.push(
    { this.addressListElement = ref; }} > - +
    ); } From ce7434984bdb181a4a45696f0d14f3c9c4a807af Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 24 Jan 2017 14:32:52 +0000 Subject: [PATCH 123/260] Expand timeline in situations when _getIndicator not null The status bar will now be expanded when: - props.numUnreadMessages - !props.atEndOfLiveTimeline - props.hasActiveCall --- src/components/structures/RoomStatusBar.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 0eb50161ec..c80c4db7cc 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -135,7 +135,11 @@ module.exports = React.createClass({ // changed - so we use '0' to indicate normal size, and other values to // indicate other sizes. _getSize: function(state, props) { - if (state.syncState === "ERROR" || state.whoisTypingString) { + if (state.syncState === "ERROR" || + state.whoisTypingString || + props.numUnreadMessages || + !props.atEndOfLiveTimeline || + props.hasActiveCall) { return STATUS_BAR_EXPANDED; } else if (props.tabCompleteEntries) { return STATUS_BAR_HIDDEN; From 3b9a1121369e48630af8a1879af5991a8cdee180 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 24 Jan 2017 14:47:11 +0000 Subject: [PATCH 124/260] Add bug report UI --- src/component-index.js | 2 + src/components/structures/UserSettings.js | 21 +++ .../views/dialogs/BugReportDialog.js | 122 ++++++++++++++++++ 3 files changed, 145 insertions(+) create mode 100644 src/components/views/dialogs/BugReportDialog.js diff --git a/src/component-index.js b/src/component-index.js index e83de8739d..f03a3e39ff 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -71,6 +71,8 @@ import views$create_room$Presets from './components/views/create_room/Presets'; views$create_room$Presets && (module.exports.components['views.create_room.Presets'] = views$create_room$Presets); import views$create_room$RoomAlias from './components/views/create_room/RoomAlias'; views$create_room$RoomAlias && (module.exports.components['views.create_room.RoomAlias'] = views$create_room$RoomAlias); +import views$dialogs$BugReportDialog from './components/views/dialogs/BugReportDialog'; +views$dialogs$BugReportDialog && (module.exports.components['views.dialogs.BugReportDialog'] = views$dialogs$BugReportDialog); import views$dialogs$ChatInviteDialog from './components/views/dialogs/ChatInviteDialog'; views$dialogs$ChatInviteDialog && (module.exports.components['views.dialogs.ChatInviteDialog'] = views$dialogs$ChatInviteDialog); import views$dialogs$DeactivateAccountDialog from './components/views/dialogs/DeactivateAccountDialog'; diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 4a1332be8c..8294eee6df 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -369,6 +369,11 @@ module.exports = React.createClass({ Modal.createDialog(DeactivateAccountDialog, {}); }, + _onBugReportClicked: function() { + const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog"); + Modal.createDialog(BugReportDialog, {}); + }, + _onInviteStateChange: function(event, member, oldMembership) { if (member.userId === this._me && oldMembership === "invite") { this.forceUpdate(); @@ -485,6 +490,21 @@ module.exports = React.createClass({ ); }, + _renderBugReport: function() { + // TODO: If there is no bug report endpoint, hide this. + return ( +
    +

    Bug Report

    +
    +

    Found a bug?

    + +
    +
    + ); + }, + _renderLabs: function() { // default to enabled if undefined if (this.props.enableLabs === false) return null; @@ -738,6 +758,7 @@ module.exports = React.createClass({ {this._renderDevicesPanel()} {this._renderCryptoInfo()} {this._renderBulkOptions()} + {this._renderBugReport()}

    Advanced

    diff --git a/src/components/views/dialogs/BugReportDialog.js b/src/components/views/dialogs/BugReportDialog.js new file mode 100644 index 0000000000..eafafb56c6 --- /dev/null +++ b/src/components/views/dialogs/BugReportDialog.js @@ -0,0 +1,122 @@ +/* +Copyright 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import sdk from '../../../index'; + +export default class BugReportDialog extends React.Component { + constructor(props, context) { + super(props, context); + this.state = { + sendLogs: true, + busy: false, + err: null, + text: "", + }; + this._onSubmit = this._onSubmit.bind(this); + this._onCancel = this._onCancel.bind(this); + this._onTextChange = this._onTextChange.bind(this); + this._onSendLogsChange = this._onSendLogsChange.bind(this); + } + + _onCancel(ev) { + this.props.onFinished(false); + } + + _onSubmit(ev) { + const sendLogs = this.state.sendLogs; + const userText = this.state.text; + if (!sendLogs && userText.trim().length === 0) { + this.setState({ + err: "Please describe the bug and/or send logs." + }); + return; + } + // TODO: Make the HTTP hit + this.setState({ busy: true, err: null }); + setTimeout(() => { + this.setState({ busy: false, err: "No bug report endpoint." }); + }, 1000); + } + + _onTextChange(ev) { + this.setState({ text: ev.target.value }); + } + + _onSendLogsChange(ev) { + this.setState({ sendLogs: ev.target.checked }); + } + + render() { + const Loader = sdk.getComponent("elements.Spinner"); + + let error = null; + if (this.state.err) { + error =
    + {this.state.err} +
    ; + } + + const okLabel = this.state.busy ? : 'Send'; + + let cancelButton = null; + if (!this.state.busy) { + cancelButton = ; + } + + return ( +
    +
    + Report a bug +
    +
    +

    Please describe the bug. What did you do? What did you expect to happen? + What actually happened?

    +