From 328d57f06320d839992a6f4eff0fae0eac4c5dfe Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 29 Oct 2018 22:57:33 +0000 Subject: [PATCH 01/21] Remove temporary account_deactivation_preferences Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/dialogs/DeactivateAccountDialog.js | 41 +------------------ 1 file changed, 1 insertion(+), 40 deletions(-) diff --git a/src/components/views/dialogs/DeactivateAccountDialog.js b/src/components/views/dialogs/DeactivateAccountDialog.js index 761a1e4209..6e87a816bb 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.js +++ b/src/components/views/dialogs/DeactivateAccountDialog.js @@ -35,19 +35,10 @@ export default class DeactivateAccountDialog extends React.Component { this._onPasswordFieldChange = this._onPasswordFieldChange.bind(this); this._onEraseFieldChange = this._onEraseFieldChange.bind(this); - const deactivationPreferences = - MatrixClientPeg.get().getAccountData('im.riot.account_deactivation_preferences'); - - const shouldErase = ( - deactivationPreferences && - deactivationPreferences.getContent() && - deactivationPreferences.getContent().shouldErase - ) || false; - this.state = { confirmButtonEnabled: false, busy: false, - shouldErase, + shouldErase: false, errStr: null, }; } @@ -67,36 +58,6 @@ export default class DeactivateAccountDialog extends React.Component { async _onOk() { this.setState({busy: true}); - // Before we deactivate the account insert an event into - // the user's account data indicating that they wish to be - // erased from the homeserver. - // - // We do this because the API for erasing after deactivation - // might not be supported by the connected homeserver. Leaving - // an indication in account data is only best-effort, and - // in the worse case, the HS maintainer would have to run a - // script to erase deactivated accounts that have shouldErase - // set to true in im.riot.account_deactivation_preferences. - // - // Note: The preferences are scoped to Riot, hence the - // "im.riot..." event type. - // - // Note: This may have already been set on previous attempts - // where, for example, the user entered the wrong password. - // This is fine because the UI always indicates the preference - // prior to us calling `deactivateAccount`. - try { - await MatrixClientPeg.get().setAccountData('im.riot.account_deactivation_preferences', { - shouldErase: this.state.shouldErase, - }); - } catch (err) { - this.setState({ - busy: false, - errStr: _t('Failed to indicate account erasure'), - }); - return; - } - try { // This assumes that the HS requires password UI auth // for this endpoint. In reality it could be any UI auth. From a2b825ba92b8b0751640a23e6573513b02dff97e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 5 Dec 2018 11:52:10 -0700 Subject: [PATCH 02/21] Sort translations by file name This keeps the strings close together and roughly in the same area as the others, and makes it easier to maintain the translation file. --- scripts/gen-i18n.js | 16 ++++++++++++++-- src/i18n/strings/en_EN.json | 22 +++++++++++----------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/scripts/gen-i18n.js b/scripts/gen-i18n.js index a1a2e6f7c5..3d3d5af116 100755 --- a/scripts/gen-i18n.js +++ b/scripts/gen-i18n.js @@ -222,10 +222,21 @@ const translatables = new Set(); const walkOpts = { listeners: { + names: function(root, nodeNamesArray) { + // Sort the names case insensitively and alphabetically to + // maintain some sense of order between the different strings. + nodeNamesArray.sort((a, b) => { + a = a.toLowerCase(); + b = b.toLowerCase(); + if (a > b) return 1; + if (a < b) return -1; + return 0; + }); + }, file: function(root, fileStats, next) { const fullPath = path.join(root, fileStats.name); - let ltrs; + let trs; if (fileStats.name.endsWith('.js')) { trs = getTranslationsJs(fullPath); } else if (fileStats.name.endsWith('.html')) { @@ -235,7 +246,8 @@ const walkOpts = { } console.log(`${fullPath} (${trs.size} strings)`); for (const tr of trs.values()) { - translatables.add(tr); + // Convert DOS line endings to unix + translatables.add(tr.replace(/\r\n/g, "\n")); } }, } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d137d2fff4..846e66bfcd 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -43,6 +43,10 @@ "The file '%(fileName)s' failed to upload": "The file '%(fileName)s' failed to upload", "The file '%(fileName)s' exceeds this home server's size limit for uploads": "The file '%(fileName)s' exceeds this home server's size limit for uploads", "Upload Failed": "Upload Failed", + "Failure to create room": "Failure to create room", + "Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.", + "Send anyway": "Send anyway", + "Send": "Send", "Sun": "Sun", "Mon": "Mon", "Tue": "Tue", @@ -82,6 +86,7 @@ "Failed to invite users to community": "Failed to invite users to community", "Failed to invite users to %(groupId)s": "Failed to invite users to %(groupId)s", "Failed to add the following rooms to %(groupId)s:": "Failed to add the following rooms to %(groupId)s:", + "Unnamed Room": "Unnamed Room", "Error": "Error", "Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.", "Dismiss": "Dismiss", @@ -210,11 +215,6 @@ "%(names)s and %(count)s others are typing|other": "%(names)s and %(count)s others are typing", "%(names)s and %(count)s others are typing|one": "%(names)s and one other is typing", "%(names)s and %(lastPerson)s are typing": "%(names)s and %(lastPerson)s are typing", - "Failure to create room": "Failure to create room", - "Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.", - "Send anyway": "Send anyway", - "Send": "Send", - "Unnamed Room": "Unnamed Room", "This homeserver has hit its Monthly Active User limit.": "This homeserver has hit its Monthly Active User limit.", "This homeserver has exceeded one of its resource limits.": "This homeserver has exceeded one of its resource limits.", "Please contact your service administrator to continue using the service.": "Please contact your service administrator to continue using the service.", @@ -222,6 +222,9 @@ "Your browser does not support the required cryptography extensions": "Your browser does not support the required cryptography extensions", "Not a valid Riot keyfile": "Not a valid Riot keyfile", "Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?", + "You do not have permission to invite people to this room.": "You do not have permission to invite people to this room.", + "User %(user_id)s does not exist": "User %(user_id)s does not exist", + "Unknown server error": "Unknown server error", "Use a few words, avoid common phrases": "Use a few words, avoid common phrases", "No need for symbols, digits, or uppercase letters": "No need for symbols, digits, or uppercase letters", "Use a longer keyboard pattern with more turns": "Use a longer keyboard pattern with more turns", @@ -247,9 +250,6 @@ "A word by itself is easy to guess": "A word by itself is easy to guess", "Names and surnames by themselves are easy to guess": "Names and surnames by themselves are easy to guess", "Common names and surnames are easy to guess": "Common names and surnames are easy to guess", - "You do not have permission to invite people to this room.": "You do not have permission to invite people to this room.", - "User %(user_id)s does not exist": "User %(user_id)s does not exist", - "Unknown server error": "Unknown server error", "Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.", "Please contact your homeserver administrator.": "Please contact your homeserver administrator.", "Failed to join room": "Failed to join room", @@ -490,11 +490,11 @@ "At this time it is not possible to reply with an emote.": "At this time it is not possible to reply with an emote.", "Markdown is disabled": "Markdown is disabled", "Markdown is enabled": "Markdown is enabled", - "Unpin Message": "Unpin Message", - "Jump to message": "Jump to message", "No pinned messages.": "No pinned messages.", "Loading...": "Loading...", "Pinned Messages": "Pinned Messages", + "Unpin Message": "Unpin Message", + "Jump to message": "Jump to message", "%(duration)ss": "%(duration)ss", "%(duration)sm": "%(duration)sm", "%(duration)sh": "%(duration)sh", @@ -733,6 +733,7 @@ "Remove this user from community?": "Remove this user from community?", "Failed to withdraw invitation": "Failed to withdraw invitation", "Failed to remove user from community": "Failed to remove user from community", + "Failed to load group members": "Failed to load group members", "Filter community members": "Filter community members", "Flair will appear if enabled in room settings": "Flair will appear if enabled in room settings", "Flair will not appear": "Flair will not appear", @@ -1103,7 +1104,6 @@ "Community %(groupId)s not found": "Community %(groupId)s not found", "This Home server does not support communities": "This Home server does not support communities", "Failed to load %(groupId)s": "Failed to load %(groupId)s", - "Failed to load group members": "Failed to load group members", "Couldn't load home page": "Couldn't load home page", "You are currently using Riot anonymously as a guest.": "You are currently using Riot anonymously as a guest.", "If you would like to create a Matrix account you can register now.": "If you would like to create a Matrix account you can register now.", From 216fc6412a425e7a43163674f14f11064cf2ea1d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 5 Dec 2018 13:52:27 -0700 Subject: [PATCH 03/21] Fix pinning of rooms without badges Fixes https://github.com/vector-im/riot-web/issues/7723 This adds consideration for rooms that are "mentions only" (or "unread-muted" as internally referenced). --- src/stores/RoomListStore.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index 0f8e5d7b4d..0d99377180 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -300,6 +300,10 @@ class RoomListStore extends Store { const ts = this._tsOfNewestEvent(room); this._updateCachedRoomState(roomId, "timestamp", ts); return ts; + } else if (type === "unread-muted") { + const unread = Unread.doesRoomHaveUnreadMessages(room); + this._updateCachedRoomState(roomId, "unread-muted", unread); + return unread; } else if (type === "unread") { const unread = room.getUnreadNotificationCount() > 0; this._updateCachedRoomState(roomId, "unread", unread); @@ -358,8 +362,21 @@ class RoomListStore extends Store { } if (pinUnread) { - const unreadA = this._getRoomState(roomA, "unread"); - const unreadB = this._getRoomState(roomB, "unread"); + let unreadA = this._getRoomState(roomA, "unread"); + let unreadB = this._getRoomState(roomB, "unread"); + if (unreadA && !unreadB) return -1; + if (!unreadA && unreadB) return 1; + + // If they both have unread messages, sort by timestamp + // If nether have unread message (the fourth check not shown + // here), then just sort by timestamp anyways. + if (unreadA && unreadB) return timestampDiff; + + // Unread can also mean "unread without badge", which is + // different from what the above checks for. We're also + // going to sort those here. + unreadA = this._getRoomState(roomA, "unread-muted"); + unreadB = this._getRoomState(roomB, "unread-muted"); if (unreadA && !unreadB) return -1; if (!unreadA && unreadB) return 1; From 93c90896b5d9508b4a4bb82dc2fdef9e3800c3fe Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 5 Dec 2018 14:00:09 -0700 Subject: [PATCH 04/21] Regenerate en_EN.json --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e7fbdcdd43..7d263f6a4c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -225,7 +225,6 @@ "You do not have permission to invite people to this room.": "You do not have permission to invite people to this room.", "User %(user_id)s does not exist": "User %(user_id)s does not exist", "Unknown server error": "Unknown server error", - "There was an error joining the room": "There was an error joining the room", "Use a few words, avoid common phrases": "Use a few words, avoid common phrases", "No need for symbols, digits, or uppercase letters": "No need for symbols, digits, or uppercase letters", "Use a longer keyboard pattern with more turns": "Use a longer keyboard pattern with more turns", @@ -251,6 +250,7 @@ "A word by itself is easy to guess": "A word by itself is easy to guess", "Names and surnames by themselves are easy to guess": "Names and surnames by themselves are easy to guess", "Common names and surnames are easy to guess": "Common names and surnames are easy to guess", + "There was an error joining the room": "There was an error joining the room", "Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.", "Please contact your homeserver administrator.": "Please contact your homeserver administrator.", "Failed to join room": "Failed to join room", From f08a54ed1e9b157c10ac05115ed69073305e3021 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 5 Dec 2018 18:00:09 -0700 Subject: [PATCH 05/21] Don't consider ACL'd servers as permalink candidates and fix the bug where it was picking 4 servers instead of 3. This was due to `<=` instead of `<` in the `MAX_SERVER_CANDIDATES` loop. Added tests for all the things. Fixes https://github.com/vector-im/riot-web/issues/7752 Fixes https://github.com/vector-im/riot-web/issues/7682 --- package.json | 1 + src/matrix-to.js | 61 +++++++++++++- test/matrix-to-test.js | 186 ++++++++++++++++++++++++++++++++++++++--- 3 files changed, 232 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 67d1f3ba1e..d444c15eab 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "gfm.css": "^1.1.1", "glob": "^5.0.14", "highlight.js": "^9.13.0", + "is-ip": "^2.0.0", "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.6", "lodash": "^4.13.1", diff --git a/src/matrix-to.js b/src/matrix-to.js index b5827f671a..fb2c8096d7 100644 --- a/src/matrix-to.js +++ b/src/matrix-to.js @@ -15,6 +15,8 @@ limitations under the License. */ import MatrixClientPeg from "./MatrixClientPeg"; +import isIp from "is-ip"; +import utils from 'matrix-js-sdk/lib/utils'; export const host = "matrix.to"; export const baseUrl = `https://${host}`; @@ -90,7 +92,9 @@ export function pickServerCandidates(roomId) { // Rationale for popular servers: It's hard to get rid of people when // they keep flocking in from a particular server. Sure, the server could // be ACL'd in the future or for some reason be evicted from the room - // however an event like that is unlikely the larger the room gets. + // however an event like that is unlikely the larger the room gets. If + // the server is ACL'd at the time of generating the link however, we + // shouldn't pick them. We also don't pick IP addresses. // Note: we don't pick the server the room was created on because the // homeserver should already be using that server as a last ditch attempt @@ -104,12 +108,29 @@ export function pickServerCandidates(roomId) { // The receiving user can then manually append the known-good server to // the list and magically have the link work. + const bannedHostsRegexps = []; + let allowedHostsRegexps = [new RegExp(".*")]; // default allow everyone + if (room.currentState) { + const aclEvent = room.currentState.getStateEvents("m.room.server_acl", ""); + if (aclEvent && aclEvent.getContent()) { + const getRegex = (hostname) => new RegExp("^" + utils.globToRegexp(hostname, false) + "$"); + + const denied = aclEvent.getContent().deny || []; + denied.forEach(h => bannedHostsRegexps.push(getRegex(h))); + + const allowed = aclEvent.getContent().allow || []; + allowedHostsRegexps = []; // we don't want to use the default rule here + allowed.forEach(h => allowedHostsRegexps.push(getRegex(h))); + } + } + const populationMap: {[server:string]:number} = {}; const highestPlUser = {userId: null, powerLevel: 0, serverName: null}; for (const member of room.getJoinedMembers()) { const serverName = member.userId.split(":").splice(1).join(":"); - if (member.powerLevel > highestPlUser.powerLevel) { + if (member.powerLevel > highestPlUser.powerLevel && !isHostnameIpAddress(serverName) + && !isHostInRegex(serverName, bannedHostsRegexps) && isHostInRegex(serverName, allowedHostsRegexps)) { highestPlUser.userId = member.userId; highestPlUser.powerLevel = member.powerLevel; highestPlUser.serverName = serverName; @@ -125,8 +146,9 @@ export function pickServerCandidates(roomId) { const beforePopulation = candidates.length; const serversByPopulation = Object.keys(populationMap) .sort((a, b) => populationMap[b] - populationMap[a]) - .filter(a => !candidates.includes(a)); - for (let i = beforePopulation; i <= MAX_SERVER_CANDIDATES; i++) { + .filter(a => !candidates.includes(a) && !isHostnameIpAddress(a) + && !isHostInRegex(a, bannedHostsRegexps) && isHostInRegex(a, allowedHostsRegexps)); + for (let i = beforePopulation; i < MAX_SERVER_CANDIDATES; i++) { const idx = i - beforePopulation; if (idx >= serversByPopulation.length) break; candidates.push(serversByPopulation[idx]); @@ -134,3 +156,34 @@ export function pickServerCandidates(roomId) { return candidates; } + +function getHostnameFromMatrixDomain(domain) { + if (!domain) return null; + + // The hostname might have a port, so we convert it to a URL and + // split out the real hostname. + const parser = document.createElement('a'); + parser.href = "https://" + domain; + return parser.hostname; +} + +function isHostInRegex(hostname, regexps) { + hostname = getHostnameFromMatrixDomain(hostname); + if (!hostname) return true; // assumed + if (regexps.length > 0 && !regexps[0].test) throw new Error(regexps[0]); + + return regexps.filter(h => h.test(hostname)).length > 0; +} + +function isHostnameIpAddress(hostname) { + hostname = getHostnameFromMatrixDomain(hostname); + if (!hostname) return false; + + // is-ip doesn't want IPv6 addresses surrounded by brackets, so + // take them off. + if (hostname.startsWith("[") && hostname.endsWith("]")) { + hostname = hostname.substring(1, hostname.length - 1); + } + + return isIp(hostname); +} \ No newline at end of file diff --git a/test/matrix-to-test.js b/test/matrix-to-test.js index 70533575c4..6392e326e9 100644 --- a/test/matrix-to-test.js +++ b/test/matrix-to-test.js @@ -150,7 +150,39 @@ describe('matrix-to', function() { expect(pickedServers[2]).toBe("third"); }); - it('should work with IPv4 hostnames', function() { + it('should pick a maximum of 3 candidate servers', function() { + peg.get().getRoom = () => { + return { + getJoinedMembers: () => [ + { + userId: "@alice:alpha", + powerLevel: 100, + }, + { + userId: "@alice:bravo", + powerLevel: 0, + }, + { + userId: "@alice:charlie", + powerLevel: 0, + }, + { + userId: "@alice:delta", + powerLevel: 0, + }, + { + userId: "@alice:echo", + powerLevel: 0, + }, + ], + }; + }; + const pickedServers = pickServerCandidates("!somewhere:example.org"); + expect(pickedServers).toExist(); + expect(pickedServers.length).toBe(3); + }); + + it('should not consider IPv4 hosts', function() { peg.get().getRoom = () => { return { getJoinedMembers: () => [ @@ -163,11 +195,10 @@ describe('matrix-to', function() { }; const pickedServers = pickServerCandidates("!somewhere:example.org"); expect(pickedServers).toExist(); - expect(pickedServers.length).toBe(1); - expect(pickedServers[0]).toBe("127.0.0.1"); + expect(pickedServers.length).toBe(0); }); - it('should work with IPv6 hostnames', function() { + it('should not consider IPv6 hosts', function() { peg.get().getRoom = () => { return { getJoinedMembers: () => [ @@ -180,11 +211,10 @@ describe('matrix-to', function() { }; const pickedServers = pickServerCandidates("!somewhere:example.org"); expect(pickedServers).toExist(); - expect(pickedServers.length).toBe(1); - expect(pickedServers[0]).toBe("[::1]"); + expect(pickedServers.length).toBe(0); }); - it('should work with IPv4 hostnames with ports', function() { + it('should not consider IPv4 hostnames with ports', function() { peg.get().getRoom = () => { return { getJoinedMembers: () => [ @@ -197,11 +227,10 @@ describe('matrix-to', function() { }; const pickedServers = pickServerCandidates("!somewhere:example.org"); expect(pickedServers).toExist(); - expect(pickedServers.length).toBe(1); - expect(pickedServers[0]).toBe("127.0.0.1:8448"); + expect(pickedServers.length).toBe(0); }); - it('should work with IPv6 hostnames with ports', function() { + it('should not consider IPv6 hostnames with ports', function() { peg.get().getRoom = () => { return { getJoinedMembers: () => [ @@ -214,8 +243,7 @@ describe('matrix-to', function() { }; const pickedServers = pickServerCandidates("!somewhere:example.org"); expect(pickedServers).toExist(); - expect(pickedServers.length).toBe(1); - expect(pickedServers[0]).toBe("[::1]:8448"); + expect(pickedServers.length).toBe(0); }); it('should work with hostnames with ports', function() { @@ -235,6 +263,140 @@ describe('matrix-to', function() { expect(pickedServers[0]).toBe("example.org:8448"); }); + it('should not consider servers explicitly denied by ACLs', function() { + peg.get().getRoom = () => { + return { + getJoinedMembers: () => [ + { + userId: "@alice:evilcorp.com", + powerLevel: 100, + }, + { + userId: "@bob:chat.evilcorp.com", + powerLevel: 0, + }, + ], + currentState: { + getStateEvents: (type, key) => { + if (type !== "m.room.server_acl" || key !== "") return null; + return { + getContent: () => { + return { + deny: ["evilcorp.com", "*.evilcorp.com"], + allow: ["*"], + }; + }, + }; + }, + }, + }; + }; + const pickedServers = pickServerCandidates("!somewhere:example.org"); + expect(pickedServers).toExist(); + expect(pickedServers.length).toBe(0); + }); + + it('should not consider servers not allowed by ACLs', function() { + peg.get().getRoom = () => { + return { + getJoinedMembers: () => [ + { + userId: "@alice:evilcorp.com", + powerLevel: 100, + }, + { + userId: "@bob:chat.evilcorp.com", + powerLevel: 0, + }, + ], + currentState: { + getStateEvents: (type, key) => { + if (type !== "m.room.server_acl" || key !== "") return null; + return { + getContent: () => { + return { + deny: [], + allow: [], // implies "ban everyone" + }; + }, + }; + }, + }, + }; + }; + const pickedServers = pickServerCandidates("!somewhere:example.org"); + expect(pickedServers).toExist(); + expect(pickedServers.length).toBe(0); + }); + + it('should consider servers not explicitly banned by ACLs', function() { + peg.get().getRoom = () => { + return { + getJoinedMembers: () => [ + { + userId: "@alice:evilcorp.com", + powerLevel: 100, + }, + { + userId: "@bob:chat.evilcorp.com", + powerLevel: 0, + }, + ], + currentState: { + getStateEvents: (type, key) => { + if (type !== "m.room.server_acl" || key !== "") return null; + return { + getContent: () => { + return { + deny: ["*.evilcorp.com"], // evilcorp.com is still good though + allow: ["*"], + }; + }, + }; + }, + }, + }; + }; + const pickedServers = pickServerCandidates("!somewhere:example.org"); + expect(pickedServers).toExist(); + expect(pickedServers.length).toBe(1); + expect(pickedServers[0]).toEqual("evilcorp.com"); + }); + + it('should consider servers not disallowed by ACLs', function() { + peg.get().getRoom = () => { + return { + getJoinedMembers: () => [ + { + userId: "@alice:evilcorp.com", + powerLevel: 100, + }, + { + userId: "@bob:chat.evilcorp.com", + powerLevel: 0, + }, + ], + currentState: { + getStateEvents: (type, key) => { + if (type !== "m.room.server_acl" || key !== "") return null; + return { + getContent: () => { + return { + deny: [], + allow: ["evilcorp.com"], // implies "ban everyone else" + }; + }, + }; + }, + }, + }; + }; + const pickedServers = pickServerCandidates("!somewhere:example.org"); + expect(pickedServers).toExist(); + expect(pickedServers.length).toBe(1); + expect(pickedServers[0]).toEqual("evilcorp.com"); + }); + it('should generate an event permalink for room IDs with no candidate servers', function() { peg.get().getRoom = () => null; const result = makeEventPermalink("!somewhere:example.org", "$something:example.com"); From 45bc1f7dbdfe72e969e80890849426bcc5285703 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 5 Dec 2018 18:14:22 -0700 Subject: [PATCH 06/21] Appease the linter --- src/matrix-to.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix-to.js b/src/matrix-to.js index fb2c8096d7..b750dff6d6 100644 --- a/src/matrix-to.js +++ b/src/matrix-to.js @@ -186,4 +186,4 @@ function isHostnameIpAddress(hostname) { } return isIp(hostname); -} \ No newline at end of file +} From 870825b1803a509cc2ae289c20db4589610a128f Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 6 Dec 2018 11:15:36 +0000 Subject: [PATCH 07/21] js-sdk rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 67d1f3ba1e..b3c8461efe 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "linkifyjs": "^2.1.6", "lodash": "^4.13.1", "lolex": "2.3.2", - "matrix-js-sdk": "0.14.1", + "matrix-js-sdk": "0.14.2-rc.1", "optimist": "^0.6.1", "pako": "^1.0.5", "prop-types": "^15.5.8", From e2c01445d34eb484dd3c353f9da383e985f7c722 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 6 Dec 2018 11:18:37 +0000 Subject: [PATCH 08/21] Prepare changelog for v0.14.7-rc.1 --- CHANGELOG.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eea47dcb8f..44d05396d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,44 @@ +Changes in [0.14.7-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.7-rc.1) (2018-12-06) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.6...v0.14.7-rc.1) + + * Suppress CORS errors in the 'failed to join room' dialog + [\#2306](https://github.com/matrix-org/matrix-react-sdk/pull/2306) + * Check if users exist before inviting them and communicate errors + [\#2317](https://github.com/matrix-org/matrix-react-sdk/pull/2317) + * Update from Weblate. + [\#2328](https://github.com/matrix-org/matrix-react-sdk/pull/2328) + * Allow group summary to load when /users fails + [\#2326](https://github.com/matrix-org/matrix-react-sdk/pull/2326) + * Show correct text if passphrase is skipped + [\#2324](https://github.com/matrix-org/matrix-react-sdk/pull/2324) + * Add password strength meter to backup creation UI + [\#2294](https://github.com/matrix-org/matrix-react-sdk/pull/2294) + * Check upload limits before trying to upload large files + [\#1876](https://github.com/matrix-org/matrix-react-sdk/pull/1876) + * Support .well-known discovery + [\#2227](https://github.com/matrix-org/matrix-react-sdk/pull/2227) + * Make create key backup dialog async + [\#2291](https://github.com/matrix-org/matrix-react-sdk/pull/2291) + * Forgot to enable continue button on download + [\#2288](https://github.com/matrix-org/matrix-react-sdk/pull/2288) + * Online incremental megolm backups (v2) + [\#2169](https://github.com/matrix-org/matrix-react-sdk/pull/2169) + * Add recovery key download button + [\#2284](https://github.com/matrix-org/matrix-react-sdk/pull/2284) + * Passphrase Support for e2e backups + [\#2283](https://github.com/matrix-org/matrix-react-sdk/pull/2283) + * Update async dialog interface to use promises + [\#2286](https://github.com/matrix-org/matrix-react-sdk/pull/2286) + * Support for m.login.sso + [\#2279](https://github.com/matrix-org/matrix-react-sdk/pull/2279) + * Added badge to non-autoplay GIFs + [\#2235](https://github.com/matrix-org/matrix-react-sdk/pull/2235) + * Improve terms auth flow + [\#2277](https://github.com/matrix-org/matrix-react-sdk/pull/2277) + * Handle crypto db version upgrade + [\#2282](https://github.com/matrix-org/matrix-react-sdk/pull/2282) + Changes in [0.14.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.6) (2018-11-22) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.5...v0.14.6) From 58ab9a09953c21375aa946b43d726cb8966a239f Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 6 Dec 2018 11:18:37 +0000 Subject: [PATCH 09/21] v0.14.7-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b3c8461efe..ebf3648f7d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.14.6", + "version": "0.14.7-rc.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 77c51aff2d26dd21f9bbd6ea6b1f813ab6c01714 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 6 Dec 2018 11:44:00 +0000 Subject: [PATCH 10/21] Ship the babelrc file to npm We ship the source files, so it probably makes sense to ship the babelrc that tells you how to compile them. --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index ebf3648f7d..09078927c4 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "license": "Apache-2.0", "main": "lib/index.js", "files": [ + ".babelrc", ".eslintrc.js", "CHANGELOG.md", "CONTRIBUTING.rst", From c94d8d6f68120a70f00a9c0f597fd4eed6c5f78a Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 6 Dec 2018 12:39:23 +0000 Subject: [PATCH 11/21] Prepare changelog for v0.14.7-rc.2 --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44d05396d9..1261ad8d40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +Changes in [0.14.7-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.7-rc.2) (2018-12-06) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.7-rc.1...v0.14.7-rc.2) + + * Ship the babelrc file to npm + [\#2332](https://github.com/matrix-org/matrix-react-sdk/pull/2332) + Changes in [0.14.7-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.7-rc.1) (2018-12-06) =============================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.6...v0.14.7-rc.1) From a82b54f25a65e49712e32eb44f32eacf81d73eda Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 6 Dec 2018 12:39:24 +0000 Subject: [PATCH 12/21] v0.14.7-rc.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 09078927c4..58aefff176 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.14.7-rc.1", + "version": "0.14.7-rc.2", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From ca1313099f4db238374f281944710bccd1b2398d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 6 Dec 2018 11:45:58 -0700 Subject: [PATCH 13/21] Show the IncomingCallBox if the call is for the RoomSubList Fixes https://github.com/vector-im/riot-web/issues/4369 Previously the RoomSubList would filter its list of rooms to verify that the incoming call belongs to it. This causes problems when the sub list is being told some rooms don't exist (ie: the list is filtered). It is trivial for the RoomList to instead track which RoomSubList (tag) it should be handing the call off to so we do that instead now. The RoomSubList trusts that the caller has already filtered it and will render the IncomingCallBox if it has an incoming call. --- src/components/structures/RoomSubList.js | 15 +++----- src/components/views/rooms/RoomList.js | 45 +++++++++++++++++++----- 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index d798070659..cdeb8926c0 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -327,17 +327,10 @@ const RoomSubList = React.createClass({ let incomingCall; if (this.props.incomingCall) { - const self = this; - // Check if the incoming call is for this section - const incomingCallRoom = this.props.list.filter(function(room) { - return self.props.incomingCall.roomId === room.roomId; - }); - - if (incomingCallRoom.length === 1) { - const IncomingCallBox = sdk.getComponent("voip.IncomingCallBox"); - incomingCall = - ; - } + // We can assume that if we have an incoming call then it is for this list + const IncomingCallBox = sdk.getComponent("voip.IncomingCallBox"); + incomingCall = + ; } const tabindex = this.props.searchFilter === "" ? "0" : "-1"; diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 3e632ba8ce..3ad35c036d 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -71,6 +71,7 @@ module.exports = React.createClass({ isLoadingLeftRooms: false, totalRoomCount: null, lists: {}, + incomingCallTag: null, incomingCall: null, selectedTags: [], }; @@ -155,11 +156,13 @@ module.exports = React.createClass({ if (call && call.call_state === 'ringing') { this.setState({ incomingCall: call, + incomingCallTag: this.getTagNameForRoomId(payload.room_id), }); this._repositionIncomingCallBox(undefined, true); } else { this.setState({ incomingCall: null, + incomingCallTag: null, }); } break; @@ -328,6 +331,26 @@ module.exports = React.createClass({ // this._lastRefreshRoomListTs = Date.now(); }, + getTagNameForRoomId: function(roomId) { + const lists = RoomListStore.getRoomLists(); + for (const tagName of Object.keys(lists)) { + for (const room of lists[tagName]) { + // Should be impossible, but guard anyways. + if (!room) { + continue; + } + const myUserId = MatrixClientPeg.get().getUserId(); + if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, myUserId, this.props.ConferenceHandler)) { + continue; + } + + if (room.roomId === roomId) return tagName; + } + } + + return null; + }, + getRoomLists: function() { const lists = RoomListStore.getRoomLists(); @@ -621,6 +644,12 @@ module.exports = React.createClass({ // so checking on every render is the sanest thing at this time. const showEmpty = SettingsStore.getValue('RoomSubList.showEmpty'); + const incomingCallIfTaggedAs = (tagName) => { + if (!this.state.incomingCall) return null; + if (this.state.incomingCallTag !== tagName) return null; + return this.state.incomingCall; + }; + const self = this; return ( @@ -750,7 +779,7 @@ module.exports = React.createClass({ tagName="m.lowpriority" editable={false} order="recent" - incomingCall={self.state.incomingCall} + incomingCall={incomingCallIfTaggedAs('m.server_notice')} collapsed={self.props.collapsed} searchFilter={self.props.searchFilter} onHeaderClick={self.onSubListHeaderClick} From 757181c322937c1a9bd650b56e2e892b16231fdf Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 6 Dec 2018 16:18:46 -0600 Subject: [PATCH 14/21] Update React guide in code style This updates React guidance to prefer JS classes and adds additional info about how to handle specific situations when using them. Signed-off-by: J. Ryan Stinnett --- code_style.md | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/code_style.md b/code_style.md index 2cac303e54..96f3879ebc 100644 --- a/code_style.md +++ b/code_style.md @@ -165,7 +165,6 @@ ECMAScript React ----- -- Use React.createClass rather than ES6 classes for components, as the boilerplate is way too heavy on ES6 currently. ES7 might improve it. - Pull out functions in props to the class, generally as specific event handlers: ```jsx @@ -174,11 +173,38 @@ React // Better // Best, if onFooClick would do anything other than directly calling doStuff ``` - - Not doing so is acceptable in a single case; in function-refs: - + + Not doing so is acceptable in a single case: in function-refs: + ```jsx this.component = self}> ``` + +- Prefer classes that extend `React.Component` (or `React.PureComponent`) instead of `React.createClass` + - You can avoid the need to bind handler functions by using [property initializers](https://reactjs.org/docs/react-component.html#constructor): + + ```js + class Widget extends React.Component + onFooClick = () => { + ... + } + } + ``` + - To define `propTypes`, use a static property: + ```js + class Widget extends React.Component + static propTypes = { + ... + } + } + ``` + - If you need to specify initial component state, [assign it](https://reactjs.org/docs/react-component.html#constructor) to `this.state` in the constructor: + ```js + constructor(props) { + super(props); + // Don't call this.setState() here! + this.state = { counter: 0 }; + } + ``` - Think about whether your component really needs state: are you duplicating information in component state that could be derived from the model? From 0b65a1ee1a91fd25f8d53f80dae521ce6aaea8b0 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 6 Dec 2018 19:24:59 -0600 Subject: [PATCH 15/21] Remove outdated info about custom skins It has been marked outdated for several years. Since it appears on the repo home page, it makes the project feel unmaintained. Signed-off-by: J. Ryan Stinnett --- README.md | 58 ------------------------------------------------------- 1 file changed, 58 deletions(-) diff --git a/README.md b/README.md index ac45497dd4..ec95fbd132 100644 --- a/README.md +++ b/README.md @@ -127,61 +127,3 @@ Github Issues All issues should be filed under https://github.com/vector-im/riot-web/issues for now. - -OUTDATED: To Create Your Own Skin -================================= - -**This is ALL LIES currently, and needs to be updated** - -Skins are modules are exported from such a package in the `lib` directory. -`lib/skins` contains one directory per-skin, named after the skin, and the -`modules` directory contains modules as their javascript files. - -A basic skin is provided in the matrix-react-skin package. This also contains -a minimal application that instantiates the basic skin making a working matrix -client. - -You can use matrix-react-sdk directly, but to do this you would have to provide -'views' for each UI component. To get started quickly, use matrix-react-skin. - -To actually change the look of a skin, you can create a base skin (which -does not use views from any other skin) or you can make a derived skin. -Note that derived skins are currently experimental: for example, the CSS -from the skins it is based on will not be automatically included. - -To make a skin, create React classes for any custom components you wish to add -in a skin within `src/skins/`. These can be based off the files in -`views` in the `matrix-react-skin` package, modifying the require() statement -appropriately. - -If you make a derived skin, you only need copy the files you wish to customise. - -Once you've made all your view files, you need to make a `skinfo.json`. This -contains all the metadata for a skin. This is a JSON file with, currently, a -single key, 'baseSkin'. Set this to the empty string if your skin is a base skin, -or for a derived skin, set it to the path of your base skin's skinfo.json file, as -you would use in a require call. - -Now you have the basis of a skin, you need to generate a skindex.json file. The -`reskindex.js` tool in matrix-react-sdk does this for you. It is suggested that -you add an npm script to run this, as in matrix-react-skin. - -For more specific detail on any of these steps, look at matrix-react-skin. - -Alternative instructions: - - * Create a new NPM project. Be sure to directly depend on react, (otherwise - you can end up with two copies of react). - * Create an index.js file that sets up react. Add require statements for - React and matrix-react-sdk. Load a skin using the 'loadSkin' method on the - SDK and call Render. This can be a skin provided by a separate package or - a skin in the same package. - * Add a way to build your project: we suggest copying the scripts block - from matrix-react-skin (which uses babel and webpack). You could use - different tools but remember that at least the skins and modules of - your project should end up in plain (ie. non ES6, non JSX) javascript in - the lib directory at the end of the build process, as well as any - packaging that you might do. - * Create an index.html file pulling in your compiled javascript and the - CSS bundle from the skin you use. For now, you'll also need to manually - import CSS from any skins that your skin inherts from. From a92d2902c4c3db3193598723cb0e2646a70b2528 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 6 Dec 2018 15:39:59 -0600 Subject: [PATCH 16/21] Add an in-room reminder to set up key recovery This adds an in-room reminder above the message timeline to set up Secure Message Recovery so that your keys will be backed up. If you try to ignore it, an additional dialog is shown to confirm. Fixes vector-im/riot-web#7783. Signed-off-by: J. Ryan Stinnett --- res/css/_components.scss | 1 + .../views/rooms/_RoomRecoveryReminder.scss | 43 ++++++++++ res/themes/dark/css/_dark.scss | 10 +++ res/themes/light/css/_base.scss | 10 +++ .../keybackup/CreateKeyBackupDialog.js | 2 +- .../keybackup/IgnoreRecoveryReminderDialog.js | 70 +++++++++++++++ src/components/structures/RoomView.js | 25 ++++++ src/components/structures/UserSettings.js | 1 + .../views/rooms/RoomRecoveryReminder.js | 85 +++++++++++++++++++ src/i18n/strings/en_EN.json | 9 +- src/settings/Settings.js | 5 ++ 11 files changed, 259 insertions(+), 2 deletions(-) create mode 100644 res/css/views/rooms/_RoomRecoveryReminder.scss create mode 100644 src/async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog.js create mode 100644 src/components/views/rooms/RoomRecoveryReminder.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 083071ef6c..579856f880 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -101,6 +101,7 @@ @import "./views/rooms/_RoomHeader.scss"; @import "./views/rooms/_RoomList.scss"; @import "./views/rooms/_RoomPreviewBar.scss"; +@import "./views/rooms/_RoomRecoveryReminder.scss"; @import "./views/rooms/_RoomSettings.scss"; @import "./views/rooms/_RoomTile.scss"; @import "./views/rooms/_RoomTooltip.scss"; diff --git a/res/css/views/rooms/_RoomRecoveryReminder.scss b/res/css/views/rooms/_RoomRecoveryReminder.scss new file mode 100644 index 0000000000..4bb42ff114 --- /dev/null +++ b/res/css/views/rooms/_RoomRecoveryReminder.scss @@ -0,0 +1,43 @@ +/* +Copyright 2018 New Vector 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. +*/ + +.mx_RoomRecoveryReminder { + display: flex; + flex-direction: column; + text-align: center; + background-color: $room-warning-bg-color; + padding: 20px; + border: 1px solid $primary-hairline-color; + border-bottom: unset; +} + +.mx_RoomRecoveryReminder_header { + font-weight: bold; + margin-bottom: 1em; +} + +.mx_RoomRecoveryReminder_body { + margin-bottom: 1em; +} + +.mx_RoomRecoveryReminder_button { + @mixin mx_DialogButton; + margin: 0 10px; +} + +.mx_RoomRecoveryReminder_button.mx_RoomRecoveryReminder_secondary { + @mixin mx_DialogButton_secondary; +} diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index b773d7c720..5dbc00af4e 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -100,6 +100,8 @@ $voip-accept-color: #80f480; $rte-bg-color: #353535; $rte-code-bg-color: #000; +$room-warning-bg-color: #2d2d2d; + // ******************** $roomtile-name-color: rgba(186, 186, 186, 0.8); @@ -169,6 +171,14 @@ $progressbar-color: #000; outline: none; } +@define-mixin mx_DialogButton_secondary { + // flip colours for the secondary ones + font-weight: 600; + border: 1px solid $accent-color ! important; + color: $accent-color; + background-color: $accent-fg-color; +} + // Nasty hacks to apply a filter to arbitrary monochrome artwork to make it // better match the theme. Typically applied to dark grey 'off' buttons or // light grey 'on' buttons. diff --git a/res/themes/light/css/_base.scss b/res/themes/light/css/_base.scss index 49347492ff..c275b94fb5 100644 --- a/res/themes/light/css/_base.scss +++ b/res/themes/light/css/_base.scss @@ -155,6 +155,8 @@ $imagebody-giflabel-border: rgba(0, 0, 0, 0.2); // unused? $progressbar-color: #000; +$room-warning-bg-color: #fff8e3; + // ***** Mixins! ***** @define-mixin mx_DialogButton { @@ -187,3 +189,11 @@ $progressbar-color: #000; font-size: 15px; padding: 0px 1.5em 0px 1.5em; } + +@define-mixin mx_DialogButton_secondary { + // flip colours for the secondary ones + font-weight: 600; + border: 1px solid $accent-color ! important; + color: $accent-color; + background-color: $accent-fg-color; +} diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 8547add256..6b115b890f 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -251,7 +251,7 @@ export default React.createClass({ />

{_t( - "If you don't want encrypted message history to be availble on other devices, "+ + "If you don't want encrypted message history to be available on other devices, "+ ".", {}, { diff --git a/src/async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog.js b/src/async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog.js new file mode 100644 index 0000000000..a9df3cca6e --- /dev/null +++ b/src/async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog.js @@ -0,0 +1,70 @@ +/* +Copyright 2018 New Vector 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 PropTypes from "prop-types"; +import sdk from "../../../../index"; +import { _t } from "../../../../languageHandler"; + +export default class IgnoreRecoveryReminderDialog extends React.PureComponent { + static propTypes = { + onDontAskAgain: PropTypes.func.isRequired, + onFinished: PropTypes.func.isRequired, + onSetup: PropTypes.func.isRequired, + } + + onDontAskAgainClick = () => { + this.props.onFinished(); + this.props.onDontAskAgain(); + } + + onSetupClick = () => { + this.props.onFinished(); + this.props.onSetup(); + } + + render() { + const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog"); + const DialogButtons = sdk.getComponent("views.elements.DialogButtons"); + + return ( + +

+

{_t( + "Without setting up Secure Message Recovery, " + + "you'll lose your secure message history when you " + + "log out.", + )}

+

{_t( + "If you don't want to set this up now, you can later " + + "in Settings.", + )}

+
+ +
+
+ + ); + } +} diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 934031e98d..0e0d56647d 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -607,6 +607,20 @@ module.exports = React.createClass({ } }, + async onRoomRecoveryReminderFinished(backupCreated) { + // If the user cancelled the key backup dialog, it suggests they don't + // want to be reminded anymore. + if (!backupCreated) { + await SettingsStore.setValue( + "showRoomRecoveryReminder", + null, + SettingLevel.ACCOUNT, + false, + ); + } + this.forceUpdate(); + }, + canResetTimeline: function() { if (!this.refs.messagePanel) { return true; @@ -1521,6 +1535,7 @@ module.exports = React.createClass({ const Loader = sdk.getComponent("elements.Spinner"); const TimelinePanel = sdk.getComponent("structures.TimelinePanel"); const RoomUpgradeWarningBar = sdk.getComponent("rooms.RoomUpgradeWarningBar"); + const RoomRecoveryReminder = sdk.getComponent("rooms.RoomRecoveryReminder"); if (!this.state.room) { if (this.state.roomLoading || this.state.peekLoading) { @@ -1655,6 +1670,13 @@ module.exports = React.createClass({ this.state.room.userMayUpgradeRoom(MatrixClientPeg.get().credentials.userId) ); + const showRoomRecoveryReminder = ( + SettingsStore.isFeatureEnabled("feature_keybackup") && + SettingsStore.getValue("showRoomRecoveryReminder") && + MatrixClientPeg.get().isRoomEncrypted(this.state.room.roomId) && + !MatrixClientPeg.get().getKeyBackupEnabled() + ); + let aux = null; let hideCancel = false; if (this.state.editingRoomSettings) { @@ -1669,6 +1691,9 @@ module.exports = React.createClass({ } else if (showRoomUpgradeBar) { aux = ; hideCancel = true; + } else if (showRoomRecoveryReminder) { + aux = ; + hideCancel = true; } else if (this.state.showingPinned) { hideCancel = true; // has own cancel aux = ; diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 4c15b4ec27..6f932d71e1 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -64,6 +64,7 @@ const SIMPLE_SETTINGS = [ { id: "urlPreviewsEnabled" }, { id: "autoplayGifsAndVideos" }, { id: "alwaysShowEncryptionIcons" }, + { id: "showRoomRecoveryReminder" }, { id: "hideReadReceipts" }, { id: "dontSendTypingNotifications" }, { id: "alwaysShowTimestamps" }, diff --git a/src/components/views/rooms/RoomRecoveryReminder.js b/src/components/views/rooms/RoomRecoveryReminder.js new file mode 100644 index 0000000000..265bfd3ee3 --- /dev/null +++ b/src/components/views/rooms/RoomRecoveryReminder.js @@ -0,0 +1,85 @@ +/* +Copyright 2018 New Vector 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 PropTypes from "prop-types"; +import sdk from "../../../index"; +import { _t } from "../../../languageHandler"; +import Modal from "../../../Modal"; + +export default class RoomRecoveryReminder extends React.PureComponent { + static propTypes = { + onFinished: PropTypes.func.isRequired, + } + + showKeyBackupDialog = () => { + Modal.createTrackedDialogAsync("Key Backup", "Key Backup", + import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"), + { + onFinished: this.props.onFinished, + }, + ); + } + + onDontAskAgainClick = () => { + // When you choose "Don't ask again" from the room reminder, we show a + // dialog to confirm the choice. + Modal.createTrackedDialogAsync("Ignore Recovery Reminder", "Ignore Recovery Reminder", + import("../../../async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog"), + { + onDontAskAgain: () => { + // Report false to the caller, who should prevent the + // reminder from appearing in the future. + this.props.onFinished(false); + }, + onSetup: () => { + this.showKeyBackupDialog(); + }, + }, + ); + } + + onSetupClick = () => { + this.showKeyBackupDialog(); + } + + render() { + const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton"); + + return ( +
+
{_t( + "Secure Message Recovery", + )}
+
{_t( + "If you log out or use another device, you'll lose your " + + "secure message history. To prevent this, set up Secure " + + "Message Recovery.", + )}
+
+ + { _t("Don't ask again") } + + + { _t("Set up") } + +
+
+ ); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7d263f6a4c..a4ce5143d7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -268,6 +268,7 @@ "Always show message timestamps": "Always show message timestamps", "Autoplay GIFs and videos": "Autoplay GIFs and videos", "Always show encryption icons": "Always show encryption icons", + "Show a reminder to enable Secure Message Recovery in encrypted rooms": "Show a reminder to enable Secure Message Recovery in encrypted rooms", "Enable automatic language detection for syntax highlighting": "Enable automatic language detection for syntax highlighting", "Hide avatars in user and room mentions": "Hide avatars in user and room mentions", "Disable big emoji in chat": "Disable big emoji in chat", @@ -562,6 +563,10 @@ "You are trying to access a room.": "You are trying to access a room.", "Click here to join the discussion!": "Click here to join the discussion!", "This is a preview of this room. Room interactions have been disabled": "This is a preview of this room. Room interactions have been disabled", + "Secure Message Recovery": "Secure Message Recovery", + "If you log out or use another device, you'll lose your secure message history. To prevent this, set up Secure Message Recovery.": "If you log out or use another device, you'll lose your secure message history. To prevent this, set up Secure Message Recovery.", + "Don't ask again": "Don't ask again", + "Set up": "Set up", "To change the room's avatar, you must be a": "To change the room's avatar, you must be a", "To change the room's name, you must be a": "To change the room's name, you must be a", "To change the room's main address, you must be a": "To change the room's main address, you must be a", @@ -1352,7 +1357,7 @@ "Secure your encrypted message history with a Recovery Passphrase.": "Secure your encrypted message history with a Recovery Passphrase.", "You'll need it if you log out or lose access to this device.": "You'll need it if you log out or lose access to this device.", "Enter a passphrase...": "Enter a passphrase...", - "If you don't want encrypted message history to be availble on other devices, .": "If you don't want encrypted message history to be availble on other devices, .", + "If you don't want encrypted message history to be available on other devices, .": "If you don't want encrypted message history to be available on other devices, .", "Or, if you don't want to create a Recovery Passphrase, skip this step and .": "Or, if you don't want to create a Recovery Passphrase, skip this step and .", "That matches!": "That matches!", "That doesn't match.": "That doesn't match.", @@ -1384,6 +1389,8 @@ "Create Key Backup": "Create Key Backup", "Unable to create key backup": "Unable to create key backup", "Retry": "Retry", + "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.": "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.", + "If you don't want to set this up now, you can later in Settings.": "If you don't want to set this up now, you can later in Settings.", "Failed to set direct chat tag": "Failed to set direct chat tag", "Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room", "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room" diff --git a/src/settings/Settings.js b/src/settings/Settings.js index eb702a729c..c9a4ecdebe 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -151,6 +151,11 @@ export const SETTINGS = { displayName: _td('Always show encryption icons'), default: true, }, + "showRoomRecoveryReminder": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + displayName: _td('Show a reminder to enable Secure Message Recovery in encrypted rooms'), + default: true, + }, "enableSyntaxHighlightLanguageDetection": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Enable automatic language detection for syntax highlighting'), From f2468f562d381310585fd7edbca24946c20b5c94 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 7 Dec 2018 20:15:21 -0700 Subject: [PATCH 17/21] Speed up room unread checks by not hitting the SettingsStore so often This was noticed as a problem after `Unread.doesRoomHaveUnreadMessages` started being called a lot more frequently. Down the call stack, `shouldHideEvent` is called which used to call into the `SettingsStore` frequently, causing performance issues in many cases. The `SettingsStore` tries to be as fast as possible, however there's still code paths that make it less than desirable to use as the first condition in an AND condition. By not hitting the `SettingsStore` so often, we can shorten those code paths. As for how much this improves things, I ran some profiling before and after this change. This was done on my massive 1200+ room account. Before it was possible to see nearly 2 seconds spent generating room lists where 20-130ms per room was spent figuring out if the room has unread messages. Afterwards, the room list was generating within ~330ms and each unread check taking 0-2ms. There's still room for improvement on generating the room list, however the significant gains here seem worth it. --- src/shouldHideEvent.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/shouldHideEvent.js b/src/shouldHideEvent.js index 3aad05a976..609f48b128 100644 --- a/src/shouldHideEvent.js +++ b/src/shouldHideEvent.js @@ -42,14 +42,14 @@ export default function shouldHideEvent(ev) { const isEnabled = (name) => SettingsStore.getValue(name, ev.getRoomId()); // Hide redacted events - if (isEnabled('hideRedactions') && ev.isRedacted()) return true; + if (ev.isRedacted() && isEnabled('hideRedactions')) return true; const eventDiff = memberEventDiff(ev); if (eventDiff.isMemberEvent) { - if (isEnabled('hideJoinLeaves') && (eventDiff.isJoin || eventDiff.isPart)) return true; - if (isEnabled('hideAvatarChanges') && eventDiff.isAvatarChange) return true; - if (isEnabled('hideDisplaynameChanges') && eventDiff.isDisplaynameChange) return true; + if ((eventDiff.isJoin || eventDiff.isPart) && isEnabled('hideJoinLeaves')) return true; + if (eventDiff.isAvatarChange && isEnabled('hideAvatarChanges')) return true; + if (eventDiff.isDisplaynameChange && isEnabled('hideDisplaynameChanges')) return true; } return false; From ebdba32393d7b1b3f7f73aab793e39dc2fedb87d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 8 Dec 2018 12:06:37 -0700 Subject: [PATCH 18/21] Add a comment about the SettingsStore being slow --- src/shouldHideEvent.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/shouldHideEvent.js b/src/shouldHideEvent.js index 609f48b128..adc89a126a 100644 --- a/src/shouldHideEvent.js +++ b/src/shouldHideEvent.js @@ -38,7 +38,9 @@ function memberEventDiff(ev) { } export default function shouldHideEvent(ev) { - // Wrap getValue() for readability + // Wrap getValue() for readability. Calling the SettingsStore can be + // fairly resource heavy, so the checks below should avoid hitting it + // where possible. const isEnabled = (name) => SettingsStore.getValue(name, ev.getRoomId()); // Hide redacted events From 5444a61e6fa7ad080ea4a11d40e96437bfb1e911 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 10 Dec 2018 13:39:35 +0000 Subject: [PATCH 19/21] Released js-sdk --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 58aefff176..8b835ec70f 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "linkifyjs": "^2.1.6", "lodash": "^4.13.1", "lolex": "2.3.2", - "matrix-js-sdk": "0.14.2-rc.1", + "matrix-js-sdk": "0.14.2", "optimist": "^0.6.1", "pako": "^1.0.5", "prop-types": "^15.5.8", From 9d456b2d0d6c7e5638a562e08619d73e33a888fc Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 10 Dec 2018 13:43:58 +0000 Subject: [PATCH 20/21] Prepare changelog for v0.14.7 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1261ad8d40..742b8b4529 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +Changes in [0.14.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.7) (2018-12-10) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.7-rc.2...v0.14.7) + + * No changes since rc.2 + Changes in [0.14.7-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.7-rc.2) (2018-12-06) =============================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.7-rc.1...v0.14.7-rc.2) From 37c984e1955185fc1d915b8620ec506dad177959 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 10 Dec 2018 13:43:59 +0000 Subject: [PATCH 21/21] v0.14.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8b835ec70f..9cca57075c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.14.7-rc.2", + "version": "0.14.7", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": {