From bdc451d66beb6944dc4fbbfa19aef14fef457858 Mon Sep 17 00:00:00 2001 From: Justin Sleep Date: Wed, 3 Jun 2020 16:36:48 -0500 Subject: [PATCH 001/194] Remove escape backslashes in non-Markdown messages --- src/Markdown.js | 8 -------- src/editor/serialize.ts | 4 ++++ test/editor/serialize-test.js | 12 ++++++++++++ 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/Markdown.js b/src/Markdown.js index fb1f8bf0ea..d312b7c5bd 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -175,14 +175,6 @@ export default class Markdown { const renderer = new commonmark.HtmlRenderer({safe: false}); const real_paragraph = 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. - renderer.out = function(s) { - // The `lit` function adds a string literal to the output buffer. - this.lit(s); - }; - renderer.paragraph = function(node, entering) { // as with toHTML, only append lines to paragraphs if there are // multiple paragraphs diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts index 4d0b8cd03a..85d2bb8e8f 100644 --- a/src/editor/serialize.ts +++ b/src/editor/serialize.ts @@ -42,6 +42,10 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = if (!parser.isPlainText() || forceHTML) { return parser.toHTML(); } + // ensure removal of escape backslashes in non-Markdown messages + if (md.indexOf("\\") > -1) { + return parser.toPlaintext(); + } } export function textSerialize(model: EditorModel) { diff --git a/test/editor/serialize-test.js b/test/editor/serialize-test.js index bd26ae91bb..f0d577ea21 100644 --- a/test/editor/serialize-test.js +++ b/test/editor/serialize-test.js @@ -61,4 +61,16 @@ describe('editor/serialize', function() { const html = htmlSerializeIfNeeded(model, {}); expect(html).toBe("Displayname]"); }); + it('escaped markdown should not retain backslashes', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.plain('\\*hello\\* world')]); + const html = htmlSerializeIfNeeded(model, {}); + expect(html).toBe('*hello* world'); + }); + it('escaped markdown should convert HTML entities', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.plain('\\*hello\\* world < hey world!')]); + const html = htmlSerializeIfNeeded(model, {}); + expect(html).toBe('*hello* world < hey world!'); + }); }); From e94f3422dfdc2045d76a8481ada485f56cd0a6e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 29 May 2020 11:44:08 +0200 Subject: [PATCH 002/194] Searching: Add support to paginate Seshat search results. --- src/Searching.js | 44 ++++++++++++++++++++++++++- src/components/structures/RoomView.js | 5 ++- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/Searching.js b/src/Searching.js index 663328fe41..c16906ade7 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -88,6 +88,7 @@ async function localSearch(searchTerm, roomId = undefined) { } const emptyResult = { + seshatQuery: searchArgs, results: [], highlights: [], }; @@ -97,6 +98,7 @@ async function localSearch(searchTerm, roomId = undefined) { const eventIndex = EventIndexPeg.get(); const localResult = await eventIndex.search(searchArgs); + emptyResult.seshatQuery.next_batch = localResult.next_batch; const response = { search_categories: { @@ -104,8 +106,25 @@ async function localSearch(searchTerm, roomId = undefined) { }, }; - const result = MatrixClientPeg.get()._processRoomEventsSearch( + return MatrixClientPeg.get()._processRoomEventsSearch( emptyResult, response); +} + +async function paginatedLocalSearch(searchResult) { + const eventIndex = EventIndexPeg.get(); + + let searchArgs = searchResult.seshatQuery; + + const localResult = await eventIndex.search(searchArgs); + + const response = { + search_categories: { + room_events: localResult, + }, + }; + + const result = MatrixClientPeg.get()._processRoomEventsSearch(searchResult, response); + searchResult.pendingRequest = null; return result; } @@ -132,6 +151,29 @@ function eventIndexSearch(term, roomId = undefined) { return searchPromise; } +function eventIndexSearchPagination(searchResult) { + const client = MatrixClientPeg.get(); + const query = searchResult.seshatQuery; + + if (!query) { + return client.backPaginateRoomEventsSearch(searchResult); + } else { + const promise = paginatedLocalSearch(searchResult); + searchResult.pendingRequest = promise; + return promise; + } +} + +export function searchPagination(searchResult) { + const eventIndex = EventIndexPeg.get(); + const client = MatrixClientPeg.get(); + + if (searchResult.pendingRequest) return searchResult.pendingRequest; + + if (eventIndex === null) return client.backPaginateRoomEventsSearch(searchResult); + else return eventIndexSearchPagination(searchResult); +} + export default function eventSearch(term, roomId = undefined) { const eventIndex = EventIndexPeg.get(); diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 49d7e3c238..6796984037 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -39,7 +39,7 @@ import Tinter from '../../Tinter'; import rate_limited_func from '../../ratelimitedfunc'; import * as ObjectUtils from '../../ObjectUtils'; import * as Rooms from '../../Rooms'; -import eventSearch from '../../Searching'; +import eventSearch, {searchPagination} from '../../Searching'; import {isOnlyCtrlOrCmdIgnoreShiftKeyEvent, isOnlyCtrlOrCmdKeyEvent, Key} from '../../Keyboard'; @@ -1035,8 +1035,7 @@ export default createReactClass({ if (this.state.searchResults.next_batch) { debuglog("requesting more search results"); - const searchPromise = this.context.backPaginateRoomEventsSearch( - this.state.searchResults); + const searchPromise = searchPagination(this.state.searchResults); return this._handleSearchResult(searchPromise); } else { debuglog("no more search results"); From c1ef193e3a8ba0e221d2655095fabc016127b4d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 29 May 2020 16:32:57 +0200 Subject: [PATCH 003/194] Searching: Add initial pagination for combined searches. --- src/Searching.js | 122 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 114 insertions(+), 8 deletions(-) diff --git a/src/Searching.js b/src/Searching.js index c16906ade7..1960f2a2b2 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -71,6 +71,18 @@ async function combinedSearch(searchTerm) { result.highlights = localResult.highlights.concat( serverSideResult.highlights); + result.seshatQuery = localResult.seshatQuery; + result.serverSideNextBatch = serverSideResult.next_batch; + result._query = serverSideResult._query; + + // We need the next batch to be set for the client to know that it can + // paginate further. + if (serverSideResult.next_batch) { + result.next_batch = serverSideResult.next_batch; + } else { + result.next_batch = localResult.next_batch; + } + return result; } @@ -106,16 +118,16 @@ async function localSearch(searchTerm, roomId = undefined) { }, }; - return MatrixClientPeg.get()._processRoomEventsSearch( - emptyResult, response); + return MatrixClientPeg.get()._processRoomEventsSearch(emptyResult, response); } -async function paginatedLocalSearch(searchResult) { +async function localPagination(searchResult) { const eventIndex = EventIndexPeg.get(); - let searchArgs = searchResult.seshatQuery; + const searchArgs = searchResult.seshatQuery; const localResult = await eventIndex.search(searchArgs); + searchResult.seshatQuery.next_batch = localResult.next_batch; const response = { search_categories: { @@ -129,6 +141,92 @@ async function paginatedLocalSearch(searchResult) { return result; } +/** + * Combine the local and server search results + */ +function combineResults(previousSearchResult, localResult = undefined, serverSideResult = undefined) { + // // cachedResults = previousSearchResult.cachedResults; + // if (localResult) { + // previousSearchResult.seshatQuery.next_batch = localResult.next_batch; + // } + const compare = (a, b) => { + const aEvent = a.result; + const bEvent = b.result; + + if (aEvent.origin_server_ts > + bEvent.origin_server_ts) return -1; + if (aEvent.origin_server_ts < + bEvent.origin_server_ts) return 1; + return 0; + }; + + const result = {}; + + result.count = previousSearchResult.count; + + if (localResult && serverSideResult) { + result.results = localResult.results.concat(serverSideResult.results).sort(compare); + result.highlights = localResult.highlights.concat(serverSideResult.highlights); + } else if (localResult) { + result.results = localResult.results; + result.highlights = localResult.highlights; + } else { + result.results = serverSideResult.results; + result.highlights = serverSideResult.highlights; + } + + if (localResult) { + previousSearchResult.seshatQuery.next_batch = localResult.next_batch; + result.next_batch = localResult.next_batch; + } + + if (serverSideResult && serverSideResult.next_batch) { + previousSearchResult.serverSideNextBatch = serverSideResult.next_batch; + result.next_batch = serverSideResult.next_batch; + } + + console.log("HELLOO COMBINING RESULTS", localResult, serverSideResult, result); + + return result +} + +async function combinedPagination(searchResult) { + const eventIndex = EventIndexPeg.get(); + const client = MatrixClientPeg.get(); + + console.log("HELLOOO WORLD"); + + const searchArgs = searchResult.seshatQuery; + + let localResult; + let serverSideResult; + + if (searchArgs.next_batch) { + localResult = await eventIndex.search(searchArgs); + } + + if (searchResult.serverSideNextBatch) { + const body = {body: searchResult._query, next_batch: searchResult.serverSideNextBatch}; + serverSideResult = await client.search(body); + } + + const combinedResult = combineResults(searchResult, localResult, serverSideResult.search_categories.room_events); + + const response = { + search_categories: { + room_events: combinedResult, + }, + }; + + const result = client._processRoomEventsSearch(searchResult, response); + + console.log("HELLO NEW RESULT", searchResult); + + searchResult.pendingRequest = null; + + return result +} + function eventIndexSearch(term, roomId = undefined) { let searchPromise; @@ -153,14 +251,22 @@ function eventIndexSearch(term, roomId = undefined) { function eventIndexSearchPagination(searchResult) { const client = MatrixClientPeg.get(); - const query = searchResult.seshatQuery; - if (!query) { + const seshatQuery = searchResult.seshatQuery; + const serverQuery = searchResult._query; + + if (!seshatQuery) { return client.backPaginateRoomEventsSearch(searchResult); - } else { - const promise = paginatedLocalSearch(searchResult); + } else if (!serverQuery) { + const promise = localPagination(searchResult); searchResult.pendingRequest = promise; + return promise; + } else { + const promise = combinedPagination(searchResult); + searchResult.pendingRequest = promise; + + return promise } } From b6198e0ab996770c7ca8a08ae2cd91e9071a5e0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 3 Jun 2020 14:16:02 +0200 Subject: [PATCH 004/194] Searching: Combine the events before letting the client process them. --- src/Searching.js | 194 +++++++++++++++++++++++++++-------------------- 1 file changed, 113 insertions(+), 81 deletions(-) diff --git a/src/Searching.js b/src/Searching.js index 1960f2a2b2..5fc5b31544 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -17,7 +17,9 @@ limitations under the License. import EventIndexPeg from "./indexing/EventIndexPeg"; import {MatrixClientPeg} from "./MatrixClientPeg"; -function serverSideSearch(term, roomId = undefined) { +async function serverSideSearch(term, roomId = undefined, processResult = true) { + const client = MatrixClientPeg.get(); + let filter; if (roomId !== undefined) { // XXX: it's unintuitive that the filter for searching doesn't have @@ -27,19 +29,59 @@ function serverSideSearch(term, roomId = undefined) { }; } - const searchPromise = MatrixClientPeg.get().searchRoomEvents({ - filter, - term, - }); + const body = { + search_categories: { + room_events: { + search_term: term, + filter: filter, + order_by: "recent", + event_context: { + before_limit: 1, + after_limit: 1, + include_profile: true, + }, + }, + }, + }; - return searchPromise; + const response = await client.search({body: body}); + + if (processResult) { + const searchResult = { + _query: body, + results: [], + highlights: [], + }; + + return client._processRoomEventsSearch(searchResult, response); + } + + const result = { + response: response, + query: body, + }; + + return result; +} + +function compareEvents(a, b) { + const aEvent = a.result; + const bEvent = b.result; + + if (aEvent.origin_server_ts > + bEvent.origin_server_ts) return -1; + if (aEvent.origin_server_ts < + bEvent.origin_server_ts) return 1; + return 0; } async function combinedSearch(searchTerm) { + const client = MatrixClientPeg.get(); + // Create two promises, one for the local search, one for the // server-side search. - const serverSidePromise = serverSideSearch(searchTerm); - const localPromise = localSearch(searchTerm); + const serverSidePromise = serverSideSearch(searchTerm, undefined, false); + const localPromise = localSearch(searchTerm, undefined, false); // Wait for both promises to resolve. await Promise.all([serverSidePromise, localPromise]); @@ -48,45 +90,34 @@ async function combinedSearch(searchTerm) { const localResult = await localPromise; const serverSideResult = await serverSidePromise; - // Combine the search results into one result. - const result = {}; + const serverQuery = serverSideResult.query; + const serverResponse = serverSideResult.response; - // Our localResult and serverSideResult are both ordered by - // recency separately, when we combine them the order might not - // be the right one so we need to sort them. - const compare = (a, b) => { - const aEvent = a.context.getEvent().event; - const bEvent = b.context.getEvent().event; + const localQuery = localResult.query; + const localResponse = localResult.response; - if (aEvent.origin_server_ts > - bEvent.origin_server_ts) return -1; - if (aEvent.origin_server_ts < - bEvent.origin_server_ts) return 1; - return 0; + const emptyResult = { + seshatQuery: localQuery, + _query: serverQuery, + serverSideNextBatch: serverResponse.next_batch, + results: [], + highlights: [], }; - result.count = localResult.count + serverSideResult.count; - result.results = localResult.results.concat( - serverSideResult.results).sort(compare); - result.highlights = localResult.highlights.concat( - serverSideResult.highlights); + const combinedResult = combineResponses(emptyResult, localResponse, serverResponse.search_categories.room_events); - result.seshatQuery = localResult.seshatQuery; - result.serverSideNextBatch = serverSideResult.next_batch; - result._query = serverSideResult._query; + const response = { + search_categories: { + room_events: combinedResult, + }, + }; - // We need the next batch to be set for the client to know that it can - // paginate further. - if (serverSideResult.next_batch) { - result.next_batch = serverSideResult.next_batch; - } else { - result.next_batch = localResult.next_batch; - } + const result = client._processRoomEventsSearch(emptyResult, response); return result; } -async function localSearch(searchTerm, roomId = undefined) { +async function localSearch(searchTerm, roomId = undefined, processResult = true) { const searchArgs = { search_term: searchTerm, before_limit: 1, @@ -110,15 +141,27 @@ async function localSearch(searchTerm, roomId = undefined) { const eventIndex = EventIndexPeg.get(); const localResult = await eventIndex.search(searchArgs); - emptyResult.seshatQuery.next_batch = localResult.next_batch; - const response = { - search_categories: { - room_events: localResult, - }, - }; + if (processResult) { + emptyResult.seshatQuery.next_batch = localResult.next_batch; - return MatrixClientPeg.get()._processRoomEventsSearch(emptyResult, response); + const response = { + search_categories: { + room_events: localResult, + }, + }; + + return MatrixClientPeg.get()._processRoomEventsSearch(emptyResult, response); + } + + searchArgs.next_batch = localResult.next_batch; + + const result = { + response: localResult, + query: searchArgs, + } + + return result; } async function localPagination(searchResult) { @@ -144,57 +187,46 @@ async function localPagination(searchResult) { /** * Combine the local and server search results */ -function combineResults(previousSearchResult, localResult = undefined, serverSideResult = undefined) { - // // cachedResults = previousSearchResult.cachedResults; - // if (localResult) { - // previousSearchResult.seshatQuery.next_batch = localResult.next_batch; - // } - const compare = (a, b) => { - const aEvent = a.result; - const bEvent = b.result; +function combineResponses(previousSearchResult, localEvents = undefined, serverEvents = undefined) { + const response = {}; - if (aEvent.origin_server_ts > - bEvent.origin_server_ts) return -1; - if (aEvent.origin_server_ts < - bEvent.origin_server_ts) return 1; - return 0; - }; - - const result = {}; - - result.count = previousSearchResult.count; - - if (localResult && serverSideResult) { - result.results = localResult.results.concat(serverSideResult.results).sort(compare); - result.highlights = localResult.highlights.concat(serverSideResult.highlights); - } else if (localResult) { - result.results = localResult.results; - result.highlights = localResult.highlights; + if (previousSearchResult.count) { + response.count = previousSearchResult.count; } else { - result.results = serverSideResult.results; - result.highlights = serverSideResult.highlights; + response.count = localEvents.count + serverEvents.count; } - if (localResult) { - previousSearchResult.seshatQuery.next_batch = localResult.next_batch; - result.next_batch = localResult.next_batch; + if (localEvents && serverEvents) { + response.results = localEvents.results.concat(serverEvents.results).sort(compareEvents); + response.highlights = localEvents.highlights.concat(serverEvents.highlights); + } else if (localEvents) { + response.results = localEvents.results; + response.highlights = localEvents.highlights; + } else { + response.results = serverEvents.results; + response.highlights = serverEvents.highlights; } - if (serverSideResult && serverSideResult.next_batch) { - previousSearchResult.serverSideNextBatch = serverSideResult.next_batch; - result.next_batch = serverSideResult.next_batch; + if (localEvents) { + previousSearchResult.seshatQuery.next_batch = localEvents.next_batch; + response.next_batch = localEvents.next_batch; } - console.log("HELLOO COMBINING RESULTS", localResult, serverSideResult, result); + if (serverEvents && serverEvents.next_batch) { + previousSearchResult.serverSideNextBatch = serverEvents.next_batch; + response.next_batch = serverEvents.next_batch; + } - return result + console.log("HELLOO COMBINING RESULTS", localEvents, serverEvents, response); + + return response } async function combinedPagination(searchResult) { const eventIndex = EventIndexPeg.get(); const client = MatrixClientPeg.get(); - console.log("HELLOOO WORLD"); + console.log("HELLOOO WORLD", searchResult.oldestEventFrom); const searchArgs = searchResult.seshatQuery; @@ -210,7 +242,7 @@ async function combinedPagination(searchResult) { serverSideResult = await client.search(body); } - const combinedResult = combineResults(searchResult, localResult, serverSideResult.search_categories.room_events); + const combinedResult = combineResponses(searchResult, localResult, serverSideResult.search_categories.room_events); const response = { search_categories: { From c2e0e10553f1bd3f08a6ea12aaa60c34f1653ea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 3 Jun 2020 14:30:21 +0200 Subject: [PATCH 005/194] Searching: Split out more logic that combines the events. --- src/Searching.js | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/Searching.js b/src/Searching.js index 5fc5b31544..1a5c8d7a48 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -184,18 +184,9 @@ async function localPagination(searchResult) { return result; } -/** - * Combine the local and server search results - */ -function combineResponses(previousSearchResult, localEvents = undefined, serverEvents = undefined) { +function combineEvents(localEvents = undefined, serverEvents = undefined, cachedEvents = undefined) { const response = {}; - if (previousSearchResult.count) { - response.count = previousSearchResult.count; - } else { - response.count = localEvents.count + serverEvents.count; - } - if (localEvents && serverEvents) { response.results = localEvents.results.concat(serverEvents.results).sort(compareEvents); response.highlights = localEvents.highlights.concat(serverEvents.highlights); @@ -207,6 +198,21 @@ function combineResponses(previousSearchResult, localEvents = undefined, serverE response.highlights = serverEvents.highlights; } + return response; +} + +/** + * Combine the local and server search responses + */ +function combineResponses(previousSearchResult, localEvents = undefined, serverEvents = undefined) { + const response = combineEvents(localEvents, serverEvents); + + if (previousSearchResult.count) { + response.count = previousSearchResult.count; + } else { + response.count = localEvents.count + serverEvents.count; + } + if (localEvents) { previousSearchResult.seshatQuery.next_batch = localEvents.next_batch; response.next_batch = localEvents.next_batch; From 28f7d23bfc674bfd899695a352f385462e8ad383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 4 Jun 2020 11:18:53 +0200 Subject: [PATCH 006/194] Searching: Correctly order the combined search results. --- src/Searching.js | 108 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 91 insertions(+), 17 deletions(-) diff --git a/src/Searching.js b/src/Searching.js index 1a5c8d7a48..42a092edb0 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -184,20 +184,78 @@ async function localPagination(searchResult) { return result; } -function combineEvents(localEvents = undefined, serverEvents = undefined, cachedEvents = undefined) { +function combineEvents(previousSearchResult, localEvents = undefined, serverEvents = undefined) { const response = {}; - if (localEvents && serverEvents) { - response.results = localEvents.results.concat(serverEvents.results).sort(compareEvents); - response.highlights = localEvents.highlights.concat(serverEvents.highlights); - } else if (localEvents) { - response.results = localEvents.results; - response.highlights = localEvents.highlights; - } else { - response.results = serverEvents.results; - response.highlights = serverEvents.highlights; + let oldestEventFrom = "server"; + let cachedEvents; + + if (previousSearchResult.oldestEventFrom) { + oldestEventFrom = previousSearchResult.oldestEventFrom; } + if (previousSearchResult.cachedEvents) { + cachedEvents = previousSearchResult.cachedEvents; + } + + if (localEvents && serverEvents) { + const oldestLocalEvent = localEvents.results[localEvents.results.length - 1].result; + const oldestServerEvent = serverEvents.results[serverEvents.results.length - 1].result; + + if (oldestLocalEvent.origin_server_ts <= oldestServerEvent.origin_server_ts) { + oldestEventFrom = "local"; + } + + const combinedEvents = localEvents.results.concat(serverEvents.results).sort(compareEvents); + response.results = combinedEvents.slice(0, 10); + response.highlights = localEvents.highlights.concat(serverEvents.highlights); + previousSearchResult.cachedEvents = combinedEvents.slice(10); + console.log("HELLOO COMBINED", combinedEvents); + } else if (localEvents) { + if (cachedEvents && cachedEvents.length > 0) { + const oldestLocalEvent = localEvents.results[localEvents.results.length - 1].result; + const oldestCachedEvent = cachedEvents[cachedEvents.length - 1].result; + + if (oldestLocalEvent.origin_server_ts <= oldestCachedEvent.origin_server_ts) { + oldestEventFrom = "local"; + } + + const combinedEvents = localEvents.results.concat(cachedEvents).sort(compareEvents); + response.results = combinedEvents.slice(0, 10); + previousSearchResult.cachedEvents = combinedEvents.slice(10); + } else { + response.results = localEvents.results; + } + + response.highlights = localEvents.highlights; + } else if (serverEvents) { + console.log("HEEEEELOO WHAT'S GOING ON", cachedEvents); + if (cachedEvents && cachedEvents.length > 0) { + const oldestServerEvent = serverEvents.results[serverEvents.results.length - 1].result; + const oldestCachedEvent = cachedEvents[cachedEvents.length - 1].result; + + if (oldestServerEvent.origin_server_ts <= oldestCachedEvent.origin_server_ts) { + oldestEventFrom = "server"; + } + + const combinedEvents = serverEvents.results.concat(cachedEvents).sort(compareEvents); + response.results = combinedEvents.slice(0, 10); + previousSearchResult.cachedEvents = combinedEvents.slice(10); + } else { + response.results = serverEvents.results; + } + response.highlights = serverEvents.highlights; + } else { + if (cachedEvents && cachedEvents.length > 0) { + response.results = cachedEvents; + } + response.highlights = []; + + delete previousSearchResult.cachedEvents; + } + + previousSearchResult.oldestEventFrom = oldestEventFrom; + return response; } @@ -205,7 +263,7 @@ function combineEvents(localEvents = undefined, serverEvents = undefined, cached * Combine the local and server search responses */ function combineResponses(previousSearchResult, localEvents = undefined, serverEvents = undefined) { - const response = combineEvents(localEvents, serverEvents); + const response = combineEvents(previousSearchResult, localEvents, serverEvents); if (previousSearchResult.count) { response.count = previousSearchResult.count; @@ -215,12 +273,20 @@ function combineResponses(previousSearchResult, localEvents = undefined, serverE if (localEvents) { previousSearchResult.seshatQuery.next_batch = localEvents.next_batch; - response.next_batch = localEvents.next_batch; } - if (serverEvents && serverEvents.next_batch) { + if (serverEvents) { previousSearchResult.serverSideNextBatch = serverEvents.next_batch; - response.next_batch = serverEvents.next_batch; + } + + if (previousSearchResult.seshatQuery.next_batch) { + response.next_batch = previousSearchResult.seshatQuery.next_batch; + } else if (previousSearchResult.serverSideNextBatch) { + response.next_batch = previousSearchResult.serverSideNextBatch; + } + + if (!response.next_batch && previousSearchResult.cachedEvents && previousSearchResult.cachedEvents.length > 0) { + response.next_batch = "cached"; } console.log("HELLOO COMBINING RESULTS", localEvents, serverEvents, response); @@ -239,16 +305,24 @@ async function combinedPagination(searchResult) { let localResult; let serverSideResult; - if (searchArgs.next_batch) { + const oldestEventFrom = searchResult.oldestEventFrom; + + if ((searchArgs.next_batch && oldestEventFrom === "server") || (!searchResult.serverSideNextBatch && searchArgs.next_batch)) { localResult = await eventIndex.search(searchArgs); } - if (searchResult.serverSideNextBatch) { + if ((searchResult.serverSideNextBatch && oldestEventFrom === "local") || (!searchArgs.next_batch && searchResult.serverSideNextBatch)) { const body = {body: searchResult._query, next_batch: searchResult.serverSideNextBatch}; serverSideResult = await client.search(body); } - const combinedResult = combineResponses(searchResult, localResult, serverSideResult.search_categories.room_events); + let serverEvents; + + if (serverSideResult) { + serverEvents = serverSideResult.search_categories.room_events; + } + + const combinedResult = combineResponses(searchResult, localResult, serverEvents); const response = { search_categories: { From c6462e11ecd385eb1bb6c0c7f5279aaef1b8584a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 4 Jun 2020 12:12:09 +0200 Subject: [PATCH 007/194] Searching: Fix some lint issues. --- src/Searching.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Searching.js b/src/Searching.js index 42a092edb0..6932836273 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -159,7 +159,7 @@ async function localSearch(searchTerm, roomId = undefined, processResult = true) const result = { response: localResult, query: searchArgs, - } + }; return result; } @@ -291,7 +291,7 @@ function combineResponses(previousSearchResult, localEvents = undefined, serverE console.log("HELLOO COMBINING RESULTS", localEvents, serverEvents, response); - return response + return response; } async function combinedPagination(searchResult) { @@ -307,11 +307,13 @@ async function combinedPagination(searchResult) { const oldestEventFrom = searchResult.oldestEventFrom; - if ((searchArgs.next_batch && oldestEventFrom === "server") || (!searchResult.serverSideNextBatch && searchArgs.next_batch)) { + if ((searchArgs.next_batch && oldestEventFrom === "server") || + (!searchResult.serverSideNextBatch && searchArgs.next_batch)) { localResult = await eventIndex.search(searchArgs); } - if ((searchResult.serverSideNextBatch && oldestEventFrom === "local") || (!searchArgs.next_batch && searchResult.serverSideNextBatch)) { + if ((searchResult.serverSideNextBatch && oldestEventFrom === "local") || + (!searchArgs.next_batch && searchResult.serverSideNextBatch)) { const body = {body: searchResult._query, next_batch: searchResult.serverSideNextBatch}; serverSideResult = await client.search(body); } @@ -336,7 +338,7 @@ async function combinedPagination(searchResult) { searchResult.pendingRequest = null; - return result + return result; } function eventIndexSearch(term, roomId = undefined) { @@ -378,7 +380,7 @@ function eventIndexSearchPagination(searchResult) { const promise = combinedPagination(searchResult); searchResult.pendingRequest = promise; - return promise + return promise; } } From df0659431456d035b323b1a2a6477a84b7cf5af0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 4 Jun 2020 15:25:02 +0200 Subject: [PATCH 008/194] Searching: Refactor out some code that combines the search results. --- src/Searching.js | 100 ++++++++++++++++++++--------------------------- 1 file changed, 43 insertions(+), 57 deletions(-) diff --git a/src/Searching.js b/src/Searching.js index 6932836273..11652a6019 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -17,6 +17,8 @@ limitations under the License. import EventIndexPeg from "./indexing/EventIndexPeg"; import {MatrixClientPeg} from "./MatrixClientPeg"; +const SEARCH_LIMIT = 10; + async function serverSideSearch(term, roomId = undefined, processResult = true) { const client = MatrixClientPeg.get(); @@ -26,6 +28,7 @@ async function serverSideSearch(term, roomId = undefined, processResult = true) // the same shape as the v2 filter API :( filter = { rooms: [roomId], + limit: SEARCH_LIMIT, }; } @@ -96,16 +99,21 @@ async function combinedSearch(searchTerm) { const localQuery = localResult.query; const localResponse = localResult.response; + // Store our queries for later on so we can support pagination. const emptyResult = { seshatQuery: localQuery, _query: serverQuery, serverSideNextBatch: serverResponse.next_batch, + cachedEvents: [], + oldestEventFrom: "server", results: [], highlights: [], }; + // Combine our results. const combinedResult = combineResponses(emptyResult, localResponse, serverResponse.search_categories.room_events); + // Let the client process the combined result. const response = { search_categories: { room_events: combinedResult, @@ -122,6 +130,7 @@ async function localSearch(searchTerm, roomId = undefined, processResult = true) search_term: searchTerm, before_limit: 1, after_limit: 1, + limit: SEARCH_LIMIT, order_by_recency: true, room_id: undefined, }; @@ -184,73 +193,53 @@ async function localPagination(searchResult) { return result; } +function compareOldestEvents(firstResults, secondResults) { + try { + const oldestFirstEvent = firstResults.results[firstResults.results.length - 1].result; + const oldestSecondEvent = secondResults.results[secondResults.results.length - 1].result; + + if (oldestFirstEvent.origin_server_ts <= oldestSecondEvent.origin_server_ts) { + return -1 + } else { + return 1 + } + } catch { + return 0 + } +} + +function combineEventSources(previousSearchResult, response, a, b) { + const combinedEvents = a.concat(b).sort(compareEvents); + response.results = combinedEvents.slice(0, SEARCH_LIMIT); + previousSearchResult.cachedEvents = combinedEvents.slice(SEARCH_LIMIT); +} + function combineEvents(previousSearchResult, localEvents = undefined, serverEvents = undefined) { const response = {}; - let oldestEventFrom = "server"; - let cachedEvents; - - if (previousSearchResult.oldestEventFrom) { - oldestEventFrom = previousSearchResult.oldestEventFrom; - } - - if (previousSearchResult.cachedEvents) { - cachedEvents = previousSearchResult.cachedEvents; - } + const cachedEvents = previousSearchResult.cachedEvents; + let oldestEventFrom = previousSearchResult.oldestEventFrom; + response.highlights = previousSearchResult.highlights; if (localEvents && serverEvents) { - const oldestLocalEvent = localEvents.results[localEvents.results.length - 1].result; - const oldestServerEvent = serverEvents.results[serverEvents.results.length - 1].result; - - if (oldestLocalEvent.origin_server_ts <= oldestServerEvent.origin_server_ts) { + if (compareOldestEvents(localEvents, serverEvents) < 0) { oldestEventFrom = "local"; } - const combinedEvents = localEvents.results.concat(serverEvents.results).sort(compareEvents); - response.results = combinedEvents.slice(0, 10); + combineEventSources(previousSearchResult, response, localEvents.results, serverEvents.results); response.highlights = localEvents.highlights.concat(serverEvents.highlights); - previousSearchResult.cachedEvents = combinedEvents.slice(10); - console.log("HELLOO COMBINED", combinedEvents); } else if (localEvents) { - if (cachedEvents && cachedEvents.length > 0) { - const oldestLocalEvent = localEvents.results[localEvents.results.length - 1].result; - const oldestCachedEvent = cachedEvents[cachedEvents.length - 1].result; - - if (oldestLocalEvent.origin_server_ts <= oldestCachedEvent.origin_server_ts) { - oldestEventFrom = "local"; - } - - const combinedEvents = localEvents.results.concat(cachedEvents).sort(compareEvents); - response.results = combinedEvents.slice(0, 10); - previousSearchResult.cachedEvents = combinedEvents.slice(10); - } else { - response.results = localEvents.results; + if (compareOldestEvents(localEvents, cachedEvents) < 0) { + oldestEventFrom = "local"; } - - response.highlights = localEvents.highlights; + combineEventSources(previousSearchResult, response, localEvents.results, cachedEvents); } else if (serverEvents) { - console.log("HEEEEELOO WHAT'S GOING ON", cachedEvents); - if (cachedEvents && cachedEvents.length > 0) { - const oldestServerEvent = serverEvents.results[serverEvents.results.length - 1].result; - const oldestCachedEvent = cachedEvents[cachedEvents.length - 1].result; - - if (oldestServerEvent.origin_server_ts <= oldestCachedEvent.origin_server_ts) { - oldestEventFrom = "server"; - } - - const combinedEvents = serverEvents.results.concat(cachedEvents).sort(compareEvents); - response.results = combinedEvents.slice(0, 10); - previousSearchResult.cachedEvents = combinedEvents.slice(10); - } else { - response.results = serverEvents.results; + if (compareOldestEvents(serverEvents, cachedEvents) < 0) { + oldestEventFrom = "server"; } - response.highlights = serverEvents.highlights; + combineEventSources(previousSearchResult, response, serverEvents.results, cachedEvents); } else { - if (cachedEvents && cachedEvents.length > 0) { - response.results = cachedEvents; - } - response.highlights = []; - + response.results = cachedEvents; delete previousSearchResult.cachedEvents; } @@ -298,15 +287,12 @@ async function combinedPagination(searchResult) { const eventIndex = EventIndexPeg.get(); const client = MatrixClientPeg.get(); - console.log("HELLOOO WORLD", searchResult.oldestEventFrom); - const searchArgs = searchResult.seshatQuery; + const oldestEventFrom = searchResult.oldestEventFrom; let localResult; let serverSideResult; - const oldestEventFrom = searchResult.oldestEventFrom; - if ((searchArgs.next_batch && oldestEventFrom === "server") || (!searchResult.serverSideNextBatch && searchArgs.next_batch)) { localResult = await eventIndex.search(searchArgs); From 96ca47381c94ea4219a326bb56d37ad51c210ac5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 4 Jun 2020 15:25:32 +0200 Subject: [PATCH 009/194] Searching: Add more docs explaining what's going on in the pagination. --- src/Searching.js | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/Searching.js b/src/Searching.js index 11652a6019..7ea1463a5e 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -71,10 +71,9 @@ function compareEvents(a, b) { const aEvent = a.result; const bEvent = b.result; - if (aEvent.origin_server_ts > - bEvent.origin_server_ts) return -1; - if (aEvent.origin_server_ts < - bEvent.origin_server_ts) return 1; + if (aEvent.origin_server_ts > bEvent.origin_server_ts) return -1; + if (aEvent.origin_server_ts < bEvent.origin_server_ts) return 1; + return 0; } @@ -254,26 +253,37 @@ function combineEvents(previousSearchResult, localEvents = undefined, serverEven function combineResponses(previousSearchResult, localEvents = undefined, serverEvents = undefined) { const response = combineEvents(previousSearchResult, localEvents, serverEvents); + // Our first search will contain counts from both sources, subsequent + // pagination requests will fetch responses only from one of the sources, so + // reuse the first count when we're paginating. if (previousSearchResult.count) { response.count = previousSearchResult.count; } else { response.count = localEvents.count + serverEvents.count; } + // Update our next batch tokens for the given search sources. if (localEvents) { previousSearchResult.seshatQuery.next_batch = localEvents.next_batch; } - if (serverEvents) { previousSearchResult.serverSideNextBatch = serverEvents.next_batch; } + // Set the response next batch token to one of the tokens from the sources, + // this makes sure that if we exhaust one of the sources we continue with + // the other one. if (previousSearchResult.seshatQuery.next_batch) { response.next_batch = previousSearchResult.seshatQuery.next_batch; } else if (previousSearchResult.serverSideNextBatch) { response.next_batch = previousSearchResult.serverSideNextBatch; } + // We collected all search results from the server as well as from Seshat, + // we still have some events cached that we'll want to display on the next + // pagination request. + // + // Provide a fake next batch token for that case. if (!response.next_batch && previousSearchResult.cachedEvents && previousSearchResult.cachedEvents.length > 0) { response.next_batch = "cached"; } @@ -356,13 +366,18 @@ function eventIndexSearchPagination(searchResult) { const serverQuery = searchResult._query; if (!seshatQuery) { + // This is a search in a non-encrypted room. Do the normal server-side + // pagination. return client.backPaginateRoomEventsSearch(searchResult); } else if (!serverQuery) { + // This is a search in a encrypted room. Do a local pagination. const promise = localPagination(searchResult); searchResult.pendingRequest = promise; return promise; } else { + // We have both queries around, this is a search across all rooms so a + // combined pagination needs to be done. const promise = combinedPagination(searchResult); searchResult.pendingRequest = promise; From 46fd36568affdb948ca3d632027ae56f2301d135 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 4 Jun 2020 15:33:51 +0200 Subject: [PATCH 010/194] Searching: Always set the limit when searching. The spec doesn't mention any default limit so different homeservers might use different defaults, since we don't want Riot to behave differently depending on the homeserver bite the bullet of sending an additional 10 or so bytes when searching. --- src/Searching.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/Searching.js b/src/Searching.js index 7ea1463a5e..f40e44a84d 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -22,15 +22,11 @@ const SEARCH_LIMIT = 10; async function serverSideSearch(term, roomId = undefined, processResult = true) { const client = MatrixClientPeg.get(); - let filter; - if (roomId !== undefined) { - // XXX: it's unintuitive that the filter for searching doesn't have - // the same shape as the v2 filter API :( - filter = { - rooms: [roomId], - limit: SEARCH_LIMIT, - }; - } + const filter = { + limit: SEARCH_LIMIT, + }; + + if (roomId !== undefined) filter.rooms = [roomId]; const body = { search_categories: { From 94d4aa17ee738bc10c78f138da04c8b23d1d4ab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 4 Jun 2020 15:35:18 +0200 Subject: [PATCH 011/194] Searching: Remove some debug logs. --- src/Searching.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Searching.js b/src/Searching.js index f40e44a84d..cb20176804 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -284,8 +284,6 @@ function combineResponses(previousSearchResult, localEvents = undefined, serverE response.next_batch = "cached"; } - console.log("HELLOO COMBINING RESULTS", localEvents, serverEvents, response); - return response; } @@ -326,8 +324,6 @@ async function combinedPagination(searchResult) { const result = client._processRoomEventsSearch(searchResult, response); - console.log("HELLO NEW RESULT", searchResult); - searchResult.pendingRequest = null; return result; From bf13032d5b9c14ab675db164b09a14ca7cb4e0de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 4 Jun 2020 15:51:06 +0200 Subject: [PATCH 012/194] Searching: Fix a lint issue and expand some docs. --- src/Searching.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Searching.js b/src/Searching.js index cb20176804..29556f0757 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -194,12 +194,12 @@ function compareOldestEvents(firstResults, secondResults) { const oldestSecondEvent = secondResults.results[secondResults.results.length - 1].result; if (oldestFirstEvent.origin_server_ts <= oldestSecondEvent.origin_server_ts) { - return -1 + return -1; } else { - return 1 + return 1; } } catch { - return 0 + return 0; } } @@ -209,6 +209,19 @@ function combineEventSources(previousSearchResult, response, a, b) { previousSearchResult.cachedEvents = combinedEvents.slice(SEARCH_LIMIT); } +/** + * Combine the events from our event sources into a sorted result + * + * @param {object} previousSearchResult A search result from a previous search + * call. + * @param {object} localEvents An unprocessed search result from the event + * index. + * @param {object} serverEvents An unprocessed search result from the server. + * + * @ return {object} A response object that combines the events from the + * different event sources. + * + */ function combineEvents(previousSearchResult, localEvents = undefined, serverEvents = undefined) { const response = {}; From 5dfe5ac135458710a185d5416e3de944e176abb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 4 Jun 2020 16:57:28 +0200 Subject: [PATCH 013/194] Searching: More docs and a bit of simplification. --- src/Searching.js | 134 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 127 insertions(+), 7 deletions(-) diff --git a/src/Searching.js b/src/Searching.js index 29556f0757..d2fd67fa27 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -204,7 +204,9 @@ function compareOldestEvents(firstResults, secondResults) { } function combineEventSources(previousSearchResult, response, a, b) { + // Merge event sources and sort the events. const combinedEvents = a.concat(b).sort(compareEvents); + // Put half of the events in the response, and cache the other half. response.results = combinedEvents.slice(0, SEARCH_LIMIT); previousSearchResult.cachedEvents = combinedEvents.slice(SEARCH_LIMIT); } @@ -212,13 +214,104 @@ function combineEventSources(previousSearchResult, response, a, b) { /** * Combine the events from our event sources into a sorted result * + * This method will first be called from the combinedSearch() method. In this + * case we will fetch SEARCH_LIMIT events from the server and the local index. + * + * The method will put the SEARCH_LIMIT newest events from the server and the + * local index in the results part of the response, the rest will be put in the + * cachedEvents field of the previousSearchResult (in this case an empty search + * result). + * + * Every subsequent call will be made from the combinedPagination() method, in + * this case we will combine the cachedEvents and the next SEARCH_LIMIT events + * from either the server or the local index. + * + * Since we have two event sources and we need to sort the results by date we + * need keep on looking for the oldest event. We are implementing a variation of + * a sliding window. + * + * If we set SEARCH_LIMIT to 3: + * + * Server events [01, 02, 04, 06, 07, 08, 11, 13] + * |01, 02, 04| + * Local events [03, 05, 09, 10, 12, 14, 15, 16] + * |03, 05, 09| + * + * We note that the oldest event is from the local index, and we combine the + * results: + * + * Server window [01, 02, 04] + * Local window [03, 05, 09] + * + * Combined events [01, 02, 03, 04, 05, 09] + * + * We split the combined result in the part that we want to present and a part + * that will be cached. + * + * Presented events [01, 02, 03] + * Cached events [04, 05, 09] + * + * We slide the window for the server since the oldest event is from the local + * index. + * + * Server events [01, 02, 04, 06, 07, 08, 11, 13] + * |06, 07, 08| + * Local events [03, 05, 09, 10, 12, 14, 15, 16] + * |XX, XX, XX| + * Cached events [04, 05, 09] + * + * We note that the oldest event is from the server and we combine the new + * server events with the cached ones. + * + * Cached events [04, 05, 09] + * Server events [06, 07, 08] + * + * Combined events [04, 05, 06, 07, 08, 09] + * + * We split again. + * + * Presented events [04, 05, 06] + * Cached events [07, 08, 09] + * + * We slide the local window, the oldest event is on the server. + * + * Server events [01, 02, 04, 06, 07, 08, 11, 13] + * |XX, XX, XX| + * Local events [03, 05, 09, 10, 12, 14, 15, 16] + * |10, 12, 14| + * + * Cached events [07, 08, 09] + * Local events [10, 12, 14] + * Combined events [07, 08, 09, 10, 12, 14] + * + * Presented events [07, 08, 09] + * Cached events [10, 12, 14] + * + * Next up we slide the server window again. + * + * Server events [01, 02, 04, 06, 07, 08, 11, 13] + * |11, 13| + * Local events [03, 05, 09, 10, 12, 14, 15, 16] + * |XX, XX, XX| + * + * Cached events [10, 12, 14] + * Server events [11, 13] + * Combined events [10, 11, 12, 13, 14] + * + * Presented events [10, 11, 12] + * Cached events [13, 14] + * + * We have one source exhausted, we fetch the rest of our events from the other + * source and combine it with our cached events. + * + * * @param {object} previousSearchResult A search result from a previous search * call. * @param {object} localEvents An unprocessed search result from the event * index. * @param {object} serverEvents An unprocessed search result from the server. * - * @ return {object} A response object that combines the events from the + * @return {object} A response object that combines the events from the * different event sources. * */ @@ -230,6 +323,9 @@ function combineEvents(previousSearchResult, localEvents = undefined, serverEven response.highlights = previousSearchResult.highlights; if (localEvents && serverEvents) { + // This is a first search call, combine the events from the server and + // the local index. Note where our oldest event came from, we shall + // fetch the next batch of events from the other source. if (compareOldestEvents(localEvents, serverEvents) < 0) { oldestEventFrom = "local"; } @@ -237,18 +333,28 @@ function combineEvents(previousSearchResult, localEvents = undefined, serverEven combineEventSources(previousSearchResult, response, localEvents.results, serverEvents.results); response.highlights = localEvents.highlights.concat(serverEvents.highlights); } else if (localEvents) { + // This is a pagination call fetching more events from the local index, + // meaning that our oldest event was on the server. + // Change the source of the oldest event if our local event is older + // than the cached one. if (compareOldestEvents(localEvents, cachedEvents) < 0) { oldestEventFrom = "local"; } combineEventSources(previousSearchResult, response, localEvents.results, cachedEvents); } else if (serverEvents) { + // This is a pagination call fetching more events from the server, + // meaning that our oldest event was in the local index. + // Change the source of the oldest event if our server event is older + // than the cached one. if (compareOldestEvents(serverEvents, cachedEvents) < 0) { oldestEventFrom = "server"; } combineEventSources(previousSearchResult, response, serverEvents.results, cachedEvents); } else { + // This is a pagination call where we exhausted both of our event + // sources, let's push the remaining cached events. response.results = cachedEvents; - delete previousSearchResult.cachedEvents; + previousSearchResult.cachedEvents = []; } previousSearchResult.oldestEventFrom = oldestEventFrom; @@ -258,8 +364,18 @@ function combineEvents(previousSearchResult, localEvents = undefined, serverEven /** * Combine the local and server search responses + * + * @param {object} previousSearchResult A search result from a previous search + * call. + * @param {object} localEvents An unprocessed search result from the event + * index. + * @param {object} serverEvents An unprocessed search result from the server. + * + * @return {object} A response object that combines the events from the + * different event sources. */ function combineResponses(previousSearchResult, localEvents = undefined, serverEvents = undefined) { + // Combine our events first. const response = combineEvents(previousSearchResult, localEvents, serverEvents); // Our first search will contain counts from both sources, subsequent @@ -293,7 +409,7 @@ function combineResponses(previousSearchResult, localEvents = undefined, serverE // pagination request. // // Provide a fake next batch token for that case. - if (!response.next_batch && previousSearchResult.cachedEvents && previousSearchResult.cachedEvents.length > 0) { + if (!response.next_batch && previousSearchResult.cachedEvents.length > 0) { response.next_batch = "cached"; } @@ -310,13 +426,15 @@ async function combinedPagination(searchResult) { let localResult; let serverSideResult; - if ((searchArgs.next_batch && oldestEventFrom === "server") || - (!searchResult.serverSideNextBatch && searchArgs.next_batch)) { + // Fetch events from the local index if we have a token for itand if it's + // the local indexes turn or the server has exhausted its results. + if (searchArgs.next_batch && (!searchResult.serverSideNextBatch || oldestEventFrom === "server")) { localResult = await eventIndex.search(searchArgs); } - if ((searchResult.serverSideNextBatch && oldestEventFrom === "local") || - (!searchArgs.next_batch && searchResult.serverSideNextBatch)) { + // Fetch events from the server if we have a token for it and if it's the + // local indexes turn or the local index has exhausted its results. + if (searchResult.serverSideNextBatch && (oldestEventFrom === "local" || !searchArgs.next_batch)) { const body = {body: searchResult._query, next_batch: searchResult.serverSideNextBatch}; serverSideResult = await client.search(body); } @@ -327,6 +445,7 @@ async function combinedPagination(searchResult) { serverEvents = serverSideResult.search_categories.room_events; } + // Combine our events. const combinedResult = combineResponses(searchResult, localResult, serverEvents); const response = { @@ -335,6 +454,7 @@ async function combinedPagination(searchResult) { }, }; + // Let the client process the combined result. const result = client._processRoomEventsSearch(searchResult, response); searchResult.pendingRequest = null; From 68e555a0c6641b0061b55f0cf59044e1a2c6c8b2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 5 Jun 2020 16:40:20 +0100 Subject: [PATCH 014/194] Support accounts with cross signing but no SSSS At least at the login stage. Fixes https://github.com/vector-im/riot-web/issues/13894 --- src/components/structures/MatrixChat.tsx | 10 ++++--- .../structures/auth/SetupEncryptionBody.js | 30 +++++++++++++------ src/i18n/strings/en_EN.json | 1 + src/stores/SetupEncryptionStore.js | 2 +- 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 7aaedcfb09..b5b77e3ae6 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -1900,10 +1900,12 @@ export default class MatrixChat extends React.PureComponent { return setLoggedInPromise; } - // Test for the master cross-signing key in SSSS as a quick proxy for - // whether cross-signing has been set up on the account. - const masterKeyInStorage = !!cli.getAccountData("m.cross_signing.master"); - if (masterKeyInStorage) { + // wait for the client to finish downloading cross-signing keys for us so we + // know whether or not we have keys set up on this account + await cli.downloadKeys([cli.getUserId()]); + + const crossSigningIsSetUp = cli.getStoredCrossSigningForUser(cli.getUserId()); + if (crossSigningIsSetUp) { this.setStateForNewView({ view: Views.COMPLETE_SECURITY }); } else if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) { this.setStateForNewView({ view: Views.E2E_SETUP }); diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.js index 7886ed26dd..4cc5c5ef75 100644 --- a/src/components/structures/auth/SetupEncryptionBody.js +++ b/src/components/structures/auth/SetupEncryptionBody.js @@ -16,7 +16,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import { _t } from '../../../languageHandler'; +import { _t, _td } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import * as sdk from '../../../index'; import withValidation from '../../views/elements/Validation'; @@ -196,11 +196,27 @@ export default class SetupEncryptionBody extends React.Component { } else if (phase === PHASE_INTRO) { const store = SetupEncryptionStore.sharedInstance(); let recoveryKeyPrompt; - if (keyHasPassphrase(store.keyInfo)) { + if (store.keyInfo && keyHasPassphrase(store.keyInfo)) { recoveryKeyPrompt = _t("Use Recovery Key or Passphrase"); - } else { + } else if (store.keyInfo) { recoveryKeyPrompt = _t("Use Recovery Key"); } + + let useRecoveryKeyButton; + let resetKeysCaption; + if (recoveryKeyPrompt) { + useRecoveryKeyButton = + {recoveryKeyPrompt} + ; + resetKeysCaption = _td( + "If you've forgotten your recovery key you can ", + ); + } else { + resetKeysCaption = _td( + "If you have no other devices you can ", + ); + } + return (

{_t( @@ -224,16 +240,12 @@ export default class SetupEncryptionBody extends React.Component {

- - {recoveryKeyPrompt} - + {useRecoveryKeyButton} {_t("Skip")}
-
{_t( - "If you've forgotten your recovery key you can " + - "", {}, { +
{_t(resetKeysCaption, {}, { button: sub => diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e54a4d8662..416d1debe7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2101,6 +2101,7 @@ "Looks good!": "Looks good!", "Use Recovery Key or Passphrase": "Use Recovery Key or Passphrase", "Use Recovery Key": "Use Recovery Key", + "If you have no other devices you can ": "If you have no other devices you can ", "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.", "This requires the latest Riot on your other devices:": "This requires the latest Riot on your other devices:", "or another cross-signing capable Matrix client": "or another cross-signing capable Matrix client", diff --git a/src/stores/SetupEncryptionStore.js b/src/stores/SetupEncryptionStore.js index cc64e24a03..e155a5c29f 100644 --- a/src/stores/SetupEncryptionStore.js +++ b/src/stores/SetupEncryptionStore.js @@ -68,7 +68,7 @@ export class SetupEncryptionStore extends EventEmitter { async fetchKeyInfo() { const keys = await MatrixClientPeg.get().isSecretStored('m.cross_signing.master', false); - if (Object.keys(keys).length === 0) { + if (!keys || Object.keys(keys).length === 0) { this.keyId = null; this.keyInfo = null; } else { From 7c59e397102310e5987ef1d0640f46658e2840cd Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 5 Jun 2020 17:52:09 +0100 Subject: [PATCH 015/194] Sort out what we wait for after login We were waiting only for the client to become logged in rather than for setLoggedIn() to finish but then we were waiting for the first sync to complete which is far longer. We need setLoggedIn to have finished for crypto to be set up so we can query cross-signing keys, so just wait for that anyway, the logic becomes a lot simpler and we're waiting the same amount of time because we have to wait for the first sync to finish. We can also download keys in parallel. --- src/components/structures/MatrixChat.tsx | 43 +++++++++--------------- 1 file changed, 15 insertions(+), 28 deletions(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index b5b77e3ae6..4d85108aaf 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -1868,42 +1868,31 @@ export default class MatrixChat extends React.PureComponent { this.accountPasswordTimer = null; }, 60 * 5 * 1000); - // Wait for the client to be logged in (but not started) - // which is enough to ask the server about account data. - const loggedIn = new Promise(resolve => { - const actionHandlerRef = dis.register(payload => { - if (payload.action !== "on_logged_in") { - return; - } - dis.unregister(actionHandlerRef); - resolve(); - }); - }); - - // Create and start the client in the background - const setLoggedInPromise = Lifecycle.setLoggedIn(credentials); - await loggedIn; + // Create and start the client + await Lifecycle.setLoggedIn(credentials); const cli = MatrixClientPeg.get(); - // We're checking `isCryptoAvailable` here instead of `isCryptoEnabled` - // because the client hasn't been started yet. - const cryptoAvailable = isCryptoAvailable(); - if (!cryptoAvailable) { + const cryptoEnabled = cli.isCryptoEnabled(); + if (!cryptoEnabled) { this.onLoggedIn(); } - this.setState({ pendingInitialSync: true }); - await this.firstSyncPromise.promise; + const promisesList = [this.firstSyncPromise.promise]; + if (cryptoEnabled) { + // wait for the client to finish downloading cross-signing keys for us so we + // know whether or not we have keys set up on this account + promisesList.push(cli.downloadKeys([cli.getUserId()])); + } - if (!cryptoAvailable) { + this.setState({ pendingInitialSync: true }); + + await Promise.all(promisesList); + + if (!cryptoEnabled) { this.setState({ pendingInitialSync: false }); return setLoggedInPromise; } - // wait for the client to finish downloading cross-signing keys for us so we - // know whether or not we have keys set up on this account - await cli.downloadKeys([cli.getUserId()]); - const crossSigningIsSetUp = cli.getStoredCrossSigningForUser(cli.getUserId()); if (crossSigningIsSetUp) { this.setStateForNewView({ view: Views.COMPLETE_SECURITY }); @@ -1913,8 +1902,6 @@ export default class MatrixChat extends React.PureComponent { this.onLoggedIn(); } this.setState({ pendingInitialSync: false }); - - return setLoggedInPromise; }; // complete security / e2e setup has finished From ed7f0fd95f0b72a7dc2c46e037ec84981ea01687 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 5 Jun 2020 18:08:25 +0100 Subject: [PATCH 016/194] This promise doesn't exist anymore --- src/components/structures/MatrixChat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 4d85108aaf..ec65fd6957 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -1890,7 +1890,7 @@ export default class MatrixChat extends React.PureComponent { if (!cryptoEnabled) { this.setState({ pendingInitialSync: false }); - return setLoggedInPromise; + return; } const crossSigningIsSetUp = cli.getStoredCrossSigningForUser(cli.getUserId()); From 39bcd8d56d1e51e62c4559daae97a867bd887741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 21 May 2020 10:10:15 +0200 Subject: [PATCH 017/194] EventIndex: Add a checkpoint if a room turns into a encrypted one. --- src/indexing/EventIndex.js | 69 +++++++++++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 15 deletions(-) diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index fac7c92b65..3f08574d7a 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -182,6 +182,14 @@ export default class EventIndex extends EventEmitter { return; } + if (ev.getType() === "m.room.encryption") { + console.log("EventIndex: Adding checkpoint for newly encrypted room", + room.roomId); + + this.addRoomCheckpoint(room.roomId, true); + return; + } + // If the event is not yet decrypted mark it for the // Event.decrypted callback. if (ev.isBeingDecrypted()) { @@ -234,26 +242,12 @@ export default class EventIndex extends EventEmitter { */ onTimelineReset = async (room, timelineSet, resetAllTimelines) => { if (room === null) return; - - const indexManager = PlatformPeg.get().getEventIndexingManager(); if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; - const timeline = room.getLiveTimeline(); - const token = timeline.getPaginationToken("b"); - - const backwardsCheckpoint = { - roomId: room.roomId, - token: token, - fullCrawl: false, - direction: "b", - }; - console.log("EventIndex: Added checkpoint because of a limited timeline", backwardsCheckpoint); - await indexManager.addCrawlerCheckpoint(backwardsCheckpoint); - - this.crawlerCheckpoints.push(backwardsCheckpoint); + this.addRoomCheckpoint(room.roomId, false); } /** @@ -319,6 +313,51 @@ export default class EventIndex extends EventEmitter { this.emit("changedCheckpoint", this.currentRoom()); } + async addEventsFromLiveTimeline(timeline) { + let events = timeline.getEvents(); + + for (let i = 0; i < events.length; i++) { + const ev = events[i]; + await this.addLiveEventToIndex(ev); + } + } + + async addRoomCheckpoint(roomId, fullCrawl = false) { + const indexManager = PlatformPeg.get().getEventIndexingManager(); + const client = MatrixClientPeg.get(); + const room = client.getRoom(roomId); + + if (!room) return; + + const timeline = room.getLiveTimeline(); + let token = timeline.getPaginationToken("b"); + + if(!token) { + // The room doesn't contain any tokens, meaning the live timeline + // contains all the events, add those to the index. + await this.addEventsFromLiveTimeline(timeline); + return; + } + + const checkpoint = { + roomId: room.roomId, + token: token, + fullCrawl: fullCrawl, + direction: "b", + }; + + console.log("EventIndex: Adding checkpoint", checkpoint); + + try{ + await indexManager.addCrawlerCheckpoint(checkpoint); + } catch (e) { + console.log("EventIndex: Error adding new checkpoint for room", + room.roomId, checkpoint, e); + } + + this.crawlerCheckpoints.push(checkpoint); + } + /** * The main crawler loop. * From f802668fff31587d541bb3059a4b42db5fca4c88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 21 May 2020 10:10:46 +0200 Subject: [PATCH 018/194] EventIndex: Add a missing await. --- src/indexing/EventIndex.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index 3f08574d7a..aefe849370 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -302,7 +302,7 @@ export default class EventIndex extends EventEmitter { avatar_url: ev.sender.getMxcAvatarUrl(), }; - indexManager.addEventToIndex(e, profile); + await indexManager.addEventToIndex(e, profile); } /** From ea35fc2881d8b47261d5f46fe0b3ea9e7f53b594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 21 May 2020 12:35:47 +0200 Subject: [PATCH 019/194] EventIndex: Fix some lint issues. --- src/indexing/EventIndex.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index aefe849370..f67738ca68 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -244,8 +244,8 @@ export default class EventIndex extends EventEmitter { if (room === null) return; if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; - console.log("EventIndex: Added checkpoint because of a limited timeline", - backwardsCheckpoint); + console.log("EventIndex: Adding a checkpoint because of a limited timeline", + room.roomId); this.addRoomCheckpoint(room.roomId, false); } @@ -314,7 +314,7 @@ export default class EventIndex extends EventEmitter { } async addEventsFromLiveTimeline(timeline) { - let events = timeline.getEvents(); + const events = timeline.getEvents(); for (let i = 0; i < events.length; i++) { const ev = events[i]; @@ -330,9 +330,9 @@ export default class EventIndex extends EventEmitter { if (!room) return; const timeline = room.getLiveTimeline(); - let token = timeline.getPaginationToken("b"); + const token = timeline.getPaginationToken("b"); - if(!token) { + if (!token) { // The room doesn't contain any tokens, meaning the live timeline // contains all the events, add those to the index. await this.addEventsFromLiveTimeline(timeline); @@ -348,7 +348,7 @@ export default class EventIndex extends EventEmitter { console.log("EventIndex: Adding checkpoint", checkpoint); - try{ + try { await indexManager.addCrawlerCheckpoint(checkpoint); } catch (e) { console.log("EventIndex: Error adding new checkpoint for room", From 7a2bb4b112daec2392f04b64dfc067aeb35af6bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 8 Jun 2020 16:43:20 +0200 Subject: [PATCH 020/194] EventIndex: Check if a newly encrypted room is indexed before adding a checkpoint. --- src/indexing/BaseEventIndexManager.ts | 13 +++++++++++ src/indexing/EventIndex.js | 33 ++++++++++++++++++++------- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/indexing/BaseEventIndexManager.ts b/src/indexing/BaseEventIndexManager.ts index c40d1300ea..32ab3b34fe 100644 --- a/src/indexing/BaseEventIndexManager.ts +++ b/src/indexing/BaseEventIndexManager.ts @@ -134,6 +134,19 @@ export default abstract class BaseEventIndexManager { throw new Error("Unimplemented"); } + /** + * Check if the room with the given id is already indexed. + * + * @param {string} roomId The ID of the room which we want to check if it + * has been already indexed. + * + * @return {Promise} Returns true if the index contains events for + * the given room, false otherwise. + */ + isRoomIndexed(roomId: string): Promise { + throw new Error("Unimplemented"); + } + /** * Get statistical information of the index. * diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index f67738ca68..fe7c71cfa6 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -62,6 +62,7 @@ export default class EventIndex extends EventEmitter { client.on('Event.decrypted', this.onEventDecrypted); client.on('Room.timelineReset', this.onTimelineReset); client.on('Room.redaction', this.onRedaction); + client.on('RoomState.events', this.onRoomStateEvent); } /** @@ -76,6 +77,7 @@ export default class EventIndex extends EventEmitter { client.removeListener('Event.decrypted', this.onEventDecrypted); client.removeListener('Room.timelineReset', this.onTimelineReset); client.removeListener('Room.redaction', this.onRedaction); + client.removeListener('RoomState.events', this.onRoomStateEvent); } /** @@ -182,14 +184,6 @@ export default class EventIndex extends EventEmitter { return; } - if (ev.getType() === "m.room.encryption") { - console.log("EventIndex: Adding checkpoint for newly encrypted room", - room.roomId); - - this.addRoomCheckpoint(room.roomId, true); - return; - } - // If the event is not yet decrypted mark it for the // Event.decrypted callback. if (ev.isBeingDecrypted()) { @@ -202,6 +196,15 @@ export default class EventIndex extends EventEmitter { } } + onRoomStateEvent = async (ev, state) => { + if (!MatrixClientPeg.get().isRoomEncrypted(state.roomId)) return; + + if (ev.getType() === "m.room.encryption" && !await this.isRoomIndexed(state.roomId)) { + console.log("EventIndex: Adding a checkpoint for a newly encrypted room", room.roomId); + this.addRoomCheckpoint(state.roomId, true); + } + } + /* * The Event.decrypted listener. * @@ -847,6 +850,20 @@ export default class EventIndex extends EventEmitter { return indexManager.getStats(); } + /** + * Check if the room with the given id is already indexed. + * + * @param {string} roomId The ID of the room which we want to check if it + * has been already indexed. + * + * @return {Promise} Returns true if the index contains events for + * the given room, false otherwise. + */ + async isRoomIndexed(roomId) { + const indexManager = PlatformPeg.get().getEventIndexingManager(); + return indexManager.isRoomIndexed(roomId); + } + /** * Get the room that we are currently crawling. * From 2c81d3eda84b8da8d1fd259608b9d9a1582c7003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 8 Jun 2020 17:30:26 +0200 Subject: [PATCH 021/194] EventIndex: Use the correct variable to get the room id. --- src/indexing/EventIndex.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index fe7c71cfa6..17c3691bc6 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -200,7 +200,7 @@ export default class EventIndex extends EventEmitter { if (!MatrixClientPeg.get().isRoomEncrypted(state.roomId)) return; if (ev.getType() === "m.room.encryption" && !await this.isRoomIndexed(state.roomId)) { - console.log("EventIndex: Adding a checkpoint for a newly encrypted room", room.roomId); + console.log("EventIndex: Adding a checkpoint for a newly encrypted room", state.roomId); this.addRoomCheckpoint(state.roomId, true); } } From 632f54d126737a5cfe3fdd15055cc86e671d0710 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 10 Jun 2020 10:09:16 +0200 Subject: [PATCH 022/194] Searching: Split out the search methods. This splits out the search methods out into ones that process the search result and ones that do not instead of toggling the return format using a boolean. --- src/Searching.js | 80 +++++++++++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/src/Searching.js b/src/Searching.js index d2fd67fa27..be5e61aca7 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -19,7 +19,7 @@ import {MatrixClientPeg} from "./MatrixClientPeg"; const SEARCH_LIMIT = 10; -async function serverSideSearch(term, roomId = undefined, processResult = true) { +async function serverSideSearch(term, roomId = undefined) { const client = MatrixClientPeg.get(); const filter = { @@ -45,16 +45,6 @@ async function serverSideSearch(term, roomId = undefined, processResult = true) const response = await client.search({body: body}); - if (processResult) { - const searchResult = { - _query: body, - results: [], - highlights: [], - }; - - return client._processRoomEventsSearch(searchResult, response); - } - const result = { response: response, query: body, @@ -63,6 +53,19 @@ async function serverSideSearch(term, roomId = undefined, processResult = true) return result; } +async function serverSideSearchProcess(term, roomId = undefined) { + const client = MatrixClientPeg.get(); + const result = await serverSideSearch(term, roomId); + + const searchResult = { + _query: result.query, + results: [], + highlights: [], + }; + + return client._processRoomEventsSearch(searchResult, result.response); +} + function compareEvents(a, b) { const aEvent = a.result; const bEvent = b.result; @@ -78,8 +81,8 @@ async function combinedSearch(searchTerm) { // Create two promises, one for the local search, one for the // server-side search. - const serverSidePromise = serverSideSearch(searchTerm, undefined, false); - const localPromise = localSearch(searchTerm, undefined, false); + const serverSidePromise = serverSideSearch(searchTerm, undefined); + const localPromise = localSearch(searchTerm, undefined); // Wait for both promises to resolve. await Promise.all([serverSidePromise, localPromise]); @@ -121,6 +124,8 @@ async function combinedSearch(searchTerm) { } async function localSearch(searchTerm, roomId = undefined, processResult = true) { + const eventIndex = EventIndexPeg.get(); + const searchArgs = { search_term: searchTerm, before_limit: 1, @@ -134,30 +139,8 @@ async function localSearch(searchTerm, roomId = undefined, processResult = true) searchArgs.room_id = roomId; } - const emptyResult = { - seshatQuery: searchArgs, - results: [], - highlights: [], - }; - - if (searchTerm === "") return emptyResult; - - const eventIndex = EventIndexPeg.get(); - const localResult = await eventIndex.search(searchArgs); - if (processResult) { - emptyResult.seshatQuery.next_batch = localResult.next_batch; - - const response = { - search_categories: { - room_events: localResult, - }, - }; - - return MatrixClientPeg.get()._processRoomEventsSearch(emptyResult, response); - } - searchArgs.next_batch = localResult.next_batch; const result = { @@ -168,6 +151,27 @@ async function localSearch(searchTerm, roomId = undefined, processResult = true) return result; } +async function localSearchProcess(searchTerm, roomId = undefined) { + const emptyResult = { + results: [], + highlights: [], + }; + + if (searchTerm === "") return emptyResult; + + const result = await localSearch(searchTerm, roomId); + + emptyResult.seshatQuery = result.query; + + const response = { + search_categories: { + room_events: result.response, + }, + }; + + return MatrixClientPeg.get()._processRoomEventsSearch(emptyResult, response); +} + async function localPagination(searchResult) { const eventIndex = EventIndexPeg.get(); @@ -469,11 +473,11 @@ function eventIndexSearch(term, roomId = undefined) { if (MatrixClientPeg.get().isRoomEncrypted(roomId)) { // The search is for a single encrypted room, use our local // search method. - searchPromise = localSearch(term, roomId); + searchPromise = localSearchProcess(term, roomId); } else { // The search is for a single non-encrypted room, use the // server-side search. - searchPromise = serverSideSearch(term, roomId); + searchPromise = serverSideSearchProcess(term, roomId); } } else { // Search across all rooms, combine a server side search and a @@ -523,6 +527,6 @@ export function searchPagination(searchResult) { export default function eventSearch(term, roomId = undefined) { const eventIndex = EventIndexPeg.get(); - if (eventIndex === null) return serverSideSearch(term, roomId); + if (eventIndex === null) return serverSideSearchProcess(term, roomId); else return eventIndexSearch(term, roomId); } From daef8f329bfd705ded000a5e3ca365d307d561ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 10 Jun 2020 10:34:11 +0200 Subject: [PATCH 023/194] Searching: Explain what the integer lists in the combineEvents docs mean. --- src/Searching.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Searching.js b/src/Searching.js index be5e61aca7..e580102232 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -234,6 +234,11 @@ function combineEventSources(previousSearchResult, response, a, b) { * need keep on looking for the oldest event. We are implementing a variation of * a sliding window. * + * The event sources are here represented as two sorted lists where the lowest + * number represents the newest event. The two sorted lists need to be merged + * so they can be shown as one search result. We first fetch SEARCH_LIMIT events + * from both sources. + * * If we set SEARCH_LIMIT to 3: * * Server events [01, 02, 04, 06, 07, 08, 11, 13] From 4c361bfeb71387664fb46382a1eb59041928fc28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 10 Jun 2020 10:34:47 +0200 Subject: [PATCH 024/194] Searching: Explain why _query and the server next batch stored as it is. --- src/Searching.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Searching.js b/src/Searching.js index e580102232..f5e373da61 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -57,6 +57,9 @@ async function serverSideSearchProcess(term, roomId = undefined) { const client = MatrixClientPeg.get(); const result = await serverSideSearch(term, roomId); + // The js-sdk method backPaginateRoomEventsSearch() uses _query internally + // so we're reusing the concept here since we wan't to delegate the + // pagination back to backPaginateRoomEventsSearch() in some cases. const searchResult = { _query: result.query, results: [], @@ -98,6 +101,16 @@ async function combinedSearch(searchTerm) { const localResponse = localResult.response; // Store our queries for later on so we can support pagination. + // + // We're reusing _query here again to not introduce separate code paths and + // concepts for our different pagination methods. We're storing the + // server-side next batch separately since the query is the json body of + // the request and next_batch needs to be a query parameter. + // + // We can't put it in the final result that _processRoomEventsSearch() + // returns since that one can be either a server-side one, a local one or a + // fake one to fetch the remaining cached events. See the docs for + // combineEvents() for an explanation why we need to cache events. const emptyResult = { seshatQuery: localQuery, _query: serverQuery, From 67cad2807b26bfca4fd33bd88b35304399c49c90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 10 Jun 2020 10:44:38 +0200 Subject: [PATCH 025/194] Searching: Better wording for what's going on in combineEvents(). --- src/Searching.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Searching.js b/src/Searching.js index f5e373da61..0cbedf2305 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -247,10 +247,10 @@ function combineEventSources(previousSearchResult, response, a, b) { * need keep on looking for the oldest event. We are implementing a variation of * a sliding window. * - * The event sources are here represented as two sorted lists where the lowest - * number represents the newest event. The two sorted lists need to be merged - * so they can be shown as one search result. We first fetch SEARCH_LIMIT events - * from both sources. + * The event sources are here represented as two sorted lists where the smallest + * number represents the newest event. The two lists need to be merged in a way + * that preserves the sorted property so they can be shown as one search result. + * We first fetch SEARCH_LIMIT events from both sources. * * If we set SEARCH_LIMIT to 3: * From 398655f1a9bdc180d9b22931b8e32d7512964af7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 10 Jun 2020 11:45:32 +0200 Subject: [PATCH 026/194] Searching: Remove some redundant undefined function arguments. --- src/Searching.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Searching.js b/src/Searching.js index 0cbedf2305..5fe89f8199 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -84,8 +84,8 @@ async function combinedSearch(searchTerm) { // Create two promises, one for the local search, one for the // server-side search. - const serverSidePromise = serverSideSearch(searchTerm, undefined); - const localPromise = localSearch(searchTerm, undefined); + const serverSidePromise = serverSideSearch(searchTerm); + const localPromise = localSearch(searchTerm); // Wait for both promises to resolve. await Promise.all([serverSidePromise, localPromise]); From 0e1e949c49573baa1aaf4a8b464cdd62edba16e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 10 Jun 2020 14:25:30 +0200 Subject: [PATCH 027/194] Searching: Refactor out the e2ee status restoring logic. --- src/Searching.js | 52 ++++++++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/src/Searching.js b/src/Searching.js index 6e6279555c..6c31a5ca1a 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -182,7 +182,11 @@ async function localSearchProcess(searchTerm, roomId = undefined) { }, }; - return MatrixClientPeg.get()._processRoomEventsSearch(emptyResult, response); + const processedResult = MatrixClientPeg.get()._processRoomEventsSearch(emptyResult, response); + // Restore our encryption info so we can properly re-verify the events. + restoreEncryptionInfo(processedResult.results); + + return processedResult; } async function localPagination(searchResult) { @@ -438,6 +442,31 @@ function combineResponses(previousSearchResult, localEvents = undefined, serverE return response; } +function restoreEncryptionInfo(searchResultSlice) { + for (let i = 0; i < searchResultSlice.length; i++) { + const timeline = searchResultSlice[i].context.getTimeline(); + + for (let j = 0; j < timeline.length; j++) { + const ev = timeline[j]; + + if (ev.event.curve25519Key) { + ev.makeEncrypted( + "m.room.encrypted", + { algorithm: ev.event.algorithm }, + ev.event.curve25519Key, + ev.event.ed25519Key, + ); + ev._forwardingCurve25519KeyChain = ev.event.forwardingCurve25519KeyChain; + + delete ev.event.curve25519Key; + delete ev.event.ed25519Key; + delete ev.event.algorithm; + delete ev.event.forwardingCurve25519KeyChain; + } + } + } +} + async function combinedPagination(searchResult) { const eventIndex = EventIndexPeg.get(); const client = MatrixClientPeg.get(); @@ -482,27 +511,6 @@ async function combinedPagination(searchResult) { searchResult.pendingRequest = null; // Restore our encryption info so we can properly re-verify the events. - for (let i = 0; i < result.results.length; i++) { - const timeline = result.results[i].context.getTimeline(); - - for (let j = 0; j < timeline.length; j++) { - const ev = timeline[j]; - if (ev.event.curve25519Key) { - ev.makeEncrypted( - "m.room.encrypted", - { algorithm: ev.event.algorithm }, - ev.event.curve25519Key, - ev.event.ed25519Key, - ); - ev._forwardingCurve25519KeyChain = ev.event.forwardingCurve25519KeyChain; - - delete ev.event.curve25519Key; - delete ev.event.ed25519Key; - delete ev.event.algorithm; - delete ev.event.forwardingCurve25519KeyChain; - } - } - } return result; } From a0934329fec605d67da635b2f2797d6b074d4829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 10 Jun 2020 15:41:55 +0200 Subject: [PATCH 028/194] Searching: Restore the encryption state for the paginated results. --- src/Searching.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Searching.js b/src/Searching.js index 6c31a5ca1a..b1507e6a49 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -133,6 +133,9 @@ async function combinedSearch(searchTerm) { const result = client._processRoomEventsSearch(emptyResult, response); + // Restore our encryption info so we can properly re-verify the events. + restoreEncryptionInfo(result.results); + return result; } @@ -197,6 +200,10 @@ async function localPagination(searchResult) { const localResult = await eventIndex.search(searchArgs); searchResult.seshatQuery.next_batch = localResult.next_batch; + // We only need to restore the encryption state for the new results, so + // remember how many of them we got. + const newResultCount = localResult.results.length; + const response = { search_categories: { room_events: localResult, @@ -204,6 +211,11 @@ async function localPagination(searchResult) { }; const result = MatrixClientPeg.get()._processRoomEventsSearch(searchResult, response); + + // Restore our encryption info so we can properly re-verify the events. + const newSlice = result.results.slice(Math.max(result.results.length - newResultCount, 0)); + restoreEncryptionInfo(newSlice); + searchResult.pendingRequest = null; return result; @@ -505,12 +517,17 @@ async function combinedPagination(searchResult) { }, }; + const oldResultCount = searchResult.results.length; + // Let the client process the combined result. const result = client._processRoomEventsSearch(searchResult, response); - searchResult.pendingRequest = null; - // Restore our encryption info so we can properly re-verify the events. + const newResultCount = result.results.length - oldResultCount; + const newSlice = result.results.slice(Math.max(result.results.length - newResultCount, 0)); + restoreEncryptionInfo(newSlice); + + searchResult.pendingRequest = null; return result; } From 1bbf2e053bbc3dc4a9b1f885566b89024a82e109 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 13 Jun 2020 11:54:40 -0600 Subject: [PATCH 029/194] Initial attempt at sticky headers Docs enclosed in diff. --- res/css/structures/_LeftPanel2.scss | 1 + res/css/views/rooms/_RoomSublist2.scss | 67 ++++++++++++++++++++---- src/components/structures/LeftPanel2.tsx | 66 ++++++++++++++++++++++- src/utils/css.ts | 30 +++++++++++ 4 files changed, 154 insertions(+), 10 deletions(-) create mode 100644 src/utils/css.ts diff --git a/res/css/structures/_LeftPanel2.scss b/res/css/structures/_LeftPanel2.scss index eca50bb639..5cdefa0324 100644 --- a/res/css/structures/_LeftPanel2.scss +++ b/res/css/structures/_LeftPanel2.scss @@ -131,6 +131,7 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations overflow-y: auto; width: 100%; max-width: 100%; + position: relative; // for sticky headers // Create a flexbox to trick the layout engine display: flex; diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index 3f5f654494..48e6ec7022 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -30,9 +30,51 @@ limitations under the License. // Create a flexbox to make ordering easy display: flex; align-items: center; + + // *************************** + // Sticky Headers Start + + // Ideally we'd be able to use `position: sticky; top: 0; bottom: 0;` on the + // headerContainer, however due to our layout concerns we actually have to + // calculate it manually so we can sticky things in the right places. We also + // target the headerText instead of the container to reduce jumps when scrolling, + // and to help hide the badges/other buttons that could appear on hover. This + // all works by ensuring the header text has a fixed height when sticky so the + // fixed height of the container can maintain the scroll position. + + // The combined height must be set in the LeftPanel2 component for sticky headers + // to work correctly. padding-bottom: 8px; height: 24px; + .mx_RoomSublist2_headerText { + z-index: 2; // Prioritize headers in the visible list over sticky ones + + // We use a generic sticky class for 2 reasons: to reduce style duplication and + // to identify when a header is sticky. If we didn't have a consistent sticky class, + // we'd have to do the "is sticky" checks again on click, as clicking the header + // when sticky scrolls instead of collapses the list. + &.mx_RoomSublist2_headerContainer_sticky { + position: fixed; + z-index: 1; // over top of other elements, but still under the ones in the visible list + height: 32px; // to match the header container + // width set by JS + } + + &.mx_RoomSublist2_headerContainer_stickyBottom { + bottom: 0; + } + + // We don't have this style because the top is dependent on the room list header's + // height, and is therefore calculated in JS. + //&.mx_RoomSublist2_headerContainer_stickyTop { + // top: 0; + //} + } + + // Sticky Headers End + // *************************** + .mx_RoomSublist2_badgeContainer { opacity: 0.8; width: 16px; @@ -76,18 +118,25 @@ limitations under the License. } .mx_RoomSublist2_headerText { - text-transform: uppercase; - opacity: 0.5; - line-height: $font-16px; - font-size: $font-12px; - flex: 1; max-width: calc(100% - 16px); // 16px is the badge width - // Ellipsize any text overflow - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; + // Set the same background color as the room list for sticky headers + background-color: $roomlist2-bg-color; + + // Target the span inside the container so we don't opacify the + // whole header, which can make the sticky header experience annoying. + > span { + text-transform: uppercase; + opacity: 0.5; + line-height: $font-16px; + font-size: $font-12px; + + // Ellipsize any text overflow + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } } } diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index a644aa4837..6dbe4bcd4f 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -86,6 +86,70 @@ export default class LeftPanel2 extends React.Component { } }; + // TODO: Apply this on resize, init, etc for reliability + private onScroll = (ev: React.MouseEvent) => { + const list = ev.target as HTMLDivElement; + const rlRect = list.getBoundingClientRect(); + const bottom = rlRect.bottom; + const top = rlRect.top; + const sublists = list.querySelectorAll(".mx_RoomSublist2"); + const headerHeight = 32; // Note: must match the CSS! + const headerRightMargin = 24; // calculated from margins and widths to align with non-sticky tiles + + const headerStickyWidth = rlRect.width - headerRightMargin; + + let gotBottom = false; + for (const sublist of sublists) { + const slRect = sublist.getBoundingClientRect(); + + const header = sublist.querySelector(".mx_RoomSublist2_headerText"); + + if (slRect.top + headerHeight > bottom && !gotBottom) { + console.log(`${header.textContent} is off the bottom`); + header.classList.add("mx_RoomSublist2_headerContainer_sticky"); + header.classList.add("mx_RoomSublist2_headerContainer_stickyBottom"); + header.style.width = `${headerStickyWidth}px`; + gotBottom = true; + } else if (slRect.top < top) { + console.log(`${header.textContent} is off the top`); + header.classList.add("mx_RoomSublist2_headerContainer_sticky"); + header.classList.add("mx_RoomSublist2_headerContainer_stickyTop"); + header.style.width = `${headerStickyWidth}px`; + header.style.top = `${rlRect.top}px`; + } else { + header.classList.remove("mx_RoomSublist2_headerContainer_sticky"); + header.classList.remove("mx_RoomSublist2_headerContainer_stickyTop"); + header.classList.remove("mx_RoomSublist2_headerContainer_stickyBottom"); + header.style.width = `unset`; + } + + // const name = header.textContent; + // if (hRect.bottom + headerHeight < top) { + // // Before the content (top of list) + // header.classList.add( + // "mx_RoomSublist2_headerContainer_sticky", + // "mx_RoomSublist2_headerContainer_stickyTop", + // ); + // } else { + // header.classList.remove( + // "mx_RoomSublist2_headerContainer_sticky", + // "mx_RoomSublist2_headerContainer_stickyTop", + // "mx_RoomSublist2_headerContainer_stickyBottom", + // ); + // } + + // if (!hitMiddle && (headerHeight + hRect.top) >= bottom) { + // // if we got here, the header is visible + // hitMiddle = true; + // header.style.backgroundColor = 'red'; + // } else { + // header.style.top = "0px"; + // header.style.bottom = "unset"; + // header.style.backgroundColor = "unset"; + // } + } + }; + private renderHeader(): React.ReactNode { // TODO: Update when profile info changes // TODO: Presence @@ -191,7 +255,7 @@ export default class LeftPanel2 extends React.Component { diff --git a/src/utils/css.ts b/src/utils/css.ts new file mode 100644 index 0000000000..cd629a0e8a --- /dev/null +++ b/src/utils/css.ts @@ -0,0 +1,30 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export function addClass(classes: string, clazz: string): string { + if (!classes.includes(clazz)) return `${classes} ${clazz}`; + return classes; +} + +export function removeClass(classes: string, clazz: string): string { + const idx = classes.indexOf(clazz); + if (idx >= 0) { + const beforeStr = classes.substring(0, idx); + const afterStr = classes.substring(idx + clazz.length); + return `${beforeStr} ${afterStr}`.trim(); + } + return classes; +} From 7af2de29d669b4dd46187a999a78ce9fc7c2ff69 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 13 Jun 2020 12:03:50 -0600 Subject: [PATCH 030/194] Remove unused utility --- src/utils/css.ts | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 src/utils/css.ts diff --git a/src/utils/css.ts b/src/utils/css.ts deleted file mode 100644 index cd629a0e8a..0000000000 --- a/src/utils/css.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* -Copyright 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -export function addClass(classes: string, clazz: string): string { - if (!classes.includes(clazz)) return `${classes} ${clazz}`; - return classes; -} - -export function removeClass(classes: string, clazz: string): string { - const idx = classes.indexOf(clazz); - if (idx >= 0) { - const beforeStr = classes.substring(0, idx); - const afterStr = classes.substring(idx + clazz.length); - return `${beforeStr} ${afterStr}`.trim(); - } - return classes; -} From c26c79bda8718cc1805a521c4fb4e7ba6f79154c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 13 Jun 2020 19:02:21 -0600 Subject: [PATCH 031/194] Remove dead code --- src/components/structures/LeftPanel2.tsx | 25 ------------------------ 1 file changed, 25 deletions(-) diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index 6dbe4bcd4f..f6482b06ae 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -122,31 +122,6 @@ export default class LeftPanel2 extends React.Component { header.classList.remove("mx_RoomSublist2_headerContainer_stickyBottom"); header.style.width = `unset`; } - - // const name = header.textContent; - // if (hRect.bottom + headerHeight < top) { - // // Before the content (top of list) - // header.classList.add( - // "mx_RoomSublist2_headerContainer_sticky", - // "mx_RoomSublist2_headerContainer_stickyTop", - // ); - // } else { - // header.classList.remove( - // "mx_RoomSublist2_headerContainer_sticky", - // "mx_RoomSublist2_headerContainer_stickyTop", - // "mx_RoomSublist2_headerContainer_stickyBottom", - // ); - // } - - // if (!hitMiddle && (headerHeight + hRect.top) >= bottom) { - // // if we got here, the header is visible - // hitMiddle = true; - // header.style.backgroundColor = 'red'; - // } else { - // header.style.top = "0px"; - // header.style.bottom = "unset"; - // header.style.backgroundColor = "unset"; - // } } }; From eeac80096cc900d2fc0a9a60a406fa5500d111aa Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 13 Jun 2020 19:07:19 -0600 Subject: [PATCH 032/194] Float the badges with the sticky headers --- res/css/views/rooms/_RoomSublist2.scss | 37 +++++++++++---------- src/components/structures/LeftPanel2.tsx | 2 +- src/components/views/rooms/RoomSublist2.tsx | 28 ++++++++-------- 3 files changed, 35 insertions(+), 32 deletions(-) diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index 48e6ec7022..746f373e64 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -27,7 +27,7 @@ limitations under the License. width: 100%; .mx_RoomSublist2_headerContainer { - // Create a flexbox to make ordering easy + // Create a flexbox to make alignment easy display: flex; align-items: center; @@ -47,9 +47,18 @@ limitations under the License. padding-bottom: 8px; height: 24px; - .mx_RoomSublist2_headerText { + .mx_RoomSublist2_stickable { + flex: 1; + max-width: 100%; z-index: 2; // Prioritize headers in the visible list over sticky ones + // Set the same background color as the room list for sticky headers + background-color: $roomlist2-bg-color; + + // Create a flexbox to make ordering easy + display: flex; + align-items: center; + // We use a generic sticky class for 2 reasons: to reduce style duplication and // to identify when a header is sticky. If we didn't have a consistent sticky class, // we'd have to do the "is sticky" checks again on click, as clicking the header @@ -120,23 +129,15 @@ limitations under the License. .mx_RoomSublist2_headerText { flex: 1; max-width: calc(100% - 16px); // 16px is the badge width + text-transform: uppercase; + opacity: 0.5; + line-height: $font-16px; + font-size: $font-12px; - // Set the same background color as the room list for sticky headers - background-color: $roomlist2-bg-color; - - // Target the span inside the container so we don't opacify the - // whole header, which can make the sticky header experience annoying. - > span { - text-transform: uppercase; - opacity: 0.5; - line-height: $font-16px; - font-size: $font-12px; - - // Ellipsize any text overflow - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - } + // Ellipsize any text overflow + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; } } diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index f6482b06ae..650828e9b0 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -102,7 +102,7 @@ export default class LeftPanel2 extends React.Component { for (const sublist of sublists) { const slRect = sublist.getBoundingClientRect(); - const header = sublist.querySelector(".mx_RoomSublist2_headerText"); + const header = sublist.querySelector(".mx_RoomSublist2_stickable"); if (slRect.top + headerHeight > bottom && !gotBottom) { console.log(`${header.textContent} is off the bottom`); diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 9f8b8579c3..5c23004a4f 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -257,19 +257,21 @@ export default class RoomSublist2 extends React.Component { // TODO: a11y (see old component) return (
- - {this.props.label} - - {this.renderMenu()} - {addRoomButton} -
- {badge} +
+ + {this.props.label} + + {this.renderMenu()} + {addRoomButton} +
+ {badge} +
); From a75dfca73eb4d1435107ea72a63976a987d82590 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 15 Jun 2020 12:11:45 +0100 Subject: [PATCH 033/194] Comment on when we start waiting for the first sync --- src/components/structures/MatrixChat.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 4ce29ea772..f581ec9114 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -1886,6 +1886,8 @@ export default class MatrixChat extends React.PureComponent { promisesList.push(cli.downloadKeys([cli.getUserId()])); } + // Now update the state to sya we're waiting for the first sync to complete rather + // than for the login to finish. this.setState({ pendingInitialSync: true }); await Promise.all(promisesList); From 8d0b9f46d3d7bbc22b5cf1ce8fd6aa3329538c3c Mon Sep 17 00:00:00 2001 From: Michael Albert Date: Mon, 15 Jun 2020 10:59:54 +0000 Subject: [PATCH 034/194] Translated using Weblate (German) Currently translated at 99.7% (2278 of 2285 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index c559447757..452a0ee969 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -2449,5 +2449,18 @@ "Show %(count)s more|other": "Zeige %(count)s weitere", "Show %(count)s more|one": "Zeige %(count)s weitere", "Leave Room": "Verlasse Raum", - "Room options": "Raumoptionen" + "Room options": "Raumoptionen", + "Activity": "Aktivität", + "A-Z": "A-Z", + "Recovery Key": "Wiederherstellungsschlüssel", + "This isn't the recovery key for your account": "Das ist nicht der Wiederherstellungsschlüssel für dein Konto", + "This isn't a valid recovery key": "Das ist kein gültiger Wiederherstellungsschlüssel", + "Looks good!": "Sieht gut aus!", + "Use Recovery Key or Passphrase": "Verwende einen Wiederherstellungsschlüssel oder deine Passphrase", + "Use Recovery Key": "Verwende einen Wiederherstellungsschlüssel", + "Enter your Recovery Key or enter a Recovery Passphrase to continue.": "Gib deinen Wiederherstellungsschlüssel oder eine Wiederherstellungspassphrase ein um fortzufahren.", + "Enter your Recovery Key to continue.": "Gib deinen Wiederherstellungsschlüssel ein um fortzufahren.", + "Create a Recovery Key": "Erzeuge einen Wiederherstellungsschlüssel", + "Upgrade your Recovery Key": "Aktualisiere deinen Wiederherstellungsschlüssel", + "Store your Recovery Key": "Speichere deinen Wiederherstellungsschlüssel" } From 3d1ec9effb5e3708d2c1c5f1753cdf60ae580a51 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 15 Jun 2020 12:59:38 +0100 Subject: [PATCH 035/194] typo Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/MatrixChat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index f581ec9114..9cb6ed078c 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -1886,7 +1886,7 @@ export default class MatrixChat extends React.PureComponent { promisesList.push(cli.downloadKeys([cli.getUserId()])); } - // Now update the state to sya we're waiting for the first sync to complete rather + // Now update the state to say we're waiting for the first sync to complete rather // than for the login to finish. this.setState({ pendingInitialSync: true }); From b02e439b8bca7bc4bb8139a49af30ce124f30caf Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 15 Jun 2020 15:18:57 +0100 Subject: [PATCH 036/194] Matrix client is no longer returned by onLoggedIn It seems non-obvious that it should do, and the doc saying it should do seems to have disappeared. --- src/components/structures/auth/Registration.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 6349614d72..3b5f5676dc 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -378,7 +378,7 @@ export default createReactClass({ } if (response.access_token) { - const cli = await this.props.onLoggedIn({ + await this.props.onLoggedIn({ userId: response.user_id, deviceId: response.device_id, homeserverUrl: this.state.matrixClient.getHomeserverUrl(), @@ -386,7 +386,7 @@ export default createReactClass({ accessToken: response.access_token, }, this.state.formVals.password); - this._setupPushers(cli); + this._setupPushers(); // we're still busy until we get unmounted: don't show the registration form again newState.busy = true; } else { @@ -397,10 +397,11 @@ export default createReactClass({ this.setState(newState); }, - _setupPushers: function(matrixClient) { + _setupPushers: function() { if (!this.props.brand) { return Promise.resolve(); } + const matrixClient = MatrixClientPeg.get(); return matrixClient.getPushers().then((resp)=>{ const pushers = resp.pushers; for (let i = 0; i < pushers.length; ++i) { From edb6bbc6c05eeb52a069beaf1845d1ad409941ad Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 15 Jun 2020 15:33:52 +0100 Subject: [PATCH 037/194] Implement font selection --- .../tabs/user/_AppearanceUserSettingsTab.scss | 14 ++++- .../tabs/user/AppearanceUserSettingsTab.tsx | 53 +++++++++++++++++++ src/dispatcher/actions.ts | 10 ++++ .../payloads/UpdateFontSizePayload.ts | 27 ++++++++++ .../payloads/UpdateSystemFontPayload.ts | 32 +++++++++++ src/i18n/strings/en_EN.json | 2 + src/settings/Settings.js | 14 +++++ ...izeController.js => FontSizeController.ts} | 6 ++- .../controllers/SystemFontController.ts | 36 +++++++++++++ .../controllers/UseSystemFontController.ts | 36 +++++++++++++ src/settings/watchers/FontWatcher.ts | 14 ++++- 11 files changed, 239 insertions(+), 5 deletions(-) create mode 100644 src/dispatcher/payloads/UpdateFontSizePayload.ts create mode 100644 src/dispatcher/payloads/UpdateSystemFontPayload.ts rename src/settings/controllers/{FontSizeController.js => FontSizeController.ts} (80%) create mode 100644 src/settings/controllers/SystemFontController.ts create mode 100644 src/settings/controllers/UseSystemFontController.ts diff --git a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss index 0756e98782..311a6b7c41 100644 --- a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss @@ -15,8 +15,7 @@ limitations under the License. */ .mx_AppearanceUserSettingsTab_fontSlider, -.mx_AppearanceUserSettingsTab_themeSection .mx_Field, -.mx_AppearanceUserSettingsTab_fontScaling .mx_Field { +.mx_AppearanceUserSettingsTab .mx_Field { @mixin mx_Settings_fullWidthField; } @@ -124,3 +123,14 @@ limitations under the License. .mx_SettingsTab_customFontSizeField { margin-left: calc($font-16px + 10px); } + +.mx_AppearanceUserSettingsTab_Advanced { + .mx_AppearanceUserSettingsTab_AdvancedToggle { + color: $accent-color; + margin-bottom: 16px; + } + + .mx_AppearanceUserSettingsTab_systemFont { + margin-left: calc($font-16px + 10px); + } +} diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index fe575c2819..fa464526d3 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -52,6 +52,9 @@ interface IState extends IThemeState { customThemeUrl: string, customThemeMessage: CustomThemeMessage, useCustomFontSize: boolean, + useSystemFont: boolean, + systemFont: string, + showAdvanced: boolean, } export default class AppearanceUserSettingsTab extends React.Component { @@ -67,6 +70,9 @@ export default class AppearanceUserSettingsTab extends React.Component {_t("Font size")}
@@ -314,6 +323,49 @@ export default class AppearanceUserSettingsTab extends React.Component; } + private renderAdvancedSection() { + const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); + const Field = sdk.getComponent("views.elements.Field"); + + const toggle =
this.setState({showAdvanced: !this.state.showAdvanced})} + > + {this.state.showAdvanced ? "Hide advanced" : "Show advanced"} +
; + + let advanced: React.ReactNode; + + if (this.state.showAdvanced) { + advanced =
+ this.setState({useSystemFont: checked})} + /> + { + this.setState({ + systemFont: value.target.value, + }) + + SettingsStore.setValue("systemFont", null, SettingLevel.DEVICE, value.target.value); + }} + tooltipContent="Set the name of a font installed on your system & Riot will attempt to use it." + disabled={!this.state.useSystemFont} + value={this.state.systemFont} + /> +
; + } + return
+ {toggle} + {advanced} +
+ } + render() { return (
@@ -323,6 +375,7 @@ export default class AppearanceUserSettingsTab extends React.Component {this.renderThemeSection()} {SettingsStore.isFeatureEnabled("feature_font_scaling") ? this.renderFontSection() : null} + {this.renderAdvancedSection()}
); } diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index 5f7ca1293c..a03c731818 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -69,4 +69,14 @@ export enum Action { * Opens the user menu (previously known as the top left menu). No additional payload information required. */ ToggleUserMenu = "toggle_user_menu", + + /** + * Sets the apps root font size. Should be used with UpdateFontSizePayload + */ + UpdateFontSize = "update-font-size", + + /** + * Sets a system font. Should be used with UpdateSystemFontPayload + */ + UpdateSystemFont = "update-system-font", } diff --git a/src/dispatcher/payloads/UpdateFontSizePayload.ts b/src/dispatcher/payloads/UpdateFontSizePayload.ts new file mode 100644 index 0000000000..3eac3e4607 --- /dev/null +++ b/src/dispatcher/payloads/UpdateFontSizePayload.ts @@ -0,0 +1,27 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +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 { ActionPayload } from "../payloads"; +import { Action } from "../actions"; + +export interface UpdateFontSizePayload extends ActionPayload { + action: Action.UpdateFontSize, + + /** + * The font size to set the root to + */ + size: number; +} diff --git a/src/dispatcher/payloads/UpdateSystemFontPayload.ts b/src/dispatcher/payloads/UpdateSystemFontPayload.ts new file mode 100644 index 0000000000..73475e10d5 --- /dev/null +++ b/src/dispatcher/payloads/UpdateSystemFontPayload.ts @@ -0,0 +1,32 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +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 { ActionPayload } from "../payloads"; +import { Action } from "../actions"; + +export interface UpdateSystemFontPayload extends ActionPayload { + action: Action.UpdateSystemFont, + + /** + * Specify whether to use a system font or the stylesheet font + */ + useSystemFont: boolean; + + /** + * The system font to use + */ + font: string; +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 396c3f9111..c53b474b13 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -459,6 +459,8 @@ "Mirror local video feed": "Mirror local video feed", "Enable Community Filter Panel": "Enable Community Filter Panel", "Match system theme": "Match system theme", + "Use a system font": "Use a system font", + "System font name": "System font name", "Allow Peer-to-Peer for 1:1 calls": "Allow Peer-to-Peer for 1:1 calls", "Send analytics data": "Send analytics data", "Never send encrypted messages to unverified sessions from this session": "Never send encrypted messages to unverified sessions from this session", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index fad932fa4b..44440c1722 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -30,6 +30,8 @@ import PushToMatrixClientController from './controllers/PushToMatrixClientContro import ReloadOnChangeController from "./controllers/ReloadOnChangeController"; import {RIGHT_PANEL_PHASES} from "../stores/RightPanelStorePhases"; import FontSizeController from './controllers/FontSizeController'; +import SystemFontController from './controllers/SystemFontController'; +import UseSystemFontController from './controllers/UseSystemFontController'; // These are just a bunch of helper arrays to avoid copy/pasting a bunch of times const LEVELS_ROOM_SETTINGS = ['device', 'room-device', 'room-account', 'account', 'config']; @@ -313,6 +315,18 @@ export const SETTINGS = { default: true, displayName: _td("Match system theme"), }, + "useSystemFont": { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: false, + displayName: _td("Use a system font"), + controller: new UseSystemFontController(), + }, + "systemFont": { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: "", + displayName: _td("System font name"), + controller: new SystemFontController(), + }, "webRtcAllowPeerToPeer": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, displayName: _td('Allow Peer-to-Peer for 1:1 calls'), diff --git a/src/settings/controllers/FontSizeController.js b/src/settings/controllers/FontSizeController.ts similarity index 80% rename from src/settings/controllers/FontSizeController.js rename to src/settings/controllers/FontSizeController.ts index 3ef01ab99b..6440fd32fe 100644 --- a/src/settings/controllers/FontSizeController.js +++ b/src/settings/controllers/FontSizeController.ts @@ -16,6 +16,8 @@ limitations under the License. import SettingController from "./SettingController"; import dis from "../../dispatcher/dispatcher"; +import { UpdateFontSizePayload } from "../../dispatcher/payloads/UpdateFontSizePayload"; +import { Action } from "../../dispatcher/actions"; export default class FontSizeController extends SettingController { constructor() { @@ -24,8 +26,8 @@ export default class FontSizeController extends SettingController { onChange(level, roomId, newValue) { // Dispatch font size change so that everything open responds to the change. - dis.dispatch({ - action: "update-font-size", + dis.dispatch({ + action: Action.UpdateFontSize, size: newValue, }); } diff --git a/src/settings/controllers/SystemFontController.ts b/src/settings/controllers/SystemFontController.ts new file mode 100644 index 0000000000..4f591efc17 --- /dev/null +++ b/src/settings/controllers/SystemFontController.ts @@ -0,0 +1,36 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +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 SettingController from "./SettingController"; +import SettingsStore from "../SettingsStore"; +import dis from "../../dispatcher/dispatcher"; +import { UpdateSystemFontPayload } from "../../dispatcher/payloads/UpdateSystemFontPayload"; +import { Action } from "../../dispatcher/actions"; + +export default class SystemFontController extends SettingController { + constructor() { + super(); + } + + onChange(level, roomId, newValue) { + // Dispatch font size change so that everything open responds to the change. + dis.dispatch({ + action: Action.UpdateSystemFont, + useSystemFont: SettingsStore.getValue("useSystemFont"), + font: newValue, + }); + } +} diff --git a/src/settings/controllers/UseSystemFontController.ts b/src/settings/controllers/UseSystemFontController.ts new file mode 100644 index 0000000000..d598b25962 --- /dev/null +++ b/src/settings/controllers/UseSystemFontController.ts @@ -0,0 +1,36 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +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 SettingController from "./SettingController"; +import SettingsStore from "../SettingsStore"; +import dis from "../../dispatcher/dispatcher"; +import { UpdateSystemFontPayload } from "../../dispatcher/payloads/UpdateSystemFontPayload"; +import { Action } from "../../dispatcher/actions"; + +export default class UseSystemFontController extends SettingController { + constructor() { + super(); + } + + onChange(level, roomId, newValue) { + // Dispatch font size change so that everything open responds to the change. + dis.dispatch({ + action: Action.UpdateSystemFont, + useSystemFont: newValue, + font: SettingsStore.getValue("systemFont"), + }); + } +} diff --git a/src/settings/watchers/FontWatcher.ts b/src/settings/watchers/FontWatcher.ts index 5527284cd0..cc843edb4d 100644 --- a/src/settings/watchers/FontWatcher.ts +++ b/src/settings/watchers/FontWatcher.ts @@ -18,6 +18,8 @@ import dis from '../../dispatcher/dispatcher'; import SettingsStore, {SettingLevel} from '../SettingsStore'; import IWatcher from "./Watcher"; import { toPx } from '../../utils/units'; +import { Action } from '../../dispatcher/actions'; +import { UpdateSystemFontPayload } from '../../dispatcher/payloads/UpdateSystemFontPayload'; export class FontWatcher implements IWatcher { public static readonly MIN_SIZE = 8; @@ -33,6 +35,10 @@ export class FontWatcher implements IWatcher { public start() { this.setRootFontSize(SettingsStore.getValue("baseFontSize")); + this.setSystemFont({ + useSystemFont: SettingsStore.getValue("useSystemFont"), + font: SettingsStore.getValue("systemFont"), + }) this.dispatcherRef = dis.register(this.onAction); } @@ -41,8 +47,10 @@ export class FontWatcher implements IWatcher { } private onAction = (payload) => { - if (payload.action === 'update-font-size') { + if (payload.action === Action.UpdateFontSize) { this.setRootFontSize(payload.size); + } else if (payload.action === Action.UpdateSystemFont) { + this.setSystemFont(payload) } }; @@ -54,4 +62,8 @@ export class FontWatcher implements IWatcher { } (document.querySelector(":root")).style.fontSize = toPx(fontSize); }; + + private setSystemFont = ({useSystemFont, font}) => { + document.body.style.fontFamily = useSystemFont ? font : ""; + } } From aee9cd51a069f5553ea2ca7379f009519dde3424 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 15 Jun 2020 16:34:56 +0100 Subject: [PATCH 038/194] Remove shadowed variable --- .../views/settings/tabs/user/AppearanceUserSettingsTab.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index fa464526d3..6c70e89e28 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -22,7 +22,6 @@ import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore import * as sdk from "../../../../../index"; import { enumerateThemes } from "../../../../../theme"; import ThemeWatcher from "../../../../../settings/watchers/ThemeWatcher"; -import Field from "../../../elements/Field"; import Slider from "../../../elements/Slider"; import AccessibleButton from "../../../elements/AccessibleButton"; import dis from "../../../../../dispatcher/dispatcher"; From 0618d82ccbbfd7a59c0f397f91a8c0083d76b8c9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 15 Jun 2020 17:41:22 +0100 Subject: [PATCH 039/194] Look for existing verification requests after login Fixes https://github.com/vector-im/riot-web/issues/13462 Requires https://github.com/matrix-org/matrix-js-sdk/pull/1405 --- src/stores/SetupEncryptionStore.js | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/stores/SetupEncryptionStore.js b/src/stores/SetupEncryptionStore.js index cc64e24a03..742b16ec98 100644 --- a/src/stores/SetupEncryptionStore.js +++ b/src/stores/SetupEncryptionStore.js @@ -49,6 +49,12 @@ export class SetupEncryptionStore extends EventEmitter { MatrixClientPeg.get().on("crypto.verification.request", this.onVerificationRequest); MatrixClientPeg.get().on('userTrustStatusChanged', this._onUserTrustStatusChanged); + const cli = MatrixClientPeg.get(); + const requestsInProgress = cli.getVerificationRequestsToDeviceInProgress(cli.getUserId()); + if (requestsInProgress.length) { + this._setActiveVerificationRequest(requestsInProgress[0]); + } + this.fetchKeyInfo(); } @@ -168,16 +174,8 @@ export class SetupEncryptionStore extends EventEmitter { } } - onVerificationRequest = async (request) => { - if (request.otherUserId !== MatrixClientPeg.get().getUserId()) return; - - if (this.verificationRequest) { - this.verificationRequest.off("change", this.onVerificationRequestChange); - } - this.verificationRequest = request; - await request.accept(); - request.on("change", this.onVerificationRequestChange); - this.emit("update"); + onVerificationRequest = (request) => { + this._setActiveVerificationRequest(request); } onVerificationRequestChange = async () => { @@ -218,4 +216,16 @@ export class SetupEncryptionStore extends EventEmitter { // async - ask other clients for keys, if necessary MatrixClientPeg.get()._crypto.cancelAndResendAllOutgoingKeyRequests(); } + + async _setActiveVerificationRequest(request) { + if (request.otherUserId !== MatrixClientPeg.get().getUserId()) return; + + if (this.verificationRequest) { + this.verificationRequest.off("change", this.onVerificationRequestChange); + } + this.verificationRequest = request; + await request.accept(); + request.on("change", this.onVerificationRequestChange); + this.emit("update"); + } } From 02ccdcb802fecff01d103984f9e1765d3e8eace4 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 15 Jun 2020 17:42:30 +0100 Subject: [PATCH 040/194] Fix field width and add tooltip --- .../settings/tabs/user/_AppearanceUserSettingsTab.scss | 7 +++++-- src/components/views/elements/Field.tsx | 8 +++++--- .../settings/tabs/user/AppearanceUserSettingsTab.tsx | 1 + 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss index 311a6b7c41..ce99b85d35 100644 --- a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss @@ -14,11 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_AppearanceUserSettingsTab_fontSlider, -.mx_AppearanceUserSettingsTab .mx_Field { +.mx_AppearanceUserSettingsTab_fontSlider { @mixin mx_Settings_fullWidthField; } +.mx_AppearanceUserSettingsTab .mx_Field { + width: 256px; +} + .mx_AppearanceUserSettingsTab_fontSlider { display: flex; flex-direction: row; diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index 771d2182ea..4d60550e02 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -60,6 +60,8 @@ interface IProps extends React.InputHTMLAttributes { const Tooltip = sdk.getComponent("elements.Tooltip"); let fieldTooltip; if (tooltipContent || this.state.feedback) { - const addlClassName = tooltipClassName ? tooltipClassName : ''; + const addClassName = tooltipClassName ? tooltipClassName : ''; fieldTooltip = ; } diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index 6c70e89e28..3259c74ff4 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -354,6 +354,7 @@ export default class AppearanceUserSettingsTab extends React.Component From 045217ee8db1ddc615c6bc7d674f1c123366efc4 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 15 Jun 2020 17:46:16 +0100 Subject: [PATCH 041/194] fix style --- .../views/settings/tabs/user/_AppearanceUserSettingsTab.scss | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss index ce99b85d35..8877535d6c 100644 --- a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss @@ -14,15 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_AppearanceUserSettingsTab_fontSlider { - @mixin mx_Settings_fullWidthField; -} - .mx_AppearanceUserSettingsTab .mx_Field { width: 256px; } .mx_AppearanceUserSettingsTab_fontSlider { + @mixin mx_Settings_fullWidthField; display: flex; flex-direction: row; align-items: center; From d90645f0ea94442d37d14dbcb1782170463a025b Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 15 Jun 2020 17:46:22 +0100 Subject: [PATCH 042/194] add comment --- src/stores/SetupEncryptionStore.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/stores/SetupEncryptionStore.js b/src/stores/SetupEncryptionStore.js index 742b16ec98..a9a0ad4aa2 100644 --- a/src/stores/SetupEncryptionStore.js +++ b/src/stores/SetupEncryptionStore.js @@ -52,6 +52,9 @@ export class SetupEncryptionStore extends EventEmitter { const cli = MatrixClientPeg.get(); const requestsInProgress = cli.getVerificationRequestsToDeviceInProgress(cli.getUserId()); if (requestsInProgress.length) { + // If there are multiple, we take the first. Equally if the user sends another request from + // another device after this screen has been shown, we'll switch to the new one, so this + // generally doesn't support multiple requests. this._setActiveVerificationRequest(requestsInProgress[0]); } From fcd3ebe051877fbd4ed79fa6cbe9706fc7252091 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 16 Jun 2020 00:41:21 +0100 Subject: [PATCH 043/194] Fix case-sensitivity of /me to match rest of slash commands also better error handling for attempted runs of unimplemented commands --- src/SlashCommands.tsx | 2 +- src/editor/serialize.ts | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 15798ae3b1..7ebdc4ee3b 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -118,7 +118,7 @@ export class Command { run(roomId: string, args: string, cmd: string) { // if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me` - if (!this.runFn) return; + if (!this.runFn) return reject(_t("Command error")); return this.runFn.bind(this)(roomId, args, cmd); } diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts index 4d0b8cd03a..40038114d4 100644 --- a/src/editor/serialize.ts +++ b/src/editor/serialize.ts @@ -62,16 +62,20 @@ export function textSerialize(model: EditorModel) { } export function containsEmote(model: EditorModel) { - return startsWith(model, "/me "); + return startsWith(model, "/me ", true); } -export function startsWith(model: EditorModel, prefix: string) { +export function startsWith(model: EditorModel, prefix: string, caseInsensitive = false) { const firstPart = model.parts[0]; // part type will be "plain" while editing, // and "command" while composing a message. - return firstPart && - (firstPart.type === "plain" || firstPart.type === "command") && - firstPart.text.startsWith(prefix); + let text = firstPart && firstPart.text; + if (caseInsensitive) { + prefix = prefix.toLowerCase(); + text = text.toLowerCase(); + } + + return firstPart && (firstPart.type === "plain" || firstPart.type === "command") && text.startsWith(prefix); } export function stripEmoteCommand(model: EditorModel) { From 3217becce8a930c9f8369c6dab0d64f021a69d49 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 16 Jun 2020 00:52:52 +0100 Subject: [PATCH 044/194] i18n --- 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 396c3f9111..100d2938d8 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -148,6 +148,7 @@ "Actions": "Actions", "Advanced": "Advanced", "Other": "Other", + "Command error": "Command error", "Usage": "Usage", "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Prepends ¯\\_(ツ)_/¯ to a plain-text message", "Sends a message as plain text, without interpreting it as markdown": "Sends a message as plain text, without interpreting it as markdown", @@ -1170,7 +1171,6 @@ "All Rooms": "All Rooms", "Search…": "Search…", "Server error": "Server error", - "Command error": "Command error", "Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.", "Unknown Command": "Unknown Command", "Unrecognised command: %(commandText)s": "Unrecognised command: %(commandText)s", From 4186070489bba1b7289280afbd7daa6ea24c42d0 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 15 Jun 2020 19:47:25 -0600 Subject: [PATCH 045/194] Support list collapsing and jumping Fixes https://github.com/vector-im/riot-web/issues/14036 --- res/css/views/rooms/_RoomSublist2.scss | 39 ++++++++++++++++++++ res/img/feather-customised/chevron-right.svg | 1 + src/components/structures/LeftPanel2.tsx | 2 - src/components/views/rooms/RoomSublist2.tsx | 28 ++++++++++++++ src/stores/room-list/ListLayout.ts | 13 +++++++ 5 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 res/img/feather-customised/chevron-right.svg diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index 746f373e64..c725b02f84 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -138,6 +138,34 @@ limitations under the License. text-overflow: ellipsis; overflow: hidden; white-space: nowrap; + + .mx_RoomSublist2_collapseBtn { + display: inline-block; + position: relative; + + // Default hidden + visibility: hidden; + width: 0; + height: 0; + + &::before { + content: ''; + width: 12px; + height: 12px; + position: absolute; + top: 1px; + left: 1px; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background: $primary-fg-color; + mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); + } + + &.mx_RoomSublist2_collapseBtn_collapsed::before { + mask-image: url('$(res)/img/feather-customised/chevron-right.svg'); + } + } } } @@ -251,6 +279,17 @@ limitations under the License. background-color: $roomlist2-button-bg-color; } } + + .mx_RoomSublist2_headerContainer { + .mx_RoomSublist2_headerText { + .mx_RoomSublist2_collapseBtn { + visibility: visible; + width: 12px; + height: 12px; + margin-right: 4px; + } + } + } } &.mx_RoomSublist2_minimized { diff --git a/res/img/feather-customised/chevron-right.svg b/res/img/feather-customised/chevron-right.svg new file mode 100644 index 0000000000..258de414a1 --- /dev/null +++ b/res/img/feather-customised/chevron-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index 650828e9b0..ba0ba211b7 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -105,13 +105,11 @@ export default class LeftPanel2 extends React.Component { const header = sublist.querySelector(".mx_RoomSublist2_stickable"); if (slRect.top + headerHeight > bottom && !gotBottom) { - console.log(`${header.textContent} is off the bottom`); header.classList.add("mx_RoomSublist2_headerContainer_sticky"); header.classList.add("mx_RoomSublist2_headerContainer_stickyBottom"); header.style.width = `${headerStickyWidth}px`; gotBottom = true; } else if (slRect.top < top) { - console.log(`${header.textContent} is off the top`); header.classList.add("mx_RoomSublist2_headerContainer_sticky"); header.classList.add("mx_RoomSublist2_headerContainer_stickyTop"); header.style.width = `${headerStickyWidth}px`; diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 5c23004a4f..2b0c549bd5 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -134,7 +134,28 @@ export default class RoomSublist2 extends React.Component { this.forceUpdate(); // because the layout doesn't trigger a re-render }; + private onHeaderClick = (ev: React.MouseEvent) => { + let target = ev.target as HTMLDivElement; + if (!target.classList.contains('mx_RoomSublist2_headerText')) { + // If we don't have the headerText class, the user clicked the span in the headerText. + target = target.parentElement as HTMLDivElement; + } + + const possibleSticky = target.parentElement; + const sublist = possibleSticky.parentElement.parentElement; + if (possibleSticky.classList.contains('mx_RoomSublist2_headerContainer_sticky')) { + // is sticky - jump to list + sublist.scrollIntoView({behavior: 'smooth'}); + } else { + // on screen - toggle collapse + this.props.layout.isCollapsed = !this.props.layout.isCollapsed; + this.forceUpdate(); // because the layout doesn't trigger an update + } + }; + private renderTiles(): React.ReactElement[] { + if (this.props.layout && this.props.layout.isCollapsed) return []; // don't waste time on rendering + const tiles: React.ReactElement[] = []; if (this.props.rooms) { @@ -249,6 +270,11 @@ export default class RoomSublist2 extends React.Component { ); } + const collapseClasses = classNames({ + 'mx_RoomSublist2_collapseBtn': true, + 'mx_RoomSublist2_collapseBtn_collapsed': this.props.layout && this.props.layout.isCollapsed, + }); + const classes = classNames({ 'mx_RoomSublist2_headerContainer': true, 'mx_RoomSublist2_headerContainer_withAux': !!addRoomButton, @@ -264,7 +290,9 @@ export default class RoomSublist2 extends React.Component { className={"mx_RoomSublist2_headerText"} role="treeitem" aria-level={1} + onClick={this.onHeaderClick} > + {this.props.label} {this.renderMenu()} diff --git a/src/stores/room-list/ListLayout.ts b/src/stores/room-list/ListLayout.ts index af9d6801a3..f17001f64e 100644 --- a/src/stores/room-list/ListLayout.ts +++ b/src/stores/room-list/ListLayout.ts @@ -21,11 +21,13 @@ const TILE_HEIGHT_PX = 44; interface ISerializedListLayout { numTiles: number; showPreviews: boolean; + collapsed: boolean; } export class ListLayout { private _n = 0; private _previews = false; + private _collapsed = false; constructor(public readonly tagId: TagID) { const serialized = localStorage.getItem(this.key); @@ -34,9 +36,19 @@ export class ListLayout { const parsed = JSON.parse(serialized); this._n = parsed.numTiles; this._previews = parsed.showPreviews; + this._collapsed = parsed.collapsed; } } + public get isCollapsed(): boolean { + return this._collapsed; + } + + public set isCollapsed(v: boolean) { + this._collapsed = v; + this.save(); + } + public get showPreviews(): boolean { return this._previews; } @@ -100,6 +112,7 @@ export class ListLayout { return { numTiles: this.visibleTiles, showPreviews: this.showPreviews, + collapsed: this.isCollapsed, }; } } From 84e60ee4391963f0bfbc4537ea717d4f63fb15ad Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 15 Jun 2020 20:00:09 -0600 Subject: [PATCH 046/194] Add a 'show less' button to the new room list --- res/css/views/rooms/_RoomSublist2.scss | 17 ++++++--- res/img/feather-customised/chevron-up.svg | 1 + src/components/views/rooms/RoomSublist2.tsx | 38 ++++++++++++++++----- 3 files changed, 43 insertions(+), 13 deletions(-) create mode 100644 res/img/feather-customised/chevron-up.svg diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index 3f5f654494..9e2c651641 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -100,7 +100,7 @@ limitations under the License. flex-direction: column; overflow: hidden; - .mx_RoomSublist2_showMoreButton { + .mx_RoomSublist2_showNButton { cursor: pointer; font-size: $font-13px; line-height: $font-18px; @@ -129,18 +129,25 @@ limitations under the License. display: flex; align-items: center; - .mx_RoomSublist2_showMoreButtonChevron { + .mx_RoomSublist2_showNButtonChevron { position: relative; width: 16px; height: 16px; margin-left: 12px; margin-right: 18px; - mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); mask-position: center; mask-size: contain; mask-repeat: no-repeat; background: $roomtile2-preview-color; } + + .mx_RoomSublist2_showMoreButtonChevron { + mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); + } + + .mx_RoomSublist2_showLessButtonChevron { + mask-image: url('$(res)/img/feather-customised/chevron-up.svg'); + } } // Class name comes from the ResizableBox component @@ -239,10 +246,10 @@ limitations under the License. .mx_RoomSublist2_resizeBox { align-items: center; - .mx_RoomSublist2_showMoreButton { + .mx_RoomSublist2_showNButton { flex-direction: column; - .mx_RoomSublist2_showMoreButtonChevron { + .mx_RoomSublist2_showNButtonChevron { margin-right: 12px; // to center } } diff --git a/res/img/feather-customised/chevron-up.svg b/res/img/feather-customised/chevron-up.svg new file mode 100644 index 0000000000..4eb5ecc33e --- /dev/null +++ b/res/img/feather-customised/chevron-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 9f8b8579c3..0b9452f55d 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -109,6 +109,11 @@ export default class RoomSublist2 extends React.Component { this.forceUpdate(); // because the layout doesn't trigger a re-render }; + private onShowLessClick = () => { + this.props.layout.visibleTiles = this.props.layout.minVisibleTiles; + this.forceUpdate(); // because the layout doesn't trigger a re-render + }; + private onOpenMenuClick = (ev: InputEvent) => { ev.preventDefault(); ev.stopPropagation(); @@ -303,25 +308,42 @@ export default class RoomSublist2 extends React.Component { const visibleTiles = tiles.slice(0, nVisible); // If we're hiding rooms, show a 'show more' button to the user. This button - // floats above the resize handle, if we have one present - let showMoreButton = null; + // floats above the resize handle, if we have one present. If the user has all + // tiles visible, it becomes 'show less'. + let showNButton = null; if (tiles.length > nVisible) { // we have a cutoff condition - add the button to show all const numMissing = tiles.length - visibleTiles.length; let showMoreText = ( - + {_t("Show %(count)s more", {count: numMissing})} ); if (this.props.isMinimized) showMoreText = null; - showMoreButton = ( -
- + showNButton = ( +
+ {/* set by CSS masking */} {showMoreText}
); + } else if (tiles.length <= nVisible) { + // we have all tiles visible - add a button to show less + let showLessText = ( + + {_t("Show less")} + + ); + if (this.props.isMinimized) showLessText = null; + showNButton = ( +
+ + {/* set by CSS masking */} + + {showLessText} +
+ ); } // Figure out if we need a handle @@ -345,7 +367,7 @@ export default class RoomSublist2 extends React.Component { // The padding is variable though, so figure out what we need padding for. let padding = 0; - if (showMoreButton) padding += showMoreHeight; + if (showNButton) padding += showMoreHeight; if (handles.length > 0) padding += resizeHandleHeight; const minTilesPx = layout.calculateTilesToPixelsMin(tiles.length, layout.minVisibleTiles, padding); @@ -365,7 +387,7 @@ export default class RoomSublist2 extends React.Component { className="mx_RoomSublist2_resizeBox" > {visibleTiles} - {showMoreButton} + {showNButton} ) } From 776e63c0d898f3f7c9b5a08fcc4ccaf763a72002 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 15 Jun 2020 20:11:06 -0600 Subject: [PATCH 047/194] Handle/hide old rooms in the room list Fixes https://github.com/vector-im/riot-web/issues/14003 --- src/stores/room-list/RoomListStore2.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index 1c4e66c4b0..9684e338f8 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -181,6 +181,12 @@ export class RoomListStore2 extends AsyncStore { const room = this.matrixClient.getRoom(roomId); const tryUpdate = async (updatedRoom: Room) => { console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()} in ${updatedRoom.roomId}`); + if (eventPayload.event.getType() === 'm.room.tombstone' && eventPayload.event.getStateKey() === '') { + console.log(`[RoomListDebug] Got tombstone event - regenerating room list`); + // TODO: We could probably be smarter about this + await this.regenerateAllLists(); + return; // don't pass the update down - we will have already handled it in the regen + } await this.handleRoomUpdate(updatedRoom, RoomUpdateCause.Timeline); }; if (!room) { @@ -334,7 +340,7 @@ export class RoomListStore2 extends AsyncStore { } await this.algorithm.populateTags(sorts, orders); - await this.algorithm.setKnownRooms(this.matrixClient.getRooms()); + await this.algorithm.setKnownRooms(this.matrixClient.getVisibleRooms()); this.initialListsGenerated = true; From 1dfed687273be4bf31ff44f0381ff0770c2ae3cf Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Mon, 15 Jun 2020 23:45:53 +0000 Subject: [PATCH 048/194] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (2287 of 2287 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 65f90f2c13..1c5b6cff83 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2507,5 +2507,23 @@ "sent an image.": "傳送圖片。", "You: %(message)s": "您:%(message)s", "Activity": "活動", - "A-Z": "A-Z" + "A-Z": "A-Z", + "Light": "淺色", + "Dark": "暗色", + "Customise your appearance": "自訂您的外觀", + "Appearance Settings only affect this Riot session.": "外觀設定僅會影響此 Riot 工作階段。", + "Recovery Key": "復原金鑰", + "This isn't the recovery key for your account": "這不是您帳號的復原金鑰", + "This isn't a valid recovery key": "這不是有效的復原金鑰", + "Looks good!": "看起來不錯!", + "Use Recovery Key or Passphrase": "使用復原金鑰或通關密語", + "Use Recovery Key": "使用復原金鑰", + "Enter your Recovery Key or enter a Recovery Passphrase to continue.": "輸入您的復原金鑰或輸入復原通關密語以繼續。", + "Enter your Recovery Key to continue.": "輸入您的復原金鑰以繼續。", + "Upgrade your Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login you'll need it to unlock your data.": "升級您的復原金鑰以儲存加密金鑰與您的帳號資料。如果您失去對此登入階段的存取權,您必須用它來解鎖您的資料。", + "Store your Recovery Key somewhere safe, it can be used to unlock your encrypted messages & data.": "將復原金鑰存放在安全的地方,它可以用於解鎖您的已加密訊息與資料。", + "Create a Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login you’ll need it to unlock your data.": "建立您的復原金鑰以儲存加密金鑰與您的帳號資料。如果您失去對此登入階段的存取權,您必須用它來解鎖您的資料。", + "Create a Recovery Key": "建立復原金鑰", + "Upgrade your Recovery Key": "升級您的復原金鑰", + "Store your Recovery Key": "儲存您的復原金鑰" } From 1959c7b8a59504c920ae57aa5f87a8a8d93110e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Mon, 15 Jun 2020 18:10:53 +0000 Subject: [PATCH 049/194] Translated using Weblate (Estonian) Currently translated at 61.3% (1401 of 2287 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/et/ --- src/i18n/strings/et.json | 45 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index 66bdeb551f..3fa70ef846 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -1432,5 +1432,48 @@ "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Vigadega seotud logid sisaldavad rakenduse teavet, sealhulgas sinu kasutajanime, külastatud jututubade kasutajatunnuseid või aliasi ning teiste kasutajate kasutajanimesid. Logides ei ole saadetud sõnumite sisu.", "Before submitting logs, you must create a GitHub issue to describe your problem.": "Enne logide saatmist sa peaksid GitHub'is looma veateate ja kirjeldama seal tekkinud probleemi.", "GitHub issue": "Veateade GitHub'is", - "Notes": "Märkused" + "Notes": "Märkused", + "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s muutis uueks teemaks \"%(topic)s\".", + "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s uuendas seda jututuba.", + "%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s muutis selle jututoa avalikuks kõigile, kes teavad tema aadressi.", + "sent an image.": "saatis ühe pildi.", + "Light": "Hele", + "Dark": "Tume", + "You signed in to a new session without verifying it:": "Sa logisid sisse uude sessiooni ilma seda verifitseerimata:", + "Verify your other session using one of the options below.": "Verifitseeri oma teine sessioon kasutades üht alljärgnevatest võimalustest.", + "%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s (%(userId)s) logis sisse uude sessiooni ilma seda verifitseerimata:", + "Not Trusted": "Ei ole usaldusväärne", + "Done": "Valmis", + "%(displayName)s is typing …": "%(displayName)s kirjutab midagi…", + "%(names)s and %(count)s others are typing …|other": "%(names)s ja %(count)s muud kasutajat kirjutavad midagi…", + "%(names)s and %(count)s others are typing …|one": "%(names)s ja üks teine kasutaja kirjutavad midagi…", + "%(names)s and %(lastPerson)s are typing …": "%(names)s ja %(lastPerson)s kirjutavad midagi…", + "Cannot reach homeserver": "Koduserver ei ole hetkel leitav", + "Ensure you have a stable internet connection, or get in touch with the server admin": "Palun kontrolli, kas sul on toimiv internetiühendus ning kui on, siis küsi abi koduserveri haldajalt", + "Your Riot is misconfigured": "Sinu Riot'i seadistused on paigast ära", + "Your homeserver does not support session management.": "Sinu koduserver ei toeta sessioonide haldust.", + "Unable to load session list": "Sessioonide laadimine ei õnnestunud", + "Identity server URL does not appear to be a valid identity server": "Isikutuvastusserveri aadress ei tundu viitama kehtivale isikutuvastusserverile", + "This isn't the recovery key for your account": "See ei ole sinu konto taastevõti", + "This isn't a valid recovery key": "See ei ole nõuetekohane taastevõti", + "Looks good!": "Tundub õige!", + "Use Recovery Key or Passphrase": "Kasuta taastevõtit või paroolifraasi", + "Use Recovery Key": "Kasuta taastevõtit", + "or another cross-signing capable Matrix client": "või mõnda teist Matrix'i klienti, mis oskab risttunnustamist", + "Enter your Recovery Key or enter a Recovery Passphrase to continue.": "Jätkamiseks sisesta oma taastevõti või taastamise paroolifraas.", + "Enter your Recovery Key to continue.": "Jätkamiseks sisesta oma taastevõti.", + "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Sinu uus sessioon on nüüd verifitseeritud. Selles sessioonis saad lugeda oma krüptitud sõnumeid ja teiste kasutajate jaoks on ta usaldusväärne.", + "Your new session is now verified. Other users will see it as trusted.": "Sinu uus sessioon on nüüd verifitseeritud. Teiste kasutajate jaoks on ta usaldusväärne.", + "Without completing security on this session, it won’t have access to encrypted messages.": "Kui sa pole selle sessiooni turvaprotseduure lõpetanud, siis sul puudub ligipääs oma krüptitud sõnumitele.", + "Go Back": "Mine tagasi", + "Failed to re-authenticate due to a homeserver problem": "Uuesti autentimine ei õnnestunud koduserveri vea tõttu", + "Incorrect password": "Vale salasõna", + "Failed to re-authenticate": "Uuesti autentimine ei õnnestunud", + "Regain access to your account and recover encryption keys stored in this session. Without them, you won’t be able to read all of your secure messages in any session.": "Taasta ligipääs oma kontole ning selles sessioonis salvestatud krüptivõtmetele. Ilma nende võtmeteta sa ei saa lugeda krüptitud sõnumeid mitte üheski oma sessioonis.", + "Command Autocomplete": "Käskude automaatne lõpetamine", + "Community Autocomplete": "Kogukondade nimede automaatne lõpetamine", + "Emoji Autocomplete": "Emoji'de automaatne lõpetamine", + "Notification Autocomplete": "Teavituste automaatne lõpetamine", + "Room Autocomplete": "Jututubade nimede automaatne lõpetamine", + "User Autocomplete": "Kasutajanimede automaatne lõpetamine" } From ee06b25e722f4500d317d0897f8703214ea44eeb Mon Sep 17 00:00:00 2001 From: Tim Hellhake Date: Mon, 15 Jun 2020 23:33:43 +0000 Subject: [PATCH 050/194] Translated using Weblate (German) Currently translated at 99.5% (2276 of 2287 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 452a0ee969..891e6b7656 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -1367,7 +1367,7 @@ "Sends the given message coloured as a rainbow": "Sendet die Nachricht in Regenbogenfarben", "Adds a custom widget by URL to the room": "Fügt ein Benutzer-Widget über eine URL zum Raum hinzu", "Please supply a https:// or http:// widget URL": "Bitte gib eine https:// oder http:// Widget-URL an", - "Sends the given emote coloured as a rainbow": "Sended das Emoji in Regenbogenfarben", + "Sends the given emote coloured as a rainbow": "Sendet das Emoji in Regenbogenfarben", "%(senderName)s made no change.": "%(senderName)s hat keine Änderung vorgenommen.", "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s hat die Einladung zum Raumbeitritt für %(targetDisplayName)s zurückgezogen.", "Cannot reach homeserver": "Der Heimserver ist nicht erreichbar", From ed14f099f3187793d22ca1386a125acb2a0637a4 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Mon, 15 Jun 2020 18:09:50 +0000 Subject: [PATCH 051/194] Translated using Weblate (Hungarian) Currently translated at 100.0% (2287 of 2287 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 511dac34ac..b88a71a30b 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2494,5 +2494,25 @@ "All settings": "Minden beállítás", "Archived rooms": "Archivált szobák", "Feedback": "Visszajelzés", - "Account settings": "Fiók beállítása" + "Account settings": "Fiók beállítása", + "Light": "Világos", + "Dark": "Sötét", + "Customise your appearance": "Szabd személyre a megjelenítést", + "Appearance Settings only affect this Riot session.": "A megjelenítési beállítások csak erre a munkamenetre vonatkoznak.", + "Activity": "Aktivitás", + "A-Z": "A-Z", + "Recovery Key": "Visszaállítási Kulcs", + "This isn't the recovery key for your account": "Ez nem a fiókod visszaállítási kulcsa", + "This isn't a valid recovery key": "Ez nem egy érvényes visszaállítási kulcs", + "Looks good!": "Jól néz ki!", + "Use Recovery Key or Passphrase": "Használd a visszaállítási kulcsot vagy jelmondatot", + "Use Recovery Key": "Használd a Visszaállítási Kulcsot", + "Enter your Recovery Key or enter a Recovery Passphrase to continue.": "A továbblépéshez add meg a Visszaállítási kulcsot vagy a Visszaállítási jelmondatot.", + "Enter your Recovery Key to continue.": "A továbblépéshez add meg a Visszaállítási Kulcsot.", + "Upgrade your Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login you'll need it to unlock your data.": "Fejleszd a Visszaállítási kulcsot, hogy a fiók adatokkal tárolhasd a titkosítási kulcsokat és jelszavakat. Szükséged lesz rá hogy hozzáférj az adataidhoz ha elveszted a hozzáférésed ehhez a bejelentkezéshez.", + "Store your Recovery Key somewhere safe, it can be used to unlock your encrypted messages & data.": "A Visszaállítási kulcsot tárold biztonságos helyen mivel felhasználható a titkosított üzenetekhez és adatokhoz való hozzáféréshez.", + "Create a Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login you’ll need it to unlock your data.": "Készíts Visszaállítási kulcsot, hogy a fiók adatokkal tárolhasd a titkosítási kulcsokat és jelszavakat. Szükséged lesz rá hogy hozzáférj az adataidhoz ha elveszted a hozzáférésed ehhez a bejelentkezéshez.", + "Create a Recovery Key": "Visszaállítási kulcs készítése", + "Upgrade your Recovery Key": "A Visszaállítási kulcs fejlesztése", + "Store your Recovery Key": "Visszaállítási kulcs tárolása" } From 884ead917da2addf49927d7a753287c2981e8c14 Mon Sep 17 00:00:00 2001 From: Laura Date: Mon, 15 Jun 2020 12:03:49 +0000 Subject: [PATCH 052/194] Translated using Weblate (Polish) Currently translated at 66.0% (1509 of 2287 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/pl/ --- src/i18n/strings/pl.json | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index 30dd5b828d..4b98bdbe2a 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -157,7 +157,7 @@ "Emoji": "Emoji", "Enable automatic language detection for syntax highlighting": "Włącz automatyczne rozpoznawanie języka dla podświetlania składni", "Enable Notifications": "Włącz powiadomienia", - "%(senderName)s ended the call.": "%(senderName)s zakończył połączenie.", + "%(senderName)s ended the call.": "%(senderName)s zakończył(a) połączenie.", "End-to-end encryption information": "Informacje o szyfrowaniu end-to-end", "Enter passphrase": "Wpisz frazę", "Error decrypting attachment": "Błąd odszyfrowywania załącznika", @@ -230,8 +230,8 @@ "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s uczynił(a) przyszłą historię pokoju widoczną dla wszyscy członkowie pokoju, od momentu ich zaproszenia.", "%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s uczynił(a) przyszłą historię pokoju widoczną dla wszyscy członkowie pokoju, od momentu ich dołączenia.", "%(senderName)s made future room history visible to all room members.": "%(senderName)s uczynił(a) przyszłą historię pokoju widoczną dla wszyscy członkowie pokoju.", - "%(senderName)s made future room history visible to anyone.": "%(senderName)s uczynił przyszłą historię pokoju widoczną dla kazdego.", - "%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s uczynił przyszłą historię pokoju widoczną dla nieznany (%(visibility)s).", + "%(senderName)s made future room history visible to anyone.": "%(senderName)s uczynił(a) przyszłą historię pokoju widoczną dla każdego.", + "%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s uczynił(a) przyszłą historię pokoju widoczną dla nieznany (%(visibility)s).", "Manage Integrations": "Zarządzaj integracjami", "Missing room_id in request": "Brakujące room_id w żądaniu", "Missing user_id in request": "Brakujące user_id w żądaniu", @@ -291,7 +291,7 @@ "Send anyway": "Wyślij mimo to", "Send Reset Email": "Wyślij e-mail resetujący hasło", "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s wysłał obraz.", - "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s wysłał zaproszenie do %(targetDisplayName)s do dołączenia do pokoju.", + "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s wysłał(a) zaproszenie do %(targetDisplayName)s do dołączenia do pokoju.", "Server error": "Błąd serwera", "Server may be unavailable, overloaded, or search timed out :(": "Serwer może być niedostępny, przeciążony, lub upłynął czas wyszukiwania :(", "Server may be unavailable, overloaded, or you hit a bug.": "Serwer może być niedostępny, przeciążony, lub trafiłeś na błąd.", @@ -363,7 +363,7 @@ "You do not have permission to do that in this room.": "Nie masz pozwolenia na wykonanie tej akcji w tym pokoju.", "You cannot place a call with yourself.": "Nie możesz wykonać połączenia do siebie.", "You cannot place VoIP calls in this browser.": "Nie możesz przeprowadzić rozmowy głosowej VoIP w tej przeglądarce.", - "You do not have permission to post to this room": "Nie jesteś uprawniony do pisania w tym pokoju", + "You do not have permission to post to this room": "Nie masz uprawnień do pisania w tym pokoju", "You have disabled URL previews by default.": "Masz domyślnie wyłączone podglądy linków.", "You have no visible notifications": "Nie masz widocznych powiadomień", "You must register to use this functionality": "Musisz się zarejestrować aby móc używać tej funkcji", @@ -915,7 +915,7 @@ "Common names and surnames are easy to guess": "Popularne imiona i nazwiska są łatwe do odgadnięcia", "You do not have permission to invite people to this room.": "Nie masz uprawnień do zapraszania ludzi do tego pokoju.", "User %(user_id)s does not exist": "Użytkownik %(user_id)s nie istnieje", - "Unknown server error": "Nieznany bład serwera", + "Unknown server error": "Nieznany błąd serwera", "%(oneUser)sleft %(count)s times|one": "%(oneUser)swyszedł(-ła)", "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)sdołączył(a) i wyszedł(-ła) %(count)s razy", "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)sdołączył(a) i wyszedł(-ła)", @@ -930,7 +930,7 @@ "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)szmienił(a) swój awatar %(count)s razy", "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)szmienił(a) swój awatar", "%(items)s and %(count)s others|other": "%(items)s i %(count)s innych", - "%(items)s and %(count)s others|one": "%(items)s i jeden inny", + "%(items)s and %(count)s others|one": "%(items)s i jedna inna osoba", "%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)szmieniło swój awatar", "%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)szmieniło swój awatar %(count)s razy", "%(oneUser)schanged their name %(count)s times|one": "%(oneUser)szmienił(a) swoją nazwę", @@ -1102,7 +1102,7 @@ "Gift": "Prezent", "Hammer": "Młotek", "Group & filter rooms by custom tags (refresh to apply changes)": "Grupuj i filtruj pokoje według niestandardowych znaczników (odśwież, aby zastosować zmiany)", - "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Poprzedza ¯\\_(ツ)_/¯ do wiadomości tekstowej", + "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Dodaje ¯\\_(ツ)_/¯ na początku wiadomości tekstowej", "Upgrades a room to a new version": "Aktualizuje pokój do nowej wersji", "Changes your display nickname in the current room only": "Zmienia Twój wyświetlany pseudonim tylko dla bieżącego pokoju", "Sets the room name": "Ustawia nazwę pokoju", @@ -1175,7 +1175,7 @@ "%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s aktywował Flair dla %(groups)s w tym pokoju.", "%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s dezaktywował Flair dla %(groups)s w tym pokoju.", "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s aktywował Flair dla %(newGroups)s i dezaktywował Flair dla %(oldGroups)s w tym pokoju.", - "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s odwołał zaproszednie dla %(targetDisplayName)s aby dołączył do pokoju.", + "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s odwołał(a) zaproszenie dla %(targetDisplayName)s aby dołączył do pokoju.", "%(names)s and %(count)s others are typing …|one": "%(names)s i jedna osoba pisze…", "Cannot reach homeserver": "Błąd połączenia z serwerem domowym", "Ensure you have a stable internet connection, or get in touch with the server admin": "Upewnij się, że posiadasz stabilne połączenie internetowe lub skontaktuj się z administratorem serwera", @@ -1605,5 +1605,9 @@ "WARNING: Session already verified, but keys do NOT MATCH!": "OSTRZEŻENIE: Sesja już zweryfikowana, ale klucze do siebie NIE PASUJĄ!", "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "OSTRZEŻENIE: WERYFIKACJA KLUCZY NIE POWIODŁA SIĘ! Klucz podpisujący dla %(userId)s oraz sesji %(deviceId)s to \"%(fprint)s\", nie pasuje on do podanego klucza \"%(fingerprint)s\". To może oznaczać że Twoja komunikacja jest przechwytywana!", "Displays information about a user": "Pokazuje informacje na temat użytkownika", - "Send a bug report with logs": "Wyślij raport błędu z logami" + "Send a bug report with logs": "Wyślij raport błędu z logami", + "Use Single Sign On to continue": "Użyj pojedynczego logowania, aby kontynuować", + "Confirm adding this email address by using Single Sign On to prove your identity.": "Potwierdź dodanie tego adresu e-mail przez użycie pojedynczego logowania, aby potwierdzić swoją tożsamość.", + "Single Sign On": "Pojedyncze logowanie", + "Confirm adding this phone number by using Single Sign On to prove your identity.": "Potwierdź dodanie tego numeru telefonu przez użycie pojedynczego logowania, aby potwierdzić swoją tożsamość." } From de7d7802deb76ede9645986606cecd653a07d960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Tue, 16 Jun 2020 06:16:34 +0000 Subject: [PATCH 053/194] Translated using Weblate (Estonian) Currently translated at 61.4% (1405 of 2287 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/et/ --- src/i18n/strings/et.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index 3fa70ef846..ef41233254 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -1475,5 +1475,9 @@ "Emoji Autocomplete": "Emoji'de automaatne lõpetamine", "Notification Autocomplete": "Teavituste automaatne lõpetamine", "Room Autocomplete": "Jututubade nimede automaatne lõpetamine", - "User Autocomplete": "Kasutajanimede automaatne lõpetamine" + "User Autocomplete": "Kasutajanimede automaatne lõpetamine", + "Enter recovery key": "Sisesta taastevõti", + "Unable to access secret storage. Please verify that you entered the correct recovery key.": "Ei õnnestu lugeda krüptitud salvestusruumi. Palun kontrolli, kas sa sisestasid õige taastevõtme.", + "This looks like a valid recovery key!": "See tundub olema õige taastevõti!", + "Not a valid recovery key": "Ei ole sobilik taastevõti" } From f7baf3a3314440e4eb1e3bc9bc7743e4291f0389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Tue, 16 Jun 2020 07:37:43 +0000 Subject: [PATCH 054/194] Translated using Weblate (French) Currently translated at 100.0% (2287 of 2287 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index f4445082d8..d2942522f4 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2508,5 +2508,23 @@ "Leave Room": "Quitter le salon", "Room options": "Options du salon", "Activity": "Activité", - "A-Z": "A-Z" + "A-Z": "A-Z", + "Light": "Clair", + "Dark": "Sombre", + "Customise your appearance": "Personnalisez l’apparence", + "Appearance Settings only affect this Riot session.": "Les paramètres d’apparence affecteront uniquement cette session de Riot.", + "Recovery Key": "Clé de récupération", + "This isn't the recovery key for your account": "Ce n’est pas la clé de récupération pour votre compte", + "This isn't a valid recovery key": "Ce n’est pas une clé de récupération valide", + "Looks good!": "Ça a l’air correct !", + "Use Recovery Key or Passphrase": "Utiliser la clé ou la phrase secrète de récupération", + "Use Recovery Key": "Utiliser la clé de récupération", + "Enter your Recovery Key or enter a Recovery Passphrase to continue.": "Saisissez votre clé de récupération ou une phrase secrète de récupération pour continuer.", + "Enter your Recovery Key to continue.": "Saisissez votre clé de récupération pour continuer.", + "Upgrade your Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login you'll need it to unlock your data.": "Mettez à niveau votre clé de récupération pour stocker vos clés et secrets de chiffrement avec les données de votre compte. Si vous n’avez plus accès à cette connexion, vous en aurez besoin pour déverrouiller vos données.", + "Store your Recovery Key somewhere safe, it can be used to unlock your encrypted messages & data.": "Stockez votre clé de récupération dans un endroit sûr, elle peut être utilisée pour déverrouiller vos messages et vos données chiffrés.", + "Create a Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login you’ll need it to unlock your data.": "Créez une clé de récupération pour stocker vos clé et secrets de chiffrement avec les données de votre compte. Si vous n’avez plus accès à cette connexion, vous en aurez besoin pour déverrouiller vos données.", + "Create a Recovery Key": "Créer une clé de récupération", + "Upgrade your Recovery Key": "Mettre à jour votre clé de récupération", + "Store your Recovery Key": "Stocker votre clé de récupération" } From ef80a0b0b473a4d2a422e6a811c2149340a1e4ca Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 16 Jun 2020 14:06:42 +0100 Subject: [PATCH 055/194] avoid negatives --- src/editor/serialize.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts index 40038114d4..fc35f1e2ea 100644 --- a/src/editor/serialize.ts +++ b/src/editor/serialize.ts @@ -62,15 +62,15 @@ export function textSerialize(model: EditorModel) { } export function containsEmote(model: EditorModel) { - return startsWith(model, "/me ", true); + return startsWith(model, "/me ", false); } -export function startsWith(model: EditorModel, prefix: string, caseInsensitive = false) { +export function startsWith(model: EditorModel, prefix: string, caseSensitive = true) { const firstPart = model.parts[0]; // part type will be "plain" while editing, // and "command" while composing a message. let text = firstPart && firstPart.text; - if (caseInsensitive) { + if (!caseSensitive) { prefix = prefix.toLowerCase(); text = text.toLowerCase(); } From 89a72b768556a94b4a175e701015c4be0d01b70d Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 16 Jun 2020 14:53:13 +0100 Subject: [PATCH 056/194] Take the last request (ie. the most recent) --- src/stores/SetupEncryptionStore.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stores/SetupEncryptionStore.js b/src/stores/SetupEncryptionStore.js index a9a0ad4aa2..4cdc845419 100644 --- a/src/stores/SetupEncryptionStore.js +++ b/src/stores/SetupEncryptionStore.js @@ -52,10 +52,10 @@ export class SetupEncryptionStore extends EventEmitter { const cli = MatrixClientPeg.get(); const requestsInProgress = cli.getVerificationRequestsToDeviceInProgress(cli.getUserId()); if (requestsInProgress.length) { - // If there are multiple, we take the first. Equally if the user sends another request from + // If there are multiple, we take the most recent. Equally if the user sends another request from // another device after this screen has been shown, we'll switch to the new one, so this // generally doesn't support multiple requests. - this._setActiveVerificationRequest(requestsInProgress[0]); + this._setActiveVerificationRequest(requestsInProgress[requestsInProgress.length - 1]); } this.fetchKeyInfo(); From bc0281ebddf21697d4f58cfec74e720a87d24912 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 16 Jun 2020 08:36:10 -0600 Subject: [PATCH 057/194] Match fuzzy filtering a bit more reliably in the new room list Fixes https://github.com/vector-im/riot-web/issues/14054 --- src/stores/room-list/filters/NameFilterCondition.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/stores/room-list/filters/NameFilterCondition.ts b/src/stores/room-list/filters/NameFilterCondition.ts index 7b6ed76e79..4ac5b68596 100644 --- a/src/stores/room-list/filters/NameFilterCondition.ts +++ b/src/stores/room-list/filters/NameFilterCondition.ts @@ -56,6 +56,14 @@ export class NameFilterCondition extends EventEmitter implements IFilterConditio return true; } } - return room.name && removeHiddenChars(room.name).toLowerCase().includes(lcFilter); + + if (!room.name) return false; // should realisitically not happen: the js-sdk always calculates a name + + // Note: we have to match the filter with the removeHiddenChars() room name because the + // function strips spaces and other characters (M becomes RN for example, in lowercase). + // We also doubly convert to lowercase to work around oddities of the library. + const noSecretsFilter = removeHiddenChars(lcFilter).toLowerCase(); + const noSecretsName = removeHiddenChars(room.name.toLowerCase()).toLowerCase(); + return noSecretsName.includes(noSecretsFilter); } } From 17dbb1ac8bf8c559b325397dd781e2f9a7627179 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 16 Jun 2020 15:41:10 +0100 Subject: [PATCH 058/194] Message preview for font slider --- .../tabs/user/_AppearanceUserSettingsTab.scss | 15 ++- .../views/elements/MessagePreview.tsx | 126 ++++++++++++++++++ .../tabs/user/AppearanceUserSettingsTab.tsx | 9 ++ src/i18n/strings/en_EN.json | 2 +- src/settings/Settings.js | 2 +- 5 files changed, 150 insertions(+), 4 deletions(-) create mode 100644 src/components/views/elements/MessagePreview.tsx diff --git a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss index 0756e98782..14189fc5e1 100644 --- a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss @@ -15,11 +15,15 @@ limitations under the License. */ .mx_AppearanceUserSettingsTab_fontSlider, -.mx_AppearanceUserSettingsTab_themeSection .mx_Field, -.mx_AppearanceUserSettingsTab_fontScaling .mx_Field { +.mx_AppearanceUserSettingsTab_fontSlider_preview, +.mx_AppearanceUserSettingsTab .mx_Field { @mixin mx_Settings_fullWidthField; } +.mx_AppearanceUserSettingsTab_fontScaling { + color: $primary-fg-color; +} + .mx_AppearanceUserSettingsTab_fontSlider { display: flex; flex-direction: row; @@ -32,6 +36,13 @@ limitations under the License. margin-bottom: 24px; } +.mx_AppearanceUserSettingsTab_fontSlider_preview { + border: 1px solid $input-darker-bg-color; + border-radius: 10px; + padding: 0 16px 9px 16px; + pointer-events: none; +} + .mx_AppearanceUserSettingsTab_fontSlider_smallText { font-size: 15px; padding-right: 20px; diff --git a/src/components/views/elements/MessagePreview.tsx b/src/components/views/elements/MessagePreview.tsx new file mode 100644 index 0000000000..c517b48019 --- /dev/null +++ b/src/components/views/elements/MessagePreview.tsx @@ -0,0 +1,126 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { MatrixClientPeg } from '../../../MatrixClientPeg'; + +import React from 'react'; + +import * as Avatar from '../../../Avatar'; +import classnames from 'classnames'; +import { MatrixEvent } from 'matrix-js-sdk/src/models/Event'; +import * as sdk from "../../../index"; + +interface IProps { + /** + * The text to be displayed in the message preview + */ + message: string; + + /** + * Whether to use the irc layout or not + */ + useIRCLayout: boolean; + + /** + * classnames to apply to the wrapper of the preview + */ + className: string; +} + +interface IState { + userId: string; + displayname: string, + avatar_url: string, +} + +const AVATAR_SIZE = 32; + +export default class MessagePreview extends React.Component { + constructor(props: IProps) { + super(props); + + this.state = { + userId: "@erim:fink.fink", + displayname: "Erimayas Fink", + avatar_url: null, + } + } + + async componentDidMount() { + // Fetch current user data + const client = MatrixClientPeg.get() + const userId = await client.getUserId(); + const profileInfo = await client.getProfileInfo(userId); + const avatar_url = Avatar.avatarUrlForUser( + {avatarUrl: profileInfo.avatar_url}, + AVATAR_SIZE, AVATAR_SIZE, "crop"); + + this.setState({ + userId, + displayname: profileInfo.displayname, + avatar_url, + }); + + } + + public render() { + const EventTile = sdk.getComponent("views.rooms.EventTile"); + + // Fake it till we make it + const event = new MatrixEvent(JSON.parse(`{ + "type": "m.room.message", + "sender": "${this.state.userId}", + "content": { + "m.new_content": { + "msgtype": "m.text", + "body": "${this.props.message}", + "displayname": "${this.state.displayname}", + "avatar_url": "${this.state.avatar_url}" + }, + "msgtype": "m.text", + "body": "${this.props.message}", + "displayname": "${this.state.displayname}", + "avatar_url": "${this.state.avatar_url}" + }, + "unsigned": { + "age": 97 + }, + "event_id": "$9999999999999999999999999999999999999999999", + "room_id": "!999999999999999999:matrix.org" + }`)) + + // Fake it more + event.sender = { + name: this.state.displayname, + userId: this.state.userId, + getAvatarUrl: (..._) => { + return this.state.avatar_url; + }, + } + + let className = classnames( + this.props.className, + { + "mx_IRCLayout": this.props.useIRCLayout, + "mx_GroupLayout": !this.props.useIRCLayout, + } + ); + + return
+ +
+ } +} diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index fe575c2819..8838979021 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -280,8 +280,15 @@ export default class AppearanceUserSettingsTab extends React.Component + {_t("Font size")} +
Aa
Aa
+ this.setState({useCustomFontSize: checked})} useCheckbox={true} /> + Date: Tue, 16 Jun 2020 15:52:35 +0100 Subject: [PATCH 059/194] Use lower case import --- src/components/views/elements/MessagePreview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/MessagePreview.tsx b/src/components/views/elements/MessagePreview.tsx index c517b48019..6a0fa9a756 100644 --- a/src/components/views/elements/MessagePreview.tsx +++ b/src/components/views/elements/MessagePreview.tsx @@ -20,7 +20,7 @@ import React from 'react'; import * as Avatar from '../../../Avatar'; import classnames from 'classnames'; -import { MatrixEvent } from 'matrix-js-sdk/src/models/Event'; +import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import * as sdk from "../../../index"; interface IProps { From 37672d43864ecfda74412b19103625b67557d31c Mon Sep 17 00:00:00 2001 From: Tuomas Hietala Date: Tue, 16 Jun 2020 16:00:04 +0000 Subject: [PATCH 060/194] Translated using Weblate (Finnish) Currently translated at 95.4% (2182 of 2287 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fi/ --- src/i18n/strings/fi.json | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index a3158f0280..1157daa260 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -2340,5 +2340,32 @@ "This address is available to use": "Tämä osoite on käytettävissä", "This address is already in use": "Tämä osoite on jo käytössä", "Set a room address to easily share your room with other people.": "Aseta huoneelle osoite, jotta voit jakaa huoneen helposti muille.", - "Address (optional)": "Osoite (valinnainen)" + "Address (optional)": "Osoite (valinnainen)", + "sent an image.": "lähetti kuvan.", + "Light": "Vaalea", + "Dark": "Tumma", + "You: %(message)s": "Sinä: %(message)s", + "Emoji picker": "Emojivalitsin", + "No recently visited rooms": "Ei hiljattain vierailtuja huoneita", + "People": "Ihmiset", + "Sort by": "Lajittelutapa", + "Unread rooms": "Lukemattomat huoneet", + "Always show first": "Näytä aina ensin", + "Show": "Näytä", + "Leave Room": "Poistu huoneesta", + "Switch to light mode": "Vaihda vaaleaan teemaan", + "Switch to dark mode": "Vaihda tummaan teemaan", + "Switch theme": "Vaihda teemaa", + "All settings": "Kaikki asetukset", + "Archived rooms": "Arkistoidut huoneet", + "Feedback": "Palaute", + "Account settings": "Tilin asetukset", + "Recovery Key": "Palautusavain", + "This isn't the recovery key for your account": "Tämä ei ole tilisi palautusavain", + "This isn't a valid recovery key": "Tämä ei ole kelvollinen palautusavain", + "Looks good!": "Hyvältä näyttää!", + "Use Recovery Key or Passphrase": "Käytä palautusavainta tai salalausetta", + "Use Recovery Key": "Käytä palautusavainta", + "Create a Recovery Key": "Luo palautusavain", + "Upgrade your Recovery Key": "Päivitä palautusavaimesi" } From 2192332968b600b492d65a677e36984818fccdd3 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 16 Jun 2020 17:55:19 +0100 Subject: [PATCH 061/194] Add layout options to the appearance tab --- res/css/views/rooms/_IRCLayout.scss | 4 +- .../tabs/user/_AppearanceUserSettingsTab.scss | 61 +++++++++++++++++++ src/components/structures/RoomView.js | 6 +- .../views/elements/StyledRadioButton.tsx | 1 + .../tabs/user/AppearanceUserSettingsTab.tsx | 61 ++++++++++++++++++- src/i18n/strings/en_EN.json | 6 +- src/settings/Settings.js | 7 ++- 7 files changed, 138 insertions(+), 8 deletions(-) diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index b4ad117573..814a614007 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -121,8 +121,8 @@ $irc-line-height: $font-18px; } } - .mx_EvenTile_line .mx_MessageActionBar, - .mx_EvenTile_line .mx_ReplyThread_wrapper { + .mx_EventTile_line .mx_MessageActionBar, + .mx_EventTile_line .mx_ReplyThread_wrapper { display: block; } diff --git a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss index 14189fc5e1..7fefdd1505 100644 --- a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss @@ -16,6 +16,7 @@ limitations under the License. .mx_AppearanceUserSettingsTab_fontSlider, .mx_AppearanceUserSettingsTab_fontSlider_preview, +.mx_AppearanceUserSettingsTab_Layout, .mx_AppearanceUserSettingsTab .mx_Field { @mixin mx_Settings_fullWidthField; } @@ -41,6 +42,14 @@ limitations under the License. border-radius: 10px; padding: 0 16px 9px 16px; pointer-events: none; + + .mx_EventTile_msgOption { + display: none; +} + + &.mx_IRCLayout { + padding-top: 9px; + } } .mx_AppearanceUserSettingsTab_fontSlider_smallText { @@ -135,3 +144,55 @@ limitations under the License. .mx_SettingsTab_customFontSizeField { margin-left: calc($font-16px + 10px); } + +.mx_AppearanceUserSettingsTab_Layout_RadioButtons { + display: flex; + flex-direction: row; + + color: $primary-fg-color; + + .mx_AppearanceUserSettingsTab_spacer { + width: 24px; + } + + > .mx_AppearanceUserSettingsTab_Layout_RadioButton { + flex-grow: 0; + flex-shrink: 1; + display: flex; + flex-direction: column; + + width: 300px; + + border: 1px solid $input-darker-bg-color; + border-radius: 10px; + + .mx_EventTile_msgOption { + display: none; + } + + .mx_AppearanceUserSettingsTab_Layout_RadioButton_preview { + flex-grow: 1; + display: flex; + align-items: center; + padding: 10px; + } + + .mx_RadioButton { + flex-grow: 0; + padding: 10px; + } + + .mx_EventTile_content { + margin-right: 0; + } + } + + .mx_RadioButton { + border-top: 1px solid $input-darker-bg-color; + } + + .mx_RadioButton_checked { + background-color: rgba($accent-color, 0.08); + } +} + diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index ab3da035c4..126ae5d4ce 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -166,7 +166,7 @@ export default createReactClass({ canReact: false, canReply: false, - useIRCLayout: SettingsStore.getValue("feature_irc_ui"), + useIRCLayout: SettingsStore.getValue("useIRCLayout"), matrixClientIsReady: this.context && this.context.isInitialSyncComplete(), }; @@ -199,7 +199,7 @@ export default createReactClass({ this._roomView = createRef(); this._searchResultsPanel = createRef(); - this._layoutWatcherRef = SettingsStore.watchSetting("feature_irc_ui", null, this.onLayoutChange); + this._layoutWatcherRef = SettingsStore.watchSetting("useIRCLayout", null, this.onLayoutChange); }, _onReadReceiptsChange: function() { @@ -546,7 +546,7 @@ export default createReactClass({ onLayoutChange: function() { this.setState({ - useIRCLayout: SettingsStore.getValue("feature_irc_ui"), + useIRCLayout: SettingsStore.getValue("useIRCLayout"), }); }, diff --git a/src/components/views/elements/StyledRadioButton.tsx b/src/components/views/elements/StyledRadioButton.tsx index fdedd16230..6af7549df8 100644 --- a/src/components/views/elements/StyledRadioButton.tsx +++ b/src/components/views/elements/StyledRadioButton.tsx @@ -36,6 +36,7 @@ export default class StyledRadioButton extends React.PureComponent diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index 8838979021..0620626181 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -52,8 +52,11 @@ interface IState extends IThemeState { customThemeUrl: string, customThemeMessage: CustomThemeMessage, useCustomFontSize: boolean, + useIRCLayout: boolean, } +const MESSAGE_PREVIEW_TEXT = "Hey you. You're the best" + export default class AppearanceUserSettingsTab extends React.Component { private themeTimer: NodeJS.Timeout; @@ -67,6 +70,7 @@ export default class AppearanceUserSettingsTab extends React.Component): void => { + const val = e.target.value === "true"; + + this.setState({ + useIRCLayout: val, + }); + + SettingsStore.setValue("useIRCLayout", null, SettingLevel.DEVICE, val); + } + private renderThemeSection() { const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); const StyledCheckbox = sdk.getComponent("views.elements.StyledCheckbox"); @@ -287,7 +301,8 @@ export default class AppearanceUserSettingsTab extends React.Component{_t("Font size")}
Aa
@@ -323,6 +338,49 @@ export default class AppearanceUserSettingsTab extends React.Component; } + private renderLayoutSection = () => { + const StyledRadioButton = sdk.getComponent("views.elements.StyledRadioButton"); + const MessagePreview = sdk.getComponent("views.elements.MessagePreview"); + + return
+ {_t("Message layout")} + +
+
+ + + {_t("Compact")} + +
+
+
+ + + {_t("Modern")} + +
+
+
+ } + render() { return (
@@ -332,6 +390,7 @@ export default class AppearanceUserSettingsTab extends React.Component {this.renderThemeSection()} {SettingsStore.isFeatureEnabled("feature_font_scaling") ? this.renderFontSection() : null} + {SettingsStore.isFeatureEnabled("feature_irc_ui") ? this.renderLayoutSection() : null}
); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index aacec471f6..157d83ba57 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -434,7 +434,7 @@ "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", "Use the improved room list (in development - refresh to apply changes)": "Use the improved room list (in development - refresh to apply changes)", "Support adding custom themes": "Support adding custom themes", - "Use IRC layout": "Use IRC layout", + "Enable IRC layout option in the appearance tab": "Enable IRC layout option in the appearance tab", "Show info about bridges in room settings": "Show info about bridges in room settings", "Font size": "Font size", "Use custom size": "Use custom size", @@ -483,6 +483,7 @@ "How fast should messages be downloaded.": "How fast should messages be downloaded.", "Manually verify all remote sessions": "Manually verify all remote sessions", "IRC display name width": "IRC display name width", + "Use IRC layout": "Use IRC layout", "Collecting app version information": "Collecting app version information", "Collecting logs": "Collecting logs", "Uploading report": "Uploading report", @@ -779,6 +780,9 @@ "Custom theme URL": "Custom theme URL", "Add theme": "Add theme", "Theme": "Theme", + "Message layout": "Message layout", + "Compact": "Compact", + "Modern": "Modern", "Customise your appearance": "Customise your appearance", "Appearance Settings only affect this Riot session.": "Appearance Settings only affect this Riot session.", "Flair": "Flair", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index b3ee71c767..70e29483a2 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -152,7 +152,7 @@ export const SETTINGS = { }, "feature_irc_ui": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, - displayName: _td('Use IRC layout'), + displayName: _td('Enable IRC layout option in the appearance tab'), default: false, isFeature: true, }, @@ -549,4 +549,9 @@ export const SETTINGS = { displayName: _td("IRC display name width"), default: 80, }, + "useIRCLayout": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + displayName: _td("Use IRC layout"), + default: false, + }, }; From 2d6077f2c8a9fe3233e4593ee5b58cf933aa8e2e Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 16 Jun 2020 18:02:34 +0100 Subject: [PATCH 062/194] Fix radio circle color --- .../views/settings/tabs/user/_AppearanceUserSettingsTab.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss index 7fefdd1505..b6fe9adccb 100644 --- a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss @@ -189,6 +189,10 @@ limitations under the License. .mx_RadioButton { border-top: 1px solid $input-darker-bg-color; + + > input + div { + border-color: rgba($muted-fg-color, 0.2); + } } .mx_RadioButton_checked { From a250bf6a971be00c582687af2db823ffcd4cedc5 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 16 Jun 2020 18:08:49 +0100 Subject: [PATCH 063/194] Fix whitespace --- .../views/settings/tabs/user/_AppearanceUserSettingsTab.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss index b6fe9adccb..044b5e2240 100644 --- a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss @@ -45,7 +45,7 @@ limitations under the License. .mx_EventTile_msgOption { display: none; -} + } &.mx_IRCLayout { padding-top: 9px; @@ -199,4 +199,3 @@ limitations under the License. background-color: rgba($accent-color, 0.08); } } - From bcebef7e5631e98e6ad7df5b2f1e5c082dbd3bbb Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 16 Jun 2020 12:13:12 -0600 Subject: [PATCH 064/194] Add a globe icon to public rooms For https://github.com/vector-im/riot-web/issues/14039 --- res/css/views/rooms/_RoomTile2.scss | 25 +++++++++++++++++++++ res/img/globe.svg | 6 +++++ src/components/views/rooms/RoomSublist2.tsx | 1 + src/components/views/rooms/RoomTile2.tsx | 13 +++++++++++ 4 files changed, 45 insertions(+) create mode 100644 res/img/globe.svg diff --git a/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss index f74d0ff5a4..2e9fe4a31e 100644 --- a/res/css/views/rooms/_RoomTile2.scss +++ b/res/css/views/rooms/_RoomTile2.scss @@ -32,6 +32,31 @@ limitations under the License. .mx_RoomTile2_avatarContainer { margin-right: 8px; + position: relative; + + .mx_RoomTile2_publicRoom { + width: 12px; + height: 12px; + border-radius: 12px; + background-color: $roomlist2-bg-color; // to match the room list itself + position: absolute; + bottom: 0; + right: 0; + + &::before { + content: ''; + width: 8px; + height: 8px; + top: 2px; + left: 2px; + position: absolute; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background: $primary-fg-color; + mask-image: url('$(res)/img/globe.svg'); + } + } } .mx_RoomTile2_nameContainer { diff --git a/res/img/globe.svg b/res/img/globe.svg new file mode 100644 index 0000000000..cc22bc6e66 --- /dev/null +++ b/res/img/globe.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 9f8b8579c3..f03cb3ecbd 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -145,6 +145,7 @@ export default class RoomSublist2 extends React.Component { key={`room-${room.roomId}`} showMessagePreview={this.props.layout.showPreviews} isMinimized={this.props.isMinimized} + tag={this.props.layout.tagId} /> ); } diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 10845d3840..f0d99eed99 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -44,6 +44,7 @@ interface IProps { room: Room; showMessagePreview: boolean; isMinimized: boolean; + tag: TagID; // TODO: Allow falsifying counts (for invites and stuff) // TODO: Transparency? Was this ever used? @@ -85,6 +86,12 @@ export default class RoomTile2 extends React.Component { ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate); } + private get isPublicRoom(): boolean { + const joinRules = this.props.room.currentState.getStateEvents("m.room.join_rules", ""); + const joinRule = joinRules && joinRules.getContent().join_rule; + return joinRule === 'public'; + } + public componentWillUnmount() { if (this.props.room) { ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate); @@ -287,6 +294,11 @@ export default class RoomTile2 extends React.Component { ); if (this.props.isMinimized) nameContainer = null; + let globe = null; + if (this.isPublicRoom && this.props.tag !== DefaultTagID.DM) { + globe = ; // sizing and such set by CSS + } + const avatarSize = 32; return ( @@ -304,6 +316,7 @@ export default class RoomTile2 extends React.Component { >
+ {globe}
{nameContainer}
From e4a51a7c01fe3d6f2e0aab8168c8afa7044d57fc Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 16 Jun 2020 14:43:48 -0600 Subject: [PATCH 065/194] Add presence icons; Convert to generic icon component For https://github.com/vector-im/riot-web/issues/14039 --- res/css/_components.scss | 1 + res/css/views/rooms/_RoomTile2.scss | 20 +-- res/css/views/rooms/_RoomTileIcon.scss | 69 +++++++++ res/themes/light/css/_light.scss | 4 + src/components/views/rooms/RoomTile2.tsx | 28 ++-- src/components/views/rooms/RoomTileIcon.tsx | 148 ++++++++++++++++++++ src/utils/presence.ts | 26 ++++ 7 files changed, 258 insertions(+), 38 deletions(-) create mode 100644 res/css/views/rooms/_RoomTileIcon.scss create mode 100644 src/components/views/rooms/RoomTileIcon.tsx create mode 100644 src/utils/presence.ts diff --git a/res/css/_components.scss b/res/css/_components.scss index 31f319e76f..66eb98ea9d 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -189,6 +189,7 @@ @import "./views/rooms/_RoomSublist2.scss"; @import "./views/rooms/_RoomTile.scss"; @import "./views/rooms/_RoomTile2.scss"; +@import "./views/rooms/_RoomTileIcon.scss"; @import "./views/rooms/_RoomUpgradeWarningBar.scss"; @import "./views/rooms/_SearchBar.scss"; @import "./views/rooms/_SendMessageComposer.scss"; diff --git a/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss index 2e9fe4a31e..001499fea5 100644 --- a/res/css/views/rooms/_RoomTile2.scss +++ b/res/css/views/rooms/_RoomTile2.scss @@ -34,28 +34,10 @@ limitations under the License. margin-right: 8px; position: relative; - .mx_RoomTile2_publicRoom { - width: 12px; - height: 12px; - border-radius: 12px; - background-color: $roomlist2-bg-color; // to match the room list itself + .mx_RoomTileIcon { position: absolute; bottom: 0; right: 0; - - &::before { - content: ''; - width: 8px; - height: 8px; - top: 2px; - left: 2px; - position: absolute; - mask-position: center; - mask-size: contain; - mask-repeat: no-repeat; - background: $primary-fg-color; - mask-image: url('$(res)/img/globe.svg'); - } } } diff --git a/res/css/views/rooms/_RoomTileIcon.scss b/res/css/views/rooms/_RoomTileIcon.scss new file mode 100644 index 0000000000..adc8ea2994 --- /dev/null +++ b/res/css/views/rooms/_RoomTileIcon.scss @@ -0,0 +1,69 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +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_RoomTileIcon { + width: 12px; + height: 12px; + border-radius: 12px; + background-color: $roomlist2-bg-color; // to match the room list itself +} + +.mx_RoomTileIcon_globe::before { + content: ''; + width: 8px; + height: 8px; + top: 2px; + left: 2px; + position: absolute; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background: $primary-fg-color; + mask-image: url('$(res)/img/globe.svg'); +} + +.mx_RoomTileIcon_offline::before { + content: ''; + width: 8px; + height: 8px; + top: 2px; + left: 2px; + position: absolute; + border-radius: 8px; + background-color: $presence-offline; +} + +.mx_RoomTileIcon_online::before { + content: ''; + width: 8px; + height: 8px; + top: 2px; + left: 2px; + position: absolute; + border-radius: 8px; + background-color: $presence-online; +} + +.mx_RoomTileIcon_away::before { + content: ''; + width: 8px; + height: 8px; + top: 2px; + left: 2px; + position: absolute; + border-radius: 8px; + background-color: $presence-away; +} diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 18a25b2663..355cc1301c 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -186,6 +186,10 @@ $roomtile2-preview-color: #9e9e9e; $roomtile2-default-badge-bg-color: #61708b; $roomtile2-selected-bg-color: #FFF; +$presence-online: $accent-color; +$presence-away: orange; // TODO: Get color +$presence-offline: #E3E8F0; + // ******************** $roomtile-name-color: #61708b; diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index f0d99eed99..8343851f66 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -21,7 +21,7 @@ import React, { createRef } from "react"; import { Room } from "matrix-js-sdk/src/models/room"; import classNames from "classnames"; import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; -import AccessibleButton, {ButtonEvent} from "../../views/elements/AccessibleButton"; +import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton"; import RoomAvatar from "../../views/avatars/RoomAvatar"; import dis from '../../../dispatcher/dispatcher'; import { Key } from "../../../Keyboard"; @@ -31,6 +31,7 @@ import { _t } from "../../../languageHandler"; import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu"; import { DefaultTagID, TagID } from "../../../stores/room-list/models"; import { MessagePreviewStore } from "../../../stores/MessagePreviewStore"; +import RoomTileIcon from "./RoomTileIcon"; /******************************************************************* * CAUTION * @@ -86,12 +87,6 @@ export default class RoomTile2 extends React.Component { ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate); } - private get isPublicRoom(): boolean { - const joinRules = this.props.room.currentState.getStateEvents("m.room.join_rules", ""); - const joinRule = joinRules && joinRules.getContent().join_rule; - return joinRule === 'public'; - } - public componentWillUnmount() { if (this.props.room) { ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate); @@ -187,25 +182,25 @@ export default class RoomTile2 extends React.Component {
  • this.onTagRoom(e, DefaultTagID.Favourite)}> - + {_t("Favourite")}
  • this.onTagRoom(e, DefaultTagID.LowPriority)}> - + {_t("Low Priority")}
  • this.onTagRoom(e, DefaultTagID.DM)}> - + {_t("Direct Chat")}
  • - + {_t("Settings")}
  • @@ -215,7 +210,7 @@ export default class RoomTile2 extends React.Component {
    • - + {_t("Leave Room")}
    • @@ -253,7 +248,7 @@ export default class RoomTile2 extends React.Component { 'mx_RoomTile2_minimized': this.props.isMinimized, }); - const badge = ; + const badge = ; // TODO: the original RoomTile uses state for the room name. Do we need to? let name = this.props.room.name; @@ -294,11 +289,6 @@ export default class RoomTile2 extends React.Component { ); if (this.props.isMinimized) nameContainer = null; - let globe = null; - if (this.isPublicRoom && this.props.tag !== DefaultTagID.DM) { - globe = ; // sizing and such set by CSS - } - const avatarSize = 32; return ( @@ -316,7 +306,7 @@ export default class RoomTile2 extends React.Component { >
      - {globe} +
      {nameContainer}
      diff --git a/src/components/views/rooms/RoomTileIcon.tsx b/src/components/views/rooms/RoomTileIcon.tsx new file mode 100644 index 0000000000..fb967bb811 --- /dev/null +++ b/src/components/views/rooms/RoomTileIcon.tsx @@ -0,0 +1,148 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 New Vector Ltd +Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2019, 2020 The Matrix.org Foundation C.I.C. + +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 { Room } from "matrix-js-sdk/src/models/room"; +import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; +import AccessibleButton from "../../views/elements/AccessibleButton"; +import RoomAvatar from "../../views/avatars/RoomAvatar"; +import ActiveRoomObserver from "../../../ActiveRoomObserver"; +import { DefaultTagID, TagID } from "../../../stores/room-list/models"; +import { User } from "matrix-js-sdk/src/models/user"; +import {MatrixEvent} from "matrix-js-sdk/src/models/event"; +import DMRoomMap from "../../../utils/DMRoomMap"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import SdkConfig from "../../../SdkConfig"; +import { isPresenceEnabled } from "../../../utils/presence"; + +enum Icon { + // Note: the names here are used in CSS class names + None = "NONE", // ... except this one + Globe = "GLOBE", + PresenceOnline = "ONLINE", + PresenceAway = "AWAY", + PresenceOffline = "OFFLINE", +} + +interface IProps { + room: Room; + tag: TagID; +} + +interface IState { + icon: Icon; +} + +export default class RoomTileIcon extends React.Component { + private isUnmounted = false; + private dmUser: User; + private isWatchingTimeline = false; + + constructor(props: IProps) { + super(props); + + this.state = { + icon: this.getIcon(), + }; + } + + private get isPublicRoom(): boolean { + const joinRules = this.props.room.currentState.getStateEvents("m.room.join_rules", ""); + const joinRule = joinRules && joinRules.getContent().join_rule; + return joinRule === 'public'; + } + + public componentWillUnmount() { + this.isUnmounted = true; + if (this.isWatchingTimeline) this.props.room.off('Room.timeline', this.onRoomTimeline); + this.unsubscribePresence(); + } + + private unsubscribePresence() { + if (this.dmUser) { + this.dmUser.off('User.currentlyActive', this.onPresenceUpdate); + this.dmUser.off('User.presence', this.onPresenceUpdate); + } + } + + private onRoomTimeline = (ev: MatrixEvent, room: Room) => { + if (this.isUnmounted) return; + + // apparently these can happen? + if (!room) return; + if (this.props.room.roomId !== room.roomId) return; + + if (ev.getType() === 'm.room.join_rules' || ev.getType() === 'm.room.member') { + this.setState({icon: this.getIcon()}); + } + }; + + private onPresenceUpdate = () => { + if (this.isUnmounted) return; + + let newIcon = this.getPresenceIcon(); + if (newIcon !== this.state.icon) this.setState({icon: newIcon}); + }; + + private getPresenceIcon(): Icon { + let newIcon = Icon.None; + + const isOnline = this.dmUser.currentlyActive || this.dmUser.presence === 'online'; + if (isOnline) { + newIcon = Icon.PresenceOnline; + } else if (this.dmUser.presence === 'offline') { + newIcon = Icon.PresenceOffline; + } else if (this.dmUser.presence === 'unavailable') { + newIcon = Icon.PresenceAway; + } + + return newIcon; + } + + private getIcon(): Icon { + let defaultIcon = Icon.None; + this.unsubscribePresence(); + if (this.props.tag === DefaultTagID.DM && this.props.room.getJoinedMemberCount() === 2) { + // Track presence, if available + if (isPresenceEnabled()) { + const otherUserId = DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId); + if (otherUserId) { + this.dmUser = MatrixClientPeg.get().getUser(otherUserId); + if (this.dmUser) { + this.dmUser.on('User.currentlyActive', this.onPresenceUpdate); + this.dmUser.on('User.presence', this.onPresenceUpdate); + defaultIcon = this.getPresenceIcon(); + } + } + } + } else { + // Track publicity + defaultIcon = this.isPublicRoom ? Icon.Globe : Icon.None; + this.props.room.on('Room.timeline', this.onRoomTimeline); + this.isWatchingTimeline = true; + } + return defaultIcon; + } + + public render(): React.ReactElement { + if (this.state.icon === Icon.None) return null; + + return ; + } +} diff --git a/src/utils/presence.ts b/src/utils/presence.ts new file mode 100644 index 0000000000..f2c208265e --- /dev/null +++ b/src/utils/presence.ts @@ -0,0 +1,26 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { MatrixClientPeg } from "../MatrixClientPeg"; +import SdkConfig from "../SdkConfig"; + +export function isPresenceEnabled() { + const hsUrl = MatrixClientPeg.get().baseUrl; + const urls = SdkConfig.get()['enable_presence_by_hs_url']; + if (!urls) return true; + if (urls[hsUrl] || urls[hsUrl] === undefined) return true; + return false; +} From 049e3fc08c6bf55789151658b54d70ecf42f9d94 Mon Sep 17 00:00:00 2001 From: Dale Harvey Date: Thu, 30 Jan 2020 11:18:14 +0100 Subject: [PATCH 066/194] Add some media queries to improve UI on mobile --- res/css/views/auth/_AuthBody.scss | 8 +++++++- res/css/views/auth/_AuthHeader.scss | 6 ++++++ res/css/views/auth/_AuthHeaderLogo.scss | 6 ++++++ res/css/views/auth/_AuthPage.scss | 6 ++++++ res/css/views/rooms/_EventTile.scss | 11 +++++++++++ res/css/views/rooms/_RoomHeader.scss | 9 +++++++++ 6 files changed, 45 insertions(+), 1 deletion(-) diff --git a/res/css/views/auth/_AuthBody.scss b/res/css/views/auth/_AuthBody.scss index 120da4c4f1..a19dddf43f 100644 --- a/res/css/views/auth/_AuthBody.scss +++ b/res/css/views/auth/_AuthBody.scss @@ -16,7 +16,7 @@ limitations under the License. */ .mx_AuthBody { - width: 500px; + max-width: 500px; font-size: $font-12px; color: $authpage-secondary-color; background-color: $authpage-body-bg-color; @@ -146,3 +146,9 @@ limitations under the License. .mx_AuthBody_spinner { margin: 1em 0; } + +@media only screen and (max-width : 480px) { + .mx_AuthBody { + padding: 10px; + } +} diff --git a/res/css/views/auth/_AuthHeader.scss b/res/css/views/auth/_AuthHeader.scss index b3d07b1925..0527f65beb 100644 --- a/res/css/views/auth/_AuthHeader.scss +++ b/res/css/views/auth/_AuthHeader.scss @@ -21,3 +21,9 @@ limitations under the License. padding: 25px 40px; box-sizing: border-box; } + +@media only screen and (max-width : 480px) { + .mx_AuthHeader { + display: none; + } +} diff --git a/res/css/views/auth/_AuthHeaderLogo.scss b/res/css/views/auth/_AuthHeaderLogo.scss index 091fb0197b..4ef7f95fff 100644 --- a/res/css/views/auth/_AuthHeaderLogo.scss +++ b/res/css/views/auth/_AuthHeaderLogo.scss @@ -23,3 +23,9 @@ limitations under the License. .mx_AuthHeaderLogo img { width: 100%; } + +@media only screen and (max-width : 480px) { + .mx_AuthHeaderLogo { + display: none; + } +} diff --git a/res/css/views/auth/_AuthPage.scss b/res/css/views/auth/_AuthPage.scss index 8ef48b6265..e58095c70c 100644 --- a/res/css/views/auth/_AuthPage.scss +++ b/res/css/views/auth/_AuthPage.scss @@ -29,3 +29,9 @@ limitations under the License. box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.33); background-color: $authpage-modal-bg-color; } + +@media only screen and (max-width : 480px) { + .mx_AuthPage_modal { + margin-top: 0; + } +} diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 40a80f17bb..0e0552e3c8 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -572,3 +572,14 @@ limitations under the License. margin-left: 1em; } } + +@media only screen and (max-width : 480px) { + .mx_EventTile_line, .mx_EventTile_reply { + padding-left: 0; + margin-right: 0; + } + .mx_EventTile_content { + margin-top: 10px; + margin-right: 0; + } +} diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index 80f6c40f39..265e39624e 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -267,3 +267,12 @@ limitations under the License. .mx_RoomHeader_pinsIndicatorUnread { background-color: $pinned-unread-color; } + +@media only screen and (max-width : 480px) { + .mx_RoomHeader_wrapper { + padding: 0; + } + .mx_RoomHeader { + overflow: hidden; + } +} From af1c2f9b2902780f5b300a0cebe15ce85842f6b8 Mon Sep 17 00:00:00 2001 From: Jovan Gerodetti Date: Wed, 27 May 2020 19:33:55 +0200 Subject: [PATCH 067/194] fix requested changes from #3991 Signed-off-by: Jovan Gerodetti --- res/css/views/auth/_AuthBody.scss | 13 ++++++++----- res/css/views/auth/_AuthHeader.scss | 8 ++++---- res/css/views/auth/_AuthHeaderLogo.scss | 8 ++++---- res/css/views/auth/_AuthPage.scss | 8 ++++---- res/css/views/rooms/_EventTile.scss | 18 +++++++++--------- res/css/views/rooms/_RoomHeader.scss | 14 +++++++------- 6 files changed, 36 insertions(+), 33 deletions(-) diff --git a/res/css/views/auth/_AuthBody.scss b/res/css/views/auth/_AuthBody.scss index a19dddf43f..b8bb1b04c2 100644 --- a/res/css/views/auth/_AuthBody.scss +++ b/res/css/views/auth/_AuthBody.scss @@ -16,7 +16,7 @@ limitations under the License. */ .mx_AuthBody { - max-width: 500px; + width: 500px; font-size: $font-12px; color: $authpage-secondary-color; background-color: $authpage-body-bg-color; @@ -147,8 +147,11 @@ limitations under the License. margin: 1em 0; } -@media only screen and (max-width : 480px) { - .mx_AuthBody { - padding: 10px; - } +@media only screen and (max-width: 480px) { + .mx_AuthBody { + border-radius: 4px; + width: auto; + max-width: 500px; + padding: 10px; + } } diff --git a/res/css/views/auth/_AuthHeader.scss b/res/css/views/auth/_AuthHeader.scss index 0527f65beb..b1372affee 100644 --- a/res/css/views/auth/_AuthHeader.scss +++ b/res/css/views/auth/_AuthHeader.scss @@ -22,8 +22,8 @@ limitations under the License. box-sizing: border-box; } -@media only screen and (max-width : 480px) { - .mx_AuthHeader { - display: none; - } +@media only screen and (max-width: 480px) { + .mx_AuthHeader { + display: none; + } } diff --git a/res/css/views/auth/_AuthHeaderLogo.scss b/res/css/views/auth/_AuthHeaderLogo.scss index 4ef7f95fff..917dcabf67 100644 --- a/res/css/views/auth/_AuthHeaderLogo.scss +++ b/res/css/views/auth/_AuthHeaderLogo.scss @@ -24,8 +24,8 @@ limitations under the License. width: 100%; } -@media only screen and (max-width : 480px) { - .mx_AuthHeaderLogo { - display: none; - } +@media only screen and (max-width: 480px) { + .mx_AuthHeaderLogo { + display: none; + } } diff --git a/res/css/views/auth/_AuthPage.scss b/res/css/views/auth/_AuthPage.scss index e58095c70c..e3409792f0 100644 --- a/res/css/views/auth/_AuthPage.scss +++ b/res/css/views/auth/_AuthPage.scss @@ -30,8 +30,8 @@ limitations under the License. background-color: $authpage-modal-bg-color; } -@media only screen and (max-width : 480px) { - .mx_AuthPage_modal { - margin-top: 0; - } +@media only screen and (max-width: 480px) { + .mx_AuthPage_modal { + margin-top: 0; + } } diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 0e0552e3c8..2b204955d8 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -573,13 +573,13 @@ limitations under the License. } } -@media only screen and (max-width : 480px) { - .mx_EventTile_line, .mx_EventTile_reply { - padding-left: 0; - margin-right: 0; - } - .mx_EventTile_content { - margin-top: 10px; - margin-right: 0; - } +@media only screen and (max-width: 480px) { + .mx_EventTile_line, .mx_EventTile_reply { + padding-left: 0; + margin-right: 0; + } + .mx_EventTile_content { + margin-top: 10px; + margin-right: 0; + } } diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index 265e39624e..a047a6f9b4 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -268,11 +268,11 @@ limitations under the License. background-color: $pinned-unread-color; } -@media only screen and (max-width : 480px) { - .mx_RoomHeader_wrapper { - padding: 0; - } - .mx_RoomHeader { - overflow: hidden; - } +@media only screen and (max-width: 480px) { + .mx_RoomHeader_wrapper { + padding: 0; + } + .mx_RoomHeader { + overflow: hidden; + } } From b69a5a525d3b4339d63be3ee5fdbe50e7b612897 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 16 Jun 2020 17:29:36 -0600 Subject: [PATCH 068/194] Fix spaces --- src/components/views/rooms/RoomTile2.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 8343851f66..0d13f856e9 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -182,25 +182,25 @@ export default class RoomTile2 extends React.Component {
      • this.onTagRoom(e, DefaultTagID.Favourite)}> - + {_t("Favourite")}
      • this.onTagRoom(e, DefaultTagID.LowPriority)}> - + {_t("Low Priority")}
      • this.onTagRoom(e, DefaultTagID.DM)}> - + {_t("Direct Chat")}
      • - + {_t("Settings")}
      • @@ -210,7 +210,7 @@ export default class RoomTile2 extends React.Component {
        • - + {_t("Leave Room")}
        • @@ -248,7 +248,7 @@ export default class RoomTile2 extends React.Component { 'mx_RoomTile2_minimized': this.props.isMinimized, }); - const badge = ; + const badge = ; // TODO: the original RoomTile uses state for the room name. Do we need to? let name = this.props.room.name; From daa552e25021c8d4a67912f42fa741579012b494 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 16 Jun 2020 17:35:56 -0600 Subject: [PATCH 069/194] Refactor listener usage --- src/components/views/rooms/RoomTileIcon.tsx | 78 +++++++++++---------- 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/src/components/views/rooms/RoomTileIcon.tsx b/src/components/views/rooms/RoomTileIcon.tsx index fb967bb811..b0cf10e313 100644 --- a/src/components/views/rooms/RoomTileIcon.tsx +++ b/src/components/views/rooms/RoomTileIcon.tsx @@ -1,8 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 New Vector Ltd -Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,16 +16,11 @@ limitations under the License. import React from "react"; import { Room } from "matrix-js-sdk/src/models/room"; -import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; -import AccessibleButton from "../../views/elements/AccessibleButton"; -import RoomAvatar from "../../views/avatars/RoomAvatar"; -import ActiveRoomObserver from "../../../ActiveRoomObserver"; import { DefaultTagID, TagID } from "../../../stores/room-list/models"; import { User } from "matrix-js-sdk/src/models/user"; -import {MatrixEvent} from "matrix-js-sdk/src/models/event"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import DMRoomMap from "../../../utils/DMRoomMap"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import SdkConfig from "../../../SdkConfig"; import { isPresenceEnabled } from "../../../utils/presence"; enum Icon { @@ -50,15 +42,15 @@ interface IState { } export default class RoomTileIcon extends React.Component { + private _dmUser: User; private isUnmounted = false; - private dmUser: User; private isWatchingTimeline = false; constructor(props: IProps) { super(props); this.state = { - icon: this.getIcon(), + icon: this.calculateIcon(), }; } @@ -68,17 +60,27 @@ export default class RoomTileIcon extends React.Component { return joinRule === 'public'; } + private get dmUser(): User { + return this._dmUser; + } + + private set dmUser(val: User) { + const oldUser = this._dmUser; + this._dmUser = val; + if (oldUser && oldUser !== this._dmUser) { + oldUser.off('User.currentlyActive', this.onPresenceUpdate); + oldUser.off('User.presence', this.onPresenceUpdate); + } + if (this._dmUser && oldUser !== this._dmUser) { + this._dmUser.on('User.currentlyActive', this.onPresenceUpdate); + this._dmUser.on('User.presence', this.onPresenceUpdate); + } + } + public componentWillUnmount() { this.isUnmounted = true; if (this.isWatchingTimeline) this.props.room.off('Room.timeline', this.onRoomTimeline); - this.unsubscribePresence(); - } - - private unsubscribePresence() { - if (this.dmUser) { - this.dmUser.off('User.currentlyActive', this.onPresenceUpdate); - this.dmUser.off('User.presence', this.onPresenceUpdate); - } + this.dmUser = null; // clear listeners, if any } private onRoomTimeline = (ev: MatrixEvent, room: Room) => { @@ -89,7 +91,7 @@ export default class RoomTileIcon extends React.Component { if (this.props.room.roomId !== room.roomId) return; if (ev.getType() === 'm.room.join_rules' || ev.getType() === 'm.room.member') { - this.setState({icon: this.getIcon()}); + this.setState({icon: this.calculateIcon()}); } }; @@ -101,43 +103,43 @@ export default class RoomTileIcon extends React.Component { }; private getPresenceIcon(): Icon { - let newIcon = Icon.None; + if (!this.dmUser) return Icon.None; + + let icon = Icon.None; const isOnline = this.dmUser.currentlyActive || this.dmUser.presence === 'online'; if (isOnline) { - newIcon = Icon.PresenceOnline; + icon = Icon.PresenceOnline; } else if (this.dmUser.presence === 'offline') { - newIcon = Icon.PresenceOffline; + icon = Icon.PresenceOffline; } else if (this.dmUser.presence === 'unavailable') { - newIcon = Icon.PresenceAway; + icon = Icon.PresenceAway; } - return newIcon; + return icon; } - private getIcon(): Icon { - let defaultIcon = Icon.None; - this.unsubscribePresence(); + private calculateIcon(): Icon { + let icon = Icon.None; + if (this.props.tag === DefaultTagID.DM && this.props.room.getJoinedMemberCount() === 2) { // Track presence, if available if (isPresenceEnabled()) { const otherUserId = DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId); if (otherUserId) { this.dmUser = MatrixClientPeg.get().getUser(otherUserId); - if (this.dmUser) { - this.dmUser.on('User.currentlyActive', this.onPresenceUpdate); - this.dmUser.on('User.presence', this.onPresenceUpdate); - defaultIcon = this.getPresenceIcon(); - } + icon = this.getPresenceIcon(); } } } else { // Track publicity - defaultIcon = this.isPublicRoom ? Icon.Globe : Icon.None; - this.props.room.on('Room.timeline', this.onRoomTimeline); - this.isWatchingTimeline = true; + icon = this.isPublicRoom ? Icon.Globe : Icon.None; + if (!this.isWatchingTimeline) { + this.props.room.on('Room.timeline', this.onRoomTimeline); + this.isWatchingTimeline = true; + } } - return defaultIcon; + return icon; } public render(): React.ReactElement { From 7a71ef9b6bba38b62c388f0cd5abf389b5856b6b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 16 Jun 2020 17:40:25 -0600 Subject: [PATCH 070/194] Fix another space --- src/components/views/rooms/RoomTile2.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 0d13f856e9..edd92836df 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -306,7 +306,7 @@ export default class RoomTile2 extends React.Component { >
          - +
          {nameContainer}
          From 9878c1dc345c14338a978f3c2ebe54b6c3fb6a21 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 16 Jun 2020 17:42:26 -0600 Subject: [PATCH 071/194] and another --- src/components/views/rooms/RoomTile2.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index edd92836df..671b981ac5 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -305,7 +305,7 @@ export default class RoomTile2 extends React.Component { role="treeitem" >
          - +
          {nameContainer} From a07918fef94334b1b06b8fba7e0d919b9a33328e Mon Sep 17 00:00:00 2001 From: "J. A. Durieux" Date: Tue, 16 Jun 2020 21:46:54 +0000 Subject: [PATCH 072/194] Translated using Weblate (Dutch) Currently translated at 88.9% (2034 of 2287 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/nl/ --- src/i18n/strings/nl.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index 69909e942e..e0565ec3dd 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -2211,12 +2211,18 @@ "Are you sure you want to deactivate your account? This is irreversible.": "Weet u zeker dat u uw account wil sluiten? Dit is onomkeerbaar.", "Confirm account deactivation": "Bevestig accountsluiting", "Room name or address": "Gespreksnaam of -adres", - "Joins room with given address": "Treedt tot het gesprek met het opgegeven adres toe", + "Joins room with given address": "Neem aan het gesprek met dat adres deel", "Unrecognised room address:": "Gespreksadres niet herkend:", "Help us improve Riot": "Help ons Riot nog beter te maken", "Send anonymous usage data which helps us improve Riot. This will use a cookie.": "Stuur anonieme gebruiksinformatie waarmee we Riot kunnen verbeteren. Dit plaatst een cookie.", "I want to help": "Ik wil helpen", "Your homeserver has exceeded its user limit.": "Uw thuisserver heeft het maximaal aantal gebruikers overschreden.", "Your homeserver has exceeded one of its resource limits.": "Uw thuisserver heeft een van zijn limieten overschreden.", - "Ok": "Oké" + "Ok": "Oké", + "sent an image.": "heeft een plaatje gestuurd.", + "Light": "Helder", + "Dark": "Donker", + "Set password": "Stel wachtwoord in", + "To return to your account in future you need to set a password": "Zonder wachtwoord kunt u later niet tot uw account terugkeren", + "Restart": "Herstarten" } From aab7e0cc144952cbbc7628a26deb726858842075 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 16 Jun 2020 18:56:56 -0600 Subject: [PATCH 073/194] Fix alignment of checkboxes in new room list's context menu At somepoint the checkbox lost its padding, so we don't need to counteract it. --- res/css/views/rooms/_RoomSublist2.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index 3f5f654494..ad827ba3b1 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -320,8 +320,4 @@ limitations under the License. .mx_RadioButton, .mx_Checkbox { margin-top: 8px; } - - .mx_Checkbox { - margin-left: -8px; // to counteract the indent from the component - } } From 493a2c3861aee25dbd35838136a8241e6beb3ea0 Mon Sep 17 00:00:00 2001 From: random Date: Wed, 17 Jun 2020 10:49:17 +0000 Subject: [PATCH 074/194] Translated using Weblate (Italian) Currently translated at 99.6% (2278 of 2287 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/it/ --- src/i18n/strings/it.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 8272dc126b..2ffe6dfef2 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -2502,5 +2502,15 @@ "Leave Room": "Lascia stanza", "Room options": "Opzioni stanza", "Activity": "Attività", - "A-Z": "A-Z" + "A-Z": "A-Z", + "Light": "Chiaro", + "Dark": "Scuro", + "Customise your appearance": "Personalizza l'aspetto", + "Appearance Settings only affect this Riot session.": "Le impostazioni dell'aspetto hanno effetto solo in questa sessione di Riot.", + "Recovery Key": "Chiave di recupero", + "This isn't the recovery key for your account": "Questa non è la chiave di recupero del tuo account", + "This isn't a valid recovery key": "Questa non è una chiave di ripristino valida", + "Looks good!": "Sembra giusta!", + "Use Recovery Key or Passphrase": "Usa la chiave o password di recupero", + "Use Recovery Key": "Usa chiave di recupero" } From 6df57eccebeb4cc88d67242edef30bab56063647 Mon Sep 17 00:00:00 2001 From: Alexey Murz Korepov Date: Wed, 17 Jun 2020 10:15:14 +0000 Subject: [PATCH 075/194] Translated using Weblate (Russian) Currently translated at 86.8% (1985 of 2287 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ru/ --- src/i18n/strings/ru.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index be9c6bfdd4..eaf38d6fe4 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -97,13 +97,13 @@ "Who can read history?": "Кто может читать историю?", "You do not have permission to post to this room": "Вы не можете писать в эту комнату", "You have no visible notifications": "Нет видимых уведомлений", - "%(targetName)s accepted an invitation.": "%(targetName)s принимает приглашение.", - "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s принимает приглашение от %(displayName)s.", + "%(targetName)s accepted an invitation.": "%(targetName)s принял приглашение.", + "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s принял приглашение от %(displayName)s.", "Active call": "Активный вызов", "%(senderName)s answered the call.": "%(senderName)s ответил(а) на звонок.", "%(senderName)s banned %(targetName)s.": "%(senderName)s забанил(а) %(targetName)s.", "Call Timeout": "Нет ответа", - "%(senderName)s changed their profile picture.": "%(senderName)s изменяет свой аватар.", + "%(senderName)s changed their profile picture.": "%(senderName)s изменил свой аватар.", "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s изменил(а) уровни прав %(powerLevelDiffText)s.", "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s изменил(а) название комнаты на %(roomName)s.", "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s изменил(а) тему комнаты на \"%(topic)s\".", @@ -115,8 +115,8 @@ "Failure to create room": "Не удалось создать комнату", "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "для %(userId)s с %(fromPowerLevel)s на %(toPowerLevel)s", "click to reveal": "нажмите для открытия", - "%(senderName)s invited %(targetName)s.": "%(senderName)s приглашает %(targetName)s.", - "%(targetName)s joined the room.": "%(targetName)s входит в комнату.", + "%(senderName)s invited %(targetName)s.": "%(senderName)s пригласил %(targetName)s.", + "%(targetName)s joined the room.": "%(targetName)s вошёл в комнату.", "%(senderName)s kicked %(targetName)s.": "%(senderName)s выгнал(а) %(targetName)s.", "%(targetName)s left the room.": "%(targetName)s покидает комнату.", "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s сделал(а) историю разговора видимой для всех собеседников с момента их приглашения.", @@ -223,9 +223,9 @@ "Reason": "Причина", "%(targetName)s rejected the invitation.": "%(targetName)s отклонил(а) приглашение.", "Reject invitation": "Отклонить приглашение", - "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s удаляет своё отображаемое имя (%(oldDisplayName)s).", - "%(senderName)s removed their profile picture.": "%(senderName)s удаляет свой аватар.", - "%(senderName)s requested a VoIP conference.": "%(senderName)s хочет начать конференц-звонок.", + "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s удалил своё отображаемое имя (%(oldDisplayName)s).", + "%(senderName)s removed their profile picture.": "%(senderName)s удалил свой аватар.", + "%(senderName)s requested a VoIP conference.": "%(senderName)s запросил конференц-звонок.", "Riot does not have permission to send you notifications - please check your browser settings": "У Riot нет разрешения на отправку уведомлений — проверьте настройки браузера", "Riot was not given permission to send notifications - please try again": "Riot не получил разрешение на отправку уведомлений, пожалуйста, попробуйте снова", "riot-web version:": "версия riot-web:", @@ -302,8 +302,8 @@ "Server may be unavailable, overloaded, or you hit a bug.": "Возможно, сервер недоступен, перегружен или случилась ошибка.", "Server unavailable, overloaded, or something else went wrong.": "Возможно, сервер недоступен, перегружен или что-то еще пошло не так.", "Session ID": "ID сессии", - "%(senderName)s set a profile picture.": "%(senderName)s устанавливает себе аватар.", - "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s меняет отображаемое имя на %(displayName)s.", + "%(senderName)s set a profile picture.": "%(senderName)s установил себе аватар.", + "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s изменил отображаемое имя на %(displayName)s.", "Signed Out": "Выполнен выход", "This room is not accessible by remote Matrix servers": "Это комната недоступна из других серверов Matrix", "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Попытка загрузить выбранный интервал истории чата этой комнаты не удалась, так как у вас нет разрешений на просмотр.", @@ -696,7 +696,7 @@ "This room is not public. You will not be able to rejoin without an invite.": "Эта комната не является публичной. Вы не сможете войти без приглашения.", "Community IDs cannot be empty.": "ID сообществ не могут быть пустыми.", "In reply to ": "В ответ на ", - "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s меняет отображаемое имя на %(displayName)s.", + "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s изменил отображаемое имя на %(displayName)s.", "Failed to set direct chat tag": "Не удалось установить тег прямого чата", "Failed to remove tag %(tagName)s from room": "Не удалось удалить тег %(tagName)s из комнаты", "Failed to add tag %(tagName)s to room": "Не удалось добавить тег %(tagName)s в комнату", From 2fcf30c1ec35c266c0af89e0d9c02bfc4b265bf5 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 17 Jun 2020 14:39:44 +0100 Subject: [PATCH 076/194] Fix copy '!' --- .../views/settings/tabs/user/AppearanceUserSettingsTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index 8838979021..881868907d 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -287,7 +287,7 @@ export default class AppearanceUserSettingsTab extends React.Component{_t("Font size")}
          Aa
          From 19d19033d18b7e75ff7f4e04605b9db84cf6357d Mon Sep 17 00:00:00 2001 From: GardeniaFair Date: Wed, 17 Jun 2020 13:33:46 +0000 Subject: [PATCH 077/194] Translated using Weblate (Spanish) Currently translated at 89.7% (2051 of 2287 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/es/ --- src/i18n/strings/es.json | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index 6b6058d837..7951a7844e 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -1557,7 +1557,7 @@ "Use your account or create a new one to continue.": "Usa tu cuenta existente o crea una nueva para continuar.", "Create Account": "Crear cuenta", "Sign In": "Registrarse", - "Sends a message as html, without interpreting it as markdown": "Envía un mensaje como html, sin interpretarlo como un markdown", + "Sends a message as html, without interpreting it as markdown": "Envía un mensaje como html, sin interpretarlo en markdown", "Failed to set topic": "No se ha podido establecer el tema", "Command failed": "El comando falló", "Could not find user in room": "No pude encontrar el usuario en la sala", @@ -1747,7 +1747,7 @@ "Can't find this server or its room list": "No puedo encontrar este servidor o su lista de salas", "All rooms": "Todas las salas", "Your server": "Tu", - "Are you sure you want to remove %(serverName)s": "¿Está seguro de querer eliminar %(serverName)s?", + "Are you sure you want to remove %(serverName)s": "¿ Está seguro de querer eliminar %(serverName)s?", "Remove server": "Quitar servidor", "Matrix": "Matrix", "Add a new server": "Añadir un nuevo servidor", @@ -2065,12 +2065,12 @@ "Unable to restore backup": "No se pudo restaurar la copia de seguridad", "No backup found!": "¡No se encontró una copia de seguridad!", "Keys restored": "Se restauraron las claves", - "Failed to decrypt %(failedCount)s sessions!": "¡Error en desencriptar %(failedCount) sesiones!", + "Failed to decrypt %(failedCount)s sessions!": "¡Error en descifrar %(failedCount) sesiones!", "Successfully restored %(sessionCount)s keys": "%(sessionCount)s claves restauradas con éxito", - "Warning: you should only set up key backup from a trusted computer.": "Advertencia: sólo debes configurar la copia de seguridad de claves desde un ordenador de su confianza.", + "Warning: you should only set up key backup from a trusted computer.": "Advertencia: deberías configurar la copia de seguridad de claves solamente usando un ordenador de confianza.", "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Acceda a su historial de mensajes seguros y configure la mensajería segura introduciendo su contraseña de recuperación.", "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options": "Si has olvidado tu contraseña de recuperación puedes usar tu clave de recuperación o configurar nuevas opciones de recuperación ", - "Warning: You should only set up key backup from a trusted computer.": "Advertencia: Sólo debes configurar la copia de seguridad de claves desde un ordenador de su confianza.", + "Warning: You should only set up key backup from a trusted computer.": "Advertencia: Configurar la copia de seguridad de claves solamente usando un ordenador de confianza.", "Access your secure message history and set up secure messaging by entering your recovery key.": "Accede a tu historial de mensajes seguros y configura la mensajería segura introduciendo tu clave de recuperación.", "If you've forgotten your recovery key you can ": "Si has olvidado tu clave de recuperación puedes ", "Resend edit": "Reenviar la edición", @@ -2189,5 +2189,7 @@ "Identity server URL does not appear to be a valid identity server": "La URL del servidor de identidad no parece ser un servidor de identidad válido", "General failure": "Error no especificado", "This homeserver does not support login using email address.": "Este servidor doméstico no admite iniciar sesión con una dirección de correo electrónico.", - "This account has been deactivated.": "Esta cuenta ha sido desactivada." + "This account has been deactivated.": "Esta cuenta ha sido desactivada.", + "Room name or address": "Nombre o dirección de la sala", + "Address (optional)": "Dirección (opcional)" } From fdeba1353bdecc408851cf753a3d44882a622cc3 Mon Sep 17 00:00:00 2001 From: Arnaud Castellanos Galea Date: Wed, 17 Jun 2020 13:49:46 +0000 Subject: [PATCH 078/194] Translated using Weblate (Spanish) Currently translated at 89.7% (2051 of 2287 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/es/ --- src/i18n/strings/es.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index 7951a7844e..a2fde8ef52 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -1179,7 +1179,7 @@ "Use a longer keyboard pattern with more turns": "Usa un patrón de tecleo más largo y con más vueltas", "Enable Community Filter Panel": "Habilitar el Panel de Filtro de Comunidad", "Verify this user by confirming the following emoji appear on their screen.": "Verifica este usuario confirmando que los siguientes emojis aparecen en su pantalla.", - "Your Riot is misconfigured": "Riot tiene un error de configuración", + "Your Riot is misconfigured": "Tu Riot está mal configurado", "Whether or not you're logged in (we don't record your username)": "Hayas o no iniciado sesión (no guardamos tu nombre de usuario)", "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Uses o no los 'breadcrumbs' (iconos sobre la lista de salas)", "A conference call could not be started because the integrations server is not available": "No se pudo iniciar la conferencia porque el servidor de integraciones no está disponible", From 43831b62af1c9095efb2c96d1d535de6b669c314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20V=C3=A1squez?= Date: Wed, 17 Jun 2020 13:49:55 +0000 Subject: [PATCH 079/194] Translated using Weblate (Spanish) Currently translated at 89.7% (2051 of 2287 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/es/ --- src/i18n/strings/es.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index a2fde8ef52..e66b81cd07 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -977,10 +977,10 @@ "User %(user_id)s does not exist": "El usuario %(user_id)s no existe", "User %(user_id)s may or may not exist": "El usuario %(user_id)s podría o no existir", "Unknown server error": "Error desconocido del servidor", - "Use a few words, avoid common phrases": "Usa unas pocas palabras, evita frases comunes", + "Use a few words, avoid common phrases": "Usa algunas palabras, evita frases comunes", "No need for symbols, digits, or uppercase letters": "No hacen falta símbolos, números o letrás en mayúscula", "Avoid repeated words and characters": "Evita repetir palabras y letras", - "Avoid sequences": "Evita frases", + "Avoid sequences": "Evita secuencias", "Avoid recent years": "Evita años recientes", "Avoid years that are associated with you": "Evita años que estén asociados contigo", "Avoid dates and years that are associated with you": "Evita fechas y años que están asociados contigo", From 9b2bd8c9cd5f366ca26dd3543e51b2218ec8d8b7 Mon Sep 17 00:00:00 2001 From: pebles Date: Wed, 17 Jun 2020 13:50:07 +0000 Subject: [PATCH 080/194] Translated using Weblate (Spanish) Currently translated at 89.7% (2051 of 2287 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/es/ --- src/i18n/strings/es.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index e66b81cd07..8004e5cd88 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -1176,7 +1176,7 @@ "Incoming Verification Request": "Petición de verificación entrante", "%(senderDisplayName)s changed the join rule to %(rule)s": "%(senderDisplayName)s cambió la regla para unirse a %(rule)s", "%(senderDisplayName)s changed guest access to %(rule)s": "%(senderDisplayName)s cambió el acceso para invitados a %(rule)s", - "Use a longer keyboard pattern with more turns": "Usa un patrón de tecleo más largo y con más vueltas", + "Use a longer keyboard pattern with more turns": "Usa un patrón de tecleo largo con más vueltas", "Enable Community Filter Panel": "Habilitar el Panel de Filtro de Comunidad", "Verify this user by confirming the following emoji appear on their screen.": "Verifica este usuario confirmando que los siguientes emojis aparecen en su pantalla.", "Your Riot is misconfigured": "Tu Riot está mal configurado", From 5f2d92c607b0557b26ee19c232cd9f633865749e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 17 Jun 2020 08:21:08 -0600 Subject: [PATCH 081/194] Make the room list labs setting reload on change Should fix confusing signals sent by having the room list visible but non-functional. --- src/i18n/strings/en_EN.json | 2 +- src/settings/Settings.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 396c3f9111..4a2c882c54 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -432,7 +432,7 @@ "Render simple counters in room header": "Render simple counters in room header", "Multiple integration managers": "Multiple integration managers", "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", - "Use the improved room list (in development - refresh to apply changes)": "Use the improved room list (in development - refresh to apply changes)", + "Use the improved room list (in development - will refresh to apply changes)": "Use the improved room list (in development - will refresh to apply changes)", "Support adding custom themes": "Support adding custom themes", "Use IRC layout": "Use IRC layout", "Show info about bridges in room settings": "Show info about bridges in room settings", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index fad932fa4b..225af15ec8 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -140,9 +140,10 @@ export const SETTINGS = { }, "feature_new_room_list": { isFeature: true, - displayName: _td("Use the improved room list (in development - refresh to apply changes)"), + displayName: _td("Use the improved room list (in development - will refresh to apply changes)"), supportedLevels: LEVELS_FEATURE, default: false, + controller: new ReloadOnChangeController(), }, "feature_custom_themes": { isFeature: true, From 0ffb36ac804581ec93bdb47ac8e15203595e10e1 Mon Sep 17 00:00:00 2001 From: XoseM Date: Wed, 17 Jun 2020 13:54:38 +0000 Subject: [PATCH 082/194] Translated using Weblate (Galician) Currently translated at 71.7% (1640 of 2287 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/gl/ --- src/i18n/strings/gl.json | 56 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 51b1f7fa25..80440a5b43 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -789,7 +789,7 @@ "Failed to set Direct Message status of room": "Fallo ao establecer o estado Mensaxe Directa da sala", "Monday": "Luns", "Remove from Directory": "Eliminar do directorio", - "Enable them now": "Activalos agora", + "Enable them now": "Activalas agora", "Toolbox": "Ferramentas", "Collecting logs": "Obtendo rexistros", "You must specify an event type!": "Debe indicar un tipo de evento!", @@ -1660,5 +1660,57 @@ "Everyone in this room is verified": "Todas nesta sala están verificadas", "Edit message": "Editar mensaxe", "Mod": "Mod", - "Your key share request has been sent - please check your other sessions for key share requests.": "Enviouse a solicitude de compartir chave - comproba as túas outras sesións para solicitudes de compartir chave." + "Your key share request has been sent - please check your other sessions for key share requests.": "Enviouse a solicitude de compartir chave - comproba as túas outras sesións para solicitudes de compartir chave.", + "Light": "Claro", + "Dark": "Escuro", + "Customise your appearance": "Personaliza o aspecto", + "Appearance Settings only affect this Riot session.": "Os axustes da aparencia só lle afectan a esta sesión Riot.", + "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "As solicitudes de compartir Chave envíanse ás outras túas sesións abertas. Se rexeitaches ou obviaches a solicitude nas outras sesións, preme aquí para voltar a facer a solicitude.", + "If your other sessions do not have the key for this message you will not be able to decrypt them.": "Se as túas outras sesións non teñen a chave para esta mensaxe non poderás descifrala.", + "Re-request encryption keys from your other sessions.": "Volta a solicitar chaves de cifrado desde as outras sesións.", + "This message cannot be decrypted": "Esta mensaxe non pode descifrarse", + "Encrypted by an unverified session": "Cifrada por unha sesión non verificada", + "Unencrypted": "Non cifrada", + "Encrypted by a deleted session": "Cifrada por unha sesión eliminada", + "Invite only": "Só por convite", + "Scroll to most recent messages": "Ir ás mensaxes máis recentes", + "Close preview": "Pechar vista previa", + "Emoji picker": "Selector Emoticona", + "Send a reply…": "Responder…", + "Send a message…": "Enviar mensaxe…", + "The conversation continues here.": "A conversa continúa aquí.", + "This room has been replaced and is no longer active.": "Esta sala foi substituída e xa non está activa.", + "Bold": "Resaltado", + "Italics": "Cursiva", + "Code block": "Bloque de código", + "No recently visited rooms": "Sen salas recentes visitadas", + "People": "Persoas", + "Joining room …": "Uníndote a sala…", + "Loading …": "Cargando…", + "Rejecting invite …": "Rexeitando convite…", + "Loading room preview": "Cargando vista previa", + "You were kicked from %(roomName)s by %(memberName)s": "Foches expulsada de %(roomName)s por %(memberName)s", + "Reason: %(reason)s": "Razón: %(reason)s", + "Forget this room": "Esquecer sala", + "You were banned from %(roomName)s by %(memberName)s": "Foches bloqueada en %(roomName)s por %(memberName)s", + "Something went wrong with your invite to %(roomName)s": "Algo fallou co teu convite para %(roomName)s", + "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to a room admin.": "Un erro (%(errcode)s) foi devolto ao intentar validar o convite. Podes intentar enviarlle esta información a administración da sala.", + "This invite to %(roomName)s was sent to %(email)s which is not associated with your account": "Este convite para %(roomName)s foi enviado a %(email)s que non está asociado coa túa conta", + "Link this email with your account in Settings to receive invites directly in Riot.": "Liga este email coa túa conta nos Axustes para recibir convites directamente en Riot.", + "This invite to %(roomName)s was sent to %(email)s": "Este convite para %(roomName)s foi enviado a %(email)s", + "Use an identity server in Settings to receive invites directly in Riot.": "Usa un servidor de identidade nos Axustes para recibir convites directamente en Riot.", + "Share this email in Settings to receive invites directly in Riot.": "Comparte este email en Axustes para recibir convites directamente en Riot.", + "Do you want to chat with %(user)s?": "Desexas conversar con %(user)s?", + " wants to chat": " quere conversar", + "Start chatting": "Comeza a conversa", + " invited you": " convidoute", + "Reject & Ignore user": "Rexeitar e Ignorar usuaria", + "This room doesn't exist. Are you sure you're at the right place?": "Esta sala non existe. ¿Tes a certeza de estar no lugar correcto?", + "Try again later, or ask a room admin to check if you have access.": "Inténtao máis tarde, ou pídelle á administración da instancia que comprobe se tes acceso.", + "%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please submit a bug report.": "%(errcode)s foi devolto ao intentar acceder a sala. Se cres que esta mensaxe non é correcta, por favor envía un informe de fallo.", + "Never lose encrypted messages": "Non perdas nunca acceso ás mensaxes cifradas", + "Messages in this room are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "As mensaxes nesta sala están aseguradas con cifrado extremo-a-extremo. Só ti e o correspondente(s) tedes as chaves para ler as mensaxes.", + "Securely back up your keys to avoid losing them. Learn more.": "Fai unha copia das chaves para evitar perdelas. Saber máis.", + "Not now": "Agora non", + "Don't ask me again": "Non preguntarme outra vez" } From 00a6277cad80d1a33f8d408f3f79404e7baa3d0d Mon Sep 17 00:00:00 2001 From: XoseM Date: Wed, 17 Jun 2020 14:24:48 +0000 Subject: [PATCH 083/194] Translated using Weblate (Galician) Currently translated at 71.8% (1643 of 2287 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/gl/ --- src/i18n/strings/gl.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 80440a5b43..1b7048e169 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -1712,5 +1712,8 @@ "Messages in this room are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "As mensaxes nesta sala están aseguradas con cifrado extremo-a-extremo. Só ti e o correspondente(s) tedes as chaves para ler as mensaxes.", "Securely back up your keys to avoid losing them. Learn more.": "Fai unha copia das chaves para evitar perdelas. Saber máis.", "Not now": "Agora non", - "Don't ask me again": "Non preguntarme outra vez" + "Don't ask me again": "Non preguntarme outra vez", + "Sort by": "Orde por", + "Activity": "Actividade", + "A-Z": "A-Z" } From 63447413ca36e9bb98e71da2df9319b3cb5eae35 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 17 Jun 2020 08:28:22 -0600 Subject: [PATCH 084/194] Replace class block with reference to class --- res/css/views/rooms/_RoomSublist2.scss | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index c725b02f84..330a0729a7 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -74,11 +74,9 @@ limitations under the License. bottom: 0; } - // We don't have this style because the top is dependent on the room list header's + // We don't have a top style because the top is dependent on the room list header's // height, and is therefore calculated in JS. - //&.mx_RoomSublist2_headerContainer_stickyTop { - // top: 0; - //} + // The class, mx_RoomSublist2_headerContainer_stickyTop, is applied though. } // Sticky Headers End From 5c3aaf46c1c844d786d19b4b96c6b9da7a57c4ab Mon Sep 17 00:00:00 2001 From: XoseM Date: Wed, 17 Jun 2020 14:25:15 +0000 Subject: [PATCH 085/194] Translated using Weblate (Galician) Currently translated at 72.3% (1654 of 2287 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/gl/ --- src/i18n/strings/gl.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 1b7048e169..f1beaa48d0 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -1715,5 +1715,18 @@ "Don't ask me again": "Non preguntarme outra vez", "Sort by": "Orde por", "Activity": "Actividade", - "A-Z": "A-Z" + "A-Z": "A-Z", + "Unread rooms": "", + "Always show first": "Mostrar sempre primeiro", + "Show": "Mostrar", + "Message preview": "Vista previa da mensaxe", + "List options": "Opcións da listaxe", + "Add room": "Engadir sala", + "Show %(count)s more|other": "Mostrar %(count)s máis", + "Show %(count)s more|one": "Mostrar %(count)s máis", + "%(count)s unread messages including mentions.|other": "%(count)s mensaxes non lidas incluíndo mencións.", + "%(count)s unread messages including mentions.|one": "1 mención non lida.", + "%(count)s unread messages.|other": "%(count)s mensaxe non lidas.", + "%(count)s unread messages.|one": "1 mensaxe non lida.", + "Unread mentions.": "Mencións non lidas." } From c00d0af986ccc5bda31b58db2199d87a653fd20e Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Wed, 17 Jun 2020 14:35:38 +0000 Subject: [PATCH 086/194] Translated using Weblate (Albanian) Currently translated at 99.9% (2284 of 2287 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index ed66bbb362..1f8f2ba48c 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -2496,5 +2496,26 @@ "Show %(count)s more|other": "Shfaq %(count)s të tjera", "Show %(count)s more|one": "Shfaq %(count)s tjetër", "Leave Room": "Dil Nga Dhoma", - "Room options": "Mundësi dhome" + "Room options": "Mundësi dhome", + "Light": "E çelët", + "Dark": "E errët", + "Use the improved room list (in development - will refresh to apply changes)": "Përdorni listën e përmirësuar të dhomave (në zhvillim - që të aplikohen ndryshimet, duhet rifreskuar)", + "Customise your appearance": "Përshtatni dukjen tuaj", + "Appearance Settings only affect this Riot session.": "Rregullimet e Dukjes prekin vetëm këtë sesion Riot.", + "Activity": "Veprimtari", + "A-Z": "A-Z", + "Recovery Key": "Kyç Rimarrjesh", + "This isn't the recovery key for your account": "Ky s’është kyçi i rimarrjeve për llogarinë tuaj", + "This isn't a valid recovery key": "Ky s’është kyç rimarrjesh i vlefshëm", + "Looks good!": "Mirë duket!", + "Use Recovery Key or Passphrase": "Përdorni Kyç ose Frazëkalim Rimarrjesh", + "Use Recovery Key": "Përdorni Kyç Rimarrjesh", + "Enter your Recovery Key or enter a Recovery Passphrase to continue.": "Që të vazhdohet, jepni Kyçin tuaj të Rimarrjeve ose jepni një Frazëkalim Rimarrjesh.", + "Enter your Recovery Key to continue.": "Që të vazhdohet, jepni Kyçin tuaj të Rimarrjeve.", + "Upgrade your Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login you'll need it to unlock your data.": "Që të depozitoni kyçe & të fshehta fshehtëzimi me të dhënat e llogarisë tuaj, përmirësoni Kyçin tuaj të Rimarrjeve. Nëse humbni këto kredenciale hyrjesh, do t’ju duhet të shkyçni të dhënat tuaja.", + "Store your Recovery Key somewhere safe, it can be used to unlock your encrypted messages & data.": "Ruajeni diku të parrezik Kyçin tuaj të Rimarrjeve, mund të përdoret për të shkyçur mesazhet & të dhënat tuaja të fshehtëzuara.", + "Create a Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login you’ll need it to unlock your data.": "Krijoni një Kyç Rimarrjesh që të depozitoni kyçe & të fshehta fshehtëzimi me të dhënat e llogarisë tuaj. Nëse humbni këto kredenciale, do t’ju duhet të shkyçni të dhënat tuaja.", + "Create a Recovery Key": "Krijoni një Kyç Rimarrjesh", + "Upgrade your Recovery Key": "Përmirësoni Kyçin tuaj të Rimarrjeve", + "Store your Recovery Key": "Depozitoni Kyçin tuaj të Rimarrjeve" } From 52ae812ef95cc7305358d5f90be253f7e9c98410 Mon Sep 17 00:00:00 2001 From: XoseM Date: Wed, 17 Jun 2020 14:28:53 +0000 Subject: [PATCH 087/194] Translated using Weblate (Galician) Currently translated at 74.6% (1706 of 2287 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/gl/ --- src/i18n/strings/gl.json | 54 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index f1beaa48d0..7cbc115085 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -1728,5 +1728,57 @@ "%(count)s unread messages including mentions.|one": "1 mención non lida.", "%(count)s unread messages.|other": "%(count)s mensaxe non lidas.", "%(count)s unread messages.|one": "1 mensaxe non lida.", - "Unread mentions.": "Mencións non lidas." + "Unread mentions.": "Mencións non lidas.", + "Unread messages.": "Mensaxes non lidas.", + "Leave Room": "Deixar a Sala", + "Room options": "Opcións da Sala", + "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Ao actualizar a sala pecharás a instancia actual da sala e crearás unha versión mellorada co mesmo nome.", + "This room has already been upgraded.": "Esta sala xa foi actualizada.", + "This room is running room version , which this homeserver has marked as unstable.": "A sala está usando a versión , que este servidor considera como non estable.", + "Only room administrators will see this warning": "Só a administración da sala pode ver este aviso", + "Unknown Command": "Comando descoñecido", + "Unrecognised command: %(commandText)s": "Comando non recoñecido: %(commandText)s", + "Hint: Begin your message with // to start it with a slash.": "Truco: Comeza a mensaxe con // para comezar cun trazo.", + "Send as message": "Enviar como mensaxe", + "Failed to connect to integration manager": "Fallou a conexión co xestor de integracións", + "Add some now": "Engade algún agora", + "Failed to revoke invite": "Fallo ao revogar o convite", + "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "Non se revogou o convite. O servidor podería estar experimentando un problema temporal ou non tes permisos suficientes para revogar o convite.", + "Revoke invite": "Revogar convite", + "Invited by %(sender)s": "Convidada por %(sender)s", + "Mark all as read": "Marcar todo como lido", + "Error updating main address": "Fallo ao actualizar o enderezo principal", + "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "Algo fallou ao actualizar o enderezo principal da sala. Podería non estar autorizado polo servidor ou ser un fallo temporal.", + "Error creating address": "Fallo ao crear o enderezo", + "There was an error creating that address. It may not be allowed by the server or a temporary failure occurred.": "Algo fallou ao crear ese enderezo. Podería non estar autorizado polo servidor ou ser un fallo temporal.", + "You don't have permission to delete the address.": "Non tes permiso para eliminar o enderezo.", + "There was an error removing that address. It may no longer exist or a temporary error occurred.": "Houbo un erro ao eliminar o enderezo. Podería non existir ou ser un fallo temporal.", + "Error removing address": "Erro ao eliminar o enderezo", + "Main address": "Enderezo principal", + "Local address": "Enderezo local", + "Published Addresses": "Enderezos publicados", + "Other published addresses:": "Outros enderezos publicados:", + "No other published addresses yet, add one below": "Aínda non hai outros enderezos publicados, engade un embaixo", + "New published address (e.g. #alias:server)": "Novo enderezo publicado (ex. #alias:servidor)", + "Local Addresses": "Enderezos locais", + "Room Name": "Nome da sala", + "Room Topic": "Asunto da sala", + "Room avatar": "Avatar da sala", + "Waiting for you to accept on your other session…": "Agardando a que aceptes na túa outra sesión…", + "Waiting for %(displayName)s to accept…": "Agardando a que %(displayName)s acepte…", + "Accepting…": "Aceptando…", + "Start Verification": "Comezar a Verificación", + "Your messages are secured and only you and the recipient have the unique keys to unlock them.": "As túas mensaxes están seguras e só ti e o correspondente tedes as únicas chaves que as desbloquean.", + "In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.": "Nas salas cifradas, as túas mensaxes están seguras e só ti e o correspondente tedes as únicas chaves que as desbloquean.", + "Verify User": "Verificar Usuaria", + "For extra security, verify this user by checking a one-time code on both of your devices.": "Para maior seguridade, verifica esta usuaria comprobando o código temporal en dous dos teus dispositivos.", + "Your messages are not secure": "As túas mensaxes non están aseguradas", + "One of the following may be compromised:": "Un dos seguintes podería estar comprometido:", + "Your homeserver": "O teu servidor", + "The homeserver the user you’re verifying is connected to": "O servidor ao que a usuaria que estás a verificar está conectada", + "Yours, or the other users’ internet connection": "A túa, ou a conexión a internet da outra usuaria", + "Yours, or the other users’ session": "A túa, ou a sesión da outra usuaria", + "Trusted": "Confiable", + "Not trusted": "Non confiable", + "%(count)s verified sessions|other": "%(count)s sesións verificadas" } From 9f9f24c6249913357ae92e85df11bd1faaf35965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 17 Jun 2020 17:12:13 +0200 Subject: [PATCH 088/194] BaseEventIndexManager: Add support to read/write user versions. --- src/indexing/BaseEventIndexManager.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/indexing/BaseEventIndexManager.ts b/src/indexing/BaseEventIndexManager.ts index c40d1300ea..c59ed9b20e 100644 --- a/src/indexing/BaseEventIndexManager.ts +++ b/src/indexing/BaseEventIndexManager.ts @@ -144,6 +144,29 @@ export default abstract class BaseEventIndexManager { throw new Error("Unimplemented"); } + + /** + * Get the user version of the database. + * @return {Promise} A promise that will resolve to the user stored + * version number. + */ + async getUserVersion(): Promise { + throw new Error("Unimplemented"); + } + + /** + * Set the user stored version to the given version number. + * + * @param {number} version The new version that should be stored in the + * database. + * + * @return {Promise} A promise that will resolve once the new version + * is stored. + */ + async setUserVersion(version: number): Promise { + throw new Error("Unimplemented"); + } + /** * Commit the previously queued up events to the index. * From 2aa00cbf4142b92146f63850e9d62280fa33b4b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 17 Jun 2020 17:13:25 +0200 Subject: [PATCH 089/194] EventIndex: Bump our user version and delete the db if it's an old db. --- src/indexing/EventIndex.js | 3 --- src/indexing/EventIndexPeg.js | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index d4e8ab0117..ca974dd4fc 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -42,9 +42,6 @@ export default class EventIndex extends EventEmitter { async init() { const indexManager = PlatformPeg.get().getEventIndexingManager(); - await indexManager.initEventIndex(); - console.log("EventIndex: Successfully initialized the event index"); - this.crawlerCheckpoints = await indexManager.loadCheckpoints(); console.log("EventIndex: Loaded checkpoints", this.crawlerCheckpoints); diff --git a/src/indexing/EventIndexPeg.js b/src/indexing/EventIndexPeg.js index ae4c14dafd..20e05f985d 100644 --- a/src/indexing/EventIndexPeg.js +++ b/src/indexing/EventIndexPeg.js @@ -23,6 +23,8 @@ import PlatformPeg from "../PlatformPeg"; import EventIndex from "../indexing/EventIndex"; import SettingsStore, {SettingLevel} from '../settings/SettingsStore'; +const INDEX_VERSION = 1; + class EventIndexPeg { constructor() { this.index = null; @@ -66,8 +68,25 @@ class EventIndexPeg { */ async initEventIndex() { const index = new EventIndex(); + const indexManager = PlatformPeg.get().getEventIndexingManager(); try { + await indexManager.initEventIndex(); + + const userVersion = await indexManager.getUserVersion(); + const eventIndexIsEmpty = await indexManager.isEventIndexEmpty(); + + if (eventIndexIsEmpty) { + await indexManager.setUserVersion(INDEX_VERSION); + } else if (userVersion === 0 && !eventIndexIsEmpty) { + await indexManager.closeEventIndex(); + await this.deleteEventIndex(); + + await indexManager.initEventIndex(); + await indexManager.setUserVersion(INDEX_VERSION); + } + + console.log("EventIndex: Successfully initialized the event index"); await index.init(); } catch (e) { console.log("EventIndex: Error initializing the event index", e); From ff98242d144158f3bf14d8c44e4dfc8c10245061 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 17 Jun 2020 16:31:42 +0100 Subject: [PATCH 090/194] clean up and fix the isMasterRuleEnabled logic --- .../controllers/NotificationControllers.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/settings/controllers/NotificationControllers.js b/src/settings/controllers/NotificationControllers.js index f74dc22e84..e38a5bded1 100644 --- a/src/settings/controllers/NotificationControllers.js +++ b/src/settings/controllers/NotificationControllers.js @@ -20,18 +20,20 @@ import {MatrixClientPeg} from '../../MatrixClientPeg'; // XXX: This feels wrong. import {PushProcessor} from "matrix-js-sdk/src/pushprocessor"; -function isMasterRuleEnabled() { +// .m.rule.master being enabled means all events match that push rule +// default action on this rule is dont_notify, but it could be something else +function isPushNotifyDisabled() { // Return the value of the master push rule as a default const processor = new PushProcessor(MatrixClientPeg.get()); const masterRule = processor.getPushRuleById(".m.rule.master"); if (!masterRule) { console.warn("No master push rule! Notifications are disabled for this user."); - return false; + return true; } - // Why enabled == false means "enabled" is beyond me. - return !masterRule.enabled; + // If the rule is enabled then check it does not notify on everything + return masterRule.enabled && !masterRule.actions.includes("notify"); } function getNotifier() { @@ -45,7 +47,7 @@ export class NotificationsEnabledController extends SettingController { if (!getNotifier().isPossible()) return false; if (calculatedValue === null || calculatedAtLevel === "default") { - return !isMasterRuleEnabled(); + return !isPushNotifyDisabled(); } return calculatedValue; @@ -63,7 +65,7 @@ export class NotificationBodyEnabledController extends SettingController { if (!getNotifier().isPossible()) return false; if (calculatedValue === null) { - return !isMasterRuleEnabled(); + return !isPushNotifyDisabled(); } return calculatedValue; From 28a9491dd65d7a10fe6a49749a14aa74255b395b Mon Sep 17 00:00:00 2001 From: random Date: Wed, 17 Jun 2020 15:30:22 +0000 Subject: [PATCH 091/194] Translated using Weblate (Italian) Currently translated at 99.9% (2286 of 2287 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/it/ --- src/i18n/strings/it.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 2ffe6dfef2..89039ec0c5 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -2512,5 +2512,14 @@ "This isn't a valid recovery key": "Questa non è una chiave di ripristino valida", "Looks good!": "Sembra giusta!", "Use Recovery Key or Passphrase": "Usa la chiave o password di recupero", - "Use Recovery Key": "Usa chiave di recupero" + "Use Recovery Key": "Usa chiave di recupero", + "Use the improved room list (in development - will refresh to apply changes)": "Usa l'elenco di stanze migliorato (in sviluppo - verrà ricaricato per applicare le modifiche)", + "Enter your Recovery Key or enter a Recovery Passphrase to continue.": "Inserisci la tua chiave di recupero o una password di recupero per continuare.", + "Enter your Recovery Key to continue.": "Inserisci la tua chiave di recupero per continuare.", + "Upgrade your Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login you'll need it to unlock your data.": "Aggiorna la tua chiave di recupero per memorizzare le chiavi di cifratura e i segreti con i dati del tuo account. Se perdi l'accesso a questo login, ti servirà per sbloccare i tuoi dati.", + "Store your Recovery Key somewhere safe, it can be used to unlock your encrypted messages & data.": "Conserva la tua chiave di recupero in un posto sicuro, può essere usata per sbloccare i tuoi messaggi e dati cifrati.", + "Create a Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login you’ll need it to unlock your data.": "Crea un chiave di recupero per memorizzare le chiavi di cifratura e i segreti con i dati del tuo account. Se perdi l'accesso a questo login, ti servirà per sbloccare i tuoi dati.", + "Create a Recovery Key": "Crea una chiave di recupero", + "Upgrade your Recovery Key": "Aggiorna la chiave di recupero", + "Store your Recovery Key": "Salva la chiave di recupero" } From 50efa5210064b48c49d315f180428c9f26bbe135 Mon Sep 17 00:00:00 2001 From: Alexey Murz Korepov Date: Wed, 17 Jun 2020 16:22:50 +0000 Subject: [PATCH 092/194] Translated using Weblate (Russian) Currently translated at 86.8% (1985 of 2287 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ru/ --- src/i18n/strings/ru.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index eaf38d6fe4..43c43a15ae 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -52,7 +52,7 @@ "Invites user with given id to current room": "Приглашает пользователя с заданным ID в текущую комнату", "Sign in with": "Войти с помощью", "Joins room with given alias": "Входит в комнату с заданным псевдонимом", - "Kicks user with given id": "Выкидывает пользователя с заданным ID", + "Kicks user with given id": "Выгоняет пользователя с заданным ID", "Labs": "Лаборатория", "Leave room": "Покинуть комнату", "Logout": "Выйти", @@ -977,7 +977,7 @@ "Render simple counters in room header": "Отображать простые счетчики в заголовке комнаты", "Enable Emoji suggestions while typing": "Включить предложения смайликов при наборе", "Show a placeholder for removed messages": "Показывать плашки вместо удалённых сообщений", - "Show join/leave messages (invites/kicks/bans unaffected)": "Показывать сообщения о входе/выходе (не влияет на приглашения, кики и баны)", + "Show join/leave messages (invites/kicks/bans unaffected)": "Показывать сообщения о входе/выходе (не влияет на приглашения, выгоны и баны)", "Show avatar changes": "Показывать изменения аватара", "Show display name changes": "Показывать изменения отображаемого имени", "Show a reminder to enable Secure Message Recovery in encrypted rooms": "Напоминать включить Безопасное Восстановление Сообщений в зашифрованных комнатах", @@ -1276,7 +1276,7 @@ "Join the conversation with an account": "Присоединиться к разговору с учётной записью", "Sign Up": "Зарегистрироваться", "Sign In": "Войти", - "You were kicked from %(roomName)s by %(memberName)s": "Вы были выгнаны %(memberName)s из %(roomName)s", + "You were kicked from %(roomName)s by %(memberName)s": "Вы были выгнаны из %(roomName)s пользователем %(memberName)s", "Reason: %(reason)s": "Причина: %(reason)s", "Forget this room": "Забыть эту комнату", "Re-join": "Пере-присоединение", From dd23a50a3cccb50bfe4c90d0009f2a5951bbccdb Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 17 Jun 2020 21:40:30 +0100 Subject: [PATCH 093/194] Upgrade matrix-js-sdk to 7.0.0-rc.1 --- package.json | 2 +- yarn.lock | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 5f9b7dde1f..98d7f7a6fa 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "is-ip": "^2.0.0", "linkifyjs": "^2.1.6", "lodash": "^4.17.14", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "7.0.0-rc.1", "minimist": "^1.2.0", "pako": "^1.0.5", "parse5": "^5.1.1", diff --git a/yarn.lock b/yarn.lock index d2d53692b5..2bb99f4602 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5820,9 +5820,10 @@ mathml-tag-names@^2.0.1: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": - version "6.2.2" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/1c194e81637fb07fe6ad67cda33be0d5d4c10115" +matrix-js-sdk@7.0.0-rc.1: + version "7.0.0-rc.1" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-7.0.0-rc.1.tgz#95a258218f5c5ec73ec4be510b28768c35809a0b" + integrity sha512-1znl0d2UxU6Mmimy+pMSQP1lQfsmDb9jxiKV5sfMvTBsLtUE2cTqEBVDNVoOHL4UJ9U4oMLsrBgu3sELkgSJLQ== dependencies: "@babel/runtime" "^7.8.3" another-json "^0.2.0" From 2533c33c54c870689960f4850e91e8b4be2c5e29 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 17 Jun 2020 21:46:33 +0100 Subject: [PATCH 094/194] Prepare changelog for v2.8.0-rc.1 --- CHANGELOG.md | 180 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 089bfa73e0..9d7a73b264 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,183 @@ +Changes in [2.8.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.8.0-rc.1) (2020-06-17) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.7.2...v2.8.0-rc.1) + + * Upgrade to JS SDK 7.0.0-rc.1 + * Fix Styled Checkbox and Radio Button disabled state + [\#4778](https://github.com/matrix-org/matrix-react-sdk/pull/4778) + * clean up and fix the isMasterRuleEnabled logic + [\#4782](https://github.com/matrix-org/matrix-react-sdk/pull/4782) + * Fix case-sensitivity of /me to match rest of slash commands + [\#4763](https://github.com/matrix-org/matrix-react-sdk/pull/4763) + * Add a 'show less' button to the new room list + [\#4765](https://github.com/matrix-org/matrix-react-sdk/pull/4765) + * Update from Weblate + [\#4781](https://github.com/matrix-org/matrix-react-sdk/pull/4781) + * Sticky and collapsing headers for new room list + [\#4758](https://github.com/matrix-org/matrix-react-sdk/pull/4758) + * Make the room list labs setting reload on change + [\#4780](https://github.com/matrix-org/matrix-react-sdk/pull/4780) + * Handle/hide old rooms in the room list + [\#4767](https://github.com/matrix-org/matrix-react-sdk/pull/4767) + * Add some media queries to improve UI on mobile (#3991) + [\#4656](https://github.com/matrix-org/matrix-react-sdk/pull/4656) + * Match fuzzy filtering a bit more reliably in the new room list + [\#4769](https://github.com/matrix-org/matrix-react-sdk/pull/4769) + * Improve Field ts definitions some more + [\#4777](https://github.com/matrix-org/matrix-react-sdk/pull/4777) + * Fix alignment of checkboxes in new room list's context menu + [\#4776](https://github.com/matrix-org/matrix-react-sdk/pull/4776) + * Fix Field ts def, fix LocalEchoWrapper and NotificationsEnabledController + [\#4775](https://github.com/matrix-org/matrix-react-sdk/pull/4775) + * Add presence indicators and globes to new room list + [\#4774](https://github.com/matrix-org/matrix-react-sdk/pull/4774) + * Include the sticky room when filtering in the new room list + [\#4772](https://github.com/matrix-org/matrix-react-sdk/pull/4772) + * Add a home button to the new room list menu when available + [\#4771](https://github.com/matrix-org/matrix-react-sdk/pull/4771) + * use group layout for search results + [\#4764](https://github.com/matrix-org/matrix-react-sdk/pull/4764) + * Fix m.id.phone spec compliance + [\#4757](https://github.com/matrix-org/matrix-react-sdk/pull/4757) + * User Info default power levels for ban/kick/redact to 50 as per spec + [\#4759](https://github.com/matrix-org/matrix-react-sdk/pull/4759) + * Match new room list's text search to old room list + [\#4768](https://github.com/matrix-org/matrix-react-sdk/pull/4768) + * Fix ordering of recent rooms in the new room list + [\#4766](https://github.com/matrix-org/matrix-react-sdk/pull/4766) + * Change theme selector to use new styled radio buttons + [\#4731](https://github.com/matrix-org/matrix-react-sdk/pull/4731) + * Use recovery keys over passphrases + [\#4686](https://github.com/matrix-org/matrix-react-sdk/pull/4686) + * Update from Weblate + [\#4760](https://github.com/matrix-org/matrix-react-sdk/pull/4760) + * Initial dark theme support for new room list + [\#4756](https://github.com/matrix-org/matrix-react-sdk/pull/4756) + * Support per-list options and algorithms on the new room list + [\#4754](https://github.com/matrix-org/matrix-react-sdk/pull/4754) + * Send read marker updates immediately after moving visually + [\#4755](https://github.com/matrix-org/matrix-react-sdk/pull/4755) + * Add a minimized view to the new room list + [\#4753](https://github.com/matrix-org/matrix-react-sdk/pull/4753) + * Fix e2e icon alignment in irc-layout + [\#4752](https://github.com/matrix-org/matrix-react-sdk/pull/4752) + * Add some resource leak protection to new room list badges + [\#4750](https://github.com/matrix-org/matrix-react-sdk/pull/4750) + * Fix read-receipt alignment + [\#4747](https://github.com/matrix-org/matrix-react-sdk/pull/4747) + * Show message previews on the new room list tiles + [\#4751](https://github.com/matrix-org/matrix-react-sdk/pull/4751) + * Fix various layout concerns with the new room list + [\#4749](https://github.com/matrix-org/matrix-react-sdk/pull/4749) + * Prioritize text on the clipboard over file + [\#4748](https://github.com/matrix-org/matrix-react-sdk/pull/4748) + * Move Settings flag to ts + [\#4729](https://github.com/matrix-org/matrix-react-sdk/pull/4729) + * Add a context menu to rooms in the new room list + [\#4743](https://github.com/matrix-org/matrix-react-sdk/pull/4743) + * Add hover states and basic context menu to new room list + [\#4742](https://github.com/matrix-org/matrix-react-sdk/pull/4742) + * Update resize handle for new designs in new room list + [\#4741](https://github.com/matrix-org/matrix-react-sdk/pull/4741) + * Improve general stability in the new room list + [\#4740](https://github.com/matrix-org/matrix-react-sdk/pull/4740) + * Reimplement breadcrumbs for new room list + [\#4735](https://github.com/matrix-org/matrix-react-sdk/pull/4735) + * Add styled radio buttons + [\#4744](https://github.com/matrix-org/matrix-react-sdk/pull/4744) + * Hide checkbox tick on dark backgrounds + [\#4730](https://github.com/matrix-org/matrix-react-sdk/pull/4730) + * Make checkboxes a11y friendly + [\#4746](https://github.com/matrix-org/matrix-react-sdk/pull/4746) + * EventIndex: Store and restore the encryption info for encrypted events. + [\#4738](https://github.com/matrix-org/matrix-react-sdk/pull/4738) + * Use IDestroyable instead of IDisposable + [\#4739](https://github.com/matrix-org/matrix-react-sdk/pull/4739) + * Add/improve badge counts in new room list + [\#4734](https://github.com/matrix-org/matrix-react-sdk/pull/4734) + * Convert FormattingUtils to TypeScript and add badge utility function + [\#4732](https://github.com/matrix-org/matrix-react-sdk/pull/4732) + * Add filtering and exploring to the new room list + [\#4736](https://github.com/matrix-org/matrix-react-sdk/pull/4736) + * Support prioritized room list filters + [\#4737](https://github.com/matrix-org/matrix-react-sdk/pull/4737) + * Clean up font scaling appearance + [\#4733](https://github.com/matrix-org/matrix-react-sdk/pull/4733) + * Add user menu to new room list + [\#4722](https://github.com/matrix-org/matrix-react-sdk/pull/4722) + * New room list basic styling and layout + [\#4711](https://github.com/matrix-org/matrix-react-sdk/pull/4711) + * Fix read receipt overlap + [\#4727](https://github.com/matrix-org/matrix-react-sdk/pull/4727) + * Load correct default font size + [\#4726](https://github.com/matrix-org/matrix-react-sdk/pull/4726) + * send state of lowBandwidth in rageshakes + [\#4724](https://github.com/matrix-org/matrix-react-sdk/pull/4724) + * Change internal font size from from 15 to 10 + [\#4725](https://github.com/matrix-org/matrix-react-sdk/pull/4725) + * Upgrade deps + [\#4723](https://github.com/matrix-org/matrix-react-sdk/pull/4723) + * Ensure active Jitsi conference is closed on widget pop-out + [\#4444](https://github.com/matrix-org/matrix-react-sdk/pull/4444) + * Introduce sticky rooms to the new room list + [\#4720](https://github.com/matrix-org/matrix-react-sdk/pull/4720) + * Handle remaining cases for room updates in new room list + [\#4721](https://github.com/matrix-org/matrix-react-sdk/pull/4721) + * Allow searching the emoji picker using other emoji + [\#4719](https://github.com/matrix-org/matrix-react-sdk/pull/4719) + * New room list scrolling and resizing + [\#4697](https://github.com/matrix-org/matrix-react-sdk/pull/4697) + * Don't show FormatBar if composer is empty + [\#4696](https://github.com/matrix-org/matrix-react-sdk/pull/4696) + * Split the left panel into new and old for new room list designs + [\#4687](https://github.com/matrix-org/matrix-react-sdk/pull/4687) + * Fix compact layout regression + [\#4712](https://github.com/matrix-org/matrix-react-sdk/pull/4712) + * fix emoji in safari + [\#4710](https://github.com/matrix-org/matrix-react-sdk/pull/4710) + * Fix not being able to dismiss new login toasts + [\#4709](https://github.com/matrix-org/matrix-react-sdk/pull/4709) + * Fix exceptions from Tooltip + [\#4708](https://github.com/matrix-org/matrix-react-sdk/pull/4708) + * Stop removing variation selector from quick reactions + [\#4707](https://github.com/matrix-org/matrix-react-sdk/pull/4707) + * Tidy up continuation algorithm and make it work for hidden profile changes + [\#4704](https://github.com/matrix-org/matrix-react-sdk/pull/4704) + * Profile settings should never show a disambiguated display name + [\#4699](https://github.com/matrix-org/matrix-react-sdk/pull/4699) + * Prevent (double) 4S bootstrap from RestoreKeyBackupDialog + [\#4701](https://github.com/matrix-org/matrix-react-sdk/pull/4701) + * Stop checkbox styling bleeding through room address selector + [\#4691](https://github.com/matrix-org/matrix-react-sdk/pull/4691) + * Center HeaderButtons + [\#4695](https://github.com/matrix-org/matrix-react-sdk/pull/4695) + * Add .well-known option to control default e2ee behaviour + [\#4605](https://github.com/matrix-org/matrix-react-sdk/pull/4605) + * Add max-width to right and left panels + [\#4692](https://github.com/matrix-org/matrix-react-sdk/pull/4692) + * Fix login loop where the sso flow returns to `#/login` + [\#4685](https://github.com/matrix-org/matrix-react-sdk/pull/4685) + * Don't clear MAU toasts when a successful sync comes in + [\#4690](https://github.com/matrix-org/matrix-react-sdk/pull/4690) + * Add initial filtering support to new room list + [\#4681](https://github.com/matrix-org/matrix-react-sdk/pull/4681) + * Bubble up a decline-to-render of verification events to outside wrapper + [\#4664](https://github.com/matrix-org/matrix-react-sdk/pull/4664) + * upgrade to twemoji 13.0.0 + [\#4672](https://github.com/matrix-org/matrix-react-sdk/pull/4672) + * Apply FocusLock to ImageView to capture Escape handling + [\#4666](https://github.com/matrix-org/matrix-react-sdk/pull/4666) + * Fix the 'complete security' screen + [\#4689](https://github.com/matrix-org/matrix-react-sdk/pull/4689) + * add null-guard for Autocomplete containerRef + [\#4688](https://github.com/matrix-org/matrix-react-sdk/pull/4688) + * Remove legacy codepaths for Unknown Device Error (UDE/UDD) handling + [\#4660](https://github.com/matrix-org/matrix-react-sdk/pull/4660) + * Remove feature_cross_signing + [\#4655](https://github.com/matrix-org/matrix-react-sdk/pull/4655) + * Autocomplete: use scrollIntoView for auto-scroll to fix it + [\#4670](https://github.com/matrix-org/matrix-react-sdk/pull/4670) + Changes in [2.7.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.7.2) (2020-06-16) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.7.1...v2.7.2) From b5aa66015c38748f1bc59a386cff8a7979903e36 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 17 Jun 2020 21:46:34 +0100 Subject: [PATCH 095/194] v2.8.0-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 98d7f7a6fa..06c4c43622 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "2.7.2", + "version": "2.8.0-rc.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 321889f95bf849d0efdae88e48b440a7fa69d450 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 17 Jun 2020 20:01:03 -0600 Subject: [PATCH 096/194] Clear `top` when not sticking headers to the top Fixes https://github.com/vector-im/riot-web/issues/14070 --- src/components/structures/LeftPanel2.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index ba0ba211b7..242d0b46de 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -108,6 +108,7 @@ export default class LeftPanel2 extends React.Component { header.classList.add("mx_RoomSublist2_headerContainer_sticky"); header.classList.add("mx_RoomSublist2_headerContainer_stickyBottom"); header.style.width = `${headerStickyWidth}px`; + header.style.top = "unset"; gotBottom = true; } else if (slRect.top < top) { header.classList.add("mx_RoomSublist2_headerContainer_sticky"); @@ -119,6 +120,7 @@ export default class LeftPanel2 extends React.Component { header.classList.remove("mx_RoomSublist2_headerContainer_stickyTop"); header.classList.remove("mx_RoomSublist2_headerContainer_stickyBottom"); header.style.width = `unset`; + header.style.top = "unset"; } } }; From 6af4d82ce7894c6dc033141dbd73cf78901ff4de Mon Sep 17 00:00:00 2001 From: Mike Pennisi Date: Wed, 17 Jun 2020 23:31:02 -0400 Subject: [PATCH 097/194] Extend QueryMatcher's sorting heuristic Use the order of the input keys as a signal for relative importance of matches. Signed-off-by: Mike Pennisi --- src/autocomplete/QueryMatcher.ts | 32 +++++++++++++---------- test/autocomplete/QueryMatcher-test.js | 35 +++++++++++++++++++++++--- 2 files changed, 50 insertions(+), 17 deletions(-) diff --git a/src/autocomplete/QueryMatcher.ts b/src/autocomplete/QueryMatcher.ts index 2c1899d813..4b8c1141fd 100644 --- a/src/autocomplete/QueryMatcher.ts +++ b/src/autocomplete/QueryMatcher.ts @@ -18,7 +18,6 @@ limitations under the License. import _at from 'lodash/at'; import _flatMap from 'lodash/flatMap'; -import _sortBy from 'lodash/sortBy'; import _uniq from 'lodash/uniq'; function stripDiacritics(str: string): string { @@ -35,8 +34,9 @@ interface IOptions { /** * Simple search matcher that matches any results with the query string anywhere * in the search string. Returns matches in the order the query string appears - * in the search key, earliest first, then in the order the items appeared in - * the source array. + * in the search key, earliest first, then in the order the search key appears + * in the provided array of keys, then in the order the items appeared in the + * source array. * * @param {Object[]} objects Initial list of objects. Equivalent to calling * setObjects() after construction @@ -49,7 +49,7 @@ export default class QueryMatcher { private _options: IOptions; private _keys: IOptions["keys"]; private _funcs: Required["funcs"]>; - private _items: Map; + private _items: Map<{value: string, weight: number}, T[]>; constructor(objects: T[], options: IOptions = { keys: [] }) { this._options = options; @@ -85,9 +85,12 @@ export default class QueryMatcher { keyValues.push(f(object)); } - for (const keyValue of keyValues) { + for (const [index, keyValue] of Object.entries(keyValues)) { if (!keyValue) continue; // skip falsy keyValues - const key = stripDiacritics(keyValue).toLowerCase(); + const key = { + value: stripDiacritics(keyValue).toLowerCase(), + weight: Number(index) + }; if (!this._items.has(key)) { this._items.set(key, []); } @@ -109,7 +112,7 @@ export default class QueryMatcher { // ES6 Map iteration order is defined to be insertion order, so results // here will come out in the order they were put in. for (const key of this._items.keys()) { - let resultKey = key; + let {value: resultKey} = key; if (this._options.shouldMatchWordsOnly) { resultKey = resultKey.replace(/[^\w]/g, ''); } @@ -119,12 +122,15 @@ export default class QueryMatcher { } } - // Sort them by where the query appeared in the search key - // lodash sortBy is a stable sort, so results where the query - // appeared in the same place will retain their order with - // respect to each other. - const sortedResults = _sortBy(results, (candidate) => { - return candidate.index; + // Sort them by where the query appeared in the search key, then by + // where the matched key appeared in the provided array of keys. + const sortedResults = results.slice().sort((a, b) => { + if (a.index < b.index) { + return -1; + } else if (a.index === b.index && a.key.weight < b.key.weight) { + return -1; + } + return 1; }); // Now map the keys to the result objects. Each result object is a list, so diff --git a/test/autocomplete/QueryMatcher-test.js b/test/autocomplete/QueryMatcher-test.js index 03f28eb984..2d0e10563b 100644 --- a/test/autocomplete/QueryMatcher-test.js +++ b/test/autocomplete/QueryMatcher-test.js @@ -81,7 +81,34 @@ describe('QueryMatcher', function() { expect(reverseResults[1].name).toBe('Victoria'); }); - it('Returns results with search string in same place in insertion order', function() { + it('Returns results with search string in same place according to key index', function() { + const objects = [ + { name: "a", first: "hit", second: "miss", third: "miss" }, + { name: "b", first: "miss", second: "hit", third: "miss" }, + { name: "c", first: "miss", second: "miss", third: "hit" }, + ]; + const qm = new QueryMatcher(objects, {keys: ["second", "first", "third"]}); + const results = qm.match('hit'); + + expect(results.length).toBe(3); + expect(results[0].name).toBe('b'); + expect(results[1].name).toBe('a'); + expect(results[2].name).toBe('c'); + + + qm.setObjects(objects.slice().reverse()); + + const reverseResults = qm.match('hit'); + + // should still be in the same order: key index + // takes precedence over input order + expect(reverseResults.length).toBe(3); + expect(reverseResults[0].name).toBe('b'); + expect(reverseResults[1].name).toBe('a'); + expect(reverseResults[2].name).toBe('c'); + }); + + it('Returns results with search string in same place and key in same place in insertion order', function() { const qm = new QueryMatcher(OBJECTS, {keys: ["name"]}); const results = qm.match('Mel'); @@ -132,9 +159,9 @@ describe('QueryMatcher', function() { const results = qm.match('Emma'); expect(results.length).toBe(3); - expect(results[0].name).toBe('Mel B'); - expect(results[1].name).toBe('Mel C'); - expect(results[2].name).toBe('Emma'); + expect(results[0].name).toBe('Emma'); + expect(results[1].name).toBe('Mel B'); + expect(results[2].name).toBe('Mel C'); }); it('Matches words only by default', function() { From 1735da8cb13a110c2caf1b62542b095acb65c76b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 17 Jun 2020 22:04:34 -0600 Subject: [PATCH 098/194] Don't show a 'show less' button when it's impossible to collapse Fixes https://github.com/vector-im/riot-web/issues/14076 --- src/components/views/rooms/RoomSublist2.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index c72e74a1be..94e0e06c00 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -359,7 +359,7 @@ export default class RoomSublist2 extends React.Component { {showMoreText}
          ); - } else if (tiles.length <= nVisible) { + } else if (tiles.length <= nVisible && tiles.length > this.props.layout.minVisibleTiles) { // we have all tiles visible - add a button to show less let showLessText = ( From 245181cf803a9563dfe1842f53dc625578922898 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 17 Jun 2020 22:09:59 -0600 Subject: [PATCH 099/194] Fix show less/more button occluding the list automatically When the user would click 'show more' they would be presented with a 'show less' button that occluded the last room. Similarly, if they resized the list so that all their rooms would be shown and refreshed the page, they would find their last room covered by the button. This changes the handling so that showAllClick() sets the height to numTiles + button padding, and adjusts the height calculations on render to deal with relative tiles. This also removes the conditional padding of the resize handle, as we always occupy the 4px of space. It was leading to rooms getting trimmed slightly by the show N button. --- src/components/views/rooms/RoomSublist2.tsx | 21 ++++++++++++--------- src/stores/room-list/ListLayout.ts | 8 ++++++-- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index c72e74a1be..04bf4a5a50 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -41,6 +41,11 @@ import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorith * warning disappears. * *******************************************************************/ +const SHOW_N_BUTTON_HEIGHT = 32; // As defined by CSS +const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS + +const MAX_PADDING_HEIGHT = SHOW_N_BUTTON_HEIGHT + RESIZE_HANDLE_HEIGHT; + interface IProps { forRooms: boolean; rooms?: Room[]; @@ -105,7 +110,7 @@ export default class RoomSublist2 extends React.Component { }; private onShowAllClick = () => { - this.props.layout.visibleTiles = this.numTiles; + this.props.layout.visibleTiles = this.props.layout.tilesWithPadding(this.numTiles, MAX_PADDING_HEIGHT); this.forceUpdate(); // because the layout doesn't trigger a re-render }; @@ -393,18 +398,16 @@ export default class RoomSublist2 extends React.Component { // goes backwards and can become wildly incorrect (visibleTiles says 18 when there's // only mathematically 7 possible). - const showMoreHeight = 32; // As defined by CSS - const resizeHandleHeight = 4; // As defined by CSS - // The padding is variable though, so figure out what we need padding for. let padding = 0; - if (showNButton) padding += showMoreHeight; - if (handles.length > 0) padding += resizeHandleHeight; + if (showNButton) padding += SHOW_N_BUTTON_HEIGHT; + padding += RESIZE_HANDLE_HEIGHT; // always append the handle height - const minTilesPx = layout.calculateTilesToPixelsMin(tiles.length, layout.minVisibleTiles, padding); + const relativeTiles = layout.tilesWithPadding(tiles.length, padding); + const minTilesPx = layout.calculateTilesToPixelsMin(relativeTiles, layout.minVisibleTiles, padding); const maxTilesPx = layout.tilesToPixelsWithPadding(tiles.length, padding); - const tilesWithoutPadding = Math.min(tiles.length, layout.visibleTiles); - const tilesPx = layout.calculateTilesToPixelsMin(tiles.length, tilesWithoutPadding, padding); + const tilesWithoutPadding = Math.min(relativeTiles, layout.visibleTiles); + const tilesPx = layout.calculateTilesToPixelsMin(relativeTiles, tilesWithoutPadding, padding); content = ( Date: Wed, 17 Jun 2020 22:42:01 -0600 Subject: [PATCH 100/194] Improve room switching in the new room list For https://github.com/vector-im/riot-web/issues/14034 One of the largest issues with room switching was that we'd regenerate the entire list when the sticky room changes, which is obviously detrimental on larger accounts (and even some medium accounts). To work through this, we simply handle the NewRoom and RoomRemoved causes (used by the sticky room handling) as splices rather than in-place updates. Overall this leads to a smoother experience as it means we're doing far less calculations and can even opt out of an update if it isn't required, such as a RoomRemoved cause being fired twice - the second one can result in an update not being required, saving render time. This commit also includes a fix for handling update causes on the sticky room, as the room list loves to print errors when this happens. We don't need to handle any updates because once the sticky room changes it'll get re-added through NewRoom, causing the underlying algorithm to slot it in where needed, effectively handling all the missed updates. --- src/stores/room-list/algorithms/Algorithm.ts | 18 ++-- .../list-ordering/ImportanceAlgorithm.ts | 87 ++++++++++++++----- .../list-ordering/NaturalAlgorithm.ts | 14 ++- .../list-ordering/OrderingAlgorithm.ts | 1 - 4 files changed, 84 insertions(+), 36 deletions(-) diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index a89167095d..9cf42a55fc 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -508,16 +508,14 @@ export class Algorithm extends EventEmitter { return true; } - if (cause === RoomUpdateCause.NewRoom) { - // TODO: Be smarter and insert rather than regen the planet. - await this.setKnownRooms([room, ...this.rooms]); - return true; - } - - if (cause === RoomUpdateCause.RoomRemoved) { - // TODO: Be smarter and splice rather than regen the planet. - await this.setKnownRooms(this.rooms.filter(r => r !== room)); - return true; + // If the update is for a room change which might be the sticky room, prevent it. We + // need to make sure that the causes (NewRoom and RoomRemoved) are still triggered though + // as the sticky room relies on this. + if (cause !== RoomUpdateCause.NewRoom && cause !== RoomUpdateCause.RoomRemoved) { + if (this.stickyRoom === room) { + console.warn(`[RoomListDebug] Received ${cause} update for sticky room ${room.roomId} - ignoring`); + return false; + } } let tags = this.roomIdsToTags[room.roomId]; diff --git a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts index 325aaf19e6..6e09b0f8d3 100644 --- a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts @@ -87,7 +87,7 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) { super(tagId, initialSortingAlgorithm); - console.log("Constructed an ImportanceAlgorithm"); + console.log(`[RoomListDebug] Constructed an ImportanceAlgorithm for ${tagId}`); } // noinspection JSMethodCanBeStatic @@ -151,8 +151,36 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { } } + private async handleSplice(room: Room, cause: RoomUpdateCause): Promise { + if (cause === RoomUpdateCause.NewRoom) { + const category = this.getRoomCategory(room); + this.alterCategoryPositionBy(category, 1, this.indices); + this.cachedOrderedRooms.splice(this.indices[category], 0, room); // splice in the new room (pre-adjusted) + } else if (cause === RoomUpdateCause.RoomRemoved) { + const roomIdx = this.getRoomIndex(room); + if (roomIdx === -1) return false; // no change + const oldCategory = this.getCategoryFromIndices(roomIdx, this.indices); + this.alterCategoryPositionBy(oldCategory, -1, this.indices); + this.cachedOrderedRooms.splice(roomIdx, 1); // remove the room + } else { + throw new Error(`Unhandled splice: ${cause}`); + } + } + + private getRoomIndex(room: Room): number { + let roomIdx = this.cachedOrderedRooms.indexOf(room); + if (roomIdx === -1) { // can only happen if the js-sdk's store goes sideways. + console.warn(`Degrading performance to find missing room in "${this.tagId}": ${room.roomId}`); + roomIdx = this.cachedOrderedRooms.findIndex(r => r.roomId === room.roomId); + } + return roomIdx; + } + public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise { - // TODO: Handle NewRoom and RoomRemoved + if (cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved) { + return this.handleSplice(room, cause); + } + if (cause !== RoomUpdateCause.Timeline && cause !== RoomUpdateCause.ReadReceipt) { throw new Error(`Unsupported update cause: ${cause}`); } @@ -162,11 +190,7 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { return; // Nothing to do here. } - let roomIdx = this.cachedOrderedRooms.indexOf(room); - if (roomIdx === -1) { // can only happen if the js-sdk's store goes sideways. - console.warn(`Degrading performance to find missing room in "${this.tagId}": ${room.roomId}`); - roomIdx = this.cachedOrderedRooms.findIndex(r => r.roomId === room.roomId); - } + const roomIdx = this.getRoomIndex(room); if (roomIdx === -1) { throw new Error(`Room ${room.roomId} has no index in ${this.tagId}`); } @@ -188,12 +212,18 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { // room from the array. } - // The room received an update, so take out the slice and sort it. This should be relatively - // quick because the room is inserted at the top of the category, and most popular sorting - // algorithms will deal with trying to keep the active room at the top/start of the category. - // For the few algorithms that will have to move the thing quite far (alphabetic with a Z room - // for example), the list should already be sorted well enough that it can rip through the - // array and slot the changed room in quickly. + // Sort the category now that we've dumped the room in + await this.sortCategory(category); + + return true; // change made + } + + private async sortCategory(category: Category) { + // This should be relatively quick because the room is usually inserted at the top of the + // category, and most popular sorting algorithms will deal with trying to keep the active + // room at the top/start of the category. For the few algorithms that will have to move the + // thing quite far (alphabetic with a Z room for example), the list should already be sorted + // well enough that it can rip through the array and slot the changed room in quickly. const nextCategoryStartIdx = category === CATEGORY_ORDER[CATEGORY_ORDER.length - 1] ? Number.MAX_SAFE_INTEGER : this.indices[CATEGORY_ORDER[CATEGORY_ORDER.indexOf(category) + 1]]; @@ -202,8 +232,6 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { const unsortedSlice = this.cachedOrderedRooms.splice(startIdx, numSort); const sorted = await sortRoomsWithAlgorithm(unsortedSlice, this.tagId, this.sortingAlgorithm); this.cachedOrderedRooms.splice(startIdx, 0, ...sorted); - - return true; // change made } // noinspection JSMethodCanBeStatic @@ -230,14 +258,29 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { // We also need to update subsequent categories as they'll all shift by nRooms, so we // loop over the order to achieve that. - for (let i = CATEGORY_ORDER.indexOf(fromCategory) + 1; i < CATEGORY_ORDER.length; i++) { - const nextCategory = CATEGORY_ORDER[i]; - indices[nextCategory] -= nRooms; - } + this.alterCategoryPositionBy(fromCategory, -nRooms, indices); + this.alterCategoryPositionBy(toCategory, +nRooms, indices); + } - for (let i = CATEGORY_ORDER.indexOf(toCategory) + 1; i < CATEGORY_ORDER.length; i++) { - const nextCategory = CATEGORY_ORDER[i]; - indices[nextCategory] += nRooms; + private alterCategoryPositionBy(category: Category, n: number, indices: ICategoryIndex) { + // Note: when we alter a category's index, we actually have to modify the ones following + // the target and not the target itself. + + // XXX: If this ever actually gets more than one room passed to it, it'll need more index + // handling. For instance, if 45 rooms are removed from the middle of a 50 room list, the + // index for the categories will be way off. + + const nextOrderIndex = CATEGORY_ORDER.indexOf(category) + 1 + if (n > 0) { + for (let i = nextOrderIndex; i < CATEGORY_ORDER.length; i++) { + const nextCategory = CATEGORY_ORDER[i]; + indices[nextCategory] += Math.abs(n); + } + } else if (n < 0) { + for (let i = nextOrderIndex; i < CATEGORY_ORDER.length; i++) { + const nextCategory = CATEGORY_ORDER[i]; + indices[nextCategory] -= Math.abs(n); + } } // Do a quick check to see if we've completely broken the index diff --git a/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts index cce7372986..96a3f58d2c 100644 --- a/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts @@ -28,7 +28,7 @@ export class NaturalAlgorithm extends OrderingAlgorithm { public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) { super(tagId, initialSortingAlgorithm); - console.log("Constructed a NaturalAlgorithm"); + console.log(`[RoomListDebug] Constructed a NaturalAlgorithm for ${tagId}`); } public async setRooms(rooms: Room[]): Promise { @@ -36,11 +36,19 @@ export class NaturalAlgorithm extends OrderingAlgorithm { } public async handleRoomUpdate(room, cause): Promise { - // TODO: Handle NewRoom and RoomRemoved - if (cause !== RoomUpdateCause.Timeline && cause !== RoomUpdateCause.ReadReceipt) { + const isSplice = cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved; + const isInPlace = cause === RoomUpdateCause.Timeline || cause === RoomUpdateCause.ReadReceipt; + if (!isSplice && !isInPlace) { throw new Error(`Unsupported update cause: ${cause}`); } + if (cause === RoomUpdateCause.NewRoom) { + this.cachedOrderedRooms.push(room); + } else if (cause === RoomUpdateCause.RoomRemoved) { + const idx = this.cachedOrderedRooms.indexOf(room); + if (idx >= 0) this.cachedOrderedRooms.splice(idx, 1); + } + // TODO: Optimize this to avoid useless operations // For example, we can skip updates to alphabetic (sometimes) and manually ordered tags this.cachedOrderedRooms = await sortRoomsWithAlgorithm(this.cachedOrderedRooms, this.tagId, this.sortingAlgorithm); diff --git a/src/stores/room-list/algorithms/list-ordering/OrderingAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/OrderingAlgorithm.ts index 263e8a4cd4..f581e30630 100644 --- a/src/stores/room-list/algorithms/list-ordering/OrderingAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/OrderingAlgorithm.ts @@ -67,6 +67,5 @@ export abstract class OrderingAlgorithm { * @param cause The cause of the update. * @returns True if the update requires the Algorithm to update the presentation layers. */ - // XXX: TODO: We assume this will only ever be a position update and NOT a NewRoom or RemoveRoom change!! public abstract handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise; } From e0a34d9deab9e31e175cf8c668fb99a6f4376566 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 18 Jun 2020 09:35:11 +0100 Subject: [PATCH 101/194] Revert "Use recovery keys over passphrases" --- .../structures/auth/_CompleteSecurity.scss | 4 - .../_CreateSecretStorageDialog.scss | 35 +- src/CrossSigningManager.js | 20 +- .../CreateSecretStorageDialog.js | 464 +++++++++++++----- .../structures/auth/CompleteSecurity.js | 4 - .../structures/auth/SetupEncryptionBody.js | 153 +----- .../keybackup/RestoreKeyBackupDialog.js | 2 +- .../AccessSecretStorageDialog.js | 9 +- .../views/settings/CrossSigningPanel.js | 2 +- src/i18n/strings/en_EN.json | 47 +- src/stores/SetupEncryptionStore.js | 64 +-- test/end-to-end-tests/src/usecases/signup.js | 23 +- 12 files changed, 400 insertions(+), 427 deletions(-) diff --git a/res/css/structures/auth/_CompleteSecurity.scss b/res/css/structures/auth/_CompleteSecurity.scss index b0462db477..f742be70e4 100644 --- a/res/css/structures/auth/_CompleteSecurity.scss +++ b/res/css/structures/auth/_CompleteSecurity.scss @@ -98,7 +98,3 @@ limitations under the License. } } } - -.mx_CompleteSecurity_resetText { - padding-top: 20px; -} diff --git a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss index 9f1d0f4998..63e5a3de09 100644 --- a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss +++ b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss @@ -73,42 +73,33 @@ limitations under the License. margin-left: 20px; } +.mx_CreateSecretStorageDialog_recoveryKeyHeader { + margin-bottom: 1em; +} + .mx_CreateSecretStorageDialog_recoveryKeyContainer { - width: 380px; - margin-left: auto; - margin-right: auto; + display: flex; } .mx_CreateSecretStorageDialog_recoveryKey { - font-weight: bold; - text-align: center; + width: 262px; padding: 20px; color: $info-plinth-fg-color; background-color: $info-plinth-bg-color; - border-radius: 6px; - word-spacing: 1em; - margin-bottom: 20px; + margin-right: 12px; } .mx_CreateSecretStorageDialog_recoveryKeyButtons { + flex: 1; display: flex; - justify-content: space-between; align-items: center; } .mx_CreateSecretStorageDialog_recoveryKeyButtons .mx_AccessibleButton { - width: 160px; - padding-left: 0px; - padding-right: 0px; + margin-right: 10px; +} + +.mx_CreateSecretStorageDialog_recoveryKeyButtons button { + flex: 1; white-space: nowrap; } - -.mx_CreateSecretStorageDialog_continueSpinner { - margin-top: 33px; - text-align: right; -} - -.mx_CreateSecretStorageDialog_continueSpinner img { - width: 20px; - height: 20px; -} diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index d40f820ac0..c37d0f8bf5 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -30,8 +30,6 @@ import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib"; // operation ends. let secretStorageKeys = {}; let secretStorageBeingAccessed = false; -// Stores the 'passphraseOnly' option for the active storage access operation -let passphraseOnlyOption = null; function isCachingAllowed() { return ( @@ -101,7 +99,6 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { const key = await inputToKey(input); return await MatrixClientPeg.get().checkSecretStorageKey(key, info); }, - passphraseOnly: passphraseOnlyOption, }, /* className= */ null, /* isPriorityModal= */ false, @@ -216,27 +213,19 @@ export async function promptForBackupPassphrase() { * * @param {Function} [func] An operation to perform once secret storage has been * bootstrapped. Optional. - * @param {object} [opts] Named options - * @param {bool} [opts.forceReset] Reset secret storage even if it's already set up - * @param {object} [opts.withKeys] Map of key ID to key for SSSS keys that the client - * already has available. If a key is not supplied here, the user will be prompted. - * @param {bool} [opts.passphraseOnly] If true, do not prompt for recovery key or to reset keys + * @param {bool} [forceReset] Reset secret storage even if it's already set up */ -export async function accessSecretStorage( - func = async () => { }, opts = {}, -) { +export async function accessSecretStorage(func = async () => { }, forceReset = false) { const cli = MatrixClientPeg.get(); secretStorageBeingAccessed = true; - passphraseOnlyOption = opts.passphraseOnly; - secretStorageKeys = Object.assign({}, opts.withKeys || {}); try { - if (!await cli.hasSecretStorageKey() || opts.forceReset) { + if (!await cli.hasSecretStorageKey() || forceReset) { // This dialog calls bootstrap itself after guiding the user through // passphrase creation. const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '', import("./async-components/views/dialogs/secretstorage/CreateSecretStorageDialog"), { - force: opts.forceReset, + force: forceReset, }, null, /* priority = */ false, /* static = */ true, ); @@ -274,6 +263,5 @@ export async function accessSecretStorage( if (!isCachingAllowed()) { secretStorageKeys = {}; } - passphraseOnlyOption = null; } } diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 192427d384..d7b79c2cfa 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -20,23 +20,25 @@ import PropTypes from 'prop-types'; import * as sdk from '../../../../index'; import {MatrixClientPeg} from '../../../../MatrixClientPeg'; import FileSaver from 'file-saver'; -import {_t} from '../../../../languageHandler'; +import {_t, _td} from '../../../../languageHandler'; import Modal from '../../../../Modal'; import { promptForBackupPassphrase } from '../../../../CrossSigningManager'; import {copyNode} from "../../../../utils/strings"; import {SSOAuthEntry} from "../../../../components/views/auth/InteractiveAuthEntryComponents"; -import AccessibleButton from "../../../../components/views/elements/AccessibleButton"; -import DialogButtons from "../../../../components/views/elements/DialogButtons"; -import InlineSpinner from "../../../../components/views/elements/InlineSpinner"; - +import PassphraseField from "../../../../components/views/auth/PassphraseField"; const PHASE_LOADING = 0; const PHASE_LOADERROR = 1; const PHASE_MIGRATE = 2; -const PHASE_INTRO = 3; -const PHASE_SHOWKEY = 4; -const PHASE_STORING = 5; -const PHASE_CONFIRM_SKIP = 6; +const PHASE_PASSPHRASE = 3; +const PHASE_PASSPHRASE_CONFIRM = 4; +const PHASE_SHOWKEY = 5; +const PHASE_KEEPITSAFE = 6; +const PHASE_STORING = 7; +const PHASE_DONE = 8; +const PHASE_CONFIRM_SKIP = 9; + +const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. /* * Walks the user through the process of creating a passphrase to guard Secure @@ -63,32 +65,34 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this.state = { phase: PHASE_LOADING, - downloaded: false, + passPhrase: '', + passPhraseValid: false, + passPhraseConfirm: '', copied: false, + downloaded: false, backupInfo: null, - backupInfoFetched: false, - backupInfoFetchError: null, backupSigStatus: null, // does the server offer a UI auth flow with just m.login.password - // for /keys/device_signing/upload? (If we have an account password, we - // assume that it can) + // for /keys/device_signing/upload? canUploadKeysWithPasswordOnly: null, - canUploadKeyCheckInProgress: false, accountPassword: props.accountPassword || "", accountPasswordCorrect: null, - // No toggle for this: if we really don't want one, remove it & just hard code true + // status of the key backup toggle switch useKeyBackup: true, }; - if (props.accountPassword) { - // If we have an account password, we assume we can upload keys with - // just a password (otherwise leave it as null so we poll to check) - this.state.canUploadKeysWithPasswordOnly = true; - } - this._passphraseField = createRef(); - this.loadData(); + this._fetchBackupInfo(); + if (this.state.accountPassword) { + // If we have an account password in memory, let's simplify and + // assume it means password auth is also supported for device + // signing key upload as well. This avoids hitting the server to + // test auth flows, which may be slow under high load. + this.state.canUploadKeysWithPasswordOnly = true; + } else { + this._queryKeyUploadAuth(); + } MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatusChange); } @@ -105,11 +109,13 @@ export default class CreateSecretStorageDialog extends React.PureComponent { MatrixClientPeg.get().isCryptoEnabled() && await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo) ); + const { force } = this.props; + const phase = (backupInfo && !force) ? PHASE_MIGRATE : PHASE_PASSPHRASE; + this.setState({ - backupInfoFetched: true, + phase, backupInfo, backupSigStatus, - backupInfoFetchError: null, }); return { @@ -117,25 +123,20 @@ export default class CreateSecretStorageDialog extends React.PureComponent { backupSigStatus, }; } catch (e) { - this.setState({backupInfoFetchError: e}); + this.setState({phase: PHASE_LOADERROR}); } } async _queryKeyUploadAuth() { try { - this.setState({canUploadKeyCheckInProgress: true}); await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {}); // We should never get here: the server should always require // UI auth to upload device signing keys. If we do, we upload // no keys which would be a no-op. console.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!"); - this.setState({canUploadKeyCheckInProgress: false}); } catch (error) { if (!error.data || !error.data.flows) { console.log("uploadDeviceSigningKeys advertised no flows!"); - this.setState({ - canUploadKeyCheckInProgress: false, - }); return; } const canUploadKeysWithPasswordOnly = error.data.flows.some(f => { @@ -143,18 +144,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent { }); this.setState({ canUploadKeysWithPasswordOnly, - canUploadKeyCheckInProgress: false, }); } } - async _createRecoveryKey() { - this._recoveryKey = await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(); - this.setState({ - phase: PHASE_SHOWKEY, - }); - } - _onKeyBackupStatusChange = () => { if (this.state.phase === PHASE_MIGRATE) this._fetchBackupInfo(); } @@ -163,6 +156,12 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this._recoveryKeyNode = n; } + _onUseKeyBackupChange = (enabled) => { + this.setState({ + useKeyBackup: enabled, + }); + } + _onMigrateFormSubmit = (e) => { e.preventDefault(); if (this.state.backupSigStatus.usable) { @@ -172,15 +171,12 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } } - _onIntroContinueClick = () => { - this._createRecoveryKey(); - } - _onCopyClick = () => { const successful = copyNode(this._recoveryKeyNode); if (successful) { this.setState({ copied: true, + phase: PHASE_KEEPITSAFE, }); } } @@ -190,8 +186,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent { type: 'text/plain;charset=us-ascii', }); FileSaver.saveAs(blob, 'recovery-key.txt'); + this.setState({ downloaded: true, + phase: PHASE_KEEPITSAFE, }); } @@ -247,9 +245,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { _bootstrapSecretStorage = async () => { this.setState({ - // we use LOADING here rather than STORING as STORING still shows the 'show key' - // screen which is not relevant: LOADING is just a generic spinner. - phase: PHASE_LOADING, + phase: PHASE_STORING, error: null, }); @@ -290,7 +286,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent { }, }); } - this.props.onFinished(true); + this.setState({ + phase: PHASE_DONE, + }); } catch (e) { if (this.state.canUploadKeysWithPasswordOnly && e.httpStatus === 401 && e.data.flows) { this.setState({ @@ -309,6 +307,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this.props.onFinished(false); } + _onDone = () => { + this.props.onFinished(true); + } + _restoreBackup = async () => { // It's possible we'll need the backup key later on for bootstrapping, // so let's stash it here, rather than prompting for it twice. @@ -335,41 +337,88 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } } - _onShowKeyContinueClick = () => { - this._bootstrapSecretStorage(); - } - _onLoadRetryClick = () => { - this.loadData(); - } - - async loadData() { this.setState({phase: PHASE_LOADING}); - const proms = []; - - if (!this.state.backupInfoFetched) proms.push(this._fetchBackupInfo()); - if (this.state.canUploadKeysWithPasswordOnly === null) proms.push(this._queryKeyUploadAuth()); - - await Promise.all(proms); - if (this.state.canUploadKeysWithPasswordOnly === null || this.state.backupInfoFetchError) { - this.setState({phase: PHASE_LOADERROR}); - } else if (this.state.backupInfo && !this.props.force) { - this.setState({phase: PHASE_MIGRATE}); - } else { - this.setState({phase: PHASE_INTRO}); - } + this._fetchBackupInfo(); } _onSkipSetupClick = () => { this.setState({phase: PHASE_CONFIRM_SKIP}); } - _onGoBackClick = () => { - if (this.state.backupInfo && !this.props.force) { - this.setState({phase: PHASE_MIGRATE}); - } else { - this.setState({phase: PHASE_INTRO}); + _onSetUpClick = () => { + this.setState({phase: PHASE_PASSPHRASE}); + } + + _onSkipPassPhraseClick = async () => { + this._recoveryKey = + await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(); + this.setState({ + copied: false, + downloaded: false, + phase: PHASE_SHOWKEY, + }); + } + + _onPassPhraseNextClick = async (e) => { + e.preventDefault(); + if (!this._passphraseField.current) return; // unmounting + + await this._passphraseField.current.validate({ allowEmpty: false }); + if (!this._passphraseField.current.state.valid) { + this._passphraseField.current.focus(); + this._passphraseField.current.validate({ allowEmpty: false, focused: true }); + return; } + + this.setState({phase: PHASE_PASSPHRASE_CONFIRM}); + }; + + _onPassPhraseConfirmNextClick = async (e) => { + e.preventDefault(); + + if (this.state.passPhrase !== this.state.passPhraseConfirm) return; + + this._recoveryKey = + await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(this.state.passPhrase); + this.setState({ + copied: false, + downloaded: false, + phase: PHASE_SHOWKEY, + }); + } + + _onSetAgainClick = () => { + this.setState({ + passPhrase: '', + passPhraseValid: false, + passPhraseConfirm: '', + phase: PHASE_PASSPHRASE, + }); + } + + _onKeepItSafeBackClick = () => { + this.setState({ + phase: PHASE_SHOWKEY, + }); + } + + _onPassPhraseValidate = (result) => { + this.setState({ + passPhraseValid: result.valid, + }); + }; + + _onPassPhraseChange = (e) => { + this.setState({ + passPhrase: e.target.value, + }); + } + + _onPassPhraseConfirmChange = (e) => { + this.setState({ + passPhraseConfirm: e.target.value, + }); } _onAccountPasswordChange = (e) => { @@ -384,14 +433,12 @@ export default class CreateSecretStorageDialog extends React.PureComponent { // Once we're confident enough in this (and it's supported enough) we can do // it automatically. // https://github.com/vector-im/riot-web/issues/11696 + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const Field = sdk.getComponent('views.elements.Field'); let authPrompt; let nextCaption = _t("Next"); - if (!this.state.backupSigStatus.usable) { - authPrompt = null; - nextCaption = _t("Upload"); - } else if (this.state.canUploadKeysWithPasswordOnly && !this.props.accountPassword) { + if (this.state.canUploadKeysWithPasswordOnly) { authPrompt =
          {_t("Enter your account password to confirm the upgrade:")}
          ; + } else if (!this.state.backupSigStatus.usable) { + authPrompt =
          +
          {_t("Restore your key backup to upgrade your encryption")}
          +
          ; + nextCaption = _t("Restore"); } else { authPrompt =

          {_t("You'll need to authenticate with the server to confirm the upgrade.")} @@ -411,9 +463,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent { return

          {_t( - "Upgrade your Recovery Key to store encryption keys & secrets " + - "with your account data. If you lose access to this login you'll " + - "need it to unlock your data.", + "Upgrade this session to allow it to verify other sessions, " + + "granting them access to encrypted messages and marking them " + + "as trusted for other users.", )}

          {authPrompt}
          ; } - _renderPhaseShowKey() { - let continueButton; - if (this.state.phase === PHASE_SHOWKEY) { - continueButton = +

          {_t( + "Set a recovery passphrase to secure encrypted information and recover it if you log out. " + + "This should be different to your account password:", + )}

          + +
          + +
          + + + + ; - } else { - continueButton =
          - -
          ; + disabled={!this.state.passPhraseValid} + > + +
          + +
          + {_t("Advanced")} + + {_t("Set up with a recovery key")} + +
          + ; + } + + _renderPhasePassPhraseConfirm() { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + const Field = sdk.getComponent('views.elements.Field'); + + let matchText; + let changeText; + if (this.state.passPhraseConfirm === this.state.passPhrase) { + matchText = _t("That matches!"); + changeText = _t("Use a different passphrase?"); + } else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) { + // only tell them they're wrong if they've actually gone wrong. + // Security concious readers will note that if you left riot-web unattended + // on this screen, this would make it easy for a malicious person to guess + // your passphrase one letter at a time, but they could get this faster by + // just opening the browser's developer tools and reading it. + // Note that not having typed anything at all will not hit this clause and + // fall through so empty box === no hint. + matchText = _t("That doesn't match."); + changeText = _t("Go back to set it again."); } + let passPhraseMatch = null; + if (matchText) { + passPhraseMatch =
          +
          {matchText}
          +
          + + {changeText} + +
          +
          ; + } + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return
          +

          {_t( + "Enter your recovery passphrase a second time to confirm it.", + )}

          +
          + +
          + {passPhraseMatch} +
          +
          + + + +
          ; + } + + _renderPhaseShowKey() { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); return

          {_t( - "Store your Recovery Key somewhere safe, it can be used to unlock your encrypted messages & data.", + "Your recovery key is a safety net - you can use it to restore " + + "access to your encrypted messages if you forget your recovery passphrase.", + )}

          +

          {_t( + "Keep a copy of it somewhere secure, like a password manager or even a safe.", )}

          +
          + {_t("Your recovery key")} +
          {this._recoveryKey.encodedPrivateKey}
          - - {_t("Download")} - - {_t("or")} - {this.state.copied ? _t("Copied!") : _t("Copy")} + {_t("Copy")} + + + {_t("Download")}
          - {continueButton} +
          ; + } + + _renderPhaseKeepItSafe() { + let introText; + if (this.state.copied) { + introText = _t( + "Your recovery key has been copied to your clipboard, paste it to:", + {}, {b: s => {s}}, + ); + } else if (this.state.downloaded) { + introText = _t( + "Your recovery key is in your Downloads folder.", + {}, {b: s => {s}}, + ); + } + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return
          + {introText} +
            +
          • {_t("Print it and store it somewhere safe", {}, {b: s => {s}})}
          • +
          • {_t("Save it on a USB key or backup drive", {}, {b: s => {s}})}
          • +
          • {_t("Copy it to your personal cloud storage", {}, {b: s => {s}})}
          • +
          + + +
          ; } @@ -483,6 +671,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } _renderPhaseLoadError() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return

          {_t("Unable to query secret storage status")}

          @@ -495,44 +684,29 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
          ; } - _renderPhaseIntro() { - let cancelButton; - if (this.props.force) { - // if this is a forced key reset then aborting will just leave the old keys - // in place, and is thereforece just 'cancel' - cancelButton = ; - } else { - // if it's setting up from scratch then aborting leaves the user without - // crypto set up, so they skipping the setup. - cancelButton = ; - } - + _renderPhaseDone() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return

          {_t( - "Create a Recovery Key to store encryption keys & secrets with your account data. " + - "If you lose access to this login you’ll need it to unlock your data.", + "You can now verify your other devices, " + + "and other users to keep your chats safe.", )}

          -
          - - {cancelButton} - -
          +
          ; } _renderPhaseSkipConfirm() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return
          {_t( "Without completing security on this session, it won’t have " + "access to encrypted messages.", )} @@ -542,15 +716,21 @@ export default class CreateSecretStorageDialog extends React.PureComponent { _titleForPhase(phase) { switch (phase) { - case PHASE_INTRO: - return _t('Create a Recovery Key'); case PHASE_MIGRATE: - return _t('Upgrade your Recovery Key'); + return _t('Upgrade your encryption'); + case PHASE_PASSPHRASE: + return _t('Set up encryption'); + case PHASE_PASSPHRASE_CONFIRM: + return _t('Confirm recovery passphrase'); case PHASE_CONFIRM_SKIP: return _t('Are you sure?'); case PHASE_SHOWKEY: + case PHASE_KEEPITSAFE: + return _t('Make a copy of your recovery key'); case PHASE_STORING: - return _t('Store your Recovery Key'); + return _t('Setting up keys'); + case PHASE_DONE: + return _t("You're done!"); default: return ''; } @@ -561,6 +741,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { let content; if (this.state.error) { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); content =

          {_t("Unable to set up secret storage")}

          @@ -579,16 +760,27 @@ export default class CreateSecretStorageDialog extends React.PureComponent { case PHASE_LOADERROR: content = this._renderPhaseLoadError(); break; - case PHASE_INTRO: - content = this._renderPhaseIntro(); - break; case PHASE_MIGRATE: content = this._renderPhaseMigrate(); break; + case PHASE_PASSPHRASE: + content = this._renderPhasePassPhrase(); + break; + case PHASE_PASSPHRASE_CONFIRM: + content = this._renderPhasePassPhraseConfirm(); + break; case PHASE_SHOWKEY: - case PHASE_STORING: content = this._renderPhaseShowKey(); break; + case PHASE_KEEPITSAFE: + content = this._renderPhaseKeepItSafe(); + break; + case PHASE_STORING: + content = this._renderBusyPhase(); + break; + case PHASE_DONE: + content = this._renderPhaseDone(); + break; case PHASE_CONFIRM_SKIP: content = this._renderPhaseSkipConfirm(); break; @@ -605,7 +797,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { onFinished={this.props.onFinished} title={this._titleForPhase(this.state.phase)} headerImage={headerImage} - hasCancel={this.props.hasCancel} + hasCancel={this.props.hasCancel && [PHASE_PASSPHRASE].includes(this.state.phase)} fixedWidth={false} >
          diff --git a/src/components/structures/auth/CompleteSecurity.js b/src/components/structures/auth/CompleteSecurity.js index e38ecd3eac..c73691611d 100644 --- a/src/components/structures/auth/CompleteSecurity.js +++ b/src/components/structures/auth/CompleteSecurity.js @@ -21,7 +21,6 @@ import * as sdk from '../../../index'; import { SetupEncryptionStore, PHASE_INTRO, - PHASE_RECOVERY_KEY, PHASE_BUSY, PHASE_DONE, PHASE_CONFIRM_SKIP, @@ -62,9 +61,6 @@ export default class CompleteSecurity extends React.Component { if (phase === PHASE_INTRO) { icon = ; title = _t("Verify this login"); - } else if (phase === PHASE_RECOVERY_KEY) { - icon = ; - title = _t("Recovery Key"); } else if (phase === PHASE_DONE) { icon = ; title = _t("Session verified"); diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.js index 7886ed26dd..26534c6e02 100644 --- a/src/components/structures/auth/SetupEncryptionBody.js +++ b/src/components/structures/auth/SetupEncryptionBody.js @@ -19,26 +19,15 @@ import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import * as sdk from '../../../index'; -import withValidation from '../../views/elements/Validation'; -import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; import { SetupEncryptionStore, PHASE_INTRO, - PHASE_RECOVERY_KEY, PHASE_BUSY, PHASE_DONE, PHASE_CONFIRM_SKIP, PHASE_FINISHED, } from '../../../stores/SetupEncryptionStore'; -function keyHasPassphrase(keyInfo) { - return ( - keyInfo.passphrase && - keyInfo.passphrase.salt && - keyInfo.passphrase.iterations - ); -} - export default class SetupEncryptionBody extends React.Component { static propTypes = { onFinished: PropTypes.func.isRequired, @@ -56,11 +45,6 @@ export default class SetupEncryptionBody extends React.Component { // Because of the latter, it lives in the state. verificationRequest: store.verificationRequest, backupInfo: store.backupInfo, - recoveryKey: '', - // whether the recovery key is a valid recovery key - recoveryKeyValid: null, - // whether the recovery key is the correct key or not - recoveryKeyCorrect: null, }; } @@ -83,19 +67,9 @@ export default class SetupEncryptionBody extends React.Component { store.stop(); } - _onResetClick = () => { + _onUsePassphraseClick = async () => { const store = SetupEncryptionStore.sharedInstance(); - store.startKeyReset(); - } - - _onUseRecoveryKeyClick = async () => { - const store = SetupEncryptionStore.sharedInstance(); - store.useRecoveryKey(); - } - - _onRecoveryKeyCancelClick() { - const store = SetupEncryptionStore.sharedInstance(); - store.cancelUseRecoveryKey(); + store.usePassPhrase(); } onSkipClick = () => { @@ -118,66 +92,6 @@ export default class SetupEncryptionBody extends React.Component { store.done(); } - _onUsePassphraseClick = () => { - const store = SetupEncryptionStore.sharedInstance(); - store.usePassPhrase(); - } - - _onRecoveryKeyChange = (e) => { - this.setState({recoveryKey: e.target.value}); - } - - _onRecoveryKeyValidate = async (fieldState) => { - const result = await this._validateRecoveryKey(fieldState); - this.setState({recoveryKeyValid: result.valid}); - return result; - } - - _validateRecoveryKey = withValidation({ - rules: [ - { - key: "required", - test: async (state) => { - try { - const decodedKey = decodeRecoveryKey(state.value); - const correct = await MatrixClientPeg.get().checkSecretStorageKey( - decodedKey, SetupEncryptionStore.sharedInstance().keyInfo, - ); - this.setState({ - recoveryKeyValid: true, - recoveryKeyCorrect: correct, - }); - return correct; - } catch (e) { - this.setState({ - recoveryKeyValid: false, - recoveryKeyCorrect: false, - }); - return false; - } - }, - invalid: function() { - if (this.state.recoveryKeyValid) { - return _t("This isn't the recovery key for your account"); - } else { - return _t("This isn't a valid recovery key"); - } - }, - valid: function() { - return _t("Looks good!"); - }, - }, - ], - }) - - _onRecoveryKeyFormSubmit = (e) => { - e.preventDefault(); - if (!this.state.recoveryKeyCorrect) return; - - const store = SetupEncryptionStore.sharedInstance(); - store.setupWithRecoveryKey(decodeRecoveryKey(this.state.recoveryKey)); - } - render() { const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); @@ -194,13 +108,6 @@ export default class SetupEncryptionBody extends React.Component { member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)} />; } else if (phase === PHASE_INTRO) { - const store = SetupEncryptionStore.sharedInstance(); - let recoveryKeyPrompt; - if (keyHasPassphrase(store.keyInfo)) { - recoveryKeyPrompt = _t("Use Recovery Key or Passphrase"); - } else { - recoveryKeyPrompt = _t("Use Recovery Key"); - } return (

          {_t( @@ -224,67 +131,15 @@ export default class SetupEncryptionBody extends React.Component {

          - - {recoveryKeyPrompt} + + {_t("Use Recovery Passphrase or Key")} {_t("Skip")}
          -
          {_t( - "If you've forgotten your recovery key you can " + - "", {}, { - button: sub => - {sub} - , - }, - )}
          ); - } else if (phase === PHASE_RECOVERY_KEY) { - const store = SetupEncryptionStore.sharedInstance(); - let keyPrompt; - if (keyHasPassphrase(store.keyInfo)) { - keyPrompt = _t( - "Enter your Recovery Key or enter a Recovery Passphrase to continue.", {}, - { - a: sub => {sub}, - }, - ); - } else { - keyPrompt = _t("Enter your Recovery Key to continue."); - } - - const Field = sdk.getComponent('elements.Field'); - return
          -

          {keyPrompt}

          -
          - -
          -
          - - {_t("Cancel")} - - - {_t("Continue")} - -
          -
          ; } else if (phase === PHASE_DONE) { let message; if (this.state.backupInfo) { diff --git a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js index 87ba6f7396..dd34dfbbf0 100644 --- a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js +++ b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js @@ -88,7 +88,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { _onResetRecoveryClick = () => { this.props.onFinished(false); - accessSecretStorage(() => {}, {forceReset: true}); + accessSecretStorage(() => {}, /* forceReset = */ true); } _onRecoveryKeyChange = (e) => { diff --git a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js index 43697f8ee7..e2ceadfbb9 100644 --- a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js +++ b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js @@ -32,9 +32,6 @@ export default class AccessSecretStorageDialog extends React.PureComponent { keyInfo: PropTypes.object.isRequired, // Function from one of { passphrase, recoveryKey } -> boolean checkPrivateKey: PropTypes.func.isRequired, - // If true, only prompt for a passphrase and do not offer to restore with - // a recovery key or reset keys. - passphraseOnly: PropTypes.bool, } constructor(props) { @@ -61,7 +58,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent { _onResetRecoveryClick = () => { // Re-enter the access flow, but resetting storage this time around. this.props.onFinished(false); - accessSecretStorage(() => {}, {forceReset: true}); + accessSecretStorage(() => {}, /* forceReset = */ true); } _onRecoveryKeyChange = (e) => { @@ -167,7 +164,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent { primaryDisabled={this.state.passPhrase.length === 0} /> - {this.props.passphraseOnly ? null : _t( + {_t( "If you've forgotten your recovery passphrase you can "+ "use your recovery key or " + "set up new recovery options." @@ -237,7 +234,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent { primaryDisabled={!this.state.recoveryKeyValid} /> - {this.props.passphraseOnly ? null : _t( + {_t( "If you've forgotten your recovery key you can "+ "." , {}, { diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js index f48ee3cd0d..7eb239cbca 100644 --- a/src/components/views/settings/CrossSigningPanel.js +++ b/src/components/views/settings/CrossSigningPanel.js @@ -113,7 +113,7 @@ export default class CrossSigningPanel extends React.PureComponent { _bootstrapSecureSecretStorage = async (forceReset=false) => { this.setState({ error: null }); try { - await accessSecretStorage(() => undefined, {forceReset}); + await accessSecretStorage(() => undefined, forceReset); } catch (e) { this.setState({ error: e }); console.error("Error bootstrapping secret storage", e); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5de33ada55..9a41517664 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2068,7 +2068,6 @@ "Account settings": "Account settings", "Could not load user profile": "Could not load user profile", "Verify this login": "Verify this login", - "Recovery Key": "Recovery Key", "Session verified": "Session verified", "Failed to send email": "Failed to send email", "The email address linked to your account must be entered.": "The email address linked to your account must be entered.", @@ -2122,16 +2121,10 @@ "You can now close this window or log in to your new account.": "You can now close this window or log in to your new account.", "Registration Successful": "Registration Successful", "Create your account": "Create your account", - "This isn't the recovery key for your account": "This isn't the recovery key for your account", - "This isn't a valid recovery key": "This isn't a valid recovery key", - "Looks good!": "Looks good!", - "Use Recovery Key or Passphrase": "Use Recovery Key or Passphrase", - "Use Recovery Key": "Use Recovery Key", "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.", "This requires the latest Riot on your other devices:": "This requires the latest Riot on your other devices:", "or another cross-signing capable Matrix client": "or another cross-signing capable Matrix client", - "Enter your Recovery Key or enter a Recovery Passphrase to continue.": "Enter your Recovery Key or enter a Recovery Passphrase to continue.", - "Enter your Recovery Key to continue.": "Enter your Recovery Key to continue.", + "Use Recovery Passphrase or Key": "Use Recovery Passphrase or Key", "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.", "Your new session is now verified. Other users will see it as trusted.": "Your new session is now verified. Other users will see it as trusted.", "Without completing security on this session, it won’t have access to encrypted messages.": "Without completing security on this session, it won’t have access to encrypted messages.", @@ -2175,43 +2168,47 @@ "Confirm encryption setup": "Confirm encryption setup", "Click the button below to confirm setting up encryption.": "Click the button below to confirm setting up encryption.", "Enter your account password to confirm the upgrade:": "Enter your account password to confirm the upgrade:", + "Restore your key backup to upgrade your encryption": "Restore your key backup to upgrade your encryption", + "Restore": "Restore", "You'll need to authenticate with the server to confirm the upgrade.": "You'll need to authenticate with the server to confirm the upgrade.", - "Upgrade your Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login you'll need it to unlock your data.": "Upgrade your Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login you'll need it to unlock your data.", - "Store your Recovery Key somewhere safe, it can be used to unlock your encrypted messages & data.": "Store your Recovery Key somewhere safe, it can be used to unlock your encrypted messages & data.", - "Download": "Download", - "Copy": "Copy", - "Unable to query secret storage status": "Unable to query secret storage status", - "Retry": "Retry", - "Create a Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login you’ll need it to unlock your data.": "Create a Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login you’ll need it to unlock your data.", - "Create a Recovery Key": "Create a Recovery Key", - "Upgrade your Recovery Key": "Upgrade your Recovery Key", - "Store your Recovery Key": "Store your Recovery Key", - "Unable to set up secret storage": "Unable to set up secret storage", - "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.": "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.", - "For maximum security, this should be different from your account password.": "For maximum security, this should be different from your account password.", + "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.", + "Set a recovery passphrase to secure encrypted information and recover it if you log out. This should be different to your account password:": "Set a recovery passphrase to secure encrypted information and recover it if you log out. This should be different to your account password:", "Enter a recovery passphrase": "Enter a recovery passphrase", "Great! This recovery passphrase looks strong enough.": "Great! This recovery passphrase looks strong enough.", + "Back up encrypted message keys": "Back up encrypted message keys", "Set up with a recovery key": "Set up with a recovery key", "That matches!": "That matches!", "Use a different passphrase?": "Use a different passphrase?", "That doesn't match.": "That doesn't match.", "Go back to set it again.": "Go back to set it again.", - "Please enter your recovery passphrase a second time to confirm.": "Please enter your recovery passphrase a second time to confirm.", - "Repeat your recovery passphrase...": "Repeat your recovery passphrase...", + "Enter your recovery passphrase a second time to confirm it.": "Enter your recovery passphrase a second time to confirm it.", + "Confirm your recovery passphrase": "Confirm your recovery passphrase", "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.": "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.", "Keep a copy of it somewhere secure, like a password manager or even a safe.": "Keep a copy of it somewhere secure, like a password manager or even a safe.", "Your recovery key": "Your recovery key", + "Copy": "Copy", + "Download": "Download", "Your recovery key has been copied to your clipboard, paste it to:": "Your recovery key has been copied to your clipboard, paste it to:", "Your recovery key is in your Downloads folder.": "Your recovery key is in your Downloads folder.", "Print it and store it somewhere safe": "Print it and store it somewhere safe", "Save it on a USB key or backup drive": "Save it on a USB key or backup drive", "Copy it to your personal cloud storage": "Copy it to your personal cloud storage", + "Unable to query secret storage status": "Unable to query secret storage status", + "Retry": "Retry", + "You can now verify your other devices, and other users to keep your chats safe.": "You can now verify your other devices, and other users to keep your chats safe.", + "Upgrade your encryption": "Upgrade your encryption", + "Confirm recovery passphrase": "Confirm recovery passphrase", + "Make a copy of your recovery key": "Make a copy of your recovery key", + "You're done!": "You're done!", + "Unable to set up secret storage": "Unable to set up secret storage", + "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.": "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.", + "For maximum security, this should be different from your account password.": "For maximum security, this should be different from your account password.", + "Please enter your recovery passphrase a second time to confirm.": "Please enter your recovery passphrase a second time to confirm.", + "Repeat your recovery passphrase...": "Repeat your recovery passphrase...", "Your keys are being backed up (the first backup could take a few minutes).": "Your keys are being backed up (the first backup could take a few minutes).", "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.": "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.", "Set up Secure Message Recovery": "Set up Secure Message Recovery", "Secure your backup with a recovery passphrase": "Secure your backup with a recovery passphrase", - "Confirm your recovery passphrase": "Confirm your recovery passphrase", - "Make a copy of your recovery key": "Make a copy of your recovery key", "Starting backup...": "Starting backup...", "Success!": "Success!", "Create key backup": "Create key backup", diff --git a/src/stores/SetupEncryptionStore.js b/src/stores/SetupEncryptionStore.js index cc64e24a03..ae1f998b02 100644 --- a/src/stores/SetupEncryptionStore.js +++ b/src/stores/SetupEncryptionStore.js @@ -20,11 +20,10 @@ import { accessSecretStorage, AccessCancelledError } from '../CrossSigningManage import { PHASE_DONE as VERIF_PHASE_DONE } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; export const PHASE_INTRO = 0; -export const PHASE_RECOVERY_KEY = 1; -export const PHASE_BUSY = 2; -export const PHASE_DONE = 3; //final done stage, but still showing UX -export const PHASE_CONFIRM_SKIP = 4; -export const PHASE_FINISHED = 5; //UX can be closed +export const PHASE_BUSY = 1; +export const PHASE_DONE = 2; //final done stage, but still showing UX +export const PHASE_CONFIRM_SKIP = 3; +export const PHASE_FINISHED = 4; //UX can be closed export class SetupEncryptionStore extends EventEmitter { static sharedInstance() { @@ -37,19 +36,11 @@ export class SetupEncryptionStore extends EventEmitter { return; } this._started = true; - this.phase = PHASE_BUSY; + this.phase = PHASE_INTRO; this.verificationRequest = null; this.backupInfo = null; - - // ID of the key that the secrets we want are encrypted with - this.keyId = null; - // Descriptor of the key that the secrets we want are encrypted with - this.keyInfo = null; - MatrixClientPeg.get().on("crypto.verification.request", this.onVerificationRequest); MatrixClientPeg.get().on('userTrustStatusChanged', this._onUserTrustStatusChanged); - - this.fetchKeyInfo(); } stop() { @@ -66,49 +57,7 @@ export class SetupEncryptionStore extends EventEmitter { } } - async fetchKeyInfo() { - const keys = await MatrixClientPeg.get().isSecretStored('m.cross_signing.master', false); - if (Object.keys(keys).length === 0) { - this.keyId = null; - this.keyInfo = null; - } else { - // If the secret is stored under more than one key, we just pick an arbitrary one - this.keyId = Object.keys(keys)[0]; - this.keyInfo = keys[this.keyId]; - } - - this.phase = PHASE_INTRO; - this.emit("update"); - } - - async startKeyReset() { - try { - await accessSecretStorage(() => {}, {forceReset: true}); - // If the keys are reset, the trust status event will fire and we'll change state - } catch (e) { - // dialog was cancelled - stay on the current screen - } - } - - async useRecoveryKey() { - this.phase = PHASE_RECOVERY_KEY; - this.emit("update"); - } - - cancelUseRecoveryKey() { - this.phase = PHASE_INTRO; - this.emit("update"); - } - - async setupWithRecoveryKey(recoveryKey) { - this.startTrustCheck({[this.keyId]: recoveryKey}); - } - async usePassPhrase() { - this.startTrustCheck(); - } - - async startTrustCheck(withKeys) { this.phase = PHASE_BUSY; this.emit("update"); const cli = MatrixClientPeg.get(); @@ -135,9 +84,6 @@ export class SetupEncryptionStore extends EventEmitter { // to advance before this. await cli.restoreKeyBackupWithSecretStorage(backupInfo); } - }, { - withKeys, - passphraseOnly: true, }).catch(reject); } catch (e) { console.error(e); diff --git a/test/end-to-end-tests/src/usecases/signup.js b/test/end-to-end-tests/src/usecases/signup.js index 2859aadbda..aa9f6b7efa 100644 --- a/test/end-to-end-tests/src/usecases/signup.js +++ b/test/end-to-end-tests/src/usecases/signup.js @@ -79,7 +79,20 @@ module.exports = async function signup(session, username, password, homeserver) const acceptButton = await session.query('.mx_InteractiveAuthEntryComponents_termsSubmit'); await acceptButton.click(); - const xsignContButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary'); + //plow through cross-signing setup by entering arbitrary details + //TODO: It's probably important for the tests to know the passphrase + const xsigningPassphrase = 'a7eaXcjpa9!Yl7#V^h$B^%dovHUVX'; // https://xkcd.com/221/ + let passphraseField = await session.query('.mx_CreateSecretStorageDialog_passPhraseField input'); + await session.replaceInputText(passphraseField, xsigningPassphrase); + await session.delay(1000); // give it a second to analyze our passphrase for security + let xsignContButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary'); + await xsignContButton.click(); + + //repeat passphrase entry + passphraseField = await session.query('.mx_CreateSecretStorageDialog_passPhraseField input'); + await session.replaceInputText(passphraseField, xsigningPassphrase); + await session.delay(1000); // give it a second to analyze our passphrase for security + xsignContButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary'); await xsignContButton.click(); //ignore the recovery key @@ -88,11 +101,13 @@ module.exports = async function signup(session, username, password, homeserver) await copyButton.click(); //acknowledge that we copied the recovery key to a safe place - const copyContinueButton = await session.query( - '.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary', - ); + const copyContinueButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_primary'); await copyContinueButton.click(); + //acknowledge that we're done cross-signing setup and our keys are safe + const doneOkButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_primary'); + await doneOkButton.click(); + //wait for registration to finish so the hash gets set //onhashchange better? From 54e235b0b9f47fd721d3ab556f6f35c29f9afca3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 18 Jun 2020 10:42:33 +0100 Subject: [PATCH 102/194] Remove labs option to cache 'passphrase' (which actually meant SSSS secrets) Fixes https://github.com/vector-im/riot-web/issues/1392 --- src/CrossSigningManager.js | 5 +---- .../views/settings/tabs/user/LabsUserSettingsTab.js | 1 - src/settings/Settings.js | 5 ----- 3 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index d40f820ac0..b8a17c0f0d 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -34,10 +34,7 @@ let secretStorageBeingAccessed = false; let passphraseOnlyOption = null; function isCachingAllowed() { - return ( - secretStorageBeingAccessed || - SettingsStore.getValue("keepSecretStoragePassphraseForSession") - ); + return secretStorageBeingAccessed; } export class AccessCancelledError extends Error { diff --git a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js index 3e69107159..9724b9934f 100644 --- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js @@ -66,7 +66,6 @@ export default class LabsUserSettingsTab extends React.Component { -
          ); diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 225af15ec8..5e439a1d71 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -521,11 +521,6 @@ export const SETTINGS = { displayName: _td("Enable message search in encrypted rooms"), default: true, }, - "keepSecretStoragePassphraseForSession": { - supportedLevels: ['device', 'config'], - displayName: _td("Keep recovery passphrase in memory for this session"), - default: false, - }, "crawlerSleepTime": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, displayName: _td("How fast should messages be downloaded."), From 793c6c549ea7f0971ac841c9a3f77a2c003a3887 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 18 Jun 2020 10:45:15 +0100 Subject: [PATCH 103/194] Unused import --- src/CrossSigningManager.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index b8a17c0f0d..cf5df3c29e 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -20,7 +20,6 @@ import {MatrixClientPeg} from './MatrixClientPeg'; import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase'; import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; import { _t } from './languageHandler'; -import SettingsStore from './settings/SettingsStore'; import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib"; // This stores the secret storage private keys in memory for the JS SDK. This is From 3c268a31c8f0461203031bb2417945ad081df993 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 18 Jun 2020 10:48:18 +0100 Subject: [PATCH 104/194] i18n --- src/i18n/strings/en_EN.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5de33ada55..c125ca6b6f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -480,7 +480,6 @@ "Send read receipts for messages (requires compatible homeserver to disable)": "Send read receipts for messages (requires compatible homeserver to disable)", "Show previews/thumbnails for images": "Show previews/thumbnails for images", "Enable message search in encrypted rooms": "Enable message search in encrypted rooms", - "Keep recovery passphrase in memory for this session": "Keep recovery passphrase in memory for this session", "How fast should messages be downloaded.": "How fast should messages be downloaded.", "Manually verify all remote sessions": "Manually verify all remote sessions", "IRC display name width": "IRC display name width", From a579ea95db812423055e8ab0f72c15bed4e62393 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 18 Jun 2020 13:28:24 +0100 Subject: [PATCH 105/194] Lint and i18n --- .../tabs/user/AppearanceUserSettingsTab.tsx | 36 +++++++++---------- src/i18n/strings/en_EN.json | 1 + 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index 0620626181..0f1779270b 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -35,27 +35,27 @@ interface IProps { } interface IThemeState { - theme: string, - useSystemTheme: boolean, + theme: string; + useSystemTheme: boolean; } export interface CustomThemeMessage { - isError: boolean, - text: string + isError: boolean; + text: string; }; interface IState extends IThemeState { // String displaying the current selected fontSize. // Needs to be string for things like '17.' without // trailing 0s. - fontSize: string, - customThemeUrl: string, - customThemeMessage: CustomThemeMessage, - useCustomFontSize: boolean, - useIRCLayout: boolean, + fontSize: string; + customThemeUrl: string; + customThemeMessage: CustomThemeMessage; + useCustomFontSize: boolean; + useIRCLayout: boolean; } -const MESSAGE_PREVIEW_TEXT = "Hey you. You're the best" +const MESSAGE_PREVIEW_TEXT = _t("Hey you. You're the best!"); export default class AppearanceUserSettingsTab extends React.Component { @@ -70,7 +70,7 @@ export default class AppearanceUserSettingsTab extends React.Component @@ -366,11 +366,11 @@ export default class AppearanceUserSettingsTab extends React.Component diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 157d83ba57..6a16f947f4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -771,6 +771,7 @@ "Downloading update...": "Downloading update...", "New version available. Update now.": "New version available. Update now.", "Check for update": "Check for update", + "Hey you. You're the best!": "Hey you. You're the best!", "Size must be a number": "Size must be a number", "Custom font size can only be between %(min)s pt and %(max)s pt": "Custom font size can only be between %(min)s pt and %(max)s pt", "Use between %(min)s pt and %(max)s pt": "Use between %(min)s pt and %(max)s pt", From 69969dfee89b56546f62a5583d6bfb2cd8de1b3b Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 18 Jun 2020 13:58:35 +0100 Subject: [PATCH 106/194] Respond to review - remove all refs to getsdk - i18n - some lints --- .../views/elements/MessagePreview.tsx | 44 ++++++++++--------- .../tabs/user/AppearanceUserSettingsTab.tsx | 31 ++++++------- src/i18n/strings/en_EN.json | 1 + 3 files changed, 39 insertions(+), 37 deletions(-) diff --git a/src/components/views/elements/MessagePreview.tsx b/src/components/views/elements/MessagePreview.tsx index 6a0fa9a756..3be0a16781 100644 --- a/src/components/views/elements/MessagePreview.tsx +++ b/src/components/views/elements/MessagePreview.tsx @@ -14,14 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixClientPeg } from '../../../MatrixClientPeg'; - import React from 'react'; - -import * as Avatar from '../../../Avatar'; import classnames from 'classnames'; import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; -import * as sdk from "../../../index"; + +import * as Avatar from '../../../Avatar'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; +import EventTile from '../rooms/EventTile'; interface IProps { /** @@ -42,8 +41,8 @@ interface IProps { interface IState { userId: string; - displayname: string, - avatar_url: string, + displayname: string; + avatar_url: string; } const AVATAR_SIZE = 32; @@ -62,7 +61,8 @@ export default class MessagePreview extends React.Component { async componentDidMount() { // Fetch current user data const client = MatrixClientPeg.get() - const userId = await client.getUserId(); + const userId = client.getUserId(); + console.log({userId}) const profileInfo = await client.getProfileInfo(userId); const avatar_url = Avatar.avatarUrlForUser( {avatarUrl: profileInfo.avatar_url}, @@ -76,24 +76,22 @@ export default class MessagePreview extends React.Component { } - public render() { - const EventTile = sdk.getComponent("views.rooms.EventTile"); - + private fakeEvent({userId, displayname, avatar_url}: IState) { // Fake it till we make it const event = new MatrixEvent(JSON.parse(`{ "type": "m.room.message", - "sender": "${this.state.userId}", + "sender": "${userId}", "content": { "m.new_content": { "msgtype": "m.text", "body": "${this.props.message}", - "displayname": "${this.state.displayname}", - "avatar_url": "${this.state.avatar_url}" + "displayname": "${displayname}", + "avatar_url": "${avatar_url}" }, "msgtype": "m.text", "body": "${this.props.message}", - "displayname": "${this.state.displayname}", - "avatar_url": "${this.state.avatar_url}" + "displayname": "${displayname}", + "avatar_url": "${avatar_url}" }, "unsigned": { "age": 97 @@ -104,13 +102,19 @@ export default class MessagePreview extends React.Component { // Fake it more event.sender = { - name: this.state.displayname, - userId: this.state.userId, + name: displayname, + userId: userId, getAvatarUrl: (..._) => { - return this.state.avatar_url; + return avatar_url; }, } + + } + + public render() { + const event = this.fakeEvent(this.state); + let className = classnames( this.props.className, { @@ -119,7 +123,7 @@ export default class MessagePreview extends React.Component { } ); - return
          + return
          } diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index 881868907d..f7fb479c12 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -19,7 +19,6 @@ limitations under the License. import React from 'react'; import {_t} from "../../../../../languageHandler"; import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore"; -import * as sdk from "../../../../../index"; import { enumerateThemes } from "../../../../../theme"; import ThemeWatcher from "../../../../../settings/watchers/ThemeWatcher"; import Field from "../../../elements/Field"; @@ -30,28 +29,32 @@ import { FontWatcher } from "../../../../../settings/watchers/FontWatcher"; import { RecheckThemePayload } from '../../../../../dispatcher/payloads/RecheckThemePayload'; import { Action } from '../../../../../dispatcher/actions'; import { IValidationResult, IFieldState } from '../../../elements/Validation'; +import StyledRadioButton from '../../../elements/StyledRadioButton'; +import StyledCheckbox from '../../../elements/StyledCheckbox'; +import SettingsFlag from '../../../elements/SettingsFlag'; +import MessagePreview from '../../../elements/MessagePreview'; interface IProps { } interface IThemeState { - theme: string, - useSystemTheme: boolean, + theme: string; + useSystemTheme: boolean; } export interface CustomThemeMessage { - isError: boolean, - text: string + isError: boolean; + text: string; }; interface IState extends IThemeState { // String displaying the current selected fontSize. // Needs to be string for things like '17.' without // trailing 0s. - fontSize: string, - customThemeUrl: string, - customThemeMessage: CustomThemeMessage, - useCustomFontSize: boolean, + fontSize: string; + customThemeUrl: string; + customThemeMessage: CustomThemeMessage; + useCustomFontSize: boolean; } export default class AppearanceUserSettingsTab extends React.Component { @@ -198,10 +201,6 @@ export default class AppearanceUserSettingsTab extends React.Component {_t("Font size")}
          Aa
          diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index aacec471f6..dbc0db6861 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -779,6 +779,7 @@ "Custom theme URL": "Custom theme URL", "Add theme": "Add theme", "Theme": "Theme", + "Hey you. You're the best!": "Hey you. You're the best!", "Customise your appearance": "Customise your appearance", "Appearance Settings only affect this Riot session.": "Appearance Settings only affect this Riot session.", "Flair": "Flair", From 05d030908169bf14c4095600d41a01c186e6c954 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 18 Jun 2020 14:32:43 +0100 Subject: [PATCH 107/194] Lint a few semicolons --- src/@types/common.ts | 2 +- src/BasePlatform.ts | 2 +- src/DeviceListener.ts | 14 ++++---- src/MatrixClientPeg.ts | 14 ++++---- src/actions/TagOrderActions.ts | 2 +- src/autocomplete/Autocompleter.ts | 10 +++--- src/autocomplete/Components.tsx | 2 +- src/components/structures/MatrixChat.tsx | 4 +-- src/components/structures/UserMenuButton.tsx | 2 +- src/components/views/auth/PassphraseField.tsx | 2 +- .../views/elements/AccessibleButton.tsx | 4 +-- src/components/views/elements/Draggable.tsx | 22 ++++++------- src/components/views/elements/Field.tsx | 12 +++---- .../elements/IRCTimelineProfileResizer.tsx | 33 ++++++++++--------- .../views/elements/SettingsFlag.tsx | 8 ++--- src/components/views/elements/Slider.tsx | 16 ++++----- .../views/elements/StyledCheckbox.tsx | 4 +-- .../views/elements/StyledRadioButton.tsx | 4 +-- .../views/elements/ToggleSwitch.tsx | 2 +- src/components/views/elements/Tooltip.tsx | 10 +++--- .../views/rooms/RoomBreadcrumbs2.tsx | 2 +- src/components/views/rooms/RoomSublist2.tsx | 2 +- src/components/views/rooms/RoomTile2.tsx | 2 +- .../tabs/user/AppearanceUserSettingsTab.tsx | 20 +++++------ .../views/toasts/VerificationRequestToast.tsx | 2 +- .../payloads/CheckUpdatesPayload.ts | 2 +- src/dispatcher/payloads/OpenToTabPayload.ts | 2 +- .../payloads/RecheckThemePayload.ts | 2 +- src/dispatcher/payloads/ViewTooltipPayload.ts | 4 +-- src/dispatcher/payloads/ViewUserPayload.ts | 2 +- src/settings/watchers/Watcher.ts | 4 +-- src/stores/room-list/algorithms/Algorithm.ts | 4 +-- src/utils/FormattingUtils.ts | 2 +- src/utils/ShieldUtils.ts | 2 +- tslint.json | 4 ++- 35 files changed, 115 insertions(+), 110 deletions(-) diff --git a/src/@types/common.ts b/src/@types/common.ts index 26e5317aa3..9109993541 100644 --- a/src/@types/common.ts +++ b/src/@types/common.ts @@ -15,5 +15,5 @@ limitations under the License. */ // Based on https://stackoverflow.com/a/53229857/3532235 -export type Without = {[P in Exclude] ? : never} +export type Without = {[P in Exclude] ? : never}; export type XOR = (T | U) extends object ? (Without & U) | (Without & T) : T | U; diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index 520c3fbe46..d54dc7dd23 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -150,7 +150,7 @@ export default abstract class BasePlatform { abstract displayNotification(title: string, msg: string, avatarUrl: string, room: Object); loudNotification(ev: Event, room: Object) { - }; + } /** * Returns a promise that resolves to a string representing the current version of the application. diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index e73b56416b..cfec2890d2 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -119,26 +119,26 @@ export default class DeviceListener { // No need to do a recheck here: we just need to get a snapshot of our devices // before we download any new ones. - } + }; _onDevicesUpdated = (users: string[]) => { if (!users.includes(MatrixClientPeg.get().getUserId())) return; this._recheck(); - } + }; _onDeviceVerificationChanged = (userId: string) => { if (userId !== MatrixClientPeg.get().getUserId()) return; this._recheck(); - } + }; _onUserTrustStatusChanged = (userId: string) => { if (userId !== MatrixClientPeg.get().getUserId()) return; this._recheck(); - } + }; _onCrossSingingKeysChanged = () => { this._recheck(); - } + }; _onAccountData = (ev) => { // User may have: @@ -152,11 +152,11 @@ export default class DeviceListener { ) { this._recheck(); } - } + }; _onSync = (state, prevState) => { if (state === 'PREPARED' && prevState === null) this._recheck(); - } + }; // The server doesn't tell us when key backup is set up, so we poll // & cache the result diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index bc550c1935..5f334a639c 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -35,13 +35,13 @@ import { crossSigningCallbacks } from './CrossSigningManager'; import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode"; export interface IMatrixClientCreds { - homeserverUrl: string, - identityServerUrl: string, - userId: string, - deviceId: string, - accessToken: string, - guest: boolean, - pickleKey?: string, + homeserverUrl: string; + identityServerUrl: string; + userId: string; + deviceId: string; + accessToken: string; + guest: boolean; + pickleKey?: string; } // TODO: Move this to the js-sdk diff --git a/src/actions/TagOrderActions.ts b/src/actions/TagOrderActions.ts index bf1820d5d1..75097952c0 100644 --- a/src/actions/TagOrderActions.ts +++ b/src/actions/TagOrderActions.ts @@ -60,7 +60,7 @@ export default class TagOrderActions { // For an optimistic update return {tags, removedTags}; }); - }; + } /** * Creates an action thunk that will do an asynchronous request to diff --git a/src/autocomplete/Autocompleter.ts b/src/autocomplete/Autocompleter.ts index 8384eb9d4f..2615736e09 100644 --- a/src/autocomplete/Autocompleter.ts +++ b/src/autocomplete/Autocompleter.ts @@ -35,15 +35,15 @@ export interface ISelectionRange { export interface ICompletion { type: "at-room" | "command" | "community" | "room" | "user"; - completion: string, + completion: string; completionId?: string; - component?: ReactElement, - range: ISelectionRange, - command?: string, + component?: ReactElement; + range: ISelectionRange; + command?: string; suffix?: string; // If provided, apply a LINK entity to the completion with the // data = { url: href }. - href?: string, + href?: string; } const PROVIDERS = [ diff --git a/src/autocomplete/Components.tsx b/src/autocomplete/Components.tsx index 0ee0088f02..6ac2f4db14 100644 --- a/src/autocomplete/Components.tsx +++ b/src/autocomplete/Components.tsx @@ -46,7 +46,7 @@ export const TextualCompletion = forwardRef((props }); interface IPillCompletionProps extends ITextualCompletionProps { - children?: React.ReactNode, + children?: React.ReactNode; } export const PillCompletion = forwardRef((props, ref) => { diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 634e13b103..d5f73fa3df 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -151,9 +151,9 @@ interface IProps { // TODO type things better // Represents the screen to display as a result of parsing the initial window.location initialScreenAfterLogin?: IScreen; // displayname, if any, to set on the device when logging in/registering. - defaultDeviceDisplayName?: string, + defaultDeviceDisplayName?: string; // A function that makes a registration URL - makeRegistrationUrl: (object) => string, + makeRegistrationUrl: (object) => string; } interface IState { diff --git a/src/components/structures/UserMenuButton.tsx b/src/components/structures/UserMenuButton.tsx index f3626ba270..6607fffdd1 100644 --- a/src/components/structures/UserMenuButton.tsx +++ b/src/components/structures/UserMenuButton.tsx @@ -291,6 +291,6 @@ export default class UserMenuButton extends React.Component { {contextMenu} - ) + ); } } diff --git a/src/components/views/auth/PassphraseField.tsx b/src/components/views/auth/PassphraseField.tsx index f09791ce26..2f5064447e 100644 --- a/src/components/views/auth/PassphraseField.tsx +++ b/src/components/views/auth/PassphraseField.tsx @@ -118,7 +118,7 @@ class PassphraseField extends PureComponent { value={this.props.value} onChange={this.props.onChange} onValidate={this.onValidate} - /> + />; } } diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index 18dd43ad02..01a27d9522 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -19,7 +19,7 @@ import React from 'react'; import {Key} from '../../../Keyboard'; import classnames from 'classnames'; -export type ButtonEvent = React.MouseEvent | React.KeyboardEvent +export type ButtonEvent = React.MouseEvent | React.KeyboardEvent; /** * children: React's magic prop. Represents all children given to the element. @@ -40,7 +40,7 @@ interface IProps extends React.InputHTMLAttributes { disabled?: boolean; className?: string; onClick?(e?: ButtonEvent): void; -}; +} interface IAccessibleButtonProps extends React.InputHTMLAttributes { ref?: React.Ref; diff --git a/src/components/views/elements/Draggable.tsx b/src/components/views/elements/Draggable.tsx index 3096ac42f7..3397fd901c 100644 --- a/src/components/views/elements/Draggable.tsx +++ b/src/components/views/elements/Draggable.tsx @@ -17,20 +17,20 @@ limitations under the License. import React from 'react'; interface IProps { - className: string, - dragFunc: (currentLocation: ILocationState, event: MouseEvent) => ILocationState, - onMouseUp: (event: MouseEvent) => void, + className: string; + dragFunc: (currentLocation: ILocationState, event: MouseEvent) => ILocationState; + onMouseUp: (event: MouseEvent) => void; } interface IState { - onMouseMove: (event: MouseEvent) => void, - onMouseUp: (event: MouseEvent) => void, - location: ILocationState, + onMouseMove: (event: MouseEvent) => void; + onMouseUp: (event: MouseEvent) => void; + location: ILocationState; } export interface ILocationState { - currentX: number, - currentY: number, + currentX: number; + currentY: number; } export default class Draggable extends React.Component { @@ -58,13 +58,13 @@ export default class Draggable extends React.Component { document.addEventListener("mousemove", this.state.onMouseMove); document.addEventListener("mouseup", this.state.onMouseUp); - } + }; private onMouseUp = (event: MouseEvent): void => { document.removeEventListener("mousemove", this.state.onMouseMove); document.removeEventListener("mouseup", this.state.onMouseUp); this.props.onMouseUp(event); - } + }; private onMouseMove(event: MouseEvent): void { const newLocation = this.props.dragFunc(this.state.location, event); @@ -75,7 +75,7 @@ export default class Draggable extends React.Component { } render() { - return
          + return
          ; } } \ No newline at end of file diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index 63bdce3e3b..fbee431d6e 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -85,20 +85,20 @@ interface ITextareaProps extends IProps, TextareaHTMLAttributes { private id: string; private input: HTMLInputElement; - private static defaultProps = { + public static readonly defaultProps = { element: "input", type: "text", - } + }; /* * This was changed from throttle to debounce: this is more traditional for diff --git a/src/components/views/elements/IRCTimelineProfileResizer.tsx b/src/components/views/elements/IRCTimelineProfileResizer.tsx index 596d46bf36..65140707d5 100644 --- a/src/components/views/elements/IRCTimelineProfileResizer.tsx +++ b/src/components/views/elements/IRCTimelineProfileResizer.tsx @@ -20,15 +20,15 @@ import Draggable, {ILocationState} from './Draggable'; interface IProps { // Current room - roomId: string, - minWidth: number, - maxWidth: number, -}; + roomId: string; + minWidth: number; + maxWidth: number; +} interface IState { - width: number, - IRCLayoutRoot: HTMLElement, -}; + width: number; + IRCLayoutRoot: HTMLElement; +} export default class IRCTimelineProfileResizer extends React.Component { constructor(props: IProps) { @@ -37,20 +37,19 @@ export default class IRCTimelineProfileResizer extends React.Component this.updateCSSWidth(this.state.width)) + }, () => this.updateCSSWidth(this.state.width)); } private dragFunc = (location: ILocationState, event: React.MouseEvent): ILocationState => { const offset = event.clientX - location.currentX; const newWidth = this.state.width + offset; - console.log({offset}) // If we're trying to go smaller than min width, don't. if (newWidth < this.props.minWidth) { return location; @@ -69,8 +68,8 @@ export default class IRCTimelineProfileResizer extends React.Component + return ; } -}; \ No newline at end of file +} diff --git a/src/components/views/elements/SettingsFlag.tsx b/src/components/views/elements/SettingsFlag.tsx index bda15ebaab..9bdd04d803 100644 --- a/src/components/views/elements/SettingsFlag.tsx +++ b/src/components/views/elements/SettingsFlag.tsx @@ -48,18 +48,18 @@ export default class SettingsFlag extends React.Component { this.props.roomId, this.props.isExplicit, ), - } + }; } private onChange = (checked: boolean): void => { this.save(checked); this.setState({ value: checked }); if (this.props.onChange) this.props.onChange(checked); - } + }; private checkBoxOnChange = (e: React.ChangeEvent) => { this.onChange(e.target.checked); - } + }; private save = (val?: boolean): void => { return SettingsStore.setValue( @@ -68,7 +68,7 @@ export default class SettingsFlag extends React.Component { this.props.level, val !== undefined ? val : this.state.value, ); - } + }; public render() { const canChange = SettingsStore.canSetValue(this.props.name, this.props.roomId, this.props.level); diff --git a/src/components/views/elements/Slider.tsx b/src/components/views/elements/Slider.tsx index f76a4684d3..7c4b09568a 100644 --- a/src/components/views/elements/Slider.tsx +++ b/src/components/views/elements/Slider.tsx @@ -65,9 +65,9 @@ export default class Slider extends React.Component { const intervalWidth = 1 / (values.length - 1); - const linearInterpolation = (value - closestLessValue) / (closestGreaterValue - closestLessValue) + const linearInterpolation = (value - closestLessValue) / (closestGreaterValue - closestLessValue); - return 100 * (closest - 1 + linearInterpolation) * intervalWidth + return 100 * (closest - 1 + linearInterpolation) * intervalWidth; } @@ -86,8 +86,8 @@ export default class Slider extends React.Component { const offset = this.offset(this.props.values, this.props.value); selection =
          -
          -
          +
          +
          ; } return
          @@ -115,13 +115,13 @@ export default class Slider extends React.Component { interface IDotProps { // Callback for behavior onclick - onClick: () => void, + onClick: () => void; // Whether the dot should appear active - active: boolean, + active: boolean; // The label on the dot - label: string, + label: string; // Whether the slider is disabled disabled: boolean; @@ -129,7 +129,7 @@ interface IDotProps { class Dot extends React.PureComponent { render(): React.ReactNode { - let className = "mx_Slider_dot" + let className = "mx_Slider_dot"; if (!this.props.disabled && this.props.active) { className += " mx_Slider_dotActive"; } diff --git a/src/components/views/elements/StyledCheckbox.tsx b/src/components/views/elements/StyledCheckbox.tsx index 341f59d5da..be983828ff 100644 --- a/src/components/views/elements/StyledCheckbox.tsx +++ b/src/components/views/elements/StyledCheckbox.tsx @@ -30,7 +30,7 @@ export default class StyledCheckbox extends React.PureComponent public static readonly defaultProps = { className: "", - } + }; constructor(props: IProps) { super(props); @@ -51,6 +51,6 @@ export default class StyledCheckbox extends React.PureComponent { this.props.children }
          - + ; } } \ No newline at end of file diff --git a/src/components/views/elements/StyledRadioButton.tsx b/src/components/views/elements/StyledRadioButton.tsx index fdedd16230..0ea786d953 100644 --- a/src/components/views/elements/StyledRadioButton.tsx +++ b/src/components/views/elements/StyledRadioButton.tsx @@ -26,7 +26,7 @@ interface IState { export default class StyledRadioButton extends React.PureComponent { public static readonly defaultProps = { className: '', - } + }; public render() { const { children, className, disabled, ...otherProps } = this.props; @@ -43,6 +43,6 @@ export default class StyledRadioButton extends React.PureComponent
          {children}
          - + ; } } diff --git a/src/components/views/elements/ToggleSwitch.tsx b/src/components/views/elements/ToggleSwitch.tsx index 7b690899ac..f05c45b3db 100644 --- a/src/components/views/elements/ToggleSwitch.tsx +++ b/src/components/views/elements/ToggleSwitch.tsx @@ -28,7 +28,7 @@ interface IProps { // Called when the checked state changes. First argument will be the new state. onChange(checked: boolean): void; -}; +} // Controlled Toggle Switch element, written with Accessibility in mind export default ({checked, disabled = false, onChange, ...props}: IProps) => { diff --git a/src/components/views/elements/Tooltip.tsx b/src/components/views/elements/Tooltip.tsx index 38960d1a58..69ad5e256c 100644 --- a/src/components/views/elements/Tooltip.tsx +++ b/src/components/views/elements/Tooltip.tsx @@ -29,15 +29,15 @@ const MIN_TOOLTIP_HEIGHT = 25; interface IProps { // Class applied to the element used to position the tooltip - className: string, + className: string; // Class applied to the tooltip itself - tooltipClassName?: string, + tooltipClassName?: string; // Whether the tooltip is visible or hidden. // The hidden state allows animating the tooltip away via CSS. // Defaults to visible if unset. - visible?: boolean, + visible?: boolean; // the react element to put into the tooltip - label: React.ReactNode, + label: React.ReactNode; } export default class Tooltip extends React.Component { @@ -126,7 +126,7 @@ export default class Tooltip extends React.Component { tooltip: this.tooltip, parent: parent, }); - } + }; public render() { // Render a placeholder diff --git a/src/components/views/rooms/RoomBreadcrumbs2.tsx b/src/components/views/rooms/RoomBreadcrumbs2.tsx index 1b912b39d6..69cca39d9f 100644 --- a/src/components/views/rooms/RoomBreadcrumbs2.tsx +++ b/src/components/views/rooms/RoomBreadcrumbs2.tsx @@ -97,7 +97,7 @@ export default class RoomBreadcrumbs2 extends React.PureComponent - ) + ); }); if (tiles.length > 0) { diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index c72e74a1be..bfa210ebf9 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -420,7 +420,7 @@ export default class RoomSublist2 extends React.Component { {visibleTiles} {showNButton} - ) + ); } // TODO: onKeyDown support diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 671b981ac5..9f4870d437 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -232,7 +232,7 @@ export default class RoomTile2 extends React.Component { /> {contextMenu} - ) + ); } public render(): React.ReactElement { diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index fe575c2819..7867852c1d 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -35,23 +35,23 @@ interface IProps { } interface IThemeState { - theme: string, - useSystemTheme: boolean, + theme: string; + useSystemTheme: boolean; } export interface CustomThemeMessage { - isError: boolean, - text: string -}; + isError: boolean; + text: string; +} interface IState extends IThemeState { // String displaying the current selected fontSize. // Needs to be string for things like '17.' without // trailing 0s. - fontSize: string, - customThemeUrl: string, - customThemeMessage: CustomThemeMessage, - useCustomFontSize: boolean, + fontSize: string; + customThemeUrl: string; + customThemeMessage: CustomThemeMessage; + useCustomFontSize: boolean; } export default class AppearanceUserSettingsTab extends React.Component { @@ -159,7 +159,7 @@ export default class AppearanceUserSettingsTab extends React.Component => { let currentThemes: string[] = SettingsStore.getValue("custom_themes"); diff --git a/src/components/views/toasts/VerificationRequestToast.tsx b/src/components/views/toasts/VerificationRequestToast.tsx index 38e7e31989..cc41e81b33 100644 --- a/src/components/views/toasts/VerificationRequestToast.tsx +++ b/src/components/views/toasts/VerificationRequestToast.tsx @@ -90,7 +90,7 @@ export default class VerificationRequestToast extends React.PureComponent { ToastStore.sharedInstance().dismissToast(this.props.toastKey); diff --git a/src/dispatcher/payloads/CheckUpdatesPayload.ts b/src/dispatcher/payloads/CheckUpdatesPayload.ts index 0f0f9a01e5..fa4fb3a538 100644 --- a/src/dispatcher/payloads/CheckUpdatesPayload.ts +++ b/src/dispatcher/payloads/CheckUpdatesPayload.ts @@ -19,7 +19,7 @@ import { Action } from "../actions"; import {UpdateCheckStatus} from "../../BasePlatform"; export interface CheckUpdatesPayload extends ActionPayload { - action: Action.CheckUpdates, + action: Action.CheckUpdates; /** * The current phase of the manual update check. diff --git a/src/dispatcher/payloads/OpenToTabPayload.ts b/src/dispatcher/payloads/OpenToTabPayload.ts index 2877ee053e..7548c342e4 100644 --- a/src/dispatcher/payloads/OpenToTabPayload.ts +++ b/src/dispatcher/payloads/OpenToTabPayload.ts @@ -18,7 +18,7 @@ import { ActionPayload } from "../payloads"; import { Action } from "../actions"; export interface OpenToTabPayload extends ActionPayload { - action: Action.ViewUserSettings | string, // TODO: Add room settings action + action: Action.ViewUserSettings | string; // TODO: Add room settings action /** * The tab ID to open in the settings view to start, if possible. diff --git a/src/dispatcher/payloads/RecheckThemePayload.ts b/src/dispatcher/payloads/RecheckThemePayload.ts index 06f7012049..e9bea612e0 100644 --- a/src/dispatcher/payloads/RecheckThemePayload.ts +++ b/src/dispatcher/payloads/RecheckThemePayload.ts @@ -18,7 +18,7 @@ import { ActionPayload } from "../payloads"; import { Action } from "../actions"; export interface RecheckThemePayload extends ActionPayload { - action: Action.RecheckTheme, + action: Action.RecheckTheme; /** * Optionally specify the exact theme which is to be loaded. diff --git a/src/dispatcher/payloads/ViewTooltipPayload.ts b/src/dispatcher/payloads/ViewTooltipPayload.ts index 8778287128..069e3a619a 100644 --- a/src/dispatcher/payloads/ViewTooltipPayload.ts +++ b/src/dispatcher/payloads/ViewTooltipPayload.ts @@ -19,7 +19,7 @@ import { Action } from "../actions"; import { Component } from "react"; export interface ViewTooltipPayload extends ActionPayload { - action: Action.ViewTooltip, + action: Action.ViewTooltip; /* * The tooltip to render. If it's null the tooltip will not be rendered @@ -31,5 +31,5 @@ export interface ViewTooltipPayload extends ActionPayload { * The parent under which to render the tooltip. Can be null to remove * the parent type. */ - parent: null | Element + parent: null | Element; } \ No newline at end of file diff --git a/src/dispatcher/payloads/ViewUserPayload.ts b/src/dispatcher/payloads/ViewUserPayload.ts index ed602d4e24..c2838d0dbb 100644 --- a/src/dispatcher/payloads/ViewUserPayload.ts +++ b/src/dispatcher/payloads/ViewUserPayload.ts @@ -19,7 +19,7 @@ import { ActionPayload } from "../payloads"; import { Action } from "../actions"; export interface ViewUserPayload extends ActionPayload { - action: Action.ViewUser, + action: Action.ViewUser; /** * The member to view. May be null or falsy to indicate that no member diff --git a/src/settings/watchers/Watcher.ts b/src/settings/watchers/Watcher.ts index a9f6f3f2c8..94a14faa27 100644 --- a/src/settings/watchers/Watcher.ts +++ b/src/settings/watchers/Watcher.ts @@ -15,6 +15,6 @@ limitations under the License. */ export default interface IWatcher { - start(): void - stop(): void + start(): void; + stop(): void; } \ No newline at end of file diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index a89167095d..d145149cc7 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -127,7 +127,7 @@ export class Algorithm extends EventEmitter { const algorithm = getListAlgorithmInstance(order, tagId, this.sortAlgorithms[tagId]); this.algorithms[tagId] = algorithm; - await algorithm.setRooms(this._cachedRooms[tagId]) + await algorithm.setRooms(this._cachedRooms[tagId]); this._cachedRooms[tagId] = algorithm.orderedRooms; this.recalculateFilteredRoomsForTag(tagId); // update filter to re-sort the list this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed @@ -541,5 +541,5 @@ export class Algorithm extends EventEmitter { } return true; - }; + } } diff --git a/src/utils/FormattingUtils.ts b/src/utils/FormattingUtils.ts index 21b50da4ca..f82500de71 100644 --- a/src/utils/FormattingUtils.ts +++ b/src/utils/FormattingUtils.ts @@ -36,7 +36,7 @@ export function formatCount(count: number): string { */ export function formatCountLong(count: number): string { const formatter = new Intl.NumberFormat(); - return formatter.format(count) + return formatter.format(count); } /** diff --git a/src/utils/ShieldUtils.ts b/src/utils/ShieldUtils.ts index adaa961a00..3f8cf06815 100644 --- a/src/utils/ShieldUtils.ts +++ b/src/utils/ShieldUtils.ts @@ -10,7 +10,7 @@ interface Client { getStoredDevicesForUser: (userId: string) => [{ deviceId: string }]; checkDeviceTrust: (userId: string, deviceId: string) => { isVerified: () => boolean - } + }; } interface Room { diff --git a/tslint.json b/tslint.json index fc234117fc..fd99d9d228 100644 --- a/tslint.json +++ b/tslint.json @@ -46,7 +46,9 @@ "quotemark": false, "radix": true, "semicolon": [ - "always" + true, + "always", + "strict-bound-class-methods" ], "triple-equals": [], "typedef-whitespace": [ From 9b90dcd1a85d78f964f7d38f9f0737b539425fd2 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Thu, 18 Jun 2020 02:04:17 +0000 Subject: [PATCH 108/194] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (2287 of 2287 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 1c5b6cff83..cc319f3e0d 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2525,5 +2525,6 @@ "Create a Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login you’ll need it to unlock your data.": "建立您的復原金鑰以儲存加密金鑰與您的帳號資料。如果您失去對此登入階段的存取權,您必須用它來解鎖您的資料。", "Create a Recovery Key": "建立復原金鑰", "Upgrade your Recovery Key": "升級您的復原金鑰", - "Store your Recovery Key": "儲存您的復原金鑰" + "Store your Recovery Key": "儲存您的復原金鑰", + "Use the improved room list (in development - will refresh to apply changes)": "使用改進的聊天室清單(開發中 ── 將會重新整理以套用變更)" } From 5ceae3085caa63edaa0b16948b78ba0821398dd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Thu, 18 Jun 2020 06:30:37 +0000 Subject: [PATCH 109/194] Translated using Weblate (French) Currently translated at 100.0% (2287 of 2287 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index d2942522f4..ef5fc07b60 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2526,5 +2526,6 @@ "Create a Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login you’ll need it to unlock your data.": "Créez une clé de récupération pour stocker vos clé et secrets de chiffrement avec les données de votre compte. Si vous n’avez plus accès à cette connexion, vous en aurez besoin pour déverrouiller vos données.", "Create a Recovery Key": "Créer une clé de récupération", "Upgrade your Recovery Key": "Mettre à jour votre clé de récupération", - "Store your Recovery Key": "Stocker votre clé de récupération" + "Store your Recovery Key": "Stocker votre clé de récupération", + "Use the improved room list (in development - will refresh to apply changes)": "Utiliser la liste de salons améliorée (en développement − actualisera pour appliquer les changements)" } From f1365a8d3061d8ef08f24ec8674a4a3c317504a2 Mon Sep 17 00:00:00 2001 From: XoseM Date: Thu, 18 Jun 2020 10:33:35 +0000 Subject: [PATCH 110/194] Translated using Weblate (Galician) Currently translated at 76.1% (1740 of 2287 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/gl/ --- src/i18n/strings/gl.json | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 7cbc115085..94d0f23b54 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -1716,7 +1716,7 @@ "Sort by": "Orde por", "Activity": "Actividade", "A-Z": "A-Z", - "Unread rooms": "", + "Unread rooms": "Salas non lidas", "Always show first": "Mostrar sempre primeiro", "Show": "Mostrar", "Message preview": "Vista previa da mensaxe", @@ -1780,5 +1780,38 @@ "Yours, or the other users’ session": "A túa, ou a sesión da outra usuaria", "Trusted": "Confiable", "Not trusted": "Non confiable", - "%(count)s verified sessions|other": "%(count)s sesións verificadas" + "%(count)s verified sessions|other": "%(count)s sesións verificadas", + "Use the improved room list (in development - will refresh to apply changes)": "Usar a lista de salas mellorada (desenvolvemento - actualizar para aplicar)", + "%(count)s verified sessions|one": "1 sesión verificada", + "Hide verified sessions": "Agochar sesións verificadas", + "%(count)s sessions|other": "%(count)s sesións", + "%(count)s sessions|one": "%(count)s sesión", + "Hide sessions": "Agochar sesións", + "Direct message": "Mensaxe directa", + "No recent messages by %(user)s found": "Non se atoparon mensaxes recentes de %(user)s", + "Try scrolling up in the timeline to see if there are any earlier ones.": "Desprázate na cronoloxía para ver se hai algúns máis recentes.", + "Remove recent messages by %(user)s": "Eliminar mensaxes recentes de %(user)s", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "Vas eliminar %(count)s mensaxes de %(user)s. Esto non ten volta, ¿desexas continuar?", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "Vas eliminar unha mensaxe de %(user)s. Esto non ten volta, ¿desexas continuar?", + "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "Podería demorar un tempo se é un número grande de mensaxes. Non actualices o cliente mentras tanto.", + "Remove %(count)s messages|other": "Eliminar %(count)s mensaxes", + "Remove %(count)s messages|one": "Eliminar 1 mensaxe", + "Remove recent messages": "Eliminar mensaxes recentes", + "%(role)s in %(roomName)s": "%(role)s en %(roomName)s", + "Deactivate user?": "¿Desactivar usuaria?", + "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Ao desactivar esta usuaria ficará desconectada e non poderá voltar a conectar. Ademáis deixará todas as salas nas que estivese. Esta acción non ten volta, ¿desexas desactivar esta usuaria?", + "Deactivate user": "Desactivar usuaria", + "Failed to deactivate user": "Fallo ao desactivar a usuaria", + "This client does not support end-to-end encryption.": "Este cliente non soporta o cifrado extremo-a-extremo.", + "Security": "Seguridade", + "The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what Riot supports. Try with a different client.": "A sesión que intentas verificar non soporta a verificación por código QR ou por emoticonas, que é o que soporta Riot. Inténtao cun cliente diferente.", + "Verify by scanning": "Verificar escaneando", + "Ask %(displayName)s to scan your code:": "Pídelle a %(displayName)s que escanee o teu código:", + "If you can't scan the code above, verify by comparing unique emoji.": "Se non podes escanear o código superior, verifica comparando as emoticonas.", + "Verify by comparing unique emoji.": "Verficación por comparación de emoticonas.", + "Verify by emoji": "Verificar por emoticonas", + "Almost there! Is your other session showing the same shield?": "Case feito! ¿Podes ver as mesmas na túa outra sesión?", + "Almost there! Is %(displayName)s showing the same shield?": "Case feito! ¿está %(displayName)s mostrando as mesmas emoticonas?", + "Yes": "Si", + "Verify all users in a room to ensure it's secure.": "Verificar todas as usuarias da sala para asegurar que é segura." } From 45a6bf3a6d37b02898aa3220cb590a7bb746afe3 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Wed, 17 Jun 2020 18:04:14 +0000 Subject: [PATCH 111/194] Translated using Weblate (Hungarian) Currently translated at 100.0% (2287 of 2287 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index b88a71a30b..800966c093 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2514,5 +2514,6 @@ "Create a Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login you’ll need it to unlock your data.": "Készíts Visszaállítási kulcsot, hogy a fiók adatokkal tárolhasd a titkosítási kulcsokat és jelszavakat. Szükséged lesz rá hogy hozzáférj az adataidhoz ha elveszted a hozzáférésed ehhez a bejelentkezéshez.", "Create a Recovery Key": "Visszaállítási kulcs készítése", "Upgrade your Recovery Key": "A Visszaállítási kulcs fejlesztése", - "Store your Recovery Key": "Visszaállítási kulcs tárolása" + "Store your Recovery Key": "Visszaállítási kulcs tárolása", + "Use the improved room list (in development - will refresh to apply changes)": "Használd a fejlesztett szoba listát (fejlesztés alatt - a változások a frissítés után aktiválódnak)" } From 8f3a6fc30ecd35abe27cc959a554c3e7aa471918 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 18 Jun 2020 07:48:36 -0600 Subject: [PATCH 112/194] Consistent quotes --- src/components/structures/LeftPanel2.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index 242d0b46de..b5da44caef 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -108,7 +108,7 @@ export default class LeftPanel2 extends React.Component { header.classList.add("mx_RoomSublist2_headerContainer_sticky"); header.classList.add("mx_RoomSublist2_headerContainer_stickyBottom"); header.style.width = `${headerStickyWidth}px`; - header.style.top = "unset"; + header.style.top = `unset`; gotBottom = true; } else if (slRect.top < top) { header.classList.add("mx_RoomSublist2_headerContainer_sticky"); @@ -120,7 +120,7 @@ export default class LeftPanel2 extends React.Component { header.classList.remove("mx_RoomSublist2_headerContainer_stickyTop"); header.classList.remove("mx_RoomSublist2_headerContainer_stickyBottom"); header.style.width = `unset`; - header.style.top = "unset"; + header.style.top = `unset`; } } }; From 74174041bb9de8e0e2246db61e810e172d6c3ef8 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 18 Jun 2020 14:53:12 +0100 Subject: [PATCH 113/194] Remove semicolon in style --- src/components/views/elements/Slider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/Slider.tsx b/src/components/views/elements/Slider.tsx index 7c4b09568a..a88c581d07 100644 --- a/src/components/views/elements/Slider.tsx +++ b/src/components/views/elements/Slider.tsx @@ -86,7 +86,7 @@ export default class Slider extends React.Component { const offset = this.offset(this.props.values, this.props.value); selection =
          -
          +
          ; } From ba0bc8f29ccec1c2b7c71dd98f46c0bce317305b Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 18 Jun 2020 14:55:24 +0100 Subject: [PATCH 114/194] Resolve "The Great Conflict" --- .../room-list/algorithms/list-ordering/ImportanceAlgorithm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts index 6e09b0f8d3..15fa00c302 100644 --- a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts @@ -270,7 +270,7 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { // handling. For instance, if 45 rooms are removed from the middle of a 50 room list, the // index for the categories will be way off. - const nextOrderIndex = CATEGORY_ORDER.indexOf(category) + 1 + const nextOrderIndex = CATEGORY_ORDER.indexOf(category) + 1; if (n > 0) { for (let i = nextOrderIndex; i < CATEGORY_ORDER.length; i++) { const nextCategory = CATEGORY_ORDER[i]; From e4d824839ac782966fe61343707f45c1797f92b2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 18 Jun 2020 09:35:11 +0100 Subject: [PATCH 115/194] Revert "Use recovery keys over passphrases" --- .../structures/auth/_CompleteSecurity.scss | 4 - .../_CreateSecretStorageDialog.scss | 35 +- src/CrossSigningManager.js | 20 +- .../CreateSecretStorageDialog.js | 464 +++++++++++++----- .../structures/auth/CompleteSecurity.js | 4 - .../structures/auth/SetupEncryptionBody.js | 153 +----- .../keybackup/RestoreKeyBackupDialog.js | 2 +- .../AccessSecretStorageDialog.js | 9 +- .../views/settings/CrossSigningPanel.js | 2 +- src/i18n/strings/en_EN.json | 47 +- src/stores/SetupEncryptionStore.js | 64 +-- test/end-to-end-tests/src/usecases/signup.js | 23 +- 12 files changed, 400 insertions(+), 427 deletions(-) diff --git a/res/css/structures/auth/_CompleteSecurity.scss b/res/css/structures/auth/_CompleteSecurity.scss index b0462db477..f742be70e4 100644 --- a/res/css/structures/auth/_CompleteSecurity.scss +++ b/res/css/structures/auth/_CompleteSecurity.scss @@ -98,7 +98,3 @@ limitations under the License. } } } - -.mx_CompleteSecurity_resetText { - padding-top: 20px; -} diff --git a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss index 9f1d0f4998..63e5a3de09 100644 --- a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss +++ b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss @@ -73,42 +73,33 @@ limitations under the License. margin-left: 20px; } +.mx_CreateSecretStorageDialog_recoveryKeyHeader { + margin-bottom: 1em; +} + .mx_CreateSecretStorageDialog_recoveryKeyContainer { - width: 380px; - margin-left: auto; - margin-right: auto; + display: flex; } .mx_CreateSecretStorageDialog_recoveryKey { - font-weight: bold; - text-align: center; + width: 262px; padding: 20px; color: $info-plinth-fg-color; background-color: $info-plinth-bg-color; - border-radius: 6px; - word-spacing: 1em; - margin-bottom: 20px; + margin-right: 12px; } .mx_CreateSecretStorageDialog_recoveryKeyButtons { + flex: 1; display: flex; - justify-content: space-between; align-items: center; } .mx_CreateSecretStorageDialog_recoveryKeyButtons .mx_AccessibleButton { - width: 160px; - padding-left: 0px; - padding-right: 0px; + margin-right: 10px; +} + +.mx_CreateSecretStorageDialog_recoveryKeyButtons button { + flex: 1; white-space: nowrap; } - -.mx_CreateSecretStorageDialog_continueSpinner { - margin-top: 33px; - text-align: right; -} - -.mx_CreateSecretStorageDialog_continueSpinner img { - width: 20px; - height: 20px; -} diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index d40f820ac0..c37d0f8bf5 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -30,8 +30,6 @@ import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib"; // operation ends. let secretStorageKeys = {}; let secretStorageBeingAccessed = false; -// Stores the 'passphraseOnly' option for the active storage access operation -let passphraseOnlyOption = null; function isCachingAllowed() { return ( @@ -101,7 +99,6 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { const key = await inputToKey(input); return await MatrixClientPeg.get().checkSecretStorageKey(key, info); }, - passphraseOnly: passphraseOnlyOption, }, /* className= */ null, /* isPriorityModal= */ false, @@ -216,27 +213,19 @@ export async function promptForBackupPassphrase() { * * @param {Function} [func] An operation to perform once secret storage has been * bootstrapped. Optional. - * @param {object} [opts] Named options - * @param {bool} [opts.forceReset] Reset secret storage even if it's already set up - * @param {object} [opts.withKeys] Map of key ID to key for SSSS keys that the client - * already has available. If a key is not supplied here, the user will be prompted. - * @param {bool} [opts.passphraseOnly] If true, do not prompt for recovery key or to reset keys + * @param {bool} [forceReset] Reset secret storage even if it's already set up */ -export async function accessSecretStorage( - func = async () => { }, opts = {}, -) { +export async function accessSecretStorage(func = async () => { }, forceReset = false) { const cli = MatrixClientPeg.get(); secretStorageBeingAccessed = true; - passphraseOnlyOption = opts.passphraseOnly; - secretStorageKeys = Object.assign({}, opts.withKeys || {}); try { - if (!await cli.hasSecretStorageKey() || opts.forceReset) { + if (!await cli.hasSecretStorageKey() || forceReset) { // This dialog calls bootstrap itself after guiding the user through // passphrase creation. const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '', import("./async-components/views/dialogs/secretstorage/CreateSecretStorageDialog"), { - force: opts.forceReset, + force: forceReset, }, null, /* priority = */ false, /* static = */ true, ); @@ -274,6 +263,5 @@ export async function accessSecretStorage( if (!isCachingAllowed()) { secretStorageKeys = {}; } - passphraseOnlyOption = null; } } diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 192427d384..d7b79c2cfa 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -20,23 +20,25 @@ import PropTypes from 'prop-types'; import * as sdk from '../../../../index'; import {MatrixClientPeg} from '../../../../MatrixClientPeg'; import FileSaver from 'file-saver'; -import {_t} from '../../../../languageHandler'; +import {_t, _td} from '../../../../languageHandler'; import Modal from '../../../../Modal'; import { promptForBackupPassphrase } from '../../../../CrossSigningManager'; import {copyNode} from "../../../../utils/strings"; import {SSOAuthEntry} from "../../../../components/views/auth/InteractiveAuthEntryComponents"; -import AccessibleButton from "../../../../components/views/elements/AccessibleButton"; -import DialogButtons from "../../../../components/views/elements/DialogButtons"; -import InlineSpinner from "../../../../components/views/elements/InlineSpinner"; - +import PassphraseField from "../../../../components/views/auth/PassphraseField"; const PHASE_LOADING = 0; const PHASE_LOADERROR = 1; const PHASE_MIGRATE = 2; -const PHASE_INTRO = 3; -const PHASE_SHOWKEY = 4; -const PHASE_STORING = 5; -const PHASE_CONFIRM_SKIP = 6; +const PHASE_PASSPHRASE = 3; +const PHASE_PASSPHRASE_CONFIRM = 4; +const PHASE_SHOWKEY = 5; +const PHASE_KEEPITSAFE = 6; +const PHASE_STORING = 7; +const PHASE_DONE = 8; +const PHASE_CONFIRM_SKIP = 9; + +const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. /* * Walks the user through the process of creating a passphrase to guard Secure @@ -63,32 +65,34 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this.state = { phase: PHASE_LOADING, - downloaded: false, + passPhrase: '', + passPhraseValid: false, + passPhraseConfirm: '', copied: false, + downloaded: false, backupInfo: null, - backupInfoFetched: false, - backupInfoFetchError: null, backupSigStatus: null, // does the server offer a UI auth flow with just m.login.password - // for /keys/device_signing/upload? (If we have an account password, we - // assume that it can) + // for /keys/device_signing/upload? canUploadKeysWithPasswordOnly: null, - canUploadKeyCheckInProgress: false, accountPassword: props.accountPassword || "", accountPasswordCorrect: null, - // No toggle for this: if we really don't want one, remove it & just hard code true + // status of the key backup toggle switch useKeyBackup: true, }; - if (props.accountPassword) { - // If we have an account password, we assume we can upload keys with - // just a password (otherwise leave it as null so we poll to check) - this.state.canUploadKeysWithPasswordOnly = true; - } - this._passphraseField = createRef(); - this.loadData(); + this._fetchBackupInfo(); + if (this.state.accountPassword) { + // If we have an account password in memory, let's simplify and + // assume it means password auth is also supported for device + // signing key upload as well. This avoids hitting the server to + // test auth flows, which may be slow under high load. + this.state.canUploadKeysWithPasswordOnly = true; + } else { + this._queryKeyUploadAuth(); + } MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatusChange); } @@ -105,11 +109,13 @@ export default class CreateSecretStorageDialog extends React.PureComponent { MatrixClientPeg.get().isCryptoEnabled() && await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo) ); + const { force } = this.props; + const phase = (backupInfo && !force) ? PHASE_MIGRATE : PHASE_PASSPHRASE; + this.setState({ - backupInfoFetched: true, + phase, backupInfo, backupSigStatus, - backupInfoFetchError: null, }); return { @@ -117,25 +123,20 @@ export default class CreateSecretStorageDialog extends React.PureComponent { backupSigStatus, }; } catch (e) { - this.setState({backupInfoFetchError: e}); + this.setState({phase: PHASE_LOADERROR}); } } async _queryKeyUploadAuth() { try { - this.setState({canUploadKeyCheckInProgress: true}); await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {}); // We should never get here: the server should always require // UI auth to upload device signing keys. If we do, we upload // no keys which would be a no-op. console.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!"); - this.setState({canUploadKeyCheckInProgress: false}); } catch (error) { if (!error.data || !error.data.flows) { console.log("uploadDeviceSigningKeys advertised no flows!"); - this.setState({ - canUploadKeyCheckInProgress: false, - }); return; } const canUploadKeysWithPasswordOnly = error.data.flows.some(f => { @@ -143,18 +144,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent { }); this.setState({ canUploadKeysWithPasswordOnly, - canUploadKeyCheckInProgress: false, }); } } - async _createRecoveryKey() { - this._recoveryKey = await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(); - this.setState({ - phase: PHASE_SHOWKEY, - }); - } - _onKeyBackupStatusChange = () => { if (this.state.phase === PHASE_MIGRATE) this._fetchBackupInfo(); } @@ -163,6 +156,12 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this._recoveryKeyNode = n; } + _onUseKeyBackupChange = (enabled) => { + this.setState({ + useKeyBackup: enabled, + }); + } + _onMigrateFormSubmit = (e) => { e.preventDefault(); if (this.state.backupSigStatus.usable) { @@ -172,15 +171,12 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } } - _onIntroContinueClick = () => { - this._createRecoveryKey(); - } - _onCopyClick = () => { const successful = copyNode(this._recoveryKeyNode); if (successful) { this.setState({ copied: true, + phase: PHASE_KEEPITSAFE, }); } } @@ -190,8 +186,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent { type: 'text/plain;charset=us-ascii', }); FileSaver.saveAs(blob, 'recovery-key.txt'); + this.setState({ downloaded: true, + phase: PHASE_KEEPITSAFE, }); } @@ -247,9 +245,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { _bootstrapSecretStorage = async () => { this.setState({ - // we use LOADING here rather than STORING as STORING still shows the 'show key' - // screen which is not relevant: LOADING is just a generic spinner. - phase: PHASE_LOADING, + phase: PHASE_STORING, error: null, }); @@ -290,7 +286,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent { }, }); } - this.props.onFinished(true); + this.setState({ + phase: PHASE_DONE, + }); } catch (e) { if (this.state.canUploadKeysWithPasswordOnly && e.httpStatus === 401 && e.data.flows) { this.setState({ @@ -309,6 +307,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this.props.onFinished(false); } + _onDone = () => { + this.props.onFinished(true); + } + _restoreBackup = async () => { // It's possible we'll need the backup key later on for bootstrapping, // so let's stash it here, rather than prompting for it twice. @@ -335,41 +337,88 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } } - _onShowKeyContinueClick = () => { - this._bootstrapSecretStorage(); - } - _onLoadRetryClick = () => { - this.loadData(); - } - - async loadData() { this.setState({phase: PHASE_LOADING}); - const proms = []; - - if (!this.state.backupInfoFetched) proms.push(this._fetchBackupInfo()); - if (this.state.canUploadKeysWithPasswordOnly === null) proms.push(this._queryKeyUploadAuth()); - - await Promise.all(proms); - if (this.state.canUploadKeysWithPasswordOnly === null || this.state.backupInfoFetchError) { - this.setState({phase: PHASE_LOADERROR}); - } else if (this.state.backupInfo && !this.props.force) { - this.setState({phase: PHASE_MIGRATE}); - } else { - this.setState({phase: PHASE_INTRO}); - } + this._fetchBackupInfo(); } _onSkipSetupClick = () => { this.setState({phase: PHASE_CONFIRM_SKIP}); } - _onGoBackClick = () => { - if (this.state.backupInfo && !this.props.force) { - this.setState({phase: PHASE_MIGRATE}); - } else { - this.setState({phase: PHASE_INTRO}); + _onSetUpClick = () => { + this.setState({phase: PHASE_PASSPHRASE}); + } + + _onSkipPassPhraseClick = async () => { + this._recoveryKey = + await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(); + this.setState({ + copied: false, + downloaded: false, + phase: PHASE_SHOWKEY, + }); + } + + _onPassPhraseNextClick = async (e) => { + e.preventDefault(); + if (!this._passphraseField.current) return; // unmounting + + await this._passphraseField.current.validate({ allowEmpty: false }); + if (!this._passphraseField.current.state.valid) { + this._passphraseField.current.focus(); + this._passphraseField.current.validate({ allowEmpty: false, focused: true }); + return; } + + this.setState({phase: PHASE_PASSPHRASE_CONFIRM}); + }; + + _onPassPhraseConfirmNextClick = async (e) => { + e.preventDefault(); + + if (this.state.passPhrase !== this.state.passPhraseConfirm) return; + + this._recoveryKey = + await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(this.state.passPhrase); + this.setState({ + copied: false, + downloaded: false, + phase: PHASE_SHOWKEY, + }); + } + + _onSetAgainClick = () => { + this.setState({ + passPhrase: '', + passPhraseValid: false, + passPhraseConfirm: '', + phase: PHASE_PASSPHRASE, + }); + } + + _onKeepItSafeBackClick = () => { + this.setState({ + phase: PHASE_SHOWKEY, + }); + } + + _onPassPhraseValidate = (result) => { + this.setState({ + passPhraseValid: result.valid, + }); + }; + + _onPassPhraseChange = (e) => { + this.setState({ + passPhrase: e.target.value, + }); + } + + _onPassPhraseConfirmChange = (e) => { + this.setState({ + passPhraseConfirm: e.target.value, + }); } _onAccountPasswordChange = (e) => { @@ -384,14 +433,12 @@ export default class CreateSecretStorageDialog extends React.PureComponent { // Once we're confident enough in this (and it's supported enough) we can do // it automatically. // https://github.com/vector-im/riot-web/issues/11696 + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const Field = sdk.getComponent('views.elements.Field'); let authPrompt; let nextCaption = _t("Next"); - if (!this.state.backupSigStatus.usable) { - authPrompt = null; - nextCaption = _t("Upload"); - } else if (this.state.canUploadKeysWithPasswordOnly && !this.props.accountPassword) { + if (this.state.canUploadKeysWithPasswordOnly) { authPrompt =
          {_t("Enter your account password to confirm the upgrade:")}
          ; + } else if (!this.state.backupSigStatus.usable) { + authPrompt =
          +
          {_t("Restore your key backup to upgrade your encryption")}
          +
          ; + nextCaption = _t("Restore"); } else { authPrompt =

          {_t("You'll need to authenticate with the server to confirm the upgrade.")} @@ -411,9 +463,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent { return

          {_t( - "Upgrade your Recovery Key to store encryption keys & secrets " + - "with your account data. If you lose access to this login you'll " + - "need it to unlock your data.", + "Upgrade this session to allow it to verify other sessions, " + + "granting them access to encrypted messages and marking them " + + "as trusted for other users.", )}

          {authPrompt}
          ; } - _renderPhaseShowKey() { - let continueButton; - if (this.state.phase === PHASE_SHOWKEY) { - continueButton = +

          {_t( + "Set a recovery passphrase to secure encrypted information and recover it if you log out. " + + "This should be different to your account password:", + )}

          + +
          + +
          + + + + ; - } else { - continueButton =
          - -
          ; + disabled={!this.state.passPhraseValid} + > + +
          + +
          + {_t("Advanced")} + + {_t("Set up with a recovery key")} + +
          + ; + } + + _renderPhasePassPhraseConfirm() { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + const Field = sdk.getComponent('views.elements.Field'); + + let matchText; + let changeText; + if (this.state.passPhraseConfirm === this.state.passPhrase) { + matchText = _t("That matches!"); + changeText = _t("Use a different passphrase?"); + } else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) { + // only tell them they're wrong if they've actually gone wrong. + // Security concious readers will note that if you left riot-web unattended + // on this screen, this would make it easy for a malicious person to guess + // your passphrase one letter at a time, but they could get this faster by + // just opening the browser's developer tools and reading it. + // Note that not having typed anything at all will not hit this clause and + // fall through so empty box === no hint. + matchText = _t("That doesn't match."); + changeText = _t("Go back to set it again."); } + let passPhraseMatch = null; + if (matchText) { + passPhraseMatch =
          +
          {matchText}
          +
          + + {changeText} + +
          +
          ; + } + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return
          +

          {_t( + "Enter your recovery passphrase a second time to confirm it.", + )}

          +
          + +
          + {passPhraseMatch} +
          +
          + + + +
          ; + } + + _renderPhaseShowKey() { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); return

          {_t( - "Store your Recovery Key somewhere safe, it can be used to unlock your encrypted messages & data.", + "Your recovery key is a safety net - you can use it to restore " + + "access to your encrypted messages if you forget your recovery passphrase.", + )}

          +

          {_t( + "Keep a copy of it somewhere secure, like a password manager or even a safe.", )}

          +
          + {_t("Your recovery key")} +
          {this._recoveryKey.encodedPrivateKey}
          - - {_t("Download")} - - {_t("or")} - {this.state.copied ? _t("Copied!") : _t("Copy")} + {_t("Copy")} + + + {_t("Download")}
          - {continueButton} +
          ; + } + + _renderPhaseKeepItSafe() { + let introText; + if (this.state.copied) { + introText = _t( + "Your recovery key has been copied to your clipboard, paste it to:", + {}, {b: s => {s}}, + ); + } else if (this.state.downloaded) { + introText = _t( + "Your recovery key is in your Downloads folder.", + {}, {b: s => {s}}, + ); + } + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return
          + {introText} +
            +
          • {_t("Print it and store it somewhere safe", {}, {b: s => {s}})}
          • +
          • {_t("Save it on a USB key or backup drive", {}, {b: s => {s}})}
          • +
          • {_t("Copy it to your personal cloud storage", {}, {b: s => {s}})}
          • +
          + + +
          ; } @@ -483,6 +671,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } _renderPhaseLoadError() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return

          {_t("Unable to query secret storage status")}

          @@ -495,44 +684,29 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
          ; } - _renderPhaseIntro() { - let cancelButton; - if (this.props.force) { - // if this is a forced key reset then aborting will just leave the old keys - // in place, and is thereforece just 'cancel' - cancelButton = ; - } else { - // if it's setting up from scratch then aborting leaves the user without - // crypto set up, so they skipping the setup. - cancelButton = ; - } - + _renderPhaseDone() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return

          {_t( - "Create a Recovery Key to store encryption keys & secrets with your account data. " + - "If you lose access to this login you’ll need it to unlock your data.", + "You can now verify your other devices, " + + "and other users to keep your chats safe.", )}

          -
          - - {cancelButton} - -
          +
          ; } _renderPhaseSkipConfirm() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return
          {_t( "Without completing security on this session, it won’t have " + "access to encrypted messages.", )} @@ -542,15 +716,21 @@ export default class CreateSecretStorageDialog extends React.PureComponent { _titleForPhase(phase) { switch (phase) { - case PHASE_INTRO: - return _t('Create a Recovery Key'); case PHASE_MIGRATE: - return _t('Upgrade your Recovery Key'); + return _t('Upgrade your encryption'); + case PHASE_PASSPHRASE: + return _t('Set up encryption'); + case PHASE_PASSPHRASE_CONFIRM: + return _t('Confirm recovery passphrase'); case PHASE_CONFIRM_SKIP: return _t('Are you sure?'); case PHASE_SHOWKEY: + case PHASE_KEEPITSAFE: + return _t('Make a copy of your recovery key'); case PHASE_STORING: - return _t('Store your Recovery Key'); + return _t('Setting up keys'); + case PHASE_DONE: + return _t("You're done!"); default: return ''; } @@ -561,6 +741,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { let content; if (this.state.error) { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); content =

          {_t("Unable to set up secret storage")}

          @@ -579,16 +760,27 @@ export default class CreateSecretStorageDialog extends React.PureComponent { case PHASE_LOADERROR: content = this._renderPhaseLoadError(); break; - case PHASE_INTRO: - content = this._renderPhaseIntro(); - break; case PHASE_MIGRATE: content = this._renderPhaseMigrate(); break; + case PHASE_PASSPHRASE: + content = this._renderPhasePassPhrase(); + break; + case PHASE_PASSPHRASE_CONFIRM: + content = this._renderPhasePassPhraseConfirm(); + break; case PHASE_SHOWKEY: - case PHASE_STORING: content = this._renderPhaseShowKey(); break; + case PHASE_KEEPITSAFE: + content = this._renderPhaseKeepItSafe(); + break; + case PHASE_STORING: + content = this._renderBusyPhase(); + break; + case PHASE_DONE: + content = this._renderPhaseDone(); + break; case PHASE_CONFIRM_SKIP: content = this._renderPhaseSkipConfirm(); break; @@ -605,7 +797,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { onFinished={this.props.onFinished} title={this._titleForPhase(this.state.phase)} headerImage={headerImage} - hasCancel={this.props.hasCancel} + hasCancel={this.props.hasCancel && [PHASE_PASSPHRASE].includes(this.state.phase)} fixedWidth={false} >
          diff --git a/src/components/structures/auth/CompleteSecurity.js b/src/components/structures/auth/CompleteSecurity.js index e38ecd3eac..c73691611d 100644 --- a/src/components/structures/auth/CompleteSecurity.js +++ b/src/components/structures/auth/CompleteSecurity.js @@ -21,7 +21,6 @@ import * as sdk from '../../../index'; import { SetupEncryptionStore, PHASE_INTRO, - PHASE_RECOVERY_KEY, PHASE_BUSY, PHASE_DONE, PHASE_CONFIRM_SKIP, @@ -62,9 +61,6 @@ export default class CompleteSecurity extends React.Component { if (phase === PHASE_INTRO) { icon = ; title = _t("Verify this login"); - } else if (phase === PHASE_RECOVERY_KEY) { - icon = ; - title = _t("Recovery Key"); } else if (phase === PHASE_DONE) { icon = ; title = _t("Session verified"); diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.js index 7886ed26dd..26534c6e02 100644 --- a/src/components/structures/auth/SetupEncryptionBody.js +++ b/src/components/structures/auth/SetupEncryptionBody.js @@ -19,26 +19,15 @@ import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import * as sdk from '../../../index'; -import withValidation from '../../views/elements/Validation'; -import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; import { SetupEncryptionStore, PHASE_INTRO, - PHASE_RECOVERY_KEY, PHASE_BUSY, PHASE_DONE, PHASE_CONFIRM_SKIP, PHASE_FINISHED, } from '../../../stores/SetupEncryptionStore'; -function keyHasPassphrase(keyInfo) { - return ( - keyInfo.passphrase && - keyInfo.passphrase.salt && - keyInfo.passphrase.iterations - ); -} - export default class SetupEncryptionBody extends React.Component { static propTypes = { onFinished: PropTypes.func.isRequired, @@ -56,11 +45,6 @@ export default class SetupEncryptionBody extends React.Component { // Because of the latter, it lives in the state. verificationRequest: store.verificationRequest, backupInfo: store.backupInfo, - recoveryKey: '', - // whether the recovery key is a valid recovery key - recoveryKeyValid: null, - // whether the recovery key is the correct key or not - recoveryKeyCorrect: null, }; } @@ -83,19 +67,9 @@ export default class SetupEncryptionBody extends React.Component { store.stop(); } - _onResetClick = () => { + _onUsePassphraseClick = async () => { const store = SetupEncryptionStore.sharedInstance(); - store.startKeyReset(); - } - - _onUseRecoveryKeyClick = async () => { - const store = SetupEncryptionStore.sharedInstance(); - store.useRecoveryKey(); - } - - _onRecoveryKeyCancelClick() { - const store = SetupEncryptionStore.sharedInstance(); - store.cancelUseRecoveryKey(); + store.usePassPhrase(); } onSkipClick = () => { @@ -118,66 +92,6 @@ export default class SetupEncryptionBody extends React.Component { store.done(); } - _onUsePassphraseClick = () => { - const store = SetupEncryptionStore.sharedInstance(); - store.usePassPhrase(); - } - - _onRecoveryKeyChange = (e) => { - this.setState({recoveryKey: e.target.value}); - } - - _onRecoveryKeyValidate = async (fieldState) => { - const result = await this._validateRecoveryKey(fieldState); - this.setState({recoveryKeyValid: result.valid}); - return result; - } - - _validateRecoveryKey = withValidation({ - rules: [ - { - key: "required", - test: async (state) => { - try { - const decodedKey = decodeRecoveryKey(state.value); - const correct = await MatrixClientPeg.get().checkSecretStorageKey( - decodedKey, SetupEncryptionStore.sharedInstance().keyInfo, - ); - this.setState({ - recoveryKeyValid: true, - recoveryKeyCorrect: correct, - }); - return correct; - } catch (e) { - this.setState({ - recoveryKeyValid: false, - recoveryKeyCorrect: false, - }); - return false; - } - }, - invalid: function() { - if (this.state.recoveryKeyValid) { - return _t("This isn't the recovery key for your account"); - } else { - return _t("This isn't a valid recovery key"); - } - }, - valid: function() { - return _t("Looks good!"); - }, - }, - ], - }) - - _onRecoveryKeyFormSubmit = (e) => { - e.preventDefault(); - if (!this.state.recoveryKeyCorrect) return; - - const store = SetupEncryptionStore.sharedInstance(); - store.setupWithRecoveryKey(decodeRecoveryKey(this.state.recoveryKey)); - } - render() { const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); @@ -194,13 +108,6 @@ export default class SetupEncryptionBody extends React.Component { member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)} />; } else if (phase === PHASE_INTRO) { - const store = SetupEncryptionStore.sharedInstance(); - let recoveryKeyPrompt; - if (keyHasPassphrase(store.keyInfo)) { - recoveryKeyPrompt = _t("Use Recovery Key or Passphrase"); - } else { - recoveryKeyPrompt = _t("Use Recovery Key"); - } return (

          {_t( @@ -224,67 +131,15 @@ export default class SetupEncryptionBody extends React.Component {

          - - {recoveryKeyPrompt} + + {_t("Use Recovery Passphrase or Key")} {_t("Skip")}
          -
          {_t( - "If you've forgotten your recovery key you can " + - "", {}, { - button: sub => - {sub} - , - }, - )}
          ); - } else if (phase === PHASE_RECOVERY_KEY) { - const store = SetupEncryptionStore.sharedInstance(); - let keyPrompt; - if (keyHasPassphrase(store.keyInfo)) { - keyPrompt = _t( - "Enter your Recovery Key or enter a Recovery Passphrase to continue.", {}, - { - a: sub => {sub}, - }, - ); - } else { - keyPrompt = _t("Enter your Recovery Key to continue."); - } - - const Field = sdk.getComponent('elements.Field'); - return
          -

          {keyPrompt}

          -
          - -
          -
          - - {_t("Cancel")} - - - {_t("Continue")} - -
          -
          ; } else if (phase === PHASE_DONE) { let message; if (this.state.backupInfo) { diff --git a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js index 87ba6f7396..dd34dfbbf0 100644 --- a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js +++ b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js @@ -88,7 +88,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { _onResetRecoveryClick = () => { this.props.onFinished(false); - accessSecretStorage(() => {}, {forceReset: true}); + accessSecretStorage(() => {}, /* forceReset = */ true); } _onRecoveryKeyChange = (e) => { diff --git a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js index 43697f8ee7..e2ceadfbb9 100644 --- a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js +++ b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js @@ -32,9 +32,6 @@ export default class AccessSecretStorageDialog extends React.PureComponent { keyInfo: PropTypes.object.isRequired, // Function from one of { passphrase, recoveryKey } -> boolean checkPrivateKey: PropTypes.func.isRequired, - // If true, only prompt for a passphrase and do not offer to restore with - // a recovery key or reset keys. - passphraseOnly: PropTypes.bool, } constructor(props) { @@ -61,7 +58,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent { _onResetRecoveryClick = () => { // Re-enter the access flow, but resetting storage this time around. this.props.onFinished(false); - accessSecretStorage(() => {}, {forceReset: true}); + accessSecretStorage(() => {}, /* forceReset = */ true); } _onRecoveryKeyChange = (e) => { @@ -167,7 +164,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent { primaryDisabled={this.state.passPhrase.length === 0} /> - {this.props.passphraseOnly ? null : _t( + {_t( "If you've forgotten your recovery passphrase you can "+ "use your recovery key or " + "set up new recovery options." @@ -237,7 +234,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent { primaryDisabled={!this.state.recoveryKeyValid} /> - {this.props.passphraseOnly ? null : _t( + {_t( "If you've forgotten your recovery key you can "+ "." , {}, { diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js index f48ee3cd0d..7eb239cbca 100644 --- a/src/components/views/settings/CrossSigningPanel.js +++ b/src/components/views/settings/CrossSigningPanel.js @@ -113,7 +113,7 @@ export default class CrossSigningPanel extends React.PureComponent { _bootstrapSecureSecretStorage = async (forceReset=false) => { this.setState({ error: null }); try { - await accessSecretStorage(() => undefined, {forceReset}); + await accessSecretStorage(() => undefined, forceReset); } catch (e) { this.setState({ error: e }); console.error("Error bootstrapping secret storage", e); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5de33ada55..9a41517664 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2068,7 +2068,6 @@ "Account settings": "Account settings", "Could not load user profile": "Could not load user profile", "Verify this login": "Verify this login", - "Recovery Key": "Recovery Key", "Session verified": "Session verified", "Failed to send email": "Failed to send email", "The email address linked to your account must be entered.": "The email address linked to your account must be entered.", @@ -2122,16 +2121,10 @@ "You can now close this window or log in to your new account.": "You can now close this window or log in to your new account.", "Registration Successful": "Registration Successful", "Create your account": "Create your account", - "This isn't the recovery key for your account": "This isn't the recovery key for your account", - "This isn't a valid recovery key": "This isn't a valid recovery key", - "Looks good!": "Looks good!", - "Use Recovery Key or Passphrase": "Use Recovery Key or Passphrase", - "Use Recovery Key": "Use Recovery Key", "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.", "This requires the latest Riot on your other devices:": "This requires the latest Riot on your other devices:", "or another cross-signing capable Matrix client": "or another cross-signing capable Matrix client", - "Enter your Recovery Key or enter a Recovery Passphrase to continue.": "Enter your Recovery Key or enter a Recovery Passphrase to continue.", - "Enter your Recovery Key to continue.": "Enter your Recovery Key to continue.", + "Use Recovery Passphrase or Key": "Use Recovery Passphrase or Key", "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.", "Your new session is now verified. Other users will see it as trusted.": "Your new session is now verified. Other users will see it as trusted.", "Without completing security on this session, it won’t have access to encrypted messages.": "Without completing security on this session, it won’t have access to encrypted messages.", @@ -2175,43 +2168,47 @@ "Confirm encryption setup": "Confirm encryption setup", "Click the button below to confirm setting up encryption.": "Click the button below to confirm setting up encryption.", "Enter your account password to confirm the upgrade:": "Enter your account password to confirm the upgrade:", + "Restore your key backup to upgrade your encryption": "Restore your key backup to upgrade your encryption", + "Restore": "Restore", "You'll need to authenticate with the server to confirm the upgrade.": "You'll need to authenticate with the server to confirm the upgrade.", - "Upgrade your Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login you'll need it to unlock your data.": "Upgrade your Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login you'll need it to unlock your data.", - "Store your Recovery Key somewhere safe, it can be used to unlock your encrypted messages & data.": "Store your Recovery Key somewhere safe, it can be used to unlock your encrypted messages & data.", - "Download": "Download", - "Copy": "Copy", - "Unable to query secret storage status": "Unable to query secret storage status", - "Retry": "Retry", - "Create a Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login you’ll need it to unlock your data.": "Create a Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login you’ll need it to unlock your data.", - "Create a Recovery Key": "Create a Recovery Key", - "Upgrade your Recovery Key": "Upgrade your Recovery Key", - "Store your Recovery Key": "Store your Recovery Key", - "Unable to set up secret storage": "Unable to set up secret storage", - "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.": "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.", - "For maximum security, this should be different from your account password.": "For maximum security, this should be different from your account password.", + "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.", + "Set a recovery passphrase to secure encrypted information and recover it if you log out. This should be different to your account password:": "Set a recovery passphrase to secure encrypted information and recover it if you log out. This should be different to your account password:", "Enter a recovery passphrase": "Enter a recovery passphrase", "Great! This recovery passphrase looks strong enough.": "Great! This recovery passphrase looks strong enough.", + "Back up encrypted message keys": "Back up encrypted message keys", "Set up with a recovery key": "Set up with a recovery key", "That matches!": "That matches!", "Use a different passphrase?": "Use a different passphrase?", "That doesn't match.": "That doesn't match.", "Go back to set it again.": "Go back to set it again.", - "Please enter your recovery passphrase a second time to confirm.": "Please enter your recovery passphrase a second time to confirm.", - "Repeat your recovery passphrase...": "Repeat your recovery passphrase...", + "Enter your recovery passphrase a second time to confirm it.": "Enter your recovery passphrase a second time to confirm it.", + "Confirm your recovery passphrase": "Confirm your recovery passphrase", "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.": "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.", "Keep a copy of it somewhere secure, like a password manager or even a safe.": "Keep a copy of it somewhere secure, like a password manager or even a safe.", "Your recovery key": "Your recovery key", + "Copy": "Copy", + "Download": "Download", "Your recovery key has been copied to your clipboard, paste it to:": "Your recovery key has been copied to your clipboard, paste it to:", "Your recovery key is in your Downloads folder.": "Your recovery key is in your Downloads folder.", "Print it and store it somewhere safe": "Print it and store it somewhere safe", "Save it on a USB key or backup drive": "Save it on a USB key or backup drive", "Copy it to your personal cloud storage": "Copy it to your personal cloud storage", + "Unable to query secret storage status": "Unable to query secret storage status", + "Retry": "Retry", + "You can now verify your other devices, and other users to keep your chats safe.": "You can now verify your other devices, and other users to keep your chats safe.", + "Upgrade your encryption": "Upgrade your encryption", + "Confirm recovery passphrase": "Confirm recovery passphrase", + "Make a copy of your recovery key": "Make a copy of your recovery key", + "You're done!": "You're done!", + "Unable to set up secret storage": "Unable to set up secret storage", + "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.": "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.", + "For maximum security, this should be different from your account password.": "For maximum security, this should be different from your account password.", + "Please enter your recovery passphrase a second time to confirm.": "Please enter your recovery passphrase a second time to confirm.", + "Repeat your recovery passphrase...": "Repeat your recovery passphrase...", "Your keys are being backed up (the first backup could take a few minutes).": "Your keys are being backed up (the first backup could take a few minutes).", "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.": "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.", "Set up Secure Message Recovery": "Set up Secure Message Recovery", "Secure your backup with a recovery passphrase": "Secure your backup with a recovery passphrase", - "Confirm your recovery passphrase": "Confirm your recovery passphrase", - "Make a copy of your recovery key": "Make a copy of your recovery key", "Starting backup...": "Starting backup...", "Success!": "Success!", "Create key backup": "Create key backup", diff --git a/src/stores/SetupEncryptionStore.js b/src/stores/SetupEncryptionStore.js index cc64e24a03..ae1f998b02 100644 --- a/src/stores/SetupEncryptionStore.js +++ b/src/stores/SetupEncryptionStore.js @@ -20,11 +20,10 @@ import { accessSecretStorage, AccessCancelledError } from '../CrossSigningManage import { PHASE_DONE as VERIF_PHASE_DONE } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; export const PHASE_INTRO = 0; -export const PHASE_RECOVERY_KEY = 1; -export const PHASE_BUSY = 2; -export const PHASE_DONE = 3; //final done stage, but still showing UX -export const PHASE_CONFIRM_SKIP = 4; -export const PHASE_FINISHED = 5; //UX can be closed +export const PHASE_BUSY = 1; +export const PHASE_DONE = 2; //final done stage, but still showing UX +export const PHASE_CONFIRM_SKIP = 3; +export const PHASE_FINISHED = 4; //UX can be closed export class SetupEncryptionStore extends EventEmitter { static sharedInstance() { @@ -37,19 +36,11 @@ export class SetupEncryptionStore extends EventEmitter { return; } this._started = true; - this.phase = PHASE_BUSY; + this.phase = PHASE_INTRO; this.verificationRequest = null; this.backupInfo = null; - - // ID of the key that the secrets we want are encrypted with - this.keyId = null; - // Descriptor of the key that the secrets we want are encrypted with - this.keyInfo = null; - MatrixClientPeg.get().on("crypto.verification.request", this.onVerificationRequest); MatrixClientPeg.get().on('userTrustStatusChanged', this._onUserTrustStatusChanged); - - this.fetchKeyInfo(); } stop() { @@ -66,49 +57,7 @@ export class SetupEncryptionStore extends EventEmitter { } } - async fetchKeyInfo() { - const keys = await MatrixClientPeg.get().isSecretStored('m.cross_signing.master', false); - if (Object.keys(keys).length === 0) { - this.keyId = null; - this.keyInfo = null; - } else { - // If the secret is stored under more than one key, we just pick an arbitrary one - this.keyId = Object.keys(keys)[0]; - this.keyInfo = keys[this.keyId]; - } - - this.phase = PHASE_INTRO; - this.emit("update"); - } - - async startKeyReset() { - try { - await accessSecretStorage(() => {}, {forceReset: true}); - // If the keys are reset, the trust status event will fire and we'll change state - } catch (e) { - // dialog was cancelled - stay on the current screen - } - } - - async useRecoveryKey() { - this.phase = PHASE_RECOVERY_KEY; - this.emit("update"); - } - - cancelUseRecoveryKey() { - this.phase = PHASE_INTRO; - this.emit("update"); - } - - async setupWithRecoveryKey(recoveryKey) { - this.startTrustCheck({[this.keyId]: recoveryKey}); - } - async usePassPhrase() { - this.startTrustCheck(); - } - - async startTrustCheck(withKeys) { this.phase = PHASE_BUSY; this.emit("update"); const cli = MatrixClientPeg.get(); @@ -135,9 +84,6 @@ export class SetupEncryptionStore extends EventEmitter { // to advance before this. await cli.restoreKeyBackupWithSecretStorage(backupInfo); } - }, { - withKeys, - passphraseOnly: true, }).catch(reject); } catch (e) { console.error(e); diff --git a/test/end-to-end-tests/src/usecases/signup.js b/test/end-to-end-tests/src/usecases/signup.js index 2859aadbda..aa9f6b7efa 100644 --- a/test/end-to-end-tests/src/usecases/signup.js +++ b/test/end-to-end-tests/src/usecases/signup.js @@ -79,7 +79,20 @@ module.exports = async function signup(session, username, password, homeserver) const acceptButton = await session.query('.mx_InteractiveAuthEntryComponents_termsSubmit'); await acceptButton.click(); - const xsignContButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary'); + //plow through cross-signing setup by entering arbitrary details + //TODO: It's probably important for the tests to know the passphrase + const xsigningPassphrase = 'a7eaXcjpa9!Yl7#V^h$B^%dovHUVX'; // https://xkcd.com/221/ + let passphraseField = await session.query('.mx_CreateSecretStorageDialog_passPhraseField input'); + await session.replaceInputText(passphraseField, xsigningPassphrase); + await session.delay(1000); // give it a second to analyze our passphrase for security + let xsignContButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary'); + await xsignContButton.click(); + + //repeat passphrase entry + passphraseField = await session.query('.mx_CreateSecretStorageDialog_passPhraseField input'); + await session.replaceInputText(passphraseField, xsigningPassphrase); + await session.delay(1000); // give it a second to analyze our passphrase for security + xsignContButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary'); await xsignContButton.click(); //ignore the recovery key @@ -88,11 +101,13 @@ module.exports = async function signup(session, username, password, homeserver) await copyButton.click(); //acknowledge that we copied the recovery key to a safe place - const copyContinueButton = await session.query( - '.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary', - ); + const copyContinueButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_primary'); await copyContinueButton.click(); + //acknowledge that we're done cross-signing setup and our keys are safe + const doneOkButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_primary'); + await doneOkButton.click(); + //wait for registration to finish so the hash gets set //onhashchange better? From adfe1ac9bfdb5066c85a924e120fb7eff3a1f347 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 18 Jun 2020 15:13:28 +0100 Subject: [PATCH 116/194] Remove stray debug --- src/components/views/elements/MessagePreview.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/elements/MessagePreview.tsx b/src/components/views/elements/MessagePreview.tsx index 3be0a16781..5d65ac8977 100644 --- a/src/components/views/elements/MessagePreview.tsx +++ b/src/components/views/elements/MessagePreview.tsx @@ -62,7 +62,6 @@ export default class MessagePreview extends React.Component { // Fetch current user data const client = MatrixClientPeg.get() const userId = client.getUserId(); - console.log({userId}) const profileInfo = await client.getProfileInfo(userId); const avatar_url = Avatar.avatarUrlForUser( {avatarUrl: profileInfo.avatar_url}, From 997c7ffc96fd71e4be7ec01194481bfccc414b51 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 18 Jun 2020 15:26:53 +0100 Subject: [PATCH 117/194] Add missing semicolons --- src/components/views/elements/MessagePreview.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/views/elements/MessagePreview.tsx b/src/components/views/elements/MessagePreview.tsx index 5d65ac8977..95df8aa477 100644 --- a/src/components/views/elements/MessagePreview.tsx +++ b/src/components/views/elements/MessagePreview.tsx @@ -55,12 +55,12 @@ export default class MessagePreview extends React.Component { userId: "@erim:fink.fink", displayname: "Erimayas Fink", avatar_url: null, - } + }; } async componentDidMount() { // Fetch current user data - const client = MatrixClientPeg.get() + const client = MatrixClientPeg.get(); const userId = client.getUserId(); const profileInfo = await client.getProfileInfo(userId); const avatar_url = Avatar.avatarUrlForUser( @@ -97,7 +97,7 @@ export default class MessagePreview extends React.Component { }, "event_id": "$9999999999999999999999999999999999999999999", "room_id": "!999999999999999999:matrix.org" - }`)) + }`)); // Fake it more event.sender = { @@ -106,7 +106,7 @@ export default class MessagePreview extends React.Component { getAvatarUrl: (..._) => { return avatar_url; }, - } + }; } @@ -124,6 +124,6 @@ export default class MessagePreview extends React.Component { return
          -
          +
          ; } } From b9fb9aae4a6fedcf615a4b81ceb3765800f9b52a Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 18 Jun 2020 15:36:23 +0100 Subject: [PATCH 118/194] Rename MessagePreview to EventTilePreview --- .../elements/{MessagePreview.tsx => EventTilePreview.tsx} | 2 +- .../views/settings/tabs/user/AppearanceUserSettingsTab.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/components/views/elements/{MessagePreview.tsx => EventTilePreview.tsx} (97%) diff --git a/src/components/views/elements/MessagePreview.tsx b/src/components/views/elements/EventTilePreview.tsx similarity index 97% rename from src/components/views/elements/MessagePreview.tsx rename to src/components/views/elements/EventTilePreview.tsx index 95df8aa477..fa600196e5 100644 --- a/src/components/views/elements/MessagePreview.tsx +++ b/src/components/views/elements/EventTilePreview.tsx @@ -47,7 +47,7 @@ interface IState { const AVATAR_SIZE = 32; -export default class MessagePreview extends React.Component { +export default class EventTilePreview extends React.Component { constructor(props: IProps) { super(props); diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index d11f2cda9b..d2165e3762 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -32,7 +32,7 @@ import { IValidationResult, IFieldState } from '../../../elements/Validation'; import StyledRadioButton from '../../../elements/StyledRadioButton'; import StyledCheckbox from '../../../elements/StyledCheckbox'; import SettingsFlag from '../../../elements/SettingsFlag'; -import MessagePreview from '../../../elements/MessagePreview'; +import EventTilePreview from '../../../elements/EventTilePreview'; interface IProps { } @@ -281,7 +281,7 @@ export default class AppearanceUserSettingsTab extends React.Component {_t("Font size")} - Date: Thu, 18 Jun 2020 15:47:26 +0100 Subject: [PATCH 119/194] Fix merge conflicts --- .../tabs/user/AppearanceUserSettingsTab.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index c3538e5099..46723ec7cd 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -58,9 +58,9 @@ interface IState extends IThemeState { useIRCLayout: boolean; } -const MESSAGE_PREVIEW_TEXT = _t("Hey you. You're the best!"); export default class AppearanceUserSettingsTab extends React.Component { + private readonly MESSAGE_PREVIEW_TEXT = _t("Hey you. You're the best!"); private themeTimer: NodeJS.Timeout; @@ -212,7 +212,7 @@ export default class AppearanceUserSettingsTab extends React.Component{_t("Font size")}
          @@ -340,9 +340,9 @@ export default class AppearanceUserSettingsTab extends React.Component
          -
          -
          -
          - } +
          ; + }; render() { return ( From 4ffc54d143ae173c872d7aa2c40ea50321db22d1 Mon Sep 17 00:00:00 2001 From: Mike Pennisi Date: Thu, 18 Jun 2020 13:24:02 -0400 Subject: [PATCH 120/194] fixup! Extend QueryMatcher's sorting heuristic --- src/autocomplete/QueryMatcher.ts | 42 +++++++++++++++++--------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/src/autocomplete/QueryMatcher.ts b/src/autocomplete/QueryMatcher.ts index 4b8c1141fd..a11928c1dd 100644 --- a/src/autocomplete/QueryMatcher.ts +++ b/src/autocomplete/QueryMatcher.ts @@ -17,7 +17,6 @@ limitations under the License. */ import _at from 'lodash/at'; -import _flatMap from 'lodash/flatMap'; import _uniq from 'lodash/uniq'; function stripDiacritics(str: string): string { @@ -49,7 +48,7 @@ export default class QueryMatcher { private _options: IOptions; private _keys: IOptions["keys"]; private _funcs: Required["funcs"]>; - private _items: Map<{value: string, weight: number}, T[]>; + private _items: Map; constructor(objects: T[], options: IOptions = { keys: [] }) { this._options = options; @@ -87,14 +86,14 @@ export default class QueryMatcher { for (const [index, keyValue] of Object.entries(keyValues)) { if (!keyValue) continue; // skip falsy keyValues - const key = { - value: stripDiacritics(keyValue).toLowerCase(), - weight: Number(index) - }; + const key = stripDiacritics(keyValue).toLowerCase(); if (!this._items.has(key)) { this._items.set(key, []); } - this._items.get(key).push(object); + this._items.get(key).push({ + keyWeight: Number(index), + object, + }); } } } @@ -107,35 +106,40 @@ export default class QueryMatcher { if (query.length === 0) { return []; } - const results = []; + const matches = []; // Iterate through the map & check each key. // ES6 Map iteration order is defined to be insertion order, so results // here will come out in the order they were put in. - for (const key of this._items.keys()) { - let {value: resultKey} = key; + for (const [key, candidates] of this._items.entries()) { + let resultKey = key; if (this._options.shouldMatchWordsOnly) { resultKey = resultKey.replace(/[^\w]/g, ''); } const index = resultKey.indexOf(query); if (index !== -1 && (!this._options.shouldMatchPrefix || index === 0)) { - results.push({key, index}); + matches.push( + ...candidates.map((candidate) => ({key, index, ...candidate})) + ); } } - // Sort them by where the query appeared in the search key, then by + // Sort matches by where the query appeared in the search key, then by // where the matched key appeared in the provided array of keys. - const sortedResults = results.slice().sort((a, b) => { + matches.sort((a, b) => { if (a.index < b.index) { return -1; - } else if (a.index === b.index && a.key.weight < b.key.weight) { - return -1; + } else if (a.index === b.index) { + if (a.keyWeight < b.keyWeight) { + return -1; + } else if (a.keyWeight === b.keyWeight) { + return 0; + } } + return 1; }); - // Now map the keys to the result objects. Each result object is a list, so - // flatMap will flatten those lists out into a single list. Also remove any - // duplicates. - return _uniq(_flatMap(sortedResults, (candidate) => this._items.get(candidate.key))); + // Now map the keys to the result objects. Also remove any duplicates. + return _uniq(matches.map((match) => match.object)); } } From 2e0cb4746a818cc328b1d205bb1956c7f354ceb3 Mon Sep 17 00:00:00 2001 From: Mike Pennisi Date: Thu, 18 Jun 2020 14:20:40 -0400 Subject: [PATCH 121/194] fixup! Extend QueryMatcher's sorting heuristic --- src/autocomplete/QueryMatcher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/autocomplete/QueryMatcher.ts b/src/autocomplete/QueryMatcher.ts index a11928c1dd..7a0219e264 100644 --- a/src/autocomplete/QueryMatcher.ts +++ b/src/autocomplete/QueryMatcher.ts @@ -118,7 +118,7 @@ export default class QueryMatcher { const index = resultKey.indexOf(query); if (index !== -1 && (!this._options.shouldMatchPrefix || index === 0)) { matches.push( - ...candidates.map((candidate) => ({key, index, ...candidate})) + ...candidates.map((candidate) => ({index, ...candidate})) ); } } From a09779063c59f0d08053912d0acb706299bcba74 Mon Sep 17 00:00:00 2001 From: vicdorke Date: Thu, 18 Jun 2020 17:05:25 +0000 Subject: [PATCH 122/194] Translated using Weblate (Chinese (Simplified)) Currently translated at 60.5% (1382 of 2283 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hans/ --- src/i18n/strings/zh_Hans.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json index 481167a645..7eedfce238 100644 --- a/src/i18n/strings/zh_Hans.json +++ b/src/i18n/strings/zh_Hans.json @@ -1494,5 +1494,9 @@ "Cannot reach homeserver": "不可连接到主服务器", "Ensure you have a stable internet connection, or get in touch with the server admin": "确保您的网络连接稳定,或与服务器管理员联系", "Ask your Riot admin to check your config for incorrect or duplicate entries.": "跟您的Riot管理员确认您的配置不正确或重复的条目。", - "Cannot reach identity server": "不可连接到身份服务器" + "Cannot reach identity server": "不可连接到身份服务器", + "Room name or address": "房间名称或地址", + "Joins room with given address": "使用给定地址加入房间", + "Verify this login": "验证此登录名", + "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "通过从其他会话之一验证此登录名并授予其访问加密信息的权限来确认您的身份" } From 045def4566c14aa898e6e6d83b8f4461ef96aca6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 18 Jun 2020 22:45:42 +0100 Subject: [PATCH 123/194] hide search results from unknown rooms Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomView.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 4a0cc470d5..779db46c76 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1314,6 +1314,13 @@ export default createReactClass({ const mxEv = result.context.getEvent(); const roomId = mxEv.getRoomId(); const room = this.context.getRoom(roomId); + if (!room) { + // if we do not have the room in js-sdk stores then hide it as we cannot easily show it + // As per the spec, an all rooms search can create this condition, + // it happens with Seshat but not Synapse. + console.log("Hiding search result from an unknown room", roomId); + continue; + } if (!haveTileForEvent(mxEv)) { // XXX: can this ever happen? It will make the result count @@ -1322,13 +1329,12 @@ export default createReactClass({ } if (this.state.searchScope === 'All') { - if (roomId != lastRoomId) { - + if (roomId !== lastRoomId) { // XXX: if we've left the room, we might not know about // it. We should tell the js sdk to go and find out about // it. But that's not an issue currently, as synapse only // returns results for rooms we're joined to. - const roomName = room ? room.name : _t("Unknown room %(roomId)s", { roomId: roomId }); + const roomName = room.name; ret.push(
        • { _t("Room") }: { roomName }

          From 0e9ef8804def8f482efa9e5728aa6ae35dc6307e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 18 Jun 2020 15:46:37 -0600 Subject: [PATCH 124/194] Mark the new room list as ready for general testing --- src/settings/Settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 5e439a1d71..5715909da3 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -140,7 +140,7 @@ export const SETTINGS = { }, "feature_new_room_list": { isFeature: true, - displayName: _td("Use the improved room list (in development - will refresh to apply changes)"), + displayName: _td("Use the improved room list (will refresh to apply changes)"), supportedLevels: LEVELS_FEATURE, default: false, controller: new ReloadOnChangeController(), From 847f12c289a57bcb149585c1b7bcf0ead4e8c97c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 18 Jun 2020 15:47:30 -0600 Subject: [PATCH 125/194] Update i18n --- 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 b659979fde..ed79fc63b7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -433,7 +433,7 @@ "Render simple counters in room header": "Render simple counters in room header", "Multiple integration managers": "Multiple integration managers", "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", - "Use the improved room list (in development - will refresh to apply changes)": "Use the improved room list (in development - will refresh to apply changes)", + "Use the improved room list (will refresh to apply changes)": "Use the improved room list (will refresh to apply changes)", "Support adding custom themes": "Support adding custom themes", "Use IRC layout": "Use IRC layout", "Show info about bridges in room settings": "Show info about bridges in room settings", From eec42cff490c00c141def8d2cb04a714f79cd594 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 18 Jun 2020 22:49:39 +0100 Subject: [PATCH 126/194] tidy up Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomView.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 779db46c76..3051a9263f 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1318,6 +1318,7 @@ export default createReactClass({ // if we do not have the room in js-sdk stores then hide it as we cannot easily show it // As per the spec, an all rooms search can create this condition, // it happens with Seshat but not Synapse. + // It will make the result count not match the displayed count. console.log("Hiding search result from an unknown room", roomId); continue; } @@ -1330,14 +1331,8 @@ export default createReactClass({ if (this.state.searchScope === 'All') { if (roomId !== lastRoomId) { - // XXX: if we've left the room, we might not know about - // it. We should tell the js sdk to go and find out about - // it. But that's not an issue currently, as synapse only - // returns results for rooms we're joined to. - const roomName = room.name; - ret.push(
        • -

          { _t("Room") }: { roomName }

          +

          { _t("Room") }: { room.name }

        • ); lastRoomId = roomId; } From 3ac028565b531a8fd16b96d3ec7b4ef70c5c9334 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 18 Jun 2020 22:50:10 +0100 Subject: [PATCH 127/194] i18n Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/i18n/strings/en_EN.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b659979fde..8a00abbde4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2038,7 +2038,6 @@ "Search failed": "Search failed", "Server may be unavailable, overloaded, or search timed out :(": "Server may be unavailable, overloaded, or search timed out :(", "No more results": "No more results", - "Unknown room %(roomId)s": "Unknown room %(roomId)s", "Room": "Room", "Failed to reject invite": "Failed to reject invite", "You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.", From 7191c01265d4a9db87c92c6038d2d06aed3313df Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 18 Jun 2020 15:52:55 -0600 Subject: [PATCH 128/194] Fix crash when filtering new room list too fast Fixes https://github.com/vector-im/riot-web/issues/14092 We were simply assuming we had a reference to a notification state, which might not be the case if we're between renders. --- src/components/views/rooms/NotificationBadge.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx index af5a84ed92..b742f8e8e7 100644 --- a/src/components/views/rooms/NotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge.tsx @@ -240,6 +240,7 @@ export class ListNotificationState extends EventEmitter implements IDestroyable this.rooms = rooms; for (const oldRoom of diff.removed) { const state = this.states[oldRoom.roomId]; + if (!state) continue; // We likely just didn't have a badge (race condition) delete this.states[oldRoom.roomId]; state.off(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate); state.destroy(); From c690cfc6c59dab6c366c17a284261a68035fb10f Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 18 Jun 2020 21:57:33 -0400 Subject: [PATCH 129/194] mark messages with a black shield if the megolm session isn't trusted --- res/css/views/rooms/_EventTile.scss | 5 +++ src/components/views/rooms/E2EIcon.js | 1 + src/components/views/rooms/EventTile.js | 51 ++++++++++++++++++------- src/i18n/strings/en_EN.json | 1 + 4 files changed, 45 insertions(+), 13 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 40a80f17bb..f6cfd9e1d1 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -354,6 +354,11 @@ limitations under the License. opacity: 1; } +.mx_EventTile_e2eIcon_unauthenticated { + background-image: url('$(res)/img/e2e/normal.svg'); + opacity: 1; +} + .mx_EventTile_e2eIcon_hidden { display: none; } diff --git a/src/components/views/rooms/E2EIcon.js b/src/components/views/rooms/E2EIcon.js index bf65c7fb7c..254e28dffa 100644 --- a/src/components/views/rooms/E2EIcon.js +++ b/src/components/views/rooms/E2EIcon.js @@ -28,6 +28,7 @@ export const E2E_STATE = { WARNING: "warning", UNKNOWN: "unknown", NORMAL: "normal", + UNAUTHENTICATED: "unauthenticated", }; const crossSigningUserTitles = { diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 7508cf3372..88c4ed2e7d 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -313,35 +313,52 @@ export default createReactClass({ return; } - // If we directly trust the device, short-circuit here - const verified = await this.context.isEventSenderVerified(mxEvent); - if (verified) { + const encryptionInfo = this.context.getEventEncryptionInfo(mxEvent); + const senderId = mxEvent.getSender(); + const userTrust = this.context.checkUserTrust(senderId); + + if (encryptionInfo.mismatchedSender) { + // something definitely wrong is going on here this.setState({ - verified: E2E_STATE.VERIFIED, - }, () => { - // Decryption may have caused a change in size - this.props.onHeightChanged(); - }); + verified: E2E_STATE.WARNING, + }, this.props.onHeightChanged); // Decryption may have caused a change in size return; } - if (!this.context.checkUserTrust(mxEvent.getSender()).isCrossSigningVerified()) { + if (!userTrust.isCrossSigningVerified()) { + // user is not verified, so default to everything is normal this.setState({ verified: E2E_STATE.NORMAL, - }, this.props.onHeightChanged); + }, this.props.onHeightChanged); // Decryption may have caused a change in size return; } - const eventSenderTrust = await this.context.checkEventSenderTrust(mxEvent); + const eventSenderTrust = this.context.checkDeviceTrust( + senderId, encryptionInfo.sender.deviceId, + ); if (!eventSenderTrust) { this.setState({ verified: E2E_STATE.UNKNOWN, - }, this.props.onHeightChanged); // Decryption may have cause a change in size + }, this.props.onHeightChanged); // Decryption may have caused a change in size + return; + } + + if (!eventSenderTrust.isVerified()) { + this.setState({ + verified: E2E_STATE.WARNING, + }, this.props.onHeightChanged); // Decryption may have caused a change in size + return; + } + + if (!encryptionInfo.authenticated) { + this.setState({ + verified: E2E_STATE.UNAUTHENTICATED, + }, this.props.onHeightChanged); // Decryption may have caused a change in size return; } this.setState({ - verified: eventSenderTrust.isVerified() ? E2E_STATE.VERIFIED : E2E_STATE.WARNING, + verified: E2E_STATE.VERIFIED, }, this.props.onHeightChanged); // Decryption may have caused a change in size }, @@ -526,6 +543,8 @@ export default createReactClass({ return; // no icon if we've not even cross-signed the user } else if (this.state.verified === E2E_STATE.VERIFIED) { return; // no icon for verified + } else if (this.state.verified === E2E_STATE.UNAUTHENTICATED) { + return (); } else if (this.state.verified === E2E_STATE.UNKNOWN) { return (); } else { @@ -976,6 +995,12 @@ function E2ePadlockUnknown(props) { ); } +function E2ePadlockUnauthenticated(props) { + return ( + + ); +} + class E2ePadlock extends React.Component { static propTypes = { icon: PropTypes.string.isRequired, diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 396c3f9111..2dcca91e82 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1023,6 +1023,7 @@ "Encrypted by an unverified session": "Encrypted by an unverified session", "Unencrypted": "Unencrypted", "Encrypted by a deleted session": "Encrypted by a deleted session", + "The authenticity of this encrypted message can't be guaranteed on this device.": "The authenticity of this encrypted message can't be guaranteed on this device.", "Please select the destination room for this message": "Please select the destination room for this message", "Invite only": "Invite only", "Scroll to most recent messages": "Scroll to most recent messages", From 95dadca8e8a2624119447750ebda755f1356ceb0 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Fri, 19 Jun 2020 00:55:53 +0000 Subject: [PATCH 130/194] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (2282 of 2282 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index cc319f3e0d..c89c049ae5 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2526,5 +2526,6 @@ "Create a Recovery Key": "建立復原金鑰", "Upgrade your Recovery Key": "升級您的復原金鑰", "Store your Recovery Key": "儲存您的復原金鑰", - "Use the improved room list (in development - will refresh to apply changes)": "使用改進的聊天室清單(開發中 ── 將會重新整理以套用變更)" + "Use the improved room list (in development - will refresh to apply changes)": "使用改進的聊天室清單(開發中 ── 將會重新整理以套用變更)", + "Use the improved room list (will refresh to apply changes)": "使用改進的聊天室清單(將會重新整理以套用變更)" } From c7f72f0d00252d094e34648c18118c625a18d3c3 Mon Sep 17 00:00:00 2001 From: Victor Grousset Date: Fri, 19 Jun 2020 02:14:36 +0000 Subject: [PATCH 131/194] Translated using Weblate (Esperanto) Currently translated at 98.7% (2252 of 2282 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/eo/ --- src/i18n/strings/eo.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json index fa4fe703b6..11012a9045 100644 --- a/src/i18n/strings/eo.json +++ b/src/i18n/strings/eo.json @@ -2374,7 +2374,7 @@ "Signing In...": "Salutante…", "If you've joined lots of rooms, this might take a while": "Se vi aliĝis al multaj ĉambroj, tio povas daŭri longe", "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "Konfirmu vian identecon per kontrolo de ĉi tiu saluto el unu el viaj aliaj salutaĵoj, permesante al ĝi legadon de ĉifritaj mesaĝoj.", - "This requires the latest Riot on your other devices:": "Ĉi tio bezonas la plej freŝan version de Rion sur viaj aliaj aparatoj:", + "This requires the latest Riot on your other devices:": "Ĉi tio bezonas la plej freŝan version de Riot en viaj aliaj aparatoj:", "or another cross-signing capable Matrix client": "aŭ alian Matrix-klienton kapablan je transiraj subskriboj", "Use Recovery Passphrase or Key": "Uzi rehavajn pasfrazon aŭ ŝlosilon", "Great! This recovery passphrase looks strong enough.": "Bonege! Ĉi tiu rehava pasfrazo ŝajnas sufiĉe forta.", From 4948e49f763a3d954923734c810211a4c1179480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Fri, 19 Jun 2020 11:57:13 +0000 Subject: [PATCH 132/194] Translated using Weblate (French) Currently translated at 100.0% (2282 of 2282 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index ef5fc07b60..1651164a69 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2527,5 +2527,6 @@ "Create a Recovery Key": "Créer une clé de récupération", "Upgrade your Recovery Key": "Mettre à jour votre clé de récupération", "Store your Recovery Key": "Stocker votre clé de récupération", - "Use the improved room list (in development - will refresh to apply changes)": "Utiliser la liste de salons améliorée (en développement − actualisera pour appliquer les changements)" + "Use the improved room list (in development - will refresh to apply changes)": "Utiliser la liste de salons améliorée (en développement − actualisera pour appliquer les changements)", + "Use the improved room list (will refresh to apply changes)": "Utiliser la liste de salons améliorée (actualisera pour appliquer les changements)" } From db14729489428c14c01e76caa6ef7eb22895f35e Mon Sep 17 00:00:00 2001 From: XoseM Date: Fri, 19 Jun 2020 15:49:12 +0000 Subject: [PATCH 133/194] Translated using Weblate (Galician) Currently translated at 79.8% (1822 of 2282 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/gl/ --- src/i18n/strings/gl.json | 87 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 94d0f23b54..e84c002ea6 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -1813,5 +1813,90 @@ "Almost there! Is your other session showing the same shield?": "Case feito! ¿Podes ver as mesmas na túa outra sesión?", "Almost there! Is %(displayName)s showing the same shield?": "Case feito! ¿está %(displayName)s mostrando as mesmas emoticonas?", "Yes": "Si", - "Verify all users in a room to ensure it's secure.": "Verificar todas as usuarias da sala para asegurar que é segura." + "Verify all users in a room to ensure it's secure.": "Verificar todas as usuarias da sala para asegurar que é segura.", + "Use the improved room list (will refresh to apply changes)": "Usa a lista de salas mellorada (actualizará para aplicar)", + "Strikethrough": "Sobrescrito", + "In encrypted rooms, verify all users to ensure it’s secure.": "En salas cifradas, verifica todas as usuarias para asegurar que é segura.", + "You've successfully verified your device!": "Verificaches correctamente o teu dispositivo!", + "You've successfully verified %(deviceName)s (%(deviceId)s)!": "Verificaches correctamente %(deviceName)s (%(deviceId)s)!", + "You've successfully verified %(displayName)s!": "Verificaches correctamente a %(displayName)s!", + "Verified": "Verficiado", + "Got it": "Vale", + "Start verification again from the notification.": "Inicia o proceso de novo desde a notificación.", + "Start verification again from their profile.": "Inicia a verificación outra vez desde o seu perfil.", + "Verification timed out.": "Verificación caducada.", + "You cancelled verification on your other session.": "Cancelaches a verificación na túa outra sesión.", + "%(displayName)s cancelled verification.": "%(displayName)s cancelou a verificación.", + "You cancelled verification.": "Cancelaches a verificación.", + "Verification cancelled": "Verificación cancelada", + "Compare emoji": "Comparar emoticonas", + "Encryption enabled": "Cifrado activado", + "Encryption not enabled": "Cifrado desactivado", + "The encryption used by this room isn't supported.": "O cifrado desta sala non está soportado.", + "React": "Reacciona", + "Message Actions": "Accións da mensaxe", + "Show image": "Mostrar imaxe", + "You have ignored this user, so their message is hidden. Show anyways.": "Estás a ignorar a esta usuaria, polo que a imaxe está agochada. Mostrar igualmente.", + "You verified %(name)s": "Verificaches a %(name)s", + "You cancelled verifying %(name)s": "Cancelaches a verificación de %(name)s", + "%(name)s cancelled verifying": "%(name)s cancelou a verificación", + "You accepted": "Aceptaches", + "%(name)s accepted": "%(name)s aceptou", + "You declined": "Declinaches", + "You cancelled": "Cancelaches", + "%(name)s declined": "%(name)s declinou", + "%(name)s cancelled": "%(name)s cancelou", + "Accepting …": "Aceptando…", + "Declining …": "Declinando…", + "%(name)s wants to verify": "%(name)s desexa verificar", + "You sent a verification request": "Enviaches unha solicitude de verificación", + "Show all": "Mostrar todo", + "Reactions": "Reaccións", + " reacted with %(content)s": " reaccionaron con %(content)s", + "reacted with %(shortName)s": "reaccionaron con %(shortName)s", + "Message deleted": "Mensaxe eliminada", + "Message deleted by %(name)s": "Mensaxe eliminada por %(name)s", + "This room is a continuation of another conversation.": "Esta sala é continuación doutra conversa.", + "Click here to see older messages.": "Preme aquí para ver mensaxes antigas.", + "Edited at %(date)s. Click to view edits.": "Editada o %(date)s. Preme para ver edicións.", + "edited": "editada", + "Can't load this message": "Non se cargou a mensaxe", + "Submit logs": "Enviar rexistro", + "Failed to load group members": "Fallou a carga dos membros do grupo", + "Frequently Used": "Utilizado con frecuencia", + "Smileys & People": "Sorrisos e Persoas", + "Animals & Nature": "Animais e Natureza", + "Food & Drink": "Comida e Bebida", + "Activities": "Actividades", + "Travel & Places": "Viaxes e Lugares", + "Objects": "Obxectos", + "Symbols": "Símbolos", + "Flags": "Bandeiras", + "Categories": "Categorías", + "Quick Reactions": "Reaccións rápidas", + "Cancel search": "Cancelar busca", + "Any of the following data may be shared:": "Calquera do seguinte podería ser compartido:", + "Your display name": "Nome mostrado", + "Your avatar URL": "URL do avatar", + "Your user ID": "ID de usuaria", + "Riot URL": "URL Riot", + "Room ID": "ID da sala", + "Widget ID": "ID do widget", + "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "Ao utilizar este widget poderías compartir datos con %(widgetDomain)s e o teu Xestor de integracións.", + "Using this widget may share data with %(widgetDomain)s.": "Ao utilizar este widget poderías compartir datos con %(widgetDomain)s.", + "Widgets do not use message encryption.": "Os Widgets non usan cifrado de mensaxes.", + "Widget added by": "Widget engadido por", + "This widget may use cookies.": "Este widget podería usar cookies.", + "Maximize apps": "Maximizar apps", + "More options": "Máis opcións", + "Please create a new issue on GitHub so that we can investigate this bug.": "Por favor abre un novo informe en GitHub para poder investigar o problema.", + "Rotate Left": "Rotar á esquerda", + "Rotate counter-clockwise": "Rotar sentido contra-horario", + "Rotate Right": "Rotar á dereita", + "Rotate clockwise": "Rotar sentido horario", + "Language Dropdown": "Selector de idioma", + "%(severalUsers)smade no changes %(count)s times|other": "%(severalUsers)s non fixeron cambios %(count)s veces", + "%(severalUsers)smade no changes %(count)s times|one": "%(severalUsers)s non fixeron cambios", + "%(oneUser)smade no changes %(count)s times|other": "%(oneUser)s non fixo cambios %(count)s veces", + "%(oneUser)smade no changes %(count)s times|one": "%(oneUser)s non fixo cambios" } From e4521a4d81049caf33a180d4089f632d3c8e18c7 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Fri, 19 Jun 2020 09:40:15 +0000 Subject: [PATCH 134/194] Translated using Weblate (Hungarian) Currently translated at 100.0% (2282 of 2282 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 800966c093..ea830bc38e 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2515,5 +2515,6 @@ "Create a Recovery Key": "Visszaállítási kulcs készítése", "Upgrade your Recovery Key": "A Visszaállítási kulcs fejlesztése", "Store your Recovery Key": "Visszaállítási kulcs tárolása", - "Use the improved room list (in development - will refresh to apply changes)": "Használd a fejlesztett szoba listát (fejlesztés alatt - a változások a frissítés után aktiválódnak)" + "Use the improved room list (in development - will refresh to apply changes)": "Használd a fejlesztett szoba listát (fejlesztés alatt - a változások a frissítés után aktiválódnak)", + "Use the improved room list (will refresh to apply changes)": "Használd a fejlesztett szoba listát (a változások életbe lépéséhez újra fog tölteni)" } From a1cb070c60fdc6f05d87d29450e62623f06985ac Mon Sep 17 00:00:00 2001 From: random Date: Fri, 19 Jun 2020 09:14:21 +0000 Subject: [PATCH 135/194] Translated using Weblate (Italian) Currently translated at 99.9% (2281 of 2282 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/it/ --- src/i18n/strings/it.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 89039ec0c5..6f213de237 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -2521,5 +2521,6 @@ "Create a Recovery Key to store encryption keys & secrets with your account data. If you lose access to this login you’ll need it to unlock your data.": "Crea un chiave di recupero per memorizzare le chiavi di cifratura e i segreti con i dati del tuo account. Se perdi l'accesso a questo login, ti servirà per sbloccare i tuoi dati.", "Create a Recovery Key": "Crea una chiave di recupero", "Upgrade your Recovery Key": "Aggiorna la chiave di recupero", - "Store your Recovery Key": "Salva la chiave di recupero" + "Store your Recovery Key": "Salva la chiave di recupero", + "Use the improved room list (will refresh to apply changes)": "Usa l'elenco stanze migliorato (verrà ricaricato per applicare le modifiche)" } From a1592704a252959c2ef2d1a97409684111011d2e Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 19 Jun 2020 17:17:04 +0100 Subject: [PATCH 136/194] Unused code & import --- src/components/structures/auth/SetupEncryptionBody.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.js index edb4a7689d..f2e702c8cb 100644 --- a/src/components/structures/auth/SetupEncryptionBody.js +++ b/src/components/structures/auth/SetupEncryptionBody.js @@ -16,7 +16,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import { _t, _td } from '../../../languageHandler'; +import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import * as sdk from '../../../index'; import { @@ -125,18 +125,10 @@ export default class SetupEncryptionBody extends React.Component { } let useRecoveryKeyButton; - let resetKeysCaption; if (recoveryKeyPrompt) { useRecoveryKeyButton = {recoveryKeyPrompt} ; - resetKeysCaption = _td( - "If you've forgotten your recovery key you can ", - ); - } else { - resetKeysCaption = _td( - "If you have no other devices you can ", - ); } return ( From 6e4a2b7efe0131e5db99aeb160d5bd6ba839b3f2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 19 Jun 2020 17:22:53 +0100 Subject: [PATCH 137/194] i18n --- src/i18n/strings/en_EN.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 945aee6382..50de4e102f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2121,7 +2121,6 @@ "Create your account": "Create your account", "Use Recovery Key or Passphrase": "Use Recovery Key or Passphrase", "Use Recovery Key": "Use Recovery Key", - "If you have no other devices you can ": "If you have no other devices you can ", "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.", "This requires the latest Riot on your other devices:": "This requires the latest Riot on your other devices:", "or another cross-signing capable Matrix client": "or another cross-signing capable Matrix client", From d1caadec9f523d392e728367216841df8bc373f5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 19 Jun 2020 20:07:20 +0100 Subject: [PATCH 138/194] Add null check --- src/stores/SetupEncryptionStore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/SetupEncryptionStore.js b/src/stores/SetupEncryptionStore.js index 5427eeb88c..63b8c428eb 100644 --- a/src/stores/SetupEncryptionStore.js +++ b/src/stores/SetupEncryptionStore.js @@ -76,7 +76,7 @@ export class SetupEncryptionStore extends EventEmitter { async fetchKeyInfo() { const keys = await MatrixClientPeg.get().isSecretStored('m.cross_signing.master', false); - if (Object.keys(keys).length === 0) { + if (keys === null || Object.keys(keys).length === 0) { this.keyId = null; this.keyInfo = null; } else { From 565fd1277079530ce6ec25a3f1f95223d804d5a3 Mon Sep 17 00:00:00 2001 From: Slimane Selyan AMIRI Date: Sat, 20 Jun 2020 16:01:56 +0000 Subject: [PATCH 139/194] Added translation using Weblate (Kabyle) --- src/i18n/strings/kab.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/i18n/strings/kab.json diff --git a/src/i18n/strings/kab.json b/src/i18n/strings/kab.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/src/i18n/strings/kab.json @@ -0,0 +1 @@ +{} From a5fd441880054181b7b980c4bfb6cdf4eebc3c7c Mon Sep 17 00:00:00 2001 From: Slimane Selyan AMIRI Date: Sat, 20 Jun 2020 16:14:14 +0000 Subject: [PATCH 140/194] Translated using Weblate (Kabyle) Currently translated at 3.1% (70 of 2282 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/kab/ --- src/i18n/strings/kab.json | 298 +++++++++++++++++++++++++++++++++++++- 1 file changed, 297 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/kab.json b/src/i18n/strings/kab.json index 0967ef424b..af6a67e56b 100644 --- a/src/i18n/strings/kab.json +++ b/src/i18n/strings/kab.json @@ -1 +1,297 @@ -{} +{ + "Confirm": "Sentem", + "Analytics": "Analytics", + "Error": "Tuccḍa", + "Dismiss": "Agwi", + "OK": "IH", + "Permission Required": "Tasiregt tlaq", + "Continue": "Kemmel", + "Cancel": "Semmet", + "Sun": "Ace", + "Mon": "Ari", + "Tue": "Ara", + "Wed": "Aha", + "Thu": "Amh", + "Fri": "Sem", + "Sat": "Sed", + "Jan": "Yen", + "Feb": "Fuṛ", + "Mar": "Meɣ", + "Apr": "Yeb", + "May": "May", + "Jun": "Yun", + "Jul": "Yul", + "Aug": "Ɣuc", + "Sep": "Cte", + "Oct": "Tub", + "Nov": "Wam", + "Dec": "Duj", + "PM": "MD", + "AM": "FT", + "Trust": "Ittkel", + "Create Account": "Rnu amiḍan", + "Sign In": "Kcem", + "Default": "Amezwer", + "Moderator": "Aseɣyad", + "Admin": "Anedbal", + "You need to be logged in.": "Tesriḍ ad teqqneḍ.", + "Messages": "Iznan", + "Actions": "Tigawin", + "Advanced": "Talqayt", + "Other": "Nniḍen", + "Usage": "Aseqdec", + "Thank you!": "Tanemmirt!", + "Reason": "Taɣẓint", + "Someone": "Albaɛḍ", + "Light": "Aceɛlal", + "Dark": "Aberkan", + "Done": "Immed", + "Add another word or two. Uncommon words are better.": "Rnu awal-nniḍen neɣ sin. Awalen imexḍa ad lhun.", + "No": "Uhu", + "Review": "Senqed", + "Later": "Ticki", + "Notifications": "Ilɣa", + "Close": "Mdel", + "Warning": "Asmigel", + "Ok": "Ih", + "Set password": "Sbadu awal uffir", + "Upgrade": "Leqqem", + "Verify": "Senqed", + "What's New": "D acu-t umaynut", + "Update": "Leqqem", + "Restart": "Ales tanekra", + "Font size": "Tuɣzi n tsefsit", + "Custom font size": "Tiddi n tsefsit yugnen", + "Decline": "Agwi", + "Accept": "Qbel", + "or": "neɣ", + "Start": "Bdu", + "Cat": "Amcic", + "Lion": "Izem", + "Rabbit": "Awtul", + "Turtle": "Afekrun", + "Tree": "Aseklu", + "Clock": "Tamrint", + "Book": "Adlis", + "Lock": "Sekkeṛ", + "Key": "Tasarut", + "Telephone": "Tiliγri", + "Flag": "Anay", + "Bicycle": "Azlalam", + "Ball": "Balles", + "Anchor": "Tamdeyt", + "Headphones": "Casque", + "Folder": "Akaram", + "Upload": "Sali", + "Remove": "Sfeḍ", + "Show less": "Sken-d drus", + "Show more": "Sken-d ugar", + "Warning!": "Ɣur-k!", + "Current password": "Awal uffir amiran", + "Password": "Awal uffir", + "New Password": "Awal uffir amaynut", + "Confirm password": "Sentem awal uffir", + "Change Password": "Snifel Awal Uffir", + "not found": "ulac-it", + "Authentication": "Asesteb", + "ID": "ID", + "Manage": "Sefrek", + "Enable": "Rmed", + "Keywords": "Awalen tisura", + "Clear notifications": "Sfeḍ ilɣuyen", + "Off": "Insa", + "Display Name": "Mefffer isem", + "Save": "Sekles", + "Disconnect": "Yeffeɣ", + "Go back": "Uɣal ɣer deffir", + "Change": "Changer", + "Theme": "Asentel", + "Success": "Yedda", + "Profile": "Amaɣnu", + "Account": "Amiḍan", + "General": "Amatu", + "Legal": "Usḍif", + "Credits": "Asenmer", + "Chat with Riot Bot": "Asqerdec akked Riot Bot", + "FAQ": "Isteqsiyen FAQ", + "Keyboard Shortcuts": "Inegzumen n unasiw", + "Versions": "Ileqman", + "None": "Ula yiwen", + "Unsubscribe": "Se désabonner", + "Ignore": "Ttu", + "Subscribe": "Jerred", + "Preferences": "Tiwelhiwin", + "Composer": "Editeur", + "Timeline": "Amazray", + "Microphone": "Asawaḍ", + "Camera": "Takamiṛatt", + "Sounds": "Imeslan", + "Reset": "Ales awennez", + "Browse": "Snirem", + "Default role": "Tamlilt tamzwert", + "Permissions": "Tisirag", + "Anyone": "Yal yiwen", + "Encryption": "Awgelhen", + "Complete": "Yemmed", + "Share": "Bḍu", + "Verification code": "Tangalt n usenqed", + "Add": "Rnu", + "Email Address": "Tansa imayl", + "Phone Number": "Uṭṭun n tiliɣri", + "Upload file": "Azen afaylu", + "Bold": "Azuran", + "Strikethrough": "Jerreḍ", + "Quote": "Citation", + "Loading...": "Yessalay-ed…", + "Idle": "Idle", + "Unknown": "D arussin", + "Settings": "Iɣewwaren", + "Search": "Nadi", + "Favourites": "Ismenyifen", + "People": "Imdanen", + "Sign Up": "Jerred", + "Reject": "Aggi", + "Not now": "Mačči tura", + "Sort by": "Smizzwer s", + "Activity": "Armud", + "A-Z": "A-Z", + "Show": "Sken", + "Options": "Tinefrunin", + "Server error": "Tuccḍa n uqeddac", + "Members": "Imedrawen", + "Files": "Ifuyla", + "Trusted": "De confiance", + "Invite": "Nced…", + "Unmute": "Susem", + "Mute": "Kkes imesli", + "Are you sure?": "Tebɣiḍ ?", + "Security": "Taɣellist", + "Yes": "Ih", + "Verified": "Verified", + "Got it": "Awi-t", + "Sunday": "Acer", + "Monday": "Arim", + "Tuesday": "Aram", + "Wednesday": "Ahad", + "Thursday": "Amhad", + "Friday": "Sem", + "Saturday": "Sed", + "Today": "Ass-a", + "Yesterday": "Iḍelli", + "View Source": "Sken aɣbalu", + "Reply": "Err", + "Edit": "Ẓreg", + "Attachment": "Attachement", + "Error decrypting attachment": "Tuccḍa deg uzmak n ufaylu yeddan", + "Show all": "Sken akk", + "Message deleted": "Izen yettwakkes", + "Copied!": "Yettusukken!", + "edited": "yeẓreg", + "Food & Drink": "Učči aked tissit", + "Objects": "Tiɣawsiwin", + "Symbols": "Izamulen", + "Flags": "Anayen", + "Categories": "Taggayin", + "More options": "Ugar n textirin", + "Join": "Semlil", + "No results": "Ulac igmad", + "collapse": "sneḍfes", + "Rotate counter-clockwise": "Zzi di tnila tanemgalt n tsegnatin n temrilt", + "Rotate clockwise": "Zzi di tnila n tsegnatin n temrilt", + "Add User": "Rnu aseqdac", + "Server name": "Isem n uqeddac", + "email address": "tansa imayl", + "Close dialog": "Mdel tanaka n usdiwen", + "Notes": "Tamawt", + "Unavailable": "Ulac-it", + "Changelog": "Aɣmis n ibeddilen", + "Removing…": "Tukksa…", + "Confirm Removal": "Serggeg tukksa", + "Example": "Amedya", + "example": "amedya", + "Create": "Snulfu-d", + "Name": "Isem", + "Sign out": "Ffeɣ", + "Back": "Retour", + "Send": "Azen", + "Suggestions": "Isumar", + "Go": "Ddu", + "Session name": "Nom de session", + "Send report": "Azen aneqqis", + "Refresh": "Sismeḍ", + "Email address": "Tansa email", + "Skip": "Zgel", + "Username not available": "Ulac isem n useqdac", + "Checking...": "Asenqed...", + "Username available": "Yella yisem n useqdac", + "Terms of Service": "Tiwtilin n useqdec", + "Service": "Ameẓlu", + "Summary": "Agzul", + "Document": "isemli", + "Next": "Ar zdat", + "Upload files": "Azen ifuyla", + "Appearance": "Udem", + "Allow": "Sireg", + "Deny": "Agwi", + "Custom": "Personnalisé", + "Source URL": "URL aγbalu", + "Notification settings": "Iɣewwaṛen n yilɣa", + "Leave": "Ffeɣ", + "Set status": "Sbadu addad", + "Hide": "Ffer", + "Home": "Agejdan", + "Sign in": "Qqen", + "Help": "Tallelt", + "Reload": "Smiren", + "powered by Matrix": "S lmendad n Matrix", + "Custom Server Options": "Iɣewwaren n uqeddac udmawan", + "Code": "Tangalt", + "Submit": "Azen", + "Email": "Imayl", + "Username": "Isem n useqdac", + "Phone": "Tiliɣri", + "Passwords don't match": "Awal uffiren ur menṭaḍen ara", + "Email (optional)": "Imayl (Afrayan)", + "Register": "Jerred", + "Free": "Ilelli", + "Failed to upload image": "Ur yezmir ad yessali tugna", + "Description": "Aseglem", + "Explore": "Snirem", + "Filter": "Imsizdeg", + "Explore rooms": "Snirem tixxamin", + "Unknown error": "Erreur inconnue", + "Logout": "Tufɣa", + "Preview": "Timeẓriwt", + "View": "Ɣeṛ", + "Guest": "Inebgi", + "Your profile": "Amaɣnu-ik", + "Feedback": "Tikti", + "Your password has been reset.": "Awal n uɛeddi inek yules awennez.", + "Syncing...": "Amtawi", + "Create account": "Rnu amiḍan", + "Create your account": "Rnu amiḍan-ik", + "Go Back": "Précédent", + "Commands": "Tiludna", + "Users": "Iseqdacen", + "Export": "Sifeḍ", + "Import": "Kter", + "Restore": "Err-d", + "Copy": "Nγel", + "Download": "Sider", + "Retry": "Ɛreḍ tikkelt nniḍen", + "Starting backup...": "Asenker n uḥraz...", + "Success!": "Akka d rrbeḥ !", + "Disable": "Désactiver", + "Navigation": "Tunigin", + "Calls": "Appels", + "Alt": "Alt", + "Shift": "Shift", + "New line": "Izirig amaynut", + "Upload a file": "Azen afaylu", + "Page Up": "Page précédente", + "Page Down": "Page suivante", + "Esc": "Échap", + "Enter": "Entrée", + "Space": "Tallunt", + "End": "Tagara" +} From 0c2f5b81cb36df057b1c7833425baf53a94ef3d7 Mon Sep 17 00:00:00 2001 From: ziriSut Date: Sun, 21 Jun 2020 05:07:57 +0000 Subject: [PATCH 141/194] Translated using Weblate (Kabyle) Currently translated at 3.1% (70 of 2282 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/kab/ --- src/i18n/strings/kab.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/kab.json b/src/i18n/strings/kab.json index af6a67e56b..12eebe45df 100644 --- a/src/i18n/strings/kab.json +++ b/src/i18n/strings/kab.json @@ -293,5 +293,10 @@ "Esc": "Échap", "Enter": "Entrée", "Space": "Tallunt", - "End": "Tagara" + "End": "Tagara", + "This email address is already in use": "Tansa-agi n yimayl tettuseqdac yakan", + "This phone number is already in use": "Uṭṭun-agi n tilifun yettuseqddac yakan", + "Your Riot is misconfigured": "Riot inek(inem) ur ittusbadu ara", + "Please install Chrome, Firefox, or Safari for the best experience.": "Ma ulac aɣilif, sebded Chrome, Firefox, neɣSafari i tirmit igerrzen.", + "I understand the risks and wish to continue": "Gziɣ ayen ara d-yeḍrun maca bɣiɣ ad kemmleɣ" } From 7dfbdb73c50f4519d707e9d13cd2a8bfa879d68b Mon Sep 17 00:00:00 2001 From: ziriSut Date: Sun, 21 Jun 2020 05:18:41 +0000 Subject: [PATCH 142/194] Translated using Weblate (Kabyle) Currently translated at 5.0% (113 of 2282 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/kab/ --- src/i18n/strings/kab.json | 54 +++++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/src/i18n/strings/kab.json b/src/i18n/strings/kab.json index 12eebe45df..9ba9935bb8 100644 --- a/src/i18n/strings/kab.json +++ b/src/i18n/strings/kab.json @@ -1,6 +1,6 @@ { "Confirm": "Sentem", - "Analytics": "Analytics", + "Analytics": "Tiselḍin", "Error": "Tuccḍa", "Dismiss": "Agwi", "OK": "IH", @@ -287,16 +287,54 @@ "Alt": "Alt", "Shift": "Shift", "New line": "Izirig amaynut", - "Upload a file": "Azen afaylu", - "Page Up": "Page précédente", - "Page Down": "Page suivante", - "Esc": "Échap", - "Enter": "Entrée", + "Upload a file": "Sali-d afaylu", + "Page Up": "Asebter afellay", + "Page Down": "Asebter adday", + "Esc": "Senser", + "Enter": "Anekcum", "Space": "Tallunt", - "End": "Tagara", + "End": "Taggara", "This email address is already in use": "Tansa-agi n yimayl tettuseqdac yakan", "This phone number is already in use": "Uṭṭun-agi n tilifun yettuseqddac yakan", "Your Riot is misconfigured": "Riot inek(inem) ur ittusbadu ara", "Please install Chrome, Firefox, or Safari for the best experience.": "Ma ulac aɣilif, sebded Chrome, Firefox, neɣSafari i tirmit igerrzen.", - "I understand the risks and wish to continue": "Gziɣ ayen ara d-yeḍrun maca bɣiɣ ad kemmleɣ" + "I understand the risks and wish to continue": "Gziɣ ayen ara d-yeḍrun maca bɣiɣ ad kemmleɣ", + "Use Single Sign On to continue": "Seqdec anekcum asuf akken ad tkemmleḍ", + "Confirm adding this email address by using Single Sign On to prove your identity.": "Sentem timerna n tansa-a n yimayl s useqdec n unekcum asuf i ubeggen n timagit-in(im).", + "Single Sign On": "Anekcum asuf", + "Confirm adding email": "Sentem timerna n yimayl", + "Click the button below to confirm adding this email address.": "Sit ɣef tqeffalt yellan ddaw i usentem n tmerna n tansa-a n yimayl.", + "Add Email Address": "Rnu tansa n yimayl", + "Failed to verify email address: make sure you clicked the link in the email": "Asenqed n tansa n yimayl ur yeddi ara: wali ma yella tsateḍ ɣef useɣwen yellan deg yimayl", + "Confirm adding this phone number by using Single Sign On to prove your identity.": "Sentem timerna n wuṭṭun n tilifun s useqdec n unekcum asuf i ubeggen n timagit-ik(im).", + "Confirm adding phone number": "Sentem timerna n wuṭṭun n tilifun", + "Click the button below to confirm adding this phone number.": "Sit ɣef tqeffalt yellan ddaw i usentem n tmerna n wuṭṭun-a n tilifun.", + "Add Phone Number": "Rnu uṭṭun n tilifun", + "The platform you're on": "Tiɣerɣert ideg telliḍ akka tura", + "The version of Riot": "Lqem n Riot", + "Whether or not you're logged in (we don't record your username)": "Ma teqqneḍ neɣ uhu (isem-ik(im) n useqdac ur yettwasekles ara)", + "Your language of choice": "Tutlayt i tferneḍ", + "Which officially provided instance you are using, if any": "Tummant i d-yettunefken tunṣibt ara tesseqdace, ma yella tella", + "Your homeserver's URL": "URL n uqeddac-ik(im) agejdan", + "e.g. %(exampleValue)s": "e.g. %(exampleValue)s", + "Every page you use in the app": "Isebtar akk i tesseqdaceḍ deg usnas", + "e.g. ": "e.g. ", + "Your user agent": "Ameggi-ik(im) aseqdac", + "Your device resolution": "Afray n yiben-ik(im)", + "Jump to oldest unread message": "Uɣal alamma d izen aqdim ur nettwaɣra ara", + "Jump to room search": "Ɛeddi ɣer unadi n texxamt", + "Navigate up/down in the room list": "Inig s afellay/adday deg tebdert n texxamin", + "Select room from the room list": "Fren taxxamt seg tebdert n texxamin", + "Collapse room list section": "Fneẓ tigemi n tebdert n texxamin", + "Expand room list section": "Snerni tigezmi n tebdert n texxamin", + "Clear room list filter field": "Kkes urti n usizdeg n tebdert n texxamin", + "Previous/next unread room or DM": "Taxxamt neɣ izen uzrid ur yettwaɣra send/seld", + "Previous/next room or DM": "Taxxamt neɣ izen usrid send/seld", + "Toggle the top left menu": "Sken/ffer umuɣ aεlayan azelmaḍ", + "Close dialog or context menu": "Mdel umuɣ n udiwenni neɣ n ugbur", + "Activate selected button": "Rmed taqeffalt i d-yettwafernen", + "Toggle right panel": "Sken/ffer agalis ayeffus", + "Toggle this dialog": "Sken/ffer adiwanni-a", + "Move autocomplete selection up/down": "Err tafrant n tacart tawurmant afellay/adday", + "Cancel autocomplete": "Sefsex tacaṛt tawurmant" } From 6f9c6ffd5f14309040a4034f649ea15026568007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Sat, 20 Jun 2020 18:47:14 +0000 Subject: [PATCH 143/194] Translated using Weblate (Estonian) Currently translated at 66.3% (1512 of 2282 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/et/ --- src/i18n/strings/et.json | 121 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 117 insertions(+), 4 deletions(-) diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index ef41233254..70eec98ec5 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -129,7 +129,7 @@ "Matrix rooms": "Matrix'i jututoad", "Explore Room State": "Uuri jututoa olekut", "Explore Account Data": "Uuri konto andmeid", - "Private Chat": "Privaatne vestlus", + "Private Chat": "Omavaheline privaatne vestlus", "Public Chat": "Avalik vestlus", "Other users can invite you to rooms using your contact details": "Teades sinu kontaktinfot võivad teised kutsuda sind osalema jututubades", "Add rooms to the community summary": "Lisa jututoad kogukonna ülevaatesse", @@ -147,7 +147,7 @@ "Indexed rooms:": "Indekseeritud jututoad:", "Remove": "Eemalda", "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "Sa peaksid enne ühenduse katkestamisst eemaldama isiklikud andmed id-serverist . Kahjuks id-server ei ole hetkel võrgus või pole kättesaadav.", - "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "Me soovitame, et eemaldad enne ühenduse katkestamist oma e-posti aadressi ja telefoninumbrid id-serverist.", + "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "Me soovitame, et eemaldad enne ühenduse katkestamist oma e-posti aadressi ja telefoninumbrid isikutuvastusserverist.", "Remove messages": "Kustuta sõnumeid", "Unable to remove contact information": "Kontaktiinfo eemaldamine ebaõnnestus", "Remove %(email)s?": "Eemalda %(email)s?", @@ -515,7 +515,7 @@ "Displays information about a user": "Näitab teavet kasutaja kohta", "This homeserver has hit its Monthly Active User limit.": "See koduserver on saavutanud igakuise aktiivsete kasutajate piiri.", "about a minute ago": "umbes minut tagasi", - "about an hour ago": "umbes tund tagasi", + "about an hour ago": "umbes tund aega tagasi", "%(num)s hours ago": "%(num)s tundi tagasi", "about a day ago": "umbes päev tagasi", "%(num)s days ago": "%(num)s päeva tagasi", @@ -1479,5 +1479,118 @@ "Enter recovery key": "Sisesta taastevõti", "Unable to access secret storage. Please verify that you entered the correct recovery key.": "Ei õnnestu lugeda krüptitud salvestusruumi. Palun kontrolli, kas sa sisestasid õige taastevõtme.", "This looks like a valid recovery key!": "See tundub olema õige taastevõti!", - "Not a valid recovery key": "Ei ole sobilik taastevõti" + "Not a valid recovery key": "Ei ole sobilik taastevõti", + "Ask your Riot admin to check your config for incorrect or duplicate entries.": "Palu, et sinu Riot'u haldur kontrolliks sinu seadistusi võimalike vigaste või topeltkirjete osas.", + "Cannot reach identity server": "Isikutuvastusserverit ei õnnestu leida", + "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Sa võid registreeruda, kuid mõned funktsionaalsused pole kasutatavad seni, kuni isikutuvastusserver pole uuesti võrgus. Kui see teade tekib järjepanu, siis palun kontrolli oma seadistusi või võta ühendust serveri haldajaga.", + "You can reset your password, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Sa võid salasõna lähtestada, kuid mõned funktsionaalsused pole kasutatavad seni, kuni isikutuvastusserver pole uuesti võrgus. Kui see teade tekib järjepanu, siis palun kontrolli oma seadistusi või võta ühendust serveri haldajaga.", + "You can log in, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Sa võid sisse logida, kuid mõned funktsionaalsused pole kasutatavad seni, kuni isikutuvastusserver pole uuesti võrgus. Kui see teade tekib järjepanu, siis palun kontrolli oma seadistusi või võta ühendust serveri haldajaga.", + "No homeserver URL provided": "Koduserveri aadress on puudu", + "Unexpected error resolving homeserver configuration": "Koduserveri seadistustest selguse saamisel tekkis ootamatu viga", + "Unexpected error resolving identity server configuration": "Isikutuvastusserveri seadistustest selguse saamisel tekkis ootamatu viga", + "The message you are trying to send is too large.": "Sõnum, mida sa proovid saata, on liiga suur.", + "This homeserver has exceeded one of its resource limits.": "See koduserver ületanud ühe oma ressursipiirangutest.", + "Please contact your service administrator to continue using the service.": "Jätkamaks selle teenuse kasutamist, palun võta ühendust oma serveri haldajaga.", + "Unable to connect to Homeserver. Retrying...": "Ei saa ühendust koduserveriga. Proovin uuesti...", + "%(items)s and %(count)s others|other": "%(items)s ja %(count)s muud", + "%(items)s and %(count)s others|one": "%(items)s ja üks muu", + "%(items)s and %(lastItem)s": "%(items)s ja %(lastItem)s", + "a few seconds ago": "mõni sekund tagasi", + "%(num)s minutes ago": "%(num)s minutit tagasi", + "a few seconds from now": "mõne sekundi pärast", + "%(num)s minutes from now": "%(num)s minuti pärast", + "Use a few words, avoid common phrases": "Kasuta paari sõna, kuid väldi levinud fraase", + "No need for symbols, digits, or uppercase letters": "Sa ei pea sisestama erilisi tähemärke, numbreid ega suurtähti", + "Use a longer keyboard pattern with more turns": "Kasuta pikemaid klahvikombinatsioone, kus vajutatud klahvid pole kõrvuti ega kohakuti", + "Avoid repeated words and characters": "Väldi korduvaid sõnu ja tähemärke", + "Avoid sequences": "Väldi korduvaid klahviseeriaid", + "Avoid recent years": "Väldi hiljutisi aastaid", + "Avoid years that are associated with you": "Väldi aastaid, mida saaks sinuga seostada", + "Avoid dates and years that are associated with you": "Väldi kuupäevi ja aastaid, mida saaks sinuga seostada", + "Capitalization doesn't help very much": "Suurtähtede kasutamisest pole suurt kasu", + "All-uppercase is almost as easy to guess as all-lowercase": "Läbiva suurtähega kirjutatud teksti on sisuliselt sama lihte ära arvata, kui läbiva väiketähega kirjutatud teksti", + "Reversed words aren't much harder to guess": "Tagurpidi kirjutatud sõnu pole eriti keeruline ära arvata", + "Predictable substitutions like '@' instead of 'a' don't help very much": "Ennustatavatest asendustest nagu '@' 'a' asemel pole eriti kasu", + "Add another word or two. Uncommon words are better.": "Lisa veel mõni sõna. Ebatavaliste sõnade kasutamine on hea mõte.", + "Repeats like \"aaa\" are easy to guess": "Kordusi, nagu \"aaa\" on lihtne ära arvata", + "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "Kordusi, nagu \"abcabcabc\" on vaid natuke raskem ära arvata kui \"abc\"", + "Sequences like abc or 6543 are easy to guess": "Jadasid nagu \"abc\" või \"6543\" on lihtne ära arvata", + "Recent years are easy to guess": "Hiljutisi aastaid on lihtne ära arvata", + "Dates are often easy to guess": "Kuupäevi on sageli lihtne ära arvata", + "A word by itself is easy to guess": "Üksikut sõna on lihtne ära arvata", + "Names and surnames by themselves are easy to guess": "Nimesid ja perenimesid on lihtne ära arvata", + "Common names and surnames are easy to guess": "Üldisi nimesid ja perenimesid on lihtne ära arvata", + "Straight rows of keys are easy to guess": "Klaviatuuril järjest paiknevaid klahvikombinatsioone on lihtne ära arvata", + "Help us improve Riot": "Aidake meil täiustada Riot'it", + "Send anonymous usage data which helps us improve Riot. This will use a cookie.": "Saada meile anonüümset kasutusteavet, mis võimaldab meil Riot'it täiustada. Selline teave põhineb küpsiste kasutamisel.", + "I want to help": "Ma soovin aidata", + "No": "Ei", + "Restart": "Käivita uuesti", + "Upgrade your Riot": "Uuenda oma Riot'it", + "A new version of Riot is available!": "Uus Riot'i versioon on saadaval!", + "You: %(message)s": "Sina: %(message)s", + "There was an error joining the room": "Jututoaga liitumisel tekkis viga", + "Sorry, your homeserver is too old to participate in this room.": "Vabandust, sinu koduserver on siin jututoas osalemiseks liiga vana.", + "Please contact your homeserver administrator.": "Palun võta ühendust koduserveri haldajaga.", + "Failed to join room": "Jututoaga liitumine ei õnnestunud", + "Font scaling": "Fontide skaleerimine", + "Custom user status messages": "Kasutajate kohandatud olekuteated", + "Use IRC layout": "Kasuta IRC-tüüpi paigutust", + "Font size": "Fontide suurus", + "Custom font size": "Fontide kohandatud suurus", + "Enable automatic language detection for syntax highlighting": "Kasuta süntaksi esiletõstmisel automaatset keeletuvastust", + "Cross-signing private keys:": "Privaatvõtmed risttunnustamise jaoks:", + "Identity Server URL must be HTTPS": "Isikutuvastusserveri URL peab kasutama HTTPS-protokolli", + "Not a valid Identity Server (status code %(code)s)": "See ei ole sobilik isikutuvastusserver (staatuskood %(code)s)", + "Could not connect to Identity Server": "Ei saanud ühendust isikutuvastusserveriga", + "Checking server": "Kontrollin serverit", + "Change identity server": "Muuda isikutuvastusserverit", + "Disconnect from the identity server and connect to instead?": "Kas katkestame ühenduse isikutuvastusserveriga ning selle asemel loome uue ühenduse serveriga ?", + "Terms of service not accepted or the identity server is invalid.": "Kas puudub nõustumine kasutustingimustega või on isikutuvastusserver vale.", + "The identity server you have chosen does not have any terms of service.": "Sinu valitud isikutuvastusserveril pole kasutustingimusi.", + "Disconnect identity server": "Katkesta ühendus isikutuvastusserveriga", + "Disconnect from the identity server ?": "Kas katkestame ühenduse isikutuvastusserveriga ?", + "Disconnect": "Katkesta ühendus", + "You should:": "Sa peaksid:", + "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "kontrollima kas mõni brauseriplugin takistab ühendust isikutuvastusserveriga (nagu näiteks Privacy Badger)", + "contact the administrators of identity server ": "võtma ühendust isikutuvastusserveri haldajaga", + "wait and try again later": "ootama ja proovima hiljem uuesti", + "Disconnect anyway": "Ikkagi katkesta ühendus", + "You are still sharing your personal data on the identity server .": "Sa jätkuvalt jagad oma isikuandmeid isikutuvastusserveriga .", + "Go back": "Mine tagasi", + "Identity Server (%(server)s)": "Isikutuvastusserver %(server)s", + "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Sinu serveri haldur on lülitanud läbiva krüptimise omavahelistes jututubades ja otsesõnumites välja.", + "This room has been replaced and is no longer active.": "See jututuba on asendatud teise jututoaga ning ei ole enam kasutusel.", + "You do not have permission to post to this room": "Sul ei ole õigusi siia jututuppa kirjutamiseks", + "Bold": "Paks kiri", + "Italics": "Kaldkiri", + "Strikethrough": "Läbikriipsutus", + "Code block": "Koodiplokk", + "Show": "Näita", + "Message preview": "Sõnumi eelvaade", + "List options": "Loendi valikud", + "Show %(count)s more|other": "Näita veel %(count)s sõnumit", + "Show %(count)s more|one": "Näita veel %(count)s sõnumit", + "This room is private, and can only be joined by invitation.": "See jututuba on vaid omavaheliseks kasutuseks ning liitumine toimub kutse alusel.", + "Upgrade this room to version %(version)s": "Uuenda jututuba versioonini %(version)s", + "Upgrade Room Version": "Uuenda jututoa versioon", + "Upgrading this room requires closing down the current instance of the room and creating a new room in its place. To give room members the best possible experience, we will:": "Selle jututoa uuendamine eeldab tema praeguse ilmingu tegevuse lõpetamist ja uue jututoa loomist selle asemele. Selleks, et kõik kulgeks jututoas osalejate jaoks ladusalt, toimime nüüd nii:", + "Create a new room with the same name, description and avatar": "loome uue samanimelise jututoa, millel on sama kirjeldus ja tunnuspilt", + "Update any local room aliases to point to the new room": "uuendame kõik jututoa aliased nii, et nad viitaks uuele jututoale", + "Stop users from speaking in the old version of the room, and post a message advising users to move to the new room": "ei võimalda kasutajatel enam vanas jututoas suhelda ning avaldame seal teate, mis soovitab kõigil kolida uude jututuppa", + "Put a link back to the old room at the start of the new room so people can see old messages": "selleks et saaks vanu sõnumeid lugeda, paneme uue jututoa algusesse viite vanale jututoale", + "Automatically invite users": "Kutsu kasutajad automaatselt", + "Upgrade private room": "Uuenda omavaheline jututuba", + "Upgrade public room": "Uuenda avalik jututuba", + "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "Jututoa uuendamine on keerukas toiming ning tavaliselt soovitatakse seda teha vaid siis, kui jututuba on vigade tõttu halvasti kasutatav, sealt on puudu vajalikke funktsionaalsusi või seal ilmneb turvavigu.", + "This usually only affects how the room is processed on the server. If you're having problems with your Riot, please report a bug.": "Selline tegevus mõjutab tavaliselt vaid viisi, kuidas jututoa andmeid töödeldakse serveris. Kui sinu kasutatavas Riot'is tekib vigu, siis palun saada meile veateade.", + "You'll upgrade this room from to .": "Sa uuendad jututoa versioonist versioonini .", + "Clear Storage and Sign Out": "Tühjenda andmeruum ja logi välja", + "Send Logs": "Saada logikirjed", + "Refresh": "Värskenda", + "Unable to restore session": "Sessiooni taastamine ei õnnestunud", + "We encountered an error trying to restore your previous session.": "Meil tekkis eelmise sessiooni taastamisel viga.", + "If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.": "Kui sa varem oled kasutanud uuemat Riot'i versiooni, siis sinu pragune sessioon ei pruugi olla sellega ühilduv. Sulge see aken ja jätka selle uuema versiooni kasutamist.", + "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Brauseri andmeruumi tühjendamine võib selle vea lahendada, kui samas logid sa ka välja ning kogu krüptitud vestlusajalugu muutub loetamatuks.", + "Verification Pending": "Verifikatsioon on ootel" } From d9b870c5fdb646c68bd06122b59416b1732368c9 Mon Sep 17 00:00:00 2001 From: XoseM Date: Fri, 19 Jun 2020 16:06:24 +0000 Subject: [PATCH 144/194] Translated using Weblate (Galician) Currently translated at 82.6% (1886 of 2282 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/gl/ --- src/i18n/strings/gl.json | 66 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index e84c002ea6..9bd477f696 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -1898,5 +1898,69 @@ "%(severalUsers)smade no changes %(count)s times|other": "%(severalUsers)s non fixeron cambios %(count)s veces", "%(severalUsers)smade no changes %(count)s times|one": "%(severalUsers)s non fixeron cambios", "%(oneUser)smade no changes %(count)s times|other": "%(oneUser)s non fixo cambios %(count)s veces", - "%(oneUser)smade no changes %(count)s times|one": "%(oneUser)s non fixo cambios" + "%(oneUser)smade no changes %(count)s times|one": "%(oneUser)s non fixo cambios", + "QR Code": "Código QR", + "Room address": "Enderezo da sala", + "e.g. my-room": "ex. a-miña-sala", + "Some characters not allowed": "Algúns caracteres non permitidos", + "Please provide a room address": "Proporciona un enderezo para a sala", + "This address is available to use": "Este enderezo está dispoñible", + "This address is already in use": "Este enderezo xa se está a utilizar", + "Enter a server name": "Escribe un nome de servidor", + "Looks good": "Pinta ben", + "Can't find this server or its room list": "Non se atopa o servidor ou a súa lista de salas", + "All rooms": "Todas as salas", + "Your server": "O teu servidor", + "Are you sure you want to remove %(serverName)s": "¿Tes a certeza de querer eliminar %(serverName)s", + "Remove server": "Eliminar servidor", + "Matrix": "Matrix", + "Add a new server": "Engadir novo servidor", + "Server name": "Nome do servidor", + "Add a new server...": "Engadir un novo servidor...", + "%(networkName)s rooms": "Salas de %(networkName)s", + "That doesn't look like a valid email address": "Non semella un enderezo de email válido", + "Use an identity server to invite by email. Use the default (%(defaultIdentityServerName)s) or manage in Settings.": "Usa un servidor de identidade para convidar por email. Usa valor por omisión (%(defaultIdentityServerName)s) ou xestionao en Axustes.", + "Use an identity server to invite by email. Manage in Settings.": "Usa un servidor de identidade para convidar por email. Xestionao en Axustes.", + "The following users may not exist": "As seguintes usuarias poderían non existir", + "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "Non se atopou o perfil dos IDs Matrix da lista inferior - ¿Desexas convidalas igualmente?", + "Invite anyway and never warn me again": "Convidar igualmente e non avisarme outra vez", + "Invite anyway": "Convidar igualmente", + "Close dialog": "Pechar diálogo", + "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "Cóntanos o que fallou ou, mellor aínda, abre un informe en GitHub que describa o problema.", + "Reminder: Your browser is unsupported, so your experience may be unpredictable.": "Lembra: o teu navegador non está soportado, polo que poderían acontecer situacións non agardadas.", + "Before submitting logs, you must create a GitHub issue to describe your problem.": "Antes de enviar os rexistros, deberías abrir un informe en GitHub para describir o problema.", + "GitHub issue": "Informe en GitHub", + "Notes": "Notas", + "Unable to load commit detail: %(msg)s": "Non se cargou o detalle do commit: %(msg)s", + "Removing…": "Eliminando…", + "Destroy cross-signing keys?": "Destruír chaves de sinatura-cruzada?", + "Clear cross-signing keys": "Baleirar chaves de sinatura-cruzada", + "Clear all data in this session?": "¿Baleirar todos os datos desta sesión?", + "Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.": "O baleirado dos datos da sesión é permanente. As mensaxes cifradas perderánse a menos que as súas chaves estiveren nunha copia de apoio.", + "Clear all data": "Eliminar todos os datos", + "Please enter a name for the room": "Escribe un nome para a sala", + "Set a room address to easily share your room with other people.": "Establece un enderezo para a sala fácil de compartir con outras persoas.", + "This room is private, and can only be joined by invitation.": "Esta sala é privada, e só te podes unir a través dun convite.", + "You can’t disable this later. Bridges & most bots won’t work yet.": "Podes desactivar esto posteriormente. As pontes e maioría de bots aínda non funcionarán.", + "Enable end-to-end encryption": "Activar cifrado extremo-a-extremo", + "Create a public room": "Crear sala pública", + "Create a private room": "Crear sala privada", + "Topic (optional)": "Asunto (optativo)", + "Make this room public": "Facer pública esta sala", + "Hide advanced": "Ocultar Avanzado", + "Show advanced": "Mostrar Avanzado", + "Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)": "Evitar que usuarias de outros servidores matrix se unan a esta sala (Este axuste non se pode cambiar máis tarde!)", + "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of Riot to do this": "Para evitar perder o historial da conversa, debes exportar as chaves da sala antes de desconectarte. Necesitarás voltar á nova versión de Riot para facer esto", + "You've previously used a newer version of Riot with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "Xa utilizaches unha versión máis nova de Riot nesta sesión. Para usar esta versión novamente con cifrado extremo-a-extremo tes que desconectarte e voltar a conectar.", + "Incompatible Database": "Base de datos non compatible", + "Continue With Encryption Disabled": "Continuar con Cifrado Desactivado", + "Are you sure you want to deactivate your account? This is irreversible.": "¿Tes a certeza de querer desactivar a túa conta? Esto é irreversible.", + "Confirm account deactivation": "Confirma a desactivación da conta", + "There was a problem communicating with the server. Please try again.": "Houbo un problema ao comunicar co servidor. Inténtao outra vez.", + "Server did not require any authentication": "O servidor non require auténticación", + "Server did not return valid authentication information.": "O servidor non devolveu información válida de autenticación.", + "View Servers in Room": "Ver Servidores na Sala", + "Verification Requests": "Solicitudes de Verificación", + "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Verifica esta usuaria para marcala como confiable. Ao confiar nas usuarias proporcionache tranquilidade extra cando usas cifrado de extremo-a-extremo.", + "Verifying this user will mark their session as trusted, and also mark your session as trusted to them.": "Ao verificar esta usuaria marcarás a súa sesión como confiable, e tamén marcará a túa sesión como confiable para elas." } From 92fb9356bef5dab122139b4244d2a4d80a7e3e0d Mon Sep 17 00:00:00 2001 From: aWeinzierl Date: Sat, 20 Jun 2020 16:42:29 +0000 Subject: [PATCH 145/194] Translated using Weblate (German) Currently translated at 99.6% (2273 of 2282 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 891e6b7656..143e3534c3 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -2242,8 +2242,8 @@ "An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "Beim Ändern der Benutzerrechte ist ein Fehler aufgetreten. Stelle sicher dass du die nötigen Berechtigungen besitzt und versuche es erneut.", "Unable to share email address": "E-Mail Adresse konnte nicht geteilt werden", "Please enter verification code sent via text.": "Gib den Verifikationscode ein, den du empfangen hast.", - "Almost there! Is your other session showing the same shield?": "Fast geschafft! Zeigt deine andere Sitzung die gleichen Zeichen?", - "Almost there! Is %(displayName)s showing the same shield?": "Fast geschafft! Werden bei %(displayName)s die gleichen Zeichen angezeigt?", + "Almost there! Is your other session showing the same shield?": "Fast geschafft! Zeigt deine andere Sitzung das gleiche Schild?", + "Almost there! Is %(displayName)s showing the same shield?": "Fast geschafft! Wird bei %(displayName)s das gleiche Schild angezeigt?", "Click the link in the email you received to verify and then click continue again.": "Klicke auf den Link in der Bestätigungs-E-Mail, und dann auf Weiter.", "Unable to revoke sharing for phone number": "Widerrufen der geteilten Telefonnummer nicht möglich", "Unable to share phone number": "Teilen der Telefonnummer nicht möglich", From abbacd788cba02f36af3561e98c225e41d5733ce Mon Sep 17 00:00:00 2001 From: take100yen Date: Sat, 20 Jun 2020 15:05:52 +0000 Subject: [PATCH 146/194] Translated using Weblate (Japanese) Currently translated at 58.0% (1323 of 2282 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ja/ --- src/i18n/strings/ja.json | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json index 07ad4f9ef7..3944aa7489 100644 --- a/src/i18n/strings/ja.json +++ b/src/i18n/strings/ja.json @@ -1400,5 +1400,36 @@ "Service": "サービス", "Summary": "概要", "Document": "ドキュメント", - "Appearance": "外観" + "Appearance": "外観", + "Other users may not trust it": "他のユーザーはこのセッションを信頼しない可能性があります", + "Show a placeholder for removed messages": "削除されたメッセージの場所にプレースホルダーを表示", + "Show join/leave messages (invites/kicks/bans unaffected)": "参加/退出のメッセージを表示 (招待/追放/ブロック には影響しません)", + "Prompt before sending invites to potentially invalid matrix IDs": "不正な可能性のある Matrix ID に招待を送るまえに確認を表示", + "Order rooms by name": "名前順で部屋を整列", + "Show rooms with unread notifications first": "未読通知のある部屋をトップに表示", + "Show shortcuts to recently viewed rooms above the room list": "最近表示した部屋のショートカットを部屋リストの上に表示", + "Show previews/thumbnails for images": "画像のプレビュー/サムネイルを表示", + "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "あなたのアカウントではクロス署名の認証情報がシークレットストレージに保存されていますが、このセッションでは信頼されていません。", + "Riot can't securely cache encrypted messages locally while running in a web browser. Use Riot Desktop for encrypted messages to appear in search results.": "Riot はウェブブラウザでは暗号化されたメッセージを安全にキャッシュできません。暗号化されたメッセージを検索結果に表示するには Riot Desktop を利用してください。", + "This session is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "このセッションではキーをバックアップしていません。ですが、あなたは復元に使用したり今後キーを追加したりできるバックアップを持っています。", + "Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.": "このセッションのみにあるキーを失なわないために、セッションをキーバックアップに接続しましょう。", + "Connect this session to Key Backup": "このセッションをキーバックアップに接続", + "Autocomplete delay (ms)": "自動補完の遅延 (ms)", + "Missing media permissions, click the button below to request.": "メディア権限が不足しています、リクエストするには下のボタンを押してください。", + "Request media permissions": "メディア権限をリクエスト", + "Joining room …": "部屋に参加中...", + "Join the discussion": "話し合いに参加", + "%(roomName)s can't be previewed. Do you want to join it?": "%(roomName)s はプレビューできません。部屋に参加しますか?", + "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "他のユーザーがあなたのホームサーバー (%(localDomain)s) を通じてこの部屋を見つけられるよう、アドレスを設定しましょう", + "Warning: You should only do this on a trusted computer.": "警告: 信頼できるコンピュータでのみこれを実行するべきです。", + "Enter recovery key": "リカバリキーを入力", + "Access your secure message history and your cross-signing identity for verifying other sessions by entering your recovery key.": "暗号化されたメッセージへアクセスしたりクロス署名認証情報で他のセッションを承認するにはリカバリキーを入力してください。", + "If you've forgotten your recovery key you can .": "リカバリキーを忘れた場合はできます。", + "Verify this login": "このログインを承認", + "Signing In...": "サインインしています...", + "If you've joined lots of rooms, this might take a while": "たくさんの部屋に参加している場合は、時間がかかる可能性があります", + "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "このログインを他のセッションで承認し、あなたの認証情報を確認すれば、暗号化されたメッセージへアクセスできるようになります。", + "This requires the latest Riot on your other devices:": "最新版の Riot が他のあなたのデバイスで実行されている必要があります:", + "or another cross-signing capable Matrix client": "もしくはクロス署名に対応した他の Matrix クライアント", + "Without completing security on this session, it won’t have access to encrypted messages.": "このセッションでのセキュリティを完了させない限り、暗号化されたメッセージにはアクセスできません。" } From ac869cf6dcbb7ce5f872ade14c4c2bc83c815dda Mon Sep 17 00:00:00 2001 From: ziriSut Date: Sun, 21 Jun 2020 10:10:42 +0000 Subject: [PATCH 147/194] Translated using Weblate (Kabyle) Currently translated at 5.6% (127 of 2282 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/kab/ --- src/i18n/strings/kab.json | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/kab.json b/src/i18n/strings/kab.json index 9ba9935bb8..8d11469afc 100644 --- a/src/i18n/strings/kab.json +++ b/src/i18n/strings/kab.json @@ -243,7 +243,7 @@ "Sign in": "Qqen", "Help": "Tallelt", "Reload": "Smiren", - "powered by Matrix": "S lmendad n Matrix", + "powered by Matrix": "s lmendad n Matrix", "Custom Server Options": "Iɣewwaren n uqeddac udmawan", "Code": "Tangalt", "Submit": "Azen", @@ -336,5 +336,15 @@ "Toggle right panel": "Sken/ffer agalis ayeffus", "Toggle this dialog": "Sken/ffer adiwanni-a", "Move autocomplete selection up/down": "Err tafrant n tacart tawurmant afellay/adday", - "Cancel autocomplete": "Sefsex tacaṛt tawurmant" + "Cancel autocomplete": "Sefsex tacaṛt tawurmant", + "Toggle Bold": "Err-it d azuran", + "Toggle Italics": "Err-it ɣer uknan", + "Toggle Quote": "Err-it ɣer yizen aneẓli", + "Navigate recent messages to edit": "Nadi iznan imaynuten i usiẓreg", + "Jump to start/end of the composer": "Ṛuḥ ɣer tazwara/taggara n yimsuddes", + "Navigate composer history": "Nadi deg uzray n yimsuddes", + "Cancel replying to a message": "Sefsex tiririt ɣef yizen", + "Toggle microphone mute": "Rmed/sens tanusi n usawaḍ", + "Toggle video on/off": "Rmed/sens tavidyut", + "Scroll up/down in the timeline": "Drurem gar afellay/addday n tesnakudt" } From b80b01076cdf98cdfe68bced7cdbdf1896458a3c Mon Sep 17 00:00:00 2001 From: Imre Kristoffer Eilertsen Date: Sat, 20 Jun 2020 14:19:39 +0000 Subject: [PATCH 148/194] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 62.1% (1416 of 2282 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/nb_NO/ --- src/i18n/strings/nb_NO.json | 132 +++++++++++++++++++++++++++++++++++- 1 file changed, 131 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/nb_NO.json b/src/i18n/strings/nb_NO.json index fe4fbf12ee..e815a2e6c3 100644 --- a/src/i18n/strings/nb_NO.json +++ b/src/i18n/strings/nb_NO.json @@ -1354,5 +1354,135 @@ "Jump to read receipt": "Hopp til lesekvitteringen", "Mention": "Nevn", "Community Name": "Samfunnets navn", - "Dismiss read marker and jump to bottom": "Avføy lesekvitteringen og hopp ned til bunnen" + "Dismiss read marker and jump to bottom": "Avføy lesekvitteringen og hopp ned til bunnen", + "If you cancel now, you won't complete verifying your other session.": "Hvis du avbryter nå, vil du ikke ha fullført verifiseringen av den andre økten din.", + "Room name or address": "Rommets navn eller adresse", + "sent an image.": "sendte et bilde.", + "Light": "Lys", + "Dark": "Mørk", + "Verify your other session using one of the options below.": "Verifiser den andre økten din med en av metodene nedenfor.", + "User %(user_id)s does not exist": "Brukeren %(user_id)s eksisterer ikke", + "Use a few words, avoid common phrases": "Bruk noen få ord, unngå vanlig fraser", + "I want to help": "Jeg vil hjelpe til", + "Ok": "OK", + "Set password": "Bestem passord", + "Encryption upgrade available": "Krypteringsoppdatering tilgjengelig", + "Verify the new login accessing your account: %(name)s": "Verifiser den nye påloggingen som vil ha tilgang til kontoen din: %(name)s", + "Restart": "Start på nytt", + "You: %(message)s": "Du: %(message)s", + "Font scaling": "Skrifttypeskalering", + "Use IRC layout": "Bruk IRC-oppsett", + "Font size": "Skriftstørrelse", + "Custom font size": "Tilpasset skriftstørrelse", + "You've successfully verified this user.": "Du har vellykket verifisert denne brukeren.", + "Waiting for your other session, %(deviceName)s (%(deviceId)s), to verify…": "Venter på at den andre økten din, %(deviceName)s (%(deviceId)s), skal verifisere …", + "Waiting for your other session to verify…": "Venter på at den andre økten din skal verifisere …", + "Santa": "Julenisse", + "Delete %(count)s sessions|other": "Slett %(count)s økter", + "Delete %(count)s sessions|one": "Slett %(count)s økt", + "Backup version: ": "Sikkerhetskopiversjon: ", + "wait and try again later": "vent og prøv igjen senere", + "Size must be a number": "Størrelsen må være et nummer", + "Customise your appearance": "Tilpass utseendet du bruker", + "Labs": "Laboratoriet", + "eg: @bot:* or example.org": "f.eks.: @bot:* eller example.org", + "To link to this room, please add an address.": "For å lenke til dette rommet, vennligst legg til en adresse.", + "Remove %(phone)s?": "Vil du fjerne %(phone)s?", + "Online for %(duration)s": "På nett i %(duration)s", + "Favourites": "Favoritter", + "Create room": "Opprett et rom", + "People": "Folk", + "Sort by": "Sorter etter", + "Activity": "Aktivitet", + "A-Z": "A-Å", + "Unread rooms": "Uleste rom", + "Always show first": "Alltid vis først", + "Show": "Vis", + "Message preview": "Meldingsforhåndsvisning", + "Leave Room": "Forlat rommet", + "This room has no local addresses": "Dette rommet har ikke noen lokale adresser", + "Waiting for you to accept on your other session…": "Venter på at du aksepterer på den andre økten din …", + "Remove recent messages by %(user)s": "Fjern nylige meldinger fra %(user)s", + "Remove %(count)s messages|other": "Slett %(count)s meldinger", + "Remove %(count)s messages|one": "Slett 1 melding", + "Almost there! Is your other session showing the same shield?": "Nesten fremme! Viser den andre økten din det samme skjoldet?", + "You've successfully verified your device!": "Du har vellykket verifisert enheten din!", + "You've successfully verified %(deviceName)s (%(deviceId)s)!": "Du har vellykket verifisert %(deviceName)s (%(deviceId)s)!", + "You've successfully verified %(displayName)s!": "Du har vellykket verifisert %(displayName)s!", + "You cancelled verification on your other session.": "Du avbrøt verifiseringen på den andre økten din.", + "You sent a verification request": "Du sendte en verifiseringsforespørsel", + "Error decrypting video": "Feil under dekryptering av video", + "Message deleted": "Meldingen ble slettet", + "Message deleted by %(name)s": "Meldingen ble slettet av %(name)s", + "Click here to see older messages.": "Klikk for å se eldre meldinger.", + "Add an Integration": "Legg til en integrering", + "Can't load this message": "Klarte ikke å laste inn denne meldingen", + "Categories": "Kategorier", + "Popout widget": "Utsprettsmodul", + "QR Code": "QR-kode", + "Room address": "Rommets adresse", + "This address is available to use": "Denne adressen er allerede i bruk", + "ex. @bob:example.com": "f.eks. @bob:example.com", + "Failed to send logs: ": "Mislyktes i å sende loggbøker: ", + "Incompatible Database": "Inkompatibel database", + "Event Content": "Hendelsesinnhold", + "Verification Requests": "Verifiseringsforespørsler", + "Integrations are disabled": "Integreringer er skrudd av", + "Integrations not allowed": "Integreringer er ikke tillatt", + "Confirm to continue": "Bekreft for å fortsette", + "Clear cache and resync": "Tøm mellomlageret og synkroniser på nytt", + "Manually export keys": "Eksporter nøkler manuelt", + "Use this session to verify your new one, granting it access to encrypted messages:": "Bruk denne økten til å verifisere din nye økt, som vil gi den tilgang til krypterte meldinger:", + "If you didn’t sign in to this session, your account may be compromised.": "Dersom det ikke var du som logget deg på økten, kan kontoen din ha blitt kompromittert.", + "Automatically invite users": "Inviter brukere automatisk", + "Verification Pending": "Avventer verifisering", + "Username invalid: %(errMessage)s": "Brukernavnet er ugyldig: %(errMessage)s", + "An error occurred: %(error_string)s": "En feil oppstod: %(error_string)s", + "Share Room": "Del rommet", + "Share User": "Del brukeren", + "Upload %(count)s other files|other": "Last opp %(count)s andre filer", + "Upload %(count)s other files|one": "Last opp %(count)s annen fil", + "Appearance": "Utseende", + "Verify other session": "Verifiser en annen økt", + "Keys restored": "Nøklene ble gjenopprettet", + "Address (optional)": "Adresse (valgfritt)", + "Reject invitation": "Avslå invitasjonen", + "Resend edit": "Send redigeringen på nytt", + "Resend removal": "Send slettingen på nytt", + "Start authentication": "Begynn autentisering", + "The email field must not be blank.": "E-postfeltet kan ikke stå tomt.", + "The username field must not be blank.": "Brukernavnfeltet kan ikke stå tomt.", + "The phone number field must not be blank.": "Telefonnummerfeltet kan ikke stå tomt.", + "The password field must not be blank.": "Passordfeltet kan ikke stå tomt.", + "Couldn't load page": "Klarte ikke å laste inn siden", + "Add to summary": "Legg til i oppsummeringen", + "Unable to accept invite": "Klarte ikke å akseptere invitasjonen", + "Unable to join community": "Klarte ikke å bli med i samfunnet", + "Unable to leave community": "Klarte ikke å forlate samfunnet", + "You are an administrator of this community": "Du er en administrator i dette samfunnet", + "You are a member of this community": "Du er et medlem av dette samfunnet", + "Failed to load %(groupId)s": "Klarte ikke å laste inn %(groupId)s", + "Liberate your communication": "Frigjør kommunikasjonen din", + "Explore Public Rooms": "Utforsk offentlige rom", + "Create a Group Chat": "Opprett en gruppechat", + "Review terms and conditions": "Gå gjennom betingelser og vilkår", + "delete the address.": "slette adressen.", + "%(count)s of your messages have not been sent.|one": "Meldingen din ble ikke sendt.", + "Jump to first invite.": "Hopp til den første invitasjonen.", + "You seem to be uploading files, are you sure you want to quit?": "Du ser til å laste opp filer, er du sikker på at du vil avslutte?", + "Switch to light mode": "Bytt til lys modus", + "Switch to dark mode": "Bytt til mørk modus", + "Switch theme": "Bytt tema", + "All settings": "Alle innstillinger", + "Archived rooms": "Arkiverte rom", + "Feedback": "Tilbakemelding", + "Account settings": "Kontoinnstillinger", + "Emoji Autocomplete": "Auto-fullfør emojier", + "Confirm encryption setup": "Bekreft krypteringsoppsett", + "Create key backup": "Opprett nøkkelsikkerhetskopi", + "Set up Secure Messages": "Sett opp sikre meldinger", + "Toggle Bold": "Veksle fet", + "Toggle Italics": "Veksle kursiv", + "Toggle Quote": "Veksle siteringsformat", + "Upload a file": "Last opp en fil" } From e19469a0c385ef0b38266c901c94c4f94ab73252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=B2=D0=B0=D1=81=D1=8F=D0=BD228?= Date: Sat, 20 Jun 2020 20:17:20 +0000 Subject: [PATCH 149/194] Translated using Weblate (Russian) Currently translated at 87.1% (1987 of 2282 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ru/ --- src/i18n/strings/ru.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index 43c43a15ae..cd5a320062 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -2122,7 +2122,7 @@ "Enable cross-signing to verify per-user instead of per-session": "Разрешить кросс-подпись для подтверждения пользователей вместо отдельных сессий", "Keep recovery passphrase in memory for this session": "Сохранить пароль восстановления в памяти для этой сессии", "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Отдельно подтверждать каждую сессию пользователя как доверенную, не доверяя кросс-подписанным устройствам.", - "Securely cache encrypted messages locally for them to appear in search results, using ": "Безопасно кэшировать шифрованные сообщения локально, чтобы они появлялись в результатах поиска с помощью ", + "Securely cache encrypted messages locally for them to appear in search results, using ": "Кэшировать шифрованные сообщения локально, чтобы они выводились в результатах поиска, используя: ", " to store messages from ": " чтобы сохранить сообщения от ", "Securely cache encrypted messages locally for them to appear in search results.": "Безопасно кэшировать шифрованные сообщения локально, чтобы они появлялись в результатах поиска.", "Riot is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom Riot Desktop with search components added.": "Отсутствуют некоторые необходимые компоненты для Riot, чтобы безопасно кэшировать шифрованные сообщения локально. Если вы хотите попробовать эту возможность, соберите самостоятельно Riot Desktop с добавлением поисковых компонентов.", @@ -2144,5 +2144,7 @@ "%(role)s in %(roomName)s": "%(role)sв %(roomName)s", "Start verification again from their profile.": "Начните подтверждение заново в профиле пользователя.", "Messages in this room are end-to-end encrypted. Learn more & verify this user in their user profile.": "Сообщения в этой комнате зашифрованы сквозным шифрованием. Посмотрите подробности и подтвердите пользователя в его профиле.", - "Send a Direct Message": "Отправить сообщение" + "Send a Direct Message": "Отправить сообщение", + "Light": "Светлая", + "Dark": "Темная" } From 3d7427cccafae58ebcae46c7f71c338b8db272aa Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 22 Jun 2020 11:39:11 +0100 Subject: [PATCH 150/194] Fix semicolons --- src/components/views/elements/Field.tsx | 4 ++-- .../tabs/user/AppearanceUserSettingsTab.tsx | 18 +++++++++--------- .../payloads/UpdateFontSizePayload.ts | 2 +- .../payloads/UpdateSystemFontPayload.ts | 2 +- src/settings/watchers/FontWatcher.ts | 6 +++--- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index 9014e2c3c9..9d53576259 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -53,9 +53,9 @@ interface IProps { flagInvalid?: boolean; // If specified, contents will appear as a tooltip on the element and // validation feedback tooltips will be suppressed. - tooltipContent?: React.ReactNode, + tooltipContent?: React.ReactNode; // If specified the tooltip will be shown regardless of feedback - forceTooltipVisible?: boolean, + forceTooltipVisible?: boolean; // If specified alongside tooltipContent, the class name to apply to the // tooltip itself. tooltipClassName?: string; diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index 10f5b4a5a4..5a08d99c19 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -47,13 +47,13 @@ interface IState extends IThemeState { // String displaying the current selected fontSize. // Needs to be string for things like '17.' without // trailing 0s. - fontSize: string, - customThemeUrl: string, - customThemeMessage: CustomThemeMessage, - useCustomFontSize: boolean, - useSystemFont: boolean, - systemFont: string, - showAdvanced: boolean, + fontSize: string; + customThemeUrl: string; + customThemeMessage: CustomThemeMessage; + useCustomFontSize: boolean; + useSystemFont: boolean; + systemFont: string; + showAdvanced: boolean; } export default class AppearanceUserSettingsTab extends React.Component { @@ -349,7 +349,7 @@ export default class AppearanceUserSettingsTab extends React.Component { this.setState({ systemFont: value.target.value, - }) + }); SettingsStore.setValue("systemFont", null, SettingLevel.DEVICE, value.target.value); }} @@ -363,7 +363,7 @@ export default class AppearanceUserSettingsTab extends React.Component {toggle} {advanced} -
          +
          ; } render() { diff --git a/src/dispatcher/payloads/UpdateFontSizePayload.ts b/src/dispatcher/payloads/UpdateFontSizePayload.ts index 3eac3e4607..6577acd594 100644 --- a/src/dispatcher/payloads/UpdateFontSizePayload.ts +++ b/src/dispatcher/payloads/UpdateFontSizePayload.ts @@ -18,7 +18,7 @@ import { ActionPayload } from "../payloads"; import { Action } from "../actions"; export interface UpdateFontSizePayload extends ActionPayload { - action: Action.UpdateFontSize, + action: Action.UpdateFontSize; /** * The font size to set the root to diff --git a/src/dispatcher/payloads/UpdateSystemFontPayload.ts b/src/dispatcher/payloads/UpdateSystemFontPayload.ts index 73475e10d5..aa59db5aa9 100644 --- a/src/dispatcher/payloads/UpdateSystemFontPayload.ts +++ b/src/dispatcher/payloads/UpdateSystemFontPayload.ts @@ -18,7 +18,7 @@ import { ActionPayload } from "../payloads"; import { Action } from "../actions"; export interface UpdateSystemFontPayload extends ActionPayload { - action: Action.UpdateSystemFont, + action: Action.UpdateSystemFont; /** * Specify whether to use a system font or the stylesheet font diff --git a/src/settings/watchers/FontWatcher.ts b/src/settings/watchers/FontWatcher.ts index cc843edb4d..9af5156704 100644 --- a/src/settings/watchers/FontWatcher.ts +++ b/src/settings/watchers/FontWatcher.ts @@ -38,7 +38,7 @@ export class FontWatcher implements IWatcher { this.setSystemFont({ useSystemFont: SettingsStore.getValue("useSystemFont"), font: SettingsStore.getValue("systemFont"), - }) + }); this.dispatcherRef = dis.register(this.onAction); } @@ -50,7 +50,7 @@ export class FontWatcher implements IWatcher { if (payload.action === Action.UpdateFontSize) { this.setRootFontSize(payload.size); } else if (payload.action === Action.UpdateSystemFont) { - this.setSystemFont(payload) + this.setSystemFont(payload); } }; @@ -65,5 +65,5 @@ export class FontWatcher implements IWatcher { private setSystemFont = ({useSystemFont, font}) => { document.body.style.fontFamily = useSystemFont ? font : ""; - } + }; } From aab42a291bd60cb2f27b881928e4fa67a93ec469 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 22 Jun 2020 12:28:15 +0100 Subject: [PATCH 151/194] implement appearance tab nits - fix border colours in dark theme - lighten dark theme preview background - add missing return statement --- .../tabs/user/_AppearanceUserSettingsTab.scss | 17 +++++++++++------ res/themes/dark/css/_dark.scss | 4 ++-- res/themes/light/css/_light.scss | 2 +- .../views/elements/EventTilePreview.tsx | 2 +- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss index 044b5e2240..e2454336b8 100644 --- a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss @@ -30,7 +30,7 @@ limitations under the License. flex-direction: row; align-items: center; padding: 15px; - background: $font-slider-bg-color; + background: rgba($appearance-tab-border-color, 0.2); border-radius: 10px; font-size: 10px; margin-top: 24px; @@ -38,7 +38,7 @@ limitations under the License. } .mx_AppearanceUserSettingsTab_fontSlider_preview { - border: 1px solid $input-darker-bg-color; + border: 1px solid $appearance-tab-border-color; border-radius: 10px; padding: 0 16px 9px 16px; pointer-events: none; @@ -56,12 +56,14 @@ limitations under the License. font-size: 15px; padding-right: 20px; padding-left: 5px; + font-weight: 500; } .mx_AppearanceUserSettingsTab_fontSlider_largeText { font-size: 18px; padding-left: 20px; padding-right: 5px; + font-weight: 500; } .mx_AppearanceUserSettingsTab { @@ -115,7 +117,8 @@ limitations under the License. } &.mx_ThemeSelector_dark { - background-color: #181b21; + // 5% lightened version of 181b21 + background-color: #25282e; color: #f3f8fd; > input > div { @@ -163,10 +166,11 @@ limitations under the License. width: 300px; - border: 1px solid $input-darker-bg-color; + border: 1px solid $appearance-tab-border-color; border-radius: 10px; - .mx_EventTile_msgOption { + .mx_EventTile_msgOption, + .mx_MessageActionBar { display: none; } @@ -175,6 +179,7 @@ limitations under the License. display: flex; align-items: center; padding: 10px; + pointer-events: none; } .mx_RadioButton { @@ -188,7 +193,7 @@ limitations under the License. } .mx_RadioButton { - border-top: 1px solid $input-darker-bg-color; + border-top: 1px solid $appearance-tab-border-color; > input + div { border-color: rgba($muted-fg-color, 0.2); diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 50f3c08782..69fc91f222 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -198,8 +198,8 @@ $breadcrumb-placeholder-bg-color: #272c35; $user-tile-hover-bg-color: $header-panel-bg-color; -// FontSlider colors -$font-slider-bg-color: $room-highlight-color; +// Appearance tab colors +$appearance-tab-border-color: $room-highlight-color; // ***** Mixins! ***** diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 355cc1301c..57dc1fa5e0 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -327,7 +327,7 @@ $breadcrumb-placeholder-bg-color: #e8eef5; $user-tile-hover-bg-color: $header-panel-bg-color; // FontSlider colors -$font-slider-bg-color: rgba($input-darker-bg-color, 0.2); +$appearance-tab-border-color: $input-darker-bg-color; // ***** Mixins! ***** diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx index fa600196e5..7d8b774955 100644 --- a/src/components/views/elements/EventTilePreview.tsx +++ b/src/components/views/elements/EventTilePreview.tsx @@ -108,7 +108,7 @@ export default class EventTilePreview extends React.Component { }, }; - + return event; } public render() { From ea929e575af9046d71a6089fba6e7646067607d0 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 22 Jun 2020 16:02:23 +0100 Subject: [PATCH 152/194] Fix MessageActionBar --- res/css/views/rooms/_IRCLayout.scss | 5 ----- 1 file changed, 5 deletions(-) diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index 814a614007..94753f9473 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -121,11 +121,6 @@ $irc-line-height: $font-18px; } } - .mx_EventTile_line .mx_MessageActionBar, - .mx_EventTile_line .mx_ReplyThread_wrapper { - display: block; - } - .mx_EventTile_reply { order: 4; } From 7bc5ce7271b337d904ae880224973f08d4c42adf Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 22 Jun 2020 16:05:32 +0100 Subject: [PATCH 153/194] Underscore actions --- src/dispatcher/actions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index a03c731818..379a0a4451 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -73,10 +73,10 @@ export enum Action { /** * Sets the apps root font size. Should be used with UpdateFontSizePayload */ - UpdateFontSize = "update-font-size", + UpdateFontSize = "update_font_size", /** * Sets a system font. Should be used with UpdateSystemFontPayload */ - UpdateSystemFont = "update-system-font", + UpdateSystemFont = "update_system_font", } From 099661c2aacd2eb360d8dcb6ac9e24972db16bc6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 22 Jun 2020 10:18:38 -0600 Subject: [PATCH 154/194] Only fire setting changes for changed settings Previously we were firing updates for everything, which is bad. This has an effect of causing the room list to update itself every time the user goes to toggle some account settings. --- .../handlers/AccountSettingsHandler.js | 9 ++-- .../MatrixClientBackedSettingsHandler.js | 4 ++ .../handlers/RoomAccountSettingsHandler.js | 9 ++-- src/settings/handlers/RoomSettingsHandler.js | 13 +++-- src/utils/arrays.ts | 22 +++++++++ src/utils/objects.ts | 49 +++++++++++++++++++ 6 files changed, 97 insertions(+), 9 deletions(-) create mode 100644 src/utils/objects.ts diff --git a/src/settings/handlers/AccountSettingsHandler.js b/src/settings/handlers/AccountSettingsHandler.js index fea2e92c62..7b78c39c3a 100644 --- a/src/settings/handlers/AccountSettingsHandler.js +++ b/src/settings/handlers/AccountSettingsHandler.js @@ -18,6 +18,7 @@ limitations under the License. import {MatrixClientPeg} from '../../MatrixClientPeg'; import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler"; import {SettingLevel} from "../SettingsStore"; +import {objectKeyChanges} from "../../utils/objects"; const BREADCRUMBS_LEGACY_EVENT_TYPE = "im.vector.riot.breadcrumb_rooms"; const BREADCRUMBS_EVENT_TYPE = "im.vector.setting.breadcrumbs"; @@ -45,7 +46,7 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa newClient.on("accountData", this._onAccountData); } - _onAccountData(event) { + _onAccountData(event, prevEvent) { if (event.getType() === "org.matrix.preview_urls") { let val = event.getContent()['disable']; if (typeof(val) !== "boolean") { @@ -56,8 +57,10 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa this._watchers.notifyUpdate("urlPreviewsEnabled", null, SettingLevel.ACCOUNT, val); } else if (event.getType() === "im.vector.web.settings") { - // We can't really discern what changed, so trigger updates for everything - for (const settingName of Object.keys(event.getContent())) { + // Figure out what changed and fire those updates + const prevContent = prevEvent ? prevEvent.getContent() : {}; + const changedSettings = objectKeyChanges(prevContent, event.getContent()); + for (const settingName of changedSettings) { const val = event.getContent()[settingName]; this._watchers.notifyUpdate(settingName, null, SettingLevel.ACCOUNT, val); } diff --git a/src/settings/handlers/MatrixClientBackedSettingsHandler.js b/src/settings/handlers/MatrixClientBackedSettingsHandler.js index effe7ae9a7..63725b4dff 100644 --- a/src/settings/handlers/MatrixClientBackedSettingsHandler.js +++ b/src/settings/handlers/MatrixClientBackedSettingsHandler.js @@ -42,6 +42,10 @@ export default class MatrixClientBackedSettingsHandler extends SettingsHandler { MatrixClientBackedSettingsHandler._instances.push(this); } + get client() { + return MatrixClientBackedSettingsHandler._matrixClient; + } + initMatrixClient() { console.warn("initMatrixClient not overridden"); } diff --git a/src/settings/handlers/RoomAccountSettingsHandler.js b/src/settings/handlers/RoomAccountSettingsHandler.js index 1e9d3f7bed..1c818cef71 100644 --- a/src/settings/handlers/RoomAccountSettingsHandler.js +++ b/src/settings/handlers/RoomAccountSettingsHandler.js @@ -18,6 +18,7 @@ limitations under the License. import {MatrixClientPeg} from '../../MatrixClientPeg'; import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler"; import {SettingLevel} from "../SettingsStore"; +import {objectKeyChanges} from "../../utils/objects"; const ALLOWED_WIDGETS_EVENT_TYPE = "im.vector.setting.allowed_widgets"; @@ -40,7 +41,7 @@ export default class RoomAccountSettingsHandler extends MatrixClientBackedSettin newClient.on("Room.accountData", this._onAccountData); } - _onAccountData(event, room) { + _onAccountData(event, room, prevEvent) { const roomId = room.roomId; if (event.getType() === "org.matrix.room.preview_urls") { @@ -55,8 +56,10 @@ export default class RoomAccountSettingsHandler extends MatrixClientBackedSettin } else if (event.getType() === "org.matrix.room.color_scheme") { this._watchers.notifyUpdate("roomColor", roomId, SettingLevel.ROOM_ACCOUNT, event.getContent()); } else if (event.getType() === "im.vector.web.settings") { - // We can't really discern what changed, so trigger updates for everything - for (const settingName of Object.keys(event.getContent())) { + // Figure out what changed and fire those updates + const prevContent = prevEvent ? prevEvent.getContent() : {}; + const changedSettings = objectKeyChanges(prevContent, event.getContent()); + for (const settingName of changedSettings) { const val = event.getContent()[settingName]; this._watchers.notifyUpdate(settingName, roomId, SettingLevel.ROOM_ACCOUNT, val); } diff --git a/src/settings/handlers/RoomSettingsHandler.js b/src/settings/handlers/RoomSettingsHandler.js index 6407818450..4116f26220 100644 --- a/src/settings/handlers/RoomSettingsHandler.js +++ b/src/settings/handlers/RoomSettingsHandler.js @@ -18,6 +18,7 @@ limitations under the License. import {MatrixClientPeg} from '../../MatrixClientPeg'; import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler"; import {SettingLevel} from "../SettingsStore"; +import {objectKeyChanges} from "../../utils/objects"; /** * Gets and sets settings at the "room" level. @@ -38,8 +39,12 @@ export default class RoomSettingsHandler extends MatrixClientBackedSettingsHandl newClient.on("RoomState.events", this._onEvent); } - _onEvent(event) { + _onEvent(event, state, prevEvent) { const roomId = event.getRoomId(); + const room = this.client.getRoom(roomId); + if (!room) throw new Error(`Unknown room caused state update: ${roomId}`); + + if (state !== room.currentState) return; // ignore state updates which are not current if (event.getType() === "org.matrix.room.preview_urls") { let val = event.getContent()['disable']; @@ -51,8 +56,10 @@ export default class RoomSettingsHandler extends MatrixClientBackedSettingsHandl this._watchers.notifyUpdate("urlPreviewsEnabled", roomId, SettingLevel.ROOM, val); } else if (event.getType() === "im.vector.web.settings") { - // We can't really discern what changed, so trigger updates for everything - for (const settingName of Object.keys(event.getContent())) { + // Figure out what changed and fire those updates + const prevContent = prevEvent ? prevEvent.getContent() : {}; + const changedSettings = objectKeyChanges(prevContent, event.getContent()); + for (const settingName of changedSettings) { this._watchers.notifyUpdate(settingName, roomId, SettingLevel.ROOM, event.getContent()[settingName]); } } diff --git a/src/utils/arrays.ts b/src/utils/arrays.ts index fea376afcd..8175d89464 100644 --- a/src/utils/arrays.ts +++ b/src/utils/arrays.ts @@ -46,6 +46,28 @@ export function arrayDiff(a: T[], b: T[]): { added: T[], removed: T[] } { }; } +/** + * Returns the union of two arrays. + * @param a The first array. Must be defined. + * @param b The second array. Must be defined. + * @returns The union of the arrays. + */ +export function arrayUnion(a: T[], b: T[]): T[] { + return a.filter(i => b.includes(i)); +} + +/** + * Merges arrays, deduping contents using a Set. + * @param a The arrays to merge. + * @returns The merged array. + */ +export function arrayMerge(...a: T[][]): T[] { + return Array.from(a.reduce((c, v) => { + v.forEach(i => c.add(i)); + return c; + }, new Set())); +} + /** * Helper functions to perform LINQ-like queries on arrays. */ diff --git a/src/utils/objects.ts b/src/utils/objects.ts new file mode 100644 index 0000000000..cbb311cc48 --- /dev/null +++ b/src/utils/objects.ts @@ -0,0 +1,49 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +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 { arrayDiff, arrayMerge, arrayUnion } from "./arrays"; + +/** + * Determines the keys added, changed, and removed between two objects. + * For changes, simple triple equal comparisons are done, not in-depth + * tree checking. + * @param a The first object. Must be defined. + * @param b The second object. Must be defined. + * @returns The difference between the keys of each object. + */ +export function objectDiff(a: any, b: any): { changed: string[], added: string[], removed: string[] } { + const aKeys = Object.keys(a); + const bKeys = Object.keys(b); + const keyDiff = arrayDiff(aKeys, bKeys); + const possibleChanges = arrayUnion(aKeys, bKeys); + const changes = possibleChanges.filter(k => a[k] !== b[k]); + + return {changed: changes, added: keyDiff.added, removed: keyDiff.removed}; +} + +/** + * Gets all the key changes (added, removed, or value difference) between + * two objects. Triple equals is used to compare values, not in-depth tree + * checking. + * @param a The first object. Must be defined. + * @param b The second object. Must be defined. + * @returns The keys which have been added, removed, or changed between the + * two objects. + */ +export function objectKeyChanges(a: any, b: any): string[] { + const diff = objectDiff(a, b); + return arrayMerge(diff.removed, diff.added, diff.changed); +} From 30d8dc06fcad02b0a2012d114b5a6fb5ecbf498c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 19 Jun 2020 15:43:05 -0600 Subject: [PATCH 155/194] Increase bold weight for unread rooms For https://github.com/vector-im/riot-web/issues/14084 --- res/css/views/rooms/_RoomTile2.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss index 001499fea5..a97d1fd5b9 100644 --- a/res/css/views/rooms/_RoomTile2.scss +++ b/res/css/views/rooms/_RoomTile2.scss @@ -67,7 +67,7 @@ limitations under the License. } .mx_RoomTile2_name.mx_RoomTile2_nameHasUnreadEvents { - font-weight: 600; + font-weight: 700; } .mx_RoomTile2_messagePreview { From eeb408a0810e96afbb178652acbace7b0b6b0841 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 19 Jun 2020 15:44:37 -0600 Subject: [PATCH 156/194] Update badge logic for new setting and behaviour For https://github.com/vector-im/riot-web/issues/14084 --- .../views/rooms/NotificationBadge.tsx | 50 ++++++++++++++++--- src/components/views/rooms/RoomSublist2.tsx | 2 +- src/components/views/rooms/RoomTile2.tsx | 8 ++- src/settings/Settings.js | 5 ++ 4 files changed, 57 insertions(+), 8 deletions(-) diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx index b742f8e8e7..65ccc88c5f 100644 --- a/src/components/views/rooms/NotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge.tsx @@ -32,13 +32,14 @@ import ActiveRoomObserver from "../../../ActiveRoomObserver"; import { EventEmitter } from "events"; import { arrayDiff } from "../../../utils/arrays"; import { IDestroyable } from "../../../utils/IDestroyable"; +import SettingsStore from "../../../settings/SettingsStore"; export const NOTIFICATION_STATE_UPDATE = "update"; export enum NotificationColor { // Inverted (None -> Red) because we do integer comparisons on this None, // nothing special - Bold, // no badge, show as unread + Bold, // no badge, show as unread // TODO: This goes away with new notification structures Grey, // unread notified messages Red, // unread pings } @@ -53,18 +54,45 @@ interface IProps { notification: INotificationState; /** - * If true, the badge will conditionally display a badge without count for the user. + * If true, the badge will show a count if at all possible. This is typically + * used to override the user's preference for things like room sublists. */ - allowNoCount: boolean; + forceCount: boolean; + + /** + * The room ID, if any, the badge represents. + */ + roomId?: string; } interface IState { + showCounts: boolean; // whether or not to show counts. Independent of props.forceCount } export default class NotificationBadge extends React.PureComponent { + private countWatcherRef: string; + constructor(props: IProps) { super(props); this.props.notification.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate); + + this.state = { + showCounts: SettingsStore.getValue("Notifications.alwaysShowBadgeCounts", this.roomId), + }; + + this.countWatcherRef = SettingsStore.watchSetting( + "Notifications.alwaysShowBadgeCounts", this.roomId, + this.countPreferenceChanged, + ); + } + + private get roomId(): string { + // We should convert this to null for safety with the SettingsStore + return this.props.roomId || null; + } + + public componentWillUnmount() { + SettingsStore.unwatchSetting(this.countWatcherRef); } public componentDidUpdate(prevProps: Readonly) { @@ -75,24 +103,34 @@ export default class NotificationBadge extends React.PureComponent { + this.setState({showCounts: SettingsStore.getValue("Notifications.alwaysShowBadgeCounts", this.roomId)}); + }; + private onNotificationUpdate = () => { this.forceUpdate(); // notification state changed - update }; public render(): React.ReactElement { // Don't show a badge if we don't need to - if (this.props.notification.color <= NotificationColor.Bold) return null; + if (this.props.notification.color <= NotificationColor.None) return null; const hasNotif = this.props.notification.color >= NotificationColor.Red; const hasCount = this.props.notification.color >= NotificationColor.Grey; - const isEmptyBadge = this.props.allowNoCount && !localStorage.getItem("mx_rl_rt_badgeCount"); + const hasUnread = this.props.notification.color >= NotificationColor.Bold; + const couldBeEmpty = !this.state.showCounts || hasUnread; + let isEmptyBadge = couldBeEmpty && (!this.state.showCounts || !hasCount); + if (this.props.forceCount) { + isEmptyBadge = false; + if (!hasCount) return null; // Can't render a badge + } let symbol = this.props.notification.symbol || formatMinimalBadgeCount(this.props.notification.count); if (isEmptyBadge) symbol = ""; const classes = classNames({ 'mx_NotificationBadge': true, - 'mx_NotificationBadge_visible': hasCount, + 'mx_NotificationBadge_visible': isEmptyBadge ? true : hasCount, 'mx_NotificationBadge_highlighted': hasNotif, 'mx_NotificationBadge_dot': isEmptyBadge, 'mx_NotificationBadge_2char': symbol.length > 0 && symbol.length < 3, diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 08a41570e3..562c307769 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -267,7 +267,7 @@ export default class RoomSublist2 extends React.Component { // TODO: Collapsed state - const badge = ; + const badge = ; let addRoomButton = null; if (!!this.props.onAddRoom) { diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 9f4870d437..7f91b5ee9d 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -248,7 +248,13 @@ export default class RoomTile2 extends React.Component { 'mx_RoomTile2_minimized': this.props.isMinimized, }); - const badge = ; + const badge = ( + + ); // TODO: the original RoomTile uses state for the room name. Do we need to? let name = this.props.room.name; diff --git a/src/settings/Settings.js b/src/settings/Settings.js index ca8647e067..7cf0e629ec 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -188,6 +188,11 @@ export const SETTINGS = { default: true, invertedSettingName: 'MessageComposerInput.dontSuggestEmoji', }, + // TODO: Wire up appropriately to UI (FTUE notifications) + "Notifications.alwaysShowBadgeCounts": { + supportedLevels: LEVELS_ROOM_OR_ACCOUNT, + default: false, + }, "useCompactLayout": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Use compact timeline layout'), From 8201eed929025bc24ae341d74fef4a5e494883ed Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 19 Jun 2020 16:03:38 -0600 Subject: [PATCH 157/194] Encourage counts if the user has a mention (red state) --- src/components/views/rooms/NotificationBadge.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx index 65ccc88c5f..2ddf095b59 100644 --- a/src/components/views/rooms/NotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge.tsx @@ -118,7 +118,7 @@ export default class NotificationBadge extends React.PureComponent= NotificationColor.Red; const hasCount = this.props.notification.color >= NotificationColor.Grey; const hasUnread = this.props.notification.color >= NotificationColor.Bold; - const couldBeEmpty = !this.state.showCounts || hasUnread; + const couldBeEmpty = (!this.state.showCounts || hasUnread) && !hasNotif; let isEmptyBadge = couldBeEmpty && (!this.state.showCounts || !hasCount); if (this.props.forceCount) { isEmptyBadge = false; From 241e0c12f0c9203b2ebb706d2ac8bc57b587f54d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 19 Jun 2020 15:45:48 -0600 Subject: [PATCH 158/194] Trigger room-specific watchers whenever a higher level change happens Otherwise the room list badges end up having to listen to `null` for a room ID, meaning they have to filter. The notification badge count setting is the first ever setting to watch based on a room ID, so there are no compatibility concerns with this change. --- src/settings/WatchManager.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/settings/WatchManager.js b/src/settings/WatchManager.js index 472b13966f..3f54ca929e 100644 --- a/src/settings/WatchManager.js +++ b/src/settings/WatchManager.js @@ -51,8 +51,17 @@ export class WatchManager { const roomWatchers = this._watchers[settingName]; const callbacks = []; - if (inRoomId !== null && roomWatchers[inRoomId]) callbacks.push(...roomWatchers[inRoomId]); - if (roomWatchers[null]) callbacks.push(...roomWatchers[null]); + if (inRoomId !== null && roomWatchers[inRoomId]) { + callbacks.push(...roomWatchers[inRoomId]); + } + + if (!inRoomId) { + // Fire updates to all the individual room watchers too, as they probably + // care about the change higher up. + callbacks.push(...Object.values(roomWatchers).reduce((r, a) => [...r, ...a], [])); + } else if (roomWatchers[null]) { + callbacks.push(...roomWatchers[null]); + } for (const callback of callbacks) { callback(inRoomId, atLevel, newValueAtLevel); From 63ad14ae1e7a06a456b6b43a0e11bf803d3c3049 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 22 Jun 2020 10:35:55 -0600 Subject: [PATCH 159/194] Clean up imports --- src/components/views/rooms/NotificationBadge.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx index 2ddf095b59..36c269beba 100644 --- a/src/components/views/rooms/NotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge.tsx @@ -18,17 +18,11 @@ import React from "react"; import classNames from "classnames"; import { formatMinimalBadgeCount } from "../../../utils/FormattingUtils"; import { Room } from "matrix-js-sdk/src/models/room"; -import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; -import AccessibleButton from "../../views/elements/AccessibleButton"; -import RoomAvatar from "../../views/avatars/RoomAvatar"; -import dis from '../../../dispatcher/dispatcher'; -import { Key } from "../../../Keyboard"; import * as RoomNotifs from '../../../RoomNotifs'; import { EffectiveMembership, getEffectiveMembership } from "../../../stores/room-list/membership"; import * as Unread from '../../../Unread'; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import ActiveRoomObserver from "../../../ActiveRoomObserver"; import { EventEmitter } from "events"; import { arrayDiff } from "../../../utils/arrays"; import { IDestroyable } from "../../../utils/IDestroyable"; From fe65b7631dfd9b985e1e1f852a4e795dca617fd9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 22 Jun 2020 10:57:08 -0600 Subject: [PATCH 160/194] Soften warning about lack of rooms in setting updates --- src/settings/handlers/RoomSettingsHandler.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/settings/handlers/RoomSettingsHandler.js b/src/settings/handlers/RoomSettingsHandler.js index 4116f26220..2ed82b577d 100644 --- a/src/settings/handlers/RoomSettingsHandler.js +++ b/src/settings/handlers/RoomSettingsHandler.js @@ -42,9 +42,12 @@ export default class RoomSettingsHandler extends MatrixClientBackedSettingsHandl _onEvent(event, state, prevEvent) { const roomId = event.getRoomId(); const room = this.client.getRoom(roomId); - if (!room) throw new Error(`Unknown room caused state update: ${roomId}`); - if (state !== room.currentState) return; // ignore state updates which are not current + // Note: the tests often fire setting updates that don't have rooms in the store, so + // we fail softly here. We shouldn't assume that the state being fired is current + // state, but we also don't need to explode just because we didn't find a room. + if (!room) console.warn(`Unknown room caused setting update: ${roomId}`); + if (room && state !== room.currentState) return; // ignore state updates which are not current if (event.getType() === "org.matrix.room.preview_urls") { let val = event.getContent()['disable']; From 784e73831bb070237e63ba2e75d527ac5b071a01 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 22 Jun 2020 11:23:38 -0600 Subject: [PATCH 161/194] Move setting to account only (no per-room) --- src/settings/Settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 7cf0e629ec..028f355ab8 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -190,7 +190,7 @@ export const SETTINGS = { }, // TODO: Wire up appropriately to UI (FTUE notifications) "Notifications.alwaysShowBadgeCounts": { - supportedLevels: LEVELS_ROOM_OR_ACCOUNT, + supportedLevels: ['account'], default: false, }, "useCompactLayout": { From 9e3c101172c1f14c827de53d0d548ba79dbff6c6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 22 Jun 2020 11:24:04 -0600 Subject: [PATCH 162/194] Clone reads of account data to prevent mutation --- src/settings/handlers/AccountSettingsHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings/handlers/AccountSettingsHandler.js b/src/settings/handlers/AccountSettingsHandler.js index 7b78c39c3a..c396a9d4de 100644 --- a/src/settings/handlers/AccountSettingsHandler.js +++ b/src/settings/handlers/AccountSettingsHandler.js @@ -162,7 +162,7 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa const event = cli.getAccountData(eventType); if (!event || !event.getContent()) return null; - return event.getContent(); + return JSON.parse(JSON.stringify(event.getContent())); // clone to prevent mutation } _notifyBreadcrumbsUpdate(event) { From 37442b92aed4395e76fdce30580c6cf519fe7fd0 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 22 Jun 2020 19:02:03 +0100 Subject: [PATCH 163/194] Update read receipt remainder for internal font size change In https://github.com/matrix-org/matrix-react-sdk/pull/4725, we changed the internal font size from 15 to 10, but the `toRem` function (currently only used for read receipts remainders curiously) was not updated. This updates the function, which restores the remainders. Fixes https://github.com/vector-im/riot-web/issues/14127 --- src/utils/units.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/units.ts b/src/utils/units.ts index 54dd6b0523..03775f4c21 100644 --- a/src/utils/units.ts +++ b/src/utils/units.ts @@ -19,7 +19,7 @@ limitations under the License. // converts a pixel value to rem. export function toRem(pixelValue: number): string { - return pixelValue / 15 + "rem"; + return pixelValue / 10 + "rem"; } export function toPx(pixelValue: number): string { From 38bf1680a242ea8d50176791354a58326db54adc Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 22 Jun 2020 19:02:03 +0100 Subject: [PATCH 164/194] Update read receipt remainder for internal font size change In https://github.com/matrix-org/matrix-react-sdk/pull/4725, we changed the internal font size from 15 to 10, but the `toRem` function (currently only used for read receipts remainders curiously) was not updated. This updates the function, which restores the remainders. Fixes https://github.com/vector-im/riot-web/issues/14127 --- src/utils/units.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/units.ts b/src/utils/units.ts index 54dd6b0523..03775f4c21 100644 --- a/src/utils/units.ts +++ b/src/utils/units.ts @@ -19,7 +19,7 @@ limitations under the License. // converts a pixel value to rem. export function toRem(pixelValue: number): string { - return pixelValue / 15 + "rem"; + return pixelValue / 10 + "rem"; } export function toPx(pixelValue: number): string { From cf92fc37d42c10616835124543790aad1ff9b84d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 22 Jun 2020 12:51:53 -0600 Subject: [PATCH 165/194] Fix layout of minimized view for new room list --- res/css/views/rooms/_RoomSublist2.scss | 8 ++++---- src/components/views/rooms/RoomSublist2.tsx | 19 +++++++++++++++---- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index 66615fb6a8..24151b15b0 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -304,18 +304,18 @@ limitations under the License. position: relative; .mx_RoomSublist2_badgeContainer { - order: 1; + order: 0; align-self: flex-end; margin-right: 0; } - .mx_RoomSublist2_headerText { - order: 2; + .mx_RoomSublist2_stickable { + order: 1; max-width: 100%; } .mx_RoomSublist2_auxButton { - order: 4; + order: 2; visibility: visible; width: 32px !important; // !important to override hover styles height: 32px !important; // !important to override hover styles diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 08a41570e3..4a145ede11 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -291,7 +291,18 @@ export default class RoomSublist2 extends React.Component { 'mx_RoomSublist2_headerContainer_withAux': !!addRoomButton, }); + const badgeContainer = ( +
          + {badge} +
          + ); + // TODO: a11y (see old component) + // Note: the addRoomButton conditionally gets moved around + // the DOM depending on whether or not the list is minimized. + // If we're minimized, we want it below the header so it + // doesn't become sticky. + // The same applies to the notification badge. return (
          @@ -307,11 +318,11 @@ export default class RoomSublist2 extends React.Component { {this.props.label} {this.renderMenu()} - {addRoomButton} -
          - {badge} -
          + {this.props.isMinimized ? null : addRoomButton} + {this.props.isMinimized ? null : badgeContainer}
          + {this.props.isMinimized ? badgeContainer : null} + {this.props.isMinimized ? addRoomButton : null}
          ); }} From 1a427b8ff78522f48d9ca885d277da68b4010363 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 22 Jun 2020 13:09:42 -0600 Subject: [PATCH 166/194] Fix sticky headers over/under extending themselves in the new room list Fixes https://github.com/vector-im/riot-web/issues/14095 --- src/components/structures/LeftPanel2.tsx | 35 +++++++++++++++++----- src/components/structures/LoggedInView.tsx | 5 +++- src/utils/ResizeNotifier.js | 21 ++++++++++--- 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index b5da44caef..378a24a70e 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -19,7 +19,6 @@ import TagPanel from "./TagPanel"; import classNames from "classnames"; import dis from "../../dispatcher/dispatcher"; import { _t } from "../../languageHandler"; -import SearchBox from "./SearchBox"; import RoomList2 from "../views/rooms/RoomList2"; import { Action } from "../../dispatcher/actions"; import { MatrixClientPeg } from "../../MatrixClientPeg"; @@ -30,6 +29,8 @@ import AccessibleButton from "../views/elements/AccessibleButton"; import RoomBreadcrumbs2 from "../views/rooms/RoomBreadcrumbs2"; import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore"; import { UPDATE_EVENT } from "../../stores/AsyncStore"; +import ResizeNotifier from "../../utils/ResizeNotifier"; +import { createRef } from "react"; /******************************************************************* * CAUTION * @@ -41,6 +42,7 @@ import { UPDATE_EVENT } from "../../stores/AsyncStore"; interface IProps { isMinimized: boolean; + resizeNotifier: ResizeNotifier; } interface IState { @@ -49,6 +51,8 @@ interface IState { } export default class LeftPanel2 extends React.Component { + private listContainerRef: React.RefObject = createRef(); + // TODO: Properly support TagPanel // TODO: Properly support searching/filtering // TODO: Properly support breadcrumbs @@ -65,10 +69,15 @@ export default class LeftPanel2 extends React.Component { }; BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); + + // We watch the middle panel because we don't actually get resized, the middle panel does. + // We listen to the noisy channel to avoid choppy reaction times. + this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize); } public componentWillUnmount() { BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); + this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize); } private onSearch = (term: string): void => { @@ -86,9 +95,7 @@ export default class LeftPanel2 extends React.Component { } }; - // TODO: Apply this on resize, init, etc for reliability - private onScroll = (ev: React.MouseEvent) => { - const list = ev.target as HTMLDivElement; + private handleStickyHeaders(list: HTMLDivElement) { const rlRect = list.getBoundingClientRect(); const bottom = rlRect.bottom; const top = rlRect.top; @@ -123,6 +130,18 @@ export default class LeftPanel2 extends React.Component { header.style.top = `unset`; } } + } + + // TODO: Apply this on resize, init, etc for reliability + private onScroll = (ev: React.MouseEvent) => { + const list = ev.target as HTMLDivElement; + this.handleStickyHeaders(list); + }; + + private onResize = () => { + console.log("Resize width"); + if (!this.listContainerRef.current) return; // ignore: no headers to sticky + this.handleStickyHeaders(this.listContainerRef.current); }; private renderHeader(): React.ReactNode { @@ -230,9 +249,11 @@ export default class LeftPanel2 extends React.Component {
          ); diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index f37f77b31b..1bc656e6a3 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -677,7 +677,10 @@ class LoggedInView extends React.PureComponent { if (SettingsStore.isFeatureEnabled("feature_new_room_list")) { // TODO: Supply props like collapsed and disabled to LeftPanel2 leftPanel = ( - + ); } diff --git a/src/utils/ResizeNotifier.js b/src/utils/ResizeNotifier.js index d65bc4bd07..f726a43e08 100644 --- a/src/utils/ResizeNotifier.js +++ b/src/utils/ResizeNotifier.js @@ -15,9 +15,13 @@ limitations under the License. */ /** - * Fires when the middle panel has been resized. + * Fires when the middle panel has been resized (throttled). * @event module:utils~ResizeNotifier#"middlePanelResized" */ +/** + * Fires when the middle panel has been resized by a pixel. + * @event module:utils~ResizeNotifier#"middlePanelResizedNoisy" + */ import { EventEmitter } from "events"; import { throttle } from "lodash"; @@ -29,15 +33,24 @@ export default class ResizeNotifier extends EventEmitter { this._throttledMiddlePanel = throttle(() => this.emit("middlePanelResized"), 200); } + _noisyMiddlePanel() { + this.emit("middlePanelResizedNoisy"); + } + + _updateMiddlePanel() { + this._throttledMiddlePanel(); + this._noisyMiddlePanel(); + } + // can be called in quick succession notifyLeftHandleResized() { // don't emit event for own region - this._throttledMiddlePanel(); + this._updateMiddlePanel(); } // can be called in quick succession notifyRightHandleResized() { - this._throttledMiddlePanel(); + this._updateMiddlePanel(); } // can be called in quick succession @@ -48,7 +61,7 @@ export default class ResizeNotifier extends EventEmitter { // taller than the available space this.emit("leftPanelResized"); - this._throttledMiddlePanel(); + this._updateMiddlePanel(); } } From 6c48966bf5417cacc4c50cf2f7bee779371d576c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 22 Jun 2020 13:34:49 -0600 Subject: [PATCH 167/194] Have the theme switcher set the device-level theme to match settings Fixes https://github.com/vector-im/riot-web/issues/14111 This is a shortcut into the Appearance tab, so use the same level. It was an explicit decision to have the tab set the theme at the device level. --- src/components/structures/UserMenuButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/UserMenuButton.tsx b/src/components/structures/UserMenuButton.tsx index 6607fffdd1..04b1b03368 100644 --- a/src/components/structures/UserMenuButton.tsx +++ b/src/components/structures/UserMenuButton.tsx @@ -117,7 +117,7 @@ export default class UserMenuButton extends React.Component { SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, false); const newTheme = this.state.isDarkTheme ? "light" : "dark"; - SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme); + SettingsStore.setValue("theme", null, SettingLevel.DEVICE, newTheme); // set at same level as Appearance tab }; private onSettingsOpen = (ev: ButtonEvent, tabId: string) => { From 1fe3e33dbf944cc0f078c18c253d019523af540d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 22 Jun 2020 14:14:43 -0600 Subject: [PATCH 168/194] Factor out cloning to a util and use it everywhere --- src/FromWidgetPostMessageApi.js | 5 +++-- src/ScalarMessaging.js | 5 +++-- src/components/views/terms/InlineTermsAgreement.js | 3 ++- src/settings/handlers/AccountSettingsHandler.js | 4 ++-- src/settings/handlers/RoomAccountSettingsHandler.js | 4 ++-- src/settings/handlers/RoomSettingsHandler.js | 4 ++-- src/utils/WidgetUtils.js | 3 ++- src/utils/objects.ts | 11 +++++++++++ src/widgets/WidgetApi.ts | 3 ++- 9 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js index 102afa6bf1..1b4aa19ebf 100644 --- a/src/FromWidgetPostMessageApi.js +++ b/src/FromWidgetPostMessageApi.js @@ -25,6 +25,7 @@ import RoomViewStore from "./stores/RoomViewStore"; import {IntegrationManagers} from "./integrations/IntegrationManagers"; import SettingsStore from "./settings/SettingsStore"; import {Capability} from "./widgets/WidgetApi"; +import {objectClone} from "./utils/objects"; const WIDGET_API_VERSION = '0.0.2'; // Current API version const SUPPORTED_WIDGET_API_VERSIONS = [ @@ -247,7 +248,7 @@ export default class FromWidgetPostMessageApi { * @param {Object} res Response data */ sendResponse(event, res) { - const data = JSON.parse(JSON.stringify(event.data)); + const data = objectClone(event.data); data.response = res; event.source.postMessage(data, event.origin); } @@ -260,7 +261,7 @@ export default class FromWidgetPostMessageApi { */ sendError(event, msg, nestedError) { console.error('Action:' + event.data.action + ' failed with message: ' + msg); - const data = JSON.parse(JSON.stringify(event.data)); + const data = objectClone(event.data); data.response = { error: { message: msg, diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index 315c2d86f4..b33aa57359 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -244,16 +244,17 @@ import RoomViewStore from './stores/RoomViewStore'; import { _t } from './languageHandler'; import {IntegrationManagers} from "./integrations/IntegrationManagers"; import {WidgetType} from "./widgets/WidgetType"; +import {objectClone} from "./utils/objects"; function sendResponse(event, res) { - const data = JSON.parse(JSON.stringify(event.data)); + const data = objectClone(event.data); data.response = res; event.source.postMessage(data, event.origin); } function sendError(event, msg, nestedError) { console.error("Action:" + event.data.action + " failed with message: " + msg); - const data = JSON.parse(JSON.stringify(event.data)); + const data = objectClone(event.data); data.response = { error: { message: msg, diff --git a/src/components/views/terms/InlineTermsAgreement.js b/src/components/views/terms/InlineTermsAgreement.js index bccd686cd3..55719fe57f 100644 --- a/src/components/views/terms/InlineTermsAgreement.js +++ b/src/components/views/terms/InlineTermsAgreement.js @@ -18,6 +18,7 @@ import React from "react"; import PropTypes from "prop-types"; import {_t, pickBestLanguage} from "../../../languageHandler"; import * as sdk from "../../.."; +import {objectClone} from "../../../utils/objects"; export default class InlineTermsAgreement extends React.Component { static propTypes = { @@ -56,7 +57,7 @@ export default class InlineTermsAgreement extends React.Component { } _togglePolicy = (index) => { - const policies = JSON.parse(JSON.stringify(this.state.policies)); // deep & cheap clone + const policies = objectClone(this.state.policies); policies[index].checked = !policies[index].checked; this.setState({policies}); }; diff --git a/src/settings/handlers/AccountSettingsHandler.js b/src/settings/handlers/AccountSettingsHandler.js index c396a9d4de..732ce6c550 100644 --- a/src/settings/handlers/AccountSettingsHandler.js +++ b/src/settings/handlers/AccountSettingsHandler.js @@ -18,7 +18,7 @@ limitations under the License. import {MatrixClientPeg} from '../../MatrixClientPeg'; import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler"; import {SettingLevel} from "../SettingsStore"; -import {objectKeyChanges} from "../../utils/objects"; +import {objectClone, objectKeyChanges} from "../../utils/objects"; const BREADCRUMBS_LEGACY_EVENT_TYPE = "im.vector.riot.breadcrumb_rooms"; const BREADCRUMBS_EVENT_TYPE = "im.vector.setting.breadcrumbs"; @@ -162,7 +162,7 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa const event = cli.getAccountData(eventType); if (!event || !event.getContent()) return null; - return JSON.parse(JSON.stringify(event.getContent())); // clone to prevent mutation + return objectClone(event.getContent()); // clone to prevent mutation } _notifyBreadcrumbsUpdate(event) { diff --git a/src/settings/handlers/RoomAccountSettingsHandler.js b/src/settings/handlers/RoomAccountSettingsHandler.js index 1c818cef71..b2af81779b 100644 --- a/src/settings/handlers/RoomAccountSettingsHandler.js +++ b/src/settings/handlers/RoomAccountSettingsHandler.js @@ -18,7 +18,7 @@ limitations under the License. import {MatrixClientPeg} from '../../MatrixClientPeg'; import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler"; import {SettingLevel} from "../SettingsStore"; -import {objectKeyChanges} from "../../utils/objects"; +import {objectClone, objectKeyChanges} from "../../utils/objects"; const ALLOWED_WIDGETS_EVENT_TYPE = "im.vector.setting.allowed_widgets"; @@ -137,6 +137,6 @@ export default class RoomAccountSettingsHandler extends MatrixClientBackedSettin const event = room.getAccountData(eventType); if (!event || !event.getContent()) return null; - return event.getContent(); + return objectClone(event.getContent()); // clone to prevent mutation } } diff --git a/src/settings/handlers/RoomSettingsHandler.js b/src/settings/handlers/RoomSettingsHandler.js index 2ed82b577d..d8e775742c 100644 --- a/src/settings/handlers/RoomSettingsHandler.js +++ b/src/settings/handlers/RoomSettingsHandler.js @@ -18,7 +18,7 @@ limitations under the License. import {MatrixClientPeg} from '../../MatrixClientPeg'; import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler"; import {SettingLevel} from "../SettingsStore"; -import {objectKeyChanges} from "../../utils/objects"; +import {objectClone, objectKeyChanges} from "../../utils/objects"; /** * Gets and sets settings at the "room" level. @@ -117,6 +117,6 @@ export default class RoomSettingsHandler extends MatrixClientBackedSettingsHandl const event = room.currentState.getStateEvents(eventType, ""); if (!event || !event.getContent()) return null; - return event.getContent(); + return objectClone(event.getContent()); // clone to prevent mutation } } diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js index b48ec481ba..f7f4be202b 100644 --- a/src/utils/WidgetUtils.js +++ b/src/utils/WidgetUtils.js @@ -31,6 +31,7 @@ import {IntegrationManagers} from "../integrations/IntegrationManagers"; import {Capability} from "../widgets/WidgetApi"; import {Room} from "matrix-js-sdk/src/models/room"; import {WidgetType} from "../widgets/WidgetType"; +import {objectClone} from "./objects"; export default class WidgetUtils { /* Returns true if user is able to send state events to modify widgets in this room @@ -222,7 +223,7 @@ export default class WidgetUtils { const client = MatrixClientPeg.get(); // Get the current widgets and clone them before we modify them, otherwise // we'll modify the content of the old event. - const userWidgets = JSON.parse(JSON.stringify(WidgetUtils.getUserWidgets())); + const userWidgets = objectClone(WidgetUtils.getUserWidgets()); // Delete existing widget with ID try { diff --git a/src/utils/objects.ts b/src/utils/objects.ts index cbb311cc48..14fa928ce2 100644 --- a/src/utils/objects.ts +++ b/src/utils/objects.ts @@ -47,3 +47,14 @@ export function objectKeyChanges(a: any, b: any): string[] { const diff = objectDiff(a, b); return arrayMerge(diff.removed, diff.added, diff.changed); } + +/** + * Clones an object by running it through JSON parsing. Note that this + * will destroy any complicated object types which do not translate to + * JSON. + * @param obj The object to clone. + * @returns The cloned object + */ +export function objectClone(obj: any): any { + return JSON.parse(JSON.stringify(obj)); +} diff --git a/src/widgets/WidgetApi.ts b/src/widgets/WidgetApi.ts index 795c6648ef..d5f2f2258e 100644 --- a/src/widgets/WidgetApi.ts +++ b/src/widgets/WidgetApi.ts @@ -19,6 +19,7 @@ limitations under the License. import { randomString } from "matrix-js-sdk/src/randomstring"; import { EventEmitter } from "events"; +import { objectClone } from "../utils/objects"; export enum Capability { Screenshot = "m.capability.screenshot", @@ -140,7 +141,7 @@ export class WidgetApi extends EventEmitter { private replyToRequest(payload: ToWidgetRequest, reply: any) { if (!window.parent) return; - const request = JSON.parse(JSON.stringify(payload)); + const request = objectClone(payload); request.response = reply; window.parent.postMessage(request, this.origin); From fb551781c20dc7f872e866b615126d1fd1b70a55 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 22 Jun 2020 14:52:17 -0600 Subject: [PATCH 169/194] Force DMs to always be red notifications This also passes the tagId to the sublist so it doesn't have to rip it out of the `layout`. It doesn't get a layout until later anyways, which causes some null issues. --- .../views/rooms/NotificationBadge.tsx | 40 +++++++++++++++++-- src/components/views/rooms/RoomList2.tsx | 1 + src/components/views/rooms/RoomSublist2.tsx | 20 +++++----- src/components/views/rooms/RoomTile2.tsx | 8 +++- 4 files changed, 54 insertions(+), 15 deletions(-) diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx index 36c269beba..37b61714b9 100644 --- a/src/components/views/rooms/NotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge.tsx @@ -27,6 +27,7 @@ import { EventEmitter } from "events"; import { arrayDiff } from "../../../utils/arrays"; import { IDestroyable } from "../../../utils/IDestroyable"; import SettingsStore from "../../../settings/SettingsStore"; +import { DefaultTagID, TagID } from "../../../stores/room-list/models"; export const NOTIFICATION_STATE_UPDATE = "update"; @@ -139,7 +140,7 @@ export default class NotificationBadge extends React.PureComponent { components.push( { super(props); this.state = { - notificationState: new ListNotificationState(this.props.isInvite), + notificationState: new ListNotificationState(this.props.isInvite, this.props.tagId), menuDisplayed: false, }; this.state.notificationState.setRooms(this.props.rooms); @@ -130,13 +132,13 @@ export default class RoomSublist2 extends React.Component { }; private onUnreadFirstChanged = async () => { - const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.layout.tagId) === ListAlgorithm.Importance; + const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance; const newAlgorithm = isUnreadFirst ? ListAlgorithm.Natural : ListAlgorithm.Importance; - await RoomListStore.instance.setListOrder(this.props.layout.tagId, newAlgorithm); + await RoomListStore.instance.setListOrder(this.props.tagId, newAlgorithm); }; private onTagSortChanged = async (sort: SortAlgorithm) => { - await RoomListStore.instance.setTagSorting(this.props.layout.tagId, sort); + await RoomListStore.instance.setTagSorting(this.props.tagId, sort); }; private onMessagePreviewChanged = () => { @@ -176,7 +178,7 @@ export default class RoomSublist2 extends React.Component { key={`room-${room.roomId}`} showMessagePreview={this.props.layout.showPreviews} isMinimized={this.props.isMinimized} - tag={this.props.layout.tagId} + tag={this.props.tagId} /> ); } @@ -189,8 +191,8 @@ export default class RoomSublist2 extends React.Component { let contextMenu = null; if (this.state.menuDisplayed) { const elementRect = this.menuButtonRef.current.getBoundingClientRect(); - const isAlphabetical = RoomListStore.instance.getTagSorting(this.props.layout.tagId) === SortAlgorithm.Alphabetic; - const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.layout.tagId) === ListAlgorithm.Importance; + const isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic; + const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance; contextMenu = ( { this.onTagSortChanged(SortAlgorithm.Recent)} checked={!isAlphabetical} - name={`mx_${this.props.layout.tagId}_sortBy`} + name={`mx_${this.props.tagId}_sortBy`} > {_t("Activity")} this.onTagSortChanged(SortAlgorithm.Alphabetic)} checked={isAlphabetical} - name={`mx_${this.props.layout.tagId}_sortBy`} + name={`mx_${this.props.tagId}_sortBy`} > {_t("A-Z")} diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 7f91b5ee9d..18b4ee8185 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -26,7 +26,11 @@ import RoomAvatar from "../../views/avatars/RoomAvatar"; import dis from '../../../dispatcher/dispatcher'; import { Key } from "../../../Keyboard"; import ActiveRoomObserver from "../../../ActiveRoomObserver"; -import NotificationBadge, { INotificationState, NotificationColor, RoomNotificationState } from "./NotificationBadge"; +import NotificationBadge, { + INotificationState, + NotificationColor, + TagSpecificNotificationState +} from "./NotificationBadge"; import { _t } from "../../../languageHandler"; import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu"; import { DefaultTagID, TagID } from "../../../stores/room-list/models"; @@ -79,7 +83,7 @@ export default class RoomTile2 extends React.Component { this.state = { hover: false, - notificationState: new RoomNotificationState(this.props.room), + notificationState: new TagSpecificNotificationState(this.props.room, this.props.tag), selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId, generalMenuDisplayed: false, }; From fc5ee64fce2f14a7dfcf971effde6ddb0beb9e5a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 22 Jun 2020 15:12:30 -0600 Subject: [PATCH 170/194] Fix read receipt handling in the new room list Fixes https://github.com/vector-im/riot-web/issues/14064 Fixes https://github.com/vector-im/riot-web/issues/14082 Turns out the event doesn't reference a room, so we need to use the accompanied room reference instead. --- src/components/views/rooms/NotificationBadge.tsx | 11 +++++++++-- src/stores/room-list/RoomListStore2.ts | 6 +++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx index 37b61714b9..523b5a55cc 100644 --- a/src/components/views/rooms/NotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge.tsx @@ -28,6 +28,7 @@ import { arrayDiff } from "../../../utils/arrays"; import { IDestroyable } from "../../../utils/IDestroyable"; import SettingsStore from "../../../settings/SettingsStore"; import { DefaultTagID, TagID } from "../../../stores/room-list/models"; +import { readReceiptChangeIsFor } from "../../../utils/read-receipts"; export const NOTIFICATION_STATE_UPDATE = "update"; @@ -147,7 +148,7 @@ export class RoomNotificationState extends EventEmitter implements IDestroyable, constructor(private room: Room) { super(); - this.room.on("Room.receipt", this.handleRoomEventUpdate); + this.room.on("Room.receipt", this.handleReadReceipt); this.room.on("Room.timeline", this.handleRoomEventUpdate); this.room.on("Room.redaction", this.handleRoomEventUpdate); MatrixClientPeg.get().on("Event.decrypted", this.handleRoomEventUpdate); @@ -171,7 +172,7 @@ export class RoomNotificationState extends EventEmitter implements IDestroyable, } public destroy(): void { - this.room.removeListener("Room.receipt", this.handleRoomEventUpdate); + this.room.removeListener("Room.receipt", this.handleReadReceipt); this.room.removeListener("Room.timeline", this.handleRoomEventUpdate); this.room.removeListener("Room.redaction", this.handleRoomEventUpdate); if (MatrixClientPeg.get()) { @@ -179,6 +180,12 @@ export class RoomNotificationState extends EventEmitter implements IDestroyable, } } + private handleReadReceipt = (event: MatrixEvent, room: Room) => { + if (!readReceiptChangeIsFor(event, MatrixClientPeg.get())) return; // not our own - ignore + if (room.roomId !== this.room.roomId) return; // not for us - ignore + this.updateNotificationState(); + }; + private handleRoomEventUpdate = (event: MatrixEvent) => { const roomId = event.getRoomId(); diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index 9684e338f8..99eee82d4e 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -158,12 +158,12 @@ export class RoomListStore2 extends AsyncStore { // First see if the receipt event is for our own user. If it was, trigger // a room update (we probably read the room on a different device). if (readReceiptChangeIsFor(payload.event, this.matrixClient)) { - console.log(`[RoomListDebug] Got own read receipt in ${payload.event.roomId}`); - const room = this.matrixClient.getRoom(payload.event.roomId); + const room = payload.room; if (!room) { - console.warn(`Own read receipt was in unknown room ${payload.event.roomId}`); + console.warn(`Own read receipt was in unknown room ${room.roomId}`); return; } + console.log(`[RoomListDebug] Got own read receipt in ${room.roomId}`); await this.handleRoomUpdate(room, RoomUpdateCause.ReadReceipt); return; } From 115c850d35b64491bb3170374bc8d91f332c9e88 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 22 Jun 2020 15:57:13 -0600 Subject: [PATCH 171/194] Use the correct timeline reference for message previews Fixes https://github.com/vector-im/riot-web/issues/14083 (hopefully) This is the same logic used by `Unread.js`, so should be correct. --- src/stores/MessagePreviewStore.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/stores/MessagePreviewStore.ts b/src/stores/MessagePreviewStore.ts index 64d65a72f3..9e225d1709 100644 --- a/src/stores/MessagePreviewStore.ts +++ b/src/stores/MessagePreviewStore.ts @@ -78,9 +78,8 @@ export class MessagePreviewStore extends AsyncStoreWithClient { } private generatePreview(room: Room) { - const timeline = room.getLiveTimeline(); - if (!timeline) return; // usually only happens in tests - const events = timeline.getEvents(); + const events = room.timeline; + if (!events) return; // should only happen in tests for (let i = events.length - 1; i >= 0; i--) { if (i === events.length - MAX_EVENTS_BACKWARDS) return; // limit reached From 680e997a94ff7b34ccfa6638f6bd8b33fe8b8896 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 23 Jun 2020 13:38:50 +0100 Subject: [PATCH 172/194] Cleanup tooltip classnames --- src/components/views/elements/Field.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index 9d53576259..834edff7df 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -244,9 +244,8 @@ export default class Field extends React.PureComponent { const Tooltip = sdk.getComponent("elements.Tooltip"); let fieldTooltip; if (tooltipContent || this.state.feedback) { - const addClassName = tooltipClassName ? tooltipClassName : ''; fieldTooltip = ; From 54ba75afd7e8ebc6809c7dcb2c678ee43bbece7b Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 23 Jun 2020 14:48:23 +0100 Subject: [PATCH 173/194] Upgrade matrix-js-sdk to 7.0.0 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 06c4c43622..07cb11d45b 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "is-ip": "^2.0.0", "linkifyjs": "^2.1.6", "lodash": "^4.17.14", - "matrix-js-sdk": "7.0.0-rc.1", + "matrix-js-sdk": "7.0.0", "minimist": "^1.2.0", "pako": "^1.0.5", "parse5": "^5.1.1", diff --git a/yarn.lock b/yarn.lock index 2bb99f4602..2aa39a257f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5820,10 +5820,10 @@ mathml-tag-names@^2.0.1: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -matrix-js-sdk@7.0.0-rc.1: - version "7.0.0-rc.1" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-7.0.0-rc.1.tgz#95a258218f5c5ec73ec4be510b28768c35809a0b" - integrity sha512-1znl0d2UxU6Mmimy+pMSQP1lQfsmDb9jxiKV5sfMvTBsLtUE2cTqEBVDNVoOHL4UJ9U4oMLsrBgu3sELkgSJLQ== +matrix-js-sdk@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-7.0.0.tgz#da2b24e57574379c3d8f7065eb68ea6c479d9806" + integrity sha512-0i1NmfwS5HzEPPjqUAXpw81o+8DImBS67QQBemJiM6D/imU3KFBacdhkmgjMLKXnAfTy6a+aCGfYBfVolfmNQw== dependencies: "@babel/runtime" "^7.8.3" another-json "^0.2.0" From 059d5f927d549a6ebb22e6adbbbd3144814a3b31 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 23 Jun 2020 15:06:52 +0100 Subject: [PATCH 174/194] Prepare changelog for v2.8.0 --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d7a73b264..21bfbf0a93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +Changes in [2.8.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.8.0) (2020-06-23) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.8.0-rc.1...v2.8.0) + + * Update read receipt remainder for internal font size change + [\#4807](https://github.com/matrix-org/matrix-react-sdk/pull/4807) + * Revert "Use recovery keys over passphrases" + [\#4793](https://github.com/matrix-org/matrix-react-sdk/pull/4793) + Changes in [2.8.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.8.0-rc.1) (2020-06-17) ============================================================================================================= [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.7.2...v2.8.0-rc.1) From 5256a86545d601a4579024f5650ce16ddc2d2f87 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 23 Jun 2020 15:06:53 +0100 Subject: [PATCH 175/194] v2.8.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 07cb11d45b..591922498f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "2.8.0-rc.1", + "version": "2.8.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From f3af6031028f0718450bb10d8e067e4504667674 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 23 Jun 2020 15:19:55 +0100 Subject: [PATCH 176/194] Reset matrix-js-sdk back to develop branch --- package.json | 2 +- yarn.lock | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 591922498f..673ea34a68 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "is-ip": "^2.0.0", "linkifyjs": "^2.1.6", "lodash": "^4.17.14", - "matrix-js-sdk": "7.0.0", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "minimist": "^1.2.0", "pako": "^1.0.5", "parse5": "^5.1.1", diff --git a/yarn.lock b/yarn.lock index 2aa39a257f..c20658f014 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5820,10 +5820,9 @@ mathml-tag-names@^2.0.1: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -matrix-js-sdk@7.0.0: +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": version "7.0.0" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-7.0.0.tgz#da2b24e57574379c3d8f7065eb68ea6c479d9806" - integrity sha512-0i1NmfwS5HzEPPjqUAXpw81o+8DImBS67QQBemJiM6D/imU3KFBacdhkmgjMLKXnAfTy6a+aCGfYBfVolfmNQw== + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/f683f4544aa5da150836b01c754062809119fa97" dependencies: "@babel/runtime" "^7.8.3" another-json "^0.2.0" From 38293627545e8ab1b7f4245e94bcd377f298ca10 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 23 Jun 2020 15:24:02 +0100 Subject: [PATCH 177/194] Fix up merge to develop --- src/components/structures/auth/SetupEncryptionBody.js | 8 ++++++++ src/stores/SetupEncryptionStore.js | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.js index 6b30f3c084..f2e702c8cb 100644 --- a/src/components/structures/auth/SetupEncryptionBody.js +++ b/src/components/structures/auth/SetupEncryptionBody.js @@ -28,6 +28,14 @@ import { PHASE_FINISHED, } from '../../../stores/SetupEncryptionStore'; +function keyHasPassphrase(keyInfo) { + return ( + keyInfo.passphrase && + keyInfo.passphrase.salt && + keyInfo.passphrase.iterations + ); +} + export default class SetupEncryptionBody extends React.Component { static propTypes = { onFinished: PropTypes.func.isRequired, diff --git a/src/stores/SetupEncryptionStore.js b/src/stores/SetupEncryptionStore.js index 2badf3bc33..63b8c428eb 100644 --- a/src/stores/SetupEncryptionStore.js +++ b/src/stores/SetupEncryptionStore.js @@ -36,7 +36,7 @@ export class SetupEncryptionStore extends EventEmitter { return; } this._started = true; - this.phase = PHASE_INTRO; + this.phase = PHASE_BUSY; this.verificationRequest = null; this.backupInfo = null; From 6739c90f442450f4cf036e777bb66cbbe32cbe0b Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 23 Jun 2020 15:25:59 +0100 Subject: [PATCH 178/194] Fix SDK in changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21bfbf0a93..964e75f7a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ Changes in [2.8.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.8.0-rc.1...v2.8.0) + * Upgrade to JS SDK 7.0.0 * Update read receipt remainder for internal font size change [\#4807](https://github.com/matrix-org/matrix-react-sdk/pull/4807) * Revert "Use recovery keys over passphrases" From 86597aabca88b538e4ec94cab3166704895f8567 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 23 Jun 2020 17:52:54 +0200 Subject: [PATCH 179/194] better naming --- src/theme.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/theme.js b/src/theme.js index 22cfd8b076..095990de3a 100644 --- a/src/theme.js +++ b/src/theme.js @@ -39,7 +39,7 @@ export function enumerateThemes() { function setCustomThemeVars(customTheme) { const {style} = document.body; - function setCSSVariable(name, hexColor, doPct = true) { + function setCSSColorVariable(name, hexColor, doPct = true) { style.setProperty(`--${name}`, hexColor); if (doPct) { // uses #rrggbbaa to define the color with alpha values at 0%, 15% and 50% @@ -53,10 +53,10 @@ function setCustomThemeVars(customTheme) { for (const [name, value] of Object.entries(customTheme.colors)) { if (Array.isArray(value)) { for (let i = 0; i < value.length; i += 1) { - setCSSVariable(`${name}_${i}`, value[i], false); + setCSSColorVariable(`${name}_${i}`, value[i], false); } } else { - setCSSVariable(name, value); + setCSSColorVariable(name, value); } } } From e083856801179eef1c6efb6452ea50b46d3d8f7e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 23 Jun 2020 17:53:16 +0200 Subject: [PATCH 180/194] allow changing the font-family --- res/themes/light-custom/css/_custom.scss | 2 ++ src/theme.js | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/res/themes/light-custom/css/_custom.scss b/res/themes/light-custom/css/_custom.scss index 6206496150..e7912e3cb0 100644 --- a/res/themes/light-custom/css/_custom.scss +++ b/res/themes/light-custom/css/_custom.scss @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +$font-family: var(--font-family, $font-family); +$monospace-font-family: var(--font-family-monospace, $monospace-font-family); // // --accent-color $accent-color: var(--accent-color); diff --git a/src/theme.js b/src/theme.js index 095990de3a..da70dc7bb1 100644 --- a/src/theme.js +++ b/src/theme.js @@ -60,6 +60,15 @@ function setCustomThemeVars(customTheme) { } } } + if (customTheme.fonts) { + const {fonts} = customTheme; + if (fonts.general) { + style.setProperty("--font-family", fonts.general); + } + if (fonts.monospace) { + style.setProperty("--font-family-monospace", fonts.monospace); + } + } } export function getCustomTheme(themeName) { From b3510aa2b47c3609810cbce2e4b2115b97291d0f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 23 Jun 2020 17:54:17 +0200 Subject: [PATCH 181/194] remove css vars when switching theme --- src/theme.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/theme.js b/src/theme.js index da70dc7bb1..7e0eaabf11 100644 --- a/src/theme.js +++ b/src/theme.js @@ -35,6 +35,16 @@ export function enumerateThemes() { return Object.assign({}, customThemeNames, BUILTIN_THEMES); } +function clearCustomTheme() { + // remove all css variables, we assume these are there because of the custom theme + const inlineStyleProps = Object.values(document.body.style); + for (const prop of inlineStyleProps) { + if (prop.startsWith("--")) { + document.body.style.removeProperty(prop); + } + } +} + function setCustomThemeVars(customTheme) { const {style} = document.body; @@ -97,6 +107,7 @@ export async function setTheme(theme) { const themeWatcher = new ThemeWatcher(); theme = themeWatcher.getEffectiveTheme(); } + clearCustomTheme(); let stylesheetName = theme; if (theme.startsWith("custom-")) { const customTheme = getCustomTheme(theme.substr(7)); From 2f6fc6bba24140367c5ecdd18d40d611251d9997 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 23 Jun 2020 17:54:38 +0200 Subject: [PATCH 182/194] allow adding custom font faces in theme --- src/theme.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/theme.js b/src/theme.js index 7e0eaabf11..d560cc96c8 100644 --- a/src/theme.js +++ b/src/theme.js @@ -43,8 +43,23 @@ function clearCustomTheme() { document.body.style.removeProperty(prop); } } + const customFontFaceStyle = document.querySelector("head > style[title='custom-theme-font-faces']"); + if (customFontFaceStyle) { + customFontFaceStyle.remove(); + } } +function generateCustomFontFaceCSS(faces) { + return Object.entries(faces).map(([fontName, face]) => { + const src = Object.entries(face.src).map(([format, url]) => { + return `url('${url}') format('${format}')`; + }).join(", "); + return `@font-face {` + + ` font-family: '${fontName}';` + + ` src: ${src};` + + `}`; + }).join("\n"); +} function setCustomThemeVars(customTheme) { const {style} = document.body; @@ -72,6 +87,14 @@ function setCustomThemeVars(customTheme) { } if (customTheme.fonts) { const {fonts} = customTheme; + if (fonts.faces) { + const css = generateCustomFontFaceCSS(fonts.faces); + const style = document.createElement("style"); + style.setAttribute("title", "custom-theme-font-faces"); + style.setAttribute("type", "text/css"); + style.appendChild(document.createTextNode(css)); + document.head.appendChild(style); + } if (fonts.general) { style.setProperty("--font-family", fonts.general); } From 3b13a623cd755af6b196558d95515c428b8a1d83 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 23 Jun 2020 17:54:57 +0200 Subject: [PATCH 183/194] cleanup --- src/theme.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/theme.js b/src/theme.js index d560cc96c8..ee9cb9c7db 100644 --- a/src/theme.js +++ b/src/theme.js @@ -179,7 +179,7 @@ export async function setTheme(theme) { if (a == styleElements[stylesheetName]) return; a.disabled = true; }); - const bodyStyles = global.getComputedStyle(document.getElementsByTagName("body")[0]); + const bodyStyles = global.getComputedStyle(document.body); if (bodyStyles.backgroundColor) { document.querySelector('meta[name="theme-color"]').content = bodyStyles.backgroundColor; } From dd9112a01a926b39f363c49c0a384b9f6e450826 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 23 Jun 2020 11:44:40 -0600 Subject: [PATCH 184/194] Decrease margin between new sublists This is an attempt to increase density without adjusting the tiles directly. --- res/css/views/rooms/_RoomSublist2.scss | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index 24151b15b0..c7dae56353 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -22,10 +22,12 @@ limitations under the License. flex-direction: column; margin-left: 8px; - margin-top: 12px; - margin-bottom: 12px; width: 100%; + &:first-child { + margin-top: 12px; // so we're not up against the search/filter + } + .mx_RoomSublist2_headerContainer { // Create a flexbox to make alignment easy display: flex; From f93d67fc65c9914286431b384020176c29b31e1d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 23 Jun 2020 16:49:39 -0600 Subject: [PATCH 185/194] Fix sticky room disappearing/jumping in search results Fixes https://github.com/vector-im/riot-web/issues/14124 Fixes https://github.com/vector-im/riot-web/issues/14154 (which was technically supposed to say that the sticky room when filtering was always last) This is all a bit complicated, but the theory is that we end up with a stable list even through filtering. There's some notes within, though I suspect it'll be difficult to understand :( --- src/stores/room-list/algorithms/Algorithm.ts | 47 +++++++++++++++++--- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index 052c58bb83..9eb0d27748 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -170,12 +170,16 @@ export class Algorithm extends EventEmitter { // When we do have a room though, we expect to be able to find it const tag = this.roomIdsToTags[val.roomId][0]; if (!tag) throw new Error(`${val.roomId} does not belong to a tag and cannot be sticky`); - let position = this.cachedRooms[tag].indexOf(val); + + // We specifically do NOT use the ordered rooms set as it contains the sticky room, which + // means we'll be off by 1 when the user is switching rooms. This leads to visual jumping + // when the user is moving south in the list (not north, because of math). + let position = this.getOrderedRoomsWithoutSticky()[tag].indexOf(val); if (position < 0) throw new Error(`${val.roomId} does not appear to be known and cannot be sticky`); // 🐉 Here be dragons. // Before we can go through with lying to the underlying algorithm about a room - // we need to ensure that when we do we're ready for the innevitable sticky room + // we need to ensure that when we do we're ready for the inevitable sticky room // update we'll receive. To prepare for that, we first remove the sticky room and // recalculate the state ourselves so that when the underlying algorithm calls for // the same thing it no-ops. After we're done calling the algorithm, we'll issue @@ -208,6 +212,12 @@ export class Algorithm extends EventEmitter { position: position, tag: tag, }; + + // We update the filtered rooms just in case, as otherwise users will end up visiting + // a room while filtering and it'll disappear. We don't update the filter earlier in + // this function simply because we don't have to. + this.recalculateFilteredRoomsForTag(tag); + if (lastStickyRoom && lastStickyRoom.tag !== tag) this.recalculateFilteredRoomsForTag(tag); this.recalculateStickyRoom(); // Finally, trigger an update @@ -231,9 +241,7 @@ export class Algorithm extends EventEmitter { // We optimize our lookups by trying to reduce sample size as much as possible // to the rooms we know will be deduped by the Set. const rooms = this.cachedRooms[tagId].map(r => r); // cheap clone - if (this._stickyRoom && this._stickyRoom.tag === tagId && this._stickyRoom.room) { - rooms.push(this._stickyRoom.room); - } + this.tryInsertStickyRoomToFilterSet(rooms, tagId); let remainingRooms = rooms.map(r => r); let allowedRoomsInThisTag = []; let lastFilterPriority = orderedFilters[0].relativePriority; @@ -263,6 +271,7 @@ export class Algorithm extends EventEmitter { this.emit(LIST_UPDATED_EVENT); } + // TODO: Remove or use. protected addPossiblyFilteredRoomsToTag(tagId: TagID, added: Room[]): void { const filters = this.allowedByFilter.keys(); for (const room of added) { @@ -281,7 +290,8 @@ export class Algorithm extends EventEmitter { protected recalculateFilteredRoomsForTag(tagId: TagID): void { console.log(`Recalculating filtered rooms for ${tagId}`); delete this.filteredRooms[tagId]; - const rooms = this.cachedRooms[tagId]; + const rooms = this.cachedRooms[tagId].map(r => r); // cheap clone + this.tryInsertStickyRoomToFilterSet(rooms, tagId); const filteredRooms = rooms.filter(r => this.allowedRoomsByFilters.has(r)); if (filteredRooms.length > 0) { this.filteredRooms[tagId] = filteredRooms; @@ -289,6 +299,17 @@ export class Algorithm extends EventEmitter { console.log(`[DEBUG] ${filteredRooms.length}/${rooms.length} rooms filtered into ${tagId}`); } + protected tryInsertStickyRoomToFilterSet(rooms: Room[], tagId: TagID) { + if (!this._stickyRoom || !this._stickyRoom.room || this._stickyRoom.tag !== tagId) return; + + const position = this._stickyRoom.position; + if (position >= rooms.length) { + rooms.push(this._stickyRoom.room); + } else { + rooms.splice(position, 0, this._stickyRoom.room); + } + } + /** * Recalculate the sticky room position. If this is being called in relation to * a specific tag being updated, it should be given to this function to optimize @@ -377,6 +398,20 @@ export class Algorithm extends EventEmitter { return this.filteredRooms; } + /** + * This returns the same as getOrderedRooms(), but without the sticky room + * map as it causes issues for sticky room handling (see sticky room handling + * for more information). + * @returns {ITagMap} The cached list of rooms, ordered, + * for each tag. May be empty, but never null/undefined. + */ + private getOrderedRoomsWithoutSticky(): ITagMap { + if (!this.hasFilters) { + return this.cachedRooms; + } + return this.filteredRooms; + } + /** * Seeds the Algorithm with a set of rooms. The algorithm will discard all * previously known information and instead use these rooms instead. From 380aed4244a7b83183247ecf3a7f710ef28e9653 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 23 Jun 2020 20:59:26 -0600 Subject: [PATCH 186/194] Update profile information in User Menu Fixes https://github.com/vector-im/riot-web/issues/14158 (we needed an HTTP avatar URL) Fixes https://github.com/vector-im/riot-web/issues/14159 Fixes https://github.com/vector-im/riot-web/issues/14157 Also fixes an issue where it wasn't updating automatically when the user changed their profile info. This is all achieved through a new OwnProfileStore which does the heavy lifting, as we have to keep at least 2 components updated. --- res/css/structures/_LeftPanel2.scss | 9 ++ res/css/structures/_UserMenuButton.scss | 3 - src/components/structures/LeftPanel2.tsx | 41 ++++--- src/components/structures/UserMenuButton.tsx | 24 ++-- src/i18n/strings/en_EN.json | 2 +- src/stores/OwnProfileStore.ts | 122 +++++++++++++++++++ 6 files changed, 170 insertions(+), 31 deletions(-) create mode 100644 src/stores/OwnProfileStore.ts diff --git a/res/css/structures/_LeftPanel2.scss b/res/css/structures/_LeftPanel2.scss index 5cdefa0324..dd28a3107c 100644 --- a/res/css/structures/_LeftPanel2.scss +++ b/res/css/structures/_LeftPanel2.scss @@ -65,6 +65,10 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations .mx_LeftPanel2_userAvatarContainer { position: relative; // to make default avatars work margin-right: 8px; + + .mx_LeftPanel2_userAvatar { + border-radius: 32px; // should match avatar size + } } .mx_LeftPanel2_userName { @@ -72,6 +76,11 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations font-size: $font-15px; line-height: $font-20px; flex: 1; + + // Ellipsize any text overflow + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; } .mx_LeftPanel2_headerButtons { diff --git a/res/css/structures/_UserMenuButton.scss b/res/css/structures/_UserMenuButton.scss index 1fbbbb5fd8..3871fc22ef 100644 --- a/res/css/structures/_UserMenuButton.scss +++ b/res/css/structures/_UserMenuButton.scss @@ -35,9 +35,6 @@ limitations under the License. // Create another flexbox of columns to handle large user IDs display: flex; flex-direction: column; - - // fit the container - flex: 1; width: calc(100% - 40px); // 40px = 32px theme button + 8px margin to theme button * { diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index 378a24a70e..ec846bd177 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -15,6 +15,7 @@ limitations under the License. */ import * as React from "react"; +import { createRef } from "react"; import TagPanel from "./TagPanel"; import classNames from "classnames"; import dis from "../../dispatcher/dispatcher"; @@ -30,7 +31,9 @@ import RoomBreadcrumbs2 from "../views/rooms/RoomBreadcrumbs2"; import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore"; import { UPDATE_EVENT } from "../../stores/AsyncStore"; import ResizeNotifier from "../../utils/ResizeNotifier"; -import { createRef } from "react"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { throttle } from 'lodash'; +import { OwnProfileStore } from "../../stores/OwnProfileStore"; /******************************************************************* * CAUTION * @@ -73,13 +76,32 @@ export default class LeftPanel2 extends React.Component { // We watch the middle panel because we don't actually get resized, the middle panel does. // We listen to the noisy channel to avoid choppy reaction times. this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize); + + OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate); } public componentWillUnmount() { BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize); + OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate); } + // TSLint wants this to be a member, but we don't want that. + // tslint:disable-next-line + private onRoomStateUpdate = throttle((ev: MatrixEvent) => { + const myUserId = MatrixClientPeg.get().getUserId(); + if (ev.getType() === 'm.room.member' && ev.getSender() === myUserId && ev.getStateKey() === myUserId) { + // noinspection JSIgnoredPromiseFromCall + this.onProfileUpdate(); + } + }, 200, {trailing: true, leading: true}); + + private onProfileUpdate = async () => { + // the store triggered an update, so force a layout update. We don't + // have any state to store here for that to magically happen. + this.forceUpdate(); + }; + private onSearch = (term: string): void => { this.setState({searchFilter: term}); }; @@ -149,16 +171,7 @@ export default class LeftPanel2 extends React.Component { // TODO: Presence // TODO: Breadcrumbs toggle // TODO: Menu button - const avatarSize = 32; - // TODO: Don't do this profile lookup in render() - const client = MatrixClientPeg.get(); - let displayName = client.getUserId(); - let avatarUrl: string = null; - const myUser = client.getUser(client.getUserId()); - if (myUser) { - displayName = myUser.rawDisplayName; - avatarUrl = myUser.avatarUrl; - } + const avatarSize = 32; // should match border-radius of the avatar let breadcrumbs; if (this.state.showBreadcrumbs) { @@ -169,7 +182,7 @@ export default class LeftPanel2 extends React.Component { ); } - let name = {displayName}; + let name = {OwnProfileStore.instance.displayName}; let buttons = ( @@ -186,8 +199,8 @@ export default class LeftPanel2 extends React.Component { { this.state = { menuDisplayed: false, - user: MatrixClientPeg.get().getUser(MatrixClientPeg.get().getUserId()), isDarkTheme: this.isUserOnDarkTheme(), }; - } - private get displayName(): string { - if (MatrixClientPeg.get().isGuest()) { - return _t("Guest"); - } else if (this.state.user) { - return this.state.user.displayName; - } else { - return MatrixClientPeg.get().getUserId(); - } + OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate); } private get hasHomePage(): boolean { @@ -81,6 +72,7 @@ export default class UserMenuButton extends React.Component { public componentWillUnmount() { if (this.themeWatcherRef) SettingsStore.unwatchSetting(this.themeWatcherRef); if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef); + OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate); } private isUserOnDarkTheme(): boolean { @@ -91,6 +83,12 @@ export default class UserMenuButton extends React.Component { return theme === "dark"; } + private onProfileUpdate = async () => { + // the store triggered an update, so force a layout update. We don't + // have any state to store here for that to magically happen. + this.forceUpdate(); + }; + private onThemeChanged = () => { this.setState({isDarkTheme: this.isUserOnDarkTheme()}); }; @@ -209,7 +207,7 @@ export default class UserMenuButton extends React.Component {
          - {this.displayName} + {OwnProfileStore.instance.displayName} {MatrixClientPeg.get().getUserId()} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 197fa109e8..74e747726a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -422,6 +422,7 @@ "Upgrade your Riot": "Upgrade your Riot", "A new version of Riot is available!": "A new version of Riot is available!", "You: %(message)s": "You: %(message)s", + "Guest": "Guest", "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.", @@ -2059,7 +2060,6 @@ "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.", "Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.", "Failed to load timeline position": "Failed to load timeline position", - "Guest": "Guest", "Your profile": "Your profile", "Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others", "Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s", diff --git a/src/stores/OwnProfileStore.ts b/src/stores/OwnProfileStore.ts new file mode 100644 index 0000000000..45d8829e30 --- /dev/null +++ b/src/stores/OwnProfileStore.ts @@ -0,0 +1,122 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +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 { ActionPayload } from "../dispatcher/payloads"; +import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; +import defaultDispatcher from "../dispatcher/dispatcher"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { User } from "matrix-js-sdk/src/models/user"; +import { throttle } from "lodash"; +import { MatrixClientPeg } from "../MatrixClientPeg"; +import { _t } from "../languageHandler"; + +interface IState { + displayName?: string; + avatarUrl?: string; +} + +export class OwnProfileStore extends AsyncStoreWithClient { + private static internalInstance = new OwnProfileStore(); + + private monitoredUser: User; + + private constructor() { + super(defaultDispatcher, {}); + } + + public static get instance(): OwnProfileStore { + return OwnProfileStore.internalInstance; + } + + /** + * Gets the display name for the user, or null if not present. + */ + public get displayName(): string { + if (!this.matrixClient) return this.state.displayName || null; + + if (this.matrixClient.isGuest()) { + return _t("Guest"); + } else if (this.state.displayName) { + return this.state.displayName; + } else { + return this.matrixClient.getUserId(); + } + } + + /** + * Gets the MXC URI of the user's avatar, or null if not present. + */ + public get avatarMxc(): string { + return this.state.avatarUrl || null; + } + + /** + * Gets the user's avatar as an HTTP URL of the given size. If the user's + * avatar is not present, this returns null. + * @param size The size of the avatar + * @returns The HTTP URL of the user's avatar + */ + public getHttpAvatarUrl(size: number): string { + if (!this.avatarMxc) return null; + return this.matrixClient.mxcUrlToHttp(this.avatarMxc, size, size); + } + + protected async onNotReady() { + if (this.monitoredUser) { + this.monitoredUser.removeListener("User.displayName", this.onProfileUpdate); + this.monitoredUser.removeListener("User.avatarUrl", this.onProfileUpdate); + } + if (this.matrixClient) { + this.matrixClient.removeListener("RoomState.events", this.onStateEvents); + } + await this.reset({}); + } + + protected async onReady() { + const myUserId = this.matrixClient.getUserId(); + this.monitoredUser = this.matrixClient.getUser(myUserId); + if (this.monitoredUser) { + this.monitoredUser.on("User.displayName", this.onProfileUpdate); + this.monitoredUser.on("User.avatarUrl", this.onProfileUpdate); + } + + // We also have to listen for membership events for ourselves as the above User events + // are fired only with presence, which matrix.org (and many others) has disabled. + this.matrixClient.on("RoomState.events", this.onStateEvents); + + await this.onProfileUpdate(); // trigger an initial update + } + + protected async onAction(payload: ActionPayload) { + // we don't actually do anything here + } + + private onProfileUpdate = async () => { + // We specifically do not use the User object we stored for profile info as it + // could easily be wrong (such as per-room instead of global profile). + const profileInfo = await this.matrixClient.getProfileInfo(this.matrixClient.getUserId()); + await this.updateState({displayName: profileInfo.displayname, avatarUrl: profileInfo.avatar_url}); + }; + + // TSLint wants this to be a member, but we don't want that. + // tslint:disable-next-line + private onStateEvents = throttle(async (ev: MatrixEvent) => { + const myUserId = MatrixClientPeg.get().getUserId(); + if (ev.getType() === 'm.room.member' && ev.getSender() === myUserId && ev.getStateKey() === myUserId) { + await this.onProfileUpdate(); + } + }, 200, {trailing: true, leading: true}); +} From 5c7e59b13200ab9dd652fca2f2e323d2dc982742 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 23 Jun 2020 21:17:39 -0600 Subject: [PATCH 187/194] Use theme-capable icons in the user menu They're still inconsistent weights, but at least they are the right color on non-light-theme clients. --- res/css/structures/_UserMenuButton.scss | 66 +++++++++++++++++++- src/components/structures/UserMenuButton.tsx | 16 ++--- 2 files changed, 73 insertions(+), 9 deletions(-) diff --git a/res/css/structures/_UserMenuButton.scss b/res/css/structures/_UserMenuButton.scss index 1fbbbb5fd8..768d643add 100644 --- a/res/css/structures/_UserMenuButton.scss +++ b/res/css/structures/_UserMenuButton.scss @@ -15,7 +15,26 @@ limitations under the License. */ .mx_UserMenuButton { - // No special styles on the button itself + > span { + width: 16px; + height: 16px; + position: relative; + display: block; + + &::before { + content: ''; + width: 16px; + height: 16px; + position: absolute; + top: 0; + left: 0; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background: $primary-fg-color; + mask-image: url('$(res)/img/feather-customised/more-horizontal.svg'); + } + } } .mx_UserMenuButton_contextMenu { @@ -79,4 +98,49 @@ limitations under the License. justify-content: center; } } + + .mx_IconizedContextMenu_icon { + position: relative; + width: 16px; + height: 16px; + + &::before { + content: ''; + width: 16px; + height: 16px; + position: absolute; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background: $primary-fg-color; + } + } + + .mx_UserMenuButton_iconHome::before { + mask-image: url('$(res)/img/feather-customised/home.svg'); + } + + .mx_UserMenuButton_iconBell::before { + mask-image: url('$(res)/img/feather-customised/notifications.svg'); + } + + .mx_UserMenuButton_iconLock::before { + mask-image: url('$(res)/img/feather-customised/lock.svg'); + } + + .mx_UserMenuButton_iconSettings::before { + mask-image: url('$(res)/img/feather-customised/settings.svg'); + } + + .mx_UserMenuButton_iconArchive::before { + mask-image: url('$(res)/img/feather-customised/archive.svg'); + } + + .mx_UserMenuButton_iconMessage::before { + mask-image: url('$(res)/img/feather-customised/message-circle.svg'); + } + + .mx_UserMenuButton_iconSignOut::before { + mask-image: url('$(res)/img/feather-customised/sign-out.svg'); + } } diff --git a/src/components/structures/UserMenuButton.tsx b/src/components/structures/UserMenuButton.tsx index 04b1b03368..f193a84648 100644 --- a/src/components/structures/UserMenuButton.tsx +++ b/src/components/structures/UserMenuButton.tsx @@ -190,7 +190,7 @@ export default class UserMenuButton extends React.Component { homeButton = (
        • - + {_t("Home")}
        • @@ -233,31 +233,31 @@ export default class UserMenuButton extends React.Component { {homeButton}
        • this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}> - + {_t("Notification settings")}
        • this.onSettingsOpen(e, USER_SECURITY_TAB)}> - + {_t("Security & privacy")}
        • this.onSettingsOpen(e, null)}> - + {_t("All settings")}
        • - + {_t("Archived rooms")}
        • - + {_t("Feedback")}
        • @@ -267,7 +267,7 @@ export default class UserMenuButton extends React.Component {
          • - + {_t("Sign out")}
          • @@ -287,7 +287,7 @@ export default class UserMenuButton extends React.Component { label={_t("Account settings")} isExpanded={this.state.menuDisplayed} > - ... + {/* masked image in CSS */} {contextMenu} From b3fd1eda03ebe6ac252d842b22b8a8db48a1463c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 24 Jun 2020 14:54:14 +0200 Subject: [PATCH 188/194] change the format of font faces to something closer to the css --- src/theme.js | 45 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/src/theme.js b/src/theme.js index ee9cb9c7db..bfee597da4 100644 --- a/src/theme.js +++ b/src/theme.js @@ -49,15 +49,46 @@ function clearCustomTheme() { } } +const allowedFontFaceProps = [ + "font-display", + "font-family", + "font-stretch", + "font-style", + "font-weight", + "font-variant", + "font-feature-settings", + "font-variation-settings", + "src", + "unicode-range" +]; + function generateCustomFontFaceCSS(faces) { - return Object.entries(faces).map(([fontName, face]) => { - const src = Object.entries(face.src).map(([format, url]) => { - return `url('${url}') format('${format}')`; + return faces.map(face => { + const src = face.src && face.src.map(srcElement => { + let format; + if (srcElement.format) { + format = `format("${srcElement.format}")`; + } + if (srcElement.url) { + return `url("${srcElement.url}") ${format}`; + } else if (srcElement.local) { + return `local("${srcElement.local}") ${format}`; + } + return ""; }).join(", "); - return `@font-face {` + - ` font-family: '${fontName}';` + - ` src: ${src};` + - `}`; + const props = Object.keys(face).filter(prop => allowedFontFaceProps.includes(prop)); + const body = props.map(prop => { + let value; + if (prop === "src") { + value = src; + } else if (prop === "font-family") { + value = `"${face[prop]}"`; + } else { + value = face[prop]; + } + return `${prop}: ${value}`; + }).join(";"); + return `@font-face {${body}}`; }).join("\n"); } From 183eb78fa8867858ff5d005be81ff79d9ba127d6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 24 Jun 2020 14:58:41 +0200 Subject: [PATCH 189/194] fix lint --- src/theme.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/theme.js b/src/theme.js index bfee597da4..c79e466933 100644 --- a/src/theme.js +++ b/src/theme.js @@ -59,7 +59,7 @@ const allowedFontFaceProps = [ "font-feature-settings", "font-variation-settings", "src", - "unicode-range" + "unicode-range", ]; function generateCustomFontFaceCSS(faces) { From 256636ccf843ba79d8d000cbc4f72ab57668600d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 24 Jun 2020 07:04:19 -0600 Subject: [PATCH 190/194] Use display:block over absolute positioning --- res/css/structures/_UserMenuButton.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/res/css/structures/_UserMenuButton.scss b/res/css/structures/_UserMenuButton.scss index 768d643add..fe5b6f29a2 100644 --- a/res/css/structures/_UserMenuButton.scss +++ b/res/css/structures/_UserMenuButton.scss @@ -100,15 +100,14 @@ limitations under the License. } .mx_IconizedContextMenu_icon { - position: relative; width: 16px; height: 16px; + display: block; &::before { content: ''; width: 16px; height: 16px; - position: absolute; mask-position: center; mask-size: contain; mask-repeat: no-repeat; From b01015f5c5a45ca0031d3a1f2d26417d55ca4e20 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 24 Jun 2020 14:30:12 +0100 Subject: [PATCH 191/194] Show cross-signing / secret storage reset button in more cases This exposes the cross-signing / secret storage reset button in more cases to hopefully give people a better chance of trying again in case something failed halfway through set up. In particular, any combination of keys existing now reveals the reset button. Fixes https://github.com/vector-im/riot-web/issues/13993 --- .../views/settings/CrossSigningPanel.js | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js index 7eb239cbca..aa512d4365 100644 --- a/src/components/views/settings/CrossSigningPanel.js +++ b/src/components/views/settings/CrossSigningPanel.js @@ -154,13 +154,6 @@ export default class CrossSigningPanel extends React.PureComponent { errorSection =
            {error.toString()}
            ; } - // Whether the various keys exist on your account (but not necessarily - // on this device). - const enabledForAccount = ( - crossSigningPrivateKeysInStorage && - secretStorageKeyInAccount - ); - let summarisedStatus; if (homeserverSupportsCrossSigning === undefined) { const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner'); @@ -184,8 +177,19 @@ export default class CrossSigningPanel extends React.PureComponent { )}

            ; } + const keysExistAnywhere = ( + secretStorageKeyInAccount || + crossSigningPrivateKeysInStorage || + crossSigningPublicKeysOnDevice + ); + const keysExistEverywhere = ( + secretStorageKeyInAccount && + crossSigningPrivateKeysInStorage && + crossSigningPublicKeysOnDevice + ); + let resetButton; - if (enabledForAccount) { + if (keysExistAnywhere) { resetButton = (
            @@ -197,10 +201,7 @@ export default class CrossSigningPanel extends React.PureComponent { // TODO: determine how better to expose this to users in addition to prompts at login/toast let bootstrapButton; - if ( - (!enabledForAccount || !crossSigningPublicKeysOnDevice) && - homeserverSupportsCrossSigning - ) { + if (!keysExistEverywhere && homeserverSupportsCrossSigning) { bootstrapButton = (
            From c047a76f1d60bb038f364278c5ea86335eca20a9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 24 Jun 2020 10:36:43 -0600 Subject: [PATCH 192/194] Update the filtering for the right tag --- src/stores/room-list/algorithms/Algorithm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index 9eb0d27748..5f7a7bd2ef 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -217,7 +217,7 @@ export class Algorithm extends EventEmitter { // a room while filtering and it'll disappear. We don't update the filter earlier in // this function simply because we don't have to. this.recalculateFilteredRoomsForTag(tag); - if (lastStickyRoom && lastStickyRoom.tag !== tag) this.recalculateFilteredRoomsForTag(tag); + if (lastStickyRoom && lastStickyRoom.tag !== tag) this.recalculateFilteredRoomsForTag(lastStickyRoom.tag); this.recalculateStickyRoom(); // Finally, trigger an update From 291914492fefae9162e84b904236baadb3afeb09 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 24 Jun 2020 19:20:43 -0600 Subject: [PATCH 193/194] Fix icons in the new user menu not showing up --- res/css/structures/_UserMenuButton.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/structures/_UserMenuButton.scss b/res/css/structures/_UserMenuButton.scss index 85c3f53aa1..c2bfe5b916 100644 --- a/res/css/structures/_UserMenuButton.scss +++ b/res/css/structures/_UserMenuButton.scss @@ -105,6 +105,7 @@ limitations under the License. content: ''; width: 16px; height: 16px; + display: block; mask-position: center; mask-size: contain; mask-repeat: no-repeat; From 90ff4585d5979f354af2b118f52ca1e5f7400d53 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 24 Jun 2020 20:14:01 -0600 Subject: [PATCH 194/194] Remove extraneous debug from the new left panel --- src/components/structures/LeftPanel2.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index ec846bd177..27583f26ee 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -161,7 +161,6 @@ export default class LeftPanel2 extends React.Component { }; private onResize = () => { - console.log("Resize width"); if (!this.listContainerRef.current) return; // ignore: no headers to sticky this.handleStickyHeaders(this.listContainerRef.current); };