From 118e752a1fd8f3b900709db205976feb84b95304 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 12 May 2019 23:24:12 +0100 Subject: [PATCH 001/334] Add button to clear all notification counts, sometimes stuck in historical Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/settings/Notifications.js | 22 +++++++++++++++++++ src/i18n/strings/en_EN.json | 1 + 2 files changed, 23 insertions(+) diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index 9b5688aa6a..9e01d44fb6 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -29,6 +29,7 @@ import { } from '../../../notifications'; import SdkConfig from "../../../SdkConfig"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; +import AccessibleButton from "../elements/AccessibleButton"; // TODO: this "view" component still has far too much application logic in it, // which should be factored out to other files. @@ -654,6 +655,17 @@ module.exports = React.createClass({ MatrixClientPeg.get().getThreePids().then((r) => this.setState({threepids: r.threepids})); }, + _onClearNotifications: function() { + const cli = MatrixClientPeg.get(); + + cli.getRooms().forEach(r => { + if (r.getUnreadNotificationCount() > 0) { + const events = r.getLiveTimeline().getEvents(); + if (events.length) cli.sendReadReceipt(events.pop()); + } + }); + }, + _updatePushRuleActions: function(rule, actions, enabled) { const cli = MatrixClientPeg.get(); @@ -746,6 +758,13 @@ module.exports = React.createClass({ label={_t('Enable notifications for this account')}/>; } + let clearNotificationsButton; + if (MatrixClientPeg.get().getRooms().some(r => r.getUnreadNotificationCount() > 0)) { + clearNotificationsButton = + {_t("Clear notifications")} + ; + } + // When enabled, the master rule inhibits all existing rules // So do not show all notification settings if (this.state.masterPushRule && this.state.masterPushRule.enabled) { @@ -756,6 +775,8 @@ module.exports = React.createClass({
{ _t('All notifications are currently disabled for all targets.') }
+ + {clearNotificationsButton} ); } @@ -877,6 +898,7 @@ module.exports = React.createClass({ { devicesSection } + { clearNotificationsButton } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8534091176..c6a12d8d56 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -501,6 +501,7 @@ "Notify for all other messages/rooms": "Notify for all other messages/rooms", "Notify me for anything else": "Notify me for anything else", "Enable notifications for this account": "Enable notifications for this account", + "Clear notifications": "Clear notifications", "All notifications are currently disabled for all targets.": "All notifications are currently disabled for all targets.", "Add an email address to configure email notifications": "Add an email address to configure email notifications", "Enable email notifications": "Enable email notifications", From b8a3ee1841f3b06a5c97016d4b75a03ab1456ac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 11 Oct 2019 16:07:59 +0200 Subject: [PATCH 002/334] BasePlatform: Add prototype methods for event indexing. --- src/BasePlatform.js | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/BasePlatform.js b/src/BasePlatform.js index a97c14bf90..7f5df822e4 100644 --- a/src/BasePlatform.js +++ b/src/BasePlatform.js @@ -151,4 +151,44 @@ export default class BasePlatform { async setMinimizeToTrayEnabled(enabled: boolean): void { throw new Error("Unimplemented"); } + + supportsEventIndexing(): boolean { + return false; + } + + async initEventIndex(userId: string): boolean { + throw new Error("Unimplemented"); + } + + async addEventToIndex(ev: {}, profile: {}): void { + throw new Error("Unimplemented"); + } + + indexIsEmpty(): Promise { + throw new Error("Unimplemented"); + } + + async commitLiveEvents(): void { + throw new Error("Unimplemented"); + } + + async searchEventIndex(term: string): Promise<{}> { + throw new Error("Unimplemented"); + } + + async addHistoricEvents(events: [], checkpoint: {} = null, oldCheckpoint: {} = null): Promise { + throw new Error("Unimplemented"); + } + + async addCrawlerCheckpoint(checkpoint: {}): Promise<> { + throw new Error("Unimplemented"); + } + + async removeCrawlerCheckpoint(checkpoint: {}): Promise<> { + throw new Error("Unimplemented"); + } + + async deleteEventIndex(): Promise<> { + throw new Error("Unimplemented"); + } } From 9ce478cb0e29fca8bf0815c7f7a13ef29fe573fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 11 Oct 2019 16:43:53 +0200 Subject: [PATCH 003/334] MatrixChat: Create an event index and start crawling for events. This patch adds support to create an event index if the clients platform supports it and starts an event crawler. The event crawler goes through the room history of encrypted rooms and eventually indexes the whole room history of such rooms. It does this by first creating crawling checkpoints and storing them inside a database. A checkpoint consists of a room_id, direction and token. After the checkpoints are added the client starts a crawler method in the background. The crawler goes through checkpoints in a round-robin way and uses them to fetch historic room messages using the rooms/roomId/messages API endpoint. Every time messages are fetched a new checkpoint is created that will be stored in the database with the fetched events in an atomic way, the old checkpoint is deleted at the same time as well. --- src/MatrixClientPeg.js | 4 + src/components/structures/MatrixChat.js | 231 ++++++++++++++++++++++++ 2 files changed, 235 insertions(+) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index bebb254afc..5c5ee6e4ec 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -30,6 +30,7 @@ import {verificationMethods} from 'matrix-js-sdk/lib/crypto'; import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler"; import * as StorageManager from './utils/StorageManager'; import IdentityAuthClient from './IdentityAuthClient'; +import PlatformPeg from "./PlatformPeg"; interface MatrixClientCreds { homeserverUrl: string, @@ -222,6 +223,9 @@ class MatrixClientPeg { this.matrixClient = createMatrixClient(opts); + const platform = PlatformPeg.get(); + if (platform.supportsEventIndexing()) platform.initEventIndex(creds.userId); + // we're going to add eventlisteners for each matrix event tile, so the // potential number of event listeners is quite high. this.matrixClient.setMaxListeners(500); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index da67416400..218b7e4d4e 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1262,6 +1262,7 @@ export default createReactClass({ // to do the first sync this.firstSyncComplete = false; this.firstSyncPromise = Promise.defer(); + this.crawlerChekpoints = []; const cli = MatrixClientPeg.get(); const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog'); @@ -1287,6 +1288,75 @@ export default createReactClass({ return self._loggedInView.child.canResetTimelineInRoom(roomId); }); + cli.on('sync', async (state, prevState, data) => { + const platform = PlatformPeg.get(); + if (!platform.supportsEventIndexing()) return; + + if (prevState === null && state === "PREPARED") { + /// Load our stored checkpoints, if any. + self.crawlerChekpoints = await platform.loadCheckpoints(); + console.log("Seshat: Loaded checkpoints", + self.crawlerChekpoints); + return; + } + + if (prevState === "PREPARED" && state === "SYNCING") { + const addInitialCheckpoints = async () => { + const client = MatrixClientPeg.get(); + const rooms = client.getRooms(); + + const isRoomEncrypted = (room) => { + return client.isRoomEncrypted(room.roomId); + }; + + // We only care to crawl the encrypted rooms, non-encrytped + // rooms can use the search provided by the Homeserver. + const encryptedRooms = rooms.filter(isRoomEncrypted); + + console.log("Seshat: Adding initial crawler checkpoints"); + + // Gather the prev_batch tokens and create checkpoints for + // our message crawler. + await Promise.all(encryptedRooms.map(async (room) => { + const timeline = room.getLiveTimeline(); + const token = timeline.getPaginationToken("b"); + + console.log("Seshat: Got token for indexer", + room.roomId, token); + + const backCheckpoint = { + roomId: room.roomId, + token: token, + direction: "b", + }; + + const forwardCheckpoint = { + roomId: room.roomId, + token: token, + direction: "f", + }; + + await platform.addCrawlerCheckpoint(backCheckpoint); + await platform.addCrawlerCheckpoint(forwardCheckpoint); + self.crawlerChekpoints.push(backCheckpoint); + self.crawlerChekpoints.push(forwardCheckpoint); + })); + }; + + // If our indexer is empty we're most likely running Riot the + // first time with indexing support or running it with an + // initial sync. Add checkpoints to crawl our encrypted rooms. + const eventIndexWasEmpty = await platform.isEventIndexEmpty(); + if (eventIndexWasEmpty) await addInitialCheckpoints(); + + // Start our crawler. + const crawlerHandle = {}; + self.crawlerFunc(crawlerHandle); + self.crawlerRef = crawlerHandle; + return; + } + }); + cli.on('sync', function(state, prevState, data) { // LifecycleStore and others cannot directly subscribe to matrix client for // events because flux only allows store state changes during flux dispatches. @@ -1930,4 +2000,165 @@ export default createReactClass({ {view} ; }, + + async crawlerFunc(handle) { + // TODO either put this in a better place or find a library provided + // method that does this. + const sleep = async (ms) => { + return new Promise(resolve => setTimeout(resolve, ms)); + }; + + let cancelled = false; + + console.log("Seshat: Started crawler function"); + + const client = MatrixClientPeg.get(); + const platform = PlatformPeg.get(); + + handle.cancel = () => { + cancelled = true; + }; + + while (!cancelled) { + // This is a low priority task and we don't want to spam our + // Homeserver with /messages requests so we set a hefty 3s timeout + // here. + await sleep(3000); + + if (cancelled) { + break; + } + + const checkpoint = this.crawlerChekpoints.shift(); + + /// There is no checkpoint available currently, one may appear if + // a sync with limited room timelines happens, so go back to sleep. + if (checkpoint === undefined) { + continue; + } + + console.log("Seshat: crawling using checkpoint", checkpoint); + + // We have a checkpoint, let us fetch some messages, again, very + // conservatively to not bother our Homeserver too much. + const eventMapper = client.getEventMapper(); + // TODO we need to ensure to use member lazy loading with this + // request so we get the correct profiles. + const res = await client._createMessagesRequest(checkpoint.roomId, + checkpoint.token, 100, checkpoint.direction); + + if (res.chunk.length === 0) { + // We got to the start/end of our timeline, lets just + // delete our checkpoint and go back to sleep. + await platform.removeCrawlerCheckpoint(checkpoint); + continue; + } + + // Convert the plain JSON events into Matrix events so they get + // decrypted if necessary. + const matrixEvents = res.chunk.map(eventMapper); + const stateEvents = res.state.map(eventMapper); + + const profiles = {}; + + stateEvents.forEach(ev => { + if (ev.event.content && + ev.event.content.membership === "join") { + profiles[ev.event.sender] = { + displayname: ev.event.content.displayname, + avatar_url: ev.event.content.avatar_url, + }; + } + }); + + const decryptionPromises = []; + + matrixEvents.forEach(ev => { + if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) { + // TODO the decryption promise is a private property, this + // should either be made public or we should convert the + // event that gets fired when decryption is done into a + // promise using the once event emitter method: + // https://nodejs.org/api/events.html#events_events_once_emitter_name + decryptionPromises.push(ev._decryptionPromise); + } + }); + + // Let us wait for all the events to get decrypted. + await Promise.all(decryptionPromises); + + // We filter out events for which decryption failed, are redacted + // or aren't of a type that we know how to index. + const isValidEvent = (value) => { + return ([ + "m.room.message", + "m.room.name", + "m.room.topic", + ].indexOf(value.getType()) >= 0 + && !value.isRedacted() && !value.isDecryptionFailure() + ); + // TODO do we need to check if the event has all the valid + // attributes? + }; + + // TODO if there ar no events at this point we're missing a lot + // decryption keys, do we wan't to retry this checkpoint at a later + // stage? + const filteredEvents = matrixEvents.filter(isValidEvent); + + // Let us convert the events back into a format that Seshat can + // consume. + const events = filteredEvents.map((ev) => { + const jsonEvent = ev.toJSON(); + + let e; + if (ev.isEncrypted()) e = jsonEvent.decrypted; + else e = jsonEvent; + + let profile = {}; + if (e.sender in profiles) profile = profiles[e.sender]; + const object = { + event: e, + profile: profile, + }; + return object; + }); + + // Create a new checkpoint so we can continue crawling the room for + // messages. + const newCheckpoint = { + roomId: checkpoint.roomId, + token: res.end, + fullCrawl: checkpoint.fullCrawl, + direction: checkpoint.direction, + }; + + console.log( + "Seshat: Crawled room", + client.getRoom(checkpoint.roomId).name, + "and fetched", events.length, "events.", + ); + + try { + const eventsAlreadyAdded = await platform.addHistoricEvents( + events, newCheckpoint, checkpoint); + // If all events were already indexed we assume that we catched + // up with our index and don't need to crawl the room further. + // Let us delete the checkpoint in that case, otherwise push + // the new checkpoint to be used by the crawler. + if (eventsAlreadyAdded === true && newCheckpoint.fullCrawl !== true) { + await platform.removeCrawlerCheckpoint(newCheckpoint); + } else { + this.crawlerChekpoints.push(newCheckpoint); + } + } catch (e) { + console.log("Seshat: Error durring a crawl", e); + // An error occured, put the checkpoint back so we + // can retry. + this.crawlerChekpoints.push(checkpoint); + } + } + + console.log("Seshat: Stopping crawler function"); + }, }); From b23ba5f8811488c16412b6ebe2d141f1b9e18f28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 11 Oct 2019 16:27:01 +0200 Subject: [PATCH 004/334] MatrixChat: Stop the crawler function and delete the index when logging out. --- src/components/structures/MatrixChat.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 218b7e4d4e..7eda69ad9b 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1221,7 +1221,15 @@ export default createReactClass({ /** * Called when the session is logged out */ - _onLoggedOut: function() { + _onLoggedOut: async function() { + const platform = PlatformPeg.get(); + + if (platform.supportsEventIndexing()) { + console.log("Seshat: Deleting event index."); + this.crawlerRef.cancel(); + await platform.deleteEventIndex(); + } + this.notifyNewScreen('login'); this.setStateForNewView({ view: VIEWS.LOGIN, From 5e7076e985fd95a7978099322457823f96daff8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 11 Oct 2019 16:28:36 +0200 Subject: [PATCH 005/334] MatrixChat: Add live events to the event index as well. --- src/components/structures/MatrixChat.js | 65 +++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 7eda69ad9b..5c4db4a562 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1271,6 +1271,7 @@ export default createReactClass({ this.firstSyncComplete = false; this.firstSyncPromise = Promise.defer(); this.crawlerChekpoints = []; + this.liveEventsForIndex = new Set(); const cli = MatrixClientPeg.get(); const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog'); @@ -1363,6 +1364,14 @@ export default createReactClass({ self.crawlerRef = crawlerHandle; return; } + + if (prevState === "SYNCING" && state === "SYNCING") { + // A sync was done, presumably we queued up some live events, + // commit them now. + console.log("Seshat: Committing events"); + await platform.commitLiveEvents(); + return; + } }); cli.on('sync', function(state, prevState, data) { @@ -1447,6 +1456,44 @@ export default createReactClass({ }, null, true); }); + cli.on("Room.timeline", async (ev, room, toStartOfTimeline, removed, data) => { + const platform = PlatformPeg.get(); + if (!platform.supportsEventIndexing()) return; + + // We only index encrypted rooms locally. + if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; + + // If it isn't a live event or if it's redacted there's nothing to + // do. + if (toStartOfTimeline || !data || !data.liveEvent + || ev.isRedacted()) { + return; + } + + // If the event is not yet decrypted mark it for the + // Event.decrypted callback. + if (ev.isBeingDecrypted()) { + const eventId = ev.getId(); + self.liveEventsForIndex.add(eventId); + } else { + // If the event is decrypted or is unencrypted add it to the + // index now. + await self.addLiveEventToIndex(ev); + } + }); + + cli.on("Event.decrypted", async (ev, err) => { + const platform = PlatformPeg.get(); + if (!platform.supportsEventIndexing()) return; + + const eventId = ev.getId(); + + // If the event isn't in our live event set, ignore it. + if (!self.liveEventsForIndex.delete(eventId)) return; + if (err) return; + await self.addLiveEventToIndex(ev); + }); + cli.on("accountData", function(ev) { if (ev.getType() === 'im.vector.web.settings') { if (ev.getContent() && ev.getContent().theme) { @@ -2009,6 +2056,24 @@ export default createReactClass({ ; }, + async addLiveEventToIndex(ev) { + const platform = PlatformPeg.get(); + if (!platform.supportsEventIndexing()) return; + + if (["m.room.message", "m.room.name", "m.room.topic"] + .indexOf(ev.getType()) == -1) { + return; + } + + const e = ev.toJSON().decrypted; + const profile = { + displayname: ev.sender.rawDisplayName, + avatar_url: ev.sender.getMxcAvatarUrl(), + }; + + platform.addEventToIndex(e, profile); + }, + async crawlerFunc(handle) { // TODO either put this in a better place or find a library provided // method that does this. From 4acec19d40ba57f789f4c1293ebeb6774babc6ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 11 Oct 2019 16:32:55 +0200 Subject: [PATCH 006/334] MatrixChat: Add new crawler checkpoints if there was a limited timeline. A sync call may not have all events that happened since the last time the client synced. In such a case the room is marked as limited and events need to be fetched separately. When such a sync call happens our event index will have a gap. To close the gap checkpoints are added to start crawling our room again. Unnecessary full re-crawls are prevented by checking if our current /room/roomId/messages request contains only events that were already present in our event index. --- src/components/structures/MatrixChat.js | 42 ++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 5c4db4a562..d423bbd592 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1281,8 +1281,11 @@ export default createReactClass({ // particularly noticeable when there are lots of 'limited' /sync responses // such as when laptops unsleep. // https://github.com/vector-im/riot-web/issues/3307#issuecomment-282895568 - cli.setCanResetTimelineCallback(function(roomId) { + cli.setCanResetTimelineCallback(async function(roomId) { console.log("Request to reset timeline in room ", roomId, " viewing:", self.state.currentRoomId); + // TODO is there a better place to plug this in + await self.addCheckpointForLimitedRoom(roomId); + if (roomId !== self.state.currentRoomId) { // It is safe to remove events from rooms we are not viewing. return true; @@ -2234,4 +2237,41 @@ export default createReactClass({ console.log("Seshat: Stopping crawler function"); }, + + async addCheckpointForLimitedRoom(roomId) { + const platform = PlatformPeg.get(); + if (!platform.supportsEventIndexing()) return; + if (!MatrixClientPeg.get().isRoomEncrypted(roomId)) return; + + const client = MatrixClientPeg.get(); + const room = client.getRoom(roomId); + + if (room === null) return; + + const timeline = room.getLiveTimeline(); + const token = timeline.getPaginationToken("b"); + + const backwardsCheckpoint = { + roomId: room.roomId, + token: token, + fullCrawl: false, + direction: "b", + }; + + const forwardsCheckpoint = { + roomId: room.roomId, + token: token, + fullCrawl: false, + direction: "f", + }; + + console.log("Seshat: Added checkpoint because of a limited timeline", + backwardsCheckpoint, forwardsCheckpoint); + + await platform.addCrawlerCheckpoint(backwardsCheckpoint); + await platform.addCrawlerCheckpoint(forwardsCheckpoint); + + this.crawlerChekpoints.push(backwardsCheckpoint); + this.crawlerChekpoints.push(forwardsCheckpoint); + }, }); From 3f5369183404be057af45fa248556572804727b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 11 Oct 2019 16:40:10 +0200 Subject: [PATCH 007/334] RoomView: Use platform specific search if our platform supports it. This patch extends our search to include our platform specific event index. There are 3 search scenarios and are handled differently when platform support for indexing is present: - Search a single non-encrypted room: Use the server-side search like before. - Search a single encrypted room: Search using our platform specific event index. - Search across all rooms: Search encrypted rooms using our local event index. Search non-encrypted rooms using the classic server-side search. Combine the results. The combined search will result in having twice the amount of search results since comparing the scores fairly wasn't deemed sensible. --- src/components/structures/RoomView.js | 115 ++++++++++++++++++++++++-- 1 file changed, 110 insertions(+), 5 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 4de573479d..1b44335f51 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -34,6 +34,7 @@ import { _t } from '../../languageHandler'; import {RoomPermalinkCreator} from '../../utils/permalinks/Permalinks'; import MatrixClientPeg from '../../MatrixClientPeg'; +import PlatformPeg from "../../PlatformPeg"; import ContentMessages from '../../ContentMessages'; import Modal from '../../Modal'; import sdk from '../../index'; @@ -1140,12 +1141,116 @@ module.exports = createReactClass({ } debuglog("sending search request"); + const platform = PlatformPeg.get(); - const searchPromise = MatrixClientPeg.get().searchRoomEvents({ - filter: filter, - term: term, - }); - this._handleSearchResult(searchPromise).done(); + if (platform.supportsEventIndexing()) { + const combinedSearchFunc = async (searchTerm) => { + // Create two promises, one for the local search, one for the + // server-side search. + const client = MatrixClientPeg.get(); + const serverSidePromise = client.searchRoomEvents({ + term: searchTerm, + }); + const localPromise = localSearchFunc(searchTerm); + + // Wait for both promises to resolve. + await Promise.all([serverSidePromise, localPromise]); + + // Get both search results. + const localResult = await localPromise; + const serverSideResult = await serverSidePromise; + + // Combine the search results into one result. + const result = {}; + + // Our localResult and serverSideResult are both ordered by + // recency separetly, 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; + + if (aEvent.origin_server_ts > + bEvent.origin_server_ts) return -1; + if (aEvent.origin_server_ts < + bEvent.origin_server_ts) return 1; + return 0; + }; + + result.count = localResult.count + serverSideResult.count; + result.results = localResult.results.concat( + serverSideResult.results).sort(compare); + result.highlights = localResult.highlights.concat( + serverSideResult.highlights); + + return result; + }; + + const localSearchFunc = async (searchTerm, roomId = undefined) => { + const searchArgs = { + search_term: searchTerm, + before_limit: 1, + after_limit: 1, + order_by_recency: true, + }; + + if (roomId !== undefined) { + searchArgs.room_id = roomId; + } + + const localResult = await platform.searchEventIndex( + searchArgs); + + const response = { + search_categories: { + room_events: localResult, + }, + }; + + const emptyResult = { + results: [], + highlights: [], + }; + + // TODO is there a better way to convert our result into what + // is expected by the handler method. + const result = MatrixClientPeg.get()._processRoomEventsSearch( + emptyResult, response); + + return result; + }; + + let searchPromise; + + if (scope === "Room") { + const roomId = this.state.room.roomId; + + if (MatrixClientPeg.get().isRoomEncrypted(roomId)) { + // The search is for a single encrypted room, use our local + // search method. + searchPromise = localSearchFunc(term, roomId); + } else { + // The search is for a single non-encrypted room, use the + // server-side search. + searchPromise = MatrixClientPeg.get().searchRoomEvents({ + filter: filter, + term: term, + }); + } + } else { + // Search across all rooms, combine a server side search and a + // local search. + searchPromise = combinedSearchFunc(term); + } + + this._handleSearchResult(searchPromise).done(); + } else { + const searchPromise = MatrixClientPeg.get().searchRoomEvents({ + filter: filter, + term: term, + }); + this._handleSearchResult(searchPromise).done(); + } }, _handleSearchResult: function(searchPromise) { From 1b63886a6baca1a4191f83992609e58e5e6dc43c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 18 Oct 2019 16:31:39 +0200 Subject: [PATCH 008/334] MatrixChat: Add more detailed logging to the event crawler. --- src/components/structures/MatrixChat.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index d423bbd592..3558cda586 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -2101,7 +2101,10 @@ export default createReactClass({ // here. await sleep(3000); + console.log("Seshat: Running the crawler loop."); + if (cancelled) { + console.log("Seshat: Cancelling the crawler."); break; } @@ -2124,6 +2127,7 @@ export default createReactClass({ checkpoint.token, 100, checkpoint.direction); if (res.chunk.length === 0) { + console.log("Seshat: Done with the checkpoint", checkpoint) // We got to the start/end of our timeline, lets just // delete our checkpoint and go back to sleep. await platform.removeCrawlerCheckpoint(checkpoint); @@ -2223,6 +2227,7 @@ export default createReactClass({ // Let us delete the checkpoint in that case, otherwise push // the new checkpoint to be used by the crawler. if (eventsAlreadyAdded === true && newCheckpoint.fullCrawl !== true) { + console.log("Seshat: Checkpoint had already all events added, stopping the crawl", checkpoint); await platform.removeCrawlerCheckpoint(newCheckpoint); } else { this.crawlerChekpoints.push(newCheckpoint); From 89f14e55a2bb31959893f138813957acd957e032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 18 Oct 2019 16:32:43 +0200 Subject: [PATCH 009/334] MatrixChat: Catch errors when fetching room messages in the crawler. --- src/components/structures/MatrixChat.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 3558cda586..2f9e64efa9 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -2123,8 +2123,17 @@ export default createReactClass({ const eventMapper = client.getEventMapper(); // TODO we need to ensure to use member lazy loading with this // request so we get the correct profiles. - const res = await client._createMessagesRequest(checkpoint.roomId, - checkpoint.token, 100, checkpoint.direction); + let res; + + try { + res = await client._createMessagesRequest( + checkpoint.roomId, checkpoint.token, 100, + checkpoint.direction); + } catch (e) { + console.log("Seshat: Error crawling events:", e) + this.crawlerChekpoints.push(checkpoint); + continue + } if (res.chunk.length === 0) { console.log("Seshat: Done with the checkpoint", checkpoint) From 64061173e19507ce40241989a1fb55ac705cd648 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 18 Oct 2019 16:33:07 +0200 Subject: [PATCH 010/334] MatrixChat: Check if our state array is empty in the crawled messages response. --- src/components/structures/MatrixChat.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 2f9e64efa9..51cf92da5f 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -2146,7 +2146,10 @@ export default createReactClass({ // Convert the plain JSON events into Matrix events so they get // decrypted if necessary. const matrixEvents = res.chunk.map(eventMapper); - const stateEvents = res.state.map(eventMapper); + let stateEvents = []; + if (res.state !== undefined) { + stateEvents = res.state.map(eventMapper); + } const profiles = {}; From 23383419e803f6916c6636de10865b386a240f73 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 13:19:54 -0600 Subject: [PATCH 011/334] Add settings base for Mjolnir rules --- .../views/dialogs/_UserSettingsDialog.scss | 4 + .../views/dialogs/UserSettingsDialog.js | 30 ++++++++ .../tabs/user/MjolnirUserSettingsTab.js | 74 +++++++++++++++++++ src/i18n/strings/en_EN.json | 11 ++- src/settings/Settings.js | 14 ++++ 5 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js diff --git a/res/css/views/dialogs/_UserSettingsDialog.scss b/res/css/views/dialogs/_UserSettingsDialog.scss index 2a046ff501..4d831d7858 100644 --- a/res/css/views/dialogs/_UserSettingsDialog.scss +++ b/res/css/views/dialogs/_UserSettingsDialog.scss @@ -45,6 +45,10 @@ limitations under the License. mask-image: url('$(res)/img/feather-customised/flag.svg'); } +.mx_UserSettingsDialog_mjolnirIcon::before { + mask-image: url('$(res)/img/feather-customised/face.svg'); +} + .mx_UserSettingsDialog_flairIcon::before { mask-image: url('$(res)/img/feather-customised/flair.svg'); } diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js index fb9045f05a..6e324ad3fb 100644 --- a/src/components/views/dialogs/UserSettingsDialog.js +++ b/src/components/views/dialogs/UserSettingsDialog.js @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +Copyright 2019 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. @@ -29,12 +30,34 @@ import HelpUserSettingsTab from "../settings/tabs/user/HelpUserSettingsTab"; import FlairUserSettingsTab from "../settings/tabs/user/FlairUserSettingsTab"; import sdk from "../../../index"; import SdkConfig from "../../../SdkConfig"; +import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab"; export default class UserSettingsDialog extends React.Component { static propTypes = { onFinished: PropTypes.func.isRequired, }; + constructor() { + super(); + + this.state = { + mjolnirEnabled: SettingsStore.isFeatureEnabled("feature_mjolnir"), + } + } + + componentDidMount(): void { + this._mjolnirWatcher = SettingsStore.watchSetting("feature_mjolnir", null, this._mjolnirChanged.bind(this)); + } + + componentWillUnmount(): void { + SettingsStore.unwatchSetting(this._mjolnirWatcher); + } + + _mjolnirChanged(settingName, roomId, atLevel, newValue) { + // We can cheat because we know what levels a feature is tracked at, and how it is tracked + this.setState({mjolnirEnabled: newValue}); + } + _getTabs() { const tabs = []; @@ -75,6 +98,13 @@ export default class UserSettingsDialog extends React.Component { , )); } + if (this.state.mjolnirEnabled) { + tabs.push(new Tab( + _td("Ignored users"), + "mx_UserSettingsDialog_mjolnirIcon", + , + )); + } tabs.push(new Tab( _td("Help & About"), "mx_UserSettingsDialog_helpIcon", diff --git a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js new file mode 100644 index 0000000000..02e64c0bc1 --- /dev/null +++ b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js @@ -0,0 +1,74 @@ +/* +Copyright 2019 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 {_t} from "../../../../../languageHandler"; +const sdk = require("../../../../.."); + +export default class MjolnirUserSettingsTab extends React.Component { + constructor() { + super(); + } + + render() { + return ( +
+
{_t("Ignored users")}
+
+
+ {_t("⚠ These settings are meant for advanced users.")}
+
+ {_t( + "Add users and servers you want to ignore here. Use asterisks " + + "to have Riot match any characters. For example, @bot:* " + + "would ignore all users that have the name 'bot' on any server.", + {}, {code: (s) => {s}}, + )}
+
+ {_t( + "Ignoring people is done through ban lists which contain rules for " + + "who to ban. Subscribing to a ban list means the users/servers blocked by " + + "that list will be hidden from you." + )} +
+
+
+ {_t("Personal ban list")} +
+ {_t( + "Your personal ban list holds all the users/servers you personally don't " + + "want to see messages from. After ignoring your first user/server, a new room " + + "will show up in your room list named 'My Ban List' - stay in this room to keep " + + "the ban list in effect.", + )} +
+

TODO

+
+
+ {_t("Subscribed lists")} +
+ {_t("Subscribing to a ban list will cause you to join it!")} +   + {_t( + "If this isn't what you want, please use a different tool to ignore users.", + )} +
+

TODO

+
+
+ ); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 524a8a1abf..e909f49159 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -335,6 +335,7 @@ "Render simple counters in room header": "Render simple counters in room header", "Multiple integration managers": "Multiple integration managers", "Use the new, consistent UserInfo panel for Room Members and Group Members": "Use the new, consistent UserInfo panel for Room Members and Group Members", + "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", "Use the new, faster, composer for writing messages": "Use the new, faster, composer for writing messages", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", "Use compact timeline layout": "Use compact timeline layout", @@ -637,6 +638,15 @@ "Access Token:": "Access Token:", "click to reveal": "click to reveal", "Labs": "Labs", + "Ignored users": "Ignored users", + "⚠ These settings are meant for advanced users.": "⚠ These settings are meant for advanced users.", + "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.", + "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.", + "Personal ban list": "Personal ban list", + "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.", + "Subscribed lists": "Subscribed lists", + "Subscribing to a ban list will cause you to join it!": "Subscribing to a ban list will cause you to join it!", + "If this isn't what you want, please use a different tool to ignore users.": "If this isn't what you want, please use a different tool to ignore users.", "Notifications": "Notifications", "Start automatically after system login": "Start automatically after system login", "Always show the window menu bar": "Always show the window menu bar", @@ -654,7 +664,6 @@ "Cryptography": "Cryptography", "Device ID:": "Device ID:", "Device key:": "Device key:", - "Ignored users": "Ignored users", "Bulk options": "Bulk options", "Accept all %(invitedRooms)s invites": "Accept all %(invitedRooms)s invites", "Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 7470641359..1cfff0182e 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -126,6 +126,20 @@ export const SETTINGS = { supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_mjolnir": { + isFeature: true, + displayName: _td("Try out new ways to ignore people (experimental)"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, + "mjolnirRooms": { + supportedLevels: ['account'], + default: [], + }, + "mjolnirPersonalRoom": { + supportedLevels: ['account'], + default: null, + }, "useCiderComposer": { displayName: _td("Use the new, faster, composer for writing messages"), supportedLevels: LEVELS_ACCOUNT_SETTINGS, From e6e12df82d1e801019f3ea993b35ae0b2b61f04c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 13:20:08 -0600 Subject: [PATCH 012/334] Add structural base for handling Mjolnir lists --- package.json | 1 + src/i18n/strings/en_EN.json | 2 + src/mjolnir/BanList.js | 98 +++++++++++++++++++++++++++++ src/mjolnir/ListRule.js | 63 +++++++++++++++++++ src/mjolnir/Mjolnir.js | 122 ++++++++++++++++++++++++++++++++++++ src/utils/MatrixGlob.js | 54 ++++++++++++++++ yarn.lock | 5 ++ 7 files changed, 345 insertions(+) create mode 100644 src/mjolnir/BanList.js create mode 100644 src/mjolnir/ListRule.js create mode 100644 src/mjolnir/Mjolnir.js create mode 100644 src/utils/MatrixGlob.js diff --git a/package.json b/package.json index e709662020..745f82d7bc 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "gemini-scrollbar": "github:matrix-org/gemini-scrollbar#91e1e566", "gfm.css": "^1.1.1", "glob": "^5.0.14", + "glob-to-regexp": "^0.4.1", "highlight.js": "^9.15.8", "is-ip": "^2.0.0", "isomorphic-fetch": "^2.2.1", diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e909f49159..770f4723ef 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -389,6 +389,8 @@ "Call invitation": "Call invitation", "Messages sent by bot": "Messages sent by bot", "When rooms are upgraded": "When rooms are upgraded", + "My Ban List": "My Ban List", + "This is your list of users/servers you have blocked - don't leave the room!": "This is your list of users/servers you have blocked - don't leave the room!", "Active call (%(roomName)s)": "Active call (%(roomName)s)", "unknown caller": "unknown caller", "Incoming voice call from %(name)s": "Incoming voice call from %(name)s", diff --git a/src/mjolnir/BanList.js b/src/mjolnir/BanList.js new file mode 100644 index 0000000000..6ebc0a7e36 --- /dev/null +++ b/src/mjolnir/BanList.js @@ -0,0 +1,98 @@ +/* +Copyright 2019 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. +*/ + +// Inspiration largely taken from Mjolnir itself + +import {ListRule, RECOMMENDATION_BAN, recommendationToStable} from "./ListRule"; +import MatrixClientPeg from "../MatrixClientPeg"; + +export const RULE_USER = "m.room.rule.user"; +export const RULE_ROOM = "m.room.rule.room"; +export const RULE_SERVER = "m.room.rule.server"; + +export const USER_RULE_TYPES = [RULE_USER, "org.matrix.mjolnir.rule.user"]; +export const ROOM_RULE_TYPES = [RULE_ROOM, "org.matrix.mjolnir.rule.room"]; +export const SERVER_RULE_TYPES = [RULE_SERVER, "org.matrix.mjolnir.rule.server"]; +export const ALL_RULE_TYPES = [...USER_RULE_TYPES, ...ROOM_RULE_TYPES, ...SERVER_RULE_TYPES]; + +export function ruleTypeToStable(rule: string, unstable = true): string { + if (USER_RULE_TYPES.includes(rule)) return unstable ? USER_RULE_TYPES[USER_RULE_TYPES.length - 1] : RULE_USER; + if (ROOM_RULE_TYPES.includes(rule)) return unstable ? ROOM_RULE_TYPES[ROOM_RULE_TYPES.length - 1] : RULE_ROOM; + if (SERVER_RULE_TYPES.includes(rule)) return unstable ? SERVER_RULE_TYPES[SERVER_RULE_TYPES.length - 1] : RULE_SERVER; + return null; +} + +export class BanList { + _rules: ListRule[] = []; + _roomId: string; + + constructor(roomId: string) { + this._roomId = roomId; + this.updateList(); + } + + get roomId(): string { + return this._roomId; + } + + get serverRules(): ListRule[] { + return this._rules.filter(r => r.kind === RULE_SERVER); + } + + get userRules(): ListRule[] { + return this._rules.filter(r => r.kind === RULE_USER); + } + + get roomRules(): ListRule[] { + return this._rules.filter(r => r.kind === RULE_ROOM); + } + + banEntity(kind: string, entity: string, reason: string): Promise { + return MatrixClientPeg.get().sendStateEvent(this._roomId, ruleTypeToStable(kind, true), { + entity: entity, + reason: reason, + recommendation: recommendationToStable(RECOMMENDATION_BAN, true), + }, "rule:" + entity); + } + + unbanEntity(kind: string, entity: string): Promise { + // Empty state event is effectively deleting it. + return MatrixClientPeg.get().sendStateEvent(this._roomId, ruleTypeToStable(kind, true), {}, "rule:" + entity); + } + + updateList() { + this._rules = []; + + const room = MatrixClientPeg.get().getRoom(this._roomId); + if (!room) return; + + for (const eventType of ALL_RULE_TYPES) { + const events = room.currentState.getStateEvents(eventType, undefined); + for (const ev of events) { + if (!ev['state_key']) continue; + + const kind = ruleTypeToStable(eventType, false); + + const entity = ev.getContent()['entity']; + const recommendation = ev.getContent()['recommendation']; + const reason = ev.getContent()['reason']; + if (!entity || !recommendation || !reason) continue; + + this._rules.push(new ListRule(entity, recommendation, reason, kind)); + } + } + } +} diff --git a/src/mjolnir/ListRule.js b/src/mjolnir/ListRule.js new file mode 100644 index 0000000000..d33248d24c --- /dev/null +++ b/src/mjolnir/ListRule.js @@ -0,0 +1,63 @@ +/* +Copyright 2019 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 {MatrixGlob} from "../utils/MatrixGlob"; + +// Inspiration largely taken from Mjolnir itself + +export const RECOMMENDATION_BAN = "m.ban"; +export const RECOMMENDATION_BAN_TYPES = [RECOMMENDATION_BAN, "org.matrix.mjolnir.ban"]; + +export function recommendationToStable(recommendation: string, unstable = true): string { + if (RECOMMENDATION_BAN_TYPES.includes(recommendation)) return unstable ? RECOMMENDATION_BAN_TYPES[RECOMMENDATION_BAN_TYPES.length - 1] : RECOMMENDATION_BAN; + return null; +} + +export class ListRule { + _glob: MatrixGlob; + _entity: string; + _action: string; + _reason: string; + _kind: string; + + constructor(entity: string, action: string, reason: string, kind: string) { + this._glob = new MatrixGlob(entity); + this._entity = entity; + this._action = recommendationToStable(action, false); + this._reason = reason; + this._kind = kind; + } + + get entity(): string { + return this._entity; + } + + get reason(): string { + return this._reason; + } + + get kind(): string { + return this._kind; + } + + get recommendation(): string { + return this._action; + } + + isMatch(entity: string): boolean { + return this._glob.test(entity); + } +} diff --git a/src/mjolnir/Mjolnir.js b/src/mjolnir/Mjolnir.js new file mode 100644 index 0000000000..a12534592d --- /dev/null +++ b/src/mjolnir/Mjolnir.js @@ -0,0 +1,122 @@ +/* +Copyright 2019 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 {ALL_RULE_TYPES, BanList} from "./BanList"; +import SettingsStore, {SettingLevel} from "../settings/SettingsStore"; +import {_t} from "../languageHandler"; + +// TODO: Move this and related files to the js-sdk or something once finalized. + +export class Mjolnir { + static _instance: Mjolnir = null; + + _lists: BanList[] = []; + _roomIds: string[] = []; + _mjolnirWatchRef = null; + + constructor() { + } + + start() { + this._updateLists(SettingsStore.getValue("mjolnirRooms")); + this._mjolnirWatchRef = SettingsStore.watchSetting("mjolnirRooms", null, this._onListsChanged.bind(this)); + + MatrixClientPeg.get().on("RoomState.events", this._onEvent.bind(this)); + } + + stop() { + SettingsStore.unwatchSetting(this._mjolnirWatchRef); + MatrixClientPeg.get().removeListener("RoomState.events", this._onEvent.bind(this)); + } + + async getOrCreatePersonalList(): Promise { + let personalRoomId = SettingsStore.getValue("mjolnirPersonalRoom"); + if (!personalRoomId) { + const resp = await MatrixClientPeg.get().createRoom({ + name: _t("My Ban List"), + topic: _t("This is your list of users/servers you have blocked - don't leave the room!"), + preset: "private_chat" + }); + personalRoomId = resp['room_id']; + SettingsStore.setValue("mjolnirPersonalRoom", null, SettingLevel.ACCOUNT, personalRoomId); + SettingsStore.setValue("mjolnirRooms", null, SettingLevel.ACCOUNT, [personalRoomId, ...this._roomIds]); + } + if (!personalRoomId) { + throw new Error("Error finding a room ID to use"); + } + + let list = this._lists.find(b => b.roomId === personalRoomId); + if (!list) list = new BanList(personalRoomId); + // we don't append the list to the tracked rooms because it should already be there. + // we're just trying to get the caller some utility access to the list + + return list; + } + + _onEvent(event) { + if (!this._roomIds.includes(event.getRoomId())) return; + if (!ALL_RULE_TYPES.includes(event.getType())) return; + + this._updateLists(this._roomIds); + } + + _onListsChanged(settingName, roomId, atLevel, newValue) { + // We know that ban lists are only recorded at one level so we don't need to re-eval them + this._updateLists(newValue); + } + + _updateLists(listRoomIds: string[]) { + this._lists = []; + this._roomIds = listRoomIds || []; + if (!listRoomIds) return; + + for (const roomId of listRoomIds) { + // Creating the list updates it + this._lists.push(new BanList(roomId)); + } + } + + isServerBanned(serverName: string): boolean { + for (const list of this._lists) { + for (const rule of list.serverRules) { + if (rule.isMatch(serverName)) { + return true; + } + } + } + return false; + } + + isUserBanned(userId: string): boolean { + for (const list of this._lists) { + for (const rule of list.userRules) { + if (rule.isMatch(userId)) { + return true; + } + } + } + return false; + } + + static sharedInstance(): Mjolnir { + if (!Mjolnir._instance) { + Mjolnir._instance = new Mjolnir(); + } + return Mjolnir._instance; + } +} + diff --git a/src/utils/MatrixGlob.js b/src/utils/MatrixGlob.js new file mode 100644 index 0000000000..cf55040625 --- /dev/null +++ b/src/utils/MatrixGlob.js @@ -0,0 +1,54 @@ +/* +Copyright 2019 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 * as globToRegexp from "glob-to-regexp"; + +// Taken with permission from matrix-bot-sdk: +// https://github.com/turt2live/matrix-js-bot-sdk/blob/eb148c2ecec7bf3ade801d73deb43df042d55aef/src/MatrixGlob.ts + +/** + * Represents a common Matrix glob. This is commonly used + * for server ACLs and similar functions. + */ +export class MatrixGlob { + _regex: RegExp; + + /** + * Creates a new Matrix Glob + * @param {string} glob The glob to convert. Eg: "*.example.org" + */ + constructor(glob: string) { + const globRegex = globToRegexp(glob, { + extended: false, + globstar: false, + }); + + // We need to convert `?` manually because globToRegexp's extended mode + // does more than we want it to. + const replaced = globRegex.toString().replace(/\\\?/g, "."); + this._regex = new RegExp(replaced.substring(1, replaced.length - 1)); + } + + /** + * Tests the glob against a value, returning true if it matches. + * @param {string} val The value to test. + * @returns {boolean} True if the value matches the glob, false otherwise. + */ + test(val: string): boolean { + return this._regex.test(val); + } + +} diff --git a/yarn.lock b/yarn.lock index aa0a06e588..a2effb975c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3674,6 +3674,11 @@ glob-to-regexp@^0.3.0: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + glob@7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" From e9c8a31e1f07031e1b315020d48bb97434f40f41 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 13:28:00 -0600 Subject: [PATCH 013/334] Start and stop Mjolnir with the lifecycle --- src/Lifecycle.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 13f3abccb1..f2b50d7f2d 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -36,6 +36,7 @@ import * as StorageManager from './utils/StorageManager'; import SettingsStore from "./settings/SettingsStore"; import TypingStore from "./stores/TypingStore"; import {IntegrationManagers} from "./integrations/IntegrationManagers"; +import {Mjolnir} from "./mjolnir/Mjolnir"; /** * Called at startup, to attempt to build a logged-in Matrix session. It tries @@ -585,6 +586,11 @@ async function startMatrixClient(startSyncing=true) { IntegrationManagers.sharedInstance().startWatching(); ActiveWidgetStore.start(); + // Start Mjolnir even though we haven't checked the feature flag yet. Starting + // the thing just wastes CPU cycles, but should result in no actual functionality + // being exposed to the user. + Mjolnir.sharedInstance().start(); + if (startSyncing) { await MatrixClientPeg.start(); } else { @@ -645,6 +651,7 @@ export function stopMatrixClient(unsetClient=true) { Presence.stop(); ActiveWidgetStore.stop(); IntegrationManagers.sharedInstance().stopWatching(); + Mjolnir.sharedInstance().stop(); if (DMRoomMap.shared()) DMRoomMap.shared().stop(); const cli = MatrixClientPeg.get(); if (cli) { From b93508728a1e4abd3dd8fa411eb6760119bf6f7d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 14:24:51 -0600 Subject: [PATCH 014/334] Add personal list management to Mjolnir section --- res/css/_components.scss | 1 + .../tabs/user/_MjolnirUserSettingsTab.scss | 23 ++++ .../tabs/user/MjolnirUserSettingsTab.js | 117 +++++++++++++++++- src/i18n/strings/en_EN.json | 11 +- src/mjolnir/BanList.js | 16 ++- src/mjolnir/Mjolnir.js | 44 ++++++- src/utils/MatrixGlob.js | 2 +- 7 files changed, 197 insertions(+), 17 deletions(-) create mode 100644 res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss diff --git a/res/css/_components.scss b/res/css/_components.scss index 29c4d2c84c..a0e5881201 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -182,6 +182,7 @@ @import "./views/settings/tabs/room/_SecurityRoomSettingsTab.scss"; @import "./views/settings/tabs/user/_GeneralUserSettingsTab.scss"; @import "./views/settings/tabs/user/_HelpUserSettingsTab.scss"; +@import "./views/settings/tabs/user/_MjolnirUserSettingsTab.scss"; @import "./views/settings/tabs/user/_NotificationUserSettingsTab.scss"; @import "./views/settings/tabs/user/_PreferencesUserSettingsTab.scss"; @import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss"; diff --git a/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss b/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss new file mode 100644 index 0000000000..930dbeb440 --- /dev/null +++ b/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss @@ -0,0 +1,23 @@ +/* +Copyright 2019 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_MjolnirUserSettingsTab .mx_Field { + @mixin mx_Settings_fullWidthField; +} + +.mx_MjolnirUserSettingsTab_personalRule { + margin-bottom: 2px; +} diff --git a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js index 02e64c0bc1..97f92bb0b2 100644 --- a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js @@ -16,28 +16,115 @@ limitations under the License. import React from 'react'; import {_t} from "../../../../../languageHandler"; +import {Mjolnir} from "../../../../../mjolnir/Mjolnir"; +import {ListRule} from "../../../../../mjolnir/ListRule"; +import {RULE_SERVER, RULE_USER} from "../../../../../mjolnir/BanList"; +import Modal from "../../../../../Modal"; + const sdk = require("../../../../.."); export default class MjolnirUserSettingsTab extends React.Component { constructor() { super(); + + this.state = { + busy: false, + newPersonalRule: "", + }; + } + + _onPersonalRuleChanged = (e) => { + this.setState({newPersonalRule: e.target.value}); + }; + + _onAddPersonalRule = async (e) => { + e.preventDefault(); + e.stopPropagation(); + + let kind = RULE_SERVER; + if (this.state.newPersonalRule.startsWith("@")) { + kind = RULE_USER; + } + + this.setState({busy: true}); + try { + const list = await Mjolnir.sharedInstance().getOrCreatePersonalList(); + await list.banEntity(kind, this.state.newPersonalRule, _t("Ignored/Blocked")); + this.setState({newPersonalRule: ""}); // this will also cause the new rule to be rendered + } catch (e) { + console.error(e); + + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to remove Mjolnir rule', '', ErrorDialog, { + title: _t('Error removing ignored user/server'), + description: _t('Something went wrong. Please try again or view your console for hints.'), + }); + } finally { + this.setState({busy: false}); + } + }; + + async _removePersonalRule(rule: ListRule) { + this.setState({busy: true}); + try { + const list = Mjolnir.sharedInstance().getPersonalList(); + await list.unbanEntity(rule.kind, rule.entity); + } catch (e) { + console.error(e); + + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to remove Mjolnir rule', '', ErrorDialog, { + title: _t('Error removing ignored user/server'), + description: _t('Something went wrong. Please try again or view your console for hints.'), + }); + } finally { + this.setState({busy: false}); + } + } + + _renderPersonalBanListRules() { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + + const list = Mjolnir.sharedInstance().getPersonalList(); + const rules = list ? [...list.userRules, ...list.serverRules] : []; + if (!list || rules.length <= 0) return {_t("You have not ignored anyone.")}; + + const tiles = []; + for (const rule of rules) { + tiles.push( +
  • + this._removePersonalRule(rule)} + disabled={this.state.busy}> + {_t("Remove")} +   + {rule.entity} +
  • , + ); + } + + return

    {_t("You are currently ignoring:")}

    +
      {tiles}
    +
    ; } render() { + const Field = sdk.getComponent('elements.Field'); + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + return ( -
    +
    {_t("Ignored users")}
    - {_t("⚠ These settings are meant for advanced users.")}
    -
    + {_t("⚠ These settings are meant for advanced users.")}
    +
    {_t( "Add users and servers you want to ignore here. Use asterisks " + "to have Riot match any characters. For example, @bot:* " + "would ignore all users that have the name 'bot' on any server.", {}, {code: (s) => {s}}, - )}
    -
    + )}
    +
    {_t( "Ignoring people is done through ban lists which contain rules for " + "who to ban. Subscribing to a ban list means the users/servers blocked by " + @@ -55,7 +142,25 @@ export default class MjolnirUserSettingsTab extends React.Component { "the ban list in effect.", )}
    -

    TODO

    +
    + {this._renderPersonalBanListRules()} +
    +
    +
    + + + {_t("Ignore")} + + +
    {_t("Subscribed lists")} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 770f4723ef..fa15433a1a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -640,12 +640,21 @@ "Access Token:": "Access Token:", "click to reveal": "click to reveal", "Labs": "Labs", + "Ignored/Blocked": "Ignored/Blocked", + "Error removing ignored user/server": "Error removing ignored user/server", + "Something went wrong. Please try again or view your console for hints.": "Something went wrong. Please try again or view your console for hints.", + "You have not ignored anyone.": "You have not ignored anyone.", + "Remove": "Remove", + "You are currently ignoring:": "You are currently ignoring:", "Ignored users": "Ignored users", "⚠ These settings are meant for advanced users.": "⚠ These settings are meant for advanced users.", "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.", "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.", "Personal ban list": "Personal ban list", "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.", + "Server or user ID to ignore": "Server or user ID to ignore", + "eg: @bot:* or example.org": "eg: @bot:* or example.org", + "Ignore": "Ignore", "Subscribed lists": "Subscribed lists", "Subscribing to a ban list will cause you to join it!": "Subscribing to a ban list will cause you to join it!", "If this isn't what you want, please use a different tool to ignore users.": "If this isn't what you want, please use a different tool to ignore users.", @@ -776,7 +785,6 @@ "Discovery options will appear once you have added a phone number above.": "Discovery options will appear once you have added a phone number above.", "Unable to remove contact information": "Unable to remove contact information", "Remove %(email)s?": "Remove %(email)s?", - "Remove": "Remove", "Invalid Email Address": "Invalid Email Address", "This doesn't appear to be a valid email address": "This doesn't appear to be a valid email address", "Unable to add email address": "Unable to add email address", @@ -843,7 +851,6 @@ "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.", "Are you sure?": "Are you sure?", "No devices with registered encryption keys": "No devices with registered encryption keys", - "Ignore": "Ignore", "Jump to read receipt": "Jump to read receipt", "Mention": "Mention", "Invite": "Invite", diff --git a/src/mjolnir/BanList.js b/src/mjolnir/BanList.js index 6ebc0a7e36..026005420a 100644 --- a/src/mjolnir/BanList.js +++ b/src/mjolnir/BanList.js @@ -60,17 +60,23 @@ export class BanList { return this._rules.filter(r => r.kind === RULE_ROOM); } - banEntity(kind: string, entity: string, reason: string): Promise { - return MatrixClientPeg.get().sendStateEvent(this._roomId, ruleTypeToStable(kind, true), { + async banEntity(kind: string, entity: string, reason: string): Promise { + await MatrixClientPeg.get().sendStateEvent(this._roomId, ruleTypeToStable(kind, true), { entity: entity, reason: reason, recommendation: recommendationToStable(RECOMMENDATION_BAN, true), }, "rule:" + entity); + this._rules.push(new ListRule(entity, RECOMMENDATION_BAN, reason, ruleTypeToStable(kind, false))); } - unbanEntity(kind: string, entity: string): Promise { + async unbanEntity(kind: string, entity: string): Promise { // Empty state event is effectively deleting it. - return MatrixClientPeg.get().sendStateEvent(this._roomId, ruleTypeToStable(kind, true), {}, "rule:" + entity); + await MatrixClientPeg.get().sendStateEvent(this._roomId, ruleTypeToStable(kind, true), {}, "rule:" + entity); + this._rules = this._rules.filter(r => { + if (r.kind !== ruleTypeToStable(kind, false)) return true; + if (r.entity !== entity) return true; + return false; // we just deleted this rule + }); } updateList() { @@ -82,7 +88,7 @@ export class BanList { for (const eventType of ALL_RULE_TYPES) { const events = room.currentState.getStateEvents(eventType, undefined); for (const ev of events) { - if (!ev['state_key']) continue; + if (!ev.getStateKey()) continue; const kind = ruleTypeToStable(eventType, false); diff --git a/src/mjolnir/Mjolnir.js b/src/mjolnir/Mjolnir.js index a12534592d..d90ea9cd04 100644 --- a/src/mjolnir/Mjolnir.js +++ b/src/mjolnir/Mjolnir.js @@ -18,6 +18,7 @@ import MatrixClientPeg from "../MatrixClientPeg"; import {ALL_RULE_TYPES, BanList} from "./BanList"; import SettingsStore, {SettingLevel} from "../settings/SettingsStore"; import {_t} from "../languageHandler"; +import dis from "../dispatcher"; // TODO: Move this and related files to the js-sdk or something once finalized. @@ -27,19 +28,39 @@ export class Mjolnir { _lists: BanList[] = []; _roomIds: string[] = []; _mjolnirWatchRef = null; + _dispatcherRef = null; constructor() { } start() { - this._updateLists(SettingsStore.getValue("mjolnirRooms")); this._mjolnirWatchRef = SettingsStore.watchSetting("mjolnirRooms", null, this._onListsChanged.bind(this)); + this._dispatcherRef = dis.register(this._onAction); + dis.dispatch({ + action: 'do_after_sync_prepared', + deferred_action: {action: 'setup_mjolnir'}, + }); + } + + _onAction = (payload) => { + if (payload['action'] === 'setup_mjolnir') { + console.log("Setting up Mjolnir: after sync"); + this.setup(); + } + }; + + setup() { + if (!MatrixClientPeg.get()) return; + this._updateLists(SettingsStore.getValue("mjolnirRooms")); MatrixClientPeg.get().on("RoomState.events", this._onEvent.bind(this)); } stop() { SettingsStore.unwatchSetting(this._mjolnirWatchRef); + dis.unregister(this._dispatcherRef); + + if (!MatrixClientPeg.get()) return; MatrixClientPeg.get().removeListener("RoomState.events", this._onEvent.bind(this)); } @@ -52,8 +73,8 @@ export class Mjolnir { preset: "private_chat" }); personalRoomId = resp['room_id']; - SettingsStore.setValue("mjolnirPersonalRoom", null, SettingLevel.ACCOUNT, personalRoomId); - SettingsStore.setValue("mjolnirRooms", null, SettingLevel.ACCOUNT, [personalRoomId, ...this._roomIds]); + await SettingsStore.setValue("mjolnirPersonalRoom", null, SettingLevel.ACCOUNT, personalRoomId); + await SettingsStore.setValue("mjolnirRooms", null, SettingLevel.ACCOUNT, [personalRoomId, ...this._roomIds]); } if (!personalRoomId) { throw new Error("Error finding a room ID to use"); @@ -67,7 +88,21 @@ export class Mjolnir { return list; } + // get without creating the list + getPersonalList(): BanList { + const personalRoomId = SettingsStore.getValue("mjolnirPersonalRoom"); + if (!personalRoomId) return null; + + let list = this._lists.find(b => b.roomId === personalRoomId); + if (!list) list = new BanList(personalRoomId); + // we don't append the list to the tracked rooms because it should already be there. + // we're just trying to get the caller some utility access to the list + + return list; + } + _onEvent(event) { + if (!MatrixClientPeg.get()) return; if (!this._roomIds.includes(event.getRoomId())) return; if (!ALL_RULE_TYPES.includes(event.getType())) return; @@ -80,6 +115,9 @@ export class Mjolnir { } _updateLists(listRoomIds: string[]) { + if (!MatrixClientPeg.get()) return; + + console.log("Updating Mjolnir ban lists to: " + listRoomIds); this._lists = []; this._roomIds = listRoomIds || []; if (!listRoomIds) return; diff --git a/src/utils/MatrixGlob.js b/src/utils/MatrixGlob.js index cf55040625..b18e20ecf4 100644 --- a/src/utils/MatrixGlob.js +++ b/src/utils/MatrixGlob.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import * as globToRegexp from "glob-to-regexp"; +import globToRegexp from "glob-to-regexp"; // Taken with permission from matrix-bot-sdk: // https://github.com/turt2live/matrix-js-bot-sdk/blob/eb148c2ecec7bf3ade801d73deb43df042d55aef/src/MatrixGlob.ts From 39b657ce7c4c3402802c836acce8d2c095c0bb9a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 15:53:18 -0600 Subject: [PATCH 015/334] Add basic structure for (un)subscribing from lists --- .../tabs/user/_MjolnirUserSettingsTab.scss | 2 +- .../tabs/user/MjolnirUserSettingsTab.js | 145 ++++++++++++++++-- src/i18n/strings/en_EN.json | 13 +- src/mjolnir/Mjolnir.js | 20 +++ 4 files changed, 166 insertions(+), 14 deletions(-) diff --git a/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss b/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss index 930dbeb440..c60cbc5dea 100644 --- a/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss @@ -18,6 +18,6 @@ limitations under the License. @mixin mx_Settings_fullWidthField; } -.mx_MjolnirUserSettingsTab_personalRule { +.mx_MjolnirUserSettingsTab_listItem { margin-bottom: 2px; } diff --git a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js index 97f92bb0b2..4e05b57567 100644 --- a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js @@ -18,8 +18,9 @@ import React from 'react'; import {_t} from "../../../../../languageHandler"; import {Mjolnir} from "../../../../../mjolnir/Mjolnir"; import {ListRule} from "../../../../../mjolnir/ListRule"; -import {RULE_SERVER, RULE_USER} from "../../../../../mjolnir/BanList"; +import {BanList, RULE_SERVER, RULE_USER} from "../../../../../mjolnir/BanList"; import Modal from "../../../../../Modal"; +import MatrixClientPeg from "../../../../../MatrixClientPeg"; const sdk = require("../../../../.."); @@ -30,6 +31,7 @@ export default class MjolnirUserSettingsTab extends React.Component { this.state = { busy: false, newPersonalRule: "", + newList: "", }; } @@ -37,6 +39,10 @@ export default class MjolnirUserSettingsTab extends React.Component { this.setState({newPersonalRule: e.target.value}); }; + _onNewListChanged = (e) => { + this.setState({newList: e.target.value}); + }; + _onAddPersonalRule = async (e) => { e.preventDefault(); e.stopPropagation(); @@ -55,8 +61,8 @@ export default class MjolnirUserSettingsTab extends React.Component { console.error(e); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to remove Mjolnir rule', '', ErrorDialog, { - title: _t('Error removing ignored user/server'), + Modal.createTrackedDialog('Failed to add Mjolnir rule', '', ErrorDialog, { + title: _t('Error adding ignored user/server'), description: _t('Something went wrong. Please try again or view your console for hints.'), }); } finally { @@ -64,6 +70,28 @@ export default class MjolnirUserSettingsTab extends React.Component { } }; + _onSubscribeList = async (e) => { + e.preventDefault(); + e.stopPropagation(); + + this.setState({busy: true}); + try { + const room = await MatrixClientPeg.get().joinRoom(this.state.newList); + await Mjolnir.sharedInstance().subscribeToList(room.roomId); + this.setState({newList: ""}); // this will also cause the new rule to be rendered + } catch (e) { + console.error(e); + + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to subscribe to Mjolnir list', '', ErrorDialog, { + title: _t('Error subscribing to list'), + description: _t('Please verify the room ID or alias and try again.'), + }); + } finally { + this.setState({busy: false}); + } + }; + async _removePersonalRule(rule: ListRule) { this.setState({busy: true}); try { @@ -82,6 +110,28 @@ export default class MjolnirUserSettingsTab extends React.Component { } } + async _unsubscribeFromList(list: BanList) { + this.setState({busy: true}); + try { + await Mjolnir.sharedInstance().unsubscribeFromList(list.roomId); + await MatrixClientPeg.get().leave(list.roomId); + } catch (e) { + console.error(e); + + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to unsubscribe from Mjolnir list', '', ErrorDialog, { + title: _t('Error unsubscribing from list'), + description: _t('Please try again or view your console for hints.'), + }); + } finally { + this.setState({busy: false}); + } + } + + _viewListRules(list: BanList) { + // TODO + } + _renderPersonalBanListRules() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); @@ -92,9 +142,12 @@ export default class MjolnirUserSettingsTab extends React.Component { const tiles = []; for (const rule of rules) { tiles.push( -
  • - this._removePersonalRule(rule)} - disabled={this.state.busy}> +
  • + this._removePersonalRule(rule)} + disabled={this.state.busy} + > {_t("Remove")}   {rule.entity} @@ -102,9 +155,52 @@ export default class MjolnirUserSettingsTab extends React.Component { ); } - return

    {_t("You are currently ignoring:")}

    -
      {tiles}
    -
    ; + return ( +
    +

    {_t("You are currently ignoring:")}

    +
      {tiles}
    +
    + ); + } + + _renderSubscribedBanLists() { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + + const personalList = Mjolnir.sharedInstance().getPersonalList(); + const lists = Mjolnir.sharedInstance().lists.filter(b => personalList ? personalList.roomId !== b.roomId : true); + if (!lists || lists.length <= 0) return {_t("You are not subscribed to any lists")}; + + const tiles = []; + for (const list of lists) { + const room = MatrixClientPeg.get().getRoom(list.roomId); + const name = room ? {room.name} ({list.roomId}) : list.roomId; + tiles.push( +
  • + this._unsubscribeFromList(list)} + disabled={this.state.busy} + > + {_t("Unsubscribe")} +   + this._viewListRules(list)} + disabled={this.state.busy} + > + {_t("View rules")} +   + {name} +
  • , + ); + } + + return ( +
    +

    {_t("You are currently subscribed to:")}

    +
      {tiles}
    +
    + ); } render() { @@ -155,8 +251,12 @@ export default class MjolnirUserSettingsTab extends React.Component { value={this.state.newPersonalRule} onChange={this._onPersonalRuleChanged} /> - + {_t("Ignore")} @@ -171,7 +271,28 @@ export default class MjolnirUserSettingsTab extends React.Component { "If this isn't what you want, please use a different tool to ignore users.", )}
    -

    TODO

    +
    + {this._renderSubscribedBanLists()} +
    +
    +
    + + + {_t("Subscribe")} + + +
    ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index fa15433a1a..561dbc4da9 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -641,11 +641,20 @@ "click to reveal": "click to reveal", "Labs": "Labs", "Ignored/Blocked": "Ignored/Blocked", - "Error removing ignored user/server": "Error removing ignored user/server", + "Error adding ignored user/server": "Error adding ignored user/server", "Something went wrong. Please try again or view your console for hints.": "Something went wrong. Please try again or view your console for hints.", + "Error subscribing to list": "Error subscribing to list", + "Please verify the room ID or alias and try again.": "Please verify the room ID or alias and try again.", + "Error removing ignored user/server": "Error removing ignored user/server", + "Error unsubscribing from list": "Error unsubscribing from list", + "Please try again or view your console for hints.": "Please try again or view your console for hints.", "You have not ignored anyone.": "You have not ignored anyone.", "Remove": "Remove", "You are currently ignoring:": "You are currently ignoring:", + "You are not subscribed to any lists": "You are not subscribed to any lists", + "Unsubscribe": "Unsubscribe", + "View rules": "View rules", + "You are currently subscribed to:": "You are currently subscribed to:", "Ignored users": "Ignored users", "⚠ These settings are meant for advanced users.": "⚠ These settings are meant for advanced users.", "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.", @@ -658,6 +667,8 @@ "Subscribed lists": "Subscribed lists", "Subscribing to a ban list will cause you to join it!": "Subscribing to a ban list will cause you to join it!", "If this isn't what you want, please use a different tool to ignore users.": "If this isn't what you want, please use a different tool to ignore users.", + "Room ID or alias of ban list": "Room ID or alias of ban list", + "Subscribe": "Subscribe", "Notifications": "Notifications", "Start automatically after system login": "Start automatically after system login", "Always show the window menu bar": "Always show the window menu bar", diff --git a/src/mjolnir/Mjolnir.js b/src/mjolnir/Mjolnir.js index d90ea9cd04..5edfe3750e 100644 --- a/src/mjolnir/Mjolnir.js +++ b/src/mjolnir/Mjolnir.js @@ -33,6 +33,14 @@ export class Mjolnir { constructor() { } + get roomIds(): string[] { + return this._roomIds; + } + + get lists(): BanList[] { + return this._lists; + } + start() { this._mjolnirWatchRef = SettingsStore.watchSetting("mjolnirRooms", null, this._onListsChanged.bind(this)); @@ -101,6 +109,18 @@ export class Mjolnir { return list; } + async subscribeToList(roomId: string) { + const roomIds = [...this._roomIds, roomId]; + await SettingsStore.setValue("mjolnirRooms", null, SettingLevel.ACCOUNT, roomIds); + this._lists.push(new BanList(roomId)); + } + + async unsubscribeFromList(roomId: string) { + const roomIds = this._roomIds.filter(r => r !== roomId); + await SettingsStore.setValue("mjolnirRooms", null, SettingLevel.ACCOUNT, roomIds); + this._lists = this._lists.filter(b => b.roomId !== roomId); + } + _onEvent(event) { if (!MatrixClientPeg.get()) return; if (!this._roomIds.includes(event.getRoomId())) return; From b420fd675857d6c3e212caafa1c56d2ddc4a16da Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 16:00:31 -0600 Subject: [PATCH 016/334] Add a view rules dialog --- .../tabs/user/MjolnirUserSettingsTab.js | 29 ++++++++++++++++++- src/i18n/strings/en_EN.json | 6 +++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js index 4e05b57567..a02ca2c570 100644 --- a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js @@ -129,7 +129,34 @@ export default class MjolnirUserSettingsTab extends React.Component { } _viewListRules(list: BanList) { - // TODO + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + + const room = MatrixClientPeg.get().getRoom(list.roomId); + const name = room ? room.name : list.roomId; + + const renderRules = (rules: ListRule[]) => { + if (rules.length === 0) return {_t("None")}; + + const tiles = []; + for (const rule of rules) { + tiles.push(
  • {rule.entity}
  • ); + } + return
      {tiles}
    ; + }; + + Modal.createTrackedDialog('View Mjolnir list rules', '', QuestionDialog, { + title: _t("Ban list rules - %(roomName)s", {roomName: name}), + description: ( +
    +

    {_t("Server rules")}

    + {renderRules(list.serverRules)} +

    {_t("User rules")}

    + {renderRules(list.userRules)} +
    + ), + button: _t("Close"), + hasCancelButton: false, + }); } _renderPersonalBanListRules() { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 561dbc4da9..58fa564250 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -648,6 +648,11 @@ "Error removing ignored user/server": "Error removing ignored user/server", "Error unsubscribing from list": "Error unsubscribing from list", "Please try again or view your console for hints.": "Please try again or view your console for hints.", + "None": "None", + "Ban list rules - %(roomName)s": "Ban list rules - %(roomName)s", + "Server rules": "Server rules", + "User rules": "User rules", + "Close": "Close", "You have not ignored anyone.": "You have not ignored anyone.", "Remove": "Remove", "You are currently ignoring:": "You are currently ignoring:", @@ -874,7 +879,6 @@ "Revoke Moderator": "Revoke Moderator", "Make Moderator": "Make Moderator", "Admin Tools": "Admin Tools", - "Close": "Close", "and %(count)s others...|other": "and %(count)s others...", "and %(count)s others...|one": "and one other...", "Invite to this room": "Invite to this room", From 11068d189cf03e309cccca75b83ee8674fb01796 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 16:19:42 -0600 Subject: [PATCH 017/334] Hide messages blocked by ban lists --- res/css/_components.scss | 1 + res/css/views/messages/_MjolnirBody.scss | 19 ++++++++ src/components/views/messages/MessageEvent.js | 24 +++++++++- src/components/views/messages/MjolnirBody.js | 47 +++++++++++++++++++ src/i18n/strings/en_EN.json | 1 + 5 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 res/css/views/messages/_MjolnirBody.scss create mode 100644 src/components/views/messages/MjolnirBody.js diff --git a/res/css/_components.scss b/res/css/_components.scss index a0e5881201..788e22a766 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -123,6 +123,7 @@ @import "./views/messages/_MTextBody.scss"; @import "./views/messages/_MessageActionBar.scss"; @import "./views/messages/_MessageTimestamp.scss"; +@import "./views/messages/_MjolnirBody.scss"; @import "./views/messages/_ReactionsRow.scss"; @import "./views/messages/_ReactionsRowButton.scss"; @import "./views/messages/_ReactionsRowButtonTooltip.scss"; diff --git a/res/css/views/messages/_MjolnirBody.scss b/res/css/views/messages/_MjolnirBody.scss new file mode 100644 index 0000000000..80be7429e5 --- /dev/null +++ b/res/css/views/messages/_MjolnirBody.scss @@ -0,0 +1,19 @@ +/* +Copyright 2019 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_MjolnirBody { + opacity: 0.4; +} diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js index a616dd96ed..2e353794d7 100644 --- a/src/components/views/messages/MessageEvent.js +++ b/src/components/views/messages/MessageEvent.js @@ -18,6 +18,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import sdk from '../../../index'; +import SettingsStore from "../../../settings/SettingsStore"; +import {Mjolnir} from "../../../mjolnir/Mjolnir"; module.exports = createReactClass({ displayName: 'MessageEvent', @@ -49,6 +51,10 @@ module.exports = createReactClass({ return this.refs.body && this.refs.body.getEventTileOps ? this.refs.body.getEventTileOps() : null; }, + onTileUpdate: function() { + this.forceUpdate(); + }, + render: function() { const UnknownBody = sdk.getComponent('messages.UnknownBody'); @@ -81,6 +87,20 @@ module.exports = createReactClass({ } } + if (SettingsStore.isFeatureEnabled("feature_mjolnir")) { + const allowRender = localStorage.getItem(`mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`) === "true"; + + if (!allowRender) { + const userDomain = this.props.mxEvent.getSender().split(':').slice(1).join(':'); + const userBanned = Mjolnir.sharedInstance().isUserBanned(this.props.mxEvent.getSender()); + const serverBanned = Mjolnir.sharedInstance().isServerBanned(userDomain); + + if (userBanned || serverBanned) { + BodyType = sdk.getComponent('messages.MjolnirBody'); + } + } + } + return ; + onHeightChanged={this.props.onHeightChanged} + onTileUpdate={this.onTileUpdate} + />; }, }); diff --git a/src/components/views/messages/MjolnirBody.js b/src/components/views/messages/MjolnirBody.js new file mode 100644 index 0000000000..994642863b --- /dev/null +++ b/src/components/views/messages/MjolnirBody.js @@ -0,0 +1,47 @@ +/* +Copyright 2019 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 PropTypes from 'prop-types'; +import {_t} from '../../../languageHandler'; + +export default class MjolnirBody extends React.Component { + static propTypes = { + mxEvent: PropTypes.object.isRequired, + onTileUpdate: PropTypes.func.isRequired, + }; + + constructor() { + super(); + } + + _onAllowClick = (e) => { + e.preventDefault(); + e.stopPropagation(); + + localStorage.setItem(`mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`, "true"); + this.props.onTileUpdate(); + }; + + render() { + return ( +
    {_t( + "You have ignored this user, so their message is hidden. Show anyways.", + {}, {a: (sub) => {sub}}, + )}
    + ); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 58fa564250..74433a9c04 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1094,6 +1094,7 @@ "Invalid file%(extra)s": "Invalid file%(extra)s", "Error decrypting image": "Error decrypting image", "Show image": "Show image", + "You have ignored this user, so their message is hidden. Show anyways.": "You have ignored this user, so their message is hidden. Show anyways.", "Error decrypting video": "Error decrypting video", "Show all": "Show all", "reacted with %(shortName)s": "reacted with %(shortName)s", From 3e4a721111f6bb6a17e219ea97ead4dfe4589792 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 16:27:45 -0600 Subject: [PATCH 018/334] Appease the linter --- src/components/views/dialogs/UserSettingsDialog.js | 2 +- src/components/views/messages/MessageEvent.js | 3 ++- src/components/views/messages/MjolnirBody.js | 3 ++- .../settings/tabs/user/MjolnirUserSettingsTab.js | 14 ++++++++------ src/mjolnir/BanList.js | 12 +++++++++--- src/mjolnir/ListRule.js | 4 +++- src/mjolnir/Mjolnir.js | 8 +++++--- src/utils/MatrixGlob.js | 1 - 8 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js index 6e324ad3fb..d3ab2b8722 100644 --- a/src/components/views/dialogs/UserSettingsDialog.js +++ b/src/components/views/dialogs/UserSettingsDialog.js @@ -42,7 +42,7 @@ export default class UserSettingsDialog extends React.Component { this.state = { mjolnirEnabled: SettingsStore.isFeatureEnabled("feature_mjolnir"), - } + }; } componentDidMount(): void { diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js index 2e353794d7..0d22658884 100644 --- a/src/components/views/messages/MessageEvent.js +++ b/src/components/views/messages/MessageEvent.js @@ -88,7 +88,8 @@ module.exports = createReactClass({ } if (SettingsStore.isFeatureEnabled("feature_mjolnir")) { - const allowRender = localStorage.getItem(`mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`) === "true"; + const key = `mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`; + const allowRender = localStorage.getItem(key) === "true"; if (!allowRender) { const userDomain = this.props.mxEvent.getSender().split(':').slice(1).join(':'); diff --git a/src/components/views/messages/MjolnirBody.js b/src/components/views/messages/MjolnirBody.js index 994642863b..d03c6c658d 100644 --- a/src/components/views/messages/MjolnirBody.js +++ b/src/components/views/messages/MjolnirBody.js @@ -32,7 +32,8 @@ export default class MjolnirBody extends React.Component { e.preventDefault(); e.stopPropagation(); - localStorage.setItem(`mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`, "true"); + const key = `mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`; + localStorage.setItem(key, "true"); this.props.onTileUpdate(); }; diff --git a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js index a02ca2c570..608be0b129 100644 --- a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js @@ -194,7 +194,9 @@ export default class MjolnirUserSettingsTab extends React.Component { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const personalList = Mjolnir.sharedInstance().getPersonalList(); - const lists = Mjolnir.sharedInstance().lists.filter(b => personalList ? personalList.roomId !== b.roomId : true); + const lists = Mjolnir.sharedInstance().lists.filter(b => { + return personalList? personalList.roomId !== b.roomId : true; + }); if (!lists || lists.length <= 0) return {_t("You are not subscribed to any lists")}; const tiles = []; @@ -239,19 +241,19 @@ export default class MjolnirUserSettingsTab extends React.Component {
    {_t("Ignored users")}
    - {_t("⚠ These settings are meant for advanced users.")}
    -
    + {_t("⚠ These settings are meant for advanced users.")}
    +
    {_t( "Add users and servers you want to ignore here. Use asterisks " + "to have Riot match any characters. For example, @bot:* " + "would ignore all users that have the name 'bot' on any server.", {}, {code: (s) => {s}}, - )}
    -
    + )}
    +
    {_t( "Ignoring people is done through ban lists which contain rules for " + "who to ban. Subscribing to a ban list means the users/servers blocked by " + - "that list will be hidden from you." + "that list will be hidden from you.", )}
    diff --git a/src/mjolnir/BanList.js b/src/mjolnir/BanList.js index 026005420a..60a924a52b 100644 --- a/src/mjolnir/BanList.js +++ b/src/mjolnir/BanList.js @@ -29,9 +29,15 @@ export const SERVER_RULE_TYPES = [RULE_SERVER, "org.matrix.mjolnir.rule.server"] export const ALL_RULE_TYPES = [...USER_RULE_TYPES, ...ROOM_RULE_TYPES, ...SERVER_RULE_TYPES]; export function ruleTypeToStable(rule: string, unstable = true): string { - if (USER_RULE_TYPES.includes(rule)) return unstable ? USER_RULE_TYPES[USER_RULE_TYPES.length - 1] : RULE_USER; - if (ROOM_RULE_TYPES.includes(rule)) return unstable ? ROOM_RULE_TYPES[ROOM_RULE_TYPES.length - 1] : RULE_ROOM; - if (SERVER_RULE_TYPES.includes(rule)) return unstable ? SERVER_RULE_TYPES[SERVER_RULE_TYPES.length - 1] : RULE_SERVER; + if (USER_RULE_TYPES.includes(rule)) { + return unstable ? USER_RULE_TYPES[USER_RULE_TYPES.length - 1] : RULE_USER; + } + if (ROOM_RULE_TYPES.includes(rule)) { + return unstable ? ROOM_RULE_TYPES[ROOM_RULE_TYPES.length - 1] : RULE_ROOM; + } + if (SERVER_RULE_TYPES.includes(rule)) { + return unstable ? SERVER_RULE_TYPES[SERVER_RULE_TYPES.length - 1] : RULE_SERVER; + } return null; } diff --git a/src/mjolnir/ListRule.js b/src/mjolnir/ListRule.js index d33248d24c..1d472e06d6 100644 --- a/src/mjolnir/ListRule.js +++ b/src/mjolnir/ListRule.js @@ -22,7 +22,9 @@ export const RECOMMENDATION_BAN = "m.ban"; export const RECOMMENDATION_BAN_TYPES = [RECOMMENDATION_BAN, "org.matrix.mjolnir.ban"]; export function recommendationToStable(recommendation: string, unstable = true): string { - if (RECOMMENDATION_BAN_TYPES.includes(recommendation)) return unstable ? RECOMMENDATION_BAN_TYPES[RECOMMENDATION_BAN_TYPES.length - 1] : RECOMMENDATION_BAN; + if (RECOMMENDATION_BAN_TYPES.includes(recommendation)) { + return unstable ? RECOMMENDATION_BAN_TYPES[RECOMMENDATION_BAN_TYPES.length - 1] : RECOMMENDATION_BAN; + } return null; } diff --git a/src/mjolnir/Mjolnir.js b/src/mjolnir/Mjolnir.js index 5edfe3750e..9177c621d1 100644 --- a/src/mjolnir/Mjolnir.js +++ b/src/mjolnir/Mjolnir.js @@ -78,11 +78,13 @@ export class Mjolnir { const resp = await MatrixClientPeg.get().createRoom({ name: _t("My Ban List"), topic: _t("This is your list of users/servers you have blocked - don't leave the room!"), - preset: "private_chat" + preset: "private_chat", }); personalRoomId = resp['room_id']; - await SettingsStore.setValue("mjolnirPersonalRoom", null, SettingLevel.ACCOUNT, personalRoomId); - await SettingsStore.setValue("mjolnirRooms", null, SettingLevel.ACCOUNT, [personalRoomId, ...this._roomIds]); + await SettingsStore.setValue( + "mjolnirPersonalRoom", null, SettingLevel.ACCOUNT, personalRoomId); + await SettingsStore.setValue( + "mjolnirRooms", null, SettingLevel.ACCOUNT, [personalRoomId, ...this._roomIds]); } if (!personalRoomId) { throw new Error("Error finding a room ID to use"); diff --git a/src/utils/MatrixGlob.js b/src/utils/MatrixGlob.js index b18e20ecf4..e07aaab541 100644 --- a/src/utils/MatrixGlob.js +++ b/src/utils/MatrixGlob.js @@ -50,5 +50,4 @@ export class MatrixGlob { test(val: string): boolean { return this._regex.test(val); } - } From 3c45a39caaab2c13f8b687e08679ead3adca7b85 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 16:30:51 -0600 Subject: [PATCH 019/334] Appease the other linter --- res/css/views/messages/_MjolnirBody.scss | 2 +- res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/res/css/views/messages/_MjolnirBody.scss b/res/css/views/messages/_MjolnirBody.scss index 80be7429e5..2760adfd7e 100644 --- a/res/css/views/messages/_MjolnirBody.scss +++ b/res/css/views/messages/_MjolnirBody.scss @@ -15,5 +15,5 @@ limitations under the License. */ .mx_MjolnirBody { - opacity: 0.4; + opacity: 0.4; } diff --git a/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss b/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss index c60cbc5dea..2a3fd12f31 100644 --- a/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss @@ -15,9 +15,9 @@ limitations under the License. */ .mx_MjolnirUserSettingsTab .mx_Field { - @mixin mx_Settings_fullWidthField; + @mixin mx_Settings_fullWidthField; } .mx_MjolnirUserSettingsTab_listItem { - margin-bottom: 2px; + margin-bottom: 2px; } From 07b8e128d2adc198767d9978329448ea59dad868 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 16:43:03 -0600 Subject: [PATCH 020/334] Bypass the tests being weird They run kinda-but-not-really async, which can lead to early/late calls to `stop()` --- src/mjolnir/Mjolnir.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/mjolnir/Mjolnir.js b/src/mjolnir/Mjolnir.js index 9177c621d1..7539dfafb0 100644 --- a/src/mjolnir/Mjolnir.js +++ b/src/mjolnir/Mjolnir.js @@ -66,7 +66,14 @@ export class Mjolnir { stop() { SettingsStore.unwatchSetting(this._mjolnirWatchRef); - dis.unregister(this._dispatcherRef); + + try { + if (this._dispatcherRef) dis.unregister(this._dispatcherRef); + } catch (e) { + console.error(e); + // Only the tests cause problems with this particular block of code. We should + // never be here in production. + } if (!MatrixClientPeg.get()) return; MatrixClientPeg.get().removeListener("RoomState.events", this._onEvent.bind(this)); From 446e21c2e129ae8387b5eaf6f6fce198731887a2 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 1 Nov 2019 10:44:30 +0000 Subject: [PATCH 021/334] Relax identity server discovery error handling If discovery results in a warning for the identity server (as in can't be found or is malformed), this allows you to continue signing in and shows the warning above the form. Fixes https://github.com/vector-im/riot-web/issues/11102 --- src/components/structures/auth/Login.js | 15 ++++++++++-- src/utils/AutoDiscoveryUtils.js | 31 +++++++++++++++---------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index ad77ed49a5..af308e1407 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -378,8 +378,19 @@ module.exports = createReactClass({ // Do a quick liveliness check on the URLs try { - await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl); - this.setState({serverIsAlive: true, errorText: ""}); + const { warning } = + await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl); + if (warning) { + this.setState({ + ...AutoDiscoveryUtils.authComponentStateForError(warning), + errorText: "", + }); + } else { + this.setState({ + serverIsAlive: true, + errorText: "", + }); + } } catch (e) { this.setState({ busy: false, diff --git a/src/utils/AutoDiscoveryUtils.js b/src/utils/AutoDiscoveryUtils.js index e94c454a3e..49898aae90 100644 --- a/src/utils/AutoDiscoveryUtils.js +++ b/src/utils/AutoDiscoveryUtils.js @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +Copyright 2019 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. @@ -33,6 +34,8 @@ export class ValidatedServerConfig { isUrl: string; isDefault: boolean; + + warning: string; } export default class AutoDiscoveryUtils { @@ -56,7 +59,14 @@ export default class AutoDiscoveryUtils { * implementation for known values. * @returns {*} The state for the component, given the error. */ - static authComponentStateForError(err: Error, pageName="login"): Object { + static authComponentStateForError(err: string | Error | null, pageName = "login"): Object { + if (!err) { + return { + serverIsAlive: true, + serverErrorIsFatal: false, + serverDeadError: null, + }; + } let title = _t("Cannot reach homeserver"); let body = _t("Ensure you have a stable internet connection, or get in touch with the server admin"); if (!AutoDiscoveryUtils.isLivelinessError(err)) { @@ -153,11 +163,9 @@ export default class AutoDiscoveryUtils { /** * Validates a server configuration, using a homeserver domain name as input. * @param {string} serverName The homeserver domain name (eg: "matrix.org") to validate. - * @param {boolean} syntaxOnly If true, errors relating to liveliness of the servers will - * not be raised. * @returns {Promise} Resolves to the validated configuration. */ - static async validateServerName(serverName: string, syntaxOnly=false): ValidatedServerConfig { + static async validateServerName(serverName: string): ValidatedServerConfig { const result = await AutoDiscovery.findClientConfig(serverName); return AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, result); } @@ -186,7 +194,7 @@ export default class AutoDiscoveryUtils { const defaultConfig = SdkConfig.get()["validated_server_config"]; // Validate the identity server first because an invalid identity server causes - // and invalid homeserver, which may not be picked up correctly. + // an invalid homeserver, which may not be picked up correctly. // Note: In the cases where we rely on the default IS from the config (namely // lack of identity server provided by the discovery method), we intentionally do not @@ -197,20 +205,18 @@ export default class AutoDiscoveryUtils { preferredIdentityUrl = isResult["base_url"]; } else if (isResult && isResult.state !== AutoDiscovery.PROMPT) { console.error("Error determining preferred identity server URL:", isResult); - if (!syntaxOnly || !AutoDiscoveryUtils.isLivelinessError(isResult.error)) { + if (isResult.state === AutoDiscovery.FAIL_ERROR) { if (AutoDiscovery.ALL_ERRORS.indexOf(isResult.error) !== -1) { throw newTranslatableError(isResult.error); } throw newTranslatableError(_td("Unexpected error resolving identity server configuration")); } // else the error is not related to syntax - continue anyways. - // rewrite homeserver error if we don't care about problems - if (syntaxOnly) { - hsResult.error = AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER; + // rewrite homeserver error since we don't care about problems + hsResult.error = AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER; - // Also use the user's supplied identity server if provided - if (isResult["base_url"]) preferredIdentityUrl = isResult["base_url"]; - } + // Also use the user's supplied identity server if provided + if (isResult["base_url"]) preferredIdentityUrl = isResult["base_url"]; } if (hsResult.state !== AutoDiscovery.SUCCESS) { @@ -241,6 +247,7 @@ export default class AutoDiscoveryUtils { hsNameIsDifferent: url.hostname !== preferredHomeserverName, isUrl: preferredIdentityUrl, isDefault: false, + warning: hsResult.error, }); } } From 86be607e92dbf148498e284d083e62b8716be2a8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 6 Nov 2019 10:52:00 -0700 Subject: [PATCH 022/334] onTileUpdate -> onMessageAllowed We keep onTileUpdate in MessgeEvent because it's a generic thing for the class to handle. onMessageAllowed is slightly different than onShowAllowed because "show allowed" doesn't quite make sense on its own, imo. --- src/components/views/messages/MessageEvent.js | 2 +- src/components/views/messages/MjolnirBody.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js index 0d22658884..e75bcc4332 100644 --- a/src/components/views/messages/MessageEvent.js +++ b/src/components/views/messages/MessageEvent.js @@ -112,7 +112,7 @@ module.exports = createReactClass({ replacingEventId={this.props.replacingEventId} editState={this.props.editState} onHeightChanged={this.props.onHeightChanged} - onTileUpdate={this.onTileUpdate} + onMessageAllowed={this.onTileUpdate} />; }, }); diff --git a/src/components/views/messages/MjolnirBody.js b/src/components/views/messages/MjolnirBody.js index d03c6c658d..baaee91657 100644 --- a/src/components/views/messages/MjolnirBody.js +++ b/src/components/views/messages/MjolnirBody.js @@ -21,7 +21,7 @@ import {_t} from '../../../languageHandler'; export default class MjolnirBody extends React.Component { static propTypes = { mxEvent: PropTypes.object.isRequired, - onTileUpdate: PropTypes.func.isRequired, + onMessageAllowed: PropTypes.func.isRequired, }; constructor() { @@ -34,7 +34,7 @@ export default class MjolnirBody extends React.Component { const key = `mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`; localStorage.setItem(key, "true"); - this.props.onTileUpdate(); + this.props.onMessageAllowed(); }; render() { From 06ab9efed639c8944c6181290c6683122151540a Mon Sep 17 00:00:00 2001 From: Slavi Pantaleev Date: Tue, 5 Nov 2019 07:16:59 +0000 Subject: [PATCH 023/334] Translated using Weblate (Bulgarian) Currently translated at 100.0% (1853 of 1853 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/bg/ --- src/i18n/strings/bg.json | 47 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/bg.json b/src/i18n/strings/bg.json index 2287c5b295..6f198b2e5a 100644 --- a/src/i18n/strings/bg.json +++ b/src/i18n/strings/bg.json @@ -2204,5 +2204,50 @@ "wait and try again later": "изчакате и опитате пак", "Clear cache and reload": "Изчисти кеша и презареди", "Show tray icon and minimize window to it on close": "Показвай икона в лентата и минимизирай прозореца там при затваряне", - "Your email address hasn't been verified yet": "Имейл адресът ви все още не е потвърден" + "Your email address hasn't been verified yet": "Имейл адресът ви все още не е потвърден", + "Click the link in the email you received to verify and then click continue again.": "Кликнете на връзката получена по имейл за да потвърдите, а след това натиснете продължи отново.", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "На път сте да премахнете 1 съобщение от %(user)s. Това е необратимо. Искате ли да продължите?", + "Remove %(count)s messages|one": "Премахни 1 съобщение", + "Room %(name)s": "Стая %(name)s", + "Recent rooms": "Скорошни стаи", + "%(count)s unread messages including mentions.|other": "%(count)s непрочетени съобщения, включително споменавания.", + "%(count)s unread messages including mentions.|one": "1 непрочетено споменаване.", + "%(count)s unread messages.|other": "%(count)s непрочетени съобщения.", + "%(count)s unread messages.|one": "1 непрочетено съобщение.", + "Unread mentions.": "Непрочетени споменавания.", + "Unread messages.": "Непрочетени съобщения.", + "Trust & Devices": "Доверие и устройства", + "Direct messages": "Директни съобщения", + "Failed to deactivate user": "Неуспешно деактивиране на потребител", + "This client does not support end-to-end encryption.": "Този клиент не поддържа шифроване от край до край.", + "Messages in this room are not end-to-end encrypted.": "Съобщенията в тази стая не са шифровани от край до край.", + "React": "Реагирай", + "Message Actions": "Действия със съобщението", + "Show image": "Покажи снимката", + "Frequently Used": "Често използвани", + "Smileys & People": "Усмивки и хора", + "Animals & Nature": "Животни и природа", + "Food & Drink": "Храна и напитки", + "Activities": "Действия", + "Travel & Places": "Пътуване и места", + "Objects": "Обекти", + "Symbols": "Символи", + "Flags": "Знамена", + "Quick Reactions": "Бързи реакции", + "Cancel search": "Отмени търсенето", + "Please create a new issue on GitHub so that we can investigate this bug.": "Моля, отворете нов проблем в GitHub за да проучим проблема.", + "To continue you need to accept the terms of this service.": "За да продължите, трябва да приемете условията за ползване.", + "Document": "Документ", + "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "Липсва публичния ключ за catcha в конфигурацията на сървъра. Съобщете това на администратора на сървъра.", + "No identity server is configured so you cannot add an email address in order to reset your password in the future.": "Не е конфигуриран сървър за самоличност, така че не можете да добавите имейл адрес за възстановяване на паролата в бъдеще.", + "%(creator)s created and configured the room.": "%(creator)s създаде и настрой стаята.", + "Jump to first unread room.": "Отиди до първата непрочетена стая.", + "Jump to first invite.": "Отиди до първата покана.", + "Command Autocomplete": "Подсказка за команди", + "Community Autocomplete": "Подсказка за общности", + "DuckDuckGo Results": "DuckDuckGo резултати", + "Emoji Autocomplete": "Подсказка за емоджита", + "Notification Autocomplete": "Подсказка за уведомления", + "Room Autocomplete": "Подсказка за стаи", + "User Autocomplete": "Подсказка за потребители" } From dc5abbe3809831e00a8d2d1f2ed85d8c77bf364a Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Tue, 5 Nov 2019 03:58:00 +0000 Subject: [PATCH 024/334] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1853 of 1853 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 94183ef83f..58dca89415 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2272,5 +2272,6 @@ "Unread messages.": "未讀的訊息。", "Show tray icon and minimize window to it on close": "顯示系統匣圖示並在關閉視窗時將其最小化至其中", "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "此動作需要存取預設的身份識別伺服器 以驗證電子郵件或電話號碼,但伺服器沒有任何服務條款。", - "Trust": "信任" + "Trust": "信任", + "Message Actions": "訊息動作" } From 6d4971c29eb2b760f15dae41085d5b164d5dc8e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Mon, 4 Nov 2019 12:28:24 +0000 Subject: [PATCH 025/334] Translated using Weblate (French) Currently translated at 100.0% (1853 of 1853 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 7807facb1c..328c3b7f9e 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2279,5 +2279,6 @@ "Unread messages.": "Messages non lus.", "Show tray icon and minimize window to it on close": "Afficher l’icône dans la barre d’état et minimiser la fenêtre lors de la fermeture", "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Cette action nécessite l’accès au serveur d’identité par défaut afin de valider une adresse e-mail ou un numéro de téléphone, mais le serveur n’a aucune condition de service.", - "Trust": "Confiance" + "Trust": "Confiance", + "Message Actions": "Actions de message" } From 4eb39190b493159f863debf7987b86980f8cd5b7 Mon Sep 17 00:00:00 2001 From: dreamerchris Date: Tue, 5 Nov 2019 12:12:12 +0000 Subject: [PATCH 026/334] Translated using Weblate (Greek) Currently translated at 39.7% (735 of 1853 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/el/ --- src/i18n/strings/el.json | 91 +++++++++++++++++++++++++++++++++------- 1 file changed, 75 insertions(+), 16 deletions(-) diff --git a/src/i18n/strings/el.json b/src/i18n/strings/el.json index c5d6468881..a2438cc22c 100644 --- a/src/i18n/strings/el.json +++ b/src/i18n/strings/el.json @@ -57,7 +57,7 @@ "%(senderDisplayName)s removed the room name.": "Ο %(senderDisplayName)s διέγραψε το όνομα του δωματίου.", "Changes your display nickname": "Αλλάζει το ψευδώνυμο χρήστη", "Conference call failed.": "Η κλήση συνδιάσκεψης απέτυχε.", - "powered by Matrix": "με τη βοήθεια του Matrix", + "powered by Matrix": "λειτουργεί με το Matrix", "Confirm password": "Επιβεβαίωση κωδικού πρόσβασης", "Confirm your new password": "Επιβεβαίωση του νέου κωδικού πρόσβασης", "Continue": "Συνέχεια", @@ -567,7 +567,7 @@ "numbullet": "απαρίθμηση", "You must join the room to see its files": "Πρέπει να συνδεθείτε στο δωμάτιο για να δείτε τα αρχεία του", "Reject all %(invitedRooms)s invites": "Απόρριψη όλων των προσκλήσεων %(invitedRooms)s", - "Failed to invite the following users to the %(roomName)s room:": "Δεν ήταν δυνατή η πρόσκληση των χρηστών στο δωμάτιο %(roomName)s:", + "Failed to invite the following users to the %(roomName)s room:": "Δεν ήταν δυνατή η πρόσκληση των παρακάτω χρηστών στο δωμάτιο %(roomName)s:", "Deops user with given id": "Deop χρήστη με το συγκεκριμένο αναγνωριστικό", "Drop here to tag %(section)s": "Απόθεση εδώ για ορισμό ετικέτας στο %(section)s", "Join as voice or video.": "Συμμετάσχετε με φωνή ή βίντεο.", @@ -575,7 +575,7 @@ "Show timestamps in 12 hour format (e.g. 2:30pm)": "Εμφάνιση χρονικών σημάνσεων σε 12ωρη μορφή ώρας (π.χ. 2:30 μ.μ.)", "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": "Το κλειδί υπογραφής που δώσατε αντιστοιχεί στο κλειδί υπογραφής που λάβατε από τη συσκευή %(userId)s %(deviceId)s. Η συσκευή έχει επισημανθεί ως επιβεβαιωμένη.", "To link to a room it must have an address.": "Για να συνδεθείτε σε ένα δωμάτιο πρέπει να έχετε μια διεύθυνση.", - "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Η διεύθυνση ηλεκτρονικής αλληλογραφίας σας δεν φαίνεται να συσχετίζεται με Matrix ID σε αυτόν τον διακομιστή.", + "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Η διεύθυνση της ηλ. αλληλογραφίας σας δεν φαίνεται να συσχετίζεται με μια ταυτότητα Matrix σε αυτόν τον Διακομιστή Φιλοξενίας.", "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Ο κωδικός πρόσβασής σας άλλαξε επιτυχώς. Δεν θα λάβετε ειδοποιήσεις push σε άλλες συσκευές μέχρι να συνδεθείτε ξανά σε αυτές", "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "Δεν θα μπορέσετε να αναιρέσετε αυτήν την αλλαγή καθώς προωθείτε τον χρήστη να έχει το ίδιο επίπεδο δύναμης με τον εαυτό σας.", "Sent messages will be stored until your connection has returned.": "Τα απεσταλμένα μηνύματα θα αποθηκευτούν μέχρι να αακτηθεί η σύνδεσή σας.", @@ -765,7 +765,7 @@ "The platform you're on": "Η πλατφόρμα στην οποία βρίσκεστε", "The version of Riot.im": "Η έκδοση του Riot.im", "Your language of choice": "Η γλώσσα επιλογής σας", - "Your homeserver's URL": "Το URL του διακομιστή σας", + "Your homeserver's URL": "Το URL του διακομιστή φιλοξενίας σας", "Every page you use in the app": "Κάθε σελίδα που χρησιμοποιείτε στην εφαρμογή", "e.g. ": "π.χ. ", "Your device resolution": "Η ανάλυση της συσκευής σας", @@ -774,7 +774,7 @@ "Whether or not you're logged in (we don't record your user name)": "Εάν είστε συνδεδεμένος/η ή όχι (δεν καταγράφουμε το όνομα χρήστη σας)", "e.g. %(exampleValue)s": "π.χ. %(exampleValue)s", "Review Devices": "Ανασκόπηση συσκευών", - "Call Anyway": "Κλήση όπως και να 'χει", + "Call Anyway": "Πραγματοποίηση Κλήσης όπως και να 'χει", "Answer Anyway": "Απάντηση όπως και να 'χει", "Call": "Κλήση", "Answer": "Απάντηση", @@ -785,20 +785,20 @@ "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Προσοχή: κάθε άτομο που προσθέτετε στην κοινότητα θε είναι δημοσίως ορατό σε οποιονδήποτε γνωρίζει το αναγνωριστικό της κοινότητας", "Invite new community members": "Προσκαλέστε νέα μέλη στην κοινότητα", "Name or matrix ID": "Όνομα ή αναγνωριστικό του matrix", - "Invite to Community": "Πρόσκληση στην κοινότητα", + "Invite to Community": "Προσκαλέστε στην κοινότητα", "Which rooms would you like to add to this community?": "Ποια δωμάτια θα θέλατε να προσθέσετε σε αυτή την κοινότητα;", "Add rooms to the community": "Προσθήκη δωματίων στην κοινότητα", "Add to community": "Προσθήκη στην κοινότητα", - "Failed to invite the following users to %(groupId)s:": "Αποτυχία πρόσκλησης των ακόλουθων χρηστών στο %(groupId)s :", + "Failed to invite the following users to %(groupId)s:": "Αποτυχία πρόσκλησης στο %(groupId)s των χρηστών:", "Failed to invite users to community": "Αποτυχία πρόσκλησης χρηστών στην κοινότητα", "Failed to invite users to %(groupId)s": "Αποτυχία πρόσκλησης χρηστών στο %(groupId)s", - "Failed to add the following rooms to %(groupId)s:": "Αποτυχία προσθήκης των ακόλουθων δωματίων στο %(groupId)s:", + "Failed to add the following rooms to %(groupId)s:": "Αποτυχία προσθήκης στο %(groupId)s των δωματίων:", "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Υπάρχουν άγνωστες συσκευές στο δωμάτιο: εάν συνεχίσετε χωρίς να τις επιβεβαιώσετε, θα μπορούσε κάποιος να κρυφακούει την κλήση σας.", "Show these rooms to non-members on the community page and room list?": "Εμφάνιση αυτών των δωματίων σε μη-μέλη στην σελίδα της κοινότητας και στη λίστα δωματίων;", "Room name or alias": "Όνομα η ψευδώνυμο δωματίου", - "Restricted": "Περιορισμένο", - "Unable to create widget.": "Αδυναμία δημιουργίας widget.", - "Reload widget": "Ανανέωση widget", + "Restricted": "Περιορισμένο/η", + "Unable to create widget.": "Αδυναμία δημιουργίας γραφικού στοιχείου.", + "Reload widget": "Επαναφόρτωση γραφικού στοιχείου", "You are not in this room.": "Δεν είστε μέλος αυτού του δωματίου.", "You do not have permission to do that in this room.": "Δεν έχετε την άδεια να το κάνετε αυτό σε αυτό το δωμάτιο.", "You are now ignoring %(userId)s": "Τώρα αγνοείτε τον/την %(userId)s", @@ -818,14 +818,14 @@ "Delete %(count)s devices|other": "Διαγραφή %(count)s συσκευών", "Delete %(count)s devices|one": "Διαγραφή συσκευής", "Select devices": "Επιλογή συσκευών", - "Cannot add any more widgets": "Δεν είναι δυνατή η προσθήκη άλλων widget", - "The maximum permitted number of widgets have already been added to this room.": "Ο μέγιστος επιτρεπτός αριθμός widget έχει ήδη προστεθεί σε αυτό το δωμάτιο.", - "Add a widget": "Προσθήκη widget", + "Cannot add any more widgets": "Δεν είναι δυνατή η προσθήκη άλλων γραφικών στοιχείων", + "The maximum permitted number of widgets have already been added to this room.": "Ο μέγιστος επιτρεπτός αριθμός γραφικών στοιχείων έχει ήδη προστεθεί σε αυτό το δωμάτιο.", + "Add a widget": "Προσθέστε ένα γραφικό στοιχείο", "%(senderName)s sent an image": "Ο/Η %(senderName)s έστειλε μία εικόνα", "%(senderName)s sent a video": "Ο/Η %(senderName)s έστειλε ένα βίντεο", "%(senderName)s uploaded a file": "Ο/Η %(senderName)s αναφόρτωσε ένα αρχείο", "If your other devices do not have the key for this message you will not be able to decrypt them.": "Εάν οι άλλες συσκευές σας δεν έχουν το κλειδί για αυτό το μήνυμα, τότε δεν θα μπορείτε να το αποκρυπτογραφήσετε.", - "Disinvite this user?": "Ακύρωση πρόσκλησης αυτού του χρήστη;", + "Disinvite this user?": "Απόσυρση της πρόσκλησης αυτού του χρήστη;", "Mention": "Αναφορά", "Invite": "Πρόσκληση", "User Options": "Επιλογές Χρήστη", @@ -849,5 +849,64 @@ "Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Διαβάστηκε από τον/την %(displayName)s (%(userName)s) στις %(dateTime)s", "Room Notification": "Ειδοποίηση Δωματίου", "Notify the whole room": "Ειδοποιήστε όλο το δωμάτιο", - "Sets the room topic": "Ορίζει το θέμα του δωματίου" + "Sets the room topic": "Ορίζει το θέμα του δωματίου", + "Add Email Address": "Προσθήκη Διεύθυνσης Ηλ. Ταχυδρομείου", + "Add Phone Number": "Προσθήκη Τηλεφωνικού Αριθμού", + "Whether or not you're logged in (we don't record your username)": "Χωρίς να έχει σημασία εάν είστε συνδεδεμένοι (δεν καταγράφουμε το όνομα χρήστη σας)", + "Which officially provided instance you are using, if any": "Ποιά επίσημα παρεχόμενη έκδοση χρησιμοποιείτε, εάν χρησιμοποιείτε κάποια", + "Whether or not you're using the Richtext mode of the Rich Text Editor": "Χωρίς να έχει σημασία εάν χρησιμοποιείτε την λειτουργία \"Πλούσιο Κείμενο\" του Επεξεργαστή Πλουσίου Κειμένου", + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Χωρίς να έχει σημασία εάν χρησιμοποιείτε το χαρακτηριστικό 'ψίχουλα' (τα άβαταρ πάνω από την λίστα δωματίων)", + "Your User Agent": "Ο Πράκτορας Χρήστη σας", + "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Όπου αυτή η σελίδα περιέχει αναγνωρίσιμες πληροφορίες, όπως ταυτότητα δωματίου, χρήστη ή ομάδας, αυτά τα δεδομένα αφαιρούνται πριν πραγματοποιηθεί αποστολή στον διακομιστή.", + "Call failed due to misconfigured server": "Η κλήση απέτυχε λόγω της λανθασμένης διάρθρωσης του διακομιστή", + "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Παρακαλείστε να ρωτήσετε τον διαχειριστή του διακομιστή φιλοξενίας σας (%(homeserverDomain)s) να ρυθμίσουν έναν διακομιστή πρωτοκόλλου TURN ώστε οι κλήσεις να λειτουργούν απρόσκοπτα.", + "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Εναλλακτικά, δοκιμάστε να χρησιμοποιήσετε τον δημόσιο διακομιστή στο turn.matrix.org, αλλά δεν θα είναι το ίδιο απρόσκοπτο, και θα κοινοποιεί την διεύθυνση IP σας με τον διακομιστή. Μπορείτε επίσης να το διαχειριστείτε στις Ρυθμίσεις.", + "Try using turn.matrix.org": "Δοκιμάστε το turn.matrix.org", + "A conference call could not be started because the integrations server is not available": "Μια κλήση συνδιάσκεψης δεν μπορούσε να ξεκινήσει διότι ο διακομιστής ενσωμάτωσης είναι μη διαθέσιμος", + "Call in Progress": "Κλήση σε Εξέλιξη", + "A call is currently being placed!": "Μια κλήση πραγματοποιείτε τώρα!", + "A call is already in progress!": "Μια κλήση είναι σε εξέλιξη ήδη!", + "Permission Required": "Απαιτείται Άδεια", + "You do not have permission to start a conference call in this room": "Δεν έχετε άδεια για να ξεκινήσετε μια κλήση συνδιάσκεψης σε αυτό το δωμάτιο", + "Replying With Files": "Απαντώντας Με Αρχεία", + "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Αυτήν την στιγμή δεν είναι δυνατό να απαντήσετε με αρχείο. Θα θέλατε να ανεβάσετε το αρχείο χωρίς να απαντήσετε;", + "The file '%(fileName)s' failed to upload.": "Απέτυχε το ανέβασμα του αρχείου '%(fileName)s'.", + "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Το αρχείο '%(fileName)s' ξεπερνάει το όριο μεγέθους ανεβάσματος αυτού του διακομιστή φιλοξενίας", + "The server does not support the room version specified.": "Ο διακομιστής δεν υποστηρίζει την έκδοση του δωματίου που ορίστηκε.", + "Name or Matrix ID": "Όνομα ή ταυτότητα Matrix", + "Identity server has no terms of service": "Ο διακομιστής ταυτοποίησης δεν έχει όρους χρήσης", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Αυτή η δράση απαιτεί την πρόσβαση στο προκαθορισμένο διακομιστή ταυτοποίησης για να επιβεβαιώσει μια διεύθυνση ηλ. ταχυδρομείου ή αριθμό τηλεφώνου, αλλά ο διακομιστής δεν έχει όρους χρήσης.", + "Only continue if you trust the owner of the server.": "Συνεχίστε μόνο εάν εμπιστεύεστε τον ιδιοκτήτη του διακομιστή.", + "Trust": "Εμπιστοσύνη", + "Unable to load! Check your network connectivity and try again.": "Αδυναμία φόρτωσης! Ελέγξτε την σύνδεση του δικτύου και προσπαθήστε ξανά.", + "Registration Required": "Απαιτείτε Εγγραφή", + "You need to register to do this. Would you like to register now?": "Χρειάζεται να γίνει εγγραφή για αυτό. Θα θέλατε να κάνετε εγγραφή τώρα;", + "Email, name or Matrix ID": "Ηλ. ταχυδρομείο, όνομα ή ταυτότητα Matrix", + "Failed to start chat": "Αποτυχία αρχικοποίησης συνομιλίας", + "Failed to invite users to the room:": "Αποτυχία πρόσκλησης χρηστών στο δωμάτιο:", + "Missing roomId.": "Λείπει η ταυτότητα δωματίου.", + "Messages": "Μηνύματα", + "Actions": "Δράσεις", + "Other": "Άλλα", + "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Προ-εισάγει ¯\\_(ツ)_/¯ σε ένα μήνυμα απλού κειμένου", + "Sends a message as plain text, without interpreting it as markdown": "Αποστέλλει ένα μήνυμα ως απλό κείμενο, χωρίς να το ερμηνεύει ως \"markdown\"", + "Upgrades a room to a new version": "Αναβαθμίζει το δωμάτιο σε μια καινούργια έκδοση", + "You do not have the required permissions to use this command.": "Δεν διαθέτετε τις απαιτούμενες άδειες για να χρησιμοποιήσετε αυτήν την εντολή.", + "Room upgrade confirmation": "Επιβεβαίωση αναβάθμισης δωματίου", + "Upgrading a room can be destructive and isn't always necessary.": "Η αναβάθμιση ενός δωματίου μπορεί να είναι καταστροφική και δεν είναι πάντα απαραίτητη.", + "Room upgrades are usually recommended when a room version is considered unstable. Unstable room versions might have bugs, missing features, or security vulnerabilities.": "Οι αναβαθμίσεις δωματίου είναι συνήθως προτεινόμενες όταν μια έκδοση δωματίου θεωρείτε ασταθής. Ασταθείς εκδόσεις δωματίων μπορεί να έχουν σφάλματα, ελλειπή χαρακτηριστικά, ή αδυναμίες ασφαλείας.", + "Room upgrades usually only affect server-side processing of the room. If you're having problems with your Riot client, please file an issue with .": "Οι αναβαθμίσεις δωματίων συνήθως επηρεάζουν μόνο την επεξεργασία του δωματίου από την πλευρά του διακομιστή. Εάν έχετε προβλήματα με το πρόγραμμα-πελάτη Riot, παρακαλώ αρχειοθετήστε ένα πρόβλημα μέσω .", + "Warning: Upgrading a room will not automatically migrate room members to the new version of the room. We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "Προσοχή: Αναβαθμίζοντας ένα δωμάτιο δεν θα μεταφέρει αυτόματα τα μέλη του δωματίου στη νέα έκδοση του δωματίου. Θα αναρτήσουμε ένα σύνδεσμο προς το νέο δωμάτιο στη παλιά έκδοση του δωματίου - τα μέλη του δωματίου θα πρέπει να πατήσουν στον σύνδεσμο για να μπουν στο νέο δωμάτιο.", + "Please confirm that you'd like to go forward with upgrading this room from to .": "Παρακαλώ επιβεβαιώστε ότι θα θέλατε να προχωρήσετε με την αναβάθμιση του δωματίου από σε .", + "Upgrade": "Αναβάθμιση", + "Changes your display nickname in the current room only": "Αλλάζει το εμφανιζόμενο ψευδώνυμο μόνο στο παρόν δωμάτιο", + "Changes the avatar of the current room": "Αλλάζει το άβαταρ αυτού του δωματίου", + "Changes your avatar in this current room only": "Αλλάζει το άβαταρ σας μόνο στο παρόν δωμάτιο", + "Changes your avatar in all rooms": "Αλλάζει το άβαταρ σας σε όλα τα δωμάτια", + "Gets or sets the room topic": "Λαμβάνει ή θέτει το θέμα του δωματίου", + "This room has no topic.": "Το δωμάτιο αυτό δεν έχει κανένα θέμα.", + "Sets the room name": "Θέτει το θέμα του δωματίου", + "Use an identity server": "Χρησιμοποιήστε ένα διακομιστή ταυτοτήτων", + "Your Riot is misconfigured": "Οι παράμετροι του Riot σας είναι λανθασμένα ρυθμισμένοι", + "Explore rooms": "Εξερευνήστε δωμάτια" } From 84d23676d6002005c7ff5b906e383eeef1300647 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Mon, 4 Nov 2019 21:14:13 +0000 Subject: [PATCH 027/334] Translated using Weblate (Hungarian) Currently translated at 100.0% (1853 of 1853 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 4946c7b14f..5d21a17e37 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2266,5 +2266,6 @@ "Unread messages.": "Olvasatlan üzenetek.", "Show tray icon and minimize window to it on close": "Tálcaikon mutatása és az ablak összecsukása bezáráskor", "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Ez a művelet az e-mail cím vagy telefonszám ellenőrzése miatt hozzáférést igényel az alapértelmezett azonosítási szerverhez (), de a szervernek nincsen semmilyen felhasználási feltétele.", - "Trust": "Megbízom benne" + "Trust": "Megbízom benne", + "Message Actions": "Üzenet Műveletek" } From 8b3844f83b6d47053d01ffa391c51b35255c9118 Mon Sep 17 00:00:00 2001 From: shuji narazaki Date: Thu, 7 Nov 2019 23:12:55 +0000 Subject: [PATCH 028/334] Translated using Weblate (Japanese) Currently translated at 60.8% (1126 of 1853 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 | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json index 23199094e8..5e3fecaed9 100644 --- a/src/i18n/strings/ja.json +++ b/src/i18n/strings/ja.json @@ -1343,5 +1343,42 @@ "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s はこの部屋をアップグレードしました。", "%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s はこの部屋をリンクを知っている人全てに公開しました。", "%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s はこの部屋を招待者のみに変更しました。", - "%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s はゲストがこの部屋に参加できるようにしました。" + "%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s はゲストがこの部屋に参加できるようにしました。", + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "「パンくずリスト」機能(部屋リストの上のアバター)を使っているかどうか", + "Only continue if you trust the owner of the server.": "そのサーバーの所有者を信頼する場合のみ続ける。", + "Trust": "信頼", + "Use an identity server to invite by email. Manage in Settings.": "メールによる招待のためにIDサーバーを用いる。設定画面で管理する。", + "%(senderDisplayName)s changed the join rule to %(rule)s": "%(senderDisplayName)s は参加ルールを %(rule)s に変更しました", + "%(senderDisplayName)s has prevented guests from joining the room.": "%(senderDisplayName)s はゲストの部屋への参加を差し止めています。", + "%(senderDisplayName)s changed guest access to %(rule)s": "%(senderDisplayName)s はゲストのアクセスを %(rule)s に変更しました", + "%(displayName)s is typing …": "%(displayName)s が入力中 …", + "%(names)s and %(count)s others are typing …|other": "%(names)s と他 %(count)s 名が入力中 …", + "%(names)s and %(count)s others are typing …|one": "%(names)s ともう一人が入力中 …", + "%(names)s and %(lastPerson)s are typing …": "%(names)s と %(lastPerson)s が入力中 …", + "Cannot reach homeserver": "ホームサーバーに接続できません", + "Your Riot is misconfigured": "あなたのRiotは設定が間違っています", + "Cannot reach identity server": "IDサーバーに接続できません", + "No homeserver URL provided": "ホームサーバーのURLが与えられていません", + "User %(userId)s is already in the room": "ユーザー %(userId)s はすでにその部屋にいます", + "User %(user_id)s does not exist": "ユーザー %(user_id)s は存在しません", + "The user's homeserver does not support the version of the room.": "そのユーザーのホームサーバーはその部屋のバージョンに対応していません。", + "Use a few words, avoid common phrases": "ありふれた語句を避けて、いくつかの単語を使ってください", + "This is a top-10 common password": "これがよく使われるパスワードの上位10個です", + "This is a top-100 common password": "これがよく使われるパスワードの上位100個です", + "This is a very common password": "これはとてもよく使われるパスワードです", + "This is similar to a commonly used password": "これはよく使われるパスワードに似ています", + "A word by itself is easy to guess": "単語一つだけだと簡単に特定されます", + "Custom user status messages": "ユーザーステータスのメッセージをカスタマイズする", + "Render simple counters in room header": "部屋のヘッダーに簡単なカウンターを表示する", + "Use the new, faster, composer for writing messages": "メッセージの編集に新しい高速なコンポーザーを使う", + "Enable Emoji suggestions while typing": "入力中の絵文字提案機能を有効にする", + "Show avatar changes": "アバターの変更を表示する", + "Show display name changes": "表示名の変更を表示する", + "Show read receipts sent by other users": "他の人の既読情報を表示する", + "Enable big emoji in chat": "チャットで大きな絵文字を有効にする", + "Send typing notifications": "入力中であることを通知する", + "Enable Community Filter Panel": "コミュニティーフィルターパネルを有効にする", + "Show recently visited rooms above the room list": "最近訪問した部屋をリストの上位に表示する", + "Low bandwidth mode": "低帯域通信モード", + "Trust & Devices": "信頼と端末" } From defe3fb5f81e2d34d4c91fcd43e820e30c2b2f3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=ED=83=9C=EC=84=AD?= Date: Mon, 4 Nov 2019 15:39:35 +0000 Subject: [PATCH 029/334] Translated using Weblate (Korean) Currently translated at 100.0% (1853 of 1853 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ko/ --- src/i18n/strings/ko.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index 1aebd0ce17..54425657cf 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -2123,5 +2123,6 @@ "Unread messages.": "읽지 않은 메시지.", "Show tray icon and minimize window to it on close": "닫을 때 창을 최소화하고 트레이 아이콘으로 표시하기", "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "이 작업에는 이메일 주소 또는 전화번호를 확인하기 위해 기본 ID 서버 에 접근해야 합니다. 하지만 서버가 서비스 약관을 갖고 있지 않습니다.", - "Trust": "신뢰함" + "Trust": "신뢰함", + "Message Actions": "메시지 동작" } From 824a26549644de047d6dd75d46b9bde0babd47a6 Mon Sep 17 00:00:00 2001 From: MamasLT Date: Mon, 4 Nov 2019 22:55:04 +0000 Subject: [PATCH 030/334] Translated using Weblate (Lithuanian) Currently translated at 50.4% (933 of 1853 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/lt/ --- src/i18n/strings/lt.json | 271 ++++++++++++++++++++++++++++----------- 1 file changed, 194 insertions(+), 77 deletions(-) diff --git a/src/i18n/strings/lt.json b/src/i18n/strings/lt.json index 6a8076ac96..2aeb207387 100644 --- a/src/i18n/strings/lt.json +++ b/src/i18n/strings/lt.json @@ -1,17 +1,17 @@ { "This email address is already in use": "Šis el. pašto adresas jau naudojamas", "This phone number is already in use": "Šis telefono numeris jau naudojamas", - "Failed to verify email address: make sure you clicked the link in the email": "Nepavyko patvirtinti el. pašto adreso: įsitikinkite, kad gautame el. laiške spustelėjote nuorodą", + "Failed to verify email address: make sure you clicked the link in the email": "Nepavyko patvirtinti el. pašto adreso: įsitikinkite, kad paspaudėte nuorodą el. laiške", "The platform you're on": "Jūsų naudojama platforma", "The version of Riot.im": "Riot.im versija", "Whether or not you're logged in (we don't record your user name)": "Nesvarbu ar esate prisijungę ar ne (mes neįrašome jūsų naudotojo vardo)", "Your language of choice": "Jūsų pasirinkta kalba", - "Which officially provided instance you are using, if any": "Kurį oficialiai pateiktą egzempliorių naudojate", - "Whether or not you're using the Richtext mode of the Rich Text Editor": "Ar jūs naudojate Raiškiojo Teksto Redaktoriaus Raiškiojo Teksto režimą ar ne", - "Your homeserver's URL": "Jūsų serverio URL adresas", - "Your identity server's URL": "Jūsų identifikavimo serverio URL adresas", + "Which officially provided instance you are using, if any": "Kurią oficialiai teikiamą instanciją naudojate, jei tokių yra", + "Whether or not you're using the Richtext mode of the Rich Text Editor": "Nepriklausomai nuo to ar jūs naudojate Raiškiojo Teksto Redaktoriaus Raiškiojo Teksto režimą", + "Your homeserver's URL": "Jūsų serverio URL", + "Your identity server's URL": "Jūsų tapatybės serverio URL", "Analytics": "Statistika", - "The information being sent to us to help make Riot.im better includes:": "Informacijoje, kuri yra siunčiama Riot.im tobulinimui yra:", + "The information being sent to us to help make Riot.im better includes:": "Informacija, siunčiama mums, kad padėtų tobulinti Riot.im, apima:", "Fetching third party location failed": "Nepavyko gauti trečios šalies vietos", "A new version of Riot is available.": "Yra prieinama nauja Riot versija.", "I understand the risks and wish to continue": "Aš suprantu riziką ir noriu tęsti", @@ -196,7 +196,7 @@ "Answer Anyway": "Vis tiek atsiliepti", "Call": "Skambinti", "Answer": "Atsiliepti", - "Unable to capture screen": "Nepavyko nufotografuoti ekraną", + "Unable to capture screen": "Nepavyko nufotografuoti ekrano", "You are already in a call.": "Jūs jau dalyvaujate skambutyje.", "VoIP is unsupported": "VoIP yra nepalaikoma", "Could not connect to the integration server": "Nepavyko prisijungti prie integracijos serverio", @@ -210,18 +210,18 @@ "Thu": "Ket", "Fri": "Pen", "Sat": "Šeš", - "Jan": "Sau", + "Jan": "Sausis", "Feb": "Vas", - "Mar": "Kov", + "Mar": "Kovas", "Apr": "Bal", "May": "Geg", - "Jun": "Bir", - "Jul": "Lie", - "Aug": "Rgp", - "Sep": "Rgs", - "Oct": "Spa", - "Nov": "Lap", - "Dec": "Gru", + "Jun": "Birž", + "Jul": "Liepa", + "Aug": "Rugpj", + "Sep": "Rugs", + "Oct": "Spalis", + "Nov": "Lapkr", + "Dec": "Gruodis", "PM": "PM", "AM": "AM", "%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s", @@ -229,16 +229,16 @@ "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(fullYear)s %(monthName)s %(day)s", "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(fullYear)s %(monthName)s %(day)s %(time)s", "Who would you like to add to this community?": "Ką norėtumėte pridėti į šią bendruomenę?", - "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Įspėjimas: bet kuris pridėtas asmuo bus matomas visiems, žinantiems bendruomenės ID", + "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Įspėjimas: bet kuris jūsų pridėtas asmuo bus viešai matomas visiems, žinantiems bendruomenės ID", "Name or matrix ID": "Vardas ar matrix ID", "Invite to Community": "Pakviesti į bendruomenę", "Which rooms would you like to add to this community?": "Kuriuos kambarius norėtumėte pridėti į šią bendruomenę?", "Add rooms to the community": "Pridėti kambarius į bendruomenę", "Add to community": "Pridėti į bendruomenę", - "Failed to invite the following users to %(groupId)s:": "Nepavyko pakviesti šių naudotojų į %(groupId)s:", - "Failed to invite users to community": "Nepavyko pakviesti naudotojus į bendruomenę", - "Failed to invite users to %(groupId)s": "Nepavyko pakviesti naudotojų į %(groupId)s", - "Failed to add the following rooms to %(groupId)s:": "Nepavyko pridėti šiuos kambarius į %(groupId)s:", + "Failed to invite the following users to %(groupId)s:": "Nepavyko pakviesti šių vartotojų į %(groupId)s:", + "Failed to invite users to community": "Nepavyko pakviesti vartotojų į bendruomenę", + "Failed to invite users to %(groupId)s": "Nepavyko pakviesti vartotojų į %(groupId)s", + "Failed to add the following rooms to %(groupId)s:": "Nepavyko pridėti šių kambarių į %(groupId)s:", "Riot does not have permission to send you notifications - please check your browser settings": "Riot neturi leidimo siųsti jums pranešimus - patikrinkite savo naršyklės nustatymus", "Riot was not given permission to send notifications - please try again": "Riot nebuvo suteiktas leidimas siųsti pranešimus - bandykite dar kartą", "Unable to enable Notifications": "Nepavyko įjungti Pranešimus", @@ -251,7 +251,7 @@ "Send Invites": "Siųsti pakvietimus", "Failed to invite user": "Nepavyko pakviesti naudotojo", "Failed to invite": "Nepavyko pakviesti", - "Failed to invite the following users to the %(roomName)s room:": "Nepavyko pakviesti šių naudotojų į kambarį %(roomName)s :", + "Failed to invite the following users to the %(roomName)s room:": "Nepavyko pakviesti šių vartotojų į kambarį %(roomName)s:", "You need to be logged in.": "Turite būti prisijungę.", "Unable to create widget.": "Nepavyko sukurti valdiklio.", "Failed to send request.": "Nepavyko išsiųsti užklausos.", @@ -263,8 +263,8 @@ "Changes your display nickname": "Pakeičia jūsų rodomą slapyvardį", "Sets the room topic": "Nustato kambario temą", "Invites user with given id to current room": "Pakviečia naudotoją su nurodytu id į esamą kambarį", - "You are now ignoring %(userId)s": "Dabar nepaisote %(userId)s", - "Opens the Developer Tools dialog": "Atveria kūrėjo įrankių dialogą", + "You are now ignoring %(userId)s": "Dabar ignoruojate %(userId)s", + "Opens the Developer Tools dialog": "Atveria programuotojo įrankių dialogą", "Unknown (user, device) pair:": "Nežinoma pora (naudotojas, įrenginys):", "Device already verified!": "Įrenginys jau patvirtintas!", "WARNING: Device already verified, but keys do NOT MATCH!": "ĮSPĖJIMAS: Įrenginys jau patvirtintas, tačiau raktai NESUTAMPA!", @@ -273,26 +273,26 @@ "Unrecognised command:": "Neatpažinta komanda:", "Reason": "Priežastis", "%(targetName)s accepted an invitation.": "%(targetName)s priėmė pakvietimą.", - "%(senderName)s invited %(targetName)s.": "%(senderName)s pakvietė naudotoją %(targetName)s.", - "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s pasikeitė savo rodomą vardą į %(displayName)s.", - "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s nusistatė savo rodomą vardą į %(displayName)s.", - "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s pašalino savo rodomą vardą (%(oldDisplayName)s).", + "%(senderName)s invited %(targetName)s.": "%(senderName)s pakvietė %(targetName)s.", + "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s pakeitė savo vardą į %(displayName)s.", + "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s nustatė savo vardą į %(displayName)s.", + "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s pašalino savo vardą (%(oldDisplayName)s).", "%(senderName)s removed their profile picture.": "%(senderName)s pašalino savo profilio paveikslą.", - "%(senderName)s changed their profile picture.": "%(senderName)s pasikeitė savo profilio paveikslą.", - "%(senderName)s set a profile picture.": "%(senderName)s nusistatė profilio paveikslą.", + "%(senderName)s changed their profile picture.": "%(senderName)s pakeitė savo profilio paveikslą.", + "%(senderName)s set a profile picture.": "%(senderName)s nustatė profilio paveikslą.", "%(targetName)s rejected the invitation.": "%(targetName)s atmetė pakvietimą.", "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s pakeitė temą į \"%(topic)s\".", "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s pakeitė kambario pavadinimą į %(roomName)s.", "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s išsiuntė paveikslą.", "Someone": "Kažkas", "%(senderName)s answered the call.": "%(senderName)s atsiliepė į skambutį.", - "(unknown failure: %(reason)s)": "(nežinoma lemtingoji klaida: %(reason)s)", + "(unknown failure: %(reason)s)": "(nežinoma klaida: %(reason)s)", "%(senderName)s ended the call.": "%(senderName)s užbaigė skambutį.", "%(displayName)s is typing": "%(displayName)s rašo", "%(names)s and %(count)s others are typing|other": "%(names)s ir dar kiti %(count)s rašo", "%(names)s and %(lastPerson)s are typing": "%(names)s ir %(lastPerson)s rašo", "Send anyway": "Vis tiek siųsti", - "Unnamed Room": "Kambarys be pavadinimo", + "Unnamed Room": "Bevardis kambarys", "Hide removed messages": "Slėpti pašalintas žinutes", "Hide display name changes": "Slėpti rodomo vardo pakeitimus", "Show timestamps in 12 hour format (e.g. 2:30pm)": "Rodyti laiko žymas 12 valandų formatu (pvz., 2:30pm)", @@ -372,7 +372,7 @@ "Settings": "Nustatymai", "Show panel": "Rodyti skydelį", "Press to start a chat with someone": "Norėdami pradėti su kuo nors pokalbį, paspauskite ", - "Community Invites": "", + "Community Invites": "Bendruomenės pakvietimai", "People": "Žmonės", "Reason: %(reasonText)s": "Priežastis: %(reasonText)s", "%(roomName)s does not exist.": "%(roomName)s nėra.", @@ -576,27 +576,27 @@ "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "Eksportavimo failas bus apsaugotas slaptafraze. Norėdami iššifruoti failą, čia turėtumėte įvesti slaptafrazę.", "File to import": "Failas, kurį importuoti", "Import": "Importuoti", - "Your User Agent": "Jūsų naudotojo agentas", + "Your User Agent": "Jūsų vartotojo agentas", "Review Devices": "Peržiūrėti įrenginius", "You do not have permission to start a conference call in this room": "Jūs neturite leidimo šiame kambaryje pradėti konferencinį pokalbį", "The file '%(fileName)s' exceeds this home server's size limit for uploads": "Failas \"%(fileName)s\" viršija šio namų serverio įkeliamų failų dydžio apribojimą", "Room name or alias": "Kambario pavadinimas ar slapyvardis", - "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Neatrodo, kad jūsų el. pašto adresas šiame namų serveryje būtų susietas su Matrix ID.", + "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Neatrodo, kad jūsų el. pašto adresas šiame serveryje būtų susietas su Matrix ID.", "Who would you like to communicate with?": "Su kuo norėtumėte susisiekti?", "Missing room_id in request": "Užklausoje trūksta room_id", "Missing user_id in request": "Užklausoje trūksta user_id", "Unrecognised room alias:": "Neatpažintas kambario slapyvardis:", - "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "ĮSPĖJIMAS: RAKTO PATVIRTINIMAS NEPAVYKO! Pasirašymo raktas, skirtas %(userId)s ir įrenginiui %(deviceId)s yra \"%(fprint)s\", o tai nesutampa su pateiktu raktu \"%(fingerprint)s\". Tai gali reikšti, kad kažkas perima jūsų komunikavimą!", - "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": "Jūsų pateiktas pasirašymo raktas sutampa su pasirašymo raktus, kuris gautas iš naudotojo %(userId)s įrenginio %(deviceId)s. Įrenginys pažymėtas kaip patvirtintas.", + "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "ĮSPĖJIMAS: RAKTO PATVIRTINIMAS NEPAVYKO! Pasirašymo raktas, skirtas %(userId)s ir įrenginiui %(deviceId)s yra \"%(fprint)s\", o tai nesutampa su pateiktu raktu \"%(fingerprint)s\". Tai gali reikšti, kad jūsų komunikacijos yra perimamos!", + "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": "Jūsų pateiktas pasirašymo raktas sutampa su pasirašymo raktu, kurį gavote iš vartotojo %(userId)s įrenginio %(deviceId)s. Įrenginys pažymėtas kaip patvirtintas.", "VoIP conference started.": "VoIP konferencija pradėta.", "VoIP conference finished.": "VoIP konferencija užbaigta.", "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s pašalino kambario pavadinimą.", - "%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).": "%(senderName)s įjungė ištisinį šifravimą (%(algorithm)s algoritmas).", + "%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).": "%(senderName)s įjungė end-to-end šifravimą (%(algorithm)s algoritmas).", "%(widgetName)s widget modified by %(senderName)s": "%(senderName)s modifikavo %(widgetName)s valdiklį", "%(widgetName)s widget added by %(senderName)s": "%(senderName)s pridėjo %(widgetName)s valdiklį", "%(widgetName)s widget removed by %(senderName)s": "%(senderName)s pašalino %(widgetName)s valdiklį", "Failure to create room": "Nepavyko sukurti kambarį", - "Server may be unavailable, overloaded, or you hit a bug.": "Gali būti, kad serveris neprieinamas, perkrautas arba susidūrėte su klaida.", + "Server may be unavailable, overloaded, or you hit a bug.": "Serveris gali būti neprieinamas, per daug apkrautas, arba susidūrėte su klaida.", "Use compact timeline layout": "Naudoti kompaktišką laiko juostos išdėstymą", "Autoplay GIFs and videos": "Automatiškai atkurti GIF ir vaizdo įrašus", "Never send encrypted messages to unverified devices from this device": "Niekada nesiųsti iš šio įrenginio šifruotų žinučių į nepatvirtintus įrenginius", @@ -624,30 +624,30 @@ "Invited": "Pakviestas", "Filter room members": "Filtruoti kambario dalyvius", "Server unavailable, overloaded, or something else went wrong.": "Serveris neprieinamas, perkrautas arba nutiko kažkas kito.", - "%(duration)ss": "%(duration)s sek.", - "%(duration)sm": "%(duration)s min.", - "%(duration)sh": "%(duration)s val.", - "%(duration)sd": "%(duration)s d.", + "%(duration)ss": "%(duration)s sek", + "%(duration)sm": "%(duration)s min", + "%(duration)sh": "%(duration)s val", + "%(duration)sd": "%(duration)s d", "Seen by %(userName)s at %(dateTime)s": "%(userName)s matė ties %(dateTime)s", "Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "%(displayName)s (%(userName)s) matė ties %(dateTime)s", - "Show these rooms to non-members on the community page and room list?": "Ar rodyti šiuos kambarius ne dalyviams bendruomenės puslapyje ir kambarių sąraše?", + "Show these rooms to non-members on the community page and room list?": "Rodyti šiuos kambarius ne nariams bendruomenės puslapyje ir kambarių sąraše?", "Invite new room members": "Pakviesti naujus kambario dalyvius", "Changes colour scheme of current room": "Pakeičia esamo kambario spalvų rinkinį", - "Kicks user with given id": "Išmeta naudotoją su nurodytu id", - "Bans user with given id": "Užblokuoja naudotoja su nurodytu id", + "Kicks user with given id": "Išmeta vartotoją su nurodytu id", + "Bans user with given id": "Užblokuoja vartotoją su nurodytu id", "Unbans user with given id": "Atblokuoja naudotoją su nurodytu id", - "%(senderName)s banned %(targetName)s.": "%(senderName)s užblokavo naudotoją %(targetName)s.", - "%(senderName)s unbanned %(targetName)s.": "%(senderName)s atblokavo naudotoją %(targetName)s.", - "%(senderName)s kicked %(targetName)s.": "%(senderName)s išmetė naudotoją %(targetName)s.", + "%(senderName)s banned %(targetName)s.": "%(senderName)s užblokavo %(targetName)s.", + "%(senderName)s unbanned %(targetName)s.": "%(senderName)s atblokavo %(targetName)s.", + "%(senderName)s kicked %(targetName)s.": "%(senderName)s išmetė %(targetName)s.", "(not supported by this browser)": "(nėra palaikoma šios naršyklės)", "(no answer)": "(nėra atsakymo)", - "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s padarė kambario ateities istoriją matomą visiems kambario dalyviams nuo to laiko, kai jie buvo pakviesti.", - "%(senderName)s made future room history visible to all room members.": "%(senderName)s padarė kambario ateities istoriją matomą visiems kambario dalyviams.", - "%(senderName)s made future room history visible to anyone.": "%(senderName)s padarė kambario ateities istoriją matomą bet kam.", + "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s padarė būsimą kambario istoriją matomą visiems kambario dalyviams, nuo pat jų pakvietimo.", + "%(senderName)s made future room history visible to all room members.": "%(senderName)s padarė būsimą kambario istoriją matomą visiems kambario dalyviams.", + "%(senderName)s made future room history visible to anyone.": "%(senderName)s padarė būsimą kambario istoriją matomą bet kam.", "%(names)s and %(count)s others are typing|one": "%(names)s ir dar vienas naudotojas rašo", "Your browser does not support the required cryptography extensions": "Jūsų naršyklė nepalaiko reikalingų kriptografijos plėtinių", "Not a valid Riot keyfile": "Negaliojantis Riot rakto failas", - "Authentication check failed: incorrect password?": "Tapatybės nustatymo patikrinimas patyrė nesėkmę: neteisingas slaptažodis?", + "Authentication check failed: incorrect password?": "Autentifikavimo patikra nepavyko: neteisingas slaptažodis?", "Send analytics data": "Siųsti analitinius duomenis", "Incoming voice call from %(name)s": "Gaunamasis balso skambutis nuo %(name)s", "Incoming video call from %(name)s": "Gaunamasis vaizdo skambutis nuo %(name)s", @@ -672,14 +672,14 @@ "Failed to set avatar.": "Nepavyko nustatyti avataro.", "Forget room": "Pamiršti kambarį", "Share room": "Bendrinti kambarį", - "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Šiame kambaryje yra nepatvirtintų įrenginių: jeigu tęsite jų nepatvirtinę, tuomet kas nors galės slapta klausytis jūsų skambučio.", + "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Šiame kambaryje yra nežinomų įrenginių: jei tęsite jų nepatvirtinę, kam nors bus įmanoma slapta klausytis jūsų skambučio.", "Usage": "Naudojimas", "Searches DuckDuckGo for results": "Atlieka rezultatų paiešką sistemoje DuckDuckGo", - "To use it, just wait for autocomplete results to load and tab through them.": "Norėdami tai naudoti, tiesiog, palaukite, kol bus įkelti automatiškai užbaigti rezultatai, o tuomet, pereikite per juos naudodami Tab klavišą.", + "To use it, just wait for autocomplete results to load and tab through them.": "Norėdami tai naudoti, tiesiog palaukite, kol bus įkelti automatiškai užbaigti rezultatai, tuomet pereikite per juos naudodami Tab klavišą.", "%(targetName)s left the room.": "%(targetName)s išėjo iš kambario.", "%(senderName)s changed the pinned messages for the room.": "%(senderName)s pakeitė prisegtas kambario žinutes.", - "Sorry, your homeserver is too old to participate in this room.": "Atleiskite, jūsų namų serveris yra per senas dalyvauti šiame kambaryje.", - "Please contact your homeserver administrator.": "Prašome susisiekti su savo namų serverio administratoriumi.", + "Sorry, your homeserver is too old to participate in this room.": "Atleiskite, jūsų serverio versija yra per sena dalyvauti šiame kambaryje.", + "Please contact your homeserver administrator.": "Prašome susisiekti su savo serverio administratoriumi.", "Enable inline URL previews by default": "Įjungti tiesiogines URL nuorodų peržiūras pagal numatymą", "Enable URL previews for this room (only affects you)": "Įjungti URL nuorodų peržiūras šiame kambaryje (įtakoja tik jus)", "Enable URL previews by default for participants in this room": "Įjungti URL nuorodų peržiūras pagal numatymą dalyviams šiame kambaryje", @@ -731,7 +731,7 @@ "If you don't specify an email address, you won't be able to reset your password. Are you sure?": "Jeigu nenurodysite savo el. pašto adreso, negalėsite atstatyti savo slaptažodį. Ar esate tikri?", "Home server URL": "Namų serverio URL", "Identity server URL": "Tapatybės serverio URL", - "Please contact your service administrator to continue using the service.": "Norėdami tęsti naudotis paslauga, susisiekite su savo paslaugos administratoriumi.", + "Please contact your service administrator to continue using the service.": "Norėdami toliau naudotis šia paslauga, susisiekite su savo paslaugos administratoriumi.", "Reload widget": "Įkelti valdiklį iš naujo", "Picture": "Paveikslas", "Create new room": "Sukurti naują kambarį", @@ -787,12 +787,12 @@ "You cannot place a call with yourself.": "Negalite skambinti patys sau.", "Registration Required": "Reikalinga registracija", "You need to register to do this. Would you like to register now?": "Norėdami tai atlikti, turite užsiregistruoti. Ar norėtumėte užsiregistruoti dabar?", - "Missing roomId.": "Trūksta kambario ID (roomId).", + "Missing roomId.": "Trūksta kambario ID.", "Leave room": "Išeiti iš kambario", "(could not connect media)": "(nepavyko prijungti medijos)", - "This homeserver has hit its Monthly Active User limit.": "Šis namų serveris pasiekė savo mėnesinį aktyvių naudotojų limitą.", - "This homeserver has exceeded one of its resource limits.": "Šis namų serveris viršijo vieno iš savo išteklių limitą.", - "Unable to connect to Homeserver. Retrying...": "Nepavyksta prisijungti prie namų serverio. Bandoma iš naujo...", + "This homeserver has hit its Monthly Active User limit.": "Šis serveris pasiekė savo mėnesinį aktyvių naudotojų limitą.", + "This homeserver has exceeded one of its resource limits.": "Šis serveris viršijo vieno iš savo išteklių limitą.", + "Unable to connect to Homeserver. Retrying...": "Nepavyksta prisijungti prie serverio. Bandoma iš naujo...", "Hide avatar changes": "Slėpti avatarų pasikeitimus", "Disable Community Filter Panel": "Išjungti bendruomenės filtro skydelį", "Enable widget screenshots on supported widgets": "Palaikomuose valdikliuose įjungti valdiklių ekrano kopijas", @@ -854,12 +854,12 @@ "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)s pasikeitė avatarą %(count)s kartų(-us)", "And %(count)s more...|other": "Ir dar %(count)s...", "Existing Call": "Esamas skambutis", - "A call is already in progress!": "Skambutis jau yra inicijuojamas!", + "A call is already in progress!": "Skambutis jau vyksta!", "Default": "Numatytasis", "Restricted": "Apribotas", "Moderator": "Moderatorius", - "Ignores a user, hiding their messages from you": "Nepaiso naudotojo, paslepiant nuo jūsų jo žinutes", - "Stops ignoring a user, showing their messages going forward": "Sustabdo naudotojo nepaisymą, rodant jo tolimesnes žinutes", + "Ignores a user, hiding their messages from you": "Ignoruoja vartotoją, slepiant nuo jūsų jo žinutes", + "Stops ignoring a user, showing their messages going forward": "Sustabdo vartotojo ignoravimą, rodant jums jo tolimesnes žinutes", "Hide avatars in user and room mentions": "Slėpti avatarus naudotojų ir kambarių paminėjimuose", "Revoke Moderator": "Panaikinti moderatorių", "deleted": "perbrauktas", @@ -871,13 +871,13 @@ "Invites": "Pakvietimai", "You have no historical rooms": "Jūs neturite istorinių kambarių", "Historical": "Istoriniai", - "Every page you use in the app": "Kiekvienas puslapis, kurį naudoji programoje", + "Every page you use in the app": "Kiekvienas puslapis, kurį jūs naudojate programoje", "Call Timeout": "Skambučio laikas baigėsi", - "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|other": "%(senderName)s kaip šio kambario adresus pridėjo %(addedAddresses)s.", - "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|one": "%(senderName)s kaip šio kambario adresą pridėjo %(addedAddresses)s.", - "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|other": "%(senderName)s kaip šio kambario adresus pašalino %(removedAddresses)s.", - "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|one": "%(senderName)s kaip šio kambario adresą pašalino %(removedAddresses)s.", - "%(senderName)s added %(addedAddresses)s and removed %(removedAddresses)s as addresses for this room.": "%(senderName)s kaip šio kambario adresus pridėjo %(addedAddresses)s ir pašalino %(removedAddresses)s.", + "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|other": "%(senderName)s pridėjo %(addedAddresses)s, kaip šio kambario adresus.", + "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|one": "%(senderName)s pridėjo %(addedAddresses)s, kaip šio kambario adresą.", + "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|other": "%(senderName)s pašalino %(removedAddresses)s, kaip šio kambario adresus.", + "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|one": "%(senderName)s pašalino %(removedAddresses)s, kaip šio kambario adresą.", + "%(senderName)s added %(addedAddresses)s and removed %(removedAddresses)s as addresses for this room.": "%(senderName)s pridėjo %(addedAddresses)s ir pašalino %(removedAddresses)s, kaip šio kambario adresus.", "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s nustatė pagrindinį šio kambario adresą į %(address)s.", "%(senderName)s removed the main address for this room.": "%(senderName)s pašalino pagrindinį šio kambario adresą.", "Disinvite": "Atšaukti pakvietimą", @@ -885,8 +885,8 @@ "Unknown for %(duration)s": "Nežinoma jau %(duration)s", "(warning: cannot be disabled again!)": "(įspėjimas: nebeįmanoma bus išjungti!)", "Unable to load! Check your network connectivity and try again.": "Nepavyko įkelti! Patikrinkite savo tinklo ryšį ir bandykite dar kartą.", - "%(targetName)s joined the room.": "%(targetName)s atėjo į kambarį.", - "User %(user_id)s does not exist": "Naudotojo %(user_id)s nėra", + "%(targetName)s joined the room.": "%(targetName)s prisijungė prie kambario.", + "User %(user_id)s does not exist": "Vartotojas %(user_id)s neegzistuoja", "Unknown server error": "Nežinoma serverio klaida", "Avoid sequences": "Venkite sekų", "Avoid recent years": "Venkite paskiausių metų", @@ -896,7 +896,7 @@ "All-uppercase is almost as easy to guess as all-lowercase": "Visas didžiąsias raides taip pat lengva atspėti kaip ir visas mažąsias", "Reversed words aren't much harder to guess": "Žodžius atvirkštine tvarka nėra sunkiau atspėti", "Predictable substitutions like '@' instead of 'a' don't help very much": "Nuspėjami pakaitalai, tokie kaip \"@\" vietoj \"a\", nelabai padeda", - "Add another word or two. Uncommon words are better.": "Pridėkite dar vieną žodį ar du. Geriau nedažnai vartojamus žodžius.", + "Add another word or two. Uncommon words are better.": "Pridėkite dar vieną ar du žodžius. Geriau nedažnai vartojamus žodžius.", "Repeats like \"aaa\" are easy to guess": "Tokius pasikartojimus kaip \"aaa\" yra lengva atspėti", "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "Tokius pasikartojimus kaip \"abcabcabc\" yra tik truputėlį sunkiau atspėti nei \"abc\"", "Sequences like abc or 6543 are easy to guess": "Tokias sekas kaip \"abc\" ar \"6543\" yra lengva atspėti", @@ -906,13 +906,13 @@ "This is a top-100 common password": "Tai yra vienas iš 100 dažniausiai naudojamų slaptažodžių", "This is a very common password": "Tai yra labai dažnai naudojamas slaptažodis", "This is similar to a commonly used password": "Šis yra panašus į dažnai naudojamą slaptažodį", - "A word by itself is easy to guess": "Patį žodį savaime yra lengva atspėti", + "A word by itself is easy to guess": "Pats žodis yra lengvai atspėjamas", "Names and surnames by themselves are easy to guess": "Pačius vardus ar pavardes yra lengva atspėti", "Common names and surnames are easy to guess": "Dažnai naudojamus vardus ar pavardes yra lengva atspėti", "Straight rows of keys are easy to guess": "Klavišų eilę yra lengva atspėti", "Short keyboard patterns are easy to guess": "Trumpus klaviatūros šablonus yra lengva atspėti", "Avoid repeated words and characters": "Venkite pasikartojančių žodžių ir simbolių", - "Use a few words, avoid common phrases": "Naudokite kelis žodžius, venkite dažnai naudojamų frazių", + "Use a few words, avoid common phrases": "Naudokite keletą žodžių, venkite dažnai naudojamų frazių", "No need for symbols, digits, or uppercase letters": "Nereikia simbolių, skaitmenų ar didžiųjų raidžių", "Encrypted messages in group chats": "Šifruotos žinutės grupės pokalbiuose", "Delete Backup": "Ištrinti atsarginę kopiją", @@ -1000,5 +1000,122 @@ "Explore rooms": "Peržiūrėti kambarius", "Your Riot is misconfigured": "Jūsų Riot yra neteisingai sukonfigūruotas", "Sign in to your Matrix account on %(serverName)s": "Prisijunkite prie savo paskyros %(serverName)s serveryje", - "Sign in to your Matrix account on ": "Prisijunkite prie savo paskyros serveryje" + "Sign in to your Matrix account on ": "Prisijunkite prie savo paskyros serveryje", + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Nepriklausomai nuo to ar jūs naudojate 'duonos trupinių' funkciją (avatarai virš kambarių sąrašo)", + "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Kur šis puslapis įtraukia identifikuojamą informaciją, kaip kambarys, vartotojas ar grupės ID, tie duomenys yra pašalinami prieš siunčiant į serverį.", + "The remote side failed to pick up": "Nuotolinėi pusėi nepavyko atsiliepti", + "Call failed due to misconfigured server": "Skambutis nepavyko dėl neteisingai sukonfigūruoto serverio", + "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Paprašykite savo serverio administratoriaus (%(homeserverDomain)s) sukonfiguruoti TURN serverį, kad skambučiai veiktų patikimai.", + "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternatyviai, jūs galite bandyti naudoti viešą serverį turn.matrix.org, bet tai nebus taip patikima, ir tai atskleis jūsų IP adresą šiam serveriui. Jūs taip pat galite tvarkyti tai Nustatymuose.", + "Try using turn.matrix.org": "Bandykite naudoti turn.matrix.org", + "A conference call could not be started because the integrations server is not available": "Konferencinio skambučio nebuvo galima pradėti, nes nėra integracijų serverio", + "Call in Progress": "Vykstantis skambutis", + "A call is currently being placed!": "Šiuo metu skambinama!", + "Replying With Files": "Atsakyti su failais", + "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Šiuo metu neįmanoma atsakyti su failu. Ar norite įkelti šį failą neatsakydami?", + "The file '%(fileName)s' failed to upload.": "Failo '%(fileName)s' nepavyko įkelti.", + "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Failas '%(fileName)s' viršyja šio serverio įkeliamų failų dydžio limitą", + "The server does not support the room version specified.": "Serveris nepalaiko nurodytos kambario versijos.", + "Invite new community members": "Pakviesti naujus bendruomenės narius", + "Name or Matrix ID": "Vardas arba Matrix ID", + "Identity server has no terms of service": "Tapatybės serveris neturi paslaugų teikimo sąlygų", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Šiam veiksmui reikalinga prieiti numatytąjį tapatybės serverį , kad patvirtinti el. pašto adresą arba telefono numerį, bet serveris neturi jokių paslaugos teikimo sąlygų.", + "Only continue if you trust the owner of the server.": "Tęskite tik tada, jei pasitikite serverio savininku.", + "Trust": "Pasitikėti", + "Email, name or Matrix ID": "El. paštas, vardas arba Matrix ID", + "Failed to start chat": "Nepavyko pradėti pokalbio", + "Failed to invite users to the room:": "Nepavyko pakviesti vartotojų į kambarį:", + "You need to be able to invite users to do that.": "Norėdami tai atlikti jūs turite turėti galimybę pakviesti vartotojus.", + "Power level must be positive integer.": "Galios lygis privalo būti teigiamas sveikasis skaičius.", + "Messages": "Žinutės", + "Actions": "Veiksmai", + "Other": "Kitas", + "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Prideda ¯\\_(ツ)_/¯ prie paprasto teksto pranešimo", + "Sends a message as plain text, without interpreting it as markdown": "SIunčia žinutę, kaip paprastą tekstą, jo neinterpretuodamas kaip pažymėto", + "Upgrades a room to a new version": "Atnaujina kambarį į naują versiją", + "You do not have the required permissions to use this command.": "Jūs neturite reikalingų leidimų naudoti šią komandą.", + "Room upgrade confirmation": "Kambario atnaujinimo patvirtinimas", + "Upgrading a room can be destructive and isn't always necessary.": "Kambario atnaujinimas gali būti destruktyvus ir nėra visada reikalingas.", + "Room upgrades are usually recommended when a room version is considered unstable. Unstable room versions might have bugs, missing features, or security vulnerabilities.": "Kambario atnaujinimai paprastai rekomenduojami, kada kambario versija yra laikoma nestabili. Nestabilios kambario versijos gali turėti klaidų, trūkstamų funkcijų, arba saugumo spragų.", + "Room upgrades usually only affect server-side processing of the room. If you're having problems with your Riot client, please file an issue with .": "Kambario atnaujinimai paprastai paveikia tik serverio pusės kambario apdorojimą. Jei jūs turite problemų su jūsų Riot klientu, prašome užregistruoti problemą .", + "Warning: Upgrading a room will not automatically migrate room members to the new version of the room. We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "Įspėjimas: Kambario atnaujinimas automatiškai nemigruos kambario dalyvių į naują kambario versiją. Mes paskelbsime nuorodą į naują kambarį senojoje kambario versijoje - kambario dalyviai turės ją paspausti, norėdami prisijungti prie naujo kambario.", + "Please confirm that you'd like to go forward with upgrading this room from to .": "Prašome patvirtinti, kad jūs norite tęsti šio kambario atnaujinimą iš į .", + "Upgrade": "Atnaujinti", + "Changes your display nickname in the current room only": "Pakeičia jūsų rodomą slapyvardį tik esamame kambaryje", + "Changes the avatar of the current room": "Pakeičia esamo kambario avatarą", + "Changes your avatar in this current room only": "Pakeičia jūsų avatarą tik esamame kambaryje", + "Changes your avatar in all rooms": "Pakeičia jūsų avatarą visuose kambariuose", + "Gets or sets the room topic": "Gauna arba nustato kambario temą", + "This room has no topic.": "Šis kambarys neturi temos.", + "Sets the room name": "Nustato kambario pavadinimą", + "Use an identity server": "Naudoti tapatybės serverį", + "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Norėdami pakviesti nurodydami el. paštą, naudokite tapatybės serverį. Tam, kad būtų naudojamas numatytasis tapatybės serveris %(defaultIdentityServerName)s, spauskite tęsti, arba tvarkykite nustatymuose.", + "Use an identity server to invite by email. Manage in Settings.": "Norėdami pakviesti nurodydami el. paštą, naudokite tapatybės serverį. Tvarkykite nustatymuose.", + "Joins room with given alias": "Prisijungia prie kambario su nurodytu slapyvardžiu", + "Unbans user with given ID": "Atblokuoja vartotoją su nurodytu id", + "Ignored user": "Ignoruojamas vartotojas", + "Unignored user": "Nebeignoruojamas vartotojas", + "You are no longer ignoring %(userId)s": "Dabar nebeignoruojate %(userId)s", + "Define the power level of a user": "Nustatykite vartotojo galios lygį", + "Deops user with given id": "Deop'ina vartotoją su nurodytu id", + "Adds a custom widget by URL to the room": "Į kambarį prideda pasirinktinį valdiklį pagal URL", + "Please supply a https:// or http:// widget URL": "Prašome pateikti https:// arba http:// valdiklio URL", + "You cannot modify widgets in this room.": "Jūs negalite keisti valdiklių šiame kambaryje.", + "Verifies a user, device, and pubkey tuple": "Patikrina vartotoją, įrenginį ir pubkey seką", + "Forces the current outbound group session in an encrypted room to be discarded": "Priverčia išmesti esamą užsibaigiančią grupės sesiją šifruotame kambaryje", + "Sends the given message coloured as a rainbow": "Išsiunčia nurodytą žinutę nuspalvintą kaip vaivorykštė", + "Sends the given emote coloured as a rainbow": "Išsiunčia nurodytą emociją nuspalvintą kaip vaivorykštė", + "Displays list of commands with usages and descriptions": "Parodo komandų sąrašą su naudojimo būdais ir aprašymais", + "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s priėmė pakvietimą %(displayName)s.", + "%(senderName)s requested a VoIP conference.": "%(senderName)s pageidauja VoIP konferencijos.", + "%(senderName)s made no change.": "%(senderName)s neatliko pakeitimo.", + "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s atšaukė %(targetName)s pakvietimą.", + "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s atnaujino šį kambarį.", + "%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s padarė kambarį viešą visiems žinantiems nuorodą.", + "%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s padarė kambarį tik pakviestiems.", + "%(senderDisplayName)s changed the join rule to %(rule)s": "%(senderDisplayName)s pakeitė prisijungimo normą į %(rule)s", + "%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s leido svečiams prisijungti prie kambario.", + "%(senderDisplayName)s has prevented guests from joining the room.": "%(senderDisplayName)s uždraudė svečiams prisijungti prie kambario.", + "%(senderDisplayName)s changed guest access to %(rule)s": "%(senderDisplayName)s pakeitė svečių prieigą prie %(rule)s", + "%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s įjungė ženkliukus bendruomenėi %(groups)s šiame kambaryje.", + "%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s išjungė ženkliukus bendruomenėi %(groups)s šiame kambaryje.", + "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s įjungė ženkliukus bendruomenėi %(newGroups)s ir išjungė ženkliukus bendruomenėi %(oldGroups)s šiame kambaryje.", + "%(senderName)s placed a %(callType)s call.": "%(senderName)s pradėjo %(callType)s skambutį.", + "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s atšaukė pakvietimą %(targetDisplayName)s prisijungti prie kambario.", + "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s išsiuntė pakvietimą %(targetDisplayName)s prisijungti prie kambario.", + "%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s padarė būsimą kambario istoriją matomą visiems kambario dalyviams, nuo pat jų prisijungimo.", + "%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s padarė būsimą kambario istoriją matomą nežinomam (%(visibility)s).", + "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s iš %(fromPowerLevel)s į %(toPowerLevel)s", + "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s pakeitė %(powerLevelDiffText)s galios lygį.", + "%(displayName)s is typing …": "%(displayName)s rašo …", + "%(names)s and %(count)s others are typing …|other": "%(names)s ir %(count)s kiti rašo …", + "%(names)s and %(count)s others are typing …|one": "%(names)s ir vienas kitas rašo …", + "%(names)s and %(lastPerson)s are typing …": "%(names)s ir %(lastPerson)s rašo …", + "Cannot reach homeserver": "Serveris nepasiekiamas", + "Ensure you have a stable internet connection, or get in touch with the server admin": "Įsitikinkite, kad jūsų interneto ryšys yra stabilus, arba susisiekite su serverio administratoriumi", + "Ask your Riot admin to check your config for incorrect or duplicate entries.": "Paprašykite savo Riot administratoriaus patikrinti ar jūsų konfigūracijoje nėra neteisingų arba pasikartojančių įrašų.", + "Cannot reach identity server": "Tapatybės serveris nepasiekiamas", + "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.": "Jūs galite registruotis, tačiau kai kurios funkcijos bus nepasiekiamos, kol tapatybės serveris prisijungs. Jei ir toliau matote šį įspėjimą, patikrinkite savo konfigūraciją arba susisiekite su serverio administratoriumi.", + "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.": "Jūs galite iš naujo nustatyti savo slaptažodį, tačiau kai kurios funkcijos bus nepasiekiamos, kol tapatybės serveris prisijungs. Jei ir toliau matote šį įspėjimą, patikrinkite savo konfigūraciją arba susisiekite su serverio administratoriumi.", + "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.": "Jūs galite prisijungti, tačiau kai kurios funkcijos bus nepasiekiamos, kol tapatybės serveris prisijungs. Jei ir toliau matote šį įspėjimą, patikrinkite savo konfigūraciją arba susisiekite su serverio administratoriumi.", + "No homeserver URL provided": "Nepateiktas serverio URL", + "Unexpected error resolving homeserver configuration": "Netikėta klaida nustatant serverio konfigūraciją", + "Unexpected error resolving identity server configuration": "Netikėta klaida nustatant tapatybės serverio konfigūraciją", + "%(items)s and %(count)s others|other": "%(items)s ir %(count)s kiti", + "%(items)s and %(count)s others|one": "%(items)s ir vienas kitas", + "%(items)s and %(lastItem)s": "%(items)s ir %(lastItem)s", + "Unrecognised address": "Neatpažintas adresas", + "You do not have permission to invite people to this room.": "Jūs neturite leidimo pakviesti žmones į šį kambarį.", + "User %(userId)s is already in the room": "Vartotojas %(userId)s jau yra kambaryje", + "User %(user_id)s may or may not exist": "Vartotojas %(user_id)s gali ir neegzistuoti", + "The user must be unbanned before they can be invited.": "Norint pakviesti vartotoją, pirmiausia turi būti pašalintas draudimas.", + "The user's homeserver does not support the version of the room.": "Vartotojo serveris nepalaiko kambario versijos.", + "Use a longer keyboard pattern with more turns": "Naudokite ilgesnį klaviatūros modelį su daugiau vijų", + "There was an error joining the room": "Prisijungiant prie kambario įvyko klaida", + "Failed to join room": "Prisijungti prie kambario nepavyko", + "Message Pinning": "Žinutės prisegimas", + "Custom user status messages": "Pasirinktinės vartotojo būsenos žinutės", + "Group & filter rooms by custom tags (refresh to apply changes)": "Grupuoti ir filtruoti kambarius pagal pasirinktines žymas (atnaujinkite, kad pritaikytumėte pakeitimus)", + "Render simple counters in room header": "Užkrauti paprastus skaitiklius kambario antraštėje", + "Multiple integration managers": "Daugialypiai integracijų valdikliai" } From bb5f532eeb5d074c16439f50af7d2a62996e6249 Mon Sep 17 00:00:00 2001 From: fenuks Date: Wed, 6 Nov 2019 00:32:52 +0000 Subject: [PATCH 031/334] Translated using Weblate (Polish) Currently translated at 74.6% (1382 of 1853 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 | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index dd09059da8..31f82bc2dd 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -1645,5 +1645,13 @@ "Chat with Riot Bot": "Rozmowa z Botem Riota", "FAQ": "Najczęściej zadawane pytania", "Always show the window menu bar": "Zawsze pokazuj pasek menu okna", - "Close button should minimize window to tray": "Przycisk zamknięcia minimalizuje okno do zasobnika" + "Close button should minimize window to tray": "Przycisk zamknięcia minimalizuje okno do zasobnika", + "Add Email Address": "Dodaj adres e-mail", + "Add Phone Number": "Dodaj numer telefonu", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Ta czynność wymaga dostępu do domyślnego serwera tożsamości do walidacji adresu e-mail, czy numeru telefonu, ale serwer nie określa warunków korzystania z usługi.", + "Sends a message as plain text, without interpreting it as markdown": "Wysyła wiadomość jako zwykły tekst, bez jego interpretacji jako markdown", + "You do not have the required permissions to use this command.": "Nie posiadasz wymaganych uprawnień do użycia tego polecenia.", + "Changes the avatar of the current room": "Zmienia awatar dla obecnego pokoju", + "Use an identity server": "Użyj serwera tożsamości", + "Show previews/thumbnails for images": "Pokaż podgląd/miniatury obrazów" } From 4fe95b0075832725f9e11a64cd3d44be29ee1ceb Mon Sep 17 00:00:00 2001 From: Walter Date: Thu, 7 Nov 2019 18:22:05 +0000 Subject: [PATCH 032/334] Translated using Weblate (Russian) Currently translated at 99.8% (1849 of 1853 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 | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index b22c627213..01065a9e96 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -1299,7 +1299,7 @@ "Two-way device verification using short text": "Двусторонняя проверка устройства используя короткий текст", "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 read receipts": "Показывать уведомления о прочтении", @@ -1347,7 +1347,7 @@ "Account management": "Управление аккаунтом", "Deactivating your account is a permanent action - be careful!": "Деактивация вашей учётной записи — это необратимое действие. Будьте осторожны!", "Chat with Riot Bot": "Чат с ботом Riot", - "Help & About": "Помощь & о программе", + "Help & About": "Помощь & О программе", "FAQ": "Часто задаваемые вопросы", "Versions": "Версии", "Lazy loading is not supported by your current homeserver.": "Ленивая подгрузка не поддерживается вашим сервером.", @@ -1391,7 +1391,7 @@ "Backing up %(sessionsRemaining)s keys...": "Резервное копирование %(sessionsRemaining)s ключей...", "All keys backed up": "Все ключи сохранены", "Developer options": "Параметры разработчика", - "General": "Общий", + "General": "Общие", "Set a new account password...": "Установить новый пароль учётной записи...", "Legal": "Законный", "At this time it is not possible to reply with an emote.": "В настоящее время невозможно ответить с помощью эмоции.", @@ -2018,7 +2018,7 @@ "Create a private room": "Создать приватную комнату", "Topic (optional)": "Тема (опционально)", "Make this room public": "Сделать комнату публичной", - "Use the new, faster, composer for writing messages": "Используйте новый, более быстрый, composer для написания сообщений.", + "Use the new, faster, composer for writing messages": "Используйте новый, более быстрый, редактор для написания сообщений.", "Send read receipts for messages (requires compatible homeserver to disable)": "Отправлять подтверждения о прочтении сообщений (требуется отключение совместимого домашнего сервера)", "Show previews/thumbnails for images": "Показать превью / миниатюры для изображений", "Disconnect from the identity server and connect to instead?": "Отключиться от сервера идентификации и вместо этого подключиться к ?", @@ -2160,5 +2160,10 @@ "Recent rooms": "Недавние комнаты", "No identity server is configured so you cannot add an email address in order to reset your password in the future.": "Сервер идентификации не настроен, поэтому вы не можете добавить адрес электронной почты, чтобы в будущем сбросить пароль.", "Jump to first unread room.": "Перейти в первую непрочитанную комнату.", - "Jump to first invite.": "Перейти к первому приглашению." + "Jump to first invite.": "Перейти к первому приглашению.", + "Trust": "Доверие", + "%(count)s unread messages including mentions.|one": "1 непрочитанное упоминание.", + "%(count)s unread messages.|one": "1 непрочитанное сообщение.", + "Unread messages.": "Непрочитанные сообщения.", + "Message Actions": "Сообщение действий" } From c4d45e87ea251f62f3ffc8c33c916613d256a9b3 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 8 Nov 2019 15:54:48 -0700 Subject: [PATCH 033/334] Use a ternary operator instead of relying on AND semantics in EditHIstoryDialog Fixes https://github.com/vector-im/riot-web/issues/11334 (probably). `allEvents` should never have a boolean in it, so given the stack trace and the code this is my best estimate for what the problem could be. I can't reproduce the problem. --- src/components/views/dialogs/MessageEditHistoryDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/MessageEditHistoryDialog.js b/src/components/views/dialogs/MessageEditHistoryDialog.js index 6014cb941c..b5e4daa1c1 100644 --- a/src/components/views/dialogs/MessageEditHistoryDialog.js +++ b/src/components/views/dialogs/MessageEditHistoryDialog.js @@ -116,7 +116,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent { nodes.push(( Date: Fri, 8 Nov 2019 18:40:35 +0000 Subject: [PATCH 034/334] Translated using Weblate (French) Currently translated at 100.0% (1864 of 1864 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 | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 328c3b7f9e..40840c8d58 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2280,5 +2280,16 @@ "Show tray icon and minimize window to it on close": "Afficher l’icône dans la barre d’état et minimiser la fenêtre lors de la fermeture", "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Cette action nécessite l’accès au serveur d’identité par défaut afin de valider une adresse e-mail ou un numéro de téléphone, mais le serveur n’a aucune condition de service.", "Trust": "Confiance", - "Message Actions": "Actions de message" + "Message Actions": "Actions de message", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "Send verification requests in direct message": "Envoyer les demandes de vérification en message direct", + "You verified %(name)s": "Vous avez vérifié %(name)s", + "You cancelled verifying %(name)s": "Vous avez annulé la vérification de %(name)s", + "%(name)s cancelled verifying": "%(name)s a annulé la vérification", + "You accepted": "Vous avez accepté", + "%(name)s accepted": "%(name)s a accepté", + "You cancelled": "Vous avez annulé", + "%(name)s cancelled": "%(name)s a annulé", + "%(name)s wants to verify": "%(name)s veut vérifier", + "You sent a verification request": "Vous avez envoyé une demande de vérification" } From 949ba89b4a5cc657d953ca3566badb5b24779c4e Mon Sep 17 00:00:00 2001 From: Elwyn Malethan Date: Sat, 9 Nov 2019 19:00:33 +0000 Subject: [PATCH 035/334] Added translation using Weblate (Welsh) --- src/i18n/strings/cy.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/i18n/strings/cy.json diff --git a/src/i18n/strings/cy.json b/src/i18n/strings/cy.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/src/i18n/strings/cy.json @@ -0,0 +1 @@ +{} From def4f90257bd486b41305b6e34c3087b29e72a46 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Sun, 10 Nov 2019 18:11:53 +0000 Subject: [PATCH 036/334] Translated using Weblate (Albanian) Currently translated at 99.8% (1860 of 1864 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 | 43 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 4f25162491..1d80a90a2b 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -2205,5 +2205,46 @@ "Emoji Autocomplete": "Vetëplotësim Emoji-sh", "Notification Autocomplete": "Vetëplotësim NJoftimesh", "Room Autocomplete": "Vetëplotësim Dhomash", - "User Autocomplete": "Vetëplotësim Përdoruesish" + "User Autocomplete": "Vetëplotësim Përdoruesish", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Ky veprim lyp hyrje te shërbyesi parazgjedhje i identiteteve për të vlerësuar një adresë email ose një numër telefoni, por shërbyesi nuk ka ndonjë kusht shërbimesh.", + "Trust": "Besim", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "Use the new, consistent UserInfo panel for Room Members and Group Members": "Përdorni panelin e ri UserInfo për Anëtarë Dhome dhe Anëtarë Grupi", + "Send verification requests in direct message": "Dërgo kërkesa verifikimi në mesazh të drejtpërdrejt", + "Show tray icon and minimize window to it on close": "Me mbylljen, shfaq ikonë paneli dhe minimizo dritaren", + "Room %(name)s": "Dhoma %(name)s", + "Recent rooms": "Dhoma së fundi", + "%(count)s unread messages including mentions.|one": "1 përmendje e palexuar.", + "%(count)s unread messages.|one": "1 mesazh i palexuar.", + "Unread messages.": "Mesazhe të palexuar.", + "Trust & Devices": "Besim & Pajisje", + "Direct messages": "Mesazhe të drejtpërdrejtë", + "Failed to deactivate user": "S’u arrit të çaktivizohet përdorues", + "This client does not support end-to-end encryption.": "Ky klient nuk mbulon fshehtëzim skaj-më-skaj.", + "Messages in this room are not end-to-end encrypted.": "Mesazhet në këtë dhomë nuk janë të fshehtëzuara skaj-më-skaj.", + "React": "Reagoni", + "Message Actions": "Veprime Mesazhesh", + "You verified %(name)s": "Verifikuat %(name)s", + "You cancelled verifying %(name)s": "Anuluat verifikimin e %(name)s", + "%(name)s cancelled verifying": "%(name)s anuloi verifikimin", + "You accepted": "Pranuat", + "%(name)s accepted": "%(name)s pranoi", + "You cancelled": "Anuluat", + "%(name)s cancelled": "%(name)s anuloi", + "%(name)s wants to verify": "%(name)s dëshiron të verifikojë", + "You sent a verification request": "Dërguat një kërkesë verifikimi", + "Frequently Used": "Përdorur Shpesh", + "Smileys & People": "Emotikone & Persona", + "Animals & Nature": "Kafshë & Natyrë", + "Food & Drink": "Ushqim & Pije", + "Activities": "Veprimtari", + "Travel & Places": "Udhëtim & Vende", + "Objects": "Objekte", + "Symbols": "Simbole", + "Flags": "Flamuj", + "Quick Reactions": "Reagime të Shpejta", + "Cancel search": "Anulo kërkimin", + "No identity server is configured so you cannot add an email address in order to reset your password in the future.": "S’ka shërbyes identitetesh të formësuar, ndaj s’mund të shtoni një adresë email që të mund të ricaktoni fjalëkalimin tuaj në të ardhmen.", + "Jump to first unread room.": "Hidhu te dhoma e parë e palexuar.", + "Jump to first invite.": "Hidhu te ftesa e parë." } From eaac3fe3b8000423f195edcc865fa32cb3ff2deb Mon Sep 17 00:00:00 2001 From: Osoitz Date: Sat, 9 Nov 2019 11:12:46 +0000 Subject: [PATCH 037/334] Translated using Weblate (Basque) Currently translated at 100.0% (1864 of 1864 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/eu/ --- src/i18n/strings/eu.json | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/eu.json b/src/i18n/strings/eu.json index 72b6fff50d..888f6984e7 100644 --- a/src/i18n/strings/eu.json +++ b/src/i18n/strings/eu.json @@ -2222,5 +2222,19 @@ "Jump to first unread room.": "Jauzi irakurri gabeko lehen gelara.", "Jump to first invite.": "Jauzi lehen gonbidapenera.", "Command Autocomplete": "Aginduak auto-osatzea", - "DuckDuckGo Results": "DuckDuckGo emaitzak" + "DuckDuckGo Results": "DuckDuckGo emaitzak", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Ekintza honek lehenetsitako identitate-zerbitzaria atzitzea eskatzen du, e-mail helbidea edo telefono zenbakia balioztatzeko, baina zerbitzariak ez du erabilera baldintzarik.", + "Trust": "Jo fidagarritzat", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "Send verification requests in direct message": "Bidali egiaztaketa eskariak mezu zuzen bidez", + "Message Actions": "Mezu-ekintzak", + "You verified %(name)s": "%(name)s egiaztatu duzu", + "You cancelled verifying %(name)s": "%(name)s egiaztatzeari utzi diozu", + "%(name)s cancelled verifying": "%(name)s(e)k egiaztaketa utzi du", + "You accepted": "Onartu duzu", + "%(name)s accepted": "%(name)s onartuta", + "You cancelled": "Utzi duzu", + "%(name)s cancelled": "%(name)s utzita", + "%(name)s wants to verify": "%(name)s(e)k egiaztatu nahi du", + "You sent a verification request": "Egiaztaketa eskari bat bidali duzu" } From a4a0dc9c2d7e14c32c1d3cba6e3663f277e443a4 Mon Sep 17 00:00:00 2001 From: Slavi Pantaleev Date: Mon, 11 Nov 2019 06:47:26 +0000 Subject: [PATCH 038/334] Translated using Weblate (Bulgarian) Currently translated at 100.0% (1864 of 1864 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/bg/ --- src/i18n/strings/bg.json | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/bg.json b/src/i18n/strings/bg.json index 6f198b2e5a..3697cc635c 100644 --- a/src/i18n/strings/bg.json +++ b/src/i18n/strings/bg.json @@ -2249,5 +2249,16 @@ "Emoji Autocomplete": "Подсказка за емоджита", "Notification Autocomplete": "Подсказка за уведомления", "Room Autocomplete": "Подсказка за стаи", - "User Autocomplete": "Подсказка за потребители" + "User Autocomplete": "Подсказка за потребители", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "Send verification requests in direct message": "Изпращай заявки за потвърждение чрез директни съобщения", + "You verified %(name)s": "Потвърдихте %(name)s", + "You cancelled verifying %(name)s": "Отказахте потвърждаването за %(name)s", + "%(name)s cancelled verifying": "%(name)s отказа потвърждаването", + "You accepted": "Приехте", + "%(name)s accepted": "%(name)s прие", + "You cancelled": "Отказахте потвърждаването", + "%(name)s cancelled": "%(name)s отказа", + "%(name)s wants to verify": "%(name)s иска да извърши потвърждение", + "You sent a verification request": "Изпратихте заявка за потвърждение" } From d8ea25403acb33a166f7f62ab668146c51cf306a Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Mon, 11 Nov 2019 03:26:55 +0000 Subject: [PATCH 039/334] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1864 of 1864 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 | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 58dca89415..185026aad5 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2273,5 +2273,16 @@ "Show tray icon and minimize window to it on close": "顯示系統匣圖示並在關閉視窗時將其最小化至其中", "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "此動作需要存取預設的身份識別伺服器 以驗證電子郵件或電話號碼,但伺服器沒有任何服務條款。", "Trust": "信任", - "Message Actions": "訊息動作" + "Message Actions": "訊息動作", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "Send verification requests in direct message": "在直接訊息中傳送驗證請求", + "You verified %(name)s": "您驗證了 %(name)s", + "You cancelled verifying %(name)s": "您已取消驗證 %(name)s", + "%(name)s cancelled verifying": "%(name)s 已取消驗證", + "You accepted": "您已接受", + "%(name)s accepted": "%(name)s 已接受", + "You cancelled": "您已取消", + "%(name)s cancelled": "%(name)s 已取消", + "%(name)s wants to verify": "%(name)s 想要驗證", + "You sent a verification request": "您已傳送了驗證請求" } From 163f9f057fd39db2beb45ecd060a34bdb49e5f2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanislav=20Luke=C5=A1?= Date: Sun, 10 Nov 2019 22:20:21 +0000 Subject: [PATCH 040/334] Translated using Weblate (Czech) Currently translated at 99.9% (1863 of 1864 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/cs/ --- src/i18n/strings/cs.json | 179 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 174 insertions(+), 5 deletions(-) diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index 2d3088c279..e4e01b0116 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -1716,7 +1716,7 @@ "Remove messages": "Odstranit zprávy", "Notify everyone": "Upozornění pro celou místnost", "Enable encryption?": "Povolit šifrování?", - "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.": "Po zapnutí už nelze šifrování v této místnosti zakázat. Zprávy v šifrovaných místostech můžou číst jenom členové místnosti, server se k obsahu nedostane. Šifrování místností nepodporuje většina botů a bridgů. Více informací o šifrování.", + "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.": "Po zapnutí už nelze šifrování v této místnosti zakázat. Zprávy v šifrovaných místostech můžou číst jenom členové místnosti, server se k obsahu nedostane. Šifrování místností nepodporuje většina botů a propojení. Více informací o šifrování.", "Error updating main address": "Nepovedlo se změnit hlavní adresu", "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "Nastala chyba při pokusu o nastavení hlavní adresy místnosti. Mohl to zakázat server, nebo to může být dočasná chyba.", "Error creating alias": "Nepovedlo se vyrobit alias", @@ -1957,8 +1957,8 @@ "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.": "Pokud nechcete na hledání existujících kontaktů používat server , zvolte si jiný server.", "Identity Server": "Server identit", "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "Aktuálně nepoužíváte žádný server identit. Na hledání existujících kontaktů a přidání se do registru kontatů přidejte server identit níže.", - "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Odpojení ze serveru identit znamená, že vás nepůjde najít podle emailové adresy a telefonního čísla and vy taky nebudete moct hledat ostatní.", - "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Použití serveru identit je volitelné. Nemusíte server identit používat, ale nepůjde vás pak najít podle emailové adresy a telefonního čísla a vy také nebudete moct hledat ostatní.", + "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Odpojení ze serveru identit znamená, že vás nepůjde najít podle emailové adresy ani telefonního čísla and vy také nebudete moct hledat ostatní.", + "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Použití serveru identit je volitelné. Nemusíte server identit používat, ale nepůjde vás pak najít podle emailové adresy ani telefonního čísla a vy také nebudete moct hledat ostatní.", "Do not use an identity server": "Nepoužívat server identit", "Enter a new identity server": "Zadejte nový server identit", "Failed to update integration manager": "Nepovedlo se aktualizovat správce integrací", @@ -1969,7 +1969,7 @@ "You are currently using %(serverName)s to manage your bots, widgets, and sticker packs.": "Aktálně používáte %(serverName)s na správu robotů, widgetů, and balíků samolepek.", "Add which integration manager you want to manage your bots, widgets, and sticker packs.": "Zadejte kterého správce integrací chcete používat na správu robotů, widgetů a balíků samolepek.", "Integration Manager": "Správce integrací", - "Enter a new integration manager": "Přidat nový správce integrací", + "Enter a new integration manager": "Přidat nového správce integrací", "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Musíte odsouhlasit podmínky použití serveru (%(serverName)s) abyste se mohli zapsat do registru emailových adres a telefonních čísel.", "Deactivate account": "Deaktivovat účet", "Always show the window menu bar": "Vždy zobrazovat horní lištu okna", @@ -2002,5 +2002,174 @@ "Command Help": "Nápověda příkazu", "Integrations Manager": "Správce integrací", "Find others by phone or email": "Hledat ostatní pomocí emailu nebo telefonu", - "Be found by phone or email": "Umožnit ostatním mě nalézt pomocí emailu nebo telefonu" + "Be found by phone or email": "Umožnit ostatním mě nalézt pomocí emailu nebo telefonu", + "Add Email Address": "Přidat emailovou adresu", + "Add Phone Number": "Přidat telefonní číslo", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Tato akce vyžaduje přístup k výchozímu serveru identity aby šlo ověřit email a telefon, ale server nemá podmínky použití.", + "Trust": "Důvěra", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "Multiple integration managers": "Více správců integrací", + "Use the new, consistent UserInfo panel for Room Members and Group Members": "Používat nový, konzistentní panel s informaci o uživalelích pro členy místností and skupin", + "Send verification requests in direct message": "Poslat žádost o verifikaci jako přímou zprávu", + "Use the new, faster, composer for writing messages": "Použít nový, rychlejší editor zpráv", + "Show previews/thumbnails for images": "Zobrazovat náhledy obrázků", + "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "Před odpojením byste měli smazat osobní údaje ze serveru identit . Bohužel, server je offline nebo neodpovídá.", + "You should:": "Měli byste:", + "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "zkontrolujte, jestli nemáte v prohlížeči nějaký doplněk blokující server identit (například Privacy Badger)", + "contact the administrators of identity server ": "konaktujte administrátora serveru identit ", + "wait and try again later": "počkejte z zkuste to znovu později", + "Discovery": "Nalezitelnost", + "Clear cache and reload": "Smazat mezipaměť a načíst znovu", + "Show tray icon and minimize window to it on close": "Zobrazovat systémovou ikonu a minimalizovat při zavření", + "Read Marker lifetime (ms)": "životnost značky přečteno (ms)", + "Read Marker off-screen lifetime (ms)": "životnost značky přečteno mimo obrazovku (ms)", + "A device's public name is visible to people you communicate with": "Veřejné jméno zařízení je viditelné pro lidi se kterými komunikujete", + "Upgrade the room": "Upgradovat místnost", + "Enable room encryption": "Povolit v místnosti šifrování", + "Error changing power level requirement": "Chyba změny požadavku na úroveň oprávnění", + "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "Došlo k chybě při změně požadované úrovně oprávnění v místnosti. Ubezpečte se, že na to máte dostatečná práva a zkuste to znovu.", + "Error changing power level": "Chyba při změně úrovně oprávnění", + "An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "Došlo k chybě při změně úrovně oprávnění uživatele. Ubezpečte se, že na to máte dostatečná práva a zkuste to znovu.", + "Unable to revoke sharing for email address": "Nepovedlo se zrušit sdílení emailové adresy", + "Unable to share email address": "Nepovedlo se nasdílet emailovou adresu", + "Your email address hasn't been verified yet": "Vaše emailová adresa nebyla zatím verifikována", + "Click the link in the email you received to verify and then click continue again.": "Pro verifikaci a pokračování klikněte na odkaz v emailu, který vím přišel.", + "Verify the link in your inbox": "Ověřte odkaz v emailové schánce", + "Complete": "Dokončit", + "Revoke": "Zneplatnit", + "Share": "Sdílet", + "Discovery options will appear once you have added an email above.": "Možnosti nalezitelnosti se objeví až výše přidáte svou emailovou adresu.", + "Unable to revoke sharing for phone number": "Nepovedlo se zrušit sdílení telefonního čísla", + "Unable to share phone number": "Nepovedlo se nasdílet telefonní číslo", + "Please enter verification code sent via text.": "Zadejte prosím ověřovací SMS kód.", + "Discovery options will appear once you have added a phone number above.": "Možnosti nalezitelnosti se objeví až zadáte telefonní číslo výše.", + "Remove %(email)s?": "Odstranit %(email)s?", + "Remove %(phone)s?": "Odstranit %(phone)s?", + "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "SMS zpráva byla odeslána na +%(msisdn)s. Zadejte prosím ověřovací kód, který obsahuje.", + "No recent messages by %(user)s found": "Nebyly nalezeny žádné nedávné zprávy od uživatele %(user)s", + "Try scrolling up in the timeline to see if there are any earlier ones.": "Zkuste zascrollovat nahoru, jestli tam nejsou nějaké dřívější.", + "Remove recent messages by %(user)s": "Odstranit nedávné zprávy od uživatele %(user)s", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "Odstraňujeme %(count)s zpráv od %(user)s. Nelze to vzít zpět. Chcete pokračovat?", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "Odstraňujeme jednu zprávu od %(user)s. Nelze to vzít zpět. Chcete pokračovat?", + "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "Pro větší množství zpráv to může nějakou dobu trvat. V průběhu prosím neobnovujte klienta.", + "Remove %(count)s messages|other": "Odstranit %(count)s zpráv", + "Remove %(count)s messages|one": "Odstranit zprávu", + "Deactivate user?": "Deaktivovat uživatele?", + "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?": "Deaktivování uživatele ho odhlásí a zabrání mu v opětovném přihlášení. Navíc bude ostraněn ze všech místností. Akci nelze vzít zpět. Jste si jistí, že chcete uživatele deaktivovat?", + "Deactivate user": "Deaktivovat uživatele", + "Remove recent messages": "Odstranit nedávné zprávy", + "Bold": "Tučně", + "Italics": "Kurzívou", + "Strikethrough": "Přešktnutě", + "Code block": "Blok kódu", + "Room %(name)s": "Místnost %(name)s", + "Recent rooms": "Nedávné místnosti", + "Loading room preview": "Načítání náhdledu místnosti", + "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to a room admin.": "Při ověřování pozvánky jsme dostali chybu (%(errcode)s). Můžete zkusit tuto informaci předat administrátorovi místnosti.", + "This invite to %(roomName)s was sent to %(email)s which is not associated with your account": "Pozvánka do místnosti %(roomName)s byla poslána na %(email)s, který není přidaný k tomuto účtu", + "Link this email with your account in Settings to receive invites directly in Riot.": "Přidejte si tento email k účtu v Nastavení, abyste dostávali pozvání přímo v Riotu.", + "This invite to %(roomName)s was sent to %(email)s": "Pozvánka do %(roomName)s byla odeslána na %(email)s", + "Use an identity server in Settings to receive invites directly in Riot.": "Používat server identit z nastavení k přijímání pozvánek přímo v Riotu.", + "Share this email in Settings to receive invites directly in Riot.": "Sdílet tento email v nastavení, abyste mohli dostávat pozvánky přímo v Riotu.", + "%(count)s unread messages including mentions.|other": "%(count)s nepřečtených zpráv a zmínek.", + "%(count)s unread messages including mentions.|one": "Nepřečtená zmínka.", + "%(count)s unread messages.|other": "%(count)s nepřečtených zpráv.", + "%(count)s unread messages.|one": "Nepřečtená zpráva.", + "Unread mentions.": "Nepřečtená zmínka.", + "Unread messages.": "Nepřečtené zprávy.", + "Failed to connect to integrations server": "Nepovedlo se připojit k serveru integrací", + "No integrations server is configured to manage stickers with": "Žádný server integrací není nakonfigurován aby spravoval samolepky", + "Trust & Devices": "Důvěra & Zařízení", + "Direct messages": "Přímé zprávy", + "Failed to deactivate user": "Deaktivace uživatele se nezdařila", + "This client does not support end-to-end encryption.": "Tento klient nepodporuje end-to-end šifrování.", + "Messages in this room are not end-to-end encrypted.": "Zprávy nejsou end-to-end šifrované.", + "React": "Reagovat", + "Message Actions": "Akce zprávy", + "Show image": "Zobrazit obrázek", + "You verified %(name)s": "Ověřili jste %(name)s", + "You cancelled verifying %(name)s": "Zrušili jste ověření %(name)s", + "%(name)s cancelled verifying": "%(name)s zrušil/a ověření", + "You accepted": "Přijali jste", + "%(name)s accepted": "%(name)s přijal/a", + "You cancelled": "Zrušili jste", + "%(name)s cancelled": "%(name)s zrušil/a", + "%(name)s wants to verify": "%(name)s chce ověřit", + "You sent a verification request": "Poslali jste požadavek na ověření", + "Show all": "Zobrazit vše", + "Edited at %(date)s. Click to view edits.": "Pozměněno v %(date)s. Klinutím zobrazíte změny.", + "Frequently Used": "Často používané", + "Smileys & People": "Obličeje & Lidé", + "Animals & Nature": "Zvířata & Příroda", + "Food & Drink": "Jídlo & Nápoje", + "Activities": "Aktivity", + "Travel & Places": "Cestování & Místa", + "Objects": "Objekty", + "Symbols": "Symboly", + "Flags": "Vlajky", + "Quick Reactions": "Rychlé reakce", + "Cancel search": "Zrušit hledání", + "Please create a new issue on GitHub so that we can investigate this bug.": "Vyrobte prosím nové issue na GitHubu abychom mohli chybu opravit.", + "%(severalUsers)smade no changes %(count)s times|other": "%(severalUsers)s neudělali %(count)s krát žádnou změnu", + "%(severalUsers)smade no changes %(count)s times|one": "%(severalUsers)sneudělali žádnou změnu", + "%(oneUser)smade no changes %(count)s times|other": "%(oneUser)sneudělal %(count)s krát žádnou změnu", + "%(oneUser)smade no changes %(count)s times|one": "%(oneUser)sneudělal žádnou změnu", + "Room alias": "Alias místnosti", + "e.g. my-room": "například moje-mistost", + "Please provide a room alias": "Zadejte prosím alias místnosti", + "This alias is available to use": "Tento alias je volný", + "This alias is already in use": "Tento alias už je používán", + "Use bots, bridges, widgets and sticker packs": "Použít roboty, propojení, widgety a balíky samolepek", + "Terms of Service": "Podmínky použití", + "To continue you need to accept the terms of this service.": "Musíte souhlasit s podmínkami použití, abychom mohli pokračovat.", + "Service": "Služba", + "Summary": "Shrnutí", + "Document": "Dokument", + "Upload all": "Nahrát vše", + "Resend edit": "Odeslat změnu znovu", + "Resend %(unsentCount)s reaction(s)": "Poslat %(unsentCount)s reakcí znovu", + "Resend removal": "Odeslat smazání znovu", + "Report Content": "Nahlásit obsah", + "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "Na domovském serveru chybí veřejný klíč pro captcha. Nahlaste to prosím administrátorovi serveru.", + "No identity server is configured so you cannot add an email address in order to reset your password in the future.": "Žádný server identit není nakonfigurován, takže nemůžete přidat emailovou adresu pro obnovení hesla.", + "Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.": "Nastavit emailovou adresu pro obnovení hesla. Také můžete použít email nebo telefon, aby vás vaši přátelé snadno nalezli.", + "Set an email for account recovery. Use email to optionally be discoverable by existing contacts.": "Nastavit emailovou adresu pro obnovení hesla. Také můžete použít email, aby vás vaši přátelé snadno nalezli.", + "Enter your custom homeserver URL What does this mean?": "Zadejte adresu domovského serveru Co to znamená?", + "Enter your custom identity server URL What does this mean?": "Zadejte adresu serveru identit Co to znamená?", + "Explore": "Prohlížet", + "Filter": "Filtrovat", + "Filter rooms…": "Filtrovat místnosti…", + "%(creator)s created and configured the room.": "%(creator)s vytvořil a nakonfiguroval místnost.", + "Preview": "Náhled", + "View": "Zobrazit", + "Find a room…": "Najít místnost…", + "Find a room… (e.g. %(exampleRoom)s)": "Najít místnost… (například %(exampleRoom)s)", + "If you can't find the room you're looking for, ask for an invite or Create a new room.": "Pokud nemůžete nelézt místnost, kterou hledáte, napište o pozvánku nebo si jí Vytvořte.", + "Explore rooms": "Prohlížet místnosti", + "Jump to first unread room.": "Skočit na první nepřečtenou místnost.", + "Jump to first invite.": "Skočit na první pozvánku.", + "No identity server is configured: add one in server settings to reset your password.": "Žádný server identit není nakonfigurován: přidejte si ho v nastavení, abyste mohli obnovit heslo.", + "This account has been deactivated.": "Tento účet byl deaktivován.", + "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "nový účet (%(newAccountId)s) je registrován, ale už jste přihlášeni pod jiným účtem (%(loggedInUserId)s).", + "Continue with previous account": "Pokračovat s předchozím účtem", + "Log in to your new account.": "Přihlaste se svým novým účtem.", + "You can now close this window or log in to your new account.": "Teď můžete okno zavřít, nebo se přihlásit svým novým účtem.", + "Registration Successful": "Úspěšná registrace", + "Failed to re-authenticate due to a homeserver problem": "Kvůli problémům s domovským server se nepovedlo autentifikovat znovu", + "Failed to re-authenticate": "Nepovedlo se autentifikovat", + "Regain access to your account and recover encryption keys stored on this device. Without them, you won’t be able to read all of your secure messages on any device.": "Získejte znovu přístup k účtu a obnovte si šifrovací klíče uložené na tomto zařízení. Bez nich nebudete schopni číst zabezpečené zprávy na některých zařízeních.", + "Enter your password to sign in and regain access to your account.": "Zadejte heslo pro přihlášení a obnovte si přístup k účtu.", + "Forgotten your password?": "Zapomněli jste heslo?", + "Sign in and regain access to your account.": "Přihlaste se a získejte přístup ke svému účtu.", + "You cannot sign in to your account. Please contact your homeserver admin for more information.": "Nemůžete se přihlásit do svého účtu. Kontaktujte administrátora domovského serveru pro více informací.", + "You're signed out": "Jste odhlášeni", + "Clear personal data": "Smazat osobní data", + "Warning: Your personal data (including encryption keys) is still stored on this device. Clear it if you're finished using this device, or want to sign in to another account.": "Varování: Vaše osobní data (včetně šifrovacích klíčů) jsou pořád uložena na tomto zařízení. Smažte je, pokud už toto zařízení nehodláte používat nebo se přihlašte pod jiný účet.", + "Command Autocomplete": "Automatické doplňování příkazů", + "Community Autocomplete": "Automatické doplňování komunit", + "DuckDuckGo Results": "Výsledky hledání DuckDuckGo", + "Emoji Autocomplete": "Automatické doplňování Emodži", + "Notification Autocomplete": "Automatické doplňování upozornění", + "Room Autocomplete": "Automatické doplňování místností", + "User Autocomplete": "Automatické doplňování uživatelů" } From 0bfbf34c3956c8364134739a78a3c983597af814 Mon Sep 17 00:00:00 2001 From: Tirifto Date: Sun, 10 Nov 2019 21:24:20 +0000 Subject: [PATCH 041/334] Translated using Weblate (Esperanto) Currently translated at 99.9% (1862 of 1864 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 | 155 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 143 insertions(+), 12 deletions(-) diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json index 7e4599aaea..bbed6773b5 100644 --- a/src/i18n/strings/eo.json +++ b/src/i18n/strings/eo.json @@ -182,7 +182,7 @@ "Answer": "Respondi", "Send anyway": "Tamen sendi", "Send": "Sendi", - "Enable automatic language detection for syntax highlighting": "Ŝalti memfaran rekonon de lingvo por sintaksa markado", + "Enable automatic language detection for syntax highlighting": "Ŝalti memagan rekonon de lingvo por sintaksa markado", "Hide avatars in user and room mentions": "Kaŝi profilbildojn en mencioj de uzantoj kaj babilejoj", "Disable big emoji in chat": "Malŝalti grandajn mienetojn en babilo", "Don't send typing notifications": "Ne elsendi sciigojn pri tajpado", @@ -1270,7 +1270,7 @@ "Ignored users": "Malatentaj uzantoj", "Key backup": "Sekurkopio de ŝlosilo", "Security & Privacy": "Sekureco & Privateco", - "Voice & Video": "Voĉo k Vido", + "Voice & Video": "Voĉo kaj vido", "Upgrade room to version %(ver)s": "Ĝisdatigi ĉambron al versio %(ver)s", "Room information": "Ĉambraj informoj", "Internal room ID:": "Ena ĉambra identigilo:", @@ -1507,7 +1507,7 @@ "Changes made to your community name and avatar might not be seen by other users for up to 30 minutes.": "Ŝanĝoj al viaj komunumaj nomo kaj profilbildo eble ne montriĝos al aliaj uzantoj ĝis 30 minutoj.", "Who can join this community?": "Kiu povas aliĝi al tiu ĉi komunumo?", "This room is not public. You will not be able to rejoin without an invite.": "Ĉi tiu ĉambro ne estas publika. Vi ne povos realiĝi sen invito.", - "Can't leave Server Notices room": "Ne eblas eliri el ĉambro «  Server Notices  »", + "Can't leave Server Notices room": "Ne eblas eliri el ĉambro « Server Notices »", "Revoke invite": "Nuligi inviton", "Invited by %(sender)s": "Invitita de %(sender)s", "Error updating main address": "Ĝisdatigo de la ĉefa adreso eraris", @@ -1710,7 +1710,7 @@ "Connect this device to Key Backup": "Konekti ĉi tiun aparaton al Savkopiado de ŝlosiloj", "Start using Key Backup": "Ekuzi Savkopiadon de ŝlosiloj", "Timeline": "Historio", - "Autocomplete delay (ms)": "Prokrasto de memfara kompletigo", + "Autocomplete delay (ms)": "Prokrasto de memaga kompletigo", "Upgrade this room to the recommended room version": "Gradaltigi ĉi tiun ĉambron al rekomendata ĉambra versio", "Open Devtools": "Malfermi programistajn ilojn", "Uploaded sound": "Alŝutita sono", @@ -1747,7 +1747,7 @@ "Go to Settings": "Iri al agordoj", "Flair": "Etikedo", "No Audio Outputs detected": "Neniu soneligo troviĝis", - "Send %(eventType)s events": "Sendi okazojn de tipo «%(eventType)s»", + "Send %(eventType)s events": "Sendi okazojn de tipo « %(eventType)s »", "Select the roles required to change various parts of the room": "Elektu la rolojn postulatajn por ŝanĝado de diversaj partoj de la ĉambro", "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.": "Post ŝalto, ĉifrado de ĉambro ne povas esti malŝaltita. Mesaĝoj senditaj al ĉifrata ĉambro ne estas videblaj por la servilo, nur por la partoprenantoj de la ĉambro. Ŝalto de ĉifrado eble malfunkciigos iujn robotojn kaj pontojn. Eksciu plion pri ĉifrado.", "To link to this room, please add an alias.": "Por ligili al la ĉambro, bonvolu aldoni kromnomon.", @@ -1756,7 +1756,7 @@ "Once enabled, encryption cannot be disabled.": "Post ŝalto, ne plu eblas malŝalti ĉifradon.", "Encrypted": "Ĉifrata", "Some devices in this encrypted room are not trusted": "Iuj aparatoj en ĉi tiu ĉifrata ĉambro ne estas fidataj", - "Key share requests are sent to your other devices automatically. If you rejected or dismissed the key share request on your other devices, click here to request the keys for this session again.": "Petoj pri kunhavigo de ŝlosiloj sendiĝas al viaj aliaj aparatoj memfare. Se vi rifuzis aŭ forlasis la peton en viaj aliaj aparatoj, klaku ĉi tien por repeti la ŝlosilojn por tiu ĉi kunsido.", + "Key share requests are sent to your other devices automatically. If you rejected or dismissed the key share request on your other devices, click here to request the keys for this session again.": "Petoj pri kunhavigo de ŝlosiloj sendiĝas al viaj aliaj aparatoj memage. Se vi rifuzis aŭ forlasis la peton en viaj aliaj aparatoj, klaku ĉi tien por repeti la ŝlosilojn por tiu ĉi kunsido.", "The conversation continues here.": "La interparolo pluas ĉi tie.", "This room has been replaced and is no longer active.": "Ĉi tiu ĉambro estas anstataŭita, kaj ne plu aktivas.", "Loading room preview": "Preparas antaŭrigardon al la ĉambro", @@ -1790,7 +1790,7 @@ "Message edits": "Redaktoj de mesaĝoj", "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.": "Se vi renkontas problemojn aŭ havas prikomentojn, bonvolu sciigi nin per GitHub.", "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "Por eviti duoblajn raportojn, bonvolu unue rigardi jamajn raportojn (kaj meti +1) aŭ raporti novan problemon se vi neniun trovos.", - "Report bugs & give feedback": "Raporti erarojn ϗ sendi prikomentojn", + "Report bugs & give feedback": "Raporti erarojn kaj sendi prikomentojn", "Clear Storage and Sign Out": "Vakigi memoron kaj adiaŭi", "We encountered an error trying to restore your previous session.": "Ni renkontis eraron provante rehavi vian antaŭan kunsidon.", "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Vakigo de la memoro de via foliumilo eble korektos la problemon, sed adiaŭigos vin, kaj malebligos legadon de historio de ĉifritaj babiloj.", @@ -1865,7 +1865,7 @@ "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:": "Gradaltigo de ĉi tiu ĉambro bezonas fermi ĝin, kaj krei novan por anstataŭi ĝin. Por plejbonigi sperton de la ĉambranoj, ni:", "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device.": "Vi adiaŭis ĉiujn aparatojn kaj ne plu ricevados sciigojn. Por reŝalti ilin, resalutu per ĉiu el la aparatoj.", "Invalid homeserver discovery response": "Nevalida eltrova respondo de hejmservilo", - "Failed to get autodiscovery configuration from server": "Malsukcesis akiri agordojn de memfara eltrovado de la servilo", + "Failed to get autodiscovery configuration from server": "Malsukcesis akiri agordojn de memaga eltrovado de la servilo", "Homeserver URL does not appear to be a valid Matrix homeserver": "URL por hejmservilo ŝajne ne ligas al valida hejmservilo de Matrix", "Invalid identity server discovery response": "Nevalida eltrova respondo de identiga servilo", "Identity server URL does not appear to be a valid identity server": "URL por identiga servilo ŝajne ne ligas al valida identiga servilo", @@ -1932,7 +1932,7 @@ "Failed to start chat": "Malsukcesis komenci babilon", "Messages": "Mesaĝoj", "Actions": "Agoj", - "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Uzu identigan servilon por inviti retpoŝte. Klaku al »[…]« por uzi la implicitan identigan servilon (%(defaultIdentityServerName)s) aŭ administru tion en Agordoj.", + "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Uzu identigan servilon por inviti retpoŝte. Klaku al « daŭrigi » por uzi la norman identigan servilon (%(defaultIdentityServerName)s) aŭ administru tion en Agordoj.", "Displays list of commands with usages and descriptions": "Montras liston de komandoj kun priskribo de uzo", "Use the new, faster, but still experimental composer for writing messages (requires refresh)": "Uzi la novan, pli rapidan, sed ankoraŭ eksperimentan komponilon de mesaĝoj (bezonas aktualigon)", "Send read receipts for messages (requires compatible homeserver to disable)": "Sendi legokonfirmojn de mesaĝoj (bezonas akordan hejmservilon por malŝalto)", @@ -1957,8 +1957,8 @@ "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "Vi nun uzas servilon por trovi kontaktojn, kaj troviĝi de ili. Vi povas ŝanĝi vian identigan servilon sube.", "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.": "Se vi ne volas uzi servilon por trovi kontaktojn kaj troviĝi mem, enigu alian identigan servilon sube.", "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "Vi nun ne uzas identigan servilon. Por trovi kontaktojn kaj troviĝi de ili mem, aldonu iun sube.", - "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Malkonektiĝo de via identiga servilo signifas, ke vi ne povos troviĝi de aliaj uzantoj, kaj vi ne povos memfare inviti aliajn per retpoŝto aŭ telefono.", - "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Vi ne devas uzi identigan servilon. Se vi tion elektos, vi ne povos troviĝi de aliaj uzantoj, kaj vi ne povos memfare inviti ilin per retpoŝto aŭ telefono.", + "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Malkonektiĝo de via identiga servilo signifas, ke vi ne povos troviĝi de aliaj uzantoj, kaj vi ne povos memage inviti aliajn per retpoŝto aŭ telefono.", + "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Vi ne devas uzi identigan servilon. Se vi tion elektos, vi ne povos troviĝi de aliaj uzantoj, kaj vi ne povos memage inviti ilin per retpoŝto aŭ telefono.", "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Konsentu al uzkondiĉoj de la identiga servilo (%(serverName)s) por esti trovita per retpoŝtadreso aŭ telefonnumero.", "Discovery": "Trovado", "Deactivate account": "Malaktivigi konton", @@ -1996,5 +1996,136 @@ "View": "Rigardo", "Find a room…": "Trovi ĉambron…", "If you can't find the room you're looking for, ask for an invite or Create a new room.": "Se vi ne povas travi la serĉatan ĉambron, petu inviton aŭ kreu novan ĉambron.", - "Explore rooms": "Esplori ĉambrojn" + "Explore rooms": "Esplori ĉambrojn", + "Add Email Address": "Aldoni retpoŝtadreson", + "Add Phone Number": "Aldoni telefonnumeron", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Ĉi tiu ago bezonas atingi la norman identigan servilon por kontroli retpoŝtadreson aŭ telefonnumeron, sed la servilo ne havas uzokondiĉojn.", + "Trust": "Fido", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "Multiple integration managers": "Pluraj kunigiloj", + "Use the new, consistent UserInfo panel for Room Members and Group Members": "Uzi la novan, koheran informan panelon por ĉambranoj kaj grupanoj", + "Send verification requests in direct message": "Sendi kontrolajn petojn per rekta mesaĝo", + "Use the new, faster, composer for writing messages": "Uzi la novan, pli rapidan verkilon de mesaĝoj", + "Show previews/thumbnails for images": "Montri antaŭrigardojn/bildetojn je bildoj", + "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "Vi forigu viajn personajn datumojn de identiga servilo antaŭ ol vi malkonektiĝos. Bedaŭrinde, identiga servilo estas nuntempe eksterreta kaj ne eblas ĝin atingi.", + "You should:": "Vi:", + "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "kontrolu kromprogramojn de via foliumilo je ĉio, kio povus malhelpi konekton al la identiga servilo (ekzemple « Privacy Badger »)", + "contact the administrators of identity server ": "kontaktu la administrantojn de la identiga servilo ", + "wait and try again later": "atendu kaj reprovu pli poste", + "Failed to update integration manager": "Malsukcesis ĝisdatigi kunigilon", + "Integration manager offline or not accessible.": "Kunigilo estas eksterreta aŭ alie neatingebla", + "Terms of service not accepted or the integration manager is invalid.": "Aŭ zokondiĉoj ne akceptiĝis, aŭ la kunigilo estas nevalida.", + "Integration manager has no terms of service": "Kunigilo havas neniujn uzokondiĉojn", + "The integration manager you have chosen does not have any terms of service.": "La elektita kunigilo havas neniujn uzokondiĉojn.", + "You are currently using %(serverName)s to manage your bots, widgets, and sticker packs.": "Vi nun mastrumas viajn robotojn, fenestaĵojn, kaj glumarkarojn per %(serverName)s.", + "Add which integration manager you want to manage your bots, widgets, and sticker packs.": "Aldonu kiun kunigilon vi volas por mastrumi viajn robotojn, fenestraĵojn, kaj glumarkarojn.", + "Integration Manager": "Kunigilo", + "Enter a new integration manager": "Metu novan kunigilon", + "Clear cache and reload": "Vakigi kaŝmemoron kaj relegi", + "Show tray icon and minimize window to it on close": "Montri pletan bildsimbolon kaj tien plejetigi la fenestron je fermo", + "Read Marker lifetime (ms)": "Vivodaŭro de legomarko (ms)", + "Read Marker off-screen lifetime (ms)": "Vivodaŭro de eksterekrana legomarko (ms)", + "Unable to revoke sharing for email address": "Ne povas senvalidigi havigadon je retpoŝtadreso", + "Unable to share email address": "Ne povas havigi vian retpoŝtadreson", + "Your email address hasn't been verified yet": "Via retpoŝtadreso ankoraŭ ne kontroliĝis", + "Click the link in the email you received to verify and then click continue again.": "Klaku la ligilon en la ricevita retletero por kontroli, kaj poste reklaku al « daŭrigi ».", + "Verify the link in your inbox": "Kontrolu la ligilon en via ricevujo.", + "Complete": "Fini", + "Revoke": "Senvalidigi", + "Share": "Havigi", + "Discovery options will appear once you have added an email above.": "Eltrovaj agordoj aperos kiam vi aldonos supre retpoŝtadreson.", + "Unable to revoke sharing for phone number": "Ne povas senvalidigi havigadon je telefonnumero", + "Unable to share phone number": "Ne povas havigi telefonnumeron", + "Please enter verification code sent via text.": "Bonvolu enigi kontrolan kodon senditan per tekstmesaĝo.", + "Discovery options will appear once you have added a phone number above.": "Eltrovaj agordoj aperos kiam vi aldonos telefonnumeron supre.", + "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "Tekstmesaĝo sendiĝis al +%(msisdn)s. Bonvolu enigi la kontrolan kodon enhavitan.", + "Try scrolling up in the timeline to see if there are any earlier ones.": "Provu rulumi supren tra la historio por kontroli, ĉu ne estas iuj pli fruaj.", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "Vi estas forigonta 1 mesaĝon de %(user)s. Ne eblas tion malfari. Ĉu vi volas pluigi?", + "Remove %(count)s messages|one": "Forigi 1 mesaĝon", + "Room %(name)s": "Ĉambro %(name)s", + "Recent rooms": "Freŝaj vizititaj ĉambroj", + "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to a room admin.": "Eraris (%(errcode)s) validigo de via invito. Vi povas transdoni ĉi tiun informon al ĉambra administranto.", + "This invite to %(roomName)s was sent to %(email)s which is not associated with your account": "Ĉi tiu invito al %(roomName)s sendiĝis al %(email)s, kiu ne estas ligita al via konto", + "Link this email with your account in Settings to receive invites directly in Riot.": "Ligu ĉi tiun retpoŝtadreson al via konto en Agordoj por ricevadi invitojn rekte per Riot.", + "This invite to %(roomName)s was sent to %(email)s": "La invito al %(roomName)s sendiĝis al %(email)s", + "Use an identity server in Settings to receive invites directly in Riot.": "Uzu identigan servilon en Agordoj por ricevadi invitojn rekte per Riot.", + "Share this email in Settings to receive invites directly in Riot.": "Havigu ĉi tiun retpoŝtadreson per Agordoj por ricevadi invitojn rekte per Riot.", + "%(count)s unread messages including mentions.|other": "%(count)s nelegitaj mesaĝoj, inkluzive menciojn.", + "%(count)s unread messages including mentions.|one": "1 nelegita mencio.", + "%(count)s unread messages.|other": "%(count)s nelegitaj mesaĝoj.", + "%(count)s unread messages.|one": "1 nelegita mesaĝo.", + "Unread mentions.": "Nelegitaj mencioj.", + "Unread messages.": "Nelegitaj mesaĝoj.", + "Trust & Devices": "Fido kaj Aparatoj", + "Direct messages": "Rektaj mesaĝoj", + "Failed to deactivate user": "Malsukcesis malaktivigi uzanton", + "This client does not support end-to-end encryption.": "Ĉi tiu kliento ne subtenas tutvojan ĉifradon.", + "Messages in this room are not end-to-end encrypted.": "Mesaĝoj en ĉi tiu ĉambro ne estas tutvoje ĉifrataj.", + "React": "Reagi", + "Message Actions": "Mesaĝaj agoj", + "Show image": "Montri bildon", + "You verified %(name)s": "Vi kontrolis %(name)s", + "You cancelled verifying %(name)s": "Vi nuligis kontrolon de %(name)s", + "%(name)s cancelled verifying": "%(name)s nuligis kontrolon", + "You accepted": "Vi akceptis", + "%(name)s accepted": "%(name)s akceptis", + "You cancelled": "Vi nuligis", + "%(name)s cancelled": "%(name)s nuligis", + "%(name)s wants to verify": "%(name)s volas kontroli", + "You sent a verification request": "Vi sendis peton de kontrolo", + "Frequently Used": "Ofte uzataj", + "Smileys & People": "Mienoj kaj homoj", + "Animals & Nature": "Bestoj kaj naturo", + "Food & Drink": "Manĝaĵoj kaj trinkaĵoj", + "Activities": "Agadoj", + "Travel & Places": "Lokoj kaj vojaĝado", + "Objects": "Aĵoj", + "Symbols": "Simboloj", + "Flags": "Flagoj", + "Quick Reactions": "Rapidaj reagoj", + "Cancel search": "Nuligi serĉon", + "Please create a new issue on GitHub so that we can investigate this bug.": "Bonvolu raporti novan problemon je GitHub, por ke ni povu ĝin esplori.", + "Room alias": "Kromnomo de ĉambro", + "e.g. my-room": "ekzemple mia-chambro", + "Please provide a room alias": "Bonvolu doni kromnomon de ĉambro", + "This alias is available to use": "Ĉi tiu kromnomo estas disponebla", + "This alias is already in use": "Ĉi tiu kromnomo jam estas uzata", + "Use an identity server to invite by email. Use the default (%(defaultIdentityServerName)s) or manage in Settings.": "Uzu identigan servilon por inviti per retpoŝto. Uzu la norman (%(defaultIdentityServerName)s) aŭ mastrumu per Agordoj.", + "Use an identity server to invite by email. Manage in Settings.": "Uzu identigan servilon por inviti per retpoŝto. Mastrumu per Agordoj.", + "Close dialog": "Fermi interagujon", + "Please enter a name for the room": "Bonvolu enigi nomon por la ĉambro", + "Set a room alias to easily share your room with other people.": "Agordu kromnomon de la ĉambro por facile ĝin kunhavigi al aliaj homoj.", + "This room is private, and can only be joined by invitation.": "Ĉi tiu ĉambro estas privata, kaj eblas aliĝi nur per invito.", + "Create a public room": "Krei publikan ĉambron", + "Create a private room": "Krei privatan ĉambron", + "Topic (optional)": "Temo (malnepra)", + "Make this room public": "Publikigi ĉi tiun ĉambron", + "Hide advanced": "Kaŝi specialajn", + "Show advanced": "Montri specialajn", + "Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)": "Bloki aliĝojn al ĉi tiu ĉambro de uzantoj el aliaj Matrix-serviloj (Ĉi tiun agordon ne eblas poste ŝanĝi!)", + "To verify that this device can be trusted, please check that the key you see in User Settings on that device matches the key below:": "Por kontroli ke tiu ĉi aparato estas fidinda, bonvolu kontroli, ke la ŝlosilo, kiun vi vidas en viaj Agordoj de uzanto je tiu aparato, akordas kun la ŝlosilo sube:", + "Please fill why you're reporting.": "Bonvolu skribi, kial vi raportas.", + "Report Content to Your Homeserver Administrator": "Raporti enhavon al la administrantode via hejmservilo", + "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "Per raporto de ĝi tiu mesaĝo vi sendos ĝian unikan « eventan identigilon » al la administranto de via hejmservilo. Se mesaĝoj en ĉi tiu ĉambro estas ĉifrataj, la administranto de via hejmservilo ne povos legi la tekston de la mesaĝo, nek rigardi dosierojn aŭ bildojn.", + "Send report": "Sendi raporton", + "Command Help": "Helpo pri komando", + "Integrations Manager": "Kunigilo", + "To continue you need to accept the terms of this service.": "Por pluigi, vi devas akcepti la uzokondiĉojn de ĉi tiu servo.", + "Document": "Dokumento", + "Report Content": "Raporti enhavon", + "No identity server is configured so you cannot add an email address in order to reset your password in the future.": "Neniu identiga servilo estas agordita, kaj tial vi ne povas aldoni retpoŝtadreson por ose restarigi vian pasvorton.", + "Enter your custom homeserver URL What does this mean?": "Enigu vian propran hejmservilan URL-on. Kion tio signifas?", + "Enter your custom identity server URL What does this mean?": "Enigu vian propran URL-on de identiga servilo. Kion tio signifas?", + "%(creator)s created and configured the room.": "%(creator)s kreis kaj agordis la ĉambron.", + "Find a room… (e.g. %(exampleRoom)s)": "Trovi ĉambron… (ekzemple (%(exampleRoom)s)", + "Jump to first unread room.": "Salti al unua nelegita ĉambro.", + "Jump to first invite.": "Salti al unua invito.", + "No identity server is configured: add one in server settings to reset your password.": "Neniu identiga servilo estas agordita: aldonu iun per la servilaj agordoj, por restarigi vian pasvorton.", + "Command Autocomplete": "Memkompletigo de komandoj", + "Community Autocomplete": "Memkompletigo de komunumoj", + "DuckDuckGo Results": "Rezultoj de DuckDuckGo", + "Emoji Autocomplete": "Memkompletigo de mienetoj", + "Notification Autocomplete": "Memkompletigo de sciigoj", + "Room Autocomplete": "Memkompletigo de ĉambroj", + "User Autocomplete": "Memkompletigo de uzantoj" } From d545a1e0b2655a55766ef9bb3016830536ea3b0b Mon Sep 17 00:00:00 2001 From: Szimszon Date: Sun, 10 Nov 2019 12:40:40 +0000 Subject: [PATCH 042/334] Translated using Weblate (Hungarian) Currently translated at 100.0% (1864 of 1864 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 | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 5d21a17e37..67af8a6a57 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2267,5 +2267,16 @@ "Show tray icon and minimize window to it on close": "Tálcaikon mutatása és az ablak összecsukása bezáráskor", "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Ez a művelet az e-mail cím vagy telefonszám ellenőrzése miatt hozzáférést igényel az alapértelmezett azonosítási szerverhez (), de a szervernek nincsen semmilyen felhasználási feltétele.", "Trust": "Megbízom benne", - "Message Actions": "Üzenet Műveletek" + "Message Actions": "Üzenet Műveletek", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "Send verification requests in direct message": "Ellenőrzés kérés küldése közvetlen üzenetben", + "You verified %(name)s": "Ellenőrizted: %(name)s", + "You cancelled verifying %(name)s": "Az ellenőrzést megszakítottad ehhez: %(name)s", + "%(name)s cancelled verifying": "%(name)s megszakította az ellenőrzést", + "You accepted": "Elfogadtad", + "%(name)s accepted": "%(name)s elfogadta", + "You cancelled": "Megszakítottad", + "%(name)s cancelled": "%(name)s megszakította", + "%(name)s wants to verify": "%(name)s ellenőrizni szeretné", + "You sent a verification request": "Ellenőrzési kérést küldtél" } From ab9f3780194637574b28f97c4f36719b054edee3 Mon Sep 17 00:00:00 2001 From: Elwyn Malethan Date: Sat, 9 Nov 2019 19:01:03 +0000 Subject: [PATCH 043/334] Translated using Weblate (Welsh) Currently translated at 0.5% (9 of 1864 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/cy/ --- src/i18n/strings/cy.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/cy.json b/src/i18n/strings/cy.json index 0967ef424b..951468c747 100644 --- a/src/i18n/strings/cy.json +++ b/src/i18n/strings/cy.json @@ -1 +1,11 @@ -{} +{ + "This email address is already in use": "Mae'r cyfeiriad e-bost yma yn cael ei ddefnyddio eisoes", + "This phone number is already in use": "Mae'r rhif ffôn yma yn cael ei ddefnyddio eisoes", + "Add Email Address": "Ychwanegu Cyfeiriad E-bost", + "Failed to verify email address: make sure you clicked the link in the email": "Methiant gwirio cyfeiriad e-bost: gwnewch yn siŵr eich bod wedi clicio'r ddolen yn yr e-bost", + "Add Phone Number": "Ychwanegu Rhif Ffôn", + "The platform you're on": "Y platfform rydych chi arno", + "The version of Riot.im": "Fersiwn Riot.im", + "Whether or not you're logged in (we don't record your username)": "Os ydych wedi mewngofnodi ai peidio (nid ydym yn cofnodi'ch enw defnyddiwr)", + "Your language of choice": "Eich iaith o ddewis" +} From ef0529413308504b6fde2a1d177a6db00c98dfac Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 11 Nov 2019 10:24:40 +0000 Subject: [PATCH 044/334] Fix draw order when hovering composer format buttons This ensures all 4 sides of a button show the hover border colour as intended. Another part of https://github.com/vector-im/riot-web/issues/11203 --- res/css/views/rooms/_MessageComposerFormatBar.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/rooms/_MessageComposerFormatBar.scss b/res/css/views/rooms/_MessageComposerFormatBar.scss index 80909529ee..1b5a21bed0 100644 --- a/res/css/views/rooms/_MessageComposerFormatBar.scss +++ b/res/css/views/rooms/_MessageComposerFormatBar.scss @@ -40,6 +40,7 @@ limitations under the License. &:hover { border-color: $message-action-bar-hover-border-color; + z-index: 1; } &:first-child { From 2c5565e5020edfe0306836fb37d49a8f410df2ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 18 Oct 2019 16:36:31 +0200 Subject: [PATCH 045/334] MatrixChat: Add some missing semicolons. --- src/components/structures/MatrixChat.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 51cf92da5f..402790df98 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -2130,13 +2130,13 @@ export default createReactClass({ checkpoint.roomId, checkpoint.token, 100, checkpoint.direction); } catch (e) { - console.log("Seshat: Error crawling events:", e) + console.log("Seshat: Error crawling events:", e); this.crawlerChekpoints.push(checkpoint); continue } if (res.chunk.length === 0) { - console.log("Seshat: Done with the checkpoint", checkpoint) + console.log("Seshat: Done with the checkpoint", checkpoint); // We got to the start/end of our timeline, lets just // delete our checkpoint and go back to sleep. await platform.removeCrawlerCheckpoint(checkpoint); @@ -2239,7 +2239,8 @@ export default createReactClass({ // Let us delete the checkpoint in that case, otherwise push // the new checkpoint to be used by the crawler. if (eventsAlreadyAdded === true && newCheckpoint.fullCrawl !== true) { - console.log("Seshat: Checkpoint had already all events added, stopping the crawl", checkpoint); + console.log("Seshat: Checkpoint had already all events", + "added, stopping the crawl", checkpoint); await platform.removeCrawlerCheckpoint(newCheckpoint); } else { this.crawlerChekpoints.push(newCheckpoint); From 44401d73b44b6de4daeb91613456d2760d50456d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 12 Nov 2019 11:40:38 +0000 Subject: [PATCH 046/334] Replace all trivial Promise.defer usages with regular Promises --- src/ContentMessages.js | 174 +++++++++--------- src/Lifecycle.js | 8 +- src/components/views/rooms/Autocomplete.js | 29 ++- .../views/settings/ChangePassword.js | 9 +- src/rageshake/submit-rageshake.js | 40 ++-- 5 files changed, 122 insertions(+), 138 deletions(-) diff --git a/src/ContentMessages.js b/src/ContentMessages.js index 2d58622db8..dab8de2465 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -59,40 +59,38 @@ export class UploadCanceledError extends Error {} * and a thumbnail key. */ function createThumbnail(element, inputWidth, inputHeight, mimeType) { - const deferred = Promise.defer(); + return new Promise((resolve) => { + let targetWidth = inputWidth; + let targetHeight = inputHeight; + if (targetHeight > MAX_HEIGHT) { + targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight)); + targetHeight = MAX_HEIGHT; + } + if (targetWidth > MAX_WIDTH) { + targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth)); + targetWidth = MAX_WIDTH; + } - let targetWidth = inputWidth; - let targetHeight = inputHeight; - if (targetHeight > MAX_HEIGHT) { - targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight)); - targetHeight = MAX_HEIGHT; - } - if (targetWidth > MAX_WIDTH) { - targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth)); - targetWidth = MAX_WIDTH; - } - - const canvas = document.createElement("canvas"); - canvas.width = targetWidth; - canvas.height = targetHeight; - canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight); - canvas.toBlob(function(thumbnail) { - deferred.resolve({ - info: { - thumbnail_info: { - w: targetWidth, - h: targetHeight, - mimetype: thumbnail.type, - size: thumbnail.size, + const canvas = document.createElement("canvas"); + canvas.width = targetWidth; + canvas.height = targetHeight; + canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight); + canvas.toBlob(function(thumbnail) { + resolve({ + info: { + thumbnail_info: { + w: targetWidth, + h: targetHeight, + mimetype: thumbnail.type, + size: thumbnail.size, + }, + w: inputWidth, + h: inputHeight, }, - w: inputWidth, - h: inputHeight, - }, - thumbnail: thumbnail, - }); - }, mimeType); - - return deferred.promise; + thumbnail: thumbnail, + }); + }, mimeType); + }); } /** @@ -179,30 +177,29 @@ function infoForImageFile(matrixClient, roomId, imageFile) { * @return {Promise} A promise that resolves with the video image element. */ function loadVideoElement(videoFile) { - const deferred = Promise.defer(); + return new Promise((resolve, reject) => { + // Load the file into an html element + const video = document.createElement("video"); - // Load the file into an html element - const video = document.createElement("video"); + const reader = new FileReader(); - const reader = new FileReader(); - reader.onload = function(e) { - video.src = e.target.result; + reader.onload = function(e) { + video.src = e.target.result; - // Once ready, returns its size - // Wait until we have enough data to thumbnail the first frame. - video.onloadeddata = function() { - deferred.resolve(video); + // Once ready, returns its size + // Wait until we have enough data to thumbnail the first frame. + video.onloadeddata = function() { + resolve(video); + }; + video.onerror = function(e) { + reject(e); + }; }; - video.onerror = function(e) { - deferred.reject(e); + reader.onerror = function(e) { + reject(e); }; - }; - reader.onerror = function(e) { - deferred.reject(e); - }; - reader.readAsDataURL(videoFile); - - return deferred.promise; + reader.readAsDataURL(videoFile); + }); } /** @@ -236,16 +233,16 @@ function infoForVideoFile(matrixClient, roomId, videoFile) { * is read. */ function readFileAsArrayBuffer(file) { - const deferred = Promise.defer(); - const reader = new FileReader(); - reader.onload = function(e) { - deferred.resolve(e.target.result); - }; - reader.onerror = function(e) { - deferred.reject(e); - }; - reader.readAsArrayBuffer(file); - return deferred.promise; + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = function(e) { + resolve(e.target.result); + }; + reader.onerror = function(e) { + reject(e); + }; + reader.readAsArrayBuffer(file); + }); } /** @@ -461,33 +458,34 @@ export default class ContentMessages { content.info.mimetype = file.type; } - const def = Promise.defer(); - if (file.type.indexOf('image/') == 0) { - content.msgtype = 'm.image'; - infoForImageFile(matrixClient, roomId, file).then((imageInfo)=>{ - extend(content.info, imageInfo); - def.resolve(); - }, (error)=>{ - console.error(error); + const prom = new Promise((resolve) => { + if (file.type.indexOf('image/') == 0) { + content.msgtype = 'm.image'; + infoForImageFile(matrixClient, roomId, file).then((imageInfo)=>{ + extend(content.info, imageInfo); + resolve(); + }, (error)=>{ + console.error(error); + content.msgtype = 'm.file'; + resolve(); + }); + } else if (file.type.indexOf('audio/') == 0) { + content.msgtype = 'm.audio'; + resolve(); + } else if (file.type.indexOf('video/') == 0) { + content.msgtype = 'm.video'; + infoForVideoFile(matrixClient, roomId, file).then((videoInfo)=>{ + extend(content.info, videoInfo); + resolve(); + }, (error)=>{ + content.msgtype = 'm.file'; + resolve(); + }); + } else { content.msgtype = 'm.file'; - def.resolve(); - }); - } else if (file.type.indexOf('audio/') == 0) { - content.msgtype = 'm.audio'; - def.resolve(); - } else if (file.type.indexOf('video/') == 0) { - content.msgtype = 'm.video'; - infoForVideoFile(matrixClient, roomId, file).then((videoInfo)=>{ - extend(content.info, videoInfo); - def.resolve(); - }, (error)=>{ - content.msgtype = 'm.file'; - def.resolve(); - }); - } else { - content.msgtype = 'm.file'; - def.resolve(); - } + resolve(); + } + }); const upload = { fileName: file.name || 'Attachment', @@ -509,7 +507,7 @@ export default class ContentMessages { dis.dispatch({action: 'upload_progress', upload: upload}); } - return def.promise.then(function() { + return prom.then(function() { // XXX: upload.promise must be the promise that // is returned by uploadFile as it has an abort() // method hacked onto it. diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 13f3abccb1..53a9b7a998 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -312,18 +312,14 @@ async function _restoreFromLocalStorage(opts) { function _handleLoadSessionFailure(e) { console.error("Unable to load session", e); - const def = Promise.defer(); const SessionRestoreErrorDialog = sdk.getComponent('views.dialogs.SessionRestoreErrorDialog'); - Modal.createTrackedDialog('Session Restore Error', '', SessionRestoreErrorDialog, { + const modal = Modal.createTrackedDialog('Session Restore Error', '', SessionRestoreErrorDialog, { error: e.message, - onFinished: (success) => { - def.resolve(success); - }, }); - return def.promise.then((success) => { + return modal.finished.then(([success]) => { if (success) { // user clicked continue. _clearStorage(); diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index ad5fa198a3..d4b51081f4 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -26,6 +26,7 @@ import { Room } from 'matrix-js-sdk'; import SettingsStore from "../../../settings/SettingsStore"; import Autocompleter from '../../../autocomplete/Autocompleter'; +import {sleep} from "../../../utils/promise"; const COMPOSER_SELECTED = 0; @@ -105,13 +106,11 @@ export default class Autocomplete extends React.Component { autocompleteDelay = 0; } - const deferred = Promise.defer(); - this.debounceCompletionsRequest = setTimeout(() => { - this.processQuery(query, selection).then(() => { - deferred.resolve(); - }); - }, autocompleteDelay); - return deferred.promise; + return new Promise((resolve) => { + this.debounceCompletionsRequest = setTimeout(() => { + resolve(this.processQuery(query, selection)); + }, autocompleteDelay); + }); } processQuery(query, selection) { @@ -197,16 +196,16 @@ export default class Autocomplete extends React.Component { } forceComplete() { - const done = Promise.defer(); - this.setState({ - forceComplete: true, - hide: false, - }, () => { - this.complete(this.props.query, this.props.selection).then(() => { - done.resolve(this.countCompletions()); + return new Promise((resolve) => { + this.setState({ + forceComplete: true, + hide: false, + }, () => { + this.complete(this.props.query, this.props.selection).then(() => { + resolve(this.countCompletions()); + }); }); }); - return done.promise; } onCompletionClicked(selectionOffset: number): boolean { diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index a086efaa6d..91292b19f9 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -178,17 +178,12 @@ module.exports = createReactClass({ }, _optionallySetEmail: function() { - const deferred = Promise.defer(); // Ask for an email otherwise the user has no way to reset their password const SetEmailDialog = sdk.getComponent("dialogs.SetEmailDialog"); - Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, { + const modal = Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, { title: _t('Do you want to set an email address?'), - onFinished: (confirmed) => { - // ignore confirmed, setting an email is optional - deferred.resolve(confirmed); - }, }); - return deferred.promise; + return modal.finished.then(([confirmed]) => confirmed); }, _onExportE2eKeysClicked: function() { diff --git a/src/rageshake/submit-rageshake.js b/src/rageshake/submit-rageshake.js index 99c412a6ab..e772912e48 100644 --- a/src/rageshake/submit-rageshake.js +++ b/src/rageshake/submit-rageshake.js @@ -105,26 +105,22 @@ export default async function sendBugReport(bugReportEndpoint, opts) { } function _submitReport(endpoint, body, progressCallback) { - const deferred = Promise.defer(); - - const req = new XMLHttpRequest(); - req.open("POST", endpoint); - req.timeout = 5 * 60 * 1000; - req.onreadystatechange = function() { - if (req.readyState === XMLHttpRequest.LOADING) { - progressCallback(_t("Waiting for response from server")); - } else if (req.readyState === XMLHttpRequest.DONE) { - on_done(); - } - }; - req.send(body); - return deferred.promise; - - function on_done() { - if (req.status < 200 || req.status >= 400) { - deferred.reject(new Error(`HTTP ${req.status}`)); - return; - } - deferred.resolve(); - } + return new Promise((resolve, reject) => { + const req = new XMLHttpRequest(); + req.open("POST", endpoint); + req.timeout = 5 * 60 * 1000; + req.onreadystatechange = function() { + if (req.readyState === XMLHttpRequest.LOADING) { + progressCallback(_t("Waiting for response from server")); + } else if (req.readyState === XMLHttpRequest.DONE) { + // on done + if (req.status < 200 || req.status >= 400) { + reject(new Error(`HTTP ${req.status}`)); + return; + } + resolve(); + } + }; + req.send(body); + }); } From 6850c147393ba7be8b98d97dfc8d7244fa503461 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 12 Nov 2019 11:45:28 +0000 Subject: [PATCH 047/334] Replace rest of defer usages using small shim. Add homebrew promise utils --- src/Modal.js | 3 +- src/components/structures/MatrixChat.js | 5 +-- src/utils/MultiInviter.js | 3 +- src/utils/promise.js | 46 +++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 src/utils/promise.js diff --git a/src/Modal.js b/src/Modal.js index 26c9da8bbb..cb19731f01 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -24,6 +24,7 @@ import sdk from './index'; import dis from './dispatcher'; import { _t } from './languageHandler'; import Promise from "bluebird"; +import {defer} from "./utils/promise"; const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer"; @@ -202,7 +203,7 @@ class ModalManager { } _getCloseFn(modal, props) { - const deferred = Promise.defer(); + const deferred = defer(); return [(...args) => { deferred.resolve(args); if (props && props.onFinished) props.onFinished.apply(null, args); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index da67416400..d12eba88f7 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -60,6 +60,7 @@ import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils"; import DMRoomMap from '../../utils/DMRoomMap'; import { countRoomsWithNotif } from '../../RoomNotifs'; import { setTheme } from "../../theme"; +import {defer} from "../../utils/promise"; // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. @@ -236,7 +237,7 @@ export default createReactClass({ // Used by _viewRoom before getting state from sync this.firstSyncComplete = false; - this.firstSyncPromise = Promise.defer(); + this.firstSyncPromise = defer(); if (this.props.config.sync_timeline_limit) { MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit; @@ -1261,7 +1262,7 @@ export default createReactClass({ // since we're about to start the client and therefore about // to do the first sync this.firstSyncComplete = false; - this.firstSyncPromise = Promise.defer(); + this.firstSyncPromise = defer(); const cli = MatrixClientPeg.get(); const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog'); diff --git a/src/utils/MultiInviter.js b/src/utils/MultiInviter.js index e8995b46d7..de5c2e7610 100644 --- a/src/utils/MultiInviter.js +++ b/src/utils/MultiInviter.js @@ -24,6 +24,7 @@ import {_t} from "../languageHandler"; import sdk from "../index"; import Modal from "../Modal"; import SettingsStore from "../settings/SettingsStore"; +import {defer} from "./promise"; /** * Invites multiple addresses to a room or group, handling rate limiting from the server @@ -71,7 +72,7 @@ export default class MultiInviter { }; } } - this.deferred = Promise.defer(); + this.deferred = defer(); this._inviteMore(0); return this.deferred.promise; diff --git a/src/utils/promise.js b/src/utils/promise.js new file mode 100644 index 0000000000..dd10f7fdd7 --- /dev/null +++ b/src/utils/promise.js @@ -0,0 +1,46 @@ +/* +Copyright 2019 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. +*/ + +// @flow + +// Returns a promise which resolves with a given value after the given number of ms +export const sleep = (ms: number, value: any): Promise => new Promise((resolve => { setTimeout(resolve, ms, value); })); + +// Returns a promise which resolves when the input promise resolves with its value +// or when the timeout of ms is reached with the value of given timeoutValue +export async function timeout(promise: Promise, timeoutValue: any, ms: number): Promise { + const timeoutPromise = new Promise((resolve) => { + const timeoutId = setTimeout(resolve, ms, timeoutValue); + promise.then(() => { + clearTimeout(timeoutId); + }); + }); + + return Promise.race([promise, timeoutPromise]); +} + +// Returns a Deferred +export function defer(): {resolve: () => {}, reject: () => {}, promise: Promise} { + let resolve; + let reject; + + const promise = new Promise((_resolve, _reject) => { + resolve = _resolve; + reject = _reject; + }); + + return {resolve, reject, promise}; +} From 0a21957b2cac0cc2d0169f428187bc2e468251a9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 12 Nov 2019 11:46:58 +0000 Subject: [PATCH 048/334] Replace Promise.delay with promise utils sleep --- src/components/structures/GroupView.js | 9 +++++---- .../views/context_menus/RoomTileContextMenu.js | 7 ++++--- src/components/views/dialogs/AddressPickerDialog.js | 3 ++- .../views/settings/tabs/user/SecurityUserSettingsTab.js | 3 ++- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 4d8f47003c..4056557a7c 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -38,6 +38,7 @@ import FlairStore from '../../stores/FlairStore'; import { showGroupAddRoomDialog } from '../../GroupAddressPicker'; import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks"; import {Group} from "matrix-js-sdk"; +import {sleep} from "../../utils/promise"; const LONG_DESC_PLACEHOLDER = _td( `

    HTML for your community's page

    @@ -692,7 +693,7 @@ export default createReactClass({ // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the // spinner disappearing after we have fetched new group data. - await Promise.delay(500); + await sleep(500); GroupStore.acceptGroupInvite(this.props.groupId).then(() => { // don't reset membershipBusy here: wait for the membership change to come down the sync @@ -711,7 +712,7 @@ export default createReactClass({ // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the // spinner disappearing after we have fetched new group data. - await Promise.delay(500); + await sleep(500); GroupStore.leaveGroup(this.props.groupId).then(() => { // don't reset membershipBusy here: wait for the membership change to come down the sync @@ -735,7 +736,7 @@ export default createReactClass({ // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the // spinner disappearing after we have fetched new group data. - await Promise.delay(500); + await sleep(500); GroupStore.joinGroup(this.props.groupId).then(() => { // don't reset membershipBusy here: wait for the membership change to come down the sync @@ -787,7 +788,7 @@ export default createReactClass({ // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the // spinner disappearing after we have fetched new group data. - await Promise.delay(500); + await sleep(500); GroupStore.leaveGroup(this.props.groupId).then(() => { // don't reset membershipBusy here: wait for the membership change to come down the sync diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js index 9bb573026f..541daef27f 100644 --- a/src/components/views/context_menus/RoomTileContextMenu.js +++ b/src/components/views/context_menus/RoomTileContextMenu.js @@ -32,6 +32,7 @@ import * as RoomNotifs from '../../../RoomNotifs'; import Modal from '../../../Modal'; import RoomListActions from '../../../actions/RoomListActions'; import RoomViewStore from '../../../stores/RoomViewStore'; +import {sleep} from "../../../utils/promise"; module.exports = createReactClass({ displayName: 'RoomTileContextMenu', @@ -62,7 +63,7 @@ module.exports = createReactClass({ _toggleTag: function(tagNameOn, tagNameOff) { if (!MatrixClientPeg.get().isGuest()) { - Promise.delay(500).then(() => { + sleep(500).then(() => { dis.dispatch(RoomListActions.tagRoom( MatrixClientPeg.get(), this.props.room, @@ -119,7 +120,7 @@ module.exports = createReactClass({ Rooms.guessAndSetDMRoom( this.props.room, newIsDirectMessage, - ).delay(500).finally(() => { + ).then(sleep(500)).finally(() => { // Close the context menu if (this.props.onFinished) { this.props.onFinished(); @@ -193,7 +194,7 @@ module.exports = createReactClass({ RoomNotifs.setRoomNotifsState(roomId, newState).done(() => { // delay slightly so that the user can see their state change // before closing the menu - return Promise.delay(500).then(() => { + return sleep(500).then(() => { if (this._unmounted) return; // Close the context menu if (this.props.onFinished) { diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index fb779fa96f..dc61f23956 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -32,6 +32,7 @@ import * as Email from '../../../email'; import IdentityAuthClient from '../../../IdentityAuthClient'; import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../utils/IdentityServerUtils'; import { abbreviateUrl } from '../../../utils/UrlUtils'; +import {sleep} from "../../../utils/promise"; const TRUNCATE_QUERY_LIST = 40; const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200; @@ -533,7 +534,7 @@ module.exports = createReactClass({ }; // wait a bit to let the user finish typing - await Promise.delay(500); + await sleep(500); if (cancelled) return null; try { diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js index e619791b01..222af48fa1 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js @@ -25,6 +25,7 @@ import Analytics from "../../../../../Analytics"; import Promise from "bluebird"; import Modal from "../../../../../Modal"; import sdk from "../../../../.."; +import {sleep} from "../../../../../utils/promise"; export class IgnoredUser extends React.Component { static propTypes = { @@ -129,7 +130,7 @@ export default class SecurityUserSettingsTab extends React.Component { if (e.errcode === "M_LIMIT_EXCEEDED") { // Add a delay between each invite change in order to avoid rate // limiting by the server. - await Promise.delay(e.retry_after_ms || 2500); + await sleep(e.retry_after_ms || 2500); // Redo last action i--; From 09a8fec26187f12fe9b2d8d201c4b4fcb86af82a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 12 Nov 2019 11:51:23 +0000 Subject: [PATCH 049/334] s/.done(/.then(/ since modern es6 track unhandled promise exceptions --- src/Lifecycle.js | 4 ++-- src/Notifier.js | 2 +- src/Resend.js | 2 +- src/ScalarMessaging.js | 8 ++++---- src/components/structures/GroupView.js | 4 ++-- src/components/structures/InteractiveAuth.js | 2 +- src/components/structures/MatrixChat.js | 8 ++++---- src/components/structures/MyGroups.js | 2 +- src/components/structures/RoomDirectory.js | 8 ++++---- src/components/structures/RoomView.js | 8 ++++---- src/components/structures/TimelinePanel.js | 4 ++-- src/components/structures/auth/ForgotPassword.js | 2 +- src/components/structures/auth/Login.js | 4 ++-- src/components/structures/auth/PostRegistration.js | 2 +- src/components/structures/auth/Registration.js | 2 +- .../views/auth/InteractiveAuthEntryComponents.js | 2 +- .../views/context_menus/RoomTileContextMenu.js | 4 ++-- .../views/dialogs/AddressPickerDialog.js | 4 ++-- src/components/views/dialogs/CreateGroupDialog.js | 2 +- src/components/views/dialogs/KeyShareDialog.js | 2 +- src/components/views/dialogs/SetEmailDialog.js | 4 ++-- src/components/views/elements/AppTile.js | 2 +- .../views/elements/EditableTextContainer.js | 4 ++-- src/components/views/elements/ErrorBoundary.js | 2 +- src/components/views/elements/ImageView.js | 2 +- src/components/views/elements/LanguageDropdown.js | 2 +- src/components/views/groups/GroupUserSettings.js | 2 +- src/components/views/messages/MAudioBody.js | 2 +- src/components/views/messages/MImageBody.js | 2 +- src/components/views/messages/MVideoBody.js | 2 +- src/components/views/right_panel/UserInfo.js | 2 +- src/components/views/rooms/LinkPreviewWidget.js | 2 +- src/components/views/rooms/MemberInfo.js | 6 +++--- src/components/views/settings/ChangeAvatar.js | 2 +- src/components/views/settings/ChangePassword.js | 2 +- src/components/views/settings/DevicesPanel.js | 2 +- src/components/views/settings/Notifications.js | 14 +++++++------- .../settings/tabs/user/HelpUserSettingsTab.js | 2 +- src/stores/RoomViewStore.js | 4 ++-- .../views/dialogs/InteractiveAuthDialog-test.js | 3 ++- .../views/elements/MemberEventListSummary-test.js | 2 +- .../views/rooms/MessageComposerInput-test.js | 3 ++- test/i18n-test/languageHandler-test.js | 2 +- 43 files changed, 74 insertions(+), 72 deletions(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 53a9b7a998..9bada98168 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -524,7 +524,7 @@ export function logout() { console.log("Failed to call logout API: token will not be invalidated"); onLoggedOut(); }, - ).done(); + ).then(); } export function softLogout() { @@ -608,7 +608,7 @@ export function onLoggedOut() { // that can occur when components try to use a null client. dis.dispatch({action: 'on_logged_out'}, true); stopMatrixClient(); - _clearStorage().done(); + _clearStorage().then(); } /** diff --git a/src/Notifier.js b/src/Notifier.js index cca0ea2b89..edb9850dfe 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -198,7 +198,7 @@ const Notifier = { if (enable) { // Attempt to get permission from user - plaf.requestNotificationPermission().done((result) => { + plaf.requestNotificationPermission().then((result) => { if (result !== 'granted') { // The permission request was dismissed or denied // TODO: Support alternative branding in messaging diff --git a/src/Resend.js b/src/Resend.js index 4eaee16d1b..51ec804c01 100644 --- a/src/Resend.js +++ b/src/Resend.js @@ -35,7 +35,7 @@ module.exports = { }, resend: function(event) { const room = MatrixClientPeg.get().getRoom(event.getRoomId()); - MatrixClientPeg.get().resendEvent(event, room).done(function(res) { + MatrixClientPeg.get().resendEvent(event, room).then(function(res) { dis.dispatch({ action: 'message_sent', event: event, diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index 910a6c4f13..c0ffc3022d 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -279,7 +279,7 @@ function inviteUser(event, roomId, userId) { } } - client.invite(roomId, userId).done(function() { + client.invite(roomId, userId).then(function() { sendResponse(event, { success: true, }); @@ -398,7 +398,7 @@ function setPlumbingState(event, roomId, status) { sendError(event, _t('You need to be logged in.')); return; } - client.sendStateEvent(roomId, "m.room.plumbing", { status: status }).done(() => { + client.sendStateEvent(roomId, "m.room.plumbing", { status: status }).then(() => { sendResponse(event, { success: true, }); @@ -414,7 +414,7 @@ function setBotOptions(event, roomId, userId) { sendError(event, _t('You need to be logged in.')); return; } - client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).done(() => { + client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).then(() => { sendResponse(event, { success: true, }); @@ -444,7 +444,7 @@ function setBotPower(event, roomId, userId, level) { }, ); - client.setPowerLevel(roomId, userId, level, powerEvent).done(() => { + client.setPowerLevel(roomId, userId, level, powerEvent).then(() => { sendResponse(event, { success: true, }); diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 4056557a7c..b97d76d72a 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -639,7 +639,7 @@ export default createReactClass({ title: _t('Error'), description: _t('Failed to upload image'), }); - }).done(); + }).then(); }, _onJoinableChange: function(ev) { @@ -678,7 +678,7 @@ export default createReactClass({ this.setState({ avatarChanged: false, }); - }).done(); + }).then(); }, _saveGroup: async function() { diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index 5e06d124c4..86fc351515 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -121,7 +121,7 @@ export default createReactClass({ this.setState({ errorText: msg, }); - }).done(); + }).then(); this._intervalId = null; if (this.props.poll) { diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index d12eba88f7..c7bf2f181f 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -541,7 +541,7 @@ export default createReactClass({ const Loader = sdk.getComponent("elements.Spinner"); const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); - MatrixClientPeg.get().leave(payload.room_id).done(() => { + MatrixClientPeg.get().leave(payload.room_id).then(() => { modal.close(); if (this.state.currentRoomId === payload.room_id) { dis.dispatch({action: 'view_next_room'}); @@ -862,7 +862,7 @@ export default createReactClass({ waitFor = this.firstSyncPromise.promise; } - waitFor.done(() => { + waitFor.then(() => { let presentedId = roomInfo.room_alias || roomInfo.room_id; const room = MatrixClientPeg.get().getRoom(roomInfo.room_id); if (room) { @@ -974,7 +974,7 @@ export default createReactClass({ const [shouldCreate, createOpts] = await modal.finished; if (shouldCreate) { - createRoom({createOpts}).done(); + createRoom({createOpts}).then(); } }, @@ -1750,7 +1750,7 @@ export default createReactClass({ return; } - cli.sendEvent(roomId, event.getType(), event.getContent()).done(() => { + cli.sendEvent(roomId, event.getType(), event.getContent()).then(() => { dis.dispatch({action: 'message_sent'}); }, (err) => { dis.dispatch({action: 'message_send_failed'}); diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index 2de15a5444..63ae14ba09 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -47,7 +47,7 @@ export default createReactClass({ }, _fetch: function() { - this.context.matrixClient.getJoinedGroups().done((result) => { + this.context.matrixClient.getJoinedGroups().then((result) => { this.setState({groups: result.groups, error: null}); }, (err) => { if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') { diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 84f402e484..941381726d 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -89,7 +89,7 @@ module.exports = createReactClass({ this.setState({protocolsLoading: false}); return; } - MatrixClientPeg.get().getThirdpartyProtocols().done((response) => { + MatrixClientPeg.get().getThirdpartyProtocols().then((response) => { this.protocols = response; this.setState({protocolsLoading: false}); }, (err) => { @@ -135,7 +135,7 @@ module.exports = createReactClass({ publicRooms: [], loading: true, }); - this.getMoreRooms().done(); + this.getMoreRooms().then(); }, getMoreRooms: function() { @@ -246,7 +246,7 @@ module.exports = createReactClass({ if (!alias) return; step = _t('delete the alias.'); return MatrixClientPeg.get().deleteAlias(alias); - }).done(() => { + }).then(() => { modal.close(); this.refreshRoomList(); }, (err) => { @@ -348,7 +348,7 @@ module.exports = createReactClass({ }); return; } - MatrixClientPeg.get().getThirdpartyLocation(protocolName, fields).done((resp) => { + MatrixClientPeg.get().getThirdpartyLocation(protocolName, fields).then((resp) => { if (resp.length > 0 && resp[0].alias) { this.showRoomAlias(resp[0].alias, true); } else { diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 4de573479d..d3ba517264 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1101,7 +1101,7 @@ module.exports = createReactClass({ } ContentMessages.sharedInstance().sendStickerContentToRoom(url, this.state.room.roomId, info, text, MatrixClientPeg.get()) - .done(undefined, (error) => { + .then(undefined, (error) => { if (error.name === "UnknownDeviceError") { // Let the staus bar handle this return; @@ -1145,7 +1145,7 @@ module.exports = createReactClass({ filter: filter, term: term, }); - this._handleSearchResult(searchPromise).done(); + this._handleSearchResult(searchPromise).then(); }, _handleSearchResult: function(searchPromise) { @@ -1316,7 +1316,7 @@ module.exports = createReactClass({ }, onForgetClick: function() { - MatrixClientPeg.get().forget(this.state.room.roomId).done(function() { + MatrixClientPeg.get().forget(this.state.room.roomId).then(function() { dis.dispatch({ action: 'view_next_room' }); }, function(err) { const errCode = err.errcode || _t("unknown error code"); @@ -1333,7 +1333,7 @@ module.exports = createReactClass({ this.setState({ rejecting: true, }); - MatrixClientPeg.get().leave(this.state.roomId).done(function() { + MatrixClientPeg.get().leave(this.state.roomId).then(function() { dis.dispatch({ action: 'view_next_room' }); self.setState({ rejecting: false, diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index faa6f2564a..573d82bb9d 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -462,7 +462,7 @@ const TimelinePanel = createReactClass({ // timeline window. // // see https://github.com/vector-im/vector-web/issues/1035 - this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).done(() => { + this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).then(() => { if (this.unmounted) { return; } const { events, liveEvents } = this._getEvents(); @@ -1088,7 +1088,7 @@ const TimelinePanel = createReactClass({ prom = prom.then(onLoaded, onError); } - prom.done(); + prom.then(); }, // handle the completion of a timeline load or localEchoUpdate, by diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 46a5fa7bd7..6f68293caa 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -105,7 +105,7 @@ module.exports = createReactClass({ phase: PHASE_SENDING_EMAIL, }); this.reset = new PasswordReset(this.props.serverConfig.hsUrl, this.props.serverConfig.isUrl); - this.reset.resetPassword(email, password).done(() => { + this.reset.resetPassword(email, password).then(() => { this.setState({ phase: PHASE_EMAIL_SENT, }); diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index ad77ed49a5..84209e514f 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -253,7 +253,7 @@ module.exports = createReactClass({ this.setState({ busy: false, }); - }).done(); + }).then(); }, onUsernameChanged: function(username) { @@ -424,7 +424,7 @@ module.exports = createReactClass({ this.setState({ busy: false, }); - }).done(); + }).then(); }, _isSupportedFlow: function(flow) { diff --git a/src/components/structures/auth/PostRegistration.js b/src/components/structures/auth/PostRegistration.js index 66075c80f7..760163585d 100644 --- a/src/components/structures/auth/PostRegistration.js +++ b/src/components/structures/auth/PostRegistration.js @@ -43,7 +43,7 @@ module.exports = createReactClass({ const cli = MatrixClientPeg.get(); this.setState({busy: true}); const self = this; - cli.getProfileInfo(cli.credentials.userId).done(function(result) { + cli.getProfileInfo(cli.credentials.userId).then(function(result) { self.setState({ avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(result.avatar_url), busy: false, diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 6321028457..6e0fc246d2 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -371,7 +371,7 @@ module.exports = createReactClass({ if (pushers[i].kind === 'email') { const emailPusher = pushers[i]; emailPusher.data = { brand: this.props.brand }; - matrixClient.setPusher(emailPusher).done(() => { + matrixClient.setPusher(emailPusher).then(() => { console.log("Set email branding to " + this.props.brand); }, (error) => { console.error("Couldn't set email branding: " + error); diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js index d19ce95b33..d61035210b 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.js +++ b/src/components/views/auth/InteractiveAuthEntryComponents.js @@ -441,7 +441,7 @@ export const MsisdnAuthEntry = createReactClass({ this.props.fail(e); }).finally(() => { this.setState({requestingToken: false}); - }).done(); + }).then(); }, /* diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js index 541daef27f..98628979e5 100644 --- a/src/components/views/context_menus/RoomTileContextMenu.js +++ b/src/components/views/context_menus/RoomTileContextMenu.js @@ -161,7 +161,7 @@ module.exports = createReactClass({ _onClickForget: function() { // FIXME: duplicated with RoomSettings (and dead code in RoomView) - MatrixClientPeg.get().forget(this.props.room.roomId).done(() => { + MatrixClientPeg.get().forget(this.props.room.roomId).then(() => { // Switch to another room view if we're currently viewing the // historical room if (RoomViewStore.getRoomId() === this.props.room.roomId) { @@ -191,7 +191,7 @@ module.exports = createReactClass({ this.setState({ roomNotifState: newState, }); - RoomNotifs.setRoomNotifsState(roomId, newState).done(() => { + RoomNotifs.setRoomNotifsState(roomId, newState).then(() => { // delay slightly so that the user can see their state change // before closing the menu return sleep(500).then(() => { diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index dc61f23956..caf6bc18c5 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -267,7 +267,7 @@ module.exports = createReactClass({ this.setState({ searchError: err.errcode ? err.message : _t('Something went wrong!'), }); - }).done(() => { + }).then(() => { this.setState({ busy: false, }); @@ -380,7 +380,7 @@ module.exports = createReactClass({ // Do a local search immediately this._doLocalSearch(query); } - }).done(() => { + }).then(() => { this.setState({ busy: false, }); diff --git a/src/components/views/dialogs/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.js index 11f4c21366..191d797a1e 100644 --- a/src/components/views/dialogs/CreateGroupDialog.js +++ b/src/components/views/dialogs/CreateGroupDialog.js @@ -93,7 +93,7 @@ export default createReactClass({ this.setState({createError: e}); }).finally(() => { this.setState({creating: false}); - }).done(); + }).then(); }, _onCancel: function() { diff --git a/src/components/views/dialogs/KeyShareDialog.js b/src/components/views/dialogs/KeyShareDialog.js index a10c25a0fb..51b02f1adf 100644 --- a/src/components/views/dialogs/KeyShareDialog.js +++ b/src/components/views/dialogs/KeyShareDialog.js @@ -78,7 +78,7 @@ export default createReactClass({ true, ); } - }).done(); + }).then(); }, componentWillUnmount: function() { diff --git a/src/components/views/dialogs/SetEmailDialog.js b/src/components/views/dialogs/SetEmailDialog.js index bedf713c4e..b527abffc9 100644 --- a/src/components/views/dialogs/SetEmailDialog.js +++ b/src/components/views/dialogs/SetEmailDialog.js @@ -62,7 +62,7 @@ export default createReactClass({ return; } this._addThreepid = new AddThreepid(); - this._addThreepid.addEmailAddress(emailAddress).done(() => { + this._addThreepid.addEmailAddress(emailAddress).then(() => { Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, { title: _t("Verification Pending"), description: _t( @@ -96,7 +96,7 @@ export default createReactClass({ }, verifyEmailAddress: function() { - this._addThreepid.checkEmailLinkClicked().done(() => { + this._addThreepid.checkEmailLinkClicked().then(() => { this.props.onFinished(true); }, (err) => { this.setState({emailBusy: false}); diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 260b63dfd4..453630413c 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -205,7 +205,7 @@ export default class AppTile extends React.Component { if (!this._scalarClient) { this._scalarClient = defaultManager.getScalarClient(); } - this._scalarClient.getScalarToken().done((token) => { + this._scalarClient.getScalarToken().then((token) => { // Append scalar_token as a query param if not already present this._scalarClient.scalarToken = token; const u = url.parse(this._addWurlParams(this.props.url)); diff --git a/src/components/views/elements/EditableTextContainer.js b/src/components/views/elements/EditableTextContainer.js index 3bf37df951..cc49c3c67f 100644 --- a/src/components/views/elements/EditableTextContainer.js +++ b/src/components/views/elements/EditableTextContainer.js @@ -51,7 +51,7 @@ export default class EditableTextContainer extends React.Component { this.setState({busy: true}); - this.props.getInitialValue().done( + this.props.getInitialValue().then( (result) => { if (this._unmounted) { return; } this.setState({ @@ -83,7 +83,7 @@ export default class EditableTextContainer extends React.Component { errorString: null, }); - this.props.onSubmit(value).done( + this.props.onSubmit(value).then( () => { if (this._unmounted) { return; } this.setState({ diff --git a/src/components/views/elements/ErrorBoundary.js b/src/components/views/elements/ErrorBoundary.js index e53e1ec0fa..e36464c4ef 100644 --- a/src/components/views/elements/ErrorBoundary.js +++ b/src/components/views/elements/ErrorBoundary.js @@ -54,7 +54,7 @@ export default class ErrorBoundary extends React.PureComponent { if (!PlatformPeg.get()) return; MatrixClientPeg.get().stopClient(); - MatrixClientPeg.get().store.deleteAllData().done(() => { + MatrixClientPeg.get().store.deleteAllData().then(() => { PlatformPeg.get().reload(); }); }; diff --git a/src/components/views/elements/ImageView.js b/src/components/views/elements/ImageView.js index 2772363bd0..09e0ff0e54 100644 --- a/src/components/views/elements/ImageView.js +++ b/src/components/views/elements/ImageView.js @@ -84,7 +84,7 @@ export default class ImageView extends React.Component { title: _t('Error'), description: _t('You cannot delete this image. (%(code)s)', {code: code}), }); - }).done(); + }).then(); }, }); }; diff --git a/src/components/views/elements/LanguageDropdown.js b/src/components/views/elements/LanguageDropdown.js index 365f9ded61..0c4b2b9d6a 100644 --- a/src/components/views/elements/LanguageDropdown.js +++ b/src/components/views/elements/LanguageDropdown.js @@ -49,7 +49,7 @@ export default class LanguageDropdown extends React.Component { this.setState({langs}); }).catch(() => { this.setState({langs: ['en']}); - }).done(); + }).then(); if (!this.props.value) { // If no value is given, we start with the first diff --git a/src/components/views/groups/GroupUserSettings.js b/src/components/views/groups/GroupUserSettings.js index 7d80bdd209..3cd5731b99 100644 --- a/src/components/views/groups/GroupUserSettings.js +++ b/src/components/views/groups/GroupUserSettings.js @@ -36,7 +36,7 @@ export default createReactClass({ }, componentWillMount: function() { - this.context.matrixClient.getJoinedGroups().done((result) => { + this.context.matrixClient.getJoinedGroups().then((result) => { this.setState({groups: result.groups || [], error: null}); }, (err) => { console.error(err); diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js index b4f26d0cbd..0246d28542 100644 --- a/src/components/views/messages/MAudioBody.js +++ b/src/components/views/messages/MAudioBody.js @@ -55,7 +55,7 @@ export default class MAudioBody extends React.Component { decryptFile(content.file).then(function(blob) { decryptedBlob = blob; return URL.createObjectURL(decryptedBlob); - }).done((url) => { + }).then((url) => { this.setState({ decryptedUrl: url, decryptedBlob: decryptedBlob, diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 640baa1966..abfd8b64cd 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -289,7 +289,7 @@ export default class MImageBody extends React.Component { this.setState({ error: err, }); - }).done(); + }).then(); } // Remember that the user wanted to show this particular image diff --git a/src/components/views/messages/MVideoBody.js b/src/components/views/messages/MVideoBody.js index d277b6eae9..bc4dbf3374 100644 --- a/src/components/views/messages/MVideoBody.js +++ b/src/components/views/messages/MVideoBody.js @@ -115,7 +115,7 @@ module.exports = createReactClass({ this.setState({ error: err, }); - }).done(); + }).then(); } }, diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 207bf29998..192efcdd8a 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -870,7 +870,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room }, ).finally(() => { stopUpdating(); - }).done(); + }).then(); }; const roomId = user.roomId; diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.js index d93fe76b46..b06a9b9a30 100644 --- a/src/components/views/rooms/LinkPreviewWidget.js +++ b/src/components/views/rooms/LinkPreviewWidget.js @@ -53,7 +53,7 @@ module.exports = createReactClass({ ); }, (error)=>{ console.error("Failed to get URL preview: " + error); - }).done(); + }).then(); }, componentDidMount: function() { diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 2ea6392e96..68e494d5eb 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -248,7 +248,7 @@ module.exports = createReactClass({ return client.getStoredDevicesForUser(member.userId); }).finally(function() { self._cancelDeviceList = null; - }).done(function(devices) { + }).then(function(devices) { if (cancelled) { // we got cancelled - presumably a different user now return; @@ -572,7 +572,7 @@ module.exports = createReactClass({ }, ).finally(()=>{ this.setState({ updating: this.state.updating - 1 }); - }).done(); + }).then(); }, onPowerChange: async function(powerLevel) { @@ -629,7 +629,7 @@ module.exports = createReactClass({ this.setState({ updating: this.state.updating + 1 }); createRoom({dmUserId: this.props.member.userId}).finally(() => { this.setState({ updating: this.state.updating - 1 }); - }).done(); + }).then(); }, onLeaveClick: function() { diff --git a/src/components/views/settings/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js index 32521006c7..904b17b15f 100644 --- a/src/components/views/settings/ChangeAvatar.js +++ b/src/components/views/settings/ChangeAvatar.js @@ -112,7 +112,7 @@ module.exports = createReactClass({ } }); - httpPromise.done(function() { + httpPromise.then(function() { self.setState({ phase: self.Phases.Display, avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(newUrl), diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 91292b19f9..15aa6203d7 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -174,7 +174,7 @@ module.exports = createReactClass({ newPassword: "", newPasswordConfirm: "", }); - }).done(); + }).then(); }, _optionallySetEmail: function() { diff --git a/src/components/views/settings/DevicesPanel.js b/src/components/views/settings/DevicesPanel.js index 30f507ea18..cb5db10be4 100644 --- a/src/components/views/settings/DevicesPanel.js +++ b/src/components/views/settings/DevicesPanel.js @@ -52,7 +52,7 @@ export default class DevicesPanel extends React.Component { } _loadDevices() { - MatrixClientPeg.get().getDevices().done( + MatrixClientPeg.get().getDevices().then( (resp) => { if (this._unmounted) { return; } this.setState({devices: resp.devices || []}); diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index e3b4cfe122..e67c61dff5 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -97,7 +97,7 @@ module.exports = createReactClass({ phase: this.phases.LOADING, }); - MatrixClientPeg.get().setPushRuleEnabled('global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked).done(function() { + MatrixClientPeg.get().setPushRuleEnabled('global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked).then(function() { self._refreshFromServer(); }); }, @@ -170,7 +170,7 @@ module.exports = createReactClass({ emailPusher.kind = null; emailPusherPromise = MatrixClientPeg.get().setPusher(emailPusher); } - emailPusherPromise.done(() => { + emailPusherPromise.then(() => { this._refreshFromServer(); }, (error) => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -274,7 +274,7 @@ module.exports = createReactClass({ } } - Promise.all(deferreds).done(function() { + Promise.all(deferreds).then(function() { self._refreshFromServer(); }, function(error) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -343,7 +343,7 @@ module.exports = createReactClass({ } } - Promise.all(deferreds).done(function(resps) { + Promise.all(deferreds).then(function(resps) { self._refreshFromServer(); }, function(error) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -398,7 +398,7 @@ module.exports = createReactClass({ }; // Then, add the new ones - Promise.all(removeDeferreds).done(function(resps) { + Promise.all(removeDeferreds).then(function(resps) { const deferreds = []; let pushRuleVectorStateKind = self.state.vectorContentRules.vectorState; @@ -434,7 +434,7 @@ module.exports = createReactClass({ } } - Promise.all(deferreds).done(function(resps) { + Promise.all(deferreds).then(function(resps) { self._refreshFromServer(); }, onError); }, onError); @@ -650,7 +650,7 @@ module.exports = createReactClass({ externalContentRules: self.state.externalContentRules, externalPushRules: self.state.externalPushRules, }); - }).done(); + }).then(); MatrixClientPeg.get().getThreePids().then((r) => this.setState({threepids: r.threepids})); }, diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js index fbad327078..875f0bfc10 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js @@ -75,7 +75,7 @@ export default class HelpUserSettingsTab extends React.Component { // stopping in the middle of the logs. console.log("Clear cache & reload clicked"); MatrixClientPeg.get().stopClient(); - MatrixClientPeg.get().store.deleteAllData().done(() => { + MatrixClientPeg.get().store.deleteAllData().then(() => { PlatformPeg.get().reload(); }); }; diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index 7e1b06c0bf..177b88c3f2 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -186,7 +186,7 @@ class RoomViewStore extends Store { roomLoading: true, roomLoadError: null, }); - MatrixClientPeg.get().getRoomIdForAlias(payload.room_alias).done( + MatrixClientPeg.get().getRoomIdForAlias(payload.room_alias).then( (result) => { dis.dispatch({ action: 'view_room', @@ -223,7 +223,7 @@ class RoomViewStore extends Store { }); MatrixClientPeg.get().joinRoom( this._state.roomAlias || this._state.roomId, payload.opts, - ).done(() => { + ).then(() => { // We don't actually need to do anything here: we do *not* // clear the 'joining' flag because the Room object and/or // our 'joined' member event may not have come down the sync diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js index b14ea7c242..7612b43b48 100644 --- a/test/components/views/dialogs/InteractiveAuthDialog-test.js +++ b/test/components/views/dialogs/InteractiveAuthDialog-test.js @@ -26,6 +26,7 @@ import sdk from 'matrix-react-sdk'; import MatrixClientPeg from '../../../../src/MatrixClientPeg'; import * as test_utils from '../../../test-utils'; +import {sleep} from "../../../../src/utils/promise"; const InteractiveAuthDialog = sdk.getComponent( 'views.dialogs.InteractiveAuthDialog', @@ -107,7 +108,7 @@ describe('InteractiveAuthDialog', function() { }, })).toBe(true); // let the request complete - return Promise.delay(1); + return sleep(1); }).then(() => { expect(onFinished.callCount).toEqual(1); expect(onFinished.calledWithExactly(true, {a: 1})).toBe(true); diff --git a/test/components/views/elements/MemberEventListSummary-test.js b/test/components/views/elements/MemberEventListSummary-test.js index 95f7e7999a..a31cbdebb5 100644 --- a/test/components/views/elements/MemberEventListSummary-test.js +++ b/test/components/views/elements/MemberEventListSummary-test.js @@ -91,7 +91,7 @@ describe('MemberEventListSummary', function() { testUtils.beforeEach(this); sandbox = testUtils.stubClient(); - languageHandler.setLanguage('en').done(done); + languageHandler.setLanguage('en').then(done); languageHandler.setMissingEntryGenerator(function(key) { return key.split('|', 2)[1]; }); diff --git a/test/components/views/rooms/MessageComposerInput-test.js b/test/components/views/rooms/MessageComposerInput-test.js index 1105a4af17..ed25c4d607 100644 --- a/test/components/views/rooms/MessageComposerInput-test.js +++ b/test/components/views/rooms/MessageComposerInput-test.js @@ -8,6 +8,7 @@ import * as testUtils from '../../../test-utils'; import sdk from 'matrix-react-sdk'; const MessageComposerInput = sdk.getComponent('views.rooms.MessageComposerInput'); import MatrixClientPeg from '../../../../src/MatrixClientPeg'; +import {sleep} from "../../../../src/utils/promise"; function addTextToDraft(text) { const components = document.getElementsByClassName('public-DraftEditor-content'); @@ -49,7 +50,7 @@ xdescribe('MessageComposerInput', () => { // warnings // (please can we make the components not setState() after // they are unmounted?) - Promise.delay(10).done(() => { + sleep(10).then(() => { if (parentDiv) { ReactDOM.unmountComponentAtNode(parentDiv); parentDiv.remove(); diff --git a/test/i18n-test/languageHandler-test.js b/test/i18n-test/languageHandler-test.js index 0d96bc15ab..8f21638703 100644 --- a/test/i18n-test/languageHandler-test.js +++ b/test/i18n-test/languageHandler-test.js @@ -11,7 +11,7 @@ describe('languageHandler', function() { testUtils.beforeEach(this); sandbox = testUtils.stubClient(); - languageHandler.setLanguage('en').done(done); + languageHandler.setLanguage('en').then(done); }); afterEach(function() { From d72dedb0cee9868792256bc8e34ee1e76e38c8dc Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 12 Nov 2019 11:43:18 +0000 Subject: [PATCH 050/334] Cache room alias to room ID mapping in memory This adds very basic cache (literally just a `Map` for now) to store room alias to room ID mappings. The improves the perceived performance of Riot when switching rooms via browser navigation (back / forward), as we no longer try to resolve the room alias every time. The cache is only in memory, so reloading manually or as part of the clear cache process will start afresh. Fixes https://github.com/vector-im/riot-web/issues/10020 --- src/RoomAliasCache.js | 35 +++++++++++++++++++++++++ src/components/structures/MatrixChat.js | 8 +++++- src/stores/RoomViewStore.js | 30 ++++++++++++++++----- 3 files changed, 66 insertions(+), 7 deletions(-) create mode 100644 src/RoomAliasCache.js diff --git a/src/RoomAliasCache.js b/src/RoomAliasCache.js new file mode 100644 index 0000000000..bb511ba4d7 --- /dev/null +++ b/src/RoomAliasCache.js @@ -0,0 +1,35 @@ +/* +Copyright 2019 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. +*/ + +/** + * This is meant to be a cache of room alias to room ID so that moving between + * rooms happens smoothly (for example using browser back / forward buttons). + * + * For the moment, it's in memory only and so only applies for the current + * session for simplicity, but could be extended further in the future. + * + * A similar thing could also be achieved via `pushState` with a state object, + * but keeping it separate like this seems easier in case we do want to extend. + */ +const aliasToIDMap = new Map(); + +export function storeRoomAliasInCache(alias, id) { + aliasToIDMap.set(alias, id); +} + +export function getCachedRoomIDForAlias(alias) { + return aliasToIDMap.get(alias); +} diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index da67416400..6cc86bf6d7 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -60,6 +60,7 @@ import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils"; import DMRoomMap from '../../utils/DMRoomMap'; import { countRoomsWithNotif } from '../../RoomNotifs'; import { setTheme } from "../../theme"; +import { storeRoomAliasInCache } from '../../RoomAliasCache'; // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. @@ -866,7 +867,12 @@ export default createReactClass({ const room = MatrixClientPeg.get().getRoom(roomInfo.room_id); if (room) { const theAlias = Rooms.getDisplayAliasForRoom(room); - if (theAlias) presentedId = theAlias; + if (theAlias) { + presentedId = theAlias; + // Store display alias of the presented room in cache to speed future + // navigation. + storeRoomAliasInCache(theAlias, room.roomId); + } // Store this as the ID of the last room accessed. This is so that we can // persist which room is being stored across refreshes and browser quits. diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index 7e1b06c0bf..e860ed8b24 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -20,6 +20,7 @@ import MatrixClientPeg from '../MatrixClientPeg'; import sdk from '../index'; import Modal from '../Modal'; import { _t } from '../languageHandler'; +import { getCachedRoomIDForAlias, storeRoomAliasInCache } from '../RoomAliasCache'; const INITIAL_STATE = { // Whether we're joining the currently viewed room (see isJoining()) @@ -137,7 +138,7 @@ class RoomViewStore extends Store { } } - _viewRoom(payload) { + async _viewRoom(payload) { if (payload.room_id) { const newState = { roomId: payload.room_id, @@ -176,6 +177,22 @@ class RoomViewStore extends Store { this._joinRoom(payload); } } else if (payload.room_alias) { + // Try the room alias to room ID navigation cache first to avoid + // blocking room navigation on the homeserver. + const roomId = getCachedRoomIDForAlias(payload.room_alias); + if (roomId) { + dis.dispatch({ + action: 'view_room', + room_id: roomId, + event_id: payload.event_id, + highlighted: payload.highlighted, + room_alias: payload.room_alias, + auto_join: payload.auto_join, + oob_data: payload.oob_data, + }); + return; + } + // Room alias cache miss, so let's ask the homeserver. // Resolve the alias and then do a second dispatch with the room ID acquired this._setState({ roomId: null, @@ -186,8 +203,9 @@ class RoomViewStore extends Store { roomLoading: true, roomLoadError: null, }); - MatrixClientPeg.get().getRoomIdForAlias(payload.room_alias).done( - (result) => { + try { + const result = await MatrixClientPeg.get().getRoomIdForAlias(payload.room_alias); + storeRoomAliasInCache(payload.room_alias, result.room_id); dis.dispatch({ action: 'view_room', room_id: result.room_id, @@ -197,14 +215,14 @@ class RoomViewStore extends Store { auto_join: payload.auto_join, oob_data: payload.oob_data, }); - }, (err) => { + } catch (err) { dis.dispatch({ action: 'view_room_error', room_id: null, room_alias: payload.room_alias, - err: err, + err, }); - }); + } } } From 168b1b68bb5b9700da9ad22b692b3db866f12128 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 12 Nov 2019 11:56:21 +0000 Subject: [PATCH 051/334] Revert "s/.done(/.then(/ since modern es6 track unhandled promise exceptions" This reverts commit 09a8fec2 --- src/Lifecycle.js | 4 ++-- src/Notifier.js | 2 +- src/Resend.js | 2 +- src/ScalarMessaging.js | 8 ++++---- src/components/structures/GroupView.js | 4 ++-- src/components/structures/InteractiveAuth.js | 2 +- src/components/structures/MatrixChat.js | 8 ++++---- src/components/structures/MyGroups.js | 2 +- src/components/structures/RoomDirectory.js | 8 ++++---- src/components/structures/RoomView.js | 8 ++++---- src/components/structures/TimelinePanel.js | 4 ++-- src/components/structures/auth/ForgotPassword.js | 2 +- src/components/structures/auth/Login.js | 4 ++-- src/components/structures/auth/PostRegistration.js | 2 +- src/components/structures/auth/Registration.js | 2 +- .../views/auth/InteractiveAuthEntryComponents.js | 2 +- .../views/context_menus/RoomTileContextMenu.js | 4 ++-- .../views/dialogs/AddressPickerDialog.js | 4 ++-- src/components/views/dialogs/CreateGroupDialog.js | 2 +- src/components/views/dialogs/KeyShareDialog.js | 2 +- src/components/views/dialogs/SetEmailDialog.js | 4 ++-- src/components/views/elements/AppTile.js | 2 +- .../views/elements/EditableTextContainer.js | 4 ++-- src/components/views/elements/ErrorBoundary.js | 2 +- src/components/views/elements/ImageView.js | 2 +- src/components/views/elements/LanguageDropdown.js | 2 +- src/components/views/groups/GroupUserSettings.js | 2 +- src/components/views/messages/MAudioBody.js | 2 +- src/components/views/messages/MImageBody.js | 2 +- src/components/views/messages/MVideoBody.js | 2 +- src/components/views/right_panel/UserInfo.js | 2 +- src/components/views/rooms/LinkPreviewWidget.js | 2 +- src/components/views/rooms/MemberInfo.js | 6 +++--- src/components/views/settings/ChangeAvatar.js | 2 +- src/components/views/settings/ChangePassword.js | 2 +- src/components/views/settings/DevicesPanel.js | 2 +- src/components/views/settings/Notifications.js | 14 +++++++------- .../settings/tabs/user/HelpUserSettingsTab.js | 2 +- src/stores/RoomViewStore.js | 4 ++-- .../views/dialogs/InteractiveAuthDialog-test.js | 3 +-- .../views/elements/MemberEventListSummary-test.js | 2 +- .../views/rooms/MessageComposerInput-test.js | 3 +-- test/i18n-test/languageHandler-test.js | 2 +- 43 files changed, 72 insertions(+), 74 deletions(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 9bada98168..53a9b7a998 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -524,7 +524,7 @@ export function logout() { console.log("Failed to call logout API: token will not be invalidated"); onLoggedOut(); }, - ).then(); + ).done(); } export function softLogout() { @@ -608,7 +608,7 @@ export function onLoggedOut() { // that can occur when components try to use a null client. dis.dispatch({action: 'on_logged_out'}, true); stopMatrixClient(); - _clearStorage().then(); + _clearStorage().done(); } /** diff --git a/src/Notifier.js b/src/Notifier.js index edb9850dfe..cca0ea2b89 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -198,7 +198,7 @@ const Notifier = { if (enable) { // Attempt to get permission from user - plaf.requestNotificationPermission().then((result) => { + plaf.requestNotificationPermission().done((result) => { if (result !== 'granted') { // The permission request was dismissed or denied // TODO: Support alternative branding in messaging diff --git a/src/Resend.js b/src/Resend.js index 51ec804c01..4eaee16d1b 100644 --- a/src/Resend.js +++ b/src/Resend.js @@ -35,7 +35,7 @@ module.exports = { }, resend: function(event) { const room = MatrixClientPeg.get().getRoom(event.getRoomId()); - MatrixClientPeg.get().resendEvent(event, room).then(function(res) { + MatrixClientPeg.get().resendEvent(event, room).done(function(res) { dis.dispatch({ action: 'message_sent', event: event, diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index c0ffc3022d..910a6c4f13 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -279,7 +279,7 @@ function inviteUser(event, roomId, userId) { } } - client.invite(roomId, userId).then(function() { + client.invite(roomId, userId).done(function() { sendResponse(event, { success: true, }); @@ -398,7 +398,7 @@ function setPlumbingState(event, roomId, status) { sendError(event, _t('You need to be logged in.')); return; } - client.sendStateEvent(roomId, "m.room.plumbing", { status: status }).then(() => { + client.sendStateEvent(roomId, "m.room.plumbing", { status: status }).done(() => { sendResponse(event, { success: true, }); @@ -414,7 +414,7 @@ function setBotOptions(event, roomId, userId) { sendError(event, _t('You need to be logged in.')); return; } - client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).then(() => { + client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).done(() => { sendResponse(event, { success: true, }); @@ -444,7 +444,7 @@ function setBotPower(event, roomId, userId, level) { }, ); - client.setPowerLevel(roomId, userId, level, powerEvent).then(() => { + client.setPowerLevel(roomId, userId, level, powerEvent).done(() => { sendResponse(event, { success: true, }); diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index b97d76d72a..4056557a7c 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -639,7 +639,7 @@ export default createReactClass({ title: _t('Error'), description: _t('Failed to upload image'), }); - }).then(); + }).done(); }, _onJoinableChange: function(ev) { @@ -678,7 +678,7 @@ export default createReactClass({ this.setState({ avatarChanged: false, }); - }).then(); + }).done(); }, _saveGroup: async function() { diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index 86fc351515..5e06d124c4 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -121,7 +121,7 @@ export default createReactClass({ this.setState({ errorText: msg, }); - }).then(); + }).done(); this._intervalId = null; if (this.props.poll) { diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index c7bf2f181f..d12eba88f7 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -541,7 +541,7 @@ export default createReactClass({ const Loader = sdk.getComponent("elements.Spinner"); const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); - MatrixClientPeg.get().leave(payload.room_id).then(() => { + MatrixClientPeg.get().leave(payload.room_id).done(() => { modal.close(); if (this.state.currentRoomId === payload.room_id) { dis.dispatch({action: 'view_next_room'}); @@ -862,7 +862,7 @@ export default createReactClass({ waitFor = this.firstSyncPromise.promise; } - waitFor.then(() => { + waitFor.done(() => { let presentedId = roomInfo.room_alias || roomInfo.room_id; const room = MatrixClientPeg.get().getRoom(roomInfo.room_id); if (room) { @@ -974,7 +974,7 @@ export default createReactClass({ const [shouldCreate, createOpts] = await modal.finished; if (shouldCreate) { - createRoom({createOpts}).then(); + createRoom({createOpts}).done(); } }, @@ -1750,7 +1750,7 @@ export default createReactClass({ return; } - cli.sendEvent(roomId, event.getType(), event.getContent()).then(() => { + cli.sendEvent(roomId, event.getType(), event.getContent()).done(() => { dis.dispatch({action: 'message_sent'}); }, (err) => { dis.dispatch({action: 'message_send_failed'}); diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index 63ae14ba09..2de15a5444 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -47,7 +47,7 @@ export default createReactClass({ }, _fetch: function() { - this.context.matrixClient.getJoinedGroups().then((result) => { + this.context.matrixClient.getJoinedGroups().done((result) => { this.setState({groups: result.groups, error: null}); }, (err) => { if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') { diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 941381726d..84f402e484 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -89,7 +89,7 @@ module.exports = createReactClass({ this.setState({protocolsLoading: false}); return; } - MatrixClientPeg.get().getThirdpartyProtocols().then((response) => { + MatrixClientPeg.get().getThirdpartyProtocols().done((response) => { this.protocols = response; this.setState({protocolsLoading: false}); }, (err) => { @@ -135,7 +135,7 @@ module.exports = createReactClass({ publicRooms: [], loading: true, }); - this.getMoreRooms().then(); + this.getMoreRooms().done(); }, getMoreRooms: function() { @@ -246,7 +246,7 @@ module.exports = createReactClass({ if (!alias) return; step = _t('delete the alias.'); return MatrixClientPeg.get().deleteAlias(alias); - }).then(() => { + }).done(() => { modal.close(); this.refreshRoomList(); }, (err) => { @@ -348,7 +348,7 @@ module.exports = createReactClass({ }); return; } - MatrixClientPeg.get().getThirdpartyLocation(protocolName, fields).then((resp) => { + MatrixClientPeg.get().getThirdpartyLocation(protocolName, fields).done((resp) => { if (resp.length > 0 && resp[0].alias) { this.showRoomAlias(resp[0].alias, true); } else { diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index d3ba517264..4de573479d 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1101,7 +1101,7 @@ module.exports = createReactClass({ } ContentMessages.sharedInstance().sendStickerContentToRoom(url, this.state.room.roomId, info, text, MatrixClientPeg.get()) - .then(undefined, (error) => { + .done(undefined, (error) => { if (error.name === "UnknownDeviceError") { // Let the staus bar handle this return; @@ -1145,7 +1145,7 @@ module.exports = createReactClass({ filter: filter, term: term, }); - this._handleSearchResult(searchPromise).then(); + this._handleSearchResult(searchPromise).done(); }, _handleSearchResult: function(searchPromise) { @@ -1316,7 +1316,7 @@ module.exports = createReactClass({ }, onForgetClick: function() { - MatrixClientPeg.get().forget(this.state.room.roomId).then(function() { + MatrixClientPeg.get().forget(this.state.room.roomId).done(function() { dis.dispatch({ action: 'view_next_room' }); }, function(err) { const errCode = err.errcode || _t("unknown error code"); @@ -1333,7 +1333,7 @@ module.exports = createReactClass({ this.setState({ rejecting: true, }); - MatrixClientPeg.get().leave(this.state.roomId).then(function() { + MatrixClientPeg.get().leave(this.state.roomId).done(function() { dis.dispatch({ action: 'view_next_room' }); self.setState({ rejecting: false, diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 573d82bb9d..faa6f2564a 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -462,7 +462,7 @@ const TimelinePanel = createReactClass({ // timeline window. // // see https://github.com/vector-im/vector-web/issues/1035 - this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).then(() => { + this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).done(() => { if (this.unmounted) { return; } const { events, liveEvents } = this._getEvents(); @@ -1088,7 +1088,7 @@ const TimelinePanel = createReactClass({ prom = prom.then(onLoaded, onError); } - prom.then(); + prom.done(); }, // handle the completion of a timeline load or localEchoUpdate, by diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 6f68293caa..46a5fa7bd7 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -105,7 +105,7 @@ module.exports = createReactClass({ phase: PHASE_SENDING_EMAIL, }); this.reset = new PasswordReset(this.props.serverConfig.hsUrl, this.props.serverConfig.isUrl); - this.reset.resetPassword(email, password).then(() => { + this.reset.resetPassword(email, password).done(() => { this.setState({ phase: PHASE_EMAIL_SENT, }); diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index 84209e514f..ad77ed49a5 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -253,7 +253,7 @@ module.exports = createReactClass({ this.setState({ busy: false, }); - }).then(); + }).done(); }, onUsernameChanged: function(username) { @@ -424,7 +424,7 @@ module.exports = createReactClass({ this.setState({ busy: false, }); - }).then(); + }).done(); }, _isSupportedFlow: function(flow) { diff --git a/src/components/structures/auth/PostRegistration.js b/src/components/structures/auth/PostRegistration.js index 760163585d..66075c80f7 100644 --- a/src/components/structures/auth/PostRegistration.js +++ b/src/components/structures/auth/PostRegistration.js @@ -43,7 +43,7 @@ module.exports = createReactClass({ const cli = MatrixClientPeg.get(); this.setState({busy: true}); const self = this; - cli.getProfileInfo(cli.credentials.userId).then(function(result) { + cli.getProfileInfo(cli.credentials.userId).done(function(result) { self.setState({ avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(result.avatar_url), busy: false, diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 6e0fc246d2..6321028457 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -371,7 +371,7 @@ module.exports = createReactClass({ if (pushers[i].kind === 'email') { const emailPusher = pushers[i]; emailPusher.data = { brand: this.props.brand }; - matrixClient.setPusher(emailPusher).then(() => { + matrixClient.setPusher(emailPusher).done(() => { console.log("Set email branding to " + this.props.brand); }, (error) => { console.error("Couldn't set email branding: " + error); diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js index d61035210b..d19ce95b33 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.js +++ b/src/components/views/auth/InteractiveAuthEntryComponents.js @@ -441,7 +441,7 @@ export const MsisdnAuthEntry = createReactClass({ this.props.fail(e); }).finally(() => { this.setState({requestingToken: false}); - }).then(); + }).done(); }, /* diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js index 98628979e5..541daef27f 100644 --- a/src/components/views/context_menus/RoomTileContextMenu.js +++ b/src/components/views/context_menus/RoomTileContextMenu.js @@ -161,7 +161,7 @@ module.exports = createReactClass({ _onClickForget: function() { // FIXME: duplicated with RoomSettings (and dead code in RoomView) - MatrixClientPeg.get().forget(this.props.room.roomId).then(() => { + MatrixClientPeg.get().forget(this.props.room.roomId).done(() => { // Switch to another room view if we're currently viewing the // historical room if (RoomViewStore.getRoomId() === this.props.room.roomId) { @@ -191,7 +191,7 @@ module.exports = createReactClass({ this.setState({ roomNotifState: newState, }); - RoomNotifs.setRoomNotifsState(roomId, newState).then(() => { + RoomNotifs.setRoomNotifsState(roomId, newState).done(() => { // delay slightly so that the user can see their state change // before closing the menu return sleep(500).then(() => { diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index caf6bc18c5..dc61f23956 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -267,7 +267,7 @@ module.exports = createReactClass({ this.setState({ searchError: err.errcode ? err.message : _t('Something went wrong!'), }); - }).then(() => { + }).done(() => { this.setState({ busy: false, }); @@ -380,7 +380,7 @@ module.exports = createReactClass({ // Do a local search immediately this._doLocalSearch(query); } - }).then(() => { + }).done(() => { this.setState({ busy: false, }); diff --git a/src/components/views/dialogs/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.js index 191d797a1e..11f4c21366 100644 --- a/src/components/views/dialogs/CreateGroupDialog.js +++ b/src/components/views/dialogs/CreateGroupDialog.js @@ -93,7 +93,7 @@ export default createReactClass({ this.setState({createError: e}); }).finally(() => { this.setState({creating: false}); - }).then(); + }).done(); }, _onCancel: function() { diff --git a/src/components/views/dialogs/KeyShareDialog.js b/src/components/views/dialogs/KeyShareDialog.js index 51b02f1adf..a10c25a0fb 100644 --- a/src/components/views/dialogs/KeyShareDialog.js +++ b/src/components/views/dialogs/KeyShareDialog.js @@ -78,7 +78,7 @@ export default createReactClass({ true, ); } - }).then(); + }).done(); }, componentWillUnmount: function() { diff --git a/src/components/views/dialogs/SetEmailDialog.js b/src/components/views/dialogs/SetEmailDialog.js index b527abffc9..bedf713c4e 100644 --- a/src/components/views/dialogs/SetEmailDialog.js +++ b/src/components/views/dialogs/SetEmailDialog.js @@ -62,7 +62,7 @@ export default createReactClass({ return; } this._addThreepid = new AddThreepid(); - this._addThreepid.addEmailAddress(emailAddress).then(() => { + this._addThreepid.addEmailAddress(emailAddress).done(() => { Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, { title: _t("Verification Pending"), description: _t( @@ -96,7 +96,7 @@ export default createReactClass({ }, verifyEmailAddress: function() { - this._addThreepid.checkEmailLinkClicked().then(() => { + this._addThreepid.checkEmailLinkClicked().done(() => { this.props.onFinished(true); }, (err) => { this.setState({emailBusy: false}); diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 453630413c..260b63dfd4 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -205,7 +205,7 @@ export default class AppTile extends React.Component { if (!this._scalarClient) { this._scalarClient = defaultManager.getScalarClient(); } - this._scalarClient.getScalarToken().then((token) => { + this._scalarClient.getScalarToken().done((token) => { // Append scalar_token as a query param if not already present this._scalarClient.scalarToken = token; const u = url.parse(this._addWurlParams(this.props.url)); diff --git a/src/components/views/elements/EditableTextContainer.js b/src/components/views/elements/EditableTextContainer.js index cc49c3c67f..3bf37df951 100644 --- a/src/components/views/elements/EditableTextContainer.js +++ b/src/components/views/elements/EditableTextContainer.js @@ -51,7 +51,7 @@ export default class EditableTextContainer extends React.Component { this.setState({busy: true}); - this.props.getInitialValue().then( + this.props.getInitialValue().done( (result) => { if (this._unmounted) { return; } this.setState({ @@ -83,7 +83,7 @@ export default class EditableTextContainer extends React.Component { errorString: null, }); - this.props.onSubmit(value).then( + this.props.onSubmit(value).done( () => { if (this._unmounted) { return; } this.setState({ diff --git a/src/components/views/elements/ErrorBoundary.js b/src/components/views/elements/ErrorBoundary.js index e36464c4ef..e53e1ec0fa 100644 --- a/src/components/views/elements/ErrorBoundary.js +++ b/src/components/views/elements/ErrorBoundary.js @@ -54,7 +54,7 @@ export default class ErrorBoundary extends React.PureComponent { if (!PlatformPeg.get()) return; MatrixClientPeg.get().stopClient(); - MatrixClientPeg.get().store.deleteAllData().then(() => { + MatrixClientPeg.get().store.deleteAllData().done(() => { PlatformPeg.get().reload(); }); }; diff --git a/src/components/views/elements/ImageView.js b/src/components/views/elements/ImageView.js index 09e0ff0e54..2772363bd0 100644 --- a/src/components/views/elements/ImageView.js +++ b/src/components/views/elements/ImageView.js @@ -84,7 +84,7 @@ export default class ImageView extends React.Component { title: _t('Error'), description: _t('You cannot delete this image. (%(code)s)', {code: code}), }); - }).then(); + }).done(); }, }); }; diff --git a/src/components/views/elements/LanguageDropdown.js b/src/components/views/elements/LanguageDropdown.js index 0c4b2b9d6a..365f9ded61 100644 --- a/src/components/views/elements/LanguageDropdown.js +++ b/src/components/views/elements/LanguageDropdown.js @@ -49,7 +49,7 @@ export default class LanguageDropdown extends React.Component { this.setState({langs}); }).catch(() => { this.setState({langs: ['en']}); - }).then(); + }).done(); if (!this.props.value) { // If no value is given, we start with the first diff --git a/src/components/views/groups/GroupUserSettings.js b/src/components/views/groups/GroupUserSettings.js index 3cd5731b99..7d80bdd209 100644 --- a/src/components/views/groups/GroupUserSettings.js +++ b/src/components/views/groups/GroupUserSettings.js @@ -36,7 +36,7 @@ export default createReactClass({ }, componentWillMount: function() { - this.context.matrixClient.getJoinedGroups().then((result) => { + this.context.matrixClient.getJoinedGroups().done((result) => { this.setState({groups: result.groups || [], error: null}); }, (err) => { console.error(err); diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js index 0246d28542..b4f26d0cbd 100644 --- a/src/components/views/messages/MAudioBody.js +++ b/src/components/views/messages/MAudioBody.js @@ -55,7 +55,7 @@ export default class MAudioBody extends React.Component { decryptFile(content.file).then(function(blob) { decryptedBlob = blob; return URL.createObjectURL(decryptedBlob); - }).then((url) => { + }).done((url) => { this.setState({ decryptedUrl: url, decryptedBlob: decryptedBlob, diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index abfd8b64cd..640baa1966 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -289,7 +289,7 @@ export default class MImageBody extends React.Component { this.setState({ error: err, }); - }).then(); + }).done(); } // Remember that the user wanted to show this particular image diff --git a/src/components/views/messages/MVideoBody.js b/src/components/views/messages/MVideoBody.js index bc4dbf3374..d277b6eae9 100644 --- a/src/components/views/messages/MVideoBody.js +++ b/src/components/views/messages/MVideoBody.js @@ -115,7 +115,7 @@ module.exports = createReactClass({ this.setState({ error: err, }); - }).then(); + }).done(); } }, diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 192efcdd8a..207bf29998 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -870,7 +870,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room }, ).finally(() => { stopUpdating(); - }).then(); + }).done(); }; const roomId = user.roomId; diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.js index b06a9b9a30..d93fe76b46 100644 --- a/src/components/views/rooms/LinkPreviewWidget.js +++ b/src/components/views/rooms/LinkPreviewWidget.js @@ -53,7 +53,7 @@ module.exports = createReactClass({ ); }, (error)=>{ console.error("Failed to get URL preview: " + error); - }).then(); + }).done(); }, componentDidMount: function() { diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 68e494d5eb..2ea6392e96 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -248,7 +248,7 @@ module.exports = createReactClass({ return client.getStoredDevicesForUser(member.userId); }).finally(function() { self._cancelDeviceList = null; - }).then(function(devices) { + }).done(function(devices) { if (cancelled) { // we got cancelled - presumably a different user now return; @@ -572,7 +572,7 @@ module.exports = createReactClass({ }, ).finally(()=>{ this.setState({ updating: this.state.updating - 1 }); - }).then(); + }).done(); }, onPowerChange: async function(powerLevel) { @@ -629,7 +629,7 @@ module.exports = createReactClass({ this.setState({ updating: this.state.updating + 1 }); createRoom({dmUserId: this.props.member.userId}).finally(() => { this.setState({ updating: this.state.updating - 1 }); - }).then(); + }).done(); }, onLeaveClick: function() { diff --git a/src/components/views/settings/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js index 904b17b15f..32521006c7 100644 --- a/src/components/views/settings/ChangeAvatar.js +++ b/src/components/views/settings/ChangeAvatar.js @@ -112,7 +112,7 @@ module.exports = createReactClass({ } }); - httpPromise.then(function() { + httpPromise.done(function() { self.setState({ phase: self.Phases.Display, avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(newUrl), diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 15aa6203d7..91292b19f9 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -174,7 +174,7 @@ module.exports = createReactClass({ newPassword: "", newPasswordConfirm: "", }); - }).then(); + }).done(); }, _optionallySetEmail: function() { diff --git a/src/components/views/settings/DevicesPanel.js b/src/components/views/settings/DevicesPanel.js index cb5db10be4..30f507ea18 100644 --- a/src/components/views/settings/DevicesPanel.js +++ b/src/components/views/settings/DevicesPanel.js @@ -52,7 +52,7 @@ export default class DevicesPanel extends React.Component { } _loadDevices() { - MatrixClientPeg.get().getDevices().then( + MatrixClientPeg.get().getDevices().done( (resp) => { if (this._unmounted) { return; } this.setState({devices: resp.devices || []}); diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index e67c61dff5..e3b4cfe122 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -97,7 +97,7 @@ module.exports = createReactClass({ phase: this.phases.LOADING, }); - MatrixClientPeg.get().setPushRuleEnabled('global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked).then(function() { + MatrixClientPeg.get().setPushRuleEnabled('global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked).done(function() { self._refreshFromServer(); }); }, @@ -170,7 +170,7 @@ module.exports = createReactClass({ emailPusher.kind = null; emailPusherPromise = MatrixClientPeg.get().setPusher(emailPusher); } - emailPusherPromise.then(() => { + emailPusherPromise.done(() => { this._refreshFromServer(); }, (error) => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -274,7 +274,7 @@ module.exports = createReactClass({ } } - Promise.all(deferreds).then(function() { + Promise.all(deferreds).done(function() { self._refreshFromServer(); }, function(error) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -343,7 +343,7 @@ module.exports = createReactClass({ } } - Promise.all(deferreds).then(function(resps) { + Promise.all(deferreds).done(function(resps) { self._refreshFromServer(); }, function(error) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -398,7 +398,7 @@ module.exports = createReactClass({ }; // Then, add the new ones - Promise.all(removeDeferreds).then(function(resps) { + Promise.all(removeDeferreds).done(function(resps) { const deferreds = []; let pushRuleVectorStateKind = self.state.vectorContentRules.vectorState; @@ -434,7 +434,7 @@ module.exports = createReactClass({ } } - Promise.all(deferreds).then(function(resps) { + Promise.all(deferreds).done(function(resps) { self._refreshFromServer(); }, onError); }, onError); @@ -650,7 +650,7 @@ module.exports = createReactClass({ externalContentRules: self.state.externalContentRules, externalPushRules: self.state.externalPushRules, }); - }).then(); + }).done(); MatrixClientPeg.get().getThreePids().then((r) => this.setState({threepids: r.threepids})); }, diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js index 875f0bfc10..fbad327078 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js @@ -75,7 +75,7 @@ export default class HelpUserSettingsTab extends React.Component { // stopping in the middle of the logs. console.log("Clear cache & reload clicked"); MatrixClientPeg.get().stopClient(); - MatrixClientPeg.get().store.deleteAllData().then(() => { + MatrixClientPeg.get().store.deleteAllData().done(() => { PlatformPeg.get().reload(); }); }; diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index 177b88c3f2..7e1b06c0bf 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -186,7 +186,7 @@ class RoomViewStore extends Store { roomLoading: true, roomLoadError: null, }); - MatrixClientPeg.get().getRoomIdForAlias(payload.room_alias).then( + MatrixClientPeg.get().getRoomIdForAlias(payload.room_alias).done( (result) => { dis.dispatch({ action: 'view_room', @@ -223,7 +223,7 @@ class RoomViewStore extends Store { }); MatrixClientPeg.get().joinRoom( this._state.roomAlias || this._state.roomId, payload.opts, - ).then(() => { + ).done(() => { // We don't actually need to do anything here: we do *not* // clear the 'joining' flag because the Room object and/or // our 'joined' member event may not have come down the sync diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js index 7612b43b48..b14ea7c242 100644 --- a/test/components/views/dialogs/InteractiveAuthDialog-test.js +++ b/test/components/views/dialogs/InteractiveAuthDialog-test.js @@ -26,7 +26,6 @@ import sdk from 'matrix-react-sdk'; import MatrixClientPeg from '../../../../src/MatrixClientPeg'; import * as test_utils from '../../../test-utils'; -import {sleep} from "../../../../src/utils/promise"; const InteractiveAuthDialog = sdk.getComponent( 'views.dialogs.InteractiveAuthDialog', @@ -108,7 +107,7 @@ describe('InteractiveAuthDialog', function() { }, })).toBe(true); // let the request complete - return sleep(1); + return Promise.delay(1); }).then(() => { expect(onFinished.callCount).toEqual(1); expect(onFinished.calledWithExactly(true, {a: 1})).toBe(true); diff --git a/test/components/views/elements/MemberEventListSummary-test.js b/test/components/views/elements/MemberEventListSummary-test.js index a31cbdebb5..95f7e7999a 100644 --- a/test/components/views/elements/MemberEventListSummary-test.js +++ b/test/components/views/elements/MemberEventListSummary-test.js @@ -91,7 +91,7 @@ describe('MemberEventListSummary', function() { testUtils.beforeEach(this); sandbox = testUtils.stubClient(); - languageHandler.setLanguage('en').then(done); + languageHandler.setLanguage('en').done(done); languageHandler.setMissingEntryGenerator(function(key) { return key.split('|', 2)[1]; }); diff --git a/test/components/views/rooms/MessageComposerInput-test.js b/test/components/views/rooms/MessageComposerInput-test.js index ed25c4d607..1105a4af17 100644 --- a/test/components/views/rooms/MessageComposerInput-test.js +++ b/test/components/views/rooms/MessageComposerInput-test.js @@ -8,7 +8,6 @@ import * as testUtils from '../../../test-utils'; import sdk from 'matrix-react-sdk'; const MessageComposerInput = sdk.getComponent('views.rooms.MessageComposerInput'); import MatrixClientPeg from '../../../../src/MatrixClientPeg'; -import {sleep} from "../../../../src/utils/promise"; function addTextToDraft(text) { const components = document.getElementsByClassName('public-DraftEditor-content'); @@ -50,7 +49,7 @@ xdescribe('MessageComposerInput', () => { // warnings // (please can we make the components not setState() after // they are unmounted?) - sleep(10).then(() => { + Promise.delay(10).done(() => { if (parentDiv) { ReactDOM.unmountComponentAtNode(parentDiv); parentDiv.remove(); diff --git a/test/i18n-test/languageHandler-test.js b/test/i18n-test/languageHandler-test.js index 8f21638703..0d96bc15ab 100644 --- a/test/i18n-test/languageHandler-test.js +++ b/test/i18n-test/languageHandler-test.js @@ -11,7 +11,7 @@ describe('languageHandler', function() { testUtils.beforeEach(this); sandbox = testUtils.stubClient(); - languageHandler.setLanguage('en').then(done); + languageHandler.setLanguage('en').done(done); }); afterEach(function() { From f9d6ed63f0e82056905fa8fcee1914d3de4deaf1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 12 Nov 2019 11:56:32 +0000 Subject: [PATCH 052/334] Revert "Replace Promise.delay with promise utils sleep" This reverts commit 0a21957b --- src/components/structures/GroupView.js | 9 ++++----- .../views/context_menus/RoomTileContextMenu.js | 7 +++---- src/components/views/dialogs/AddressPickerDialog.js | 3 +-- .../views/settings/tabs/user/SecurityUserSettingsTab.js | 3 +-- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 4056557a7c..4d8f47003c 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -38,7 +38,6 @@ import FlairStore from '../../stores/FlairStore'; import { showGroupAddRoomDialog } from '../../GroupAddressPicker'; import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks"; import {Group} from "matrix-js-sdk"; -import {sleep} from "../../utils/promise"; const LONG_DESC_PLACEHOLDER = _td( `

    HTML for your community's page

    @@ -693,7 +692,7 @@ export default createReactClass({ // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the // spinner disappearing after we have fetched new group data. - await sleep(500); + await Promise.delay(500); GroupStore.acceptGroupInvite(this.props.groupId).then(() => { // don't reset membershipBusy here: wait for the membership change to come down the sync @@ -712,7 +711,7 @@ export default createReactClass({ // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the // spinner disappearing after we have fetched new group data. - await sleep(500); + await Promise.delay(500); GroupStore.leaveGroup(this.props.groupId).then(() => { // don't reset membershipBusy here: wait for the membership change to come down the sync @@ -736,7 +735,7 @@ export default createReactClass({ // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the // spinner disappearing after we have fetched new group data. - await sleep(500); + await Promise.delay(500); GroupStore.joinGroup(this.props.groupId).then(() => { // don't reset membershipBusy here: wait for the membership change to come down the sync @@ -788,7 +787,7 @@ export default createReactClass({ // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the // spinner disappearing after we have fetched new group data. - await sleep(500); + await Promise.delay(500); GroupStore.leaveGroup(this.props.groupId).then(() => { // don't reset membershipBusy here: wait for the membership change to come down the sync diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js index 541daef27f..9bb573026f 100644 --- a/src/components/views/context_menus/RoomTileContextMenu.js +++ b/src/components/views/context_menus/RoomTileContextMenu.js @@ -32,7 +32,6 @@ import * as RoomNotifs from '../../../RoomNotifs'; import Modal from '../../../Modal'; import RoomListActions from '../../../actions/RoomListActions'; import RoomViewStore from '../../../stores/RoomViewStore'; -import {sleep} from "../../../utils/promise"; module.exports = createReactClass({ displayName: 'RoomTileContextMenu', @@ -63,7 +62,7 @@ module.exports = createReactClass({ _toggleTag: function(tagNameOn, tagNameOff) { if (!MatrixClientPeg.get().isGuest()) { - sleep(500).then(() => { + Promise.delay(500).then(() => { dis.dispatch(RoomListActions.tagRoom( MatrixClientPeg.get(), this.props.room, @@ -120,7 +119,7 @@ module.exports = createReactClass({ Rooms.guessAndSetDMRoom( this.props.room, newIsDirectMessage, - ).then(sleep(500)).finally(() => { + ).delay(500).finally(() => { // Close the context menu if (this.props.onFinished) { this.props.onFinished(); @@ -194,7 +193,7 @@ module.exports = createReactClass({ RoomNotifs.setRoomNotifsState(roomId, newState).done(() => { // delay slightly so that the user can see their state change // before closing the menu - return sleep(500).then(() => { + return Promise.delay(500).then(() => { if (this._unmounted) return; // Close the context menu if (this.props.onFinished) { diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index dc61f23956..fb779fa96f 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -32,7 +32,6 @@ import * as Email from '../../../email'; import IdentityAuthClient from '../../../IdentityAuthClient'; import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../utils/IdentityServerUtils'; import { abbreviateUrl } from '../../../utils/UrlUtils'; -import {sleep} from "../../../utils/promise"; const TRUNCATE_QUERY_LIST = 40; const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200; @@ -534,7 +533,7 @@ module.exports = createReactClass({ }; // wait a bit to let the user finish typing - await sleep(500); + await Promise.delay(500); if (cancelled) return null; try { diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js index 222af48fa1..e619791b01 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js @@ -25,7 +25,6 @@ import Analytics from "../../../../../Analytics"; import Promise from "bluebird"; import Modal from "../../../../../Modal"; import sdk from "../../../../.."; -import {sleep} from "../../../../../utils/promise"; export class IgnoredUser extends React.Component { static propTypes = { @@ -130,7 +129,7 @@ export default class SecurityUserSettingsTab extends React.Component { if (e.errcode === "M_LIMIT_EXCEEDED") { // Add a delay between each invite change in order to avoid rate // limiting by the server. - await sleep(e.retry_after_ms || 2500); + await Promise.delay(e.retry_after_ms || 2500); // Redo last action i--; From 7a512f7299f5316aba434cc639a01c4b65a5a3aa Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 12 Nov 2019 11:56:43 +0000 Subject: [PATCH 053/334] Revert "Replace rest of defer usages using small shim. Add homebrew promise utils" This reverts commit 6850c147 --- src/Modal.js | 3 +- src/components/structures/MatrixChat.js | 5 ++- src/utils/MultiInviter.js | 3 +- src/utils/promise.js | 46 ------------------------- 4 files changed, 4 insertions(+), 53 deletions(-) delete mode 100644 src/utils/promise.js diff --git a/src/Modal.js b/src/Modal.js index cb19731f01..26c9da8bbb 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -24,7 +24,6 @@ import sdk from './index'; import dis from './dispatcher'; import { _t } from './languageHandler'; import Promise from "bluebird"; -import {defer} from "./utils/promise"; const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer"; @@ -203,7 +202,7 @@ class ModalManager { } _getCloseFn(modal, props) { - const deferred = defer(); + const deferred = Promise.defer(); return [(...args) => { deferred.resolve(args); if (props && props.onFinished) props.onFinished.apply(null, args); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index d12eba88f7..da67416400 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -60,7 +60,6 @@ import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils"; import DMRoomMap from '../../utils/DMRoomMap'; import { countRoomsWithNotif } from '../../RoomNotifs'; import { setTheme } from "../../theme"; -import {defer} from "../../utils/promise"; // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. @@ -237,7 +236,7 @@ export default createReactClass({ // Used by _viewRoom before getting state from sync this.firstSyncComplete = false; - this.firstSyncPromise = defer(); + this.firstSyncPromise = Promise.defer(); if (this.props.config.sync_timeline_limit) { MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit; @@ -1262,7 +1261,7 @@ export default createReactClass({ // since we're about to start the client and therefore about // to do the first sync this.firstSyncComplete = false; - this.firstSyncPromise = defer(); + this.firstSyncPromise = Promise.defer(); const cli = MatrixClientPeg.get(); const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog'); diff --git a/src/utils/MultiInviter.js b/src/utils/MultiInviter.js index de5c2e7610..e8995b46d7 100644 --- a/src/utils/MultiInviter.js +++ b/src/utils/MultiInviter.js @@ -24,7 +24,6 @@ import {_t} from "../languageHandler"; import sdk from "../index"; import Modal from "../Modal"; import SettingsStore from "../settings/SettingsStore"; -import {defer} from "./promise"; /** * Invites multiple addresses to a room or group, handling rate limiting from the server @@ -72,7 +71,7 @@ export default class MultiInviter { }; } } - this.deferred = defer(); + this.deferred = Promise.defer(); this._inviteMore(0); return this.deferred.promise; diff --git a/src/utils/promise.js b/src/utils/promise.js deleted file mode 100644 index dd10f7fdd7..0000000000 --- a/src/utils/promise.js +++ /dev/null @@ -1,46 +0,0 @@ -/* -Copyright 2019 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. -*/ - -// @flow - -// Returns a promise which resolves with a given value after the given number of ms -export const sleep = (ms: number, value: any): Promise => new Promise((resolve => { setTimeout(resolve, ms, value); })); - -// Returns a promise which resolves when the input promise resolves with its value -// or when the timeout of ms is reached with the value of given timeoutValue -export async function timeout(promise: Promise, timeoutValue: any, ms: number): Promise { - const timeoutPromise = new Promise((resolve) => { - const timeoutId = setTimeout(resolve, ms, timeoutValue); - promise.then(() => { - clearTimeout(timeoutId); - }); - }); - - return Promise.race([promise, timeoutPromise]); -} - -// Returns a Deferred -export function defer(): {resolve: () => {}, reject: () => {}, promise: Promise} { - let resolve; - let reject; - - const promise = new Promise((_resolve, _reject) => { - resolve = _resolve; - reject = _reject; - }); - - return {resolve, reject, promise}; -} From 548e38cba9fc24748b4fde73895ac9b30e53d2bf Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 12 Nov 2019 11:56:53 +0000 Subject: [PATCH 054/334] Revert "Replace all trivial Promise.defer usages with regular Promises" This reverts commit 44401d73 --- src/ContentMessages.js | 174 +++++++++--------- src/Lifecycle.js | 8 +- src/components/views/rooms/Autocomplete.js | 29 +-- .../views/settings/ChangePassword.js | 9 +- src/rageshake/submit-rageshake.js | 40 ++-- 5 files changed, 138 insertions(+), 122 deletions(-) diff --git a/src/ContentMessages.js b/src/ContentMessages.js index dab8de2465..2d58622db8 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -59,38 +59,40 @@ export class UploadCanceledError extends Error {} * and a thumbnail key. */ function createThumbnail(element, inputWidth, inputHeight, mimeType) { - return new Promise((resolve) => { - let targetWidth = inputWidth; - let targetHeight = inputHeight; - if (targetHeight > MAX_HEIGHT) { - targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight)); - targetHeight = MAX_HEIGHT; - } - if (targetWidth > MAX_WIDTH) { - targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth)); - targetWidth = MAX_WIDTH; - } + const deferred = Promise.defer(); - const canvas = document.createElement("canvas"); - canvas.width = targetWidth; - canvas.height = targetHeight; - canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight); - canvas.toBlob(function(thumbnail) { - resolve({ - info: { - thumbnail_info: { - w: targetWidth, - h: targetHeight, - mimetype: thumbnail.type, - size: thumbnail.size, - }, - w: inputWidth, - h: inputHeight, + let targetWidth = inputWidth; + let targetHeight = inputHeight; + if (targetHeight > MAX_HEIGHT) { + targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight)); + targetHeight = MAX_HEIGHT; + } + if (targetWidth > MAX_WIDTH) { + targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth)); + targetWidth = MAX_WIDTH; + } + + const canvas = document.createElement("canvas"); + canvas.width = targetWidth; + canvas.height = targetHeight; + canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight); + canvas.toBlob(function(thumbnail) { + deferred.resolve({ + info: { + thumbnail_info: { + w: targetWidth, + h: targetHeight, + mimetype: thumbnail.type, + size: thumbnail.size, }, - thumbnail: thumbnail, - }); - }, mimeType); - }); + w: inputWidth, + h: inputHeight, + }, + thumbnail: thumbnail, + }); + }, mimeType); + + return deferred.promise; } /** @@ -177,29 +179,30 @@ function infoForImageFile(matrixClient, roomId, imageFile) { * @return {Promise} A promise that resolves with the video image element. */ function loadVideoElement(videoFile) { - return new Promise((resolve, reject) => { - // Load the file into an html element - const video = document.createElement("video"); + const deferred = Promise.defer(); - const reader = new FileReader(); + // Load the file into an html element + const video = document.createElement("video"); - reader.onload = function(e) { - video.src = e.target.result; + const reader = new FileReader(); + reader.onload = function(e) { + video.src = e.target.result; - // Once ready, returns its size - // Wait until we have enough data to thumbnail the first frame. - video.onloadeddata = function() { - resolve(video); - }; - video.onerror = function(e) { - reject(e); - }; + // Once ready, returns its size + // Wait until we have enough data to thumbnail the first frame. + video.onloadeddata = function() { + deferred.resolve(video); }; - reader.onerror = function(e) { - reject(e); + video.onerror = function(e) { + deferred.reject(e); }; - reader.readAsDataURL(videoFile); - }); + }; + reader.onerror = function(e) { + deferred.reject(e); + }; + reader.readAsDataURL(videoFile); + + return deferred.promise; } /** @@ -233,16 +236,16 @@ function infoForVideoFile(matrixClient, roomId, videoFile) { * is read. */ function readFileAsArrayBuffer(file) { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = function(e) { - resolve(e.target.result); - }; - reader.onerror = function(e) { - reject(e); - }; - reader.readAsArrayBuffer(file); - }); + const deferred = Promise.defer(); + const reader = new FileReader(); + reader.onload = function(e) { + deferred.resolve(e.target.result); + }; + reader.onerror = function(e) { + deferred.reject(e); + }; + reader.readAsArrayBuffer(file); + return deferred.promise; } /** @@ -458,34 +461,33 @@ export default class ContentMessages { content.info.mimetype = file.type; } - const prom = new Promise((resolve) => { - if (file.type.indexOf('image/') == 0) { - content.msgtype = 'm.image'; - infoForImageFile(matrixClient, roomId, file).then((imageInfo)=>{ - extend(content.info, imageInfo); - resolve(); - }, (error)=>{ - console.error(error); - content.msgtype = 'm.file'; - resolve(); - }); - } else if (file.type.indexOf('audio/') == 0) { - content.msgtype = 'm.audio'; - resolve(); - } else if (file.type.indexOf('video/') == 0) { - content.msgtype = 'm.video'; - infoForVideoFile(matrixClient, roomId, file).then((videoInfo)=>{ - extend(content.info, videoInfo); - resolve(); - }, (error)=>{ - content.msgtype = 'm.file'; - resolve(); - }); - } else { + const def = Promise.defer(); + if (file.type.indexOf('image/') == 0) { + content.msgtype = 'm.image'; + infoForImageFile(matrixClient, roomId, file).then((imageInfo)=>{ + extend(content.info, imageInfo); + def.resolve(); + }, (error)=>{ + console.error(error); content.msgtype = 'm.file'; - resolve(); - } - }); + def.resolve(); + }); + } else if (file.type.indexOf('audio/') == 0) { + content.msgtype = 'm.audio'; + def.resolve(); + } else if (file.type.indexOf('video/') == 0) { + content.msgtype = 'm.video'; + infoForVideoFile(matrixClient, roomId, file).then((videoInfo)=>{ + extend(content.info, videoInfo); + def.resolve(); + }, (error)=>{ + content.msgtype = 'm.file'; + def.resolve(); + }); + } else { + content.msgtype = 'm.file'; + def.resolve(); + } const upload = { fileName: file.name || 'Attachment', @@ -507,7 +509,7 @@ export default class ContentMessages { dis.dispatch({action: 'upload_progress', upload: upload}); } - return prom.then(function() { + return def.promise.then(function() { // XXX: upload.promise must be the promise that // is returned by uploadFile as it has an abort() // method hacked onto it. diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 53a9b7a998..13f3abccb1 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -312,14 +312,18 @@ async function _restoreFromLocalStorage(opts) { function _handleLoadSessionFailure(e) { console.error("Unable to load session", e); + const def = Promise.defer(); const SessionRestoreErrorDialog = sdk.getComponent('views.dialogs.SessionRestoreErrorDialog'); - const modal = Modal.createTrackedDialog('Session Restore Error', '', SessionRestoreErrorDialog, { + Modal.createTrackedDialog('Session Restore Error', '', SessionRestoreErrorDialog, { error: e.message, + onFinished: (success) => { + def.resolve(success); + }, }); - return modal.finished.then(([success]) => { + return def.promise.then((success) => { if (success) { // user clicked continue. _clearStorage(); diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index d4b51081f4..ad5fa198a3 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -26,7 +26,6 @@ import { Room } from 'matrix-js-sdk'; import SettingsStore from "../../../settings/SettingsStore"; import Autocompleter from '../../../autocomplete/Autocompleter'; -import {sleep} from "../../../utils/promise"; const COMPOSER_SELECTED = 0; @@ -106,11 +105,13 @@ export default class Autocomplete extends React.Component { autocompleteDelay = 0; } - return new Promise((resolve) => { - this.debounceCompletionsRequest = setTimeout(() => { - resolve(this.processQuery(query, selection)); - }, autocompleteDelay); - }); + const deferred = Promise.defer(); + this.debounceCompletionsRequest = setTimeout(() => { + this.processQuery(query, selection).then(() => { + deferred.resolve(); + }); + }, autocompleteDelay); + return deferred.promise; } processQuery(query, selection) { @@ -196,16 +197,16 @@ export default class Autocomplete extends React.Component { } forceComplete() { - return new Promise((resolve) => { - this.setState({ - forceComplete: true, - hide: false, - }, () => { - this.complete(this.props.query, this.props.selection).then(() => { - resolve(this.countCompletions()); - }); + const done = Promise.defer(); + this.setState({ + forceComplete: true, + hide: false, + }, () => { + this.complete(this.props.query, this.props.selection).then(() => { + done.resolve(this.countCompletions()); }); }); + return done.promise; } onCompletionClicked(selectionOffset: number): boolean { diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 91292b19f9..a086efaa6d 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -178,12 +178,17 @@ module.exports = createReactClass({ }, _optionallySetEmail: function() { + const deferred = Promise.defer(); // Ask for an email otherwise the user has no way to reset their password const SetEmailDialog = sdk.getComponent("dialogs.SetEmailDialog"); - const modal = Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, { + Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, { title: _t('Do you want to set an email address?'), + onFinished: (confirmed) => { + // ignore confirmed, setting an email is optional + deferred.resolve(confirmed); + }, }); - return modal.finished.then(([confirmed]) => confirmed); + return deferred.promise; }, _onExportE2eKeysClicked: function() { diff --git a/src/rageshake/submit-rageshake.js b/src/rageshake/submit-rageshake.js index e772912e48..99c412a6ab 100644 --- a/src/rageshake/submit-rageshake.js +++ b/src/rageshake/submit-rageshake.js @@ -105,22 +105,26 @@ export default async function sendBugReport(bugReportEndpoint, opts) { } function _submitReport(endpoint, body, progressCallback) { - return new Promise((resolve, reject) => { - const req = new XMLHttpRequest(); - req.open("POST", endpoint); - req.timeout = 5 * 60 * 1000; - req.onreadystatechange = function() { - if (req.readyState === XMLHttpRequest.LOADING) { - progressCallback(_t("Waiting for response from server")); - } else if (req.readyState === XMLHttpRequest.DONE) { - // on done - if (req.status < 200 || req.status >= 400) { - reject(new Error(`HTTP ${req.status}`)); - return; - } - resolve(); - } - }; - req.send(body); - }); + const deferred = Promise.defer(); + + const req = new XMLHttpRequest(); + req.open("POST", endpoint); + req.timeout = 5 * 60 * 1000; + req.onreadystatechange = function() { + if (req.readyState === XMLHttpRequest.LOADING) { + progressCallback(_t("Waiting for response from server")); + } else if (req.readyState === XMLHttpRequest.DONE) { + on_done(); + } + }; + req.send(body); + return deferred.promise; + + function on_done() { + if (req.status < 200 || req.status >= 400) { + deferred.reject(new Error(`HTTP ${req.status}`)); + return; + } + deferred.resolve(); + } } From 217dfc3eed0c5f018a986ddc4f1c05a1a31b5960 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 12 Nov 2019 11:40:38 +0000 Subject: [PATCH 055/334] Replace all trivial Promise.defer usages with regular Promises (cherry picked from commit 44401d73b44b6de4daeb91613456d2760d50456d) --- src/ContentMessages.js | 174 +++++++++--------- src/Lifecycle.js | 8 +- src/components/views/rooms/Autocomplete.js | 29 ++- .../views/settings/ChangePassword.js | 9 +- src/rageshake/submit-rageshake.js | 40 ++-- 5 files changed, 122 insertions(+), 138 deletions(-) diff --git a/src/ContentMessages.js b/src/ContentMessages.js index 2d58622db8..dab8de2465 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -59,40 +59,38 @@ export class UploadCanceledError extends Error {} * and a thumbnail key. */ function createThumbnail(element, inputWidth, inputHeight, mimeType) { - const deferred = Promise.defer(); + return new Promise((resolve) => { + let targetWidth = inputWidth; + let targetHeight = inputHeight; + if (targetHeight > MAX_HEIGHT) { + targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight)); + targetHeight = MAX_HEIGHT; + } + if (targetWidth > MAX_WIDTH) { + targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth)); + targetWidth = MAX_WIDTH; + } - let targetWidth = inputWidth; - let targetHeight = inputHeight; - if (targetHeight > MAX_HEIGHT) { - targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight)); - targetHeight = MAX_HEIGHT; - } - if (targetWidth > MAX_WIDTH) { - targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth)); - targetWidth = MAX_WIDTH; - } - - const canvas = document.createElement("canvas"); - canvas.width = targetWidth; - canvas.height = targetHeight; - canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight); - canvas.toBlob(function(thumbnail) { - deferred.resolve({ - info: { - thumbnail_info: { - w: targetWidth, - h: targetHeight, - mimetype: thumbnail.type, - size: thumbnail.size, + const canvas = document.createElement("canvas"); + canvas.width = targetWidth; + canvas.height = targetHeight; + canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight); + canvas.toBlob(function(thumbnail) { + resolve({ + info: { + thumbnail_info: { + w: targetWidth, + h: targetHeight, + mimetype: thumbnail.type, + size: thumbnail.size, + }, + w: inputWidth, + h: inputHeight, }, - w: inputWidth, - h: inputHeight, - }, - thumbnail: thumbnail, - }); - }, mimeType); - - return deferred.promise; + thumbnail: thumbnail, + }); + }, mimeType); + }); } /** @@ -179,30 +177,29 @@ function infoForImageFile(matrixClient, roomId, imageFile) { * @return {Promise} A promise that resolves with the video image element. */ function loadVideoElement(videoFile) { - const deferred = Promise.defer(); + return new Promise((resolve, reject) => { + // Load the file into an html element + const video = document.createElement("video"); - // Load the file into an html element - const video = document.createElement("video"); + const reader = new FileReader(); - const reader = new FileReader(); - reader.onload = function(e) { - video.src = e.target.result; + reader.onload = function(e) { + video.src = e.target.result; - // Once ready, returns its size - // Wait until we have enough data to thumbnail the first frame. - video.onloadeddata = function() { - deferred.resolve(video); + // Once ready, returns its size + // Wait until we have enough data to thumbnail the first frame. + video.onloadeddata = function() { + resolve(video); + }; + video.onerror = function(e) { + reject(e); + }; }; - video.onerror = function(e) { - deferred.reject(e); + reader.onerror = function(e) { + reject(e); }; - }; - reader.onerror = function(e) { - deferred.reject(e); - }; - reader.readAsDataURL(videoFile); - - return deferred.promise; + reader.readAsDataURL(videoFile); + }); } /** @@ -236,16 +233,16 @@ function infoForVideoFile(matrixClient, roomId, videoFile) { * is read. */ function readFileAsArrayBuffer(file) { - const deferred = Promise.defer(); - const reader = new FileReader(); - reader.onload = function(e) { - deferred.resolve(e.target.result); - }; - reader.onerror = function(e) { - deferred.reject(e); - }; - reader.readAsArrayBuffer(file); - return deferred.promise; + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = function(e) { + resolve(e.target.result); + }; + reader.onerror = function(e) { + reject(e); + }; + reader.readAsArrayBuffer(file); + }); } /** @@ -461,33 +458,34 @@ export default class ContentMessages { content.info.mimetype = file.type; } - const def = Promise.defer(); - if (file.type.indexOf('image/') == 0) { - content.msgtype = 'm.image'; - infoForImageFile(matrixClient, roomId, file).then((imageInfo)=>{ - extend(content.info, imageInfo); - def.resolve(); - }, (error)=>{ - console.error(error); + const prom = new Promise((resolve) => { + if (file.type.indexOf('image/') == 0) { + content.msgtype = 'm.image'; + infoForImageFile(matrixClient, roomId, file).then((imageInfo)=>{ + extend(content.info, imageInfo); + resolve(); + }, (error)=>{ + console.error(error); + content.msgtype = 'm.file'; + resolve(); + }); + } else if (file.type.indexOf('audio/') == 0) { + content.msgtype = 'm.audio'; + resolve(); + } else if (file.type.indexOf('video/') == 0) { + content.msgtype = 'm.video'; + infoForVideoFile(matrixClient, roomId, file).then((videoInfo)=>{ + extend(content.info, videoInfo); + resolve(); + }, (error)=>{ + content.msgtype = 'm.file'; + resolve(); + }); + } else { content.msgtype = 'm.file'; - def.resolve(); - }); - } else if (file.type.indexOf('audio/') == 0) { - content.msgtype = 'm.audio'; - def.resolve(); - } else if (file.type.indexOf('video/') == 0) { - content.msgtype = 'm.video'; - infoForVideoFile(matrixClient, roomId, file).then((videoInfo)=>{ - extend(content.info, videoInfo); - def.resolve(); - }, (error)=>{ - content.msgtype = 'm.file'; - def.resolve(); - }); - } else { - content.msgtype = 'm.file'; - def.resolve(); - } + resolve(); + } + }); const upload = { fileName: file.name || 'Attachment', @@ -509,7 +507,7 @@ export default class ContentMessages { dis.dispatch({action: 'upload_progress', upload: upload}); } - return def.promise.then(function() { + return prom.then(function() { // XXX: upload.promise must be the promise that // is returned by uploadFile as it has an abort() // method hacked onto it. diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 13f3abccb1..53a9b7a998 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -312,18 +312,14 @@ async function _restoreFromLocalStorage(opts) { function _handleLoadSessionFailure(e) { console.error("Unable to load session", e); - const def = Promise.defer(); const SessionRestoreErrorDialog = sdk.getComponent('views.dialogs.SessionRestoreErrorDialog'); - Modal.createTrackedDialog('Session Restore Error', '', SessionRestoreErrorDialog, { + const modal = Modal.createTrackedDialog('Session Restore Error', '', SessionRestoreErrorDialog, { error: e.message, - onFinished: (success) => { - def.resolve(success); - }, }); - return def.promise.then((success) => { + return modal.finished.then(([success]) => { if (success) { // user clicked continue. _clearStorage(); diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index ad5fa198a3..d4b51081f4 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -26,6 +26,7 @@ import { Room } from 'matrix-js-sdk'; import SettingsStore from "../../../settings/SettingsStore"; import Autocompleter from '../../../autocomplete/Autocompleter'; +import {sleep} from "../../../utils/promise"; const COMPOSER_SELECTED = 0; @@ -105,13 +106,11 @@ export default class Autocomplete extends React.Component { autocompleteDelay = 0; } - const deferred = Promise.defer(); - this.debounceCompletionsRequest = setTimeout(() => { - this.processQuery(query, selection).then(() => { - deferred.resolve(); - }); - }, autocompleteDelay); - return deferred.promise; + return new Promise((resolve) => { + this.debounceCompletionsRequest = setTimeout(() => { + resolve(this.processQuery(query, selection)); + }, autocompleteDelay); + }); } processQuery(query, selection) { @@ -197,16 +196,16 @@ export default class Autocomplete extends React.Component { } forceComplete() { - const done = Promise.defer(); - this.setState({ - forceComplete: true, - hide: false, - }, () => { - this.complete(this.props.query, this.props.selection).then(() => { - done.resolve(this.countCompletions()); + return new Promise((resolve) => { + this.setState({ + forceComplete: true, + hide: false, + }, () => { + this.complete(this.props.query, this.props.selection).then(() => { + resolve(this.countCompletions()); + }); }); }); - return done.promise; } onCompletionClicked(selectionOffset: number): boolean { diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index a086efaa6d..91292b19f9 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -178,17 +178,12 @@ module.exports = createReactClass({ }, _optionallySetEmail: function() { - const deferred = Promise.defer(); // Ask for an email otherwise the user has no way to reset their password const SetEmailDialog = sdk.getComponent("dialogs.SetEmailDialog"); - Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, { + const modal = Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, { title: _t('Do you want to set an email address?'), - onFinished: (confirmed) => { - // ignore confirmed, setting an email is optional - deferred.resolve(confirmed); - }, }); - return deferred.promise; + return modal.finished.then(([confirmed]) => confirmed); }, _onExportE2eKeysClicked: function() { diff --git a/src/rageshake/submit-rageshake.js b/src/rageshake/submit-rageshake.js index 99c412a6ab..e772912e48 100644 --- a/src/rageshake/submit-rageshake.js +++ b/src/rageshake/submit-rageshake.js @@ -105,26 +105,22 @@ export default async function sendBugReport(bugReportEndpoint, opts) { } function _submitReport(endpoint, body, progressCallback) { - const deferred = Promise.defer(); - - const req = new XMLHttpRequest(); - req.open("POST", endpoint); - req.timeout = 5 * 60 * 1000; - req.onreadystatechange = function() { - if (req.readyState === XMLHttpRequest.LOADING) { - progressCallback(_t("Waiting for response from server")); - } else if (req.readyState === XMLHttpRequest.DONE) { - on_done(); - } - }; - req.send(body); - return deferred.promise; - - function on_done() { - if (req.status < 200 || req.status >= 400) { - deferred.reject(new Error(`HTTP ${req.status}`)); - return; - } - deferred.resolve(); - } + return new Promise((resolve, reject) => { + const req = new XMLHttpRequest(); + req.open("POST", endpoint); + req.timeout = 5 * 60 * 1000; + req.onreadystatechange = function() { + if (req.readyState === XMLHttpRequest.LOADING) { + progressCallback(_t("Waiting for response from server")); + } else if (req.readyState === XMLHttpRequest.DONE) { + // on done + if (req.status < 200 || req.status >= 400) { + reject(new Error(`HTTP ${req.status}`)); + return; + } + resolve(); + } + }; + req.send(body); + }); } From 2ea239d1923170ef9d226eeae99dcfc12432dc30 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 12 Nov 2019 11:45:28 +0000 Subject: [PATCH 056/334] Replace rest of defer usages using small shim. Add homebrew promise utils (cherry picked from commit 6850c147393ba7be8b98d97dfc8d7244fa503461) --- src/Modal.js | 3 +- src/components/structures/MatrixChat.js | 5 +-- src/utils/MultiInviter.js | 3 +- src/utils/promise.js | 46 +++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 src/utils/promise.js diff --git a/src/Modal.js b/src/Modal.js index 26c9da8bbb..cb19731f01 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -24,6 +24,7 @@ import sdk from './index'; import dis from './dispatcher'; import { _t } from './languageHandler'; import Promise from "bluebird"; +import {defer} from "./utils/promise"; const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer"; @@ -202,7 +203,7 @@ class ModalManager { } _getCloseFn(modal, props) { - const deferred = Promise.defer(); + const deferred = defer(); return [(...args) => { deferred.resolve(args); if (props && props.onFinished) props.onFinished.apply(null, args); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index da67416400..d12eba88f7 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -60,6 +60,7 @@ import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils"; import DMRoomMap from '../../utils/DMRoomMap'; import { countRoomsWithNotif } from '../../RoomNotifs'; import { setTheme } from "../../theme"; +import {defer} from "../../utils/promise"; // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. @@ -236,7 +237,7 @@ export default createReactClass({ // Used by _viewRoom before getting state from sync this.firstSyncComplete = false; - this.firstSyncPromise = Promise.defer(); + this.firstSyncPromise = defer(); if (this.props.config.sync_timeline_limit) { MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit; @@ -1261,7 +1262,7 @@ export default createReactClass({ // since we're about to start the client and therefore about // to do the first sync this.firstSyncComplete = false; - this.firstSyncPromise = Promise.defer(); + this.firstSyncPromise = defer(); const cli = MatrixClientPeg.get(); const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog'); diff --git a/src/utils/MultiInviter.js b/src/utils/MultiInviter.js index e8995b46d7..de5c2e7610 100644 --- a/src/utils/MultiInviter.js +++ b/src/utils/MultiInviter.js @@ -24,6 +24,7 @@ import {_t} from "../languageHandler"; import sdk from "../index"; import Modal from "../Modal"; import SettingsStore from "../settings/SettingsStore"; +import {defer} from "./promise"; /** * Invites multiple addresses to a room or group, handling rate limiting from the server @@ -71,7 +72,7 @@ export default class MultiInviter { }; } } - this.deferred = Promise.defer(); + this.deferred = defer(); this._inviteMore(0); return this.deferred.promise; diff --git a/src/utils/promise.js b/src/utils/promise.js new file mode 100644 index 0000000000..dd10f7fdd7 --- /dev/null +++ b/src/utils/promise.js @@ -0,0 +1,46 @@ +/* +Copyright 2019 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. +*/ + +// @flow + +// Returns a promise which resolves with a given value after the given number of ms +export const sleep = (ms: number, value: any): Promise => new Promise((resolve => { setTimeout(resolve, ms, value); })); + +// Returns a promise which resolves when the input promise resolves with its value +// or when the timeout of ms is reached with the value of given timeoutValue +export async function timeout(promise: Promise, timeoutValue: any, ms: number): Promise { + const timeoutPromise = new Promise((resolve) => { + const timeoutId = setTimeout(resolve, ms, timeoutValue); + promise.then(() => { + clearTimeout(timeoutId); + }); + }); + + return Promise.race([promise, timeoutPromise]); +} + +// Returns a Deferred +export function defer(): {resolve: () => {}, reject: () => {}, promise: Promise} { + let resolve; + let reject; + + const promise = new Promise((_resolve, _reject) => { + resolve = _resolve; + reject = _reject; + }); + + return {resolve, reject, promise}; +} From 2b34cf4362ab03b0409da6086199ef2eab54dee7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 12 Nov 2019 11:46:58 +0000 Subject: [PATCH 057/334] Replace Promise.delay with promise utils sleep (cherry picked from commit 0a21957b2cac0cc2d0169f428187bc2e468251a9) --- src/components/structures/GroupView.js | 9 +++++---- .../views/context_menus/RoomTileContextMenu.js | 7 ++++--- src/components/views/dialogs/AddressPickerDialog.js | 3 ++- .../views/settings/tabs/user/SecurityUserSettingsTab.js | 3 ++- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 4d8f47003c..4056557a7c 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -38,6 +38,7 @@ import FlairStore from '../../stores/FlairStore'; import { showGroupAddRoomDialog } from '../../GroupAddressPicker'; import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks"; import {Group} from "matrix-js-sdk"; +import {sleep} from "../../utils/promise"; const LONG_DESC_PLACEHOLDER = _td( `

    HTML for your community's page

    @@ -692,7 +693,7 @@ export default createReactClass({ // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the // spinner disappearing after we have fetched new group data. - await Promise.delay(500); + await sleep(500); GroupStore.acceptGroupInvite(this.props.groupId).then(() => { // don't reset membershipBusy here: wait for the membership change to come down the sync @@ -711,7 +712,7 @@ export default createReactClass({ // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the // spinner disappearing after we have fetched new group data. - await Promise.delay(500); + await sleep(500); GroupStore.leaveGroup(this.props.groupId).then(() => { // don't reset membershipBusy here: wait for the membership change to come down the sync @@ -735,7 +736,7 @@ export default createReactClass({ // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the // spinner disappearing after we have fetched new group data. - await Promise.delay(500); + await sleep(500); GroupStore.joinGroup(this.props.groupId).then(() => { // don't reset membershipBusy here: wait for the membership change to come down the sync @@ -787,7 +788,7 @@ export default createReactClass({ // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the // spinner disappearing after we have fetched new group data. - await Promise.delay(500); + await sleep(500); GroupStore.leaveGroup(this.props.groupId).then(() => { // don't reset membershipBusy here: wait for the membership change to come down the sync diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js index 9bb573026f..541daef27f 100644 --- a/src/components/views/context_menus/RoomTileContextMenu.js +++ b/src/components/views/context_menus/RoomTileContextMenu.js @@ -32,6 +32,7 @@ import * as RoomNotifs from '../../../RoomNotifs'; import Modal from '../../../Modal'; import RoomListActions from '../../../actions/RoomListActions'; import RoomViewStore from '../../../stores/RoomViewStore'; +import {sleep} from "../../../utils/promise"; module.exports = createReactClass({ displayName: 'RoomTileContextMenu', @@ -62,7 +63,7 @@ module.exports = createReactClass({ _toggleTag: function(tagNameOn, tagNameOff) { if (!MatrixClientPeg.get().isGuest()) { - Promise.delay(500).then(() => { + sleep(500).then(() => { dis.dispatch(RoomListActions.tagRoom( MatrixClientPeg.get(), this.props.room, @@ -119,7 +120,7 @@ module.exports = createReactClass({ Rooms.guessAndSetDMRoom( this.props.room, newIsDirectMessage, - ).delay(500).finally(() => { + ).then(sleep(500)).finally(() => { // Close the context menu if (this.props.onFinished) { this.props.onFinished(); @@ -193,7 +194,7 @@ module.exports = createReactClass({ RoomNotifs.setRoomNotifsState(roomId, newState).done(() => { // delay slightly so that the user can see their state change // before closing the menu - return Promise.delay(500).then(() => { + return sleep(500).then(() => { if (this._unmounted) return; // Close the context menu if (this.props.onFinished) { diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index fb779fa96f..dc61f23956 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -32,6 +32,7 @@ import * as Email from '../../../email'; import IdentityAuthClient from '../../../IdentityAuthClient'; import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../utils/IdentityServerUtils'; import { abbreviateUrl } from '../../../utils/UrlUtils'; +import {sleep} from "../../../utils/promise"; const TRUNCATE_QUERY_LIST = 40; const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200; @@ -533,7 +534,7 @@ module.exports = createReactClass({ }; // wait a bit to let the user finish typing - await Promise.delay(500); + await sleep(500); if (cancelled) return null; try { diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js index e619791b01..222af48fa1 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js @@ -25,6 +25,7 @@ import Analytics from "../../../../../Analytics"; import Promise from "bluebird"; import Modal from "../../../../../Modal"; import sdk from "../../../../.."; +import {sleep} from "../../../../../utils/promise"; export class IgnoredUser extends React.Component { static propTypes = { @@ -129,7 +130,7 @@ export default class SecurityUserSettingsTab extends React.Component { if (e.errcode === "M_LIMIT_EXCEEDED") { // Add a delay between each invite change in order to avoid rate // limiting by the server. - await Promise.delay(e.retry_after_ms || 2500); + await sleep(e.retry_after_ms || 2500); // Redo last action i--; From f5d467b3917849cb1f17445b75fe9ea1ba38098e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 12 Nov 2019 12:26:07 +0000 Subject: [PATCH 058/334] delint --- src/components/views/context_menus/RoomTileContextMenu.js | 1 - src/components/views/dialogs/AddressPickerDialog.js | 1 - .../views/settings/tabs/user/SecurityUserSettingsTab.js | 1 - 3 files changed, 3 deletions(-) diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js index 541daef27f..fb056ee47f 100644 --- a/src/components/views/context_menus/RoomTileContextMenu.js +++ b/src/components/views/context_menus/RoomTileContextMenu.js @@ -17,7 +17,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index dc61f23956..24d8b96e0c 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -25,7 +25,6 @@ import { _t, _td } from '../../../languageHandler'; import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; import dis from '../../../dispatcher'; -import Promise from 'bluebird'; import { addressTypes, getAddressType } from '../../../UserAddress.js'; import GroupStore from '../../../stores/GroupStore'; import * as Email from '../../../email'; diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js index 222af48fa1..0732bcf926 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js @@ -22,7 +22,6 @@ import MatrixClientPeg from "../../../../../MatrixClientPeg"; import * as FormattingUtils from "../../../../../utils/FormattingUtils"; import AccessibleButton from "../../../elements/AccessibleButton"; import Analytics from "../../../../../Analytics"; -import Promise from "bluebird"; import Modal from "../../../../../Modal"; import sdk from "../../../../.."; import {sleep} from "../../../../../utils/promise"; From cfdcf45ac6eb019242b5d969ce8018fae195caec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 12 Nov 2019 13:29:07 +0100 Subject: [PATCH 059/334] MatrixChat: Move the event indexing logic into separate modules. --- src/EventIndexPeg.js | 74 +++++ src/EventIndexing.js | 404 ++++++++++++++++++++++++ src/Lifecycle.js | 2 + src/MatrixClientPeg.js | 4 +- src/components/structures/MatrixChat.js | 371 ++-------------------- 5 files changed, 499 insertions(+), 356 deletions(-) create mode 100644 src/EventIndexPeg.js create mode 100644 src/EventIndexing.js diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js new file mode 100644 index 0000000000..794450e4b7 --- /dev/null +++ b/src/EventIndexPeg.js @@ -0,0 +1,74 @@ +/* +Copyright 2019 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* + * Holds the current Platform object used by the code to do anything + * specific to the platform we're running on (eg. web, electron) + * Platforms are provided by the app layer. + * This allows the app layer to set a Platform without necessarily + * having to have a MatrixChat object + */ + +import PlatformPeg from "./PlatformPeg"; +import EventIndex from "./EventIndexing"; +import MatrixClientPeg from "./MatrixClientPeg"; + +class EventIndexPeg { + constructor() { + this.index = null; + } + + /** + * Returns the current Event index object for the application. Can be null + * if the platform doesn't support event indexing. + */ + get() { + return this.index; + } + + /** Create a new EventIndex and initialize it if the platform supports it. + * Returns true if an EventIndex was successfully initialized, false + * otherwise. + */ + async init() { + const platform = PlatformPeg.get(); + if (!platform.supportsEventIndexing()) return false; + + let index = new EventIndex(); + + const userId = MatrixClientPeg.get().getUserId(); + // TODO log errors here and return false if it errors out. + await index.init(userId); + this.index = index; + + return true + } + + async stop() { + if (this.index == null) return; + index.stopCrawler(); + } + + async deleteEventIndex() { + if (this.index == null) return; + index.deleteEventIndex(); + } +} + +if (!global.mxEventIndexPeg) { + global.mxEventIndexPeg = new EventIndexPeg(); +} +module.exports = global.mxEventIndexPeg; diff --git a/src/EventIndexing.js b/src/EventIndexing.js new file mode 100644 index 0000000000..21ee8f3da6 --- /dev/null +++ b/src/EventIndexing.js @@ -0,0 +1,404 @@ +/* +Copyright 2019 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import PlatformPeg from "./PlatformPeg"; +import MatrixClientPeg from "./MatrixClientPeg"; + +/** + * Event indexing class that wraps the platform specific event indexing. + */ +export default class EventIndexer { + constructor() { + this.crawlerChekpoints = []; + // The time that the crawler will wait between /rooms/{room_id}/messages + // requests + this._crawler_timeout = 3000; + this._crawlerRef = null; + this.liveEventsForIndex = new Set(); + } + + async init(userId) { + const platform = PlatformPeg.get(); + if (!platform.supportsEventIndexing()) return false; + platform.initEventIndex(userId); + } + + async onSync(state, prevState, data) { + const platform = PlatformPeg.get(); + if (!platform.supportsEventIndexing()) return; + + if (prevState === null && state === "PREPARED") { + // Load our stored checkpoints, if any. + this.crawlerChekpoints = await platform.loadCheckpoints(); + console.log("Seshat: Loaded checkpoints", + this.crawlerChekpoints); + return; + } + + if (prevState === "PREPARED" && state === "SYNCING") { + const addInitialCheckpoints = async () => { + const client = MatrixClientPeg.get(); + const rooms = client.getRooms(); + + const isRoomEncrypted = (room) => { + return client.isRoomEncrypted(room.roomId); + }; + + // We only care to crawl the encrypted rooms, non-encrytped + // rooms can use the search provided by the Homeserver. + const encryptedRooms = rooms.filter(isRoomEncrypted); + + console.log("Seshat: Adding initial crawler checkpoints"); + + // Gather the prev_batch tokens and create checkpoints for + // our message crawler. + await Promise.all(encryptedRooms.map(async (room) => { + const timeline = room.getLiveTimeline(); + const token = timeline.getPaginationToken("b"); + + console.log("Seshat: Got token for indexer", + room.roomId, token); + + const backCheckpoint = { + roomId: room.roomId, + token: token, + direction: "b", + }; + + const forwardCheckpoint = { + roomId: room.roomId, + token: token, + direction: "f", + }; + + await platform.addCrawlerCheckpoint(backCheckpoint); + await platform.addCrawlerCheckpoint(forwardCheckpoint); + this.crawlerChekpoints.push(backCheckpoint); + this.crawlerChekpoints.push(forwardCheckpoint); + })); + }; + + // If our indexer is empty we're most likely running Riot the + // first time with indexing support or running it with an + // initial sync. Add checkpoints to crawl our encrypted rooms. + const eventIndexWasEmpty = await platform.isEventIndexEmpty(); + if (eventIndexWasEmpty) await addInitialCheckpoints(); + + // Start our crawler. + this.startCrawler(); + return; + } + + if (prevState === "SYNCING" && state === "SYNCING") { + // A sync was done, presumably we queued up some live events, + // commit them now. + console.log("Seshat: Committing events"); + await platform.commitLiveEvents(); + return; + } + } + + async onRoomTimeline(ev, room, toStartOfTimeline, removed, data) { + const platform = PlatformPeg.get(); + if (!platform.supportsEventIndexing()) return; + + // We only index encrypted rooms locally. + if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; + + // If it isn't a live event or if it's redacted there's nothing to + // do. + if (toStartOfTimeline || !data || !data.liveEvent + || ev.isRedacted()) { + return; + } + + // If the event is not yet decrypted mark it for the + // Event.decrypted callback. + if (ev.isBeingDecrypted()) { + const eventId = ev.getId(); + this.liveEventsForIndex.add(eventId); + } else { + // If the event is decrypted or is unencrypted add it to the + // index now. + await this.addLiveEventToIndex(ev); + } + } + + async onEventDecrypted(ev, err) { + const platform = PlatformPeg.get(); + if (!platform.supportsEventIndexing()) return; + + const eventId = ev.getId(); + + // If the event isn't in our live event set, ignore it. + if (!this.liveEventsForIndex.delete(eventId)) return; + if (err) return; + await this.addLiveEventToIndex(ev); + } + + async addLiveEventToIndex(ev) { + const platform = PlatformPeg.get(); + if (!platform.supportsEventIndexing()) return; + + if (["m.room.message", "m.room.name", "m.room.topic"] + .indexOf(ev.getType()) == -1) { + return; + } + + const e = ev.toJSON().decrypted; + const profile = { + displayname: ev.sender.rawDisplayName, + avatar_url: ev.sender.getMxcAvatarUrl(), + }; + + platform.addEventToIndex(e, profile); + } + + async crawlerFunc(handle) { + // TODO either put this in a better place or find a library provided + // method that does this. + const sleep = async (ms) => { + return new Promise(resolve => setTimeout(resolve, ms)); + }; + + let cancelled = false; + + console.log("Seshat: Started crawler function"); + + const client = MatrixClientPeg.get(); + const platform = PlatformPeg.get(); + + handle.cancel = () => { + cancelled = true; + }; + + while (!cancelled) { + // This is a low priority task and we don't want to spam our + // Homeserver with /messages requests so we set a hefty timeout + // here. + await sleep(this._crawler_timeout); + + console.log("Seshat: Running the crawler loop."); + + if (cancelled) { + console.log("Seshat: Cancelling the crawler."); + break; + } + + const checkpoint = this.crawlerChekpoints.shift(); + + /// There is no checkpoint available currently, one may appear if + // a sync with limited room timelines happens, so go back to sleep. + if (checkpoint === undefined) { + continue; + } + + console.log("Seshat: crawling using checkpoint", checkpoint); + + // We have a checkpoint, let us fetch some messages, again, very + // conservatively to not bother our Homeserver too much. + const eventMapper = client.getEventMapper(); + // TODO we need to ensure to use member lazy loading with this + // request so we get the correct profiles. + let res; + + try { + res = await client._createMessagesRequest( + checkpoint.roomId, checkpoint.token, 100, + checkpoint.direction); + } catch (e) { + console.log("Seshat: Error crawling events:", e); + this.crawlerChekpoints.push(checkpoint); + continue + } + + if (res.chunk.length === 0) { + console.log("Seshat: Done with the checkpoint", checkpoint); + // We got to the start/end of our timeline, lets just + // delete our checkpoint and go back to sleep. + await platform.removeCrawlerCheckpoint(checkpoint); + continue; + } + + // Convert the plain JSON events into Matrix events so they get + // decrypted if necessary. + const matrixEvents = res.chunk.map(eventMapper); + let stateEvents = []; + if (res.state !== undefined) { + stateEvents = res.state.map(eventMapper); + } + + const profiles = {}; + + stateEvents.forEach(ev => { + if (ev.event.content && + ev.event.content.membership === "join") { + profiles[ev.event.sender] = { + displayname: ev.event.content.displayname, + avatar_url: ev.event.content.avatar_url, + }; + } + }); + + const decryptionPromises = []; + + matrixEvents.forEach(ev => { + if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) { + // TODO the decryption promise is a private property, this + // should either be made public or we should convert the + // event that gets fired when decryption is done into a + // promise using the once event emitter method: + // https://nodejs.org/api/events.html#events_events_once_emitter_name + decryptionPromises.push(ev._decryptionPromise); + } + }); + + // Let us wait for all the events to get decrypted. + await Promise.all(decryptionPromises); + + // We filter out events for which decryption failed, are redacted + // or aren't of a type that we know how to index. + const isValidEvent = (value) => { + return ([ + "m.room.message", + "m.room.name", + "m.room.topic", + ].indexOf(value.getType()) >= 0 + && !value.isRedacted() && !value.isDecryptionFailure() + ); + // TODO do we need to check if the event has all the valid + // attributes? + }; + + // TODO if there ar no events at this point we're missing a lot + // decryption keys, do we wan't to retry this checkpoint at a later + // stage? + const filteredEvents = matrixEvents.filter(isValidEvent); + + // Let us convert the events back into a format that Seshat can + // consume. + const events = filteredEvents.map((ev) => { + const jsonEvent = ev.toJSON(); + + let e; + if (ev.isEncrypted()) e = jsonEvent.decrypted; + else e = jsonEvent; + + let profile = {}; + if (e.sender in profiles) profile = profiles[e.sender]; + const object = { + event: e, + profile: profile, + }; + return object; + }); + + // Create a new checkpoint so we can continue crawling the room for + // messages. + const newCheckpoint = { + roomId: checkpoint.roomId, + token: res.end, + fullCrawl: checkpoint.fullCrawl, + direction: checkpoint.direction, + }; + + console.log( + "Seshat: Crawled room", + client.getRoom(checkpoint.roomId).name, + "and fetched", events.length, "events.", + ); + + try { + const eventsAlreadyAdded = await platform.addHistoricEvents( + events, newCheckpoint, checkpoint); + // If all events were already indexed we assume that we catched + // up with our index and don't need to crawl the room further. + // Let us delete the checkpoint in that case, otherwise push + // the new checkpoint to be used by the crawler. + if (eventsAlreadyAdded === true && newCheckpoint.fullCrawl !== true) { + console.log("Seshat: Checkpoint had already all events", + "added, stopping the crawl", checkpoint); + await platform.removeCrawlerCheckpoint(newCheckpoint); + } else { + this.crawlerChekpoints.push(newCheckpoint); + } + } catch (e) { + console.log("Seshat: Error durring a crawl", e); + // An error occured, put the checkpoint back so we + // can retry. + this.crawlerChekpoints.push(checkpoint); + } + } + + console.log("Seshat: Stopping crawler function"); + } + + async addCheckpointForLimitedRoom(roomId) { + const platform = PlatformPeg.get(); + if (!platform.supportsEventIndexing()) return; + if (!MatrixClientPeg.get().isRoomEncrypted(roomId)) return; + + const client = MatrixClientPeg.get(); + const room = client.getRoom(roomId); + + if (room === null) return; + + const timeline = room.getLiveTimeline(); + const token = timeline.getPaginationToken("b"); + + const backwardsCheckpoint = { + roomId: room.roomId, + token: token, + fullCrawl: false, + direction: "b", + }; + + const forwardsCheckpoint = { + roomId: room.roomId, + token: token, + fullCrawl: false, + direction: "f", + }; + + console.log("Seshat: Added checkpoint because of a limited timeline", + backwardsCheckpoint, forwardsCheckpoint); + + await platform.addCrawlerCheckpoint(backwardsCheckpoint); + await platform.addCrawlerCheckpoint(forwardsCheckpoint); + + this.crawlerChekpoints.push(backwardsCheckpoint); + this.crawlerChekpoints.push(forwardsCheckpoint); + } + + async deleteEventIndex() { + if (platform.supportsEventIndexing()) { + console.log("Seshat: Deleting event index."); + this.crawlerRef.cancel(); + await platform.deleteEventIndex(); + } + } + + startCrawler() { + const crawlerHandle = {}; + this.crawlerFunc(crawlerHandle); + this.crawlerRef = crawlerHandle; + } + + stopCrawler() { + this._crawlerRef.cancel(); + this._crawlerRef = null; + } +} diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 7490c5d464..0b44f2ed84 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -20,6 +20,7 @@ import Promise from 'bluebird'; import Matrix from 'matrix-js-sdk'; import MatrixClientPeg from './MatrixClientPeg'; +import EventIndexPeg from './EventIndexPeg'; import createMatrixClient from './utils/createMatrixClient'; import Analytics from './Analytics'; import Notifier from './Notifier'; @@ -587,6 +588,7 @@ async function startMatrixClient(startSyncing=true) { if (startSyncing) { await MatrixClientPeg.start(); + await EventIndexPeg.init(); } else { console.warn("Caller requested only auxiliary services be started"); await MatrixClientPeg.assign(); diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 5c5ee6e4ec..6c5b465bb0 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -31,6 +31,7 @@ import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientB import * as StorageManager from './utils/StorageManager'; import IdentityAuthClient from './IdentityAuthClient'; import PlatformPeg from "./PlatformPeg"; +import EventIndexPeg from "./EventIndexPeg"; interface MatrixClientCreds { homeserverUrl: string, @@ -223,9 +224,6 @@ class MatrixClientPeg { this.matrixClient = createMatrixClient(opts); - const platform = PlatformPeg.get(); - if (platform.supportsEventIndexing()) platform.initEventIndex(creds.userId); - // we're going to add eventlisteners for each matrix event tile, so the // potential number of event listeners is quite high. this.matrixClient.setMaxListeners(500); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 402790df98..d006247151 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -31,6 +31,7 @@ import Analytics from "../../Analytics"; import { DecryptionFailureTracker } from "../../DecryptionFailureTracker"; import MatrixClientPeg from "../../MatrixClientPeg"; import PlatformPeg from "../../PlatformPeg"; +import EventIndexPeg from "../../EventIndexPeg"; import SdkConfig from "../../SdkConfig"; import * as RoomListSorter from "../../RoomListSorter"; import dis from "../../dispatcher"; @@ -1224,12 +1225,6 @@ export default createReactClass({ _onLoggedOut: async function() { const platform = PlatformPeg.get(); - if (platform.supportsEventIndexing()) { - console.log("Seshat: Deleting event index."); - this.crawlerRef.cancel(); - await platform.deleteEventIndex(); - } - this.notifyNewScreen('login'); this.setStateForNewView({ view: VIEWS.LOGIN, @@ -1270,8 +1265,6 @@ export default createReactClass({ // to do the first sync this.firstSyncComplete = false; this.firstSyncPromise = Promise.defer(); - this.crawlerChekpoints = []; - this.liveEventsForIndex = new Set(); const cli = MatrixClientPeg.get(); const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog'); @@ -1284,7 +1277,10 @@ export default createReactClass({ cli.setCanResetTimelineCallback(async function(roomId) { console.log("Request to reset timeline in room ", roomId, " viewing:", self.state.currentRoomId); // TODO is there a better place to plug this in - await self.addCheckpointForLimitedRoom(roomId); + const eventIndex = EventIndexPeg.get(); + if (eventIndex !== null) { + await eventIndex.addCheckpointForLimitedRoom(roomId); + } if (roomId !== self.state.currentRoomId) { // It is safe to remove events from rooms we are not viewing. @@ -1301,80 +1297,21 @@ export default createReactClass({ }); cli.on('sync', async (state, prevState, data) => { - const platform = PlatformPeg.get(); - if (!platform.supportsEventIndexing()) return; + const eventIndex = EventIndexPeg.get(); + if (eventIndex === null) return; + await eventIndex.onSync(state, prevState, data); + }); - if (prevState === null && state === "PREPARED") { - /// Load our stored checkpoints, if any. - self.crawlerChekpoints = await platform.loadCheckpoints(); - console.log("Seshat: Loaded checkpoints", - self.crawlerChekpoints); - return; - } + cli.on("Room.timeline", async (ev, room, toStartOfTimeline, removed, data) => { + const eventIndex = EventIndexPeg.get(); + if (eventIndex === null) return; + await eventIndex.onRoomTimeline(ev, room, toStartOfTimeline, removed, data); + }); - if (prevState === "PREPARED" && state === "SYNCING") { - const addInitialCheckpoints = async () => { - const client = MatrixClientPeg.get(); - const rooms = client.getRooms(); - - const isRoomEncrypted = (room) => { - return client.isRoomEncrypted(room.roomId); - }; - - // We only care to crawl the encrypted rooms, non-encrytped - // rooms can use the search provided by the Homeserver. - const encryptedRooms = rooms.filter(isRoomEncrypted); - - console.log("Seshat: Adding initial crawler checkpoints"); - - // Gather the prev_batch tokens and create checkpoints for - // our message crawler. - await Promise.all(encryptedRooms.map(async (room) => { - const timeline = room.getLiveTimeline(); - const token = timeline.getPaginationToken("b"); - - console.log("Seshat: Got token for indexer", - room.roomId, token); - - const backCheckpoint = { - roomId: room.roomId, - token: token, - direction: "b", - }; - - const forwardCheckpoint = { - roomId: room.roomId, - token: token, - direction: "f", - }; - - await platform.addCrawlerCheckpoint(backCheckpoint); - await platform.addCrawlerCheckpoint(forwardCheckpoint); - self.crawlerChekpoints.push(backCheckpoint); - self.crawlerChekpoints.push(forwardCheckpoint); - })); - }; - - // If our indexer is empty we're most likely running Riot the - // first time with indexing support or running it with an - // initial sync. Add checkpoints to crawl our encrypted rooms. - const eventIndexWasEmpty = await platform.isEventIndexEmpty(); - if (eventIndexWasEmpty) await addInitialCheckpoints(); - - // Start our crawler. - const crawlerHandle = {}; - self.crawlerFunc(crawlerHandle); - self.crawlerRef = crawlerHandle; - return; - } - - if (prevState === "SYNCING" && state === "SYNCING") { - // A sync was done, presumably we queued up some live events, - // commit them now. - console.log("Seshat: Committing events"); - await platform.commitLiveEvents(); - return; - } + cli.on("Event.decrypted", async (ev, err) => { + const eventIndex = EventIndexPeg.get(); + if (eventIndex === null) return; + await eventIndex.onEventDecrypted(ev, err); }); cli.on('sync', function(state, prevState, data) { @@ -1459,44 +1396,6 @@ export default createReactClass({ }, null, true); }); - cli.on("Room.timeline", async (ev, room, toStartOfTimeline, removed, data) => { - const platform = PlatformPeg.get(); - if (!platform.supportsEventIndexing()) return; - - // We only index encrypted rooms locally. - if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; - - // If it isn't a live event or if it's redacted there's nothing to - // do. - if (toStartOfTimeline || !data || !data.liveEvent - || ev.isRedacted()) { - return; - } - - // If the event is not yet decrypted mark it for the - // Event.decrypted callback. - if (ev.isBeingDecrypted()) { - const eventId = ev.getId(); - self.liveEventsForIndex.add(eventId); - } else { - // If the event is decrypted or is unencrypted add it to the - // index now. - await self.addLiveEventToIndex(ev); - } - }); - - cli.on("Event.decrypted", async (ev, err) => { - const platform = PlatformPeg.get(); - if (!platform.supportsEventIndexing()) return; - - const eventId = ev.getId(); - - // If the event isn't in our live event set, ignore it. - if (!self.liveEventsForIndex.delete(eventId)) return; - if (err) return; - await self.addLiveEventToIndex(ev); - }); - cli.on("accountData", function(ev) { if (ev.getType() === 'im.vector.web.settings') { if (ev.getContent() && ev.getContent().theme) { @@ -2058,238 +1957,4 @@ export default createReactClass({ {view} ; }, - - async addLiveEventToIndex(ev) { - const platform = PlatformPeg.get(); - if (!platform.supportsEventIndexing()) return; - - if (["m.room.message", "m.room.name", "m.room.topic"] - .indexOf(ev.getType()) == -1) { - return; - } - - const e = ev.toJSON().decrypted; - const profile = { - displayname: ev.sender.rawDisplayName, - avatar_url: ev.sender.getMxcAvatarUrl(), - }; - - platform.addEventToIndex(e, profile); - }, - - async crawlerFunc(handle) { - // TODO either put this in a better place or find a library provided - // method that does this. - const sleep = async (ms) => { - return new Promise(resolve => setTimeout(resolve, ms)); - }; - - let cancelled = false; - - console.log("Seshat: Started crawler function"); - - const client = MatrixClientPeg.get(); - const platform = PlatformPeg.get(); - - handle.cancel = () => { - cancelled = true; - }; - - while (!cancelled) { - // This is a low priority task and we don't want to spam our - // Homeserver with /messages requests so we set a hefty 3s timeout - // here. - await sleep(3000); - - console.log("Seshat: Running the crawler loop."); - - if (cancelled) { - console.log("Seshat: Cancelling the crawler."); - break; - } - - const checkpoint = this.crawlerChekpoints.shift(); - - /// There is no checkpoint available currently, one may appear if - // a sync with limited room timelines happens, so go back to sleep. - if (checkpoint === undefined) { - continue; - } - - console.log("Seshat: crawling using checkpoint", checkpoint); - - // We have a checkpoint, let us fetch some messages, again, very - // conservatively to not bother our Homeserver too much. - const eventMapper = client.getEventMapper(); - // TODO we need to ensure to use member lazy loading with this - // request so we get the correct profiles. - let res; - - try { - res = await client._createMessagesRequest( - checkpoint.roomId, checkpoint.token, 100, - checkpoint.direction); - } catch (e) { - console.log("Seshat: Error crawling events:", e); - this.crawlerChekpoints.push(checkpoint); - continue - } - - if (res.chunk.length === 0) { - console.log("Seshat: Done with the checkpoint", checkpoint); - // We got to the start/end of our timeline, lets just - // delete our checkpoint and go back to sleep. - await platform.removeCrawlerCheckpoint(checkpoint); - continue; - } - - // Convert the plain JSON events into Matrix events so they get - // decrypted if necessary. - const matrixEvents = res.chunk.map(eventMapper); - let stateEvents = []; - if (res.state !== undefined) { - stateEvents = res.state.map(eventMapper); - } - - const profiles = {}; - - stateEvents.forEach(ev => { - if (ev.event.content && - ev.event.content.membership === "join") { - profiles[ev.event.sender] = { - displayname: ev.event.content.displayname, - avatar_url: ev.event.content.avatar_url, - }; - } - }); - - const decryptionPromises = []; - - matrixEvents.forEach(ev => { - if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) { - // TODO the decryption promise is a private property, this - // should either be made public or we should convert the - // event that gets fired when decryption is done into a - // promise using the once event emitter method: - // https://nodejs.org/api/events.html#events_events_once_emitter_name - decryptionPromises.push(ev._decryptionPromise); - } - }); - - // Let us wait for all the events to get decrypted. - await Promise.all(decryptionPromises); - - // We filter out events for which decryption failed, are redacted - // or aren't of a type that we know how to index. - const isValidEvent = (value) => { - return ([ - "m.room.message", - "m.room.name", - "m.room.topic", - ].indexOf(value.getType()) >= 0 - && !value.isRedacted() && !value.isDecryptionFailure() - ); - // TODO do we need to check if the event has all the valid - // attributes? - }; - - // TODO if there ar no events at this point we're missing a lot - // decryption keys, do we wan't to retry this checkpoint at a later - // stage? - const filteredEvents = matrixEvents.filter(isValidEvent); - - // Let us convert the events back into a format that Seshat can - // consume. - const events = filteredEvents.map((ev) => { - const jsonEvent = ev.toJSON(); - - let e; - if (ev.isEncrypted()) e = jsonEvent.decrypted; - else e = jsonEvent; - - let profile = {}; - if (e.sender in profiles) profile = profiles[e.sender]; - const object = { - event: e, - profile: profile, - }; - return object; - }); - - // Create a new checkpoint so we can continue crawling the room for - // messages. - const newCheckpoint = { - roomId: checkpoint.roomId, - token: res.end, - fullCrawl: checkpoint.fullCrawl, - direction: checkpoint.direction, - }; - - console.log( - "Seshat: Crawled room", - client.getRoom(checkpoint.roomId).name, - "and fetched", events.length, "events.", - ); - - try { - const eventsAlreadyAdded = await platform.addHistoricEvents( - events, newCheckpoint, checkpoint); - // If all events were already indexed we assume that we catched - // up with our index and don't need to crawl the room further. - // Let us delete the checkpoint in that case, otherwise push - // the new checkpoint to be used by the crawler. - if (eventsAlreadyAdded === true && newCheckpoint.fullCrawl !== true) { - console.log("Seshat: Checkpoint had already all events", - "added, stopping the crawl", checkpoint); - await platform.removeCrawlerCheckpoint(newCheckpoint); - } else { - this.crawlerChekpoints.push(newCheckpoint); - } - } catch (e) { - console.log("Seshat: Error durring a crawl", e); - // An error occured, put the checkpoint back so we - // can retry. - this.crawlerChekpoints.push(checkpoint); - } - } - - console.log("Seshat: Stopping crawler function"); - }, - - async addCheckpointForLimitedRoom(roomId) { - const platform = PlatformPeg.get(); - if (!platform.supportsEventIndexing()) return; - if (!MatrixClientPeg.get().isRoomEncrypted(roomId)) return; - - const client = MatrixClientPeg.get(); - const room = client.getRoom(roomId); - - if (room === null) return; - - const timeline = room.getLiveTimeline(); - const token = timeline.getPaginationToken("b"); - - const backwardsCheckpoint = { - roomId: room.roomId, - token: token, - fullCrawl: false, - direction: "b", - }; - - const forwardsCheckpoint = { - roomId: room.roomId, - token: token, - fullCrawl: false, - direction: "f", - }; - - console.log("Seshat: Added checkpoint because of a limited timeline", - backwardsCheckpoint, forwardsCheckpoint); - - await platform.addCrawlerCheckpoint(backwardsCheckpoint); - await platform.addCrawlerCheckpoint(forwardsCheckpoint); - - this.crawlerChekpoints.push(backwardsCheckpoint); - this.crawlerChekpoints.push(forwardsCheckpoint); - }, }); From c3df2f941dccf5e135ac48408fccc25cf3c5da30 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 12 Nov 2019 12:30:05 +0000 Subject: [PATCH 060/334] attach promise utils atop bluebird --- src/utils/promise.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/utils/promise.js b/src/utils/promise.js index dd10f7fdd7..f7a2e7c3e7 100644 --- a/src/utils/promise.js +++ b/src/utils/promise.js @@ -14,6 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ +// This is only here to allow access to methods like done for the time being +import Promise from "bluebird"; + // @flow // Returns a promise which resolves with a given value after the given number of ms From 3f2b77189e31c0cb3617d78105987190f10502a9 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 12 Nov 2019 13:29:01 +0000 Subject: [PATCH 061/334] Simplify dispatch blocks --- src/stores/RoomViewStore.js | 75 +++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 41 deletions(-) diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index e860ed8b24..6a405124f4 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -179,50 +179,43 @@ class RoomViewStore extends Store { } else if (payload.room_alias) { // Try the room alias to room ID navigation cache first to avoid // blocking room navigation on the homeserver. - const roomId = getCachedRoomIDForAlias(payload.room_alias); - if (roomId) { - dis.dispatch({ - action: 'view_room', - room_id: roomId, - event_id: payload.event_id, - highlighted: payload.highlighted, - room_alias: payload.room_alias, - auto_join: payload.auto_join, - oob_data: payload.oob_data, + let roomId = getCachedRoomIDForAlias(payload.room_alias); + if (!roomId) { + // Room alias cache miss, so let's ask the homeserver. Resolve the alias + // and then do a second dispatch with the room ID acquired. + this._setState({ + roomId: null, + initialEventId: null, + initialEventPixelOffset: null, + isInitialEventHighlighted: null, + roomAlias: payload.room_alias, + roomLoading: true, + roomLoadError: null, }); - return; + try { + const result = await MatrixClientPeg.get().getRoomIdForAlias(payload.room_alias); + storeRoomAliasInCache(payload.room_alias, result.room_id); + roomId = result.room_id; + } catch (err) { + dis.dispatch({ + action: 'view_room_error', + room_id: null, + room_alias: payload.room_alias, + err, + }); + return; + } } - // Room alias cache miss, so let's ask the homeserver. - // Resolve the alias and then do a second dispatch with the room ID acquired - this._setState({ - roomId: null, - initialEventId: null, - initialEventPixelOffset: null, - isInitialEventHighlighted: null, - roomAlias: payload.room_alias, - roomLoading: true, - roomLoadError: null, + + dis.dispatch({ + action: 'view_room', + room_id: roomId, + event_id: payload.event_id, + highlighted: payload.highlighted, + room_alias: payload.room_alias, + auto_join: payload.auto_join, + oob_data: payload.oob_data, }); - try { - const result = await MatrixClientPeg.get().getRoomIdForAlias(payload.room_alias); - storeRoomAliasInCache(payload.room_alias, result.room_id); - dis.dispatch({ - action: 'view_room', - room_id: result.room_id, - event_id: payload.event_id, - highlighted: payload.highlighted, - room_alias: payload.room_alias, - auto_join: payload.auto_join, - oob_data: payload.oob_data, - }); - } catch (err) { - dis.dispatch({ - action: 'view_room_error', - room_id: null, - room_alias: payload.room_alias, - err, - }); - } } } From e296fd05c0048e95a98c8777209ecb2990d787f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 12 Nov 2019 15:39:26 +0100 Subject: [PATCH 062/334] RoomView: Move the search logic into a separate module. --- src/EventIndexing.js | 5 + src/Searching.js | 137 ++++++++++++++++++++++++++ src/components/structures/RoomView.js | 125 +---------------------- 3 files changed, 147 insertions(+), 120 deletions(-) create mode 100644 src/Searching.js diff --git a/src/EventIndexing.js b/src/EventIndexing.js index 21ee8f3da6..29f9c48842 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -401,4 +401,9 @@ export default class EventIndexer { this._crawlerRef.cancel(); this._crawlerRef = null; } + + async search(searchArgs) { + const platform = PlatformPeg.get(); + return platform.searchEventIndex(searchArgs) + } } diff --git a/src/Searching.js b/src/Searching.js new file mode 100644 index 0000000000..cd06d9bc67 --- /dev/null +++ b/src/Searching.js @@ -0,0 +1,137 @@ +/* +Copyright 2019 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import EventIndexPeg from "./EventIndexPeg"; +import MatrixClientPeg from "./MatrixClientPeg"; + +function serverSideSearch(term, roomId = undefined) { + let filter; + if (roomId !== undefined) { + filter = { + // XXX: it's unintuitive that the filter for searching doesn't have the same shape as the v2 filter API :( + rooms: [roomId], + }; + } + + let searchPromise = MatrixClientPeg.get().searchRoomEvents({ + filter: filter, + term: term, + }); + + return searchPromise; +} + +function eventIndexSearch(term, roomId = undefined) { + const combinedSearchFunc = async (searchTerm) => { + // Create two promises, one for the local search, one for the + // server-side search. + const client = MatrixClientPeg.get(); + const serverSidePromise = serverSideSearch(searchTerm); + const localPromise = localSearchFunc(searchTerm); + + // Wait for both promises to resolve. + await Promise.all([serverSidePromise, localPromise]); + + // Get both search results. + const localResult = await localPromise; + const serverSideResult = await serverSidePromise; + + // Combine the search results into one result. + const result = {}; + + // Our localResult and serverSideResult are both ordered by + // recency separetly, 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; + + if (aEvent.origin_server_ts > + bEvent.origin_server_ts) return -1; + if (aEvent.origin_server_ts < + bEvent.origin_server_ts) return 1; + return 0; + }; + + result.count = localResult.count + serverSideResult.count; + result.results = localResult.results.concat( + serverSideResult.results).sort(compare); + result.highlights = localResult.highlights.concat( + serverSideResult.highlights); + + return result; + }; + + const localSearchFunc = async (searchTerm, roomId = undefined) => { + const searchArgs = { + search_term: searchTerm, + before_limit: 1, + after_limit: 1, + order_by_recency: true, + }; + + if (roomId !== undefined) { + searchArgs.room_id = roomId; + } + + const eventIndex = EventIndexPeg.get(); + + const localResult = await eventIndex.search(searchArgs); + + const response = { + search_categories: { + room_events: localResult, + }, + }; + + const emptyResult = { + results: [], + highlights: [], + }; + + const result = MatrixClientPeg.get()._processRoomEventsSearch( + emptyResult, response); + + return result; + }; + + let searchPromise; + + if (roomId !== undefined) { + if (MatrixClientPeg.get().isRoomEncrypted(roomId)) { + // The search is for a single encrypted room, use our local + // search method. + searchPromise = localSearchFunc(term, roomId); + } else { + // The search is for a single non-encrypted room, use the + // server-side search. + searchPromise = serverSideSearch(term, roomId); + } + } else { + // Search across all rooms, combine a server side search and a + // local search. + searchPromise = combinedSearchFunc(term); + } + + return searchPromise +} + +export default function eventSearch(term, roomId = undefined) { + const eventIndex = EventIndexPeg.get(); + + if (eventIndex === null) return serverSideSearch(term, roomId); + else return eventIndexSearch(term, roomId); +} diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 1b44335f51..9fe54ad164 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -34,7 +34,6 @@ import { _t } from '../../languageHandler'; import {RoomPermalinkCreator} from '../../utils/permalinks/Permalinks'; import MatrixClientPeg from '../../MatrixClientPeg'; -import PlatformPeg from "../../PlatformPeg"; import ContentMessages from '../../ContentMessages'; import Modal from '../../Modal'; import sdk from '../../index'; @@ -44,6 +43,7 @@ import Tinter from '../../Tinter'; import rate_limited_func from '../../ratelimitedfunc'; import ObjectUtils from '../../ObjectUtils'; import * as Rooms from '../../Rooms'; +import eventSearch from '../../Searching'; import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard'; @@ -1130,127 +1130,12 @@ module.exports = createReactClass({ // todo: should cancel any previous search requests. this.searchId = new Date().getTime(); - let filter; - if (scope === "Room") { - filter = { - // XXX: it's unintuitive that the filter for searching doesn't have the same shape as the v2 filter API :( - rooms: [ - this.state.room.roomId, - ], - }; - } + let roomId; + if (scope === "Room") roomId = this.state.room.roomId, debuglog("sending search request"); - const platform = PlatformPeg.get(); - - if (platform.supportsEventIndexing()) { - const combinedSearchFunc = async (searchTerm) => { - // Create two promises, one for the local search, one for the - // server-side search. - const client = MatrixClientPeg.get(); - const serverSidePromise = client.searchRoomEvents({ - term: searchTerm, - }); - const localPromise = localSearchFunc(searchTerm); - - // Wait for both promises to resolve. - await Promise.all([serverSidePromise, localPromise]); - - // Get both search results. - const localResult = await localPromise; - const serverSideResult = await serverSidePromise; - - // Combine the search results into one result. - const result = {}; - - // Our localResult and serverSideResult are both ordered by - // recency separetly, 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; - - if (aEvent.origin_server_ts > - bEvent.origin_server_ts) return -1; - if (aEvent.origin_server_ts < - bEvent.origin_server_ts) return 1; - return 0; - }; - - result.count = localResult.count + serverSideResult.count; - result.results = localResult.results.concat( - serverSideResult.results).sort(compare); - result.highlights = localResult.highlights.concat( - serverSideResult.highlights); - - return result; - }; - - const localSearchFunc = async (searchTerm, roomId = undefined) => { - const searchArgs = { - search_term: searchTerm, - before_limit: 1, - after_limit: 1, - order_by_recency: true, - }; - - if (roomId !== undefined) { - searchArgs.room_id = roomId; - } - - const localResult = await platform.searchEventIndex( - searchArgs); - - const response = { - search_categories: { - room_events: localResult, - }, - }; - - const emptyResult = { - results: [], - highlights: [], - }; - - // TODO is there a better way to convert our result into what - // is expected by the handler method. - const result = MatrixClientPeg.get()._processRoomEventsSearch( - emptyResult, response); - - return result; - }; - - let searchPromise; - - if (scope === "Room") { - const roomId = this.state.room.roomId; - - if (MatrixClientPeg.get().isRoomEncrypted(roomId)) { - // The search is for a single encrypted room, use our local - // search method. - searchPromise = localSearchFunc(term, roomId); - } else { - // The search is for a single non-encrypted room, use the - // server-side search. - searchPromise = MatrixClientPeg.get().searchRoomEvents({ - filter: filter, - term: term, - }); - } - } else { - // Search across all rooms, combine a server side search and a - // local search. - searchPromise = combinedSearchFunc(term); - } - - this._handleSearchResult(searchPromise).done(); - } else { - const searchPromise = MatrixClientPeg.get().searchRoomEvents({ - filter: filter, - term: term, - }); - this._handleSearchResult(searchPromise).done(); - } + const searchPromise = eventSearch(term, roomId); + this._handleSearchResult(searchPromise).done(); }, _handleSearchResult: function(searchPromise) { From d911055f5d8016ebd2f036d68a9c1ee3f7343af2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 12 Nov 2019 15:39:54 +0100 Subject: [PATCH 063/334] MatrixChat: Move the indexing limited room logic to a different event. --- src/components/structures/MatrixChat.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index d006247151..0d3d5abd55 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1276,11 +1276,6 @@ export default createReactClass({ // https://github.com/vector-im/riot-web/issues/3307#issuecomment-282895568 cli.setCanResetTimelineCallback(async function(roomId) { console.log("Request to reset timeline in room ", roomId, " viewing:", self.state.currentRoomId); - // TODO is there a better place to plug this in - const eventIndex = EventIndexPeg.get(); - if (eventIndex !== null) { - await eventIndex.addCheckpointForLimitedRoom(roomId); - } if (roomId !== self.state.currentRoomId) { // It is safe to remove events from rooms we are not viewing. @@ -1314,6 +1309,13 @@ export default createReactClass({ await eventIndex.onEventDecrypted(ev, err); }); + cli.on("Room.timelineReset", async (room, timelineSet, resetAllTimelines) => { + const eventIndex = EventIndexPeg.get(); + if (eventIndex === null) return; + if (resetAllTimelines === true) return; + await eventIndex.addCheckpointForLimitedRoom(roomId); + }); + cli.on('sync', function(state, prevState, data) { // LifecycleStore and others cannot directly subscribe to matrix client for // events because flux only allows store state changes during flux dispatches. From ecbc47c5488bf60b5cc068b09d4a51672b9a5c14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 12 Nov 2019 15:40:49 +0100 Subject: [PATCH 064/334] EventIndexing: Rename the stop method. --- src/EventIndexPeg.js | 9 +++++---- src/EventIndexing.js | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index 794450e4b7..86fb889c7a 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -57,13 +57,14 @@ class EventIndexPeg { return true } - async stop() { - if (this.index == null) return; - index.stopCrawler(); + stop() { + if (this.index === null) return; + index.stop(); + this.index = null; } async deleteEventIndex() { - if (this.index == null) return; + if (this.index === null) return; index.deleteEventIndex(); } } diff --git a/src/EventIndexing.js b/src/EventIndexing.js index 29f9c48842..92a3a5a1f8 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -34,6 +34,7 @@ export default class EventIndexer { const platform = PlatformPeg.get(); if (!platform.supportsEventIndexing()) return false; platform.initEventIndex(userId); + return true; } async onSync(state, prevState, data) { @@ -397,7 +398,7 @@ export default class EventIndexer { this.crawlerRef = crawlerHandle; } - stopCrawler() { + stop() { this._crawlerRef.cancel(); this._crawlerRef = null; } From d69eb78b661764e9241d14a0c08dff23906245c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 12 Nov 2019 15:41:14 +0100 Subject: [PATCH 065/334] EventIndexing: Add a missing platform getting. --- src/EventIndexing.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/EventIndexing.js b/src/EventIndexing.js index 92a3a5a1f8..ebd2ffe983 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -385,6 +385,7 @@ export default class EventIndexer { } async deleteEventIndex() { + const platform = PlatformPeg.get(); if (platform.supportsEventIndexing()) { console.log("Seshat: Deleting event index."); this.crawlerRef.cancel(); From 3502454c615f1a7bc74588f3512661278604d2d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 12 Nov 2019 15:58:38 +0100 Subject: [PATCH 066/334] LifeCycle: Stop the crawler and delete the index when whe log out. --- src/Lifecycle.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 0b44f2ed84..7360cd3231 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -613,6 +613,7 @@ export function onLoggedOut() { // so that React components unmount first. This avoids React soft crashes // that can occur when components try to use a null client. dis.dispatch({action: 'on_logged_out'}); + EventIndexPeg.deleteEventIndex().done(); stopMatrixClient(); _clearStorage().done(); } @@ -648,6 +649,7 @@ export function stopMatrixClient(unsetClient=true) { ActiveWidgetStore.stop(); IntegrationManagers.sharedInstance().stopWatching(); if (DMRoomMap.shared()) DMRoomMap.shared().stop(); + EventIndexPeg.stop(); const cli = MatrixClientPeg.get(); if (cli) { cli.stopClient(); From 87f9e0d5650b42383f85804623c2b40aaba50854 Mon Sep 17 00:00:00 2001 From: take100yen Date: Tue, 12 Nov 2019 11:28:20 +0000 Subject: [PATCH 067/334] Translated using Weblate (Japanese) Currently translated at 62.7% (1168 of 1864 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 | 53 +++++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json index 5e3fecaed9..22b092c06b 100644 --- a/src/i18n/strings/ja.json +++ b/src/i18n/strings/ja.json @@ -17,12 +17,12 @@ "Hide read receipts": "既読を表示しない", "Invited": "招待中", "%(displayName)s is typing": "%(displayName)s 文字入力中", - "%(targetName)s joined the room.": "%(targetName)s 部屋に参加しました。", + "%(targetName)s joined the room.": "%(targetName)s が部屋に参加しました。", "Low priority": "低優先度", "Mute": "通知しない", "New password": "新しいパスワード", "Notifications": "通知", - "Cancel": "取消", + "Cancel": "キャンセル", "Create new room": "新しい部屋を作成", "Room directory": "公開部屋一覧", "Search": "検索", @@ -490,7 +490,7 @@ "Delete %(count)s devices|one": "端末を削除する", "Device ID": "端末ID", "Device Name": "端末名", - "Last seen": "最後のシーン", + "Last seen": "最後に表示した時刻", "Select devices": "端末の選択", "Failed to set display name": "表示名の設定に失敗しました", "Disable Notifications": "通知を無効にする", @@ -1162,8 +1162,8 @@ "No media permissions": "メディア権限がありません", "You may need to manually permit Riot to access your microphone/webcam": "自分のマイク/ウェブカメラにアクセスするために手動でRiotを許可する必要があるかもしれません", "Missing Media Permissions, click here to request.": "メディアアクセス権がありません。ここをクリックしてリクエストしてください。", - "No Audio Outputs detected": "オーディオ出力が検出されなかった", - "Default Device": "標準端末", + "No Audio Outputs detected": "オーディオ出力が検出されませんでした", + "Default Device": "既定のデバイス", "Audio Output": "音声出力", "VoIP": "VoIP", "Email": "Eメール", @@ -1380,5 +1380,46 @@ "Enable Community Filter Panel": "コミュニティーフィルターパネルを有効にする", "Show recently visited rooms above the room list": "最近訪問した部屋をリストの上位に表示する", "Low bandwidth mode": "低帯域通信モード", - "Trust & Devices": "信頼と端末" + "Trust & Devices": "信頼と端末", + "Public Name": "パブリック名", + "Upload profile picture": "プロフィール画像をアップロード", + "Upgrade to your own domain": "あなた自身のドメインにアップグレード", + "Phone numbers": "電話番号", + "Set a new account password...": "アカウントの新しいパスワードを設定...", + "Language and region": "言語と地域", + "Theme": "テーマ", + "General": "一般", + "Preferences": "環境設定", + "Security & Privacy": "セキュリティとプライバシー", + "A device's public name is visible to people you communicate with": "デバイスのパブリック名はあなたと会話するすべての人が閲覧できます", + "Room information": "部屋の情報", + "Internal room ID:": "内部 部屋ID:", + "Room version": "部屋のバージョン", + "Room version:": "部屋のバージョン:", + "Developer options": "開発者オプション", + "Room Addresses": "部屋のアドレス", + "Sounds": "音", + "Notification sound": "通知音", + "Reset": "リセット", + "Set a new custom sound": "カスタム音を設定", + "Browse": "参照", + "Roles & Permissions": "役割と権限", + "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "誰が履歴を読み取れるかに関する変更は、今後送信されるメッセージにのみ適用されます。既に存在する履歴の表示は変更されません。", + "Encryption": "暗号化", + "Once enabled, encryption cannot be disabled.": "もし有効化された場合、二度と無効化できません。", + "Encrypted": "暗号化", + "Email Address": "メールアドレス", + "Main address": "メインアドレス", + "Join": "参加", + "This room is private, and can only be joined by invitation.": "この部屋はプライベートです。招待によってのみ参加できます。", + "Create a private room": "プライベートな部屋を作成", + "Topic (optional)": "トピック (オプション)", + "Hide advanced": "高度な設定を非表示", + "Show advanced": "高度な設定を表示", + "Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)": "他の Matrix ホームサーバーからの参加を禁止する (この設定はあとから変更できません!)", + "Room Settings - %(roomName)s": "部屋の設定 - %(roomName)s", + "Explore": "探索", + "Filter": "フィルター", + "Find a room… (e.g. %(exampleRoom)s)": "部屋を探す… (例: %(exampleRoom)s)", + "If you can't find the room you're looking for, ask for an invite or Create a new room.": "もしお探しの部屋が見つからない場合、招待してもらうか部屋を作成しましょう。" } From 5fa4bc7192d22cdb0c811f676b9479b3ff99ad6a Mon Sep 17 00:00:00 2001 From: shuji narazaki Date: Tue, 12 Nov 2019 11:52:45 +0000 Subject: [PATCH 068/334] Translated using Weblate (Japanese) Currently translated at 62.7% (1168 of 1864 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 | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json index 22b092c06b..54e2c29a21 100644 --- a/src/i18n/strings/ja.json +++ b/src/i18n/strings/ja.json @@ -389,8 +389,8 @@ "%(senderName)s kicked %(targetName)s.": "%(senderName)s は %(targetName)s を追放しました。", "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s は %(targetName)s の招待を撤回しました。", "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s はトピックを \"%(topic)s\" に変更しました。", - "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s はルーム名を削除しました。", - "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s はルーム名を %(roomName)s に変更しました。", + "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s は部屋名を削除しました。", + "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s は部屋名を %(roomName)s に変更しました。", "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s はイメージを送信しました。", "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|other": "%(senderName)s はこの部屋のアドレスとして %(addedAddresses)s を追加しました。", "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|one": "%(senderName)s はこの部屋のアドレスとして %(addedAddresses)s を追加しました。", @@ -1421,5 +1421,6 @@ "Explore": "探索", "Filter": "フィルター", "Find a room… (e.g. %(exampleRoom)s)": "部屋を探す… (例: %(exampleRoom)s)", - "If you can't find the room you're looking for, ask for an invite or Create a new room.": "もしお探しの部屋が見つからない場合、招待してもらうか部屋を作成しましょう。" + "If you can't find the room you're looking for, ask for an invite or Create a new room.": "もしお探しの部屋が見つからない場合、招待してもらうか部屋を作成しましょう。", + "Enable room encryption": "部屋の暗号化を有効にする" } From 523e17838de98b9a0744b229db36741a21d22996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=ED=83=9C=EC=84=AD?= Date: Tue, 12 Nov 2019 12:15:45 +0000 Subject: [PATCH 069/334] Translated using Weblate (Korean) Currently translated at 100.0% (1864 of 1864 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ko/ --- src/i18n/strings/ko.json | 47 +++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index 54425657cf..3d01d5be67 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -128,8 +128,8 @@ "Deactivate Account": "계정 비활성화", "Deactivate my account": "계정 정지하기", "Decline": "거절", - "Decrypt %(text)s": "%(text)s 해독", - "Decryption error": "암호 해독 오류", + "Decrypt %(text)s": "%(text)s 복호화", + "Decryption error": "암호 복호화 오류", "Delete": "지우기", "Deops user with given id": "받은 ID로 사용자의 등급을 낮추기", "Device ID:": "기기 ID:", @@ -156,7 +156,7 @@ "End-to-end encryption is in beta and may not be reliable": "종단 간 암호화는 베타 테스트 중이며 신뢰하기 힘들 수 있습니다.", "Enter Code": "코드를 입력하세요", "Enter passphrase": "암호 입력", - "Error decrypting attachment": "첨부 파일 해독 중 오류", + "Error decrypting attachment": "첨부 파일 복호화 중 오류", "Error: Problem communicating with the given homeserver.": "오류: 지정한 홈서버와 통신에 문제가 있습니다.", "Event information": "이벤트 정보", "Existing Call": "기존 전화", @@ -394,7 +394,7 @@ "Unable to capture screen": "화면을 찍을 수 없음", "Unable to enable Notifications": "알림을 사용할 수 없음", "Unable to load device list": "기기 목록을 불러올 수 없음", - "Undecryptable": "해독할 수 없음", + "Undecryptable": "복호화할 수 없음", "Unencrypted room": "암호화하지 않은 방", "unencrypted": "암호화하지 않음", "Unencrypted message": "암호화하지 않은 메시지", @@ -549,10 +549,10 @@ "Unknown error": "알 수 없는 오류", "Incorrect password": "맞지 않는 비밀번호", "To continue, please enter your password.": "계속하려면, 비밀번호를 입력해주세요.", - "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "이 과정으로 암호화한 방에서 받은 메시지의 키를 로컬 파일로 내보낼 수 있습니다. 그런 다음 나중에 다른 Matrix 클라이언트에서 파일을 가져와서, 해당 클라이언트에서도 이 메시지를 해독할 수 있도록 할 수 있습니다.", - "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "내보낸 파일이 있으면 누구든 암호화한 메시지를 해독해서 읽을 수 있으므로, 보안에 신경을 써야 합니다. 이런 이유로 내보낸 파일을 암호화하도록 아래에 암호를 입력하는 것을 추천합니다. 같은 암호를 사용해야 데이터를 불러올 수 있을 것입니다.", - "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "이 과정으로 다른 Matrix 클라이언트에서 내보낸 암호화 키를 가져올 수 있습니다. 그런 다음 이전 클라이언트에서 해독할 수 있는 모든 메시지를 해독할 수 있습니다.", - "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "내보낸 파일이 암호로 보호되어 있습니다. 파일을 해독하려면, 여기에 암호를 입력해야 합니다.", + "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "이 과정으로 암호화한 방에서 받은 메시지의 키를 로컬 파일로 내보낼 수 있습니다. 그런 다음 나중에 다른 Matrix 클라이언트에서 파일을 가져와서, 해당 클라이언트에서도 이 메시지를 복호화할 수 있도록 할 수 있습니다.", + "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "내보낸 파일이 있으면 누구든 암호화한 메시지를 복호화해서 읽을 수 있으므로, 보안에 신경을 써야 합니다. 이런 이유로 내보낸 파일을 암호화하도록 아래에 암호를 입력하는 것을 추천합니다. 같은 암호를 사용해야 데이터를 불러올 수 있을 것입니다.", + "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "이 과정으로 다른 Matrix 클라이언트에서 내보낸 암호화 키를 가져올 수 있습니다. 그런 다음 이전 클라이언트에서 복호화할 수 있는 모든 메시지를 복호화할 수 있습니다.", + "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "내보낸 파일이 암호로 보호되어 있습니다. 파일을 복호화하려면, 여기에 암호를 입력해야 합니다.", "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "이 이벤트를 감추길(삭제하길) 원하세요? 방 이름을 삭제하거나 주제를 바꾸면, 다시 생길 수도 있습니다.", "To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:": "이 기기를 신뢰할 수 있는 지 인증하려면, 다른 방법(예를 들자면 직접 만나거나 전화를 걸어서)으로 소유자 분에게 연락해, 사용자 설정에 있는 키가 아래 키와 같은지 물어보세요:", "Device name": "기기 이름", @@ -590,9 +590,9 @@ "Custom server": "사용자 지정 서버", "Home server URL": "홈 서버 URL", "Identity server URL": "ID 서버 URL", - "Error decrypting audio": "음성 해독 오류", - "Error decrypting image": "사진 해독 중 오류", - "Error decrypting video": "영상 해독 중 오류", + "Error decrypting audio": "음성 복호화 오류", + "Error decrypting image": "사진 복호화 중 오류", + "Error decrypting video": "영상 복호화 중 오류", "Add an Integration": "통합 추가", "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "%(integrationsUrl)s에서 쓸 수 있도록 계정을 인증하려고 다른 사이트로 이동하고 있습니다. 계속하겠습니까?", "Removed or unknown message type": "감췄거나 알 수 없는 메시지 유형", @@ -670,7 +670,7 @@ "Messages containing my display name": "내 표시 이름이 포함된 메시지", "Messages in one-to-one chats": "1:1 대화 메시지", "Unavailable": "이용할 수 없음", - "View Decrypted Source": "해독된 소스 보기", + "View Decrypted Source": "복호화된 소스 보기", "Send": "보내기", "remove %(name)s from the directory.": "목록에서 %(name)s 방을 제거했습니다.", "Notifications on the following keywords follow rules which can’t be displayed here:": "여기에 표시할 수 없는 규칙에 따르는 다음 키워드에 대한 알림:", @@ -779,7 +779,7 @@ "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)s님이 아바타를 %(count)s번 바꿨습니다", "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)s님이 아바타를 바꿨습니다", "This setting cannot be changed later!": "이 설정은 나중에 바꿀 수 없습니다!", - "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "이전 버전 Riot의 데이터가 감지됬습니다. 이 때문에 이전 버전에서 종단간 암호화가 작동하지 않을 수 있습니다. 이전 버전을 사용하면서 최근에 교환한 종단간 암호화 메시지를 이 버전에서는 해독할 수 없습니다. 이 버전에서 메시지를 교환할 수 없을 수도 있습니다. 문제가 발생하면 로그아웃한 후 다시 로그인하세요. 메시지 기록을 유지하려면 키를 내보낸 후 다시 가져오세요.", + "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "이전 버전 Riot의 데이터가 감지됬습니다. 이 때문에 이전 버전에서 종단간 암호화가 작동하지 않을 수 있습니다. 이전 버전을 사용하면서 최근에 교환한 종단간 암호화 메시지를 이 버전에서는 복호화할 수 없습니다. 이 버전에서 메시지를 교환할 수 없을 수도 있습니다. 문제가 발생하면 로그아웃한 후 다시 로그인하세요. 메시지 기록을 유지하려면 키를 내보낸 후 다시 가져오세요.", "Hide display name changes": "별명 변경 내역 숨기기", "This event could not be displayed": "이 이벤트를 표시할 수 없음", "Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "%(displayName)s(%(userName)s)님이 %(dateTime)s에 확인함", @@ -902,7 +902,7 @@ "%(senderName)s sent a video": "%(senderName)s님이 영상을 보냄", "%(senderName)s uploaded a file": "%(senderName)s님이 파일을 업로드함", "Key request sent.": "키 요청을 보냈습니다.", - "If your other devices do not have the key for this message you will not be able to decrypt them.": "당신의 다른 기기에 이 메시지를 읽기 위한 키가 없다면 메시지를 해독할 수 없습니다.", + "If your other devices do not have the key for this message you will not be able to decrypt them.": "당신의 다른 기기에 이 메시지를 읽기 위한 키가 없다면 메시지를 복호화할 수 없습니다.", "Encrypting": "암호화 중", "Encrypted, not sent": "암호화 됨, 보내지지 않음", "Disinvite this user?": "이 사용자에 대한 초대를 취소할까요?", @@ -1811,13 +1811,13 @@ "Deny": "거부", "Unable to load backup status": "백업 상태 불러올 수 없음", "Recovery Key Mismatch": "복구 키가 맞지 않음", - "Backup could not be decrypted with this key: please verify that you entered the correct recovery key.": "이 키로 백업을 해독할 수 없음: 맞는 복구 키를 입력해서 인증해주세요.", + "Backup could not be decrypted with this key: please verify that you entered the correct recovery key.": "이 키로 백업을 복호화할 수 없음: 맞는 복구 키를 입력해서 인증해주세요.", "Incorrect Recovery Passphrase": "맞지 않은 복구 암호", - "Backup could not be decrypted with this passphrase: please verify that you entered the correct recovery passphrase.": "이 암호로 백업을 해독할 수 없음: 맞는 암호를 입력해서 입증해주세요.", + "Backup could not be decrypted with this passphrase: please verify that you entered the correct recovery passphrase.": "이 암호로 백업을 복호화할 수 없음: 맞는 암호를 입력해서 입증해주세요.", "Unable to restore backup": "백업을 복구할 수 없음", "No backup found!": "백업을 찾을 수 없습니다!", "Backup Restored": "백업 복구됨", - "Failed to decrypt %(failedCount)s sessions!": "%(failedCount)s개의 세션 해독에 실패했습니다!", + "Failed to decrypt %(failedCount)s sessions!": "%(failedCount)s개의 세션 복호화에 실패했습니다!", "Restored %(sessionCount)s session keys": "%(sessionCount)s개의 세션 키 복구됨", "Enter Recovery Passphrase": "복구 암호 입력", "Warning: you should only set up key backup from a trusted computer.": "경고: 신뢰할 수 있는 컴퓨터에서만 키 백업을 설정해야 합니다.", @@ -2124,5 +2124,16 @@ "Show tray icon and minimize window to it on close": "닫을 때 창을 최소화하고 트레이 아이콘으로 표시하기", "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "이 작업에는 이메일 주소 또는 전화번호를 확인하기 위해 기본 ID 서버 에 접근해야 합니다. 하지만 서버가 서비스 약관을 갖고 있지 않습니다.", "Trust": "신뢰함", - "Message Actions": "메시지 동작" + "Message Actions": "메시지 동작", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "Send verification requests in direct message": "다이렉트 메시지에서 확인 요청 보내기", + "You verified %(name)s": "%(name)s님을 확인했습니다", + "You cancelled verifying %(name)s": "%(name)s님의 확인을 취소했습니다", + "%(name)s cancelled verifying": "%(name)s님이 확인을 취소했습니다", + "You accepted": "당신이 수락했습니다", + "%(name)s accepted": "%(name)s님이 수락했습니다", + "You cancelled": "당신이 취소했습니다", + "%(name)s cancelled": "%(name)s님이 취소했습니다", + "%(name)s wants to verify": "%(name)s님이 확인을 요청합니다", + "You sent a verification request": "확인 요청을 보냈습니다" } From fd28cf7a4c9e284ffe3bca8921e7402aacdd2924 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 12 Nov 2019 15:12:54 -0700 Subject: [PATCH 070/334] Move notification count to in front of the room name in the page title Fixes https://github.com/vector-im/riot-web/issues/10943 --- src/components/structures/MatrixChat.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 6cc86bf6d7..cd5b27f2b9 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1767,10 +1767,12 @@ export default createReactClass({ const client = MatrixClientPeg.get(); const room = client && client.getRoom(this.state.currentRoomId); if (room) { - subtitle = `| ${ room.name } ${subtitle}`; + subtitle = `${this.subTitleStatus} | ${ room.name } ${subtitle}`; } + } else { + subtitle = `${this.subTitleStatus} ${subtitle}`; } - document.title = `${SdkConfig.get().brand || 'Riot'} ${subtitle} ${this.subTitleStatus}`; + document.title = `${SdkConfig.get().brand || 'Riot'} ${subtitle}`; }, updateStatusIndicator: function(state, prevState) { From 1aa0ab13e682fd73c2c9b7d46d77202339593a1e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 12 Nov 2019 15:38:24 -0700 Subject: [PATCH 071/334] Add some logging/recovery for lost rooms Zero inserts is not normal, so we apply the same recovery technique from the categorization logic above this block: insert it to be the very first room and hope that someone complains that the room is ordered incorrectly. There's some additional logging to try and identify what went wrong because it should definitely be inserted. The `!== 1` check is not supposed to be called, ever. Logging for https://github.com/vector-im/riot-web/issues/11303 --- src/stores/RoomListStore.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index 980753551a..134870398f 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -515,7 +515,21 @@ class RoomListStore extends Store { } if (count !== 1) { - console.warn(`!! Room ${room.roomId} inserted ${count} times`); + console.warn(`!! Room ${room.roomId} inserted ${count} times to ${targetTag}`); + } + + // This is a workaround for https://github.com/vector-im/riot-web/issues/11303 + // The logging is to try and identify what happened exactly. + if (count === 0) { + // Something went very badly wrong - try to recover the room. + // We don't bother checking how the target list is ordered - we're expecting + // to just insert it. + console.warn(`!! Recovering ${room.roomId} for tag ${targetTag} at position 0`); + if (!listsClone[targetTag]) { + console.warn(`!! List for tag ${targetTag} does not exist - creating`); + listsClone[targetTag] = []; + } + listsClone[targetTag].splice(0, 0, {room, category}); } } From fa6e02fafb30b44b0ac0d833335780b18fc80851 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 12 Nov 2019 15:38:24 -0700 Subject: [PATCH 072/334] Revert "Add some logging/recovery for lost rooms" This reverts commit 1aa0ab13e682fd73c2c9b7d46d77202339593a1e. --- src/stores/RoomListStore.js | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index 134870398f..980753551a 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -515,21 +515,7 @@ class RoomListStore extends Store { } if (count !== 1) { - console.warn(`!! Room ${room.roomId} inserted ${count} times to ${targetTag}`); - } - - // This is a workaround for https://github.com/vector-im/riot-web/issues/11303 - // The logging is to try and identify what happened exactly. - if (count === 0) { - // Something went very badly wrong - try to recover the room. - // We don't bother checking how the target list is ordered - we're expecting - // to just insert it. - console.warn(`!! Recovering ${room.roomId} for tag ${targetTag} at position 0`); - if (!listsClone[targetTag]) { - console.warn(`!! List for tag ${targetTag} does not exist - creating`); - listsClone[targetTag] = []; - } - listsClone[targetTag].splice(0, 0, {room, category}); + console.warn(`!! Room ${room.roomId} inserted ${count} times`); } } From 651098b2cabbb6c9211a571a9beff480f8b8b1bf Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Tue, 12 Nov 2019 17:46:17 +0000 Subject: [PATCH 073/334] Translated using Weblate (Albanian) Currently translated at 99.8% (1894 of 1898 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 | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 1d80a90a2b..2efe4ddd68 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -2063,7 +2063,7 @@ "Discovery options will appear once you have added a phone number above.": "Mundësitë e zbulimit do të shfaqen sapo të keni shtuar më sipër një numër telefoni.", "Call failed due to misconfigured server": "Thirrja dështoi për shkak shërbyesi të keqformësuar", "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Që thirrjet të funksionojnë pa probleme, ju lutemi, kërkojini përgjegjësit të shërbyesit tuaj Home (%(homeserverDomain)s) të formësojë një shërbyes TURN.", - "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Ndryshe, mund të provoni të përdorni shërbyesin publik te turn.matrix.org, por kjo s’do të jetë edhe aq e qëndrueshme, dhe adresa juaj IP do t’i bëhet e njohur atij shërbyesi.Këtë mund ta bëni edhe që nga Rregullimet.", + "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Ndryshe, mund të provoni të përdorni shërbyesin publik te turn.matrix.org, por kjo s’do të jetë edhe aq e qëndrueshme, dhe adresa juaj IP do t’i bëhet e njohur atij shërbyesi. Këtë mund ta bëni edhe që nga Rregullimet.", "Try using turn.matrix.org": "Provo të përdorësh turn.matrix.org", "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Lejoni shërbyes rrugëzgjidhje asistimi thirrjesh turn.matrix.org kur shërbyesi juaj Home nuk ofron një të tillë (gjatë thirrjes, adresa juaj IP do t’i bëhet e ditur)", "ID": "ID", @@ -2246,5 +2246,39 @@ "Cancel search": "Anulo kërkimin", "No identity server is configured so you cannot add an email address in order to reset your password in the future.": "S’ka shërbyes identitetesh të formësuar, ndaj s’mund të shtoni një adresë email që të mund të ricaktoni fjalëkalimin tuaj në të ardhmen.", "Jump to first unread room.": "Hidhu te dhoma e parë e palexuar.", - "Jump to first invite.": "Hidhu te ftesa e parë." + "Jump to first invite.": "Hidhu te ftesa e parë.", + "Try out new ways to ignore people (experimental)": "Provoni rrugë të reja për shpërfillje personash (eksperimentale)", + "My Ban List": "Lista Ime e Dëbimeve", + "This is your list of users/servers you have blocked - don't leave the room!": "Kjo është lista juaj e përdoruesve/shërbyesve që keni bllokuar - mos dilni nga dhoma!", + "Ignored/Blocked": "Të shpërfillur/Të bllokuar", + "Error adding ignored user/server": "Gabim shtimi përdoruesi/shërbyesi të shpërfillur", + "Something went wrong. Please try again or view your console for hints.": "Diç shkoi ters. Ju lutemi, riprovoni ose, për ndonjë ide, shihni konsolën tuaj.", + "Error subscribing to list": "Gabim pajtimi te lista", + "Please verify the room ID or alias and try again.": "Ju lutemi, verifikoni ID-në ose aliasin e dhomës dhe riprovoni.", + "Error removing ignored user/server": "Gabim në heqje përdoruesi/shërbyes të shpërfillur", + "Error unsubscribing from list": "Gabim shpajtimi nga lista", + "Please try again or view your console for hints.": "Ju lutemi, riprovoni, ose shihni konsolën tuaj, për ndonjë ide.", + "None": "Asnjë", + "Ban list rules - %(roomName)s": "Rregulla liste dëbimesh - %(roomName)s", + "Server rules": "Rregulla shërbyesi", + "User rules": "Rregulla përdoruesi", + "You have not ignored anyone.": "S’keni shpërfillur ndonjë.", + "You are currently ignoring:": "Aktualisht shpërfillni:", + "You are not subscribed to any lists": "S’jeni pajtuar te ndonjë listë", + "Unsubscribe": "Shpajtohuni", + "View rules": "Shihni rregulla", + "You are currently subscribed to:": "Jeni i pajtuar te:", + "⚠ These settings are meant for advanced users.": "⚠ Këto rregullime janë menduar për përdorues të përparuar.", + "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "Shtoni këtu përdorues dhe shërbyes që doni të shpërfillen. Që Riot të kërkojë për përputhje me çfarëdo shkronjash, përdorni yllthin. Për shembull, @bot:* do të shpërfillë krej përdoruesit që kanë emrin 'bot' në çfarëdo shërbyesi.", + "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "Shpërfillja e personave kryhet përmes listash dëbimi, të cilat përmbajnë rregulla se cilët të dëbohen. Pajtimi te një listë dëbimesh do të thotë se përdoruesit/shërbyesit e bllokuar nga ajo listë do t’ju fshihen juve.", + "Personal ban list": "Listë personale dëbimesh", + "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "Lista juaj personale e dëbimeve mban krejt përdoruesit/shërbyesit prej të cilëve ju personalisht s’dëshironi të shihni mesazhe. Pas shpërfilljes së përdoruesit/shërbyesit tuaj të parë, te lista juaj e dhomave do të shfaqet një dhomë e re e quajtur 'Lista Ime e Dëbimeve' - qëndroni në këtë dhomë që ta mbani listën e dëbimeve në fuqi.", + "Server or user ID to ignore": "Shërbyes ose ID përdoruesi për t’u shpërfillur", + "eg: @bot:* or example.org": "p.sh.: @bot:* ose example.org", + "Subscribed lists": "Lista me pajtim", + "Subscribing to a ban list will cause you to join it!": "Pajtimi te një listë dëbimesh do të shkaktojë pjesëmarrjen tuaj në të!", + "If this isn't what you want, please use a different tool to ignore users.": "Nëse kjo s’është ajo çka doni, ju lutemi, përdorni një tjetër mjet për të shpërfillur përdorues.", + "Room ID or alias of ban list": "ID dhome ose alias e listës së dëbimeve", + "Subscribe": "Pajtohuni", + "You have ignored this user, so their message is hidden. Show anyways.": "E keni shpërfillur këtë përdorues, ndaj mesazhi i tij është fshehur. Shfaqe, sido qoftë." } From 6b8d8d13f241d996bab6b2345db7e084a287adee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jiri=20Gr=C3=B6nroos?= Date: Tue, 12 Nov 2019 20:28:10 +0000 Subject: [PATCH 074/334] Translated using Weblate (Finnish) Currently translated at 95.6% (1814 of 1898 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 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index 5331bdb5c8..a1163f564b 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -1533,7 +1533,7 @@ "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Lisää ”¯\\_(ツ)_/¯” viestin alkuun", "User %(userId)s is already in the room": "Käyttäjä %(userId)s on jo huoneessa", "The user must be unbanned before they can be invited.": "Käyttäjän porttikielto täytyy poistaa ennen kutsumista.", - "Upgrade to your own domain": "Päivitä omaan verkkotunnukseesi", + "Upgrade to your own domain": "Päivitä omaan verkkotunnukseen", "Accept all %(invitedRooms)s invites": "Hyväksy kaikki %(invitedRooms)s kutsut", "Change room avatar": "Vaihda huoneen kuva", "Change room name": "Vaihda huoneen nimi", From a0ff1e9e9b547de4386ac38a6900db5b612be840 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Tue, 12 Nov 2019 20:08:29 +0000 Subject: [PATCH 075/334] Translated using Weblate (Hungarian) Currently translated at 100.0% (1898 of 1898 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 | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 67af8a6a57..29747bb1b6 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2278,5 +2278,39 @@ "You cancelled": "Megszakítottad", "%(name)s cancelled": "%(name)s megszakította", "%(name)s wants to verify": "%(name)s ellenőrizni szeretné", - "You sent a verification request": "Ellenőrzési kérést küldtél" + "You sent a verification request": "Ellenőrzési kérést küldtél", + "Try out new ways to ignore people (experimental)": "Emberek figyelmen kívül hagyásához próbálj ki új utakat (kísérleti)", + "My Ban List": "Tiltólistám", + "This is your list of users/servers you have blocked - don't leave the room!": "Ez az általad tiltott felhasználók/szerverek listája - ne hagyd el ezt a szobát!", + "Ignored/Blocked": "Figyelmen kívül hagyott/Tiltott", + "Error adding ignored user/server": "Hiba a felhasználó/szerver hozzáadásánál a figyelmen kívül hagyandók listájához", + "Something went wrong. Please try again or view your console for hints.": "Valami nem sikerült. Kérjük próbáld újra vagy nézd meg a konzolt a hiba okának felderítéséhez.", + "Error subscribing to list": "A listára való feliratkozásnál hiba történt", + "Please verify the room ID or alias and try again.": "Kérünk ellenőrizd a szoba azonosítóját vagy alternatív nevét és próbáld újra.", + "Error removing ignored user/server": "Hiba a felhasználó/szerver törlésénél a figyelmen kívül hagyandók listájából", + "Error unsubscribing from list": "A listáról való leiratkozásnál hiba történt", + "Please try again or view your console for hints.": "Kérjük próbáld újra vagy nézd meg a konzolt a hiba okának felderítéséhez.", + "None": "Semmi", + "Ban list rules - %(roomName)s": "Tiltási lista szabályok - %(roomName)s", + "Server rules": "Szerver szabályok", + "User rules": "Felhasználói szabályok", + "You have not ignored anyone.": "Senkit nem hagysz figyelmen kívül.", + "You are currently ignoring:": "Jelenleg őket hagyod figyelmen kívül:", + "You are not subscribed to any lists": "Nem iratkoztál fel egyetlen listára sem", + "Unsubscribe": "Leiratkozás", + "View rules": "Szabályok megtekintése", + "You are currently subscribed to:": "Jelenleg ezekre iratkoztál fel:", + "⚠ These settings are meant for advanced users.": "⚠ Ezek a beállítások haladó felhasználók számára vannak.", + "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "Adj hozzá olyan felhasználókat és szervereket akiket figyelmen kívül kívánsz hagyni. Használj csillagot ahol bármilyen karakter állhat. Például: @bot:* minden „bot” nevű felhasználót figyelmen kívül fog hagyni akármelyik szerverről.", + "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "Emberek figyelmen kívül hagyása tiltólistán keresztül történik ami arról tartalmaz szabályokat, hogy kiket kell kitiltani. A feliratkozás a tiltólistára azt jelenti, hogy azok a felhasználók/szerverek amik tiltva vannak a lista által, azoknak az üzenetei rejtve maradnak számodra.", + "Personal ban list": "Személyes tiltó lista", + "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "A személyes tiltólistád tartalmazza azokat a személyeket/szervereket akiktől nem szeretnél üzeneteket látni. Az első felhasználó/szerver figyelmen kívül hagyása után egy új szoba jelenik meg a szobák listájában „Tiltólistám” névvel - ahhoz, hogy a lista érvényben maradjon maradj a szobában.", + "Server or user ID to ignore": "Figyelmen kívül hagyandó szerver vagy felhasználói azonosító", + "eg: @bot:* or example.org": "pl.: @bot:* vagy example.org", + "Subscribed lists": "Feliratkozott listák", + "Subscribing to a ban list will cause you to join it!": "A feliratkozás egy tiltó listára azzal jár, hogy csatlakozol hozzá!", + "If this isn't what you want, please use a different tool to ignore users.": "Ha nem ez az amit szeretnél, kérlek használj más eszközt a felhasználók figyelmen kívül hagyásához.", + "Room ID or alias of ban list": "Tiltó lista szoba azonosítója vagy alternatív neve", + "Subscribe": "Feliratkozás", + "You have ignored this user, so their message is hidden. Show anyways.": "Ezt a felhasználót figyelmen kívül hagyod, így az üzenetei el lesznek rejtve. Mindenképpen megmutat." } From 3dcc92b79da7458e25b6511f6d0a478da746714b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 12 Nov 2019 15:38:24 -0700 Subject: [PATCH 076/334] Add some logging/recovery for lost rooms Zero inserts is not normal, so we apply the same recovery technique from the categorization logic above this block: insert it to be the very first room and hope that someone complains that the room is ordered incorrectly. There's some additional logging to try and identify what went wrong because it should definitely be inserted. The `!== 1` check is not supposed to be called, ever. Logging for https://github.com/vector-im/riot-web/issues/11303 --- src/stores/RoomListStore.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index 980753551a..134870398f 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -515,7 +515,21 @@ class RoomListStore extends Store { } if (count !== 1) { - console.warn(`!! Room ${room.roomId} inserted ${count} times`); + console.warn(`!! Room ${room.roomId} inserted ${count} times to ${targetTag}`); + } + + // This is a workaround for https://github.com/vector-im/riot-web/issues/11303 + // The logging is to try and identify what happened exactly. + if (count === 0) { + // Something went very badly wrong - try to recover the room. + // We don't bother checking how the target list is ordered - we're expecting + // to just insert it. + console.warn(`!! Recovering ${room.roomId} for tag ${targetTag} at position 0`); + if (!listsClone[targetTag]) { + console.warn(`!! List for tag ${targetTag} does not exist - creating`); + listsClone[targetTag] = []; + } + listsClone[targetTag].splice(0, 0, {room, category}); } } From 008554463d0478e9b06f0da35dce1af83d08eb55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 13 Nov 2019 09:52:59 +0100 Subject: [PATCH 077/334] Lifecycle: Move the event index deletion into the clear storage method. --- src/Lifecycle.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 7360cd3231..1e68bcc062 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -613,7 +613,6 @@ export function onLoggedOut() { // so that React components unmount first. This avoids React soft crashes // that can occur when components try to use a null client. dis.dispatch({action: 'on_logged_out'}); - EventIndexPeg.deleteEventIndex().done(); stopMatrixClient(); _clearStorage().done(); } @@ -633,7 +632,13 @@ function _clearStorage() { // we'll never make any requests, so can pass a bogus HS URL baseUrl: "", }); - return cli.clearStores(); + + const clear = async() => { + await EventIndexPeg.deleteEventIndex(); + await cli.clearStores(); + } + + return clear(); } /** From 1cc64f2426bc049257985b06855b9ba9dbcd0113 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 13 Nov 2019 10:10:35 +0100 Subject: [PATCH 078/334] Searching: Move the small helper functions out of the eventIndexSearch function. --- src/Searching.js | 146 +++++++++++++++++++++++------------------------ 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/src/Searching.js b/src/Searching.js index cd06d9bc67..cff5742b04 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -34,80 +34,80 @@ function serverSideSearch(term, roomId = undefined) { return searchPromise; } +async function combinedSearchFunc(searchTerm) { + // Create two promises, one for the local search, one for the + // server-side search. + const client = MatrixClientPeg.get(); + const serverSidePromise = serverSideSearch(searchTerm); + const localPromise = localSearchFunc(searchTerm); + + // Wait for both promises to resolve. + await Promise.all([serverSidePromise, localPromise]); + + // Get both search results. + const localResult = await localPromise; + const serverSideResult = await serverSidePromise; + + // Combine the search results into one result. + const result = {}; + + // Our localResult and serverSideResult are both ordered by + // recency separetly, 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; + + if (aEvent.origin_server_ts > + bEvent.origin_server_ts) return -1; + if (aEvent.origin_server_ts < + bEvent.origin_server_ts) return 1; + return 0; + }; + + result.count = localResult.count + serverSideResult.count; + result.results = localResult.results.concat( + serverSideResult.results).sort(compare); + result.highlights = localResult.highlights.concat( + serverSideResult.highlights); + + return result; +} + +async function localSearchFunc(searchTerm, roomId = undefined) { + const searchArgs = { + search_term: searchTerm, + before_limit: 1, + after_limit: 1, + order_by_recency: true, + }; + + if (roomId !== undefined) { + searchArgs.room_id = roomId; + } + + const eventIndex = EventIndexPeg.get(); + + const localResult = await eventIndex.search(searchArgs); + + const response = { + search_categories: { + room_events: localResult, + }, + }; + + const emptyResult = { + results: [], + highlights: [], + }; + + const result = MatrixClientPeg.get()._processRoomEventsSearch( + emptyResult, response); + + return result; +} + function eventIndexSearch(term, roomId = undefined) { - const combinedSearchFunc = async (searchTerm) => { - // Create two promises, one for the local search, one for the - // server-side search. - const client = MatrixClientPeg.get(); - const serverSidePromise = serverSideSearch(searchTerm); - const localPromise = localSearchFunc(searchTerm); - - // Wait for both promises to resolve. - await Promise.all([serverSidePromise, localPromise]); - - // Get both search results. - const localResult = await localPromise; - const serverSideResult = await serverSidePromise; - - // Combine the search results into one result. - const result = {}; - - // Our localResult and serverSideResult are both ordered by - // recency separetly, 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; - - if (aEvent.origin_server_ts > - bEvent.origin_server_ts) return -1; - if (aEvent.origin_server_ts < - bEvent.origin_server_ts) return 1; - return 0; - }; - - result.count = localResult.count + serverSideResult.count; - result.results = localResult.results.concat( - serverSideResult.results).sort(compare); - result.highlights = localResult.highlights.concat( - serverSideResult.highlights); - - return result; - }; - - const localSearchFunc = async (searchTerm, roomId = undefined) => { - const searchArgs = { - search_term: searchTerm, - before_limit: 1, - after_limit: 1, - order_by_recency: true, - }; - - if (roomId !== undefined) { - searchArgs.room_id = roomId; - } - - const eventIndex = EventIndexPeg.get(); - - const localResult = await eventIndex.search(searchArgs); - - const response = { - search_categories: { - room_events: localResult, - }, - }; - - const emptyResult = { - results: [], - highlights: [], - }; - - const result = MatrixClientPeg.get()._processRoomEventsSearch( - emptyResult, response); - - return result; - }; - let searchPromise; if (roomId !== undefined) { From 1df28c75262e113ea0111a6cc0dccb74a512e93d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 13 Nov 2019 10:30:38 +0100 Subject: [PATCH 079/334] Fix some lint errors. --- src/EventIndexPeg.js | 12 +++++++----- src/Lifecycle.js | 4 ++-- src/MatrixClientPeg.js | 2 -- src/Searching.js | 5 ++--- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index 86fb889c7a..15d34ea230 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -32,7 +32,9 @@ class EventIndexPeg { } /** - * Returns the current Event index object for the application. Can be null + * Get the current event index. + * + * @Returns The EventIndex object for the application. Can be null * if the platform doesn't support event indexing. */ get() { @@ -47,25 +49,25 @@ class EventIndexPeg { const platform = PlatformPeg.get(); if (!platform.supportsEventIndexing()) return false; - let index = new EventIndex(); + const index = new EventIndex(); const userId = MatrixClientPeg.get().getUserId(); // TODO log errors here and return false if it errors out. await index.init(userId); this.index = index; - return true + return true; } stop() { if (this.index === null) return; - index.stop(); + this.index.stop(); this.index = null; } async deleteEventIndex() { if (this.index === null) return; - index.deleteEventIndex(); + this.index.deleteEventIndex(); } } diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 1e68bcc062..aa900c81a1 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -633,10 +633,10 @@ function _clearStorage() { baseUrl: "", }); - const clear = async() => { + const clear = async () => { await EventIndexPeg.deleteEventIndex(); await cli.clearStores(); - } + }; return clear(); } diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 6c5b465bb0..bebb254afc 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -30,8 +30,6 @@ import {verificationMethods} from 'matrix-js-sdk/lib/crypto'; import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler"; import * as StorageManager from './utils/StorageManager'; import IdentityAuthClient from './IdentityAuthClient'; -import PlatformPeg from "./PlatformPeg"; -import EventIndexPeg from "./EventIndexPeg"; interface MatrixClientCreds { homeserverUrl: string, diff --git a/src/Searching.js b/src/Searching.js index cff5742b04..84e73b91f4 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -26,7 +26,7 @@ function serverSideSearch(term, roomId = undefined) { }; } - let searchPromise = MatrixClientPeg.get().searchRoomEvents({ + const searchPromise = MatrixClientPeg.get().searchRoomEvents({ filter: filter, term: term, }); @@ -37,7 +37,6 @@ function serverSideSearch(term, roomId = undefined) { async function combinedSearchFunc(searchTerm) { // Create two promises, one for the local search, one for the // server-side search. - const client = MatrixClientPeg.get(); const serverSidePromise = serverSideSearch(searchTerm); const localPromise = localSearchFunc(searchTerm); @@ -126,7 +125,7 @@ function eventIndexSearch(term, roomId = undefined) { searchPromise = combinedSearchFunc(term); } - return searchPromise + return searchPromise; } export default function eventSearch(term, roomId = undefined) { From 54b352f69cd1e9d82fff759c6838af1affca4f32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 13 Nov 2019 10:37:20 +0100 Subject: [PATCH 080/334] MatrixChat: Fix the limited timeline checkpoint adding. --- src/EventIndexing.js | 9 ++------- src/components/structures/MatrixChat.js | 4 +--- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/EventIndexing.js b/src/EventIndexing.js index ebd2ffe983..bf3f50690f 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -347,15 +347,10 @@ export default class EventIndexer { console.log("Seshat: Stopping crawler function"); } - async addCheckpointForLimitedRoom(roomId) { + async addCheckpointForLimitedRoom(room) { const platform = PlatformPeg.get(); if (!platform.supportsEventIndexing()) return; - if (!MatrixClientPeg.get().isRoomEncrypted(roomId)) return; - - const client = MatrixClientPeg.get(); - const room = client.getRoom(roomId); - - if (room === null) return; + if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; const timeline = room.getLiveTimeline(); const token = timeline.getPaginationToken("b"); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 0d3d5abd55..ccc8b5e1d6 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1223,8 +1223,6 @@ export default createReactClass({ * Called when the session is logged out */ _onLoggedOut: async function() { - const platform = PlatformPeg.get(); - this.notifyNewScreen('login'); this.setStateForNewView({ view: VIEWS.LOGIN, @@ -1313,7 +1311,7 @@ export default createReactClass({ const eventIndex = EventIndexPeg.get(); if (eventIndex === null) return; if (resetAllTimelines === true) return; - await eventIndex.addCheckpointForLimitedRoom(roomId); + await eventIndex.addCheckpointForLimitedRoom(room); }); cli.on('sync', function(state, prevState, data) { From 80b28004e15821bd127bee3121baabd1cf6226a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 13 Nov 2019 11:02:54 +0100 Subject: [PATCH 081/334] Searching: Define the room id in the const object. --- src/Searching.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Searching.js b/src/Searching.js index 84e73b91f4..ee46a66fb8 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -79,6 +79,7 @@ async function localSearchFunc(searchTerm, roomId = undefined) { before_limit: 1, after_limit: 1, order_by_recency: true, + room_id: undefined, }; if (roomId !== undefined) { From f453fea24acf110d0b297d8374234a8c873bec80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 13 Nov 2019 12:25:16 +0100 Subject: [PATCH 082/334] BasePlatform: Move the event indexing methods into a separate class. --- src/BaseEventIndexManager.js | 208 +++++++++++++++++++++++++++++++++++ src/BasePlatform.js | 41 +------ src/EventIndexPeg.js | 6 +- src/EventIndexing.js | 62 +++++------ 4 files changed, 246 insertions(+), 71 deletions(-) create mode 100644 src/BaseEventIndexManager.js diff --git a/src/BaseEventIndexManager.js b/src/BaseEventIndexManager.js new file mode 100644 index 0000000000..cd7a735e8d --- /dev/null +++ b/src/BaseEventIndexManager.js @@ -0,0 +1,208 @@ +// @flow + +/* +Copyright 2019 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export interface MatrixEvent { + type: string; + sender: string; + content: {}; + event_id: string; + origin_server_ts: number; + unsigned: ?{}; + room_id: string; +} + +export interface MatrixProfile { + avatar_url: string; + displayname: string; +} + +export interface CrawlerCheckpoint { + roomId: string; + token: string; + fullCrawl: boolean; + direction: string; +} + +export interface ResultContext { + events_before: [MatrixEvent]; + events_after: [MatrixEvent]; + profile_info: Map; +} + +export interface ResultsElement { + rank: number; + result: MatrixEvent; + context: ResultContext; +} + +export interface SearchResult { + count: number; + results: [ResultsElement]; + highlights: [string]; +} + +export interface SearchArgs { + search_term: string; + before_limit: number; + after_limit: number; + order_by_recency: boolean; + room_id: ?string; +} + +export interface HistoricEvent { + event: MatrixEvent; + profile: MatrixProfile; +} + +/** + * Base class for classes that provide platform-specific event indexing. + * + * Instances of this class are provided by the application. + */ +export default class BaseEventIndexManager { + /** + * Initialize the event index for the given user. + * + * @param {string} userId The unique identifier of the logged in user that + * owns the index. + * + * @return {Promise} A promise that will resolve when the event index is + * initialized. + */ + async initEventIndex(userId: string): Promise<> { + throw new Error("Unimplemented"); + } + + /** + * Queue up an event to be added to the index. + * + * @param {MatrixEvent} ev The event that should be added to the index. + * @param {MatrixProfile} profile The profile of the event sender at the + * time of the event receival. + * + * @return {Promise} A promise that will resolve when the was queued up for + * addition. + */ + async addEventToIndex(ev: MatrixEvent, profile: MatrixProfile): Promise<> { + throw new Error("Unimplemented"); + } + + /** + * Check if our event index is empty. + * + * @return {Promise} A promise that will resolve to true if the + * event index is empty, false otherwise. + */ + indexIsEmpty(): Promise { + throw new Error("Unimplemented"); + } + + /** + * Commit the previously queued up events to the index. + * + * @return {Promise} A promise that will resolve once the queued up events + * were added to the index. + */ + async commitLiveEvents(): Promise<> { + throw new Error("Unimplemented"); + } + + /** + * Search the event index using the given term for matching events. + * + * @param {SearchArgs} searchArgs The search configuration sets what should + * be searched for and what should be contained in the search result. + * + * @return {Promise<[SearchResult]>} A promise that will resolve to an array + * of search results once the search is done. + */ + async searchEventIndex(searchArgs: SearchArgs): Promise { + throw new Error("Unimplemented"); + } + + /** + * Add events from the room history to the event index. + * + * This is used to add a batch of events to the index. + * + * @param {[HistoricEvent]} events The list of events and profiles that + * should be added to the event index. + * @param {[CrawlerCheckpoint]} checkpoint A new crawler checkpoint that + * should be stored in the index which should be used to continue crawling + * the room. + * @param {[CrawlerCheckpoint]} oldCheckpoint The checkpoint that was used + * to fetch the current batch of events. This checkpoint will be removed + * from the index. + * + * @return {Promise} A promise that will resolve to true if all the events + * were already added to the index, false otherwise. + */ + async addHistoricEvents( + events: [HistoricEvent], + checkpoint: CrawlerCheckpoint | null = null, + oldCheckpoint: CrawlerCheckpoint | null = null, + ): Promise { + throw new Error("Unimplemented"); + } + + /** + * Add a new crawler checkpoint to the index. + * + * @param {CrawlerCheckpoint} checkpoint The checkpoint that should be added + * to the index. + * + * @return {Promise} A promise that will resolve once the checkpoint has + * been stored. + */ + async addCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise<> { + throw new Error("Unimplemented"); + } + + /** + * Add a new crawler checkpoint to the index. + * + * @param {CrawlerCheckpoint} checkpoint The checkpoint that should be + * removed from the index. + * + * @return {Promise} A promise that will resolve once the checkpoint has + * been removed. + */ + async removeCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise<> { + throw new Error("Unimplemented"); + } + + /** + * Load the stored checkpoints from the index. + * + * @return {Promise<[CrawlerCheckpoint]>} A promise that will resolve to an + * array of crawler checkpoints once they have been loaded from the index. + */ + async loadCheckpoints(): Promise<[CrawlerCheckpoint]> { + throw new Error("Unimplemented"); + } + + /** + * Delete our current event index. + * + * @return {Promise} A promise that will resolve once the event index has + * been deleted. + */ + async deleteEventIndex(): Promise<> { + throw new Error("Unimplemented"); + } +} diff --git a/src/BasePlatform.js b/src/BasePlatform.js index 7f5df822e4..582ac24cb0 100644 --- a/src/BasePlatform.js +++ b/src/BasePlatform.js @@ -19,6 +19,7 @@ limitations under the License. */ import dis from './dispatcher'; +import BaseEventIndexManager from './BaseEventIndexManager'; /** * Base class for classes that provide platform-specific functionality @@ -152,43 +153,7 @@ export default class BasePlatform { throw new Error("Unimplemented"); } - supportsEventIndexing(): boolean { - return false; - } - - async initEventIndex(userId: string): boolean { - throw new Error("Unimplemented"); - } - - async addEventToIndex(ev: {}, profile: {}): void { - throw new Error("Unimplemented"); - } - - indexIsEmpty(): Promise { - throw new Error("Unimplemented"); - } - - async commitLiveEvents(): void { - throw new Error("Unimplemented"); - } - - async searchEventIndex(term: string): Promise<{}> { - throw new Error("Unimplemented"); - } - - async addHistoricEvents(events: [], checkpoint: {} = null, oldCheckpoint: {} = null): Promise { - throw new Error("Unimplemented"); - } - - async addCrawlerCheckpoint(checkpoint: {}): Promise<> { - throw new Error("Unimplemented"); - } - - async removeCrawlerCheckpoint(checkpoint: {}): Promise<> { - throw new Error("Unimplemented"); - } - - async deleteEventIndex(): Promise<> { - throw new Error("Unimplemented"); + getEventIndexingManager(): BaseEventIndexManager | null { + return null; } } diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index 15d34ea230..bec3f075b6 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -46,9 +46,11 @@ class EventIndexPeg { * otherwise. */ async init() { - const platform = PlatformPeg.get(); - if (!platform.supportsEventIndexing()) return false; + const indexManager = PlatformPeg.get().getEventIndexingManager(); + console.log("Initializing event index, got {}", indexManager); + if (indexManager === null) return false; + console.log("Seshat: Creatingnew EventIndex object", indexManager); const index = new EventIndex(); const userId = MatrixClientPeg.get().getUserId(); diff --git a/src/EventIndexing.js b/src/EventIndexing.js index bf3f50690f..60482b76b5 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -31,19 +31,19 @@ export default class EventIndexer { } async init(userId) { - const platform = PlatformPeg.get(); - if (!platform.supportsEventIndexing()) return false; - platform.initEventIndex(userId); + const indexManager = PlatformPeg.get().getEventIndexingManager(); + if (indexManager === null) return false; + indexManager.initEventIndex(userId); return true; } async onSync(state, prevState, data) { - const platform = PlatformPeg.get(); - if (!platform.supportsEventIndexing()) return; + const indexManager = PlatformPeg.get().getEventIndexingManager(); + if (indexManager === null) return; if (prevState === null && state === "PREPARED") { // Load our stored checkpoints, if any. - this.crawlerChekpoints = await platform.loadCheckpoints(); + this.crawlerChekpoints = await indexManager.loadCheckpoints(); console.log("Seshat: Loaded checkpoints", this.crawlerChekpoints); return; @@ -85,8 +85,8 @@ export default class EventIndexer { direction: "f", }; - await platform.addCrawlerCheckpoint(backCheckpoint); - await platform.addCrawlerCheckpoint(forwardCheckpoint); + await indexManager.addCrawlerCheckpoint(backCheckpoint); + await indexManager.addCrawlerCheckpoint(forwardCheckpoint); this.crawlerChekpoints.push(backCheckpoint); this.crawlerChekpoints.push(forwardCheckpoint); })); @@ -95,7 +95,7 @@ export default class EventIndexer { // If our indexer is empty we're most likely running Riot the // first time with indexing support or running it with an // initial sync. Add checkpoints to crawl our encrypted rooms. - const eventIndexWasEmpty = await platform.isEventIndexEmpty(); + const eventIndexWasEmpty = await indexManager.isEventIndexEmpty(); if (eventIndexWasEmpty) await addInitialCheckpoints(); // Start our crawler. @@ -107,14 +107,14 @@ export default class EventIndexer { // A sync was done, presumably we queued up some live events, // commit them now. console.log("Seshat: Committing events"); - await platform.commitLiveEvents(); + await indexManager.commitLiveEvents(); return; } } async onRoomTimeline(ev, room, toStartOfTimeline, removed, data) { - const platform = PlatformPeg.get(); - if (!platform.supportsEventIndexing()) return; + const indexManager = PlatformPeg.get().getEventIndexingManager(); + if (indexManager === null) return; // We only index encrypted rooms locally. if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; @@ -139,8 +139,8 @@ export default class EventIndexer { } async onEventDecrypted(ev, err) { - const platform = PlatformPeg.get(); - if (!platform.supportsEventIndexing()) return; + const indexManager = PlatformPeg.get().getEventIndexingManager(); + if (indexManager === null) return; const eventId = ev.getId(); @@ -151,8 +151,8 @@ export default class EventIndexer { } async addLiveEventToIndex(ev) { - const platform = PlatformPeg.get(); - if (!platform.supportsEventIndexing()) return; + const indexManager = PlatformPeg.get().getEventIndexingManager(); + if (indexManager === null) return; if (["m.room.message", "m.room.name", "m.room.topic"] .indexOf(ev.getType()) == -1) { @@ -165,7 +165,7 @@ export default class EventIndexer { avatar_url: ev.sender.getMxcAvatarUrl(), }; - platform.addEventToIndex(e, profile); + indexManager.addEventToIndex(e, profile); } async crawlerFunc(handle) { @@ -180,7 +180,7 @@ export default class EventIndexer { console.log("Seshat: Started crawler function"); const client = MatrixClientPeg.get(); - const platform = PlatformPeg.get(); + const indexManager = PlatformPeg.get().getEventIndexingManager(); handle.cancel = () => { cancelled = true; @@ -223,14 +223,14 @@ export default class EventIndexer { } catch (e) { console.log("Seshat: Error crawling events:", e); this.crawlerChekpoints.push(checkpoint); - continue + continue; } if (res.chunk.length === 0) { console.log("Seshat: Done with the checkpoint", checkpoint); // We got to the start/end of our timeline, lets just // delete our checkpoint and go back to sleep. - await platform.removeCrawlerCheckpoint(checkpoint); + await indexManager.removeCrawlerCheckpoint(checkpoint); continue; } @@ -323,7 +323,7 @@ export default class EventIndexer { ); try { - const eventsAlreadyAdded = await platform.addHistoricEvents( + const eventsAlreadyAdded = await indexManager.addHistoricEvents( events, newCheckpoint, checkpoint); // If all events were already indexed we assume that we catched // up with our index and don't need to crawl the room further. @@ -332,7 +332,7 @@ export default class EventIndexer { if (eventsAlreadyAdded === true && newCheckpoint.fullCrawl !== true) { console.log("Seshat: Checkpoint had already all events", "added, stopping the crawl", checkpoint); - await platform.removeCrawlerCheckpoint(newCheckpoint); + await indexManager.removeCrawlerCheckpoint(newCheckpoint); } else { this.crawlerChekpoints.push(newCheckpoint); } @@ -348,8 +348,8 @@ export default class EventIndexer { } async addCheckpointForLimitedRoom(room) { - const platform = PlatformPeg.get(); - if (!platform.supportsEventIndexing()) return; + const indexManager = PlatformPeg.get().getEventIndexingManager(); + if (indexManager === null) return; if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; const timeline = room.getLiveTimeline(); @@ -372,19 +372,19 @@ export default class EventIndexer { console.log("Seshat: Added checkpoint because of a limited timeline", backwardsCheckpoint, forwardsCheckpoint); - await platform.addCrawlerCheckpoint(backwardsCheckpoint); - await platform.addCrawlerCheckpoint(forwardsCheckpoint); + await indexManager.addCrawlerCheckpoint(backwardsCheckpoint); + await indexManager.addCrawlerCheckpoint(forwardsCheckpoint); this.crawlerChekpoints.push(backwardsCheckpoint); this.crawlerChekpoints.push(forwardsCheckpoint); } async deleteEventIndex() { - const platform = PlatformPeg.get(); - if (platform.supportsEventIndexing()) { + const indexManager = PlatformPeg.get().getEventIndexingManager(); + if (indexManager !== null) { console.log("Seshat: Deleting event index."); this.crawlerRef.cancel(); - await platform.deleteEventIndex(); + await indexManager.deleteEventIndex(); } } @@ -400,7 +400,7 @@ export default class EventIndexer { } async search(searchArgs) { - const platform = PlatformPeg.get(); - return platform.searchEventIndex(searchArgs) + const indexManager = PlatformPeg.get().getEventIndexingManager(); + return indexManager.searchEventIndex(searchArgs); } } From 1316e04776b90ec7cc3d7770822b400795de171b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 13 Nov 2019 15:23:08 +0100 Subject: [PATCH 083/334] EventIndexing: Check if there is a room when resetting the timeline. --- src/EventIndexing.js | 13 ++----------- src/components/structures/MatrixChat.js | 4 ++-- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/EventIndexing.js b/src/EventIndexing.js index 60482b76b5..4817df4b32 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -347,7 +347,7 @@ export default class EventIndexer { console.log("Seshat: Stopping crawler function"); } - async addCheckpointForLimitedRoom(room) { + async onLimitedTimeline(room) { const indexManager = PlatformPeg.get().getEventIndexingManager(); if (indexManager === null) return; if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; @@ -362,21 +362,12 @@ export default class EventIndexer { direction: "b", }; - const forwardsCheckpoint = { - roomId: room.roomId, - token: token, - fullCrawl: false, - direction: "f", - }; - console.log("Seshat: Added checkpoint because of a limited timeline", - backwardsCheckpoint, forwardsCheckpoint); + backwardsCheckpoint); await indexManager.addCrawlerCheckpoint(backwardsCheckpoint); - await indexManager.addCrawlerCheckpoint(forwardsCheckpoint); this.crawlerChekpoints.push(backwardsCheckpoint); - this.crawlerChekpoints.push(forwardsCheckpoint); } async deleteEventIndex() { diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index ccc8b5e1d6..f78bb5c168 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1310,8 +1310,8 @@ export default createReactClass({ cli.on("Room.timelineReset", async (room, timelineSet, resetAllTimelines) => { const eventIndex = EventIndexPeg.get(); if (eventIndex === null) return; - if (resetAllTimelines === true) return; - await eventIndex.addCheckpointForLimitedRoom(room); + if (room === null) return; + await eventIndex.onLimitedTimeline(room); }); cli.on('sync', function(state, prevState, data) { From ab7f34b45a66748fde1ee361faa7f31bc86db0e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 13 Nov 2019 15:26:27 +0100 Subject: [PATCH 084/334] EventIndexing: Don't mention Seshat in the logs. --- src/EventIndexing.js | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/EventIndexing.js b/src/EventIndexing.js index 4817df4b32..f67d4c9eb3 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -44,7 +44,7 @@ export default class EventIndexer { if (prevState === null && state === "PREPARED") { // Load our stored checkpoints, if any. this.crawlerChekpoints = await indexManager.loadCheckpoints(); - console.log("Seshat: Loaded checkpoints", + console.log("EventIndex: Loaded checkpoints", this.crawlerChekpoints); return; } @@ -62,7 +62,7 @@ export default class EventIndexer { // rooms can use the search provided by the Homeserver. const encryptedRooms = rooms.filter(isRoomEncrypted); - console.log("Seshat: Adding initial crawler checkpoints"); + console.log("EventIndex: Adding initial crawler checkpoints"); // Gather the prev_batch tokens and create checkpoints for // our message crawler. @@ -70,7 +70,7 @@ export default class EventIndexer { const timeline = room.getLiveTimeline(); const token = timeline.getPaginationToken("b"); - console.log("Seshat: Got token for indexer", + console.log("EventIndex: Got token for indexer", room.roomId, token); const backCheckpoint = { @@ -106,7 +106,7 @@ export default class EventIndexer { if (prevState === "SYNCING" && state === "SYNCING") { // A sync was done, presumably we queued up some live events, // commit them now. - console.log("Seshat: Committing events"); + console.log("EventIndex: Committing events"); await indexManager.commitLiveEvents(); return; } @@ -177,7 +177,7 @@ export default class EventIndexer { let cancelled = false; - console.log("Seshat: Started crawler function"); + console.log("EventIndex: Started crawler function"); const client = MatrixClientPeg.get(); const indexManager = PlatformPeg.get().getEventIndexingManager(); @@ -192,10 +192,10 @@ export default class EventIndexer { // here. await sleep(this._crawler_timeout); - console.log("Seshat: Running the crawler loop."); + console.log("EventIndex: Running the crawler loop."); if (cancelled) { - console.log("Seshat: Cancelling the crawler."); + console.log("EventIndex: Cancelling the crawler."); break; } @@ -207,7 +207,7 @@ export default class EventIndexer { continue; } - console.log("Seshat: crawling using checkpoint", checkpoint); + console.log("EventIndex: crawling using checkpoint", checkpoint); // We have a checkpoint, let us fetch some messages, again, very // conservatively to not bother our Homeserver too much. @@ -221,13 +221,13 @@ export default class EventIndexer { checkpoint.roomId, checkpoint.token, 100, checkpoint.direction); } catch (e) { - console.log("Seshat: Error crawling events:", e); + console.log("EventIndex: Error crawling events:", e); this.crawlerChekpoints.push(checkpoint); continue; } if (res.chunk.length === 0) { - console.log("Seshat: Done with the checkpoint", checkpoint); + console.log("EventIndex: Done with the checkpoint", checkpoint); // We got to the start/end of our timeline, lets just // delete our checkpoint and go back to sleep. await indexManager.removeCrawlerCheckpoint(checkpoint); @@ -289,7 +289,7 @@ export default class EventIndexer { // stage? const filteredEvents = matrixEvents.filter(isValidEvent); - // Let us convert the events back into a format that Seshat can + // Let us convert the events back into a format that EventIndex can // consume. const events = filteredEvents.map((ev) => { const jsonEvent = ev.toJSON(); @@ -317,7 +317,7 @@ export default class EventIndexer { }; console.log( - "Seshat: Crawled room", + "EventIndex: Crawled room", client.getRoom(checkpoint.roomId).name, "and fetched", events.length, "events.", ); @@ -330,21 +330,21 @@ export default class EventIndexer { // Let us delete the checkpoint in that case, otherwise push // the new checkpoint to be used by the crawler. if (eventsAlreadyAdded === true && newCheckpoint.fullCrawl !== true) { - console.log("Seshat: Checkpoint had already all events", + console.log("EventIndex: Checkpoint had already all events", "added, stopping the crawl", checkpoint); await indexManager.removeCrawlerCheckpoint(newCheckpoint); } else { this.crawlerChekpoints.push(newCheckpoint); } } catch (e) { - console.log("Seshat: Error durring a crawl", e); + console.log("EventIndex: Error durring a crawl", e); // An error occured, put the checkpoint back so we // can retry. this.crawlerChekpoints.push(checkpoint); } } - console.log("Seshat: Stopping crawler function"); + console.log("EventIndex: Stopping crawler function"); } async onLimitedTimeline(room) { @@ -362,7 +362,7 @@ export default class EventIndexer { direction: "b", }; - console.log("Seshat: Added checkpoint because of a limited timeline", + console.log("EventIndex: Added checkpoint because of a limited timeline", backwardsCheckpoint); await indexManager.addCrawlerCheckpoint(backwardsCheckpoint); @@ -373,7 +373,7 @@ export default class EventIndexer { async deleteEventIndex() { const indexManager = PlatformPeg.get().getEventIndexingManager(); if (indexManager !== null) { - console.log("Seshat: Deleting event index."); + console.log("EventIndex: Deleting event index."); this.crawlerRef.cancel(); await indexManager.deleteEventIndex(); } From c33f5ba0ca8292116e1623a9d0c932aac62479a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 13 Nov 2019 15:39:06 +0100 Subject: [PATCH 085/334] BaseEventIndexManager: Add a method to perform runtime checks for indexing support. --- src/BaseEventIndexManager.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/BaseEventIndexManager.js b/src/BaseEventIndexManager.js index cd7a735e8d..a74eac658a 100644 --- a/src/BaseEventIndexManager.js +++ b/src/BaseEventIndexManager.js @@ -75,6 +75,19 @@ export interface HistoricEvent { * Instances of this class are provided by the application. */ export default class BaseEventIndexManager { + /** + * Does our EventIndexManager support event indexing. + * + * If an EventIndexManager imlpementor has runtime dependencies that + * optionally enable event indexing they may override this method to perform + * the necessary runtime checks here. + * + * @return {Promise} A promise that will resolve to true if event indexing + * is supported, false otherwise. + */ + async supportsEventIndexing(): Promise { + return true; + } /** * Initialize the event index for the given user. * From bf558b46c3cfc9ee7b19dbe7a92ac79ed118e498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 13 Nov 2019 15:39:39 +0100 Subject: [PATCH 086/334] EventIndexPeg: Clean up the event index initialization. --- src/EventIndexPeg.js | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index bec3f075b6..3ce88339eb 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -47,15 +47,25 @@ class EventIndexPeg { */ async init() { const indexManager = PlatformPeg.get().getEventIndexingManager(); - console.log("Initializing event index, got {}", indexManager); if (indexManager === null) return false; - console.log("Seshat: Creatingnew EventIndex object", indexManager); - const index = new EventIndex(); + if (await indexManager.supportsEventIndexing() !== true) { + console.log("EventIndex: Platform doesn't support event indexing,", + "not initializing."); + return false; + } + const index = new EventIndex(); const userId = MatrixClientPeg.get().getUserId(); - // TODO log errors here and return false if it errors out. - await index.init(userId); + + try { + await index.init(userId); + } catch (e) { + console.log("EventIndex: Error initializing the event index", e); + } + + console.log("EventIndex: Successfully initialized the event index"); + this.index = index; return true; From c26df9d9efc836b1a6b5d660edd702448a22b3a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 13 Nov 2019 15:57:12 +0100 Subject: [PATCH 087/334] EventIndexing: Fix a typo. --- src/EventIndexing.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/EventIndexing.js b/src/EventIndexing.js index f67d4c9eb3..af77979040 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -22,7 +22,7 @@ import MatrixClientPeg from "./MatrixClientPeg"; */ export default class EventIndexer { constructor() { - this.crawlerChekpoints = []; + this.crawlerCheckpoints = []; // The time that the crawler will wait between /rooms/{room_id}/messages // requests this._crawler_timeout = 3000; @@ -43,9 +43,9 @@ export default class EventIndexer { if (prevState === null && state === "PREPARED") { // Load our stored checkpoints, if any. - this.crawlerChekpoints = await indexManager.loadCheckpoints(); + this.crawlerCheckpoints = await indexManager.loadCheckpoints(); console.log("EventIndex: Loaded checkpoints", - this.crawlerChekpoints); + this.crawlerCheckpoints); return; } @@ -87,8 +87,8 @@ export default class EventIndexer { await indexManager.addCrawlerCheckpoint(backCheckpoint); await indexManager.addCrawlerCheckpoint(forwardCheckpoint); - this.crawlerChekpoints.push(backCheckpoint); - this.crawlerChekpoints.push(forwardCheckpoint); + this.crawlerCheckpoints.push(backCheckpoint); + this.crawlerCheckpoints.push(forwardCheckpoint); })); }; @@ -199,7 +199,7 @@ export default class EventIndexer { break; } - const checkpoint = this.crawlerChekpoints.shift(); + const checkpoint = this.crawlerCheckpoints.shift(); /// There is no checkpoint available currently, one may appear if // a sync with limited room timelines happens, so go back to sleep. @@ -222,7 +222,7 @@ export default class EventIndexer { checkpoint.direction); } catch (e) { console.log("EventIndex: Error crawling events:", e); - this.crawlerChekpoints.push(checkpoint); + this.crawlerCheckpoints.push(checkpoint); continue; } @@ -334,13 +334,13 @@ export default class EventIndexer { "added, stopping the crawl", checkpoint); await indexManager.removeCrawlerCheckpoint(newCheckpoint); } else { - this.crawlerChekpoints.push(newCheckpoint); + this.crawlerCheckpoints.push(newCheckpoint); } } catch (e) { console.log("EventIndex: Error durring a crawl", e); // An error occured, put the checkpoint back so we // can retry. - this.crawlerChekpoints.push(checkpoint); + this.crawlerCheckpoints.push(checkpoint); } } @@ -367,7 +367,7 @@ export default class EventIndexer { await indexManager.addCrawlerCheckpoint(backwardsCheckpoint); - this.crawlerChekpoints.push(backwardsCheckpoint); + this.crawlerCheckpoints.push(backwardsCheckpoint); } async deleteEventIndex() { From f2f8a82876a4dc46701ee71b79fa47bf697f7002 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Wed, 13 Nov 2019 03:31:51 +0000 Subject: [PATCH 088/334] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1898 of 1898 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 | 36 ++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 185026aad5..a4898bd328 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2284,5 +2284,39 @@ "You cancelled": "您已取消", "%(name)s cancelled": "%(name)s 已取消", "%(name)s wants to verify": "%(name)s 想要驗證", - "You sent a verification request": "您已傳送了驗證請求" + "You sent a verification request": "您已傳送了驗證請求", + "Try out new ways to ignore people (experimental)": "試用新的方式來忽略人們(實驗性)", + "My Ban List": "我的封鎖清單", + "This is your list of users/servers you have blocked - don't leave the room!": "這是您已封鎖的的使用者/伺服器清單,不要離開聊天室!", + "Ignored/Blocked": "已忽略/已封鎖", + "Error adding ignored user/server": "新增要忽略的使用者/伺服器錯誤", + "Something went wrong. Please try again or view your console for hints.": "有東西出問題了。請重試或檢視您的主控臺以取得更多資訊。", + "Error subscribing to list": "訂閱清單發生錯誤", + "Please verify the room ID or alias and try again.": "請驗證聊天室 ID 或別名並再試一次。", + "Error removing ignored user/server": "移除要忽略的使用者/伺服器發生錯誤", + "Error unsubscribing from list": "從清單取消訂閱時發生錯誤", + "Please try again or view your console for hints.": "請重試或檢視您的主控臺以取得更多資訊。", + "None": "無", + "Ban list rules - %(roomName)s": "封鎖清單規則 - %(roomName)s", + "Server rules": "伺服器規則", + "User rules": "使用者規則", + "You have not ignored anyone.": "您尚未忽略任何人。", + "You are currently ignoring:": "您目前正在忽略:", + "You are not subscribed to any lists": "您尚未訂閱任何清單", + "Unsubscribe": "取消訂閱", + "View rules": "檢視規則", + "You are currently subscribed to:": "您目前已訂閱:", + "⚠ These settings are meant for advanced users.": "⚠ 這些設定適用於進階使用者。", + "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "在此新增您想要忽略的使用者與伺服器。使用星號以讓 Riot 核對所有字元。舉例來說,@bot:* 將會忽略在任何伺服器上,所有有 'bot' 名稱的使用者。", + "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "忽略人們已透過封鎖清單完成,其中包含了誰要被封鎖的規則。訂閱封鎖清單代表被此清單封鎖的使用者/伺服器會對您隱藏。", + "Personal ban list": "個人封鎖清單", + "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "您的個人封鎖清單包含了您個人不想要看到的所有使用者/伺服器。在忽略您的第一個使用者/伺服器後,您的聊天室清單中會出現新的聊天室,其名為「我的封鎖清單」,留在這個聊天室裡面以讓封鎖清單生效。", + "Server or user ID to ignore": "要忽略的伺服器或使用者 ID", + "eg: @bot:* or example.org": "例子:@bot:* 或 example.org", + "Subscribed lists": "已訂閱的清單", + "Subscribing to a ban list will cause you to join it!": "訂閱封鎖清單會讓您加入它!", + "If this isn't what you want, please use a different tool to ignore users.": "如果這不是您想要的,請使用不同的工具來忽略使用者。", + "Room ID or alias of ban list": "聊天室 ID 或封鎖清單的別名", + "Subscribe": "訂閱", + "You have ignored this user, so their message is hidden. Show anyways.": "您已經忽略了這個使用者,所以他們的訊息會隱藏。無論如何都顯示。" } From 95b8e83cd3f97cf6ce3f2ff144bc505518e155c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Wed, 13 Nov 2019 10:31:31 +0000 Subject: [PATCH 089/334] Translated using Weblate (French) Currently translated at 100.0% (1898 of 1898 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 | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 40840c8d58..f4e889d955 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2291,5 +2291,39 @@ "You cancelled": "Vous avez annulé", "%(name)s cancelled": "%(name)s a annulé", "%(name)s wants to verify": "%(name)s veut vérifier", - "You sent a verification request": "Vous avez envoyé une demande de vérification" + "You sent a verification request": "Vous avez envoyé une demande de vérification", + "Try out new ways to ignore people (experimental)": "Essayez de nouvelles façons d’ignorer les gens (expérimental)", + "My Ban List": "Ma liste de bannissement", + "This is your list of users/servers you have blocked - don't leave the room!": "C’est la liste des utilisateurs/serveurs que vous avez bloqués − ne quittez pas le salon !", + "Ignored/Blocked": "Ignoré/bloqué", + "Error adding ignored user/server": "Erreur lors de l’ajout de l’utilisateur/du serveur ignoré", + "Something went wrong. Please try again or view your console for hints.": "Une erreur est survenue. Réessayez ou consultez votre console pour des indices.", + "Error subscribing to list": "Erreur lors de l’inscription à la liste", + "Please verify the room ID or alias and try again.": "Vérifiez l’identifiant ou l’alias du salon et réessayez.", + "Error removing ignored user/server": "Erreur lors de la suppression de l’utilisateur/du serveur ignoré", + "Error unsubscribing from list": "Erreur lors de la désinscription de la liste", + "Please try again or view your console for hints.": "Réessayez ou consultez votre console pour des indices.", + "None": "Aucun", + "Ban list rules - %(roomName)s": "Règles de la liste de bannissement − %(roomName)s", + "Server rules": "Règles de serveur", + "User rules": "Règles d’utilisateur", + "You have not ignored anyone.": "Vous n’avez ignoré personne.", + "You are currently ignoring:": "Vous ignorez actuellement :", + "You are not subscribed to any lists": "Vous n’êtes inscrit(e) à aucune liste", + "Unsubscribe": "Se désinscrire", + "View rules": "Voir les règles", + "You are currently subscribed to:": "Vous êtes actuellement inscrit(e) à :", + "⚠ These settings are meant for advanced users.": "⚠ Ces paramètres sont prévus pour les utilisateurs avancés.", + "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "Ajoutez les utilisateurs et les serveurs que vous voulez ignorer ici. Utilisez des astérisques pour remplacer n’importe quel caractère. Par exemple, @bot:* ignorerait tous les utilisateurs qui ont le nom « bot » sur n’importe quel serveur.", + "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "Ignorer les gens est possible grâce à des listes de bannissement qui contiennent des règles sur les personnes à bannir. L’inscription à une liste de bannissement signifie que les utilisateurs/serveurs bloqués par cette liste seront cachés pour vous.", + "Personal ban list": "Liste de bannissement personnelle", + "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "Votre liste de bannissement personnelle contient tous les utilisateurs/serveurs dont vous ne voulez pas voir les messages personnellement. Quand vous aurez ignoré votre premier utilisateur/serveur, un nouveau salon nommé « Ma liste de bannissement » apparaîtra dans la liste de vos salons − restez dans ce salon pour que la liste de bannissement soit effective.", + "Server or user ID to ignore": "Serveur ou identifiant d’utilisateur à ignorer", + "eg: @bot:* or example.org": "par ex. : @bot:* ou exemple.org", + "Subscribed lists": "Listes souscrites", + "Subscribing to a ban list will cause you to join it!": "En vous inscrivant à une liste de bannissement, vous la rejoindrez !", + "If this isn't what you want, please use a different tool to ignore users.": "Si ce n’est pas ce que vous voulez, utilisez un autre outil pour ignorer les utilisateurs.", + "Room ID or alias of ban list": "Identifiant ou alias du salon de la liste de bannissement", + "Subscribe": "S’inscrire", + "You have ignored this user, so their message is hidden. Show anyways.": "Vous avez ignoré cet utilisateur, donc ses messages sont cachés. Les montrer quand même." } From f0696dcc3f7922a007bf9b3f2b547de9d6b528d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=ED=83=9C=EC=84=AD?= Date: Wed, 13 Nov 2019 01:12:50 +0000 Subject: [PATCH 090/334] Translated using Weblate (Korean) Currently translated at 98.4% (1868 of 1898 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ko/ --- src/i18n/strings/ko.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index 3d01d5be67..01ef0e8ae1 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -2135,5 +2135,9 @@ "You cancelled": "당신이 취소했습니다", "%(name)s cancelled": "%(name)s님이 취소했습니다", "%(name)s wants to verify": "%(name)s님이 확인을 요청합니다", - "You sent a verification request": "확인 요청을 보냈습니다" + "You sent a verification request": "확인 요청을 보냈습니다", + "Try out new ways to ignore people (experimental)": "새 방식으로 사람들을 무시하기 (실험)", + "My Ban List": "차단 목록", + "This is your list of users/servers you have blocked - don't leave the room!": "차단한 사용자/서버 목록입니다 - 방을 떠나지 마세요!", + "Ignored/Blocked": "무시됨/차단됨" } From cc2ee53824b955e513def52cf4a08118d853e646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 13 Nov 2019 16:21:26 +0100 Subject: [PATCH 091/334] EventIndex: Add some more docs and fix some lint issues. --- src/BaseEventIndexManager.js | 2 +- src/BasePlatform.js | 6 ++++++ src/EventIndexPeg.js | 20 ++++++++++++++++---- src/components/structures/RoomView.js | 2 +- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/BaseEventIndexManager.js b/src/BaseEventIndexManager.js index a74eac658a..48a96c4d88 100644 --- a/src/BaseEventIndexManager.js +++ b/src/BaseEventIndexManager.js @@ -168,7 +168,7 @@ export default class BaseEventIndexManager { async addHistoricEvents( events: [HistoricEvent], checkpoint: CrawlerCheckpoint | null = null, - oldCheckpoint: CrawlerCheckpoint | null = null, + oldCheckpoint: CrawlerCheckpoint | null = null, ): Promise { throw new Error("Unimplemented"); } diff --git a/src/BasePlatform.js b/src/BasePlatform.js index 582ac24cb0..f6301fd173 100644 --- a/src/BasePlatform.js +++ b/src/BasePlatform.js @@ -153,6 +153,12 @@ export default class BasePlatform { throw new Error("Unimplemented"); } + /** + * Get our platform specific EventIndexManager. + * + * @return {BaseEventIndexManager} The EventIndex manager for our platform, + * can be null if the platform doesn't support event indexing. + */ getEventIndexingManager(): BaseEventIndexManager | null { return null; } diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index 3ce88339eb..1b380e273f 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -34,16 +34,16 @@ class EventIndexPeg { /** * Get the current event index. * - * @Returns The EventIndex object for the application. Can be null - * if the platform doesn't support event indexing. + * @return {EventIndex} The current event index. */ get() { return this.index; } /** Create a new EventIndex and initialize it if the platform supports it. - * Returns true if an EventIndex was successfully initialized, false - * otherwise. + * + * @return {Promise} A promise that will resolve to true if an + * EventIndex was successfully initialized, false otherwise. */ async init() { const indexManager = PlatformPeg.get().getEventIndexingManager(); @@ -71,15 +71,27 @@ class EventIndexPeg { return true; } + /** + * Stop our event indexer. + */ stop() { if (this.index === null) return; this.index.stop(); this.index = null; } + /** + * Delete our event indexer. + * + * After a call to this the init() method will need to be called again. + * + * @return {Promise} A promise that will resolve once the event index is + * deleted. + */ async deleteEventIndex() { if (this.index === null) return; this.index.deleteEventIndex(); + this.index = null; } } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 9fe54ad164..6dee60bec7 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1131,7 +1131,7 @@ module.exports = createReactClass({ this.searchId = new Date().getTime(); let roomId; - if (scope === "Room") roomId = this.state.room.roomId, + if (scope === "Room") roomId = this.state.room.roomId; debuglog("sending search request"); const searchPromise = eventSearch(term, roomId); From 368a77ec3ef318f7e0e55832bf97877b8575f737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 13 Nov 2019 16:35:04 +0100 Subject: [PATCH 092/334] EventIndexing: Fix a style issue. --- src/EventIndexing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EventIndexing.js b/src/EventIndexing.js index af77979040..5830106e84 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -25,7 +25,7 @@ export default class EventIndexer { this.crawlerCheckpoints = []; // The time that the crawler will wait between /rooms/{room_id}/messages // requests - this._crawler_timeout = 3000; + this._crawlerTimeout = 3000; this._crawlerRef = null; this.liveEventsForIndex = new Set(); } From d4b31cb7e037301b0786372d3ae643c96b2b48e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 13 Nov 2019 16:35:26 +0100 Subject: [PATCH 093/334] EventIndexing: Move the max events per crawl constant into the class. --- src/EventIndexing.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/EventIndexing.js b/src/EventIndexing.js index 5830106e84..77c4022480 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -26,6 +26,9 @@ export default class EventIndexer { // The time that the crawler will wait between /rooms/{room_id}/messages // requests this._crawlerTimeout = 3000; + // The maximum number of events our crawler should fetch in a single + // crawl. + this._eventsPerCrawl = 100; this._crawlerRef = null; this.liveEventsForIndex = new Set(); } @@ -218,7 +221,7 @@ export default class EventIndexer { try { res = await client._createMessagesRequest( - checkpoint.roomId, checkpoint.token, 100, + checkpoint.roomId, checkpoint.token, this._eventsPerCrawl, checkpoint.direction); } catch (e) { console.log("EventIndex: Error crawling events:", e); From 9b32ec10b43cc274df28d938610fbf8c4b53479b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 13 Nov 2019 16:47:21 +0100 Subject: [PATCH 094/334] EventIndexing: Use the correct timeout value. --- src/EventIndexing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EventIndexing.js b/src/EventIndexing.js index 77c4022480..67bd894c67 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -193,7 +193,7 @@ export default class EventIndexer { // This is a low priority task and we don't want to spam our // Homeserver with /messages requests so we set a hefty timeout // here. - await sleep(this._crawler_timeout); + await sleep(this._crawlerTimeout); console.log("EventIndex: Running the crawler loop."); From 56ad164c69ab497efed2949de62b7282fe86da2e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 13 Nov 2019 14:01:07 -0700 Subject: [PATCH 095/334] Add a function to get the "base" theme for a theme Useful for trying to load the right assets first. See https://github.com/vector-im/riot-web/pull/11381 --- src/theme.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/theme.js b/src/theme.js index d479170792..8a15c606d7 100644 --- a/src/theme.js +++ b/src/theme.js @@ -60,6 +60,22 @@ function getCustomTheme(themeName) { return customTheme; } +/** + * Gets the underlying theme name for the given theme. This is usually the theme or + * CSS resource that the theme relies upon to load. + * @param {string} theme The theme name to get the base of. + * @returns {string} The base theme (typically "light" or "dark"). + */ +export function getBaseTheme(theme) { + if (!theme) return "light"; + if (theme.startsWith("custom-")) { + const customTheme = getCustomTheme(theme.substr(7)); + return customTheme.is_dark ? "dark-custom" : "light-custom"; + } + + return theme; // it's probably a base theme +} + /** * Called whenever someone changes the theme * From bc90789c71b5b6d90e445061b1692269c93dbf3c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 14 Nov 2019 00:39:48 +0000 Subject: [PATCH 096/334] Remove unused promise utils method Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/utils/promise.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/utils/promise.js b/src/utils/promise.js index f7a2e7c3e7..8842bfa1b7 100644 --- a/src/utils/promise.js +++ b/src/utils/promise.js @@ -22,19 +22,6 @@ import Promise from "bluebird"; // Returns a promise which resolves with a given value after the given number of ms export const sleep = (ms: number, value: any): Promise => new Promise((resolve => { setTimeout(resolve, ms, value); })); -// Returns a promise which resolves when the input promise resolves with its value -// or when the timeout of ms is reached with the value of given timeoutValue -export async function timeout(promise: Promise, timeoutValue: any, ms: number): Promise { - const timeoutPromise = new Promise((resolve) => { - const timeoutId = setTimeout(resolve, ms, timeoutValue); - promise.then(() => { - clearTimeout(timeoutId); - }); - }); - - return Promise.race([promise, timeoutPromise]); -} - // Returns a Deferred export function defer(): {resolve: () => {}, reject: () => {}, promise: Promise} { let resolve; From 5c24547ef5215b00333da3e9e7b61a55f222f7f6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 14 Nov 2019 09:37:26 +0000 Subject: [PATCH 097/334] re-add and actually use promise timeout util Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/settings/SetIdServer.js | 12 +++++------- src/utils/promise.js | 13 +++++++++++++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 126cdc9557..a7a2e01c22 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -26,6 +26,7 @@ import { getThreepidsWithBindStatus } from '../../../boundThreepids'; import IdentityAuthClient from "../../../IdentityAuthClient"; import {abbreviateUrl, unabbreviateUrl} from "../../../utils/UrlUtils"; import { getDefaultIdentityServerUrl, doesIdentityServerHaveTerms } from '../../../utils/IdentityServerUtils'; +import {timeout} from "../../../utils/promise"; // We'll wait up to this long when checking for 3PID bindings on the IS. const REACHABILITY_TIMEOUT = 10000; // ms @@ -245,14 +246,11 @@ export default class SetIdServer extends React.Component { let threepids = []; let currentServerReachable = true; try { - threepids = await Promise.race([ + threepids = await timeout( getThreepidsWithBindStatus(MatrixClientPeg.get()), - new Promise((resolve, reject) => { - setTimeout(() => { - reject(new Error("Timeout attempting to reach identity server")); - }, REACHABILITY_TIMEOUT); - }), - ]); + Promise.reject(new Error("Timeout attempting to reach identity server")), + REACHABILITY_TIMEOUT, + ); } catch (e) { currentServerReachable = false; console.warn( diff --git a/src/utils/promise.js b/src/utils/promise.js index 8842bfa1b7..f7a2e7c3e7 100644 --- a/src/utils/promise.js +++ b/src/utils/promise.js @@ -22,6 +22,19 @@ import Promise from "bluebird"; // Returns a promise which resolves with a given value after the given number of ms export const sleep = (ms: number, value: any): Promise => new Promise((resolve => { setTimeout(resolve, ms, value); })); +// Returns a promise which resolves when the input promise resolves with its value +// or when the timeout of ms is reached with the value of given timeoutValue +export async function timeout(promise: Promise, timeoutValue: any, ms: number): Promise { + const timeoutPromise = new Promise((resolve) => { + const timeoutId = setTimeout(resolve, ms, timeoutValue); + promise.then(() => { + clearTimeout(timeoutId); + }); + }); + + return Promise.race([promise, timeoutPromise]); +} + // Returns a Deferred export function defer(): {resolve: () => {}, reject: () => {}, promise: Promise} { let resolve; From 28d2e658a4d184d7f51d2423cc0cde5c6ad41986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 14 Nov 2019 14:13:49 +0100 Subject: [PATCH 098/334] EventIndexing: Don't scope the event index per user. --- src/BaseEventIndexManager.js | 5 +---- src/EventIndexPeg.js | 3 +-- src/EventIndexing.js | 4 ++-- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/BaseEventIndexManager.js b/src/BaseEventIndexManager.js index 48a96c4d88..073bdbec81 100644 --- a/src/BaseEventIndexManager.js +++ b/src/BaseEventIndexManager.js @@ -91,13 +91,10 @@ export default class BaseEventIndexManager { /** * Initialize the event index for the given user. * - * @param {string} userId The unique identifier of the logged in user that - * owns the index. - * * @return {Promise} A promise that will resolve when the event index is * initialized. */ - async initEventIndex(userId: string): Promise<> { + async initEventIndex(): Promise<> { throw new Error("Unimplemented"); } diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index 1b380e273f..ff1b2099f2 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -56,10 +56,9 @@ class EventIndexPeg { } const index = new EventIndex(); - const userId = MatrixClientPeg.get().getUserId(); try { - await index.init(userId); + await index.init(); } catch (e) { console.log("EventIndex: Error initializing the event index", e); } diff --git a/src/EventIndexing.js b/src/EventIndexing.js index 67bd894c67..1fc9197082 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -33,10 +33,10 @@ export default class EventIndexer { this.liveEventsForIndex = new Set(); } - async init(userId) { + async init() { const indexManager = PlatformPeg.get().getEventIndexingManager(); if (indexManager === null) return false; - indexManager.initEventIndex(userId); + indexManager.initEventIndex(); return true; } From 54dcaf130255c39abc89de8d67fab06f1d0bf712 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 14 Nov 2019 13:52:17 +0000 Subject: [PATCH 099/334] Replace bluebird specific promise things. Fix uses of sync promise code. Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/GroupAddressPicker.js | 5 +++-- src/autocomplete/Autocompleter.js | 26 +++++++++------------- src/components/structures/GroupView.js | 12 +++++----- src/components/structures/TimelinePanel.js | 13 +++++------ src/rageshake/rageshake.js | 18 ++++++++++----- src/utils/promise.js | 17 ++++++++++++++ 6 files changed, 53 insertions(+), 38 deletions(-) diff --git a/src/GroupAddressPicker.js b/src/GroupAddressPicker.js index 7da37b6df1..793f5c9227 100644 --- a/src/GroupAddressPicker.js +++ b/src/GroupAddressPicker.js @@ -21,6 +21,7 @@ import MultiInviter from './utils/MultiInviter'; import { _t } from './languageHandler'; import MatrixClientPeg from './MatrixClientPeg'; import GroupStore from './stores/GroupStore'; +import {allSettled} from "./utils/promise"; export function showGroupInviteDialog(groupId) { return new Promise((resolve, reject) => { @@ -118,7 +119,7 @@ function _onGroupInviteFinished(groupId, addrs) { function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) { const matrixClient = MatrixClientPeg.get(); const errorList = []; - return Promise.all(addrs.map((addr) => { + return allSettled(addrs.map((addr) => { return GroupStore .addRoomToGroup(groupId, addr.address, addRoomsPublicly) .catch(() => { errorList.push(addr.address); }) @@ -138,7 +139,7 @@ function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) { groups.push(groupId); return MatrixClientPeg.get().sendStateEvent(roomId, 'm.room.related_groups', {groups}, ''); } - }).reflect(); + }); })).then(() => { if (errorList.length === 0) { return; diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index af2744950f..c385e13878 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -27,6 +27,7 @@ import UserProvider from './UserProvider'; import EmojiProvider from './EmojiProvider'; import NotifProvider from './NotifProvider'; import Promise from 'bluebird'; +import {timeout} from "../utils/promise"; export type SelectionRange = { beginning: boolean, // whether the selection is in the first block of the editor or not @@ -77,23 +78,16 @@ export default class Autocompleter { while the user is interacting with the list, which makes it difficult to predict whether an action will actually do what is intended */ - const completionsList = await Promise.all( - // Array of inspections of promises that might timeout. Instead of allowing a - // single timeout to reject the Promise.all, reflect each one and once they've all - // settled, filter for the fulfilled ones - this.providers.map(provider => - provider - .getCompletions(query, selection, force) - .timeout(PROVIDER_COMPLETION_TIMEOUT) - .reflect(), - ), - ); + const completionsList = await Promise.all(this.providers.map(provider => { + return timeout(provider.getCompletions(query, selection, force), null, PROVIDER_COMPLETION_TIMEOUT); + })); + + // map then filter to maintain the index for the map-operation, for this.providers to line up + return completionsList.map((completions, i) => { + if (!completions || !completions.length) return; - return completionsList.filter( - (inspection) => inspection.isFulfilled(), - ).map((completionsState, i) => { return { - completions: completionsState.value(), + completions, provider: this.providers[i], /* the currently matched "command" the completer tried to complete @@ -102,6 +96,6 @@ export default class Autocompleter { */ command: this.providers[i].getCurrentCommand(query, selection, force), }; - }); + }).filter(Boolean); } } diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 4056557a7c..776e7f0d6d 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -38,7 +38,7 @@ import FlairStore from '../../stores/FlairStore'; import { showGroupAddRoomDialog } from '../../GroupAddressPicker'; import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks"; import {Group} from "matrix-js-sdk"; -import {sleep} from "../../utils/promise"; +import {allSettled, sleep} from "../../utils/promise"; const LONG_DESC_PLACEHOLDER = _td( `

    HTML for your community's page

    @@ -99,11 +99,10 @@ const CategoryRoomList = createReactClass({ onFinished: (success, addrs) => { if (!success) return; const errorList = []; - Promise.all(addrs.map((addr) => { + allSettled(addrs.map((addr) => { return GroupStore .addRoomToGroupSummary(this.props.groupId, addr.address) - .catch(() => { errorList.push(addr.address); }) - .reflect(); + .catch(() => { errorList.push(addr.address); }); })).then(() => { if (errorList.length === 0) { return; @@ -276,11 +275,10 @@ const RoleUserList = createReactClass({ onFinished: (success, addrs) => { if (!success) return; const errorList = []; - Promise.all(addrs.map((addr) => { + allSettled(addrs.map((addr) => { return GroupStore .addUserToGroupSummary(addr.address) - .catch(() => { errorList.push(addr.address); }) - .reflect(); + .catch(() => { errorList.push(addr.address); }); })).then(() => { if (errorList.length === 0) { return; diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index faa6f2564a..3dd5ea761e 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -1064,8 +1064,6 @@ const TimelinePanel = createReactClass({ }); }; - let prom = this._timelineWindow.load(eventId, INITIAL_SIZE); - // if we already have the event in question, TimelineWindow.load // returns a resolved promise. // @@ -1074,9 +1072,13 @@ const TimelinePanel = createReactClass({ // quite slow. So we detect that situation and shortcut straight to // calling _reloadEvents and updating the state. - if (prom.isFulfilled()) { + const timeline = this.props.timelineSet.getTimelineForEvent(eventId); + if (timeline) { + // This is a hot-path optimization by skipping a promise tick + // by repeating a no-op sync branch in TimelineSet.getTimelineForEvent & MatrixClient.getEventTimeline onLoaded(); } else { + const prom = this._timelineWindow.load(eventId, INITIAL_SIZE); this.setState({ events: [], liveEvents: [], @@ -1084,11 +1086,8 @@ const TimelinePanel = createReactClass({ canForwardPaginate: false, timelineLoading: true, }); - - prom = prom.then(onLoaded, onError); + prom.then(onLoaded, onError); } - - prom.done(); }, // handle the completion of a timeline load or localEchoUpdate, by diff --git a/src/rageshake/rageshake.js b/src/rageshake/rageshake.js index d61956c925..ee1aed2294 100644 --- a/src/rageshake/rageshake.js +++ b/src/rageshake/rageshake.js @@ -136,6 +136,8 @@ class IndexedDBLogStore { this.id = "instance-" + Math.random() + Date.now(); this.index = 0; this.db = null; + + // these promises are cleared as soon as fulfilled this.flushPromise = null; // set if flush() is called whilst one is ongoing this.flushAgainPromise = null; @@ -208,15 +210,15 @@ class IndexedDBLogStore { */ flush() { // check if a flush() operation is ongoing - if (this.flushPromise && this.flushPromise.isPending()) { - if (this.flushAgainPromise && this.flushAgainPromise.isPending()) { - // this is the 3rd+ time we've called flush() : return the same - // promise. + if (this.flushPromise) { + if (this.flushAgainPromise) { + // this is the 3rd+ time we've called flush() : return the same promise. return this.flushAgainPromise; } - // queue up a flush to occur immediately after the pending one - // completes. + // queue up a flush to occur immediately after the pending one completes. this.flushAgainPromise = this.flushPromise.then(() => { + // clear this.flushAgainPromise + this.flushAgainPromise = null; return this.flush(); }); return this.flushAgainPromise; @@ -232,12 +234,16 @@ class IndexedDBLogStore { } const lines = this.logger.flush(); if (lines.length === 0) { + // clear this.flushPromise + this.flushPromise = null; resolve(); return; } const txn = this.db.transaction(["logs", "logslastmod"], "readwrite"); const objStore = txn.objectStore("logs"); txn.oncomplete = (event) => { + // clear this.flushPromise + this.flushPromise = null; resolve(); }; txn.onerror = (event) => { diff --git a/src/utils/promise.js b/src/utils/promise.js index f7a2e7c3e7..e6e6ccb5c8 100644 --- a/src/utils/promise.js +++ b/src/utils/promise.js @@ -47,3 +47,20 @@ export function defer(): {resolve: () => {}, reject: () => {}, promise: Promise} return {resolve, reject, promise}; } + +// Promise.allSettled polyfill until browser support is stable in Firefox +export function allSettled(promises: Promise[]): {status: string, value?: any, reason?: any}[] { + if (Promise.allSettled) { + return Promise.allSettled(promises); + } + + return Promise.all(promises.map((promise) => { + return promise.then(value => ({ + status: "fulfilled", + value, + })).catch(reason => ({ + status: "rejected", + reason, + })); + })); +} From 41f4f3ef823646bdb82b8de5e0d29f9d35d0b7ee Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 14 Nov 2019 14:04:50 +0000 Subject: [PATCH 100/334] make end-to-end test failure more verbose Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- test/end-to-end-tests/src/usecases/signup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/end-to-end-tests/src/usecases/signup.js b/test/end-to-end-tests/src/usecases/signup.js index 391ce76441..fd2b948572 100644 --- a/test/end-to-end-tests/src/usecases/signup.js +++ b/test/end-to-end-tests/src/usecases/signup.js @@ -61,7 +61,7 @@ module.exports = async function signup(session, username, password, homeserver) await session.query(".mx_Field_valid #mx_RegistrationForm_password"); //check no errors const errorText = await session.tryGetInnertext('.mx_Login_error'); - assert.strictEqual(!!errorText, false); + assert.strictEqual(errorText, null); //submit form //await page.screenshot({path: "beforesubmit.png", fullPage: true}); await registerButton.click(); From b3760cdd6e10b387c4e534c3b8b611870463b7c5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 14 Nov 2019 14:25:54 +0000 Subject: [PATCH 101/334] Replace usages of Promise.delay(...) with own utils Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- test/components/views/dialogs/InteractiveAuthDialog-test.js | 3 ++- test/components/views/rooms/MessageComposerInput-test.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js index b14ea7c242..7612b43b48 100644 --- a/test/components/views/dialogs/InteractiveAuthDialog-test.js +++ b/test/components/views/dialogs/InteractiveAuthDialog-test.js @@ -26,6 +26,7 @@ import sdk from 'matrix-react-sdk'; import MatrixClientPeg from '../../../../src/MatrixClientPeg'; import * as test_utils from '../../../test-utils'; +import {sleep} from "../../../../src/utils/promise"; const InteractiveAuthDialog = sdk.getComponent( 'views.dialogs.InteractiveAuthDialog', @@ -107,7 +108,7 @@ describe('InteractiveAuthDialog', function() { }, })).toBe(true); // let the request complete - return Promise.delay(1); + return sleep(1); }).then(() => { expect(onFinished.callCount).toEqual(1); expect(onFinished.calledWithExactly(true, {a: 1})).toBe(true); diff --git a/test/components/views/rooms/MessageComposerInput-test.js b/test/components/views/rooms/MessageComposerInput-test.js index 1105a4af17..04a5c83ed0 100644 --- a/test/components/views/rooms/MessageComposerInput-test.js +++ b/test/components/views/rooms/MessageComposerInput-test.js @@ -8,6 +8,7 @@ import * as testUtils from '../../../test-utils'; import sdk from 'matrix-react-sdk'; const MessageComposerInput = sdk.getComponent('views.rooms.MessageComposerInput'); import MatrixClientPeg from '../../../../src/MatrixClientPeg'; +import {sleep} from "../../../../src/utils/promise"; function addTextToDraft(text) { const components = document.getElementsByClassName('public-DraftEditor-content'); @@ -49,7 +50,7 @@ xdescribe('MessageComposerInput', () => { // warnings // (please can we make the components not setState() after // they are unmounted?) - Promise.delay(10).done(() => { + sleep(10).done(() => { if (parentDiv) { ReactDOM.unmountComponentAtNode(parentDiv); parentDiv.remove(); From 51a65f388bc3339e7378636c7c75808b63061e3d Mon Sep 17 00:00:00 2001 From: random Date: Thu, 14 Nov 2019 14:15:42 +0000 Subject: [PATCH 102/334] Translated using Weblate (Italian) Currently translated at 99.9% (1896 of 1898 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 | 48 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 6262315012..8c7edbadd8 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -2224,5 +2224,51 @@ "%(count)s unread messages including mentions.|one": "1 citazione non letta.", "%(count)s unread messages.|one": "1 messaggio non letto.", "Unread messages.": "Messaggi non letti.", - "Show tray icon and minimize window to it on close": "Mostra icona in tray e usala alla chiusura della finestra" + "Show tray icon and minimize window to it on close": "Mostra icona in tray e usala alla chiusura della finestra", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Questa azione richiede l'accesso al server di identità predefinito per verificare un indirizzo email o numero di telefono, ma il server non ha termini di servizio.", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "Try out new ways to ignore people (experimental)": "Prova nuovi metodi per ignorare persone (sperimentale)", + "Send verification requests in direct message": "Invia richieste di verifica in un messaggio diretto", + "My Ban List": "Mia lista ban", + "This is your list of users/servers you have blocked - don't leave the room!": "Questa è la lista degli utenti/server che hai bloccato - non lasciare la stanza!", + "Error adding ignored user/server": "Errore di aggiunta utente/server ignorato", + "Something went wrong. Please try again or view your console for hints.": "Qualcosa è andato storto. Riprova o controlla la console per suggerimenti.", + "Error subscribing to list": "Errore di iscrizione alla lista", + "Please verify the room ID or alias and try again.": "Verifica l'ID della stanza o l'alias e riprova.", + "Error removing ignored user/server": "Errore di rimozione utente/server ignorato", + "Error unsubscribing from list": "Errore di disiscrizione dalla lista", + "Please try again or view your console for hints.": "Riprova o controlla la console per suggerimenti.", + "None": "Nessuno", + "Ban list rules - %(roomName)s": "Regole lista banditi - %(roomName)s", + "Server rules": "Regole server", + "User rules": "Regole utente", + "You have not ignored anyone.": "Non hai ignorato nessuno.", + "You are currently ignoring:": "Attualmente stai ignorando:", + "You are not subscribed to any lists": "Non sei iscritto ad alcuna lista", + "Unsubscribe": "Disiscriviti", + "View rules": "Vedi regole", + "You are currently subscribed to:": "Attualmente sei iscritto a:", + "⚠ These settings are meant for advanced users.": "⚠ Queste opzioni sono pensate per utenti esperti.", + "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "Aggiungi qui gli utenti e i server che vuoi ignorare. Usa l'asterisco perchè Riot consideri qualsiasi carattere. Ad esempio, @bot:* ignorerà tutti gli utenti che hanno il nome 'bot' su qualsiasi server.", + "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "Si possono ignorare persone attraverso liste di ban contenenti regole per chi bandire. Iscriversi ad una lista di ban significa che gli utenti/server bloccati da quella lista ti verranno nascosti.", + "Personal ban list": "Lista di ban personale", + "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "La tua lista personale di ban contiene tutti gli utenti/server da cui non vuoi vedere messaggi. Dopo aver ignorato il tuo primo utente/server, apparirà una nuova stanza nel tuo elenco stanze chiamata 'Mia lista ban' - resta in questa stanza per mantenere effettiva la lista ban.", + "Server or user ID to ignore": "Server o ID utente da ignorare", + "eg: @bot:* or example.org": "es: @bot:* o esempio.org", + "Subscribed lists": "Liste sottoscritte", + "Subscribing to a ban list will cause you to join it!": "Iscriversi ad una lista di ban implica di unirsi ad essa!", + "If this isn't what you want, please use a different tool to ignore users.": "Se non è ciò che vuoi, usa uno strumento diverso per ignorare utenti.", + "Room ID or alias of ban list": "ID stanza o alias della lista di ban", + "Subscribe": "Iscriviti", + "Message Actions": "Azioni messaggio", + "You have ignored this user, so their message is hidden. Show anyways.": "Hai ignorato questo utente, perciò il suo messaggio è nascosto. Mostra comunque.", + "You verified %(name)s": "Hai verificato %(name)s", + "You cancelled verifying %(name)s": "Hai annullato la verifica di %(name)s", + "%(name)s cancelled verifying": "%(name)s ha annullato la verifica", + "You accepted": "Hai accettato", + "%(name)s accepted": "%(name)s ha accettato", + "You cancelled": "Hai annullato", + "%(name)s cancelled": "%(name)s ha annullato", + "%(name)s wants to verify": "%(name)s vuole verificare", + "You sent a verification request": "Hai inviato una richiesta di verifica" } From 154fb7ecacc795facc748adf1e9be3a8dd05efde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=ED=83=9C=EC=84=AD?= Date: Thu, 14 Nov 2019 03:08:03 +0000 Subject: [PATCH 103/334] Translated using Weblate (Korean) Currently translated at 99.4% (1887 of 1898 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ko/ --- src/i18n/strings/ko.json | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index 01ef0e8ae1..fe8c929acd 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -2139,5 +2139,24 @@ "Try out new ways to ignore people (experimental)": "새 방식으로 사람들을 무시하기 (실험)", "My Ban List": "차단 목록", "This is your list of users/servers you have blocked - don't leave the room!": "차단한 사용자/서버 목록입니다 - 방을 떠나지 마세요!", - "Ignored/Blocked": "무시됨/차단됨" + "Ignored/Blocked": "무시됨/차단됨", + "Error adding ignored user/server": "무시한 사용자/서버 추가 중 오류", + "Something went wrong. Please try again or view your console for hints.": "무언가 잘못되었습니다. 다시 시도하거나 콘솔을 통해 원인을 알아봐주세요.", + "Error subscribing to list": "목록으로 구독하는 중 오류", + "Please verify the room ID or alias and try again.": "방 ID나 별칭을 확인한 후 다시 시도해주세요.", + "Error removing ignored user/server": "무시한 사용자/서버를 지우는 중 오류", + "Error unsubscribing from list": "목록에서 구독 해제 중 오류", + "Please try again or view your console for hints.": "다시 시도하거나 콘솔을 통해 원인을 알아봐주세요.", + "None": "없음", + "Ban list rules - %(roomName)s": "차단 목록 규칙 - %(roomName)s", + "Server rules": "서버 규칙", + "User rules": "사용자 규칙", + "You have not ignored anyone.": "아무도 무시하고 있지 않습니다.", + "You are currently ignoring:": "현재 무시하고 있음:", + "You are not subscribed to any lists": "어느 목록에도 구독하고 있지 않습니다", + "Unsubscribe": "구독 해제", + "View rules": "규칙 보기", + "You are currently subscribed to:": "현재 구독 중임:", + "⚠ These settings are meant for advanced users.": "⚠ 이 설정은 고급 사용자를 위한 것입니다.", + "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "무시하고 싶은 사용자와 서버를 여기에 추가하세요. 별표(*)를 사용해서 Riot이 이름과 문자를 맞춰볼 수 있습니다. 예를 들어, @bot:*이라면 모든 서버에서 'bot'이라는 문자를 가진 이름의 모든 사용자를 무시합니다." } From bcb7ec081453b15fc2ea7640b5d1ace3e3e5e1f5 Mon Sep 17 00:00:00 2001 From: andriusign Date: Wed, 13 Nov 2019 20:45:07 +0000 Subject: [PATCH 104/334] Translated using Weblate (Lithuanian) Currently translated at 49.2% (934 of 1898 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/lt/ --- src/i18n/strings/lt.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/lt.json b/src/i18n/strings/lt.json index 2aeb207387..5f3a76caa0 100644 --- a/src/i18n/strings/lt.json +++ b/src/i18n/strings/lt.json @@ -1117,5 +1117,6 @@ "Custom user status messages": "Pasirinktinės vartotojo būsenos žinutės", "Group & filter rooms by custom tags (refresh to apply changes)": "Grupuoti ir filtruoti kambarius pagal pasirinktines žymas (atnaujinkite, kad pritaikytumėte pakeitimus)", "Render simple counters in room header": "Užkrauti paprastus skaitiklius kambario antraštėje", - "Multiple integration managers": "Daugialypiai integracijų valdikliai" + "Multiple integration managers": "Daugialypiai integracijų valdikliai", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)" } From 448c9a82908b9e1504ed28a66dd1a68cb9daf9c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 14 Nov 2019 16:01:14 +0100 Subject: [PATCH 105/334] EventIndexPeg: Add a missing return statement. --- src/EventIndexPeg.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index ff1b2099f2..a4ab1815c9 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -61,6 +61,7 @@ class EventIndexPeg { await index.init(); } catch (e) { console.log("EventIndex: Error initializing the event index", e); + return false; } console.log("EventIndex: Successfully initialized the event index"); From 7516f2724aeb34f13ae379f7d5c2124beca1b5bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 14 Nov 2019 16:13:22 +0100 Subject: [PATCH 106/334] EventIndexing: Rework the index initialization and deletion. --- src/BaseEventIndexManager.js | 10 +++++++++ src/EventIndexPeg.js | 43 ++++++++++++++++++++++++------------ src/EventIndexing.js | 40 +++++++++++++++++++-------------- src/Lifecycle.js | 1 + 4 files changed, 63 insertions(+), 31 deletions(-) diff --git a/src/BaseEventIndexManager.js b/src/BaseEventIndexManager.js index 073bdbec81..4e52344e76 100644 --- a/src/BaseEventIndexManager.js +++ b/src/BaseEventIndexManager.js @@ -206,6 +206,16 @@ export default class BaseEventIndexManager { throw new Error("Unimplemented"); } + /** + * close our event index. + * + * @return {Promise} A promise that will resolve once the event index has + * been closed. + */ + async closeEventIndex(): Promise<> { + throw new Error("Unimplemented"); + } + /** * Delete our current event index. * diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index a4ab1815c9..dc25b11cf7 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -31,15 +31,6 @@ class EventIndexPeg { this.index = null; } - /** - * Get the current event index. - * - * @return {EventIndex} The current event index. - */ - get() { - return this.index; - } - /** Create a new EventIndex and initialize it if the platform supports it. * * @return {Promise} A promise that will resolve to true if an @@ -72,11 +63,30 @@ class EventIndexPeg { } /** - * Stop our event indexer. + * Get the current event index. + * + * @return {EventIndex} The current event index. */ + get() { + return this.index; + } + stop() { if (this.index === null) return; - this.index.stop(); + this.index.stopCrawler(); + } + + /** + * Unset our event store + * + * After a call to this the init() method will need to be called again. + * + * @return {Promise} A promise that will resolve once the event index is + * closed. + */ + async unset() { + if (this.index === null) return; + this.index.close(); this.index = null; } @@ -89,9 +99,14 @@ class EventIndexPeg { * deleted. */ async deleteEventIndex() { - if (this.index === null) return; - this.index.deleteEventIndex(); - this.index = null; + const indexManager = PlatformPeg.get().getEventIndexingManager(); + + if (indexManager !== null) { + this.stop(); + console.log("EventIndex: Deleting event index."); + await indexManager.deleteEventIndex(); + this.index = null; + } } } diff --git a/src/EventIndexing.js b/src/EventIndexing.js index 1fc9197082..37167cf600 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -35,9 +35,7 @@ export default class EventIndexer { async init() { const indexManager = PlatformPeg.get().getEventIndexingManager(); - if (indexManager === null) return false; - indexManager.initEventIndex(); - return true; + return indexManager.initEventIndex(); } async onSync(state, prevState, data) { @@ -198,7 +196,6 @@ export default class EventIndexer { console.log("EventIndex: Running the crawler loop."); if (cancelled) { - console.log("EventIndex: Cancelling the crawler."); break; } @@ -373,26 +370,35 @@ export default class EventIndexer { this.crawlerCheckpoints.push(backwardsCheckpoint); } + startCrawler() { + if (this._crawlerRef !== null) return; + + const crawlerHandle = {}; + this.crawlerFunc(crawlerHandle); + this._crawlerRef = crawlerHandle; + } + + stopCrawler() { + if (this._crawlerRef === null) return; + + this._crawlerRef.cancel(); + this._crawlerRef = null; + } + + async close() { + const indexManager = PlatformPeg.get().getEventIndexingManager(); + this.stopCrawler(); + return indexManager.closeEventIndex(); + } + async deleteEventIndex() { const indexManager = PlatformPeg.get().getEventIndexingManager(); if (indexManager !== null) { - console.log("EventIndex: Deleting event index."); - this.crawlerRef.cancel(); + this.stopCrawler(); await indexManager.deleteEventIndex(); } } - startCrawler() { - const crawlerHandle = {}; - this.crawlerFunc(crawlerHandle); - this.crawlerRef = crawlerHandle; - } - - stop() { - this._crawlerRef.cancel(); - this._crawlerRef = null; - } - async search(searchArgs) { const indexManager = PlatformPeg.get().getEventIndexingManager(); return indexManager.searchEventIndex(searchArgs); diff --git a/src/Lifecycle.js b/src/Lifecycle.js index aa900c81a1..1d38934ade 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -662,6 +662,7 @@ export function stopMatrixClient(unsetClient=true) { if (unsetClient) { MatrixClientPeg.unset(); + EventIndexPeg.unset().done(); } } } From d82d4246e92800588c77ed74f3e4f957a554ffbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 14 Nov 2019 16:17:50 +0100 Subject: [PATCH 107/334] BaseEventIndexManager: Remove a return from a docstring. --- src/BaseEventIndexManager.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/BaseEventIndexManager.js b/src/BaseEventIndexManager.js index 4e52344e76..fe59cee673 100644 --- a/src/BaseEventIndexManager.js +++ b/src/BaseEventIndexManager.js @@ -114,9 +114,6 @@ export default class BaseEventIndexManager { /** * Check if our event index is empty. - * - * @return {Promise} A promise that will resolve to true if the - * event index is empty, false otherwise. */ indexIsEmpty(): Promise { throw new Error("Unimplemented"); From eb0b0a400f72d8ada1e9018192eff00c42dcf250 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 14 Nov 2019 16:18:36 +0100 Subject: [PATCH 108/334] EventIndexPeg: Remove the now unused import of MatrixClientPeg. --- src/EventIndexPeg.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index dc25b11cf7..da5c5425e4 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -24,7 +24,6 @@ limitations under the License. import PlatformPeg from "./PlatformPeg"; import EventIndex from "./EventIndexing"; -import MatrixClientPeg from "./MatrixClientPeg"; class EventIndexPeg { constructor() { From 413b90328fe3fc916045c194522cb0ba1721d4a4 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 14 Nov 2019 15:23:04 +0000 Subject: [PATCH 109/334] Show server details on login for unreachable homeserver This fixes the login page to be more helpful when the current homeserver is unreachable: it reveals the server change field, so you have some chance to progress forward. Fixes https://github.com/vector-im/riot-web/issues/11077 --- src/components/structures/auth/Login.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index ad77ed49a5..b35110bf6b 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -386,7 +386,11 @@ module.exports = createReactClass({ ...AutoDiscoveryUtils.authComponentStateForError(e), }); if (this.state.serverErrorIsFatal) { - return; // Server is dead - do not continue. + // Server is dead: show server details prompt instead + this.setState({ + phase: PHASE_SERVER_DETAILS, + }); + return; } } From 84f78ae7269400eba84ab42c37e8815c0db23fb6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 14 Nov 2019 16:05:09 +0000 Subject: [PATCH 110/334] Revert ripping bluebird out of rageshake.js for time being Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/rageshake/rageshake.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/rageshake/rageshake.js b/src/rageshake/rageshake.js index ee1aed2294..d61956c925 100644 --- a/src/rageshake/rageshake.js +++ b/src/rageshake/rageshake.js @@ -136,8 +136,6 @@ class IndexedDBLogStore { this.id = "instance-" + Math.random() + Date.now(); this.index = 0; this.db = null; - - // these promises are cleared as soon as fulfilled this.flushPromise = null; // set if flush() is called whilst one is ongoing this.flushAgainPromise = null; @@ -210,15 +208,15 @@ class IndexedDBLogStore { */ flush() { // check if a flush() operation is ongoing - if (this.flushPromise) { - if (this.flushAgainPromise) { - // this is the 3rd+ time we've called flush() : return the same promise. + if (this.flushPromise && this.flushPromise.isPending()) { + if (this.flushAgainPromise && this.flushAgainPromise.isPending()) { + // this is the 3rd+ time we've called flush() : return the same + // promise. return this.flushAgainPromise; } - // queue up a flush to occur immediately after the pending one completes. + // queue up a flush to occur immediately after the pending one + // completes. this.flushAgainPromise = this.flushPromise.then(() => { - // clear this.flushAgainPromise - this.flushAgainPromise = null; return this.flush(); }); return this.flushAgainPromise; @@ -234,16 +232,12 @@ class IndexedDBLogStore { } const lines = this.logger.flush(); if (lines.length === 0) { - // clear this.flushPromise - this.flushPromise = null; resolve(); return; } const txn = this.db.transaction(["logs", "logslastmod"], "readwrite"); const objStore = txn.objectStore("logs"); txn.oncomplete = (event) => { - // clear this.flushPromise - this.flushPromise = null; resolve(); }; txn.onerror = (event) => { From b05dabe2b7746975ba285d6e52b7594e27a4b8b6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 14 Nov 2019 12:02:16 -0700 Subject: [PATCH 111/334] Add better error handling to Synapse user deactivation Also clearly flag it as a Synapse user deactivation in the analytics, so we don't get confused. Fixes https://github.com/vector-im/riot-web/issues/10986 --- src/components/views/right_panel/UserInfo.js | 7 +++++-- src/components/views/rooms/MemberInfo.js | 11 ++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 207bf29998..7a88c80ce5 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -842,10 +842,13 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room const [accepted] = await finished; if (!accepted) return; try { - cli.deactivateSynapseUser(user.userId); + await cli.deactivateSynapseUser(user.userId); } catch (err) { + console.error("Failed to deactivate user"); + console.error(err); + const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); - Modal.createTrackedDialog('Failed to deactivate user', '', ErrorDialog, { + Modal.createTrackedDialog('Failed to deactivate Synapse user', '', ErrorDialog, { title: _t('Failed to deactivate user'), description: ((err && err.message) ? err.message : _t("Operation failed")), }); diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 2ea6392e96..9364f2f49d 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -550,7 +550,16 @@ module.exports = createReactClass({ danger: true, onFinished: (accepted) => { if (!accepted) return; - this.context.matrixClient.deactivateSynapseUser(this.props.member.userId); + this.context.matrixClient.deactivateSynapseUser(this.props.member.userId).catch(e => { + console.error("Failed to deactivate user"); + console.error(e); + + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to deactivate Synapse user', '', ErrorDialog, { + title: _t('Failed to deactivate user'), + description: ((e && e.message) ? e.message : _t("Operation failed")), + }); + }); }, }); }, From 0f2f500a16078b23e818340f4bc2491e2d0e3d56 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 14 Nov 2019 12:08:04 -0700 Subject: [PATCH 112/334] i18n update --- 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 dc9773ad21..8f1344d5c9 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -867,6 +867,7 @@ "Deactivate user?": "Deactivate user?", "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?": "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?", "Deactivate user": "Deactivate user", + "Failed to deactivate user": "Failed to deactivate user", "Failed to change power level": "Failed to change power level", "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.", "Are you sure?": "Are you sure?", @@ -1073,7 +1074,6 @@ "Remove this user from community?": "Remove this user from community?", "Failed to withdraw invitation": "Failed to withdraw invitation", "Failed to remove user from community": "Failed to remove user from community", - "Failed to deactivate user": "Failed to deactivate user", "This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.", "Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.", "Sunday": "Sunday", From af4ad488bdf753edb514e13b88957a2ef103dcda Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 11 Nov 2019 16:54:12 +0100 Subject: [PATCH 113/334] Restyle Avatar Make it a circle with the profile picture centered, with a max height/width of 30vh --- res/css/views/right_panel/_UserInfo.scss | 27 +++++++++++++++----- src/components/views/right_panel/UserInfo.js | 18 +++++++++++-- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index df536a7388..db08fe18bf 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -38,6 +38,7 @@ limitations under the License. mask-repeat: no-repeat; mask-position: 16px center; background-color: $rightpanel-button-color; + position: absolute; } .mx_UserInfo_profile h2 { @@ -47,7 +48,7 @@ limitations under the License. } .mx_UserInfo h2 { - font-size: 16px; + font-size: 18px; font-weight: 600; margin: 16px 0 8px 0; } @@ -74,15 +75,27 @@ limitations under the License. } .mx_UserInfo_avatar { - background: $tagpanel-bg-color; + margin: 24px 32px 0 32px; } -.mx_UserInfo_avatar > img { - height: auto; - width: 100%; +.mx_UserInfo_avatar > div { + max-width: 30vh; + margin: 0 auto; +} + +.mx_UserInfo_avatar > div > div { + /* use padding-top instead of height to make this element square, + as the % in padding is a % of the width (including margin, + that's why we had to put the margin to center on a parent div), + and not a % of the parent height. */ + padding-top: 100%; + height: 0; + border-radius: 100%; max-height: 30vh; - object-fit: contain; - display: block; + box-sizing: content-box; + background-repeat: no-repeat; + background-size: cover; + background-position: center; } .mx_UserInfo_avatar .mx_BaseAvatar.mx_BaseAvatar_image { diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 207bf29998..e55aae74f5 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -40,6 +40,7 @@ import MatrixClientPeg from "../../../MatrixClientPeg"; import E2EIcon from "../rooms/E2EIcon"; import withLegacyMatrixClient from "../../../utils/withLegacyMatrixClient"; import {useEventEmitter} from "../../../hooks/useEventEmitter"; +import {ContentRepo} from 'matrix-js-sdk'; const _disambiguateDevices = (devices) => { const names = Object.create(null); @@ -917,6 +918,12 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); }, [user.roomId, user.userId, room && room.currentState, cli]); // eslint-disable-line + const onMemberAvatarKey = e => { + if (e.key === "Enter") { + onMemberAvatarClick(); + } + }; + const onMemberAvatarClick = useCallback(() => { const member = user; const avatarUrl = member.getMxcAvatarUrl(); @@ -1045,8 +1052,15 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room let avatarElement; if (avatarUrl) { const httpUrl = cli.mxcUrlToHttp(avatarUrl, 800, 800); - avatarElement =
    - {_t("Profile + avatarElement =
    +
    ; } From f4988392f9cdec443ddd01c09bfb931f1beead7f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 12 Nov 2019 16:25:38 +0100 Subject: [PATCH 114/334] restyle e2e icons --- res/css/views/rooms/_E2EIcon.scss | 53 +++++++++++++++++++++++---- res/img/e2e/verified.svg | 13 ++++++- res/img/e2e/warning.svg | 16 +++++--- src/components/views/rooms/E2EIcon.js | 8 +++- 4 files changed, 75 insertions(+), 15 deletions(-) diff --git a/res/css/views/rooms/_E2EIcon.scss b/res/css/views/rooms/_E2EIcon.scss index 84a16611de..c609d70f4c 100644 --- a/res/css/views/rooms/_E2EIcon.scss +++ b/res/css/views/rooms/_E2EIcon.scss @@ -17,17 +17,56 @@ limitations under the License. .mx_E2EIcon { width: 25px; height: 25px; - mask-repeat: no-repeat; - mask-position: center 0; margin: 0 9px; + position: relative; + display: block; } -.mx_E2EIcon_verified { - mask-image: url('$(res)/img/e2e/lock-verified.svg'); - background-color: $accent-color; +.mx_E2EIcon_verified::before, .mx_E2EIcon_warning::before { + content: ""; + display: block; + /* the symbols in the shield icons are cut out the make the themeable with css masking. + if they appear on a different background than white, the symbol wouldn't be white though, so we + add a rectangle here below the masked element to shine through the symbol cutout. + hardcoding white and not using a theme variable as this would probably be white for any theme. */ + background-color: white; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; } -.mx_E2EIcon_warning { - mask-image: url('$(res)/img/e2e/lock-warning.svg'); +.mx_E2EIcon_verified::after, .mx_E2EIcon_warning::after { + content: ""; + display: block; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + mask-repeat: no-repeat; + mask-size: contain; +} + +.mx_E2EIcon_verified::before { + /* white rectangle below checkmark of shield */ + margin: 25% 28% 38% 25%; +} + + +.mx_E2EIcon_verified::after { + mask-image: url('$(res)/img/e2e/verified.svg'); + background-color: $warning-color; +} + + +.mx_E2EIcon_warning::before { + /* white rectangle below "!" of shield */ + margin: 18% 40% 25% 40%; +} + +.mx_E2EIcon_warning::after { + mask-image: url('$(res)/img/e2e/warning.svg'); background-color: $warning-color; } diff --git a/res/img/e2e/verified.svg b/res/img/e2e/verified.svg index 459a552a40..af6bb92297 100644 --- a/res/img/e2e/verified.svg +++ b/res/img/e2e/verified.svg @@ -1,3 +1,12 @@ - - + + + diff --git a/res/img/e2e/warning.svg b/res/img/e2e/warning.svg index 3d5fba550c..2501da6ab3 100644 --- a/res/img/e2e/warning.svg +++ b/res/img/e2e/warning.svg @@ -1,6 +1,12 @@ - - - - - + + + diff --git a/src/components/views/rooms/E2EIcon.js b/src/components/views/rooms/E2EIcon.js index 54260e4ee2..d6baa30c8e 100644 --- a/src/components/views/rooms/E2EIcon.js +++ b/src/components/views/rooms/E2EIcon.js @@ -36,7 +36,13 @@ export default function(props) { _t("All devices for this user are trusted") : _t("All devices in this encrypted room are trusted"); } - const icon = (
    ); + + let style = null; + if (props.size) { + style = {width: `${props.size}px`, height: `${props.size}px`}; + } + + const icon = (
    ); if (props.onClick) { return ({ icon }); } else { From 3e356756aae08459f22e71e25e33fcbc50d880b7 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 12 Nov 2019 16:26:26 +0100 Subject: [PATCH 115/334] style profile info --- res/css/views/right_panel/_UserInfo.scss | 40 +++++++++++--------- src/components/views/right_panel/UserInfo.js | 12 +++--- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index db08fe18bf..aee0252c4e 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -22,13 +22,6 @@ limitations under the License. overflow-y: auto; } -.mx_UserInfo_profile .mx_E2EIcon { - display: inline; - margin: auto; - padding-right: 25px; - mask-size: contain; -} - .mx_UserInfo_cancel { height: 16px; width: 16px; @@ -41,16 +34,10 @@ limitations under the License. position: absolute; } -.mx_UserInfo_profile h2 { - flex: 1; - overflow-x: auto; - max-height: 50px; -} - .mx_UserInfo h2 { font-size: 18px; font-weight: 600; - margin: 16px 0 8px 0; + margin: 0; } .mx_UserInfo_container { @@ -76,6 +63,7 @@ limitations under the License. .mx_UserInfo_avatar { margin: 24px 32px 0 32px; + cursor: pointer; } .mx_UserInfo_avatar > div { @@ -110,12 +98,30 @@ limitations under the License. margin: 4px 0; } -.mx_UserInfo_profileField { - font-size: 15px; - position: relative; +.mx_UserInfo_profile { + font-size: 12px; text-align: center; + + h2 { + flex: 1; + overflow-x: auto; + max-height: 50px; + display: flex; + justify-self: ; + justify-content: center; + align-items: center; + + .mx_E2EIcon { + margin: 5px; + } + } + + .mx_UserInfo_profileStatus { + margin-top: 12px; + } } + .mx_UserInfo_memberDetails { text-align: center; } diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index e55aae74f5..43c5833faa 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -1157,7 +1157,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room let e2eIcon; if (isRoomEncrypted && devices) { - e2eIcon = ; + e2eIcon = ; } return ( @@ -1167,16 +1167,14 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
    -
    -

    +
    +

    { e2eIcon } { displayName }

    -
    - { user.userId } -
    -
    +
    { user.userId }
    +
    {presenceLabel} {statusLabel}
    From b475bc9e912f0764f6a759e2434ea806fedf1e5b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 12 Nov 2019 16:40:07 +0100 Subject: [PATCH 116/334] Add direct message button While we don't have canonical DMs yet, it takes you to the most recently active DM room --- src/components/views/right_panel/UserInfo.js | 30 ++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 43c5833faa..0c058a8859 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -186,6 +186,26 @@ const DirectChatsSection = withLegacyMatrixClient(({matrixClient: cli, userId, s ); }); +function openDMForUser(cli, userId) { + const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId); + const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => { + const room = cli.getRoom(roomId); + if (!lastActiveRoom || (room && lastActiveRoom.getLastActiveTimestamp() < room.getLastActiveTimestamp())) { + return room; + } + return lastActiveRoom; + }, null); + + if (lastActiveRoom) { + dis.dispatch({ + action: 'view_room', + room_id: lastActiveRoom.roomId, + }); + } else { + createRoom({dmUserId: userId}); + } +} + const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, isIgnored, canInvite}) => { let ignoreButton = null; let insertPillButton = null; @@ -286,10 +306,20 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i ); + let directMessageButton; + if (!isMe) { + directMessageButton = ( + openDMForUser(cli, member.userId)} className="mx_UserInfo_field"> + { _t('Direct message') } + + ); + } + return (

    { _t("User Options") }

    + { directMessageButton } { readReceiptButton } { shareUserButton } { insertPillButton } From 0a2255ce7303d0fbdcfefb6f7a107b168c0c533a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 12 Nov 2019 17:32:34 +0100 Subject: [PATCH 117/334] fixup: bring back margin above display name --- res/css/views/right_panel/_UserInfo.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index aee0252c4e..07b8ed2879 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -37,7 +37,7 @@ limitations under the License. .mx_UserInfo h2 { font-size: 18px; font-weight: 600; - margin: 0; + margin: 18px 0 0 0; } .mx_UserInfo_container { From 8dd7d8e5c0fdb9f4166a85a15e8bf3a0e2b62ab8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 12 Nov 2019 17:33:24 +0100 Subject: [PATCH 118/334] fixup: don't consider left DM rooms --- src/components/views/right_panel/UserInfo.js | 7 +++++-- src/i18n/strings/en_EN.json | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 0c058a8859..c008cfe1f0 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -190,7 +190,10 @@ function openDMForUser(cli, userId) { const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId); const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => { const room = cli.getRoom(roomId); - if (!lastActiveRoom || (room && lastActiveRoom.getLastActiveTimestamp() < room.getLastActiveTimestamp())) { + if (!room || room.getMyMembership() === "leave") { + return lastActiveRoom; + } + if (!lastActiveRoom || lastActiveRoom.getLastActiveTimestamp() < room.getLastActiveTimestamp()) { return room; } return lastActiveRoom; @@ -317,7 +320,7 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i return (
    -

    { _t("User Options") }

    +

    { _t("Options") }

    { directMessageButton } { readReceiptButton } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index dc9773ad21..4b86c399a4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1068,6 +1068,8 @@ "Files": "Files", "Trust & Devices": "Trust & Devices", "Direct messages": "Direct messages", + "Direct message": "Direct message", + "Options": "Options", "Remove from community": "Remove from community", "Disinvite this user from community?": "Disinvite this user from community?", "Remove this user from community?": "Remove this user from community?", @@ -1091,7 +1093,6 @@ "Reply": "Reply", "Edit": "Edit", "Message Actions": "Message Actions", - "Options": "Options", "Attachment": "Attachment", "Error decrypting attachment": "Error decrypting attachment", "Decrypt %(text)s": "Decrypt %(text)s", From 238555f4ec20ed6fa60be311e18d52cf0d447a4e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 12 Nov 2019 17:34:35 +0100 Subject: [PATCH 119/334] fixup: isMe --- src/components/views/right_panel/UserInfo.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index c008cfe1f0..1964b5601c 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -215,6 +215,10 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i let inviteUserButton = null; let readReceiptButton = null; + const isMe = member.userId === cli.getUserId(); + + + const onShareUserClick = () => { const ShareDialog = sdk.getComponent("dialogs.ShareDialog"); Modal.createTrackedDialog('share room member dialog', '', ShareDialog, { @@ -224,7 +228,7 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i // Only allow the user to ignore the user if its not ourselves // same goes for jumping to read receipt - if (member.userId !== cli.getUserId()) { + if (!isMe) { const onIgnoreToggle = () => { const ignoredUsers = cli.getIgnoredUsers(); if (isIgnored) { From bd2bf4500adf8d753b857b210c15e849faf8b682 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 12 Nov 2019 17:35:38 +0100 Subject: [PATCH 120/334] remove direct message list from UserInfo --- src/components/views/right_panel/UserInfo.js | 107 ------------------- 1 file changed, 107 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 1964b5601c..412bf92831 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -89,103 +89,6 @@ const DevicesSection = ({devices, userId, loading}) => { ); }; -const onRoomTileClick = (roomId) => { - dis.dispatch({ - action: 'view_room', - room_id: roomId, - }); -}; - -const DirectChatsSection = withLegacyMatrixClient(({matrixClient: cli, userId, startUpdating, stopUpdating}) => { - const onNewDMClick = async () => { - startUpdating(); - await createRoom({dmUserId: userId}); - stopUpdating(); - }; - - // TODO: Immutable DMs replaces a lot of this - // dmRooms will not include dmRooms that we have been invited into but did not join. - // Because DMRoomMap runs off account_data[m.direct] which is only set on join of dm room. - // XXX: we potentially want DMs we have been invited to, to also show up here :L - // especially as logic below concerns specially if we haven't joined but have been invited - const [dmRooms, setDmRooms] = useState(new DMRoomMap(cli).getDMRoomsForUserId(userId)); - - // TODO bind the below - // cli.on("Room", this.onRoom); - // cli.on("Room.name", this.onRoomName); - // cli.on("deleteRoom", this.onDeleteRoom); - - const accountDataHandler = useCallback((ev) => { - if (ev.getType() === "m.direct") { - const dmRoomMap = new DMRoomMap(cli); - setDmRooms(dmRoomMap.getDMRoomsForUserId(userId)); - } - }, [cli, userId]); - useEventEmitter(cli, "accountData", accountDataHandler); - - const RoomTile = sdk.getComponent("rooms.RoomTile"); - - const tiles = []; - for (const roomId of dmRooms) { - const room = cli.getRoom(roomId); - if (room) { - const myMembership = room.getMyMembership(); - // not a DM room if we have are not joined - if (myMembership !== 'join') continue; - - const them = room.getMember(userId); - // not a DM room if they are not joined - if (!them || !them.membership || them.membership !== 'join') continue; - - const highlight = room.getUnreadNotificationCount('highlight') > 0; - - tiles.push( - , - ); - } - } - - const labelClasses = classNames({ - mx_UserInfo_createRoom_label: true, - mx_RoomTile_name: true, - }); - - let body = tiles; - if (!body) { - body = ( - -
    - {_t("Start -
    -
    { _t("Start a chat") }
    -
    - ); - } - - return ( -
    -
    -

    { _t("Direct messages") }

    - -
    - { body } -
    - ); -}); - function openDMForUser(cli, userId) { const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId); const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => { @@ -217,8 +120,6 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i const isMe = member.userId === cli.getUserId(); - - const onShareUserClick = () => { const ShareDialog = sdk.getComponent("dialogs.ShareDialog"); Modal.createTrackedDialog('share room member dialog', '', ShareDialog, { @@ -979,11 +880,6 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room let synapseDeactivateButton; let spinner; - let directChatsSection; - if (user.userId !== cli.getUserId()) { - directChatsSection = ; - } - // We don't need a perfect check here, just something to pass as "probably not our homeserver". If // someone does figure out how to bypass this check the worst that happens is an error. // FIXME this should be using cli instead of MatrixClientPeg.matrixClient @@ -1226,9 +1122,6 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room { devicesSection } - - { directChatsSection } - Date: Wed, 13 Nov 2019 12:09:20 +0100 Subject: [PATCH 121/334] update when room encryption is turned on also don't download devices as long as room is not encrypted --- src/components/views/right_panel/UserInfo.js | 31 +++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 412bf92831..28b5af358a 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -64,6 +64,18 @@ const _getE2EStatus = (devices) => { return hasUnverifiedDevice ? "warning" : "verified"; }; +function useIsEncrypted(cli, room) { + const [isEncrypted, setIsEncrypted] = useState(cli.isRoomEncrypted(room.roomId)); + + const update = useCallback((event) => { + if (event.getType() === "m.room.encryption") { + setIsEncrypted(cli.isRoomEncrypted(room.roomId)); + } + }, [cli, room]); + useEventEmitter(room.currentState, "RoomState.events", update); + return isEncrypted; +} + const DevicesSection = ({devices, userId, loading}) => { const MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo'); const Spinner = sdk.getComponent("elements.Spinner"); @@ -1005,6 +1017,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room title={_t('Close')} />; } + const isRoomEncrypted = useIsEncrypted(cli, room); // undefined means yet to be loaded, null means failed to load, otherwise list of devices const [devices, setDevices] = useState(undefined); // Download device lists @@ -1029,14 +1042,15 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room setDevices(null); } } - - _downloadDeviceList(); + if (isRoomEncrypted) { + _downloadDeviceList(); + } // Handle being unmounted return () => { cancelled = true; }; - }, [cli, user.userId]); + }, [cli, user.userId, isRoomEncrypted]); // Listen to changes useEffect(() => { @@ -1053,16 +1067,19 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room } }; - cli.on("deviceVerificationChanged", onDeviceVerificationChanged); + if (isRoomEncrypted) { + cli.on("deviceVerificationChanged", onDeviceVerificationChanged); + } // Handle being unmounted return () => { cancel = true; - cli.removeListener("deviceVerificationChanged", onDeviceVerificationChanged); + if (isRoomEncrypted) { + cli.removeListener("deviceVerificationChanged", onDeviceVerificationChanged); + } }; - }, [cli, user.userId]); + }, [cli, user.userId, isRoomEncrypted]); let devicesSection; - const isRoomEncrypted = _enableDevices && room && cli.isRoomEncrypted(room.roomId); if (isRoomEncrypted) { devicesSection = ; } else { From e32a948d5d15a44b6dd2cb6a1615a69e8fa8fd8e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 12:10:30 +0100 Subject: [PATCH 122/334] add "unverify user" action to user info --- src/components/views/right_panel/UserInfo.js | 23 +++++++++++++++++++- src/i18n/strings/en_EN.json | 1 + 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 28b5af358a..c61746293e 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -64,6 +64,17 @@ const _getE2EStatus = (devices) => { return hasUnverifiedDevice ? "warning" : "verified"; }; +async function unverifyUser(matrixClient, userId) { + const devices = await matrixClient.getStoredDevicesForUser(userId); + for (const device of devices) { + if (device.isVerified()) { + matrixClient.setDeviceVerified( + userId, device.deviceId, false, + ); + } + } +} + function useIsEncrypted(cli, room) { const [isEncrypted, setIsEncrypted] = useState(cli.isRoomEncrypted(room.roomId)); @@ -124,7 +135,7 @@ function openDMForUser(cli, userId) { } } -const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, isIgnored, canInvite}) => { +const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, isIgnored, canInvite, devices}) => { let ignoreButton = null; let insertPillButton = null; let inviteUserButton = null; @@ -234,6 +245,14 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i ); } + let unverifyButton; + if (devices && devices.some(device => device.isVerified())) { + unverifyButton = ( + unverifyUser(cli, member.userId)} className="mx_UserInfo_field"> + { _t('Unverify user') } + + ); + } return (
    @@ -245,6 +264,7 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i { insertPillButton } { ignoreButton } { inviteUserButton } + { unverifyButton }
    ); @@ -1140,6 +1160,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room { devicesSection } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4b86c399a4..fcf43af31f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1069,6 +1069,7 @@ "Trust & Devices": "Trust & Devices", "Direct messages": "Direct messages", "Direct message": "Direct message", + "Unverify user": "Unverify user", "Options": "Options", "Remove from community": "Remove from community", "Disinvite this user from community?": "Disinvite this user from community?", From 4a1dc5567341c478fefd275010dabf81329f3efb Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 12:11:06 +0100 Subject: [PATCH 123/334] fixup: rearrange openDMForUser --- src/components/views/right_panel/UserInfo.js | 46 ++++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index c61746293e..e7277a52e2 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -75,6 +75,29 @@ async function unverifyUser(matrixClient, userId) { } } +function openDMForUser(matrixClient, userId) { + const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId); + const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => { + const room = matrixClient.getRoom(roomId); + if (!room || room.getMyMembership() === "leave") { + return lastActiveRoom; + } + if (!lastActiveRoom || lastActiveRoom.getLastActiveTimestamp() < room.getLastActiveTimestamp()) { + return room; + } + return lastActiveRoom; + }, null); + + if (lastActiveRoom) { + dis.dispatch({ + action: 'view_room', + room_id: lastActiveRoom.roomId, + }); + } else { + createRoom({dmUserId: userId}); + } +} + function useIsEncrypted(cli, room) { const [isEncrypted, setIsEncrypted] = useState(cli.isRoomEncrypted(room.roomId)); @@ -112,29 +135,6 @@ const DevicesSection = ({devices, userId, loading}) => { ); }; -function openDMForUser(cli, userId) { - const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId); - const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => { - const room = cli.getRoom(roomId); - if (!room || room.getMyMembership() === "leave") { - return lastActiveRoom; - } - if (!lastActiveRoom || lastActiveRoom.getLastActiveTimestamp() < room.getLastActiveTimestamp()) { - return room; - } - return lastActiveRoom; - }, null); - - if (lastActiveRoom) { - dis.dispatch({ - action: 'view_room', - room_id: lastActiveRoom.roomId, - }); - } else { - createRoom({dmUserId: userId}); - } -} - const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, isIgnored, canInvite, devices}) => { let ignoreButton = null; let insertPillButton = null; From 6afeeddb36cba22823b815d37a20fb6de158ca27 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 15:59:49 +0100 Subject: [PATCH 124/334] hide verified devices by default with expand button --- src/components/views/right_panel/UserInfo.js | 66 +++++++++++++------- src/i18n/strings/en_EN.json | 6 +- 2 files changed, 47 insertions(+), 25 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index e7277a52e2..1e59ec2c44 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -114,6 +114,8 @@ const DevicesSection = ({devices, userId, loading}) => { const MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo'); const Spinner = sdk.getComponent("elements.Spinner"); + const [isExpanded, setExpanded] = useState(false); + if (loading) { // still loading return ; @@ -121,16 +123,37 @@ const DevicesSection = ({devices, userId, loading}) => { if (devices === null) { return _t("Unable to load device list"); } - if (devices.length === 0) { - return _t("No devices with registered encryption keys"); + + const unverifiedDevices = devices.filter(d => !d.isVerified()); + const verifiedDevices = devices.filter(d => d.isVerified()); + + let expandButton; + if (verifiedDevices.length) { + if (isExpanded) { + expandButton = ( setExpanded(false)}> + {_t("Hide verified Sign-In's")} + ); + } else { + expandButton = ( setExpanded(true)}> + {_t("%(count)s verified Sign-In's", {count: verifiedDevices.length})} + ); + } + } + + let deviceList = unverifiedDevices.map((device, i) => { + return (); + }); + if (isExpanded) { + const keyStart = unverifiedDevices.length; + deviceList = deviceList.concat(verifiedDevices.map((device, i) => { + return (); + })); } return ( -
    -

    { _t("Trust & Devices") }

    -
    - { devices.map((device, i) => ) } -
    +
    +
    {deviceList}
    +
    {expandButton}
    ); }; @@ -1099,12 +1122,8 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room }; }, [cli, user.userId, isRoomEncrypted]); - let devicesSection; - if (isRoomEncrypted) { - devicesSection = ; - } else { - let text; - + let text; + if (!isRoomEncrypted) { if (!_enableDevices) { text = _t("This client does not support end-to-end encryption."); } else if (room) { @@ -1112,19 +1131,18 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room } else { // TODO what to render for GroupMember } - - if (text) { - devicesSection = ( -
    -

    { _t("Trust & Devices") }

    -
    - { text } -
    -
    - ); - } + } else { + text = _t("Messages in this room are end-to-end encrypted."); } + const devicesSection = ( +
    +

    { _t("Security") }

    +

    { text }

    + +
    + ); + let e2eIcon; if (isRoomEncrypted && devices) { e2eIcon = ; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index fcf43af31f..9322e71b19 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1066,8 +1066,10 @@ "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.", "Members": "Members", "Files": "Files", - "Trust & Devices": "Trust & Devices", "Direct messages": "Direct messages", + "Hide verified Sign-In's": "Hide verified Sign-In's", + "%(count)s verified Sign-In's|one": "1 verified Sign-In", + "%(count)s verified Sign-In's|other": "%(count)s verified Sign-In's", "Direct message": "Direct message", "Unverify user": "Unverify user", "Options": "Options", @@ -1079,6 +1081,8 @@ "Failed to deactivate user": "Failed to deactivate user", "This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.", "Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.", + "Messages in this room are end-to-end encrypted.": "Messages in this room are end-to-end encrypted.", + "Security": "Security", "Sunday": "Sunday", "Monday": "Monday", "Tuesday": "Tuesday", From 04731d0ae331420123683ba6cbaa366ffd99c44b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 16:00:13 +0100 Subject: [PATCH 125/334] RoomState.events fired on RoomState object, not room --- src/components/views/right_panel/UserInfo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 1e59ec2c44..c1a6442409 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -346,7 +346,7 @@ const useRoomPowerLevels = (room) => { }; }, [room]); - useEventEmitter(room, "RoomState.events", update); + useEventEmitter(room.currentState, "RoomState.events", update); useEffect(() => { update(); return () => { From 73b6575082820216f0d62ff70d634224a3aa6ae2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 17:56:04 +0100 Subject: [PATCH 126/334] fixup: verified shield should be green --- res/css/views/rooms/_E2EIcon.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/rooms/_E2EIcon.scss b/res/css/views/rooms/_E2EIcon.scss index c609d70f4c..c8b1be47f9 100644 --- a/res/css/views/rooms/_E2EIcon.scss +++ b/res/css/views/rooms/_E2EIcon.scss @@ -57,7 +57,7 @@ limitations under the License. .mx_E2EIcon_verified::after { mask-image: url('$(res)/img/e2e/verified.svg'); - background-color: $warning-color; + background-color: $accent-color; } From 0bd1e7112df59a3dabde0946c4926347d2c6779f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 17:59:22 +0100 Subject: [PATCH 127/334] style security section as per design --- res/css/views/right_panel/_UserInfo.scss | 325 ++++++++++--------- src/components/views/right_panel/UserInfo.js | 53 ++- src/i18n/strings/en_EN.json | 5 +- 3 files changed, 213 insertions(+), 170 deletions(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index 07b8ed2879..79211bb38a 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -20,175 +20,182 @@ limitations under the License. flex-direction: column; flex: 1; overflow-y: auto; -} - -.mx_UserInfo_cancel { - height: 16px; - width: 16px; - padding: 10px 0 10px 10px; - cursor: pointer; - mask-image: url('$(res)/img/minimise.svg'); - mask-repeat: no-repeat; - mask-position: 16px center; - background-color: $rightpanel-button-color; - position: absolute; -} - -.mx_UserInfo h2 { - font-size: 18px; - font-weight: 600; - margin: 18px 0 0 0; -} - -.mx_UserInfo_container { - padding: 0 16px 16px 16px; - border-bottom: 1px solid lightgray; -} - -.mx_UserInfo_memberDetailsContainer { - padding-bottom: 0; -} - -.mx_UserInfo .mx_RoomTile_nameContainer { - width: 154px; -} - -.mx_UserInfo .mx_RoomTile_badge { - display: none; -} - -.mx_UserInfo .mx_RoomTile_name { - width: 160px; -} - -.mx_UserInfo_avatar { - margin: 24px 32px 0 32px; - cursor: pointer; -} - -.mx_UserInfo_avatar > div { - max-width: 30vh; - margin: 0 auto; -} - -.mx_UserInfo_avatar > div > div { - /* use padding-top instead of height to make this element square, - as the % in padding is a % of the width (including margin, - that's why we had to put the margin to center on a parent div), - and not a % of the parent height. */ - padding-top: 100%; - height: 0; - border-radius: 100%; - max-height: 30vh; - box-sizing: content-box; - background-repeat: no-repeat; - background-size: cover; - background-position: center; -} - -.mx_UserInfo_avatar .mx_BaseAvatar.mx_BaseAvatar_image { - cursor: zoom-in; -} - -.mx_UserInfo h3 { - text-transform: uppercase; - color: $input-darker-fg-color; - font-weight: bold; font-size: 12px; - margin: 4px 0; -} -.mx_UserInfo_profile { - font-size: 12px; - text-align: center; + .mx_UserInfo_cancel { + height: 16px; + width: 16px; + padding: 10px 0 10px 10px; + cursor: pointer; + mask-image: url('$(res)/img/minimise.svg'); + mask-repeat: no-repeat; + mask-position: 16px center; + background-color: $rightpanel-button-color; + position: absolute; + } h2 { - flex: 1; - overflow-x: auto; - max-height: 50px; - display: flex; - justify-self: ; - justify-content: center; - align-items: center; + font-size: 18px; + font-weight: 600; + margin: 18px 0 0 0; + } - .mx_E2EIcon { - margin: 5px; + .mx_UserInfo_container { + padding: 0 16px 16px 16px; + border-bottom: 1px solid lightgray; + } + + .mx_UserInfo_memberDetailsContainer { + padding-bottom: 0; + } + + .mx_RoomTile_nameContainer { + width: 154px; + } + + .mx_RoomTile_badge { + display: none; + } + + .mx_RoomTile_name { + width: 160px; + } + + .mx_UserInfo_avatar { + margin: 24px 32px 0 32px; + cursor: pointer; + } + + .mx_UserInfo_avatar > div { + max-width: 30vh; + margin: 0 auto; + } + + .mx_UserInfo_avatar > div > div { + /* use padding-top instead of height to make this element square, + as the % in padding is a % of the width (including margin, + that's why we had to put the margin to center on a parent div), + and not a % of the parent height. */ + padding-top: 100%; + height: 0; + border-radius: 100%; + max-height: 30vh; + box-sizing: content-box; + background-repeat: no-repeat; + background-size: cover; + background-position: center; + } + + .mx_UserInfo_avatar .mx_BaseAvatar.mx_BaseAvatar_image { + cursor: zoom-in; + } + + h3 { + text-transform: uppercase; + color: $notice-secondary-color; + font-weight: bold; + font-size: 12px; + margin: 4px 0; + } + + p { + margin: 5px 0; + } + + .mx_UserInfo_profile { + text-align: center; + + h2 { + font-size: 18px; + line-height: 25px; + flex: 1; + overflow-x: auto; + max-height: 50px; + display: flex; + justify-self: ; + justify-content: center; + align-items: center; + + .mx_E2EIcon { + margin: 5px; + } + } + + .mx_UserInfo_profileStatus { + margin-top: 12px; } } - .mx_UserInfo_profileStatus { - margin-top: 12px; + .mx_UserInfo_memberDetails { + text-align: center; } -} + .mx_UserInfo_field { + cursor: pointer; + color: $accent-color; + line-height: 16px; + margin: 8px 0; -.mx_UserInfo_memberDetails { - text-align: center; -} - -.mx_UserInfo_field { - cursor: pointer; - font-size: 15px; - color: $primary-fg-color; - margin-left: 8px; - line-height: 23px; -} - -.mx_UserInfo_createRoom { - cursor: pointer; - display: flex; - align-items: center; - padding: 0 8px; -} - -.mx_UserInfo_createRoom_label { - width: initial !important; - cursor: pointer; -} - -.mx_UserInfo_statusMessage { - font-size: 11px; - opacity: 0.5; - overflow: hidden; - white-space: nowrap; - text-overflow: clip; -} -.mx_UserInfo .mx_UserInfo_scrollContainer { - flex: 1; - padding-bottom: 16px; -} - -.mx_UserInfo .mx_UserInfo_scrollContainer .mx_UserInfo_container { - padding-top: 16px; - padding-bottom: 0; - border-bottom: none; -} - -.mx_UserInfo_container_header { - display: flex; -} - -.mx_UserInfo_container_header_right { - position: relative; - margin-left: auto; -} - -.mx_UserInfo_newDmButton { - background-color: $roomheader-addroom-bg-color; - border-radius: 10px; // 16/2 + 2 padding - height: 16px; - flex: 0 0 16px; - - &::before { - background-color: $roomheader-addroom-fg-color; - mask: url('$(res)/img/icons-room-add.svg'); - mask-repeat: no-repeat; - mask-position: center; - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; } + + .mx_UserInfo_statusMessage { + font-size: 11px; + opacity: 0.5; + overflow: hidden; + white-space: nowrap; + text-overflow: clip; + } + + .mx_UserInfo_scrollContainer { + flex: 1; + padding-bottom: 16px; + } + + .mx_UserInfo_scrollContainer .mx_UserInfo_container { + padding-top: 16px; + padding-bottom: 0; + border-bottom: none; + + >:not(h3) { + margin-left: 8px; + } + } + + .mx_UserInfo_devices { + .mx_UserInfo_device { + display: flex; + + &.mx_UserInfo_device_verified { + .mx_UserInfo_device_trusted { + color: $accent-color; + } + } + &.mx_UserInfo_device_unverified { + .mx_UserInfo_device_trusted { + color: $warning-color; + } + } + + .mx_UserInfo_device_name { + flex: 1; + margin-right: 5px; + } + } + + // both for icon in expand button and device item + .mx_E2EIcon { + // don't squeeze + flex: 0 0 auto; + margin: 2px 5px 0 0; + width: 12px; + height: 12px; + } + + .mx_UserInfo_expand { + display: flex; + margin-top: 11px; + color: $accent-color; + } + } + } diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index c1a6442409..12a38c468e 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -110,8 +110,42 @@ function useIsEncrypted(cli, room) { return isEncrypted; } -const DevicesSection = ({devices, userId, loading}) => { - const MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo'); +function verifyDevice(userId, device) { + const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog'); + Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, { + userId: userId, + device: device, + }); +} + +function DeviceItem({userId, device}) { + const classes = classNames("mx_UserInfo_device", { + mx_UserInfo_device_verified: device.isVerified(), + mx_UserInfo_device_unverified: !device.isVerified(), + }); + const iconClasses = classNames("mx_E2EIcon", { + mx_E2EIcon_verified: device.isVerified(), + mx_E2EIcon_warning: !device.isVerified(), + }); + + const onDeviceClick = () => { + if (!device.isVerified()) { + verifyDevice(userId, device); + } + }; + + const deviceName = device.ambiguous ? + (device.getDisplayName() ? device.getDisplayName() : "") + " (" + device.deviceId + ")" : + device.getDisplayName(); + const trustedLabel = device.isVerified() ? _t("Trusted") : _t("Not trusted"); + return ( +
    +
    {deviceName}
    +
    {trustedLabel}
    + ); +} + +function DevicesSection({devices, userId, loading}) { const Spinner = sdk.getComponent("elements.Spinner"); const [isExpanded, setExpanded] = useState(false); @@ -130,23 +164,24 @@ const DevicesSection = ({devices, userId, loading}) => { let expandButton; if (verifiedDevices.length) { if (isExpanded) { - expandButton = ( setExpanded(false)}> - {_t("Hide verified Sign-In's")} + expandButton = ( setExpanded(false)}> +
    {_t("Hide verified Sign-In's")}
    ); } else { - expandButton = ( setExpanded(true)}> - {_t("%(count)s verified Sign-In's", {count: verifiedDevices.length})} + expandButton = ( setExpanded(true)}> +
    +
    {_t("%(count)s verified Sign-In's", {count: verifiedDevices.length})}
    ); } } let deviceList = unverifiedDevices.map((device, i) => { - return (); + return (); }); if (isExpanded) { const keyStart = unverifiedDevices.length; deviceList = deviceList.concat(verifiedDevices.map((device, i) => { - return (); + return (); })); } @@ -156,7 +191,7 @@ const DevicesSection = ({devices, userId, loading}) => {
    {expandButton}
    ); -}; +} const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, isIgnored, canInvite, devices}) => { let ignoreButton = null; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9322e71b19..a7bcf29407 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1066,10 +1066,11 @@ "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.", "Members": "Members", "Files": "Files", - "Direct messages": "Direct messages", + "Trusted": "Trusted", + "Not trusted": "Not trusted", "Hide verified Sign-In's": "Hide verified Sign-In's", - "%(count)s verified Sign-In's|one": "1 verified Sign-In", "%(count)s verified Sign-In's|other": "%(count)s verified Sign-In's", + "%(count)s verified Sign-In's|one": "1 verified Sign-In", "Direct message": "Direct message", "Unverify user": "Unverify user", "Options": "Options", From 030827f77d78eead4f9c613e93299ac4b7ad75eb Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 18:00:35 +0100 Subject: [PATCH 128/334] mark destructive actions in red --- res/css/views/right_panel/_UserInfo.scss | 3 +++ src/components/views/right_panel/UserInfo.js | 24 +++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index 79211bb38a..49f52d3387 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -136,6 +136,9 @@ limitations under the License. line-height: 16px; margin: 8px 0; + &.mx_UserInfo_destructive { + color: $warning-color; + } } .mx_UserInfo_statusMessage { diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 12a38c468e..0b8fb8782c 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -224,7 +224,7 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i }; ignoreButton = ( - + { isIgnored ? _t("Unignore") : _t("Ignore") } ); @@ -306,7 +306,7 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i let unverifyButton; if (devices && devices.some(device => device.isVerified())) { unverifyButton = ( - unverifyUser(cli, member.userId)} className="mx_UserInfo_field"> + unverifyUser(cli, member.userId)} className="mx_UserInfo_field mx_UserInfo_destructive"> { _t('Unverify user') } ); @@ -428,7 +428,7 @@ const RoomKickButton = withLegacyMatrixClient(({matrixClient: cli, member, start }; const kickLabel = member.membership === "invite" ? _t("Disinvite") : _t("Kick"); - return + return { kickLabel } ; }); @@ -501,7 +501,7 @@ const RedactMessagesButton = withLegacyMatrixClient(({matrixClient: cli, member} } }; - return + return { _t("Remove recent messages") } ; }); @@ -553,7 +553,11 @@ const BanToggleButton = withLegacyMatrixClient(({matrixClient: cli, member, star label = _t("Unban"); } - return + const classes = classNames("mx_UserInfo_field", { + mx_UserInfo_destructive: member.membership !== 'ban', + }); + + return { label } ; }); @@ -610,8 +614,12 @@ const MuteToggleButton = withLegacyMatrixClient( } }; + const classes = classNames("mx_UserInfo_field", { + mx_UserInfo_destructive: !isMuted, + }); + const muteLabel = isMuted ? _t("Unmute") : _t("Mute"); - return + return { muteLabel } ; }, @@ -734,7 +742,7 @@ const GroupAdminToolsSection = withLegacyMatrixClient( }; const kickButton = ( - + { isInvited ? _t('Disinvite') : _t('Remove from community') } ); @@ -975,7 +983,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room // FIXME this should be using cli instead of MatrixClientPeg.matrixClient if (isSynapseAdmin && user.userId.endsWith(`:${MatrixClientPeg.getHomeserverName()}`)) { synapseDeactivateButton = ( - + {_t("Deactivate user")} ); From 9e8a2eda1fd77c29fad4067bf3192464aa876069 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 18:00:57 +0100 Subject: [PATCH 129/334] small fixes --- src/components/views/right_panel/UserInfo.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 0b8fb8782c..52caa69fcf 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -40,7 +40,6 @@ import MatrixClientPeg from "../../../MatrixClientPeg"; import E2EIcon from "../rooms/E2EIcon"; import withLegacyMatrixClient from "../../../utils/withLegacyMatrixClient"; import {useEventEmitter} from "../../../hooks/useEventEmitter"; -import {ContentRepo} from 'matrix-js-sdk'; const _disambiguateDevices = (devices) => { const names = Object.create(null); @@ -315,13 +314,13 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i return (

    { _t("Options") }

    -
    +
    { directMessageButton } { readReceiptButton } { shareUserButton } { insertPillButton } - { ignoreButton } { inviteUserButton } + { ignoreButton } { unverifyButton }
    From ca12e6c0106942747f5923b625561788d7a1e9ac Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 18:37:25 +0100 Subject: [PATCH 130/334] don't render unverified state on bubbles as they are only used for verification right now, and verification events will be unverified by definition, so no need to alarm users needlessly. Also, this breaks the bubble layout on hover due to e2e icons and verified left border style. --- src/components/views/rooms/EventTile.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 22f1f914b6..5fcf1e4491 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -606,8 +606,8 @@ module.exports = createReactClass({ mx_EventTile_last: this.props.last, mx_EventTile_contextual: this.props.contextual, mx_EventTile_actionBarFocused: this.state.actionBarFocused, - mx_EventTile_verified: this.state.verified === true, - mx_EventTile_unverified: this.state.verified === false, + mx_EventTile_verified: !isBubbleMessage && this.state.verified === true, + mx_EventTile_unverified: !isBubbleMessage && this.state.verified === false, mx_EventTile_bad: isEncryptionFailure, mx_EventTile_emote: msgtype === 'm.emote', mx_EventTile_redacted: isRedacted, @@ -800,7 +800,7 @@ module.exports = createReactClass({ { timestamp } - { this._renderE2EPadlock() } + { !isBubbleMessage && this._renderE2EPadlock() } { thread } { sender } -
    +
    { timestamp } - { this._renderE2EPadlock() } + { !isBubbleMessage && this._renderE2EPadlock() } { thread } Date: Wed, 13 Nov 2019 18:38:55 +0100 Subject: [PATCH 131/334] don't need this, as it takes height from the constrained width --- res/css/views/right_panel/_UserInfo.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index 49f52d3387..a5dae148f4 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -79,7 +79,6 @@ limitations under the License. padding-top: 100%; height: 0; border-radius: 100%; - max-height: 30vh; box-sizing: content-box; background-repeat: no-repeat; background-size: cover; From e3f7fe51dc6be1768d76c707ef95b7dd71ab795c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 18:45:12 +0100 Subject: [PATCH 132/334] use normal shield for verification requests --- res/css/views/messages/_MKeyVerificationRequest.scss | 3 ++- res/img/e2e/normal.svg | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 res/img/e2e/normal.svg diff --git a/res/css/views/messages/_MKeyVerificationRequest.scss b/res/css/views/messages/_MKeyVerificationRequest.scss index aff44e4109..b4cde4e7ef 100644 --- a/res/css/views/messages/_MKeyVerificationRequest.scss +++ b/res/css/views/messages/_MKeyVerificationRequest.scss @@ -25,7 +25,7 @@ limitations under the License. width: 12px; height: 16px; content: ""; - mask: url("$(res)/img/e2e/verified.svg"); + mask: url("$(res)/img/e2e/normal.svg"); mask-repeat: no-repeat; mask-size: 100%; margin-top: 4px; @@ -33,6 +33,7 @@ limitations under the License. } &.mx_KeyVerification_icon_verified::after { + mask: url("$(res)/img/e2e/verified.svg"); background-color: $accent-color; } diff --git a/res/img/e2e/normal.svg b/res/img/e2e/normal.svg new file mode 100644 index 0000000000..5b848bc27f --- /dev/null +++ b/res/img/e2e/normal.svg @@ -0,0 +1,3 @@ + + + From 942a1c9a56dcef794db5d8c2ce4e9650a551d035 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 18:55:11 +0100 Subject: [PATCH 133/334] fix e2e icon in composer having wrong colors --- res/css/views/rooms/_MessageComposer.scss | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index e9f33183f5..14562fe7ed 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -78,7 +78,10 @@ limitations under the License. .mx_MessageComposer_e2eIcon.mx_E2EIcon { position: absolute; left: 60px; - background-color: $composer-e2e-icon-color; + + &::after { + background-color: $composer-e2e-icon-color; + } } .mx_MessageComposer_noperm_error { From b278531f2fbabfdee31bd46434f30f9e462e56ea Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 14 Nov 2019 16:58:07 +0100 Subject: [PATCH 134/334] add IconButton as in design --- res/css/_components.scss | 1 + res/css/views/elements/_IconButton.scss | 55 +++++++++++++++++++++ res/img/feather-customised/edit.svg | 4 ++ src/components/views/elements/IconButton.js | 34 +++++++++++++ 4 files changed, 94 insertions(+) create mode 100644 res/css/views/elements/_IconButton.scss create mode 100644 res/img/feather-customised/edit.svg create mode 100644 src/components/views/elements/IconButton.js diff --git a/res/css/_components.scss b/res/css/_components.scss index c8ea237dcd..40a2c576d0 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -90,6 +90,7 @@ @import "./views/elements/_ErrorBoundary.scss"; @import "./views/elements/_EventListSummary.scss"; @import "./views/elements/_Field.scss"; +@import "./views/elements/_IconButton.scss"; @import "./views/elements/_ImageView.scss"; @import "./views/elements/_InlineSpinner.scss"; @import "./views/elements/_InteractiveTooltip.scss"; diff --git a/res/css/views/elements/_IconButton.scss b/res/css/views/elements/_IconButton.scss new file mode 100644 index 0000000000..d8ebbeb65e --- /dev/null +++ b/res/css/views/elements/_IconButton.scss @@ -0,0 +1,55 @@ +/* +Copyright 2019 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_IconButton { + width: 32px; + height: 32px; + border-radius: 100%; + background-color: $accent-bg-color; + // don't shrink or grow if in a flex container + flex: 0 0 auto; + + &.mx_AccessibleButton_disabled { + background-color: none; + + &::before { + background-color: lightgrey; + } + } + + &:hover { + opacity: 90%; + } + + &::before { + content: ""; + display: block; + width: 100%; + height: 100%; + mask-repeat: no-repeat; + mask-position: center; + mask-size: 55%; + background-color: $accent-color; + } + + &.mx_IconButton_icon_check::before { + mask-image: url('$(res)/img/feather-customised/check.svg'); + } + + &.mx_IconButton_icon_edit::before { + mask-image: url('$(res)/img/feather-customised/edit.svg'); + } +} diff --git a/res/img/feather-customised/edit.svg b/res/img/feather-customised/edit.svg new file mode 100644 index 0000000000..f511aa1477 --- /dev/null +++ b/res/img/feather-customised/edit.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/views/elements/IconButton.js b/src/components/views/elements/IconButton.js new file mode 100644 index 0000000000..9f5bf77426 --- /dev/null +++ b/src/components/views/elements/IconButton.js @@ -0,0 +1,34 @@ +/* + Copyright 2016 Jani Mustonen + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import AccessibleButton from "./AccessibleButton"; + +export default function IconButton(props) { + const {icon, className, ...restProps} = props; + + let newClassName = (className || "") + " mx_IconButton"; + newClassName = newClassName + " mx_IconButton_icon_" + icon; + + const allProps = Object.assign({}, restProps, {className: newClassName}); + + return React.createElement(AccessibleButton, allProps); +} + +IconButton.propTypes = Object.assign({ + icon: PropTypes.string, +}, AccessibleButton.propTypes); From d0914f9208f44e624eb91bff5bc654c4a9f25bfe Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 14 Nov 2019 16:58:37 +0100 Subject: [PATCH 135/334] allow label to be empty on power selector --- src/components/views/elements/PowerSelector.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/PowerSelector.js b/src/components/views/elements/PowerSelector.js index 5bc8eeba58..e6babded32 100644 --- a/src/components/views/elements/PowerSelector.js +++ b/src/components/views/elements/PowerSelector.js @@ -129,10 +129,11 @@ module.exports = createReactClass({ render: function() { let picker; + const label = typeof this.props.label === "undefined" ? _t("Power level") : this.props.label; if (this.state.custom) { picker = ( ); @@ -151,7 +152,7 @@ module.exports = createReactClass({ picker = ( {options} From 91e02aa623e91f1d09d8cea2ad447f8cd5222dd0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 14 Nov 2019 16:58:56 +0100 Subject: [PATCH 136/334] hide PL numbers on labels --- src/Roles.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Roles.js b/src/Roles.js index 10c4ceaf1e..4c0d2ab4e6 100644 --- a/src/Roles.js +++ b/src/Roles.js @@ -28,8 +28,8 @@ export function levelRoleMap(usersDefault) { export function textualPowerLevel(level, usersDefault) { const LEVEL_ROLE_MAP = levelRoleMap(usersDefault); if (LEVEL_ROLE_MAP[level]) { - return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${usersDefault})`); + return LEVEL_ROLE_MAP[level]; } else { - return level; + return _t("Custom %(level)s", {level}); } } From 6db162a3a7a5a13e99d4e31b43091c25831a0223 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 14 Nov 2019 16:59:09 +0100 Subject: [PATCH 137/334] add PL edit mode, don't show selector by default still saves when changing the selector though --- res/css/views/right_panel/_UserInfo.scss | 30 ++- src/components/views/right_panel/UserInfo.js | 192 +++++++++++-------- src/i18n/strings/en_EN.json | 2 + 3 files changed, 141 insertions(+), 83 deletions(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index a5dae148f4..9e4d4dc471 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -125,8 +125,34 @@ limitations under the License. } } - .mx_UserInfo_memberDetails { - text-align: center; + .mx_UserInfo_memberDetails .mx_UserInfo_profileField { + display: flex; + justify-content: center; + align-items: center; + + margin: 6px 0; + + .mx_IconButton { + margin-left: 6px; + width: 16px; + height: 16px; + + &::before { + mask-size: 80%; + } + } + + .mx_UserInfo_roleDescription { + display: flex; + justify-content: center; + align-items: center; + // try to make it the same height as the dropdown + margin: 11px 0 12px 0; + } + + .mx_Field { + margin: 0; + } } .mx_UserInfo_field { diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 52caa69fcf..379292c152 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -27,7 +27,6 @@ import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import createRoom from '../../../createRoom'; import DMRoomMap from '../../../utils/DMRoomMap'; -import Unread from '../../../Unread'; import AccessibleButton from '../elements/AccessibleButton'; import SdkConfig from '../../../SdkConfig'; import SettingsStore from "../../../settings/SettingsStore"; @@ -40,6 +39,7 @@ import MatrixClientPeg from "../../../MatrixClientPeg"; import E2EIcon from "../rooms/E2EIcon"; import withLegacyMatrixClient from "../../../utils/withLegacyMatrixClient"; import {useEventEmitter} from "../../../hooks/useEventEmitter"; +import {textualPowerLevel} from '../../../Roles'; const _disambiguateDevices = (devices) => { const names = Object.create(null); @@ -780,43 +780,11 @@ const useIsSynapseAdmin = (cli) => { return isAdmin; }; -// cli is injected by withLegacyMatrixClient -const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, roomId, onClose}) => { - // Load room if we are given a room id and memoize it - const room = useMemo(() => roomId ? cli.getRoom(roomId) : null, [cli, roomId]); - - // only display the devices list if our client supports E2E - const _enableDevices = cli.isCryptoEnabled(); - - // Load whether or not we are a Synapse Admin - const isSynapseAdmin = useIsSynapseAdmin(cli); - - // Check whether the user is ignored - const [isIgnored, setIsIgnored] = useState(cli.isUserIgnored(user.userId)); - // Recheck if the user or client changes - useEffect(() => { - setIsIgnored(cli.isUserIgnored(user.userId)); - }, [cli, user.userId]); - // Recheck also if we receive new accountData m.ignored_user_list - const accountDataHandler = useCallback((ev) => { - if (ev.getType() === "m.ignored_user_list") { - setIsIgnored(cli.isUserIgnored(user.userId)); - } - }, [cli, user.userId]); - useEventEmitter(cli, "accountData", accountDataHandler); - - // Count of how many operations are currently in progress, if > 0 then show a Spinner - const [pendingUpdateCount, setPendingUpdateCount] = useState(0); - const startUpdating = useCallback(() => { - setPendingUpdateCount(pendingUpdateCount + 1); - }, [pendingUpdateCount]); - const stopUpdating = useCallback(() => { - setPendingUpdateCount(pendingUpdateCount - 1); - }, [pendingUpdateCount]); - +function useRoomPermissions(cli, room, user) { const [roomPermissions, setRoomPermissions] = useState({ // modifyLevelMax is the max PL we can set this user to, typically min(their PL, our PL) && canSetPL modifyLevelMax: -1, + canAffectUser: false, canInvite: false, }); const updateRoomPermissions = useCallback(async () => { @@ -847,6 +815,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room setRoomPermissions({ canInvite: me.powerLevel >= powerLevels.invite, + canAffectUser, modifyLevelMax, }); }, [cli, user, room]); @@ -856,38 +825,16 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room return () => { setRoomPermissions({ maximalPowerLevel: -1, + canAffectUser: false, canInvite: false, }); }; }, [updateRoomPermissions]); - const onSynapseDeactivate = useCallback(async () => { - const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog'); - const {finished} = Modal.createTrackedDialog('Synapse User Deactivation', '', QuestionDialog, { - title: _t("Deactivate user?"), - description: -
    { _t( - "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?", - ) }
    , - button: _t("Deactivate user"), - danger: true, - }); - - const [accepted] = await finished; - if (!accepted) return; - try { - cli.deactivateSynapseUser(user.userId); - } catch (err) { - const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); - Modal.createTrackedDialog('Failed to deactivate user', '', ErrorDialog, { - title: _t('Failed to deactivate user'), - description: ((err && err.message) ? err.message : _t("Operation failed")), - }); - } - }, [cli, user.userId]); + return roomPermissions; +} +const PowerLevelEditor = withLegacyMatrixClient(({matrixClient: cli, user, room, startUpdating, stopUpdating, roomPermissions}) => { const onPowerChange = useCallback(async (powerLevel) => { const _applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => { startUpdating(); @@ -953,6 +900,104 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); }, [user.roomId, user.userId, room && room.currentState, cli]); // eslint-disable-line + + const [isEditingPL, setEditingPL] = useState(false); + if (room && user.roomId) { // is in room + const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); + const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0; + const powerLevel = parseInt(user.powerLevel); + const IconButton = sdk.getComponent('elements.IconButton'); + if (isEditingPL) { + const PowerSelector = sdk.getComponent('elements.PowerSelector'); + return ( +
    + + setEditingPL(false)} /> +
    + ); + } else { + const modifyButton = roomPermissions.canAffectUser ? + ( setEditingPL(true)} />) : null; + const role = textualPowerLevel(powerLevel, powerLevelUsersDefault); + const label = _t("%(role)s in %(roomName)s", + {role, roomName: room.name}, + {strong: label => {label}}, + ); + return (
    {label}{modifyButton}
    ); + } + } +}); + +// cli is injected by withLegacyMatrixClient +const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, roomId, onClose}) => { + // Load room if we are given a room id and memoize it + const room = useMemo(() => roomId ? cli.getRoom(roomId) : null, [cli, roomId]); + + // only display the devices list if our client supports E2E + const _enableDevices = cli.isCryptoEnabled(); + + // Load whether or not we are a Synapse Admin + const isSynapseAdmin = useIsSynapseAdmin(cli); + + // Check whether the user is ignored + const [isIgnored, setIsIgnored] = useState(cli.isUserIgnored(user.userId)); + // Recheck if the user or client changes + useEffect(() => { + setIsIgnored(cli.isUserIgnored(user.userId)); + }, [cli, user.userId]); + // Recheck also if we receive new accountData m.ignored_user_list + const accountDataHandler = useCallback((ev) => { + if (ev.getType() === "m.ignored_user_list") { + setIsIgnored(cli.isUserIgnored(user.userId)); + } + }, [cli, user.userId]); + useEventEmitter(cli, "accountData", accountDataHandler); + + // Count of how many operations are currently in progress, if > 0 then show a Spinner + const [pendingUpdateCount, setPendingUpdateCount] = useState(0); + const startUpdating = useCallback(() => { + setPendingUpdateCount(pendingUpdateCount + 1); + }, [pendingUpdateCount]); + const stopUpdating = useCallback(() => { + setPendingUpdateCount(pendingUpdateCount - 1); + }, [pendingUpdateCount]); + + const roomPermissions = useRoomPermissions(cli, room, user); + + const onSynapseDeactivate = useCallback(async () => { + const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog'); + const {finished} = Modal.createTrackedDialog('Synapse User Deactivation', '', QuestionDialog, { + title: _t("Deactivate user?"), + description: +
    { _t( + "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?", + ) }
    , + button: _t("Deactivate user"), + danger: true, + }); + + const [accepted] = await finished; + if (!accepted) return; + try { + cli.deactivateSynapseUser(user.userId); + } catch (err) { + const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); + Modal.createTrackedDialog('Failed to deactivate user', '', ErrorDialog, { + title: _t('Failed to deactivate user'), + description: ((err && err.message) ? err.message : _t("Operation failed")), + }); + } + }, [cli, user.userId]); + + const onMemberAvatarKey = e => { if (e.key === "Enter") { onMemberAvatarClick(); @@ -1058,26 +1103,6 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room statusLabel = { statusMessage }; } - let memberDetails = null; - - if (room && user.roomId) { // is in room - const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); - const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0; - - const PowerSelector = sdk.getComponent('elements.PowerSelector'); - memberDetails =
    -
    - -
    - -
    ; - } - const avatarUrl = user.getMxcAvatarUrl ? user.getMxcAvatarUrl() : user.avatarUrl; let avatarElement; if (avatarUrl) { @@ -1102,6 +1127,11 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room title={_t('Close')} />; } + const memberDetails = ; + const isRoomEncrypted = useIsEncrypted(cli, room); // undefined means yet to be loaded, null means failed to load, otherwise list of devices const [devices, setDevices] = useState(undefined); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a7bcf29407..46ad7d5135 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -118,6 +118,7 @@ "Restricted": "Restricted", "Moderator": "Moderator", "Admin": "Admin", + "Custom %(level)s": "Custom %(level)s", "Start a chat": "Start a chat", "Who would you like to communicate with?": "Who would you like to communicate with?", "Email, name or Matrix ID": "Email, name or Matrix ID", @@ -1080,6 +1081,7 @@ "Failed to withdraw invitation": "Failed to withdraw invitation", "Failed to remove user from community": "Failed to remove user from community", "Failed to deactivate user": "Failed to deactivate user", + "%(role)s in %(roomName)s": "%(role)s in %(roomName)s", "This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.", "Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.", "Messages in this room are end-to-end encrypted.": "Messages in this room are end-to-end encrypted.", From bd853b3102a3da670a705aae338b11cf4b4da9f8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 12:10:56 +0100 Subject: [PATCH 138/334] listen for RoomState.members instead of RoomState.events as the powerlevel on the member is not yet updated at the time RoomState.events is emitted. Also listen on the client for this event as the currentState object can change when the timeline is reset. --- src/components/views/right_panel/UserInfo.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 379292c152..8931b3ed1f 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -365,10 +365,13 @@ const _isMuted = (member, powerLevelContent) => { return member.powerLevel < levelToSend; }; -const useRoomPowerLevels = (room) => { +const useRoomPowerLevels = (cli, room) => { const [powerLevels, setPowerLevels] = useState({}); const update = useCallback(() => { + if (!room) { + return; + } const event = room.currentState.getStateEvents("m.room.power_levels", ""); if (event) { setPowerLevels(event.getContent()); @@ -380,7 +383,7 @@ const useRoomPowerLevels = (room) => { }; }, [room]); - useEventEmitter(room.currentState, "RoomState.events", update); + useEventEmitter(cli, "RoomState.members", update); useEffect(() => { update(); return () => { @@ -819,7 +822,7 @@ function useRoomPermissions(cli, room, user) { modifyLevelMax, }); }, [cli, user, room]); - useEventEmitter(cli, "RoomState.events", updateRoomPermissions); + useEventEmitter(cli, "RoomState.members", updateRoomPermissions); useEffect(() => { updateRoomPermissions(); return () => { From e86ceb986fc913e958c6eb72cb0f237b86ebeb07 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 15:13:22 +0100 Subject: [PATCH 139/334] pass powerlevels state to power level section and admin section --- src/components/views/right_panel/UserInfo.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 8931b3ed1f..a0a256e4c8 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -628,13 +628,12 @@ const MuteToggleButton = withLegacyMatrixClient( ); const RoomAdminToolsContainer = withLegacyMatrixClient( - ({matrixClient: cli, room, children, member, startUpdating, stopUpdating}) => { + ({matrixClient: cli, room, children, member, startUpdating, stopUpdating, powerLevels}) => { let kickButton; let banButton; let muteButton; let redactButton; - const powerLevels = useRoomPowerLevels(room); const editPowerLevel = ( (powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) || powerLevels.state_default @@ -837,8 +836,7 @@ function useRoomPermissions(cli, room, user) { return roomPermissions; } -const PowerLevelEditor = withLegacyMatrixClient(({matrixClient: cli, user, room, startUpdating, stopUpdating, roomPermissions}) => { - const onPowerChange = useCallback(async (powerLevel) => { +const PowerLevelSection = withLegacyMatrixClient(({matrixClient: cli, user, room, roomPermissions, powerLevels}) => { const _applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => { startUpdating(); cli.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then( @@ -945,6 +943,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room // only display the devices list if our client supports E2E const _enableDevices = cli.isCryptoEnabled(); + const powerLevels = useRoomPowerLevels(cli, room); // Load whether or not we are a Synapse Admin const isSynapseAdmin = useIsSynapseAdmin(cli); @@ -1040,6 +1039,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room if (room && user.roomId) { adminToolsContainer = ( ; } - const memberDetails = ; const isRoomEncrypted = useIsEncrypted(cli, room); From 48b1207c6ed2ea07d764f96db5429c2a6c72f24d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 15:16:23 +0100 Subject: [PATCH 140/334] split up PowerLevelEditor into two components one while editing (PowerLevelEditor) and one while not editing (PowerLevelSection). Also save when pressing the button, not when changing the power dropdown. Also show the spinner next to the dropdown when saving, not at the bottom of the component. --- res/css/views/right_panel/_UserInfo.scss | 8 +- src/Roles.js | 2 +- src/components/views/right_panel/UserInfo.js | 190 +++++++++++-------- src/i18n/strings/en_EN.json | 4 +- 4 files changed, 122 insertions(+), 82 deletions(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index 9e4d4dc471..2b2add49ee 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -132,8 +132,8 @@ limitations under the License. margin: 6px 0; - .mx_IconButton { - margin-left: 6px; + .mx_IconButton, .mx_Spinner { + margin-left: 20px; width: 16px; height: 16px; @@ -148,6 +148,10 @@ limitations under the License. align-items: center; // try to make it the same height as the dropdown margin: 11px 0 12px 0; + + .mx_IconButton { + margin-left: 6px; + } } .mx_Field { diff --git a/src/Roles.js b/src/Roles.js index 4c0d2ab4e6..7cc3c880d7 100644 --- a/src/Roles.js +++ b/src/Roles.js @@ -30,6 +30,6 @@ export function textualPowerLevel(level, usersDefault) { if (LEVEL_ROLE_MAP[level]) { return LEVEL_ROLE_MAP[level]; } else { - return _t("Custom %(level)s", {level}); + return _t("Custom (%(level)s)", {level}); } } diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index a0a256e4c8..589eca9a08 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -837,9 +837,46 @@ function useRoomPermissions(cli, room, user) { } const PowerLevelSection = withLegacyMatrixClient(({matrixClient: cli, user, room, roomPermissions, powerLevels}) => { + const [isEditing, setEditing] = useState(false); + if (room && user.roomId) { // is in room + if (isEditing) { + return ( setEditing(false)} />); + } else { + const IconButton = sdk.getComponent('elements.IconButton'); + const powerLevelUsersDefault = powerLevels.users_default || 0; + const powerLevel = parseInt(user.powerLevel, 10); + const modifyButton = roomPermissions.canEdit ? + ( setEditing(true)} />) : null; + const role = textualPowerLevel(powerLevel, powerLevelUsersDefault); + const label = _t("%(role)s in %(roomName)s", + {role, roomName: room.name}, + {strong: label => {label}}, + ); + return ( +
    +
    {label}{modifyButton}
    +
    + ); + } + } else { + return null; + } +}); + +const PowerLevelEditor = withLegacyMatrixClient(({matrixClient: cli, user, room, roomPermissions, onFinished}) => { + const [isUpdating, setIsUpdating] = useState(false); + const [selectedPowerLevel, setSelectedPowerLevel] = useState(parseInt(user.powerLevel, 10)); + const [isDirty, setIsDirty] = useState(false); + const onPowerChange = useCallback((powerLevel) => { + setIsDirty(true); + setSelectedPowerLevel(parseInt(powerLevel, 10)); + }, [setSelectedPowerLevel, setIsDirty]); + + const changePowerLevel = useCallback(async () => { const _applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => { - startUpdating(); - cli.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then( + return cli.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then( function() { // NO-OP; rely on the m.room.member event coming down else we could // get out of sync if we force setState here! @@ -852,87 +889,86 @@ const PowerLevelSection = withLegacyMatrixClient(({matrixClient: cli, user, room description: _t("Failed to change power level"), }); }, - ).finally(() => { - stopUpdating(); - }).done(); + ); }; - const roomId = user.roomId; - const target = user.userId; - - const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); - if (!powerLevelEvent) return; - - if (!powerLevelEvent.getContent().users) { - _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); - return; - } - - const myUserId = cli.getUserId(); - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - - // If we are changing our own PL it can only ever be decreasing, which we cannot reverse. - if (myUserId === target) { - try { - if (!(await _warnSelfDemote())) return; - _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); - } catch (e) { - console.error("Failed to warn about self demotion: ", e); + try { + if (!isDirty) { + return; } - return; + + setIsUpdating(true); + + const powerLevel = selectedPowerLevel; + + const roomId = user.roomId; + const target = user.userId; + + const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); + if (!powerLevelEvent) return; + + if (!powerLevelEvent.getContent().users) { + _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); + return; + } + + const myUserId = cli.getUserId(); + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + + // If we are changing our own PL it can only ever be decreasing, which we cannot reverse. + if (myUserId === target) { + try { + if (!(await _warnSelfDemote())) return; + } catch (e) { + console.error("Failed to warn about self demotion: ", e); + } + await _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); + return; + } + + const myPower = powerLevelEvent.getContent().users[myUserId]; + if (parseInt(myPower) === parseInt(powerLevel)) { + const {finished} = Modal.createTrackedDialog('Promote to PL100 Warning', '', QuestionDialog, { + title: _t("Warning!"), + description: +
    + { _t("You will not be able to undo this change as you are promoting the user " + + "to have the same power level as yourself.") }
    + { _t("Are you sure?") } +
    , + button: _t("Continue"), + }); + + const [confirmed] = await finished; + if (confirmed) return; + } + await _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); + } finally { + onFinished(); } + }, [user.roomId, user.userId, cli, selectedPowerLevel, isDirty, setIsUpdating, onFinished, room]); - const myPower = powerLevelEvent.getContent().users[myUserId]; - if (parseInt(myPower) === parseInt(powerLevel)) { - const {finished} = Modal.createTrackedDialog('Promote to PL100 Warning', '', QuestionDialog, { - title: _t("Warning!"), - description: -
    - { _t("You will not be able to undo this change as you are promoting the user " + - "to have the same power level as yourself.") }
    - { _t("Are you sure?") } -
    , - button: _t("Continue"), - }); + const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); + const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0; + const IconButton = sdk.getComponent('elements.IconButton'); + const Spinner = sdk.getComponent("elements.Spinner"); + const buttonOrSpinner = isUpdating ? : + ; - const [confirmed] = await finished; - if (confirmed) return; - } - _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); - }, [user.roomId, user.userId, room && room.currentState, cli]); // eslint-disable-line - - - const [isEditingPL, setEditingPL] = useState(false); - if (room && user.roomId) { // is in room - const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); - const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0; - const powerLevel = parseInt(user.powerLevel); - const IconButton = sdk.getComponent('elements.IconButton'); - if (isEditingPL) { - const PowerSelector = sdk.getComponent('elements.PowerSelector'); - return ( -
    - - setEditingPL(false)} /> -
    - ); - } else { - const modifyButton = roomPermissions.canAffectUser ? - ( setEditingPL(true)} />) : null; - const role = textualPowerLevel(powerLevel, powerLevelUsersDefault); - const label = _t("%(role)s in %(roomName)s", - {role, roomName: room.name}, - {strong: label => {label}}, - ); - return (
    {label}{modifyButton}
    ); - } - } + const PowerSelector = sdk.getComponent('elements.PowerSelector'); + return ( +
    + + {buttonOrSpinner} +
    + ); }); // cli is injected by withLegacyMatrixClient diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 46ad7d5135..029000e9d2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -118,7 +118,7 @@ "Restricted": "Restricted", "Moderator": "Moderator", "Admin": "Admin", - "Custom %(level)s": "Custom %(level)s", + "Custom (%(level)s)": "Custom (%(level)s)", "Start a chat": "Start a chat", "Who would you like to communicate with?": "Who would you like to communicate with?", "Email, name or Matrix ID": "Email, name or Matrix ID", @@ -1080,8 +1080,8 @@ "Remove this user from community?": "Remove this user from community?", "Failed to withdraw invitation": "Failed to withdraw invitation", "Failed to remove user from community": "Failed to remove user from community", - "Failed to deactivate user": "Failed to deactivate user", "%(role)s in %(roomName)s": "%(role)s in %(roomName)s", + "Failed to deactivate user": "Failed to deactivate user", "This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.", "Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.", "Messages in this room are end-to-end encrypted.": "Messages in this room are end-to-end encrypted.", From 264c8181c2f6c6b52d673e16bbf76708f06c3f30 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 15:18:04 +0100 Subject: [PATCH 141/334] add canEdit to permission state, more explicit than maxLevel >= 0 --- src/components/views/right_panel/UserInfo.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 589eca9a08..681336df7c 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -786,7 +786,7 @@ function useRoomPermissions(cli, room, user) { const [roomPermissions, setRoomPermissions] = useState({ // modifyLevelMax is the max PL we can set this user to, typically min(their PL, our PL) && canSetPL modifyLevelMax: -1, - canAffectUser: false, + canEdit: false, canInvite: false, }); const updateRoomPermissions = useCallback(async () => { @@ -817,7 +817,7 @@ function useRoomPermissions(cli, room, user) { setRoomPermissions({ canInvite: me.powerLevel >= powerLevels.invite, - canAffectUser, + canEdit: modifyLevelMax >= 0, modifyLevelMax, }); }, [cli, user, room]); @@ -827,7 +827,7 @@ function useRoomPermissions(cli, room, user) { return () => { setRoomPermissions({ maximalPowerLevel: -1, - canAffectUser: false, + canEdit: false, canInvite: false, }); }; From 92237f10452eb77b077f3e49373a497791b6bb7e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 15:18:36 +0100 Subject: [PATCH 142/334] cleanup --- src/components/views/right_panel/UserInfo.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 681336df7c..a194b89eee 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -789,8 +789,10 @@ function useRoomPermissions(cli, room, user) { canEdit: false, canInvite: false, }); - const updateRoomPermissions = useCallback(async () => { - if (!room) return; + const updateRoomPermissions = useCallback(() => { + if (!room) { + return; + } const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); if (!powerLevelEvent) return; From 53019c5e9135b2ad0bc45b23438407fff2763272 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 15:19:16 +0100 Subject: [PATCH 143/334] don't show devices section when not encrypted, as it just shows spinner --- src/components/views/right_panel/UserInfo.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index a194b89eee..88cc41766d 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -1248,11 +1248,13 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room text = _t("Messages in this room are end-to-end encrypted."); } - const devicesSection = ( + const devicesSection = isRoomEncrypted ? + () : null; + const securitySection = (

    { _t("Security") }

    { text }

    - + { devicesSection }
    ); @@ -1289,7 +1291,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
    } - { devicesSection } + { securitySection } Date: Fri, 15 Nov 2019 15:23:23 +0100 Subject: [PATCH 144/334] prevent https://github.com/vector-im/riot-web/issues/11338 --- src/components/views/right_panel/UserInfo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 88cc41766d..7355153ec7 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -1115,7 +1115,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room let presenceCurrentlyActive; let statusMessage; - if (user instanceof RoomMember) { + if (user instanceof RoomMember && user.user) { presenceState = user.user.presence; presenceLastActiveAgo = user.user.lastActiveAgo; presenceCurrentlyActive = user.user.currentlyActive; From 1162d1ee43862b994245641f118c2d2bade438e2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 15:29:23 +0100 Subject: [PATCH 145/334] Fix user info scroll container growing larger than available height making whole room view grow taller than viewport --- res/css/views/right_panel/_UserInfo.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index 2b2add49ee..c5c0d9f42f 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -179,7 +179,7 @@ limitations under the License. } .mx_UserInfo_scrollContainer { - flex: 1; + flex: 1 1 0; padding-bottom: 16px; } From edd5d3c9150679a5b7c0b08e06da3a886ac16a80 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 15:37:43 +0100 Subject: [PATCH 146/334] make custom power level not grow too wide --- res/css/views/elements/_Field.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/elements/_Field.scss b/res/css/views/elements/_Field.scss index 4d012a136e..b260d4b097 100644 --- a/res/css/views/elements/_Field.scss +++ b/res/css/views/elements/_Field.scss @@ -49,6 +49,7 @@ limitations under the License. color: $primary-fg-color; background-color: $primary-bg-color; flex: 1; + min-width: 0; } .mx_Field select { From ecc842629a989ff824d8114c22acd73affd4f243 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 15:48:04 +0100 Subject: [PATCH 147/334] fix css lint errors --- res/css/views/right_panel/_UserInfo.scss | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index c5c0d9f42f..7fda114a79 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -111,7 +111,6 @@ limitations under the License. overflow-x: auto; max-height: 50px; display: flex; - justify-self: ; justify-content: center; align-items: center; @@ -188,7 +187,7 @@ limitations under the License. padding-bottom: 0; border-bottom: none; - >:not(h3) { + > :not(h3) { margin-left: 8px; } } @@ -229,5 +228,4 @@ limitations under the License. color: $accent-color; } } - } From d416ba2c0ceea8f7e8eca9c0f871552ec8a6220c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 16:04:00 +0100 Subject: [PATCH 148/334] add verify button while we don't have the verification in right panel --- res/css/views/right_panel/_UserInfo.scss | 10 ++++++++++ src/components/views/right_panel/UserInfo.js | 1 + src/i18n/strings/en_EN.json | 1 + 3 files changed, 12 insertions(+) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index 7fda114a79..c68f3ffd37 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -228,4 +228,14 @@ limitations under the License. color: $accent-color; } } + + .mx_UserInfo_verify { + display: block; + background-color: $accent-color; + color: $accent-fg-color; + border-radius: 4px; + padding: 7px 1.5em; + text-align: center; + margin: 16px 0; + } } diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 7355153ec7..53a87ed1c6 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -1254,6 +1254,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room

    { _t("Security") }

    { text }

    + verifyDevice(user.userId, null)}>{_t("Verify")} { devicesSection }
    ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 029000e9d2..a90af471c2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1086,6 +1086,7 @@ "Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.", "Messages in this room are end-to-end encrypted.": "Messages in this room are end-to-end encrypted.", "Security": "Security", + "Verify": "Verify", "Sunday": "Sunday", "Monday": "Monday", "Tuesday": "Tuesday", From 0f39a9f72dd20f4e4dfa372b9356c0f468fbfcbd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 17:29:38 +0100 Subject: [PATCH 149/334] fix pr feedback --- res/css/views/rooms/_E2EIcon.scss | 4 ++-- src/components/views/elements/IconButton.js | 22 ++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/res/css/views/rooms/_E2EIcon.scss b/res/css/views/rooms/_E2EIcon.scss index c8b1be47f9..bc11ac6e1c 100644 --- a/res/css/views/rooms/_E2EIcon.scss +++ b/res/css/views/rooms/_E2EIcon.scss @@ -25,9 +25,9 @@ limitations under the License. .mx_E2EIcon_verified::before, .mx_E2EIcon_warning::before { content: ""; display: block; - /* the symbols in the shield icons are cut out the make the themeable with css masking. + /* the symbols in the shield icons are cut out to make it themeable with css masking. if they appear on a different background than white, the symbol wouldn't be white though, so we - add a rectangle here below the masked element to shine through the symbol cutout. + add a rectangle here below the masked element to shine through the symbol cut-out. hardcoding white and not using a theme variable as this would probably be white for any theme. */ background-color: white; position: absolute; diff --git a/src/components/views/elements/IconButton.js b/src/components/views/elements/IconButton.js index 9f5bf77426..ef7b4a8399 100644 --- a/src/components/views/elements/IconButton.js +++ b/src/components/views/elements/IconButton.js @@ -1,18 +1,18 @@ /* - Copyright 2016 Jani Mustonen +Copyright 2019 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 +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 +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. - */ +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ import React from 'react'; import PropTypes from 'prop-types'; From 5f31efadb68c84a2d54599a4324e919e71d17d63 Mon Sep 17 00:00:00 2001 From: Victor Grousset Date: Fri, 15 Nov 2019 09:33:19 +0000 Subject: [PATCH 150/334] Translated using Weblate (Esperanto) Currently translated at 98.1% (1862 of 1898 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 bbed6773b5..b160c5390b 100644 --- a/src/i18n/strings/eo.json +++ b/src/i18n/strings/eo.json @@ -1995,7 +1995,7 @@ "Preview": "Antaŭrigardo", "View": "Rigardo", "Find a room…": "Trovi ĉambron…", - "If you can't find the room you're looking for, ask for an invite or Create a new room.": "Se vi ne povas travi la serĉatan ĉambron, petu inviton aŭ kreu novan ĉambron.", + "If you can't find the room you're looking for, ask for an invite or Create a new room.": "Se vi ne povas trovi la serĉatan ĉambron, petu inviton aŭ kreu novan ĉambron.", "Explore rooms": "Esplori ĉambrojn", "Add Email Address": "Aldoni retpoŝtadreson", "Add Phone Number": "Aldoni telefonnumeron", From 21dc9b9f25dfe3d21ed0970e8eef2a91c439ec5f Mon Sep 17 00:00:00 2001 From: Tuomas Hietala Date: Fri, 15 Nov 2019 11:47:01 +0000 Subject: [PATCH 151/334] Translated using Weblate (Finnish) Currently translated at 96.4% (1830 of 1898 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 | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index a1163f564b..80fbb9b138 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -2121,5 +2121,21 @@ "Emoji Autocomplete": "Emojien automaattinen täydennys", "Notification Autocomplete": "Ilmoitusten automaattinen täydennys", "Room Autocomplete": "Huoneiden automaattinen täydennys", - "User Autocomplete": "Käyttäjien automaattinen täydennys" + "User Autocomplete": "Käyttäjien automaattinen täydennys", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Tämä toiminto vaatii oletusidentiteettipalvelimen käyttämistä sähköpostiosoitteen tai puhelinnumeron validointiin, mutta palvelimella ei ole käyttöehtoja.", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "My Ban List": "Tekemäni estot", + "This is your list of users/servers you have blocked - don't leave the room!": "Tämä on luettelo käyttäjistä ja palvelimista, jotka olet estänyt - älä poistu huoneesta!", + "Something went wrong. Please try again or view your console for hints.": "Jotain meni vikaan. Yritä uudelleen tai katso vihjeitä konsolista.", + "Please verify the room ID or alias and try again.": "Varmista huoneen tunnus tai alias ja yritä uudelleen.", + "Please try again or view your console for hints.": "Yritä uudelleen tai katso vihjeitä konsolista.", + "⚠ These settings are meant for advanced users.": "⚠ Nämä asetukset on tarkoitettu edistyneille käyttäjille.", + "eg: @bot:* or example.org": "esim. @bot:* tai esimerkki.org", + "Show tray icon and minimize window to it on close": "Näytä ilmaisinalueen kuvake ja pienennä ikkuna siihen suljettaessa", + "Your email address hasn't been verified yet": "Sähköpostiosoitettasi ei ole vielä varmistettu", + "Verify the link in your inbox": "Varmista sähköpostiisi saapunut linkki", + "%(count)s unread messages including mentions.|one": "Yksi lukematon maininta.", + "%(count)s unread messages.|one": "Yksi lukematon viesti.", + "Unread messages.": "Lukemattomat viestit.", + "Message Actions": "Viestitoiminnot" } From 9a92f3d38eb237ab808c8887391ad6f65dd94811 Mon Sep 17 00:00:00 2001 From: Volodymyr Kostyrko Date: Thu, 14 Nov 2019 20:50:50 +0000 Subject: [PATCH 152/334] Translated using Weblate (Ukrainian) Currently translated at 29.0% (551 of 1898 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/uk/ --- src/i18n/strings/uk.json | 73 +++++++++++++++++++++++++++++++++------- 1 file changed, 61 insertions(+), 12 deletions(-) diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json index 7b1c9e1126..4c03a7019d 100644 --- a/src/i18n/strings/uk.json +++ b/src/i18n/strings/uk.json @@ -23,8 +23,8 @@ "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "Текстове повідомлення було надіслано +%(msisdn)s. Введіть, будь ласка, код підтвердження з цього повідомлення", "Accept": "Прийняти", "Account": "Обліковка", - "%(targetName)s accepted an invitation.": "%(targetName)s прийняв/ла запрошення.", - "%(targetName)s accepted the invitation for %(displayName)s.": "Запрошення від %(displayName)s прийнято %(targetName)s.", + "%(targetName)s accepted an invitation.": "%(targetName)s приймає запрошення.", + "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s приймає запрошення від %(displayName)s.", "Access Token:": "Токен доступу:", "Active call (%(roomName)s)": "Активний виклик (%(roomName)s)", "Add": "Додати", @@ -71,7 +71,7 @@ "%(senderName)s banned %(targetName)s.": "%(senderName)s заблокував/ла %(targetName)s.", "Ban": "Заблокувати", "Banned users": "Заблоковані користувачі", - "Bans user with given id": "Блокує користувача з вказаним ID", + "Bans user with given id": "Блокує користувача з вказаним ідентифікатором", "Blacklisted": "В чорному списку", "Bulk Options": "Групові параметри", "Call Timeout": "Час очікування виклика", @@ -389,15 +389,15 @@ "Changes colour scheme of current room": "Змінює кольорову схему кімнати", "Sets the room topic": "Встановлює тему кімнати", "Invites user with given id to current room": "Запрошує користувача з вказаним ідентифікатором до кімнати", - "Joins room with given alias": "Приєднується до кімнати з поданим ідентифікатором", + "Joins room with given alias": "Приєднується до кімнати під іншим псевдонімом", "Leave room": "Покинути кімнату", - "Unrecognised room alias:": "Кімнату не знайдено:", - "Kicks user with given id": "Викинути з кімнати користувача з вказаним ідентифікатором", + "Unrecognised room alias:": "Не розпізнано псевдонім кімнати:", + "Kicks user with given id": "Вилучити з кімнати користувача з вказаним ідентифікатором", "Unbans user with given id": "Розблоковує користувача з вказаним ідентифікатором", - "Ignores a user, hiding their messages from you": "Ігнорувати користувача (приховує повідомлення від них)", + "Ignores a user, hiding their messages from you": "Ігнорує користувача, приховуючи повідомлення від них", "Ignored user": "Користувача ігноровано", "You are now ignoring %(userId)s": "Ви ігноруєте %(userId)s", - "Stops ignoring a user, showing their messages going forward": "Припинити ігнорувати користувача (показує їхні повідомлення від цього моменту)", + "Stops ignoring a user, showing their messages going forward": "Припиняє ігнорувати користувача, від цього моменту показуючи їхні повідомлення", "Unignored user": "Припинено ігнорування користувача", "You are no longer ignoring %(userId)s": "Ви більше не ігноруєте %(userId)s", "Define the power level of a user": "Вказати рівень прав користувача", @@ -407,9 +407,9 @@ "Unknown (user, device) pair:": "Невідома комбінація користувача і пристрою:", "Device already verified!": "Пристрій вже перевірено!", "WARNING: Device already verified, but keys do NOT MATCH!": "УВАГА: Пристрій уже перевірено, але ключі НЕ ЗБІГАЮТЬСЯ!", - "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "УВАГА: КЛЮЧ НЕ ПРОЙШОВ ПЕРЕВІРКУ! Підписний ключ %(userId)s на пристрої %(deviceId)s — це «%(fprint)s», і він не збігається з наданим ключем «%(fingerprint)s». Це може означати, що ваші повідомлення перехоплюють!", - "Verified key": "Ключ перевірено", - "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": "Підписний ключ, який ви вказали, збігається з підписним ключем, отриманим від пристрою %(deviceId)s користувача %(userId)s. Пристрій позначено як перевірений.", + "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "УВАГА: КЛЮЧ НЕ ПРОЙШОВ ПЕРЕВІРКУ! Ключ підпису %(userId)s на пристрої %(deviceId)s — це «%(fprint)s», і він не збігається з наданим ключем «%(fingerprint)s». Це може означати, що ваші повідомлення перехоплюють!", + "Verified key": "Перевірений ключ", + "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": "Ключ підпису, який ви вказали, збігається з ключем підпису, отриманим від пристрою %(deviceId)s користувача %(userId)s. Пристрій позначено як перевірений.", "Displays action": "Показує дію", "Unrecognised command:": "Невідома команда:", "Reason": "Причина", @@ -579,5 +579,54 @@ "A conference call could not be started because the integrations server is not available": "Конференц-дзвінок не можна розпочати оскільки інтеграційний сервер недоступний", "The file '%(fileName)s' failed to upload.": "Файл '%(fileName)s' не вийшло відвантажити.", "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Файл '%(fileName)s' перевищує ліміт розміру для відвантажень домашнього сервера", - "The server does not support the room version specified.": "Сервер не підтримує вказану версію кімнати." + "The server does not support the room version specified.": "Сервер не підтримує вказану версію кімнати.", + "Add Email Address": "Додати адресу е-пошти", + "Add Phone Number": "Додати номер телефону", + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Чи використовуєте ви «хлібні крихти» (аватари на списком кімнат)", + "Call failed due to misconfigured server": "Виклик не вдався через неправильне налаштування сервера", + "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Запропонуйте адміністратору вашого домашнього серверу (%(homeserverDomain)s) налаштувати сервер TURN для надійної роботи викликів.", + "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Також ви можете спробувати використати публічний сервер turn.matrix.org, але це буде не настільки надійно, а також цей сервер матиме змогу бачити вашу IP-адресу. Ви можете керувати цим у налаштуваннях.", + "Try using turn.matrix.org": "Спробуйте використати turn.matrix.org", + "Replying With Files": "Відповісти файлами", + "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Зараз неможливо відповісти файлом. Хочете завантажити цей файл без відповіді?", + "Name or Matrix ID": "Імʼя або Matrix ID", + "Identity server has no terms of service": "Сервер ідентифікації не має умов надання послуг", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Щоб підтвердити адресу е-пошту або телефон ця дія потребує доступу до типового серверу ідентифікації , але сервер не має жодних умов надання послуг.", + "Only continue if you trust the owner of the server.": "Продовжуйте тільки якщо довіряєте власнику сервера.", + "Trust": "Довіра", + "Unable to load! Check your network connectivity and try again.": "Завантаження неможливе! Перевірте інтернет-зʼєднання та спробуйте ще.", + "Email, name or Matrix ID": "Е-пошта, імʼя або Matrix ID", + "Failed to start chat": "Не вдалося розпочати чат", + "Failed to invite users to the room:": "Не вдалося запросити користувачів до кімнати:", + "Messages": "Повідомлення", + "Actions": "Дії", + "Other": "Інше", + "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Додає ¯\\_(ツ)_/¯ на початку текстового повідомлення", + "Sends a message as plain text, without interpreting it as markdown": "Надсилає повідомлення як чистий текст, не використовуючи markdown", + "Upgrades a room to a new version": "Покращує кімнату до нової версії", + "You do not have the required permissions to use this command.": "Вам бракує дозволу на використання цієї команди.", + "Room upgrade confirmation": "Підтвердження покращення кімнати", + "Upgrading a room can be destructive and isn't always necessary.": "Покращення кімнати може призвести до втрати даних та не є обовʼязковим.", + "Room upgrades are usually recommended when a room version is considered unstable. Unstable room versions might have bugs, missing features, or security vulnerabilities.": "Рекомендується покращувати кімнату, якщо поточна її версія вважається нестабільною. Нестабільні версії кімнат можуть мати вади, відсутні функції або вразливості безпеки.", + "Room upgrades usually only affect server-side processing of the room. If you're having problems with your Riot client, please file an issue with .": "Покращення кімнати загалом впливає лише на роботу з кімнатою на сервері. Якщо ви маєте проблему із вашим клієнтом Riot, надішліть свою проблему на .", + "Warning: Upgrading a room will not automatically migrate room members to the new version of the room. We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "Увага!: Покращення кімнати не перенесе автоматично усіх учасників до нової версії кімнати. Ми опублікуємо посилання на нову кімнату у старій версії кімнати, а учасники мають власноруч клацнути це посилання, щоб приєднатися до нової кімнати.", + "Please confirm that you'd like to go forward with upgrading this room from to .": "Підтвердьте, що ви згодні продовжити покращення цієї кімнати з до .", + "Upgrade": "Покращення", + "Changes your display nickname in the current room only": "Змінює ваше псевдо тільки для поточної кімнати", + "Changes the avatar of the current room": "Змінює аватар поточної кімнати", + "Changes your avatar in this current room only": "Змінює ваш аватар для поточної кімнати", + "Changes your avatar in all rooms": "Змінює ваш аватар для усіх кімнат", + "Gets or sets the room topic": "Показує чи встановлює тему кімнати", + "This room has no topic.": "Ця кімната не має теми.", + "Sets the room name": "Встановлює назву кімнати", + "Use an identity server": "Використовувати сервер ідентифікації", + "Use an identity server to invite by email. Manage in Settings.": "Використовувати ідентифікаційний сервер для запрошення через е-пошту. Керування у настройках.", + "Unbans user with given ID": "Розблоковує користувача з вказаним ідентифікатором", + "Adds a custom widget by URL to the room": "Додає власний віджет до кімнати за посиланням", + "Please supply a https:// or http:// widget URL": "Вкажіть посилання на віджет — https:// або http://", + "You cannot modify widgets in this room.": "Ви не можете змінювати віджети у цій кімнаті.", + "Forces the current outbound group session in an encrypted room to be discarded": "Примусово відкидає поточний вихідний груповий сеанс у шифрованій кімнаті", + "Sends the given message coloured as a rainbow": "Надсилає вказане повідомлення розфарбоване веселкою", + "Your Riot is misconfigured": "Ваш Riot налаштовано неправильно", + "Join the discussion": "Приєднатися до обговорення" } From 61454bcf32d6d547bbcce8b3466778f87cf30c24 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 15 Nov 2019 10:25:58 -0700 Subject: [PATCH 153/334] Fix i18n after merge --- 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 f0ff0275f7..655c7030c4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1082,7 +1082,6 @@ "Failed to withdraw invitation": "Failed to withdraw invitation", "Failed to remove user from community": "Failed to remove user from community", "%(role)s in %(roomName)s": "%(role)s in %(roomName)s", - "Failed to deactivate user": "Failed to deactivate user", "This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.", "Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.", "Messages in this room are end-to-end encrypted.": "Messages in this room are end-to-end encrypted.", From 6b726a8e13786f6f10222cac3c1655f47c213354 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 15 Nov 2019 14:25:53 -0700 Subject: [PATCH 154/334] Implement the bulk of the new widget permission prompt design Part 1 of https://github.com/vector-im/riot-web/issues/11262 This is all the visual changes - the actual wiring of the UI to the right places is for another PR (though this PR still works independently). The help icon is known to be weird here - it's a bug in the svg we have. The tooltip also goes right instead of up because making the tooltip go up is not easy work for this PR - maybe a future one if we *really* want it to go up. --- res/css/_common.scss | 16 ++ res/css/views/rooms/_AppsDrawer.scss | 62 ++++--- .../views/elements/AppPermission.js | 152 +++++++++++------- src/components/views/elements/AppTile.js | 4 +- .../views/elements/TextWithTooltip.js | 4 +- src/i18n/strings/en_EN.json | 17 +- 6 files changed, 169 insertions(+), 86 deletions(-) diff --git a/res/css/_common.scss b/res/css/_common.scss index 70ab2457f1..5987275f7f 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -550,6 +550,22 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { color: $username-variant8-color; } +@define-mixin mx_Tooltip_dark { + box-shadow: none; + background-color: $tooltip-timeline-bg-color; + color: $tooltip-timeline-fg-color; + border: none; + border-radius: 3px; + padding: 6px 8px; +} + +// This is a workaround for our mixins not supporting child selectors +.mx_Tooltip_dark { + .mx_Tooltip_chevron::after { + border-right-color: $tooltip-timeline-bg-color; + } +} + @define-mixin mx_Settings_fullWidthField { margin-right: 100px; } diff --git a/res/css/views/rooms/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss index 9ca6954af7..6f5e3abade 100644 --- a/res/css/views/rooms/_AppsDrawer.scss +++ b/res/css/views/rooms/_AppsDrawer.scss @@ -294,49 +294,61 @@ form.mx_Custom_Widget_Form div { .mx_AppPermissionWarning { text-align: center; - background-color: $primary-bg-color; + background-color: $widget-menu-bar-bg-color; display: flex; height: 100%; flex-direction: column; justify-content: center; align-items: center; + font-size: 16px; } -.mx_AppPermissionWarningImage { - margin: 10px 0; +.mx_AppPermissionWarning_row { + margin-bottom: 12px; } -.mx_AppPermissionWarningImage img { - width: 100px; +.mx_AppPermissionWarning_smallText { + font-size: 12px; } -.mx_AppPermissionWarningText { - max-width: 90%; - margin: 10px auto 10px auto; - color: $primary-fg-color; +.mx_AppPermissionWarning_bolder { + font-weight: 600; } -.mx_AppPermissionWarningTextLabel { - font-weight: bold; - display: block; +.mx_AppPermissionWarning h4 { + margin: 0; + padding: 0; } -.mx_AppPermissionWarningTextURL { +.mx_AppPermissionWarning_helpIcon { + margin-top: 1px; + margin-right: 2px; + width: 10px; + height: 10px; display: inline-block; - max-width: 100%; - color: $accent-color; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; } -.mx_AppPermissionButton { - border: none; - padding: 5px 20px; - border-radius: 5px; - background-color: $button-bg-color; - color: $button-fg-color; - cursor: pointer; +.mx_AppPermissionWarning_helpIcon::before { + display: inline-block; + background-color: $accent-color; + mask-repeat: no-repeat; + mask-size: 12px; + width: 12px; + height: 12px; + mask-position: center; + content: ''; + vertical-align: middle; + mask-image: url('$(res)/img/feather-customised/help-circle.svg'); +} + +.mx_AppPermissionWarning_tooltip { + @mixin mx_Tooltip_dark; + + ul { + list-style-position: inside; + padding-left: 2px; + margin-left: 0; + } } .mx_AppLoading { diff --git a/src/components/views/elements/AppPermission.js b/src/components/views/elements/AppPermission.js index 1e019c0287..422427d4c4 100644 --- a/src/components/views/elements/AppPermission.js +++ b/src/components/views/elements/AppPermission.js @@ -19,79 +19,123 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import url from 'url'; +import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import WidgetUtils from "../../../utils/WidgetUtils"; +import MatrixClientPeg from "../../../MatrixClientPeg"; export default class AppPermission extends React.Component { + static propTypes = { + url: PropTypes.string.isRequired, + creatorUserId: PropTypes.string.isRequired, + roomId: PropTypes.string.isRequired, + onPermissionGranted: PropTypes.func.isRequired, + }; + + static defaultProps = { + onPermissionGranted: () => {}, + }; + constructor(props) { super(props); - const curlBase = this.getCurlBase(); - this.state = { curlBase: curlBase}; + // The first step is to pick apart the widget so we can render information about it + const urlInfo = this.parseWidgetUrl(); + + // The second step is to find the user's profile so we can show it on the prompt + const room = MatrixClientPeg.get().getRoom(this.props.roomId); + let roomMember; + if (room) roomMember = room.getMember(this.props.creatorUserId); + + // Set all this into the initial state + this.state = { + ...urlInfo, + roomMember, + }; } - // Return string representation of content URL without query parameters - getCurlBase() { - const wurl = url.parse(this.props.url); - let curl; - let curlString; + parseWidgetUrl() { + const widgetUrl = url.parse(this.props.url); + const params = new URLSearchParams(widgetUrl.search); - const searchParams = new URLSearchParams(wurl.search); - - if (WidgetUtils.isScalarUrl(wurl) && searchParams && searchParams.get('url')) { - curl = url.parse(searchParams.get('url')); - if (curl) { - curl.search = curl.query = ""; - curlString = curl.format(); - } + // HACK: We're relying on the query params when we should be relying on the widget's `data`. + // This is a workaround for Scalar. + if (WidgetUtils.isScalarUrl(widgetUrl) && params && params.get('url')) { + const unwrappedUrl = url.parse(params.get('url')); + return { + widgetDomain: unwrappedUrl.host || unwrappedUrl.hostname, + isWrapped: true, + }; + } else { + return { + widgetDomain: widgetUrl.host || widgetUrl.hostname, + isWrapped: false, + }; } - if (!curl && wurl) { - wurl.search = wurl.query = ""; - curlString = wurl.format(); - } - return curlString; } render() { - let e2eWarningText; - if (this.props.isRoomEncrypted) { - e2eWarningText = - { _t('NOTE: Apps are not end-to-end encrypted') }; - } - const cookieWarning = - - { _t('Warning: This widget might use cookies.') } - ; + const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton"); + const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar"); + const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar"); + const TextWithTooltip = sdk.getComponent("views.elements.TextWithTooltip"); + + const displayName = this.state.roomMember ? this.state.roomMember.name : this.props.creatorUserId; + const userId = displayName === this.props.creatorUserId ? null : this.props.creatorUserId; + + const avatar = this.state.roomMember + ? + : ; + + const warningTooltipText = ( +
    + {_t("Any of the following data may be shared:")} +
      +
    • {_t("Your display name")}
    • +
    • {_t("Your avatar URL")}
    • +
    • {_t("Your user ID")}
    • +
    • {_t("Your theme")}
    • +
    • {_t("Riot URL")}
    • +
    • {_t("Room ID")}
    • +
    • {_t("Widget ID")}
    • +
    +
    + ); + const warningTooltip = ( + + + + ); + + // Due to i18n limitations, we can't dedupe the code for variables in these two messages. + const warning = this.state.isWrapped + ? _t("Using this widget may share data with %(widgetDomain)s & your Integration Manager.", + {widgetDomain: this.state.widgetDomain}, {helpIcon: () => warningTooltip}) + : _t("Using this widget may share data with %(widgetDomain)s.", + {widgetDomain: this.state.widgetDomain}, {helpIcon: () => warningTooltip}); + return (
    -
    - {_t('Warning!')} +
    + {_t("Widget added by")}
    -
    - {_t('Do you want to load widget from URL:')} - {this.state.curlBase} - { e2eWarningText } - { cookieWarning } +
    + {avatar} +

    {displayName}

    +
    {userId}
    +
    +
    + {warning} +
    +
    + {_t("This widget may use cookies.")} +
    +
    + + {_t("Continue")} +
    -
    ); } } - -AppPermission.propTypes = { - isRoomEncrypted: PropTypes.bool, - url: PropTypes.string.isRequired, - onPermissionGranted: PropTypes.func.isRequired, -}; -AppPermission.defaultProps = { - isRoomEncrypted: false, - onPermissionGranted: function() {}, -}; diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 260b63dfd4..ffd9d73cca 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -569,11 +569,11 @@ export default class AppTile extends React.Component {
    ); if (!this.state.hasPermissionToLoad) { - const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId); appTileBody = (
    diff --git a/src/components/views/elements/TextWithTooltip.js b/src/components/views/elements/TextWithTooltip.js index 61c3a2125a..f6cef47117 100644 --- a/src/components/views/elements/TextWithTooltip.js +++ b/src/components/views/elements/TextWithTooltip.js @@ -21,7 +21,8 @@ import sdk from '../../../index'; export default class TextWithTooltip extends React.Component { static propTypes = { class: PropTypes.string, - tooltip: PropTypes.string.isRequired, + tooltipClass: PropTypes.string, + tooltip: PropTypes.node.isRequired, }; constructor() { @@ -49,6 +50,7 @@ export default class TextWithTooltip extends React.Component { ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 655c7030c4..37383b7e4e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1183,10 +1183,18 @@ "Quick Reactions": "Quick Reactions", "Cancel search": "Cancel search", "Unknown Address": "Unknown Address", - "NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted", - "Warning: This widget might use cookies.": "Warning: This widget might use cookies.", - "Do you want to load widget from URL:": "Do you want to load widget from URL:", - "Allow": "Allow", + "Any of the following data may be shared:": "Any of the following data may be shared:", + "Your display name": "Your display name", + "Your avatar URL": "Your avatar URL", + "Your user ID": "Your user ID", + "Your theme": "Your theme", + "Riot URL": "Riot URL", + "Room ID": "Room ID", + "Widget ID": "Widget ID", + "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "Using this widget may share data with %(widgetDomain)s & your Integration Manager.", + "Using this widget may share data with %(widgetDomain)s.": "Using this widget may share data with %(widgetDomain)s.", + "Widget added by": "Widget added by", + "This widget may use cookies.": "This widget may use cookies.", "Delete Widget": "Delete Widget", "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?", "Delete widget": "Delete widget", @@ -1494,6 +1502,7 @@ "A widget would like to verify your identity": "A widget would like to verify your identity", "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.", "Remember my selection for this widget": "Remember my selection for this widget", + "Allow": "Allow", "Deny": "Deny", "Unable to load backup status": "Unable to load backup status", "Recovery Key Mismatch": "Recovery Key Mismatch", From d56ae702873a8a655e0d8aff0f5327f60b15f204 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Sat, 16 Nov 2019 08:37:59 +0000 Subject: [PATCH 155/334] Translated using Weblate (Albanian) Currently translated at 99.8% (1904 of 1908 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 2efe4ddd68..2bf5732131 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -2280,5 +2280,17 @@ "If this isn't what you want, please use a different tool to ignore users.": "Nëse kjo s’është ajo çka doni, ju lutemi, përdorni një tjetër mjet për të shpërfillur përdorues.", "Room ID or alias of ban list": "ID dhome ose alias e listës së dëbimeve", "Subscribe": "Pajtohuni", - "You have ignored this user, so their message is hidden. Show anyways.": "E keni shpërfillur këtë përdorues, ndaj mesazhi i tij është fshehur. Shfaqe, sido qoftë." + "You have ignored this user, so their message is hidden. Show anyways.": "E keni shpërfillur këtë përdorues, ndaj mesazhi i tij është fshehur. Shfaqe, sido qoftë.", + "Custom (%(level)s)": "Vetjak (%(level)s)", + "Trusted": "E besuar", + "Not trusted": "Jo e besuar", + "Hide verified Sign-In's": "Fshihi Hyrjet e verifikuara", + "%(count)s verified Sign-In's|other": "%(count)s Hyrje të verifikuara", + "%(count)s verified Sign-In's|one": "1 Hyrje e verifikuar", + "Direct message": "Mesazh i Drejtpërdrejtë", + "Unverify user": "Hiqi verifikimin përdoruesit", + "%(role)s in %(roomName)s": "%(role)s në %(roomName)s", + "Messages in this room are end-to-end encrypted.": "Mesazhet në këtë dhomë janë të fshehtëzuara skaj-më-skaj.", + "Security": "Siguri", + "Verify": "Verifikoje" } From a9ef6bde6333e425c50cb4bf6ca60cd5b1050e36 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Sat, 16 Nov 2019 13:09:01 +0000 Subject: [PATCH 156/334] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1908 of 1908 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index a4898bd328..5c6e69c864 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2318,5 +2318,17 @@ "If this isn't what you want, please use a different tool to ignore users.": "如果這不是您想要的,請使用不同的工具來忽略使用者。", "Room ID or alias of ban list": "聊天室 ID 或封鎖清單的別名", "Subscribe": "訂閱", - "You have ignored this user, so their message is hidden. Show anyways.": "您已經忽略了這個使用者,所以他們的訊息會隱藏。無論如何都顯示。" + "You have ignored this user, so their message is hidden. Show anyways.": "您已經忽略了這個使用者,所以他們的訊息會隱藏。無論如何都顯示。", + "Custom (%(level)s)": "自訂 (%(level)s)", + "Trusted": "已信任", + "Not trusted": "不信任", + "Hide verified Sign-In's": "隱藏已驗證的登入", + "%(count)s verified Sign-In's|other": "%(count)s 個已驗證的登入", + "%(count)s verified Sign-In's|one": "1 個已驗證的登入", + "Direct message": "直接訊息", + "Unverify user": "未驗證的使用者", + "%(role)s in %(roomName)s": "%(role)s 在 %(roomName)s", + "Messages in this room are end-to-end encrypted.": "在此聊天室中的訊息為端到端加密。", + "Security": "安全", + "Verify": "驗證" } From 78c36e593650ec6fac3b592d7aea5647603b04ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Fri, 15 Nov 2019 21:17:15 +0000 Subject: [PATCH 157/334] Translated using Weblate (French) Currently translated at 100.0% (1908 of 1908 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index f4e889d955..eef9438761 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2325,5 +2325,17 @@ "If this isn't what you want, please use a different tool to ignore users.": "Si ce n’est pas ce que vous voulez, utilisez un autre outil pour ignorer les utilisateurs.", "Room ID or alias of ban list": "Identifiant ou alias du salon de la liste de bannissement", "Subscribe": "S’inscrire", - "You have ignored this user, so their message is hidden. Show anyways.": "Vous avez ignoré cet utilisateur, donc ses messages sont cachés. Les montrer quand même." + "You have ignored this user, so their message is hidden. Show anyways.": "Vous avez ignoré cet utilisateur, donc ses messages sont cachés. Les montrer quand même.", + "Custom (%(level)s)": "Personnalisé (%(level)s)", + "Trusted": "Fiable", + "Not trusted": "Non vérifié", + "Hide verified Sign-In's": "Masquer les connexions vérifiées", + "%(count)s verified Sign-In's|other": "%(count)s connexions vérifiées", + "%(count)s verified Sign-In's|one": "1 connexion vérifiée", + "Direct message": "Message direct", + "Unverify user": "Ne plus marquer l’utilisateur comme vérifié", + "%(role)s in %(roomName)s": "%(role)s dans %(roomName)s", + "Messages in this room are end-to-end encrypted.": "Les messages dans ce salon sont chiffrés de bout en bout.", + "Security": "Sécurité", + "Verify": "Vérifier" } From 16a107a1634c9f3071810172f57c74528789d49b Mon Sep 17 00:00:00 2001 From: Szimszon Date: Sat, 16 Nov 2019 09:23:56 +0000 Subject: [PATCH 158/334] Translated using Weblate (Hungarian) Currently translated at 100.0% (1908 of 1908 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 29747bb1b6..3c049cc321 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2312,5 +2312,17 @@ "If this isn't what you want, please use a different tool to ignore users.": "Ha nem ez az amit szeretnél, kérlek használj más eszközt a felhasználók figyelmen kívül hagyásához.", "Room ID or alias of ban list": "Tiltó lista szoba azonosítója vagy alternatív neve", "Subscribe": "Feliratkozás", - "You have ignored this user, so their message is hidden. Show anyways.": "Ezt a felhasználót figyelmen kívül hagyod, így az üzenetei el lesznek rejtve. Mindenképpen megmutat." + "You have ignored this user, so their message is hidden. Show anyways.": "Ezt a felhasználót figyelmen kívül hagyod, így az üzenetei el lesznek rejtve. Mindenképpen megmutat.", + "Custom (%(level)s)": "Egyedi (%(level)s)", + "Trusted": "Megbízható", + "Not trusted": "Megbízhatatlan", + "Hide verified Sign-In's": "Ellenőrzött belépések elrejtése", + "%(count)s verified Sign-In's|other": "%(count)s ellenőrzött belépés", + "%(count)s verified Sign-In's|one": "1 ellenőrzött belépés", + "Direct message": "Közvetlen beszélgetés", + "Unverify user": "Ellenőrizetlen felhasználó", + "%(role)s in %(roomName)s": "%(role)s a szobában: %(roomName)s", + "Messages in this room are end-to-end encrypted.": "Az üzenetek a szobában végponttól végpontig titkosítottak.", + "Security": "Biztonság", + "Verify": "Ellenőriz" } From 20dd731ed017439929a4f8908e004bcd11a0e1e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=ED=83=9C=EC=84=AD?= Date: Sat, 16 Nov 2019 04:00:39 +0000 Subject: [PATCH 159/334] Translated using Weblate (Korean) Currently translated at 100.0% (1908 of 1908 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ko/ --- src/i18n/strings/ko.json | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index fe8c929acd..8d34fab025 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -2158,5 +2158,28 @@ "View rules": "규칙 보기", "You are currently subscribed to:": "현재 구독 중임:", "⚠ These settings are meant for advanced users.": "⚠ 이 설정은 고급 사용자를 위한 것입니다.", - "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "무시하고 싶은 사용자와 서버를 여기에 추가하세요. 별표(*)를 사용해서 Riot이 이름과 문자를 맞춰볼 수 있습니다. 예를 들어, @bot:*이라면 모든 서버에서 'bot'이라는 문자를 가진 이름의 모든 사용자를 무시합니다." + "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "무시하고 싶은 사용자와 서버를 여기에 추가하세요. 별표(*)를 사용해서 Riot이 이름과 문자를 맞춰볼 수 있습니다. 예를 들어, @bot:*이라면 모든 서버에서 'bot'이라는 문자를 가진 이름의 모든 사용자를 무시합니다.", + "Custom (%(level)s)": "맞춤 (%(level)s)", + "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "차단당하는 사람은 규칙에 따라 차단 목록을 통해 무시됩니다. 차단 목록을 구독하면 그 목록에서 차단당한 사용자/서버를 당신으로부터 감추게됩니다.", + "Personal ban list": "개인 차단 목록", + "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "개인 차단 목록은 개인적으로 보고 싶지 않은 메시지를 보낸 모든 사용자/서버를 담고 있습니다. 처음 사용자/서버를 무시했다면, 방 목록에 '나의 차단 목록'이라는 이름의 새 방이 나타납니다 - 차단 목록의 효력을 유지하려면 이 방을 그대로 두세요.", + "Server or user ID to ignore": "무시할 서버 또는 사용자 ID", + "eg: @bot:* or example.org": "예: @bot:* 또는 example.org", + "Subscribed lists": "구독 목록", + "Subscribing to a ban list will cause you to join it!": "차단 목록을 구독하면 차단 목록에 참여하게 됩니다!", + "If this isn't what you want, please use a different tool to ignore users.": "이것을 원한 것이 아니라면, 사용자를 무시하는 다른 도구를 사용해주세요.", + "Room ID or alias of ban list": "방 ID 또는 차단 목록의 별칭", + "Subscribe": "구독", + "Trusted": "신뢰함", + "Not trusted": "신뢰하지 않음", + "Hide verified Sign-In's": "확인 로그인 숨기기", + "%(count)s verified Sign-In's|other": "확인된 %(count)s개의 로그인", + "%(count)s verified Sign-In's|one": "확인된 1개의 로그인", + "Direct message": "다이렉트 메시지", + "Unverify user": "사용자 확인 취소", + "%(role)s in %(roomName)s": "%(roomName)s 방의 %(role)s", + "Messages in this room are end-to-end encrypted.": "이 방의 메시지는 종단간 암호화되었습니다.", + "Security": "보안", + "Verify": "확인", + "You have ignored this user, so their message is hidden. Show anyways.": "이 사용자를 무시했습니다. 사용자의 메시지는 숨겨집니다. 무시하고 보이기." } From 5d3a84db855bb8c3e89d8759b35c6feefa06eb47 Mon Sep 17 00:00:00 2001 From: Nobbele Date: Sun, 17 Nov 2019 11:58:39 +0000 Subject: [PATCH 160/334] Translated using Weblate (Swedish) Currently translated at 76.4% (1457 of 1908 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sv/ --- src/i18n/strings/sv.json | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index 6d611be464..a1c31faf57 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -757,7 +757,7 @@ "Device Name": "Enhetsnamn", "Select devices": "Välj enheter", "Disable Emoji suggestions while typing": "Inaktivera Emoji-förslag medan du skriver", - "Use compact timeline layout": "Använd kompakt chattlayout", + "Use compact timeline layout": "Använd kompakt tidslinjelayout", "Not a valid Riot keyfile": "Inte en giltig Riot-nyckelfil", "Authentication check failed: incorrect password?": "Autentiseringskontroll misslyckades: felaktigt lösenord?", "Always show encryption icons": "Visa alltid krypteringsikoner", @@ -1019,7 +1019,7 @@ "Add rooms to the community": "Lägg till rum i communityn", "Add to community": "Lägg till i community", "Failed to invite users to community": "Det gick inte att bjuda in användare till communityn", - "Mirror local video feed": "Spegelvänd lokal video", + "Mirror local video feed": "Speglad lokal-video", "Disable Community Filter Panel": "Inaktivera community-filterpanel", "Community Invites": "Community-inbjudningar", "Invalid community ID": "Ogiltigt community-ID", @@ -1353,9 +1353,9 @@ "Show read receipts sent by other users": "Visa läskvitton som skickats av andra användare", "Show avatars in user and room mentions": "Visa avatarer i användar- och rumsnämningar", "Enable big emoji in chat": "Aktivera stor emoji i chatt", - "Send typing notifications": "Skicka \"skriver\"-status", + "Send typing notifications": "Skicka \"skriver\"-statusar", "Enable Community Filter Panel": "Aktivera community-filterpanel", - "Allow Peer-to-Peer for 1:1 calls": "Tillåt enhet-till-enhet-kommunikation för direktsamtal (mellan två personer)", + "Allow Peer-to-Peer for 1:1 calls": "Tillåt peer-to-peer kommunikation för 1:1 samtal", "Messages containing my username": "Meddelanden som innehåller mitt användarnamn", "Messages containing @room": "Meddelanden som innehåller @room", "Encrypted messages in one-to-one chats": "Krypterade meddelanden i privata chattar", @@ -1564,7 +1564,7 @@ "Please supply a https:// or http:// widget URL": "Ange en widget-URL med https:// eller http://", "You cannot modify widgets in this room.": "Du kan inte ändra widgets i detta rum.", "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s återkallade inbjudan för %(targetDisplayName)s att gå med i rummet.", - "Show a reminder to enable Secure Message Recovery in encrypted rooms": "", + "Show a reminder to enable Secure Message Recovery in encrypted rooms": "Visa en påminnelse för att sätta på säker meddelande återhämtning i krypterade rum", "The other party cancelled the verification.": "Den andra parten avbröt verifieringen.", "Verified!": "Verifierad!", "You've successfully verified this user.": "Du har verifierat den här användaren.", @@ -1817,5 +1817,11 @@ "Add Phone Number": "Lägg till telefonnummer", "Identity server has no terms of service": "Identitetsserver har inga användarvillkor", "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Den här åtgärden kräver åtkomst till standardidentitetsservern för att validera en e-postadress eller telefonnummer, men servern har inga användarvillkor.", - "Trust": "Förtroende" + "Trust": "Förtroende", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "Use the new, consistent UserInfo panel for Room Members and Group Members": "Använd den nya, konsistenta UserInfo panelen för rum medlemmar och grupp medlemmar", + "Try out new ways to ignore people (experimental)": "Testa nya sätt att ignorera personer (experimentalt)", + "Send verification requests in direct message": "Skicka verifikations begäran i direkt meddelanden", + "Use the new, faster, composer for writing messages": "Använd den nya, snabbare kompositören för att skriva meddelanden", + "Show previews/thumbnails for images": "Visa förhandsvisning/tumnagel för bilder" } From 47948812b083f903b4be8d4e249f61e229ff9cb9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 18 Nov 2019 09:47:37 +0000 Subject: [PATCH 161/334] Attempt number two at ripping out Bluebird from rageshake.js --- src/rageshake/rageshake.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/rageshake/rageshake.js b/src/rageshake/rageshake.js index d61956c925..820550af88 100644 --- a/src/rageshake/rageshake.js +++ b/src/rageshake/rageshake.js @@ -136,6 +136,8 @@ class IndexedDBLogStore { this.id = "instance-" + Math.random() + Date.now(); this.index = 0; this.db = null; + + // these promises are cleared as soon as fulfilled this.flushPromise = null; // set if flush() is called whilst one is ongoing this.flushAgainPromise = null; @@ -208,16 +210,16 @@ class IndexedDBLogStore { */ flush() { // check if a flush() operation is ongoing - if (this.flushPromise && this.flushPromise.isPending()) { - if (this.flushAgainPromise && this.flushAgainPromise.isPending()) { - // this is the 3rd+ time we've called flush() : return the same - // promise. + if (this.flushPromise) { + if (this.flushAgainPromise) { + // this is the 3rd+ time we've called flush() : return the same promise. return this.flushAgainPromise; } - // queue up a flush to occur immediately after the pending one - // completes. + // queue up a flush to occur immediately after the pending one completes. this.flushAgainPromise = this.flushPromise.then(() => { return this.flush(); + }).then(() => { + this.flushAgainPromise = null; }); return this.flushAgainPromise; } @@ -225,8 +227,7 @@ class IndexedDBLogStore { // a brand new one, destroying the chain which may have been built up. this.flushPromise = new Promise((resolve, reject) => { if (!this.db) { - // not connected yet or user rejected access for us to r/w to - // the db. + // not connected yet or user rejected access for us to r/w to the db. reject(new Error("No connected database")); return; } @@ -251,6 +252,8 @@ class IndexedDBLogStore { objStore.add(this._generateLogEntry(lines)); const lastModStore = txn.objectStore("logslastmod"); lastModStore.put(this._generateLastModifiedTime()); + }).then(() => { + this.flushPromise = null; }); return this.flushPromise; } From 30d4dd36a7d2086123b52d95123cbd3e43122754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 10:05:11 +0100 Subject: [PATCH 162/334] BaseEventIndexManager: Remove the flow annotation. --- src/BaseEventIndexManager.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/BaseEventIndexManager.js b/src/BaseEventIndexManager.js index fe59cee673..c5a3273a45 100644 --- a/src/BaseEventIndexManager.js +++ b/src/BaseEventIndexManager.js @@ -1,5 +1,3 @@ -// @flow - /* Copyright 2019 New Vector Ltd From ab93745460501c13ec9f68fe118fbaa9f2c06480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 10:16:29 +0100 Subject: [PATCH 163/334] Fix the copyright headers from New Vector to The Matrix Foundation. --- src/BaseEventIndexManager.js | 2 +- src/EventIndexPeg.js | 2 +- src/EventIndexing.js | 2 +- src/Searching.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/BaseEventIndexManager.js b/src/BaseEventIndexManager.js index c5a3273a45..7cefb023d1 100644 --- a/src/BaseEventIndexManager.js +++ b/src/BaseEventIndexManager.js @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019 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. diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index da5c5425e4..54d9c40079 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019 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. diff --git a/src/EventIndexing.js b/src/EventIndexing.js index 37167cf600..f2c3c5c433 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019 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. diff --git a/src/Searching.js b/src/Searching.js index ee46a66fb8..cb641ec72a 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019 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. From 9fa8e8238a8cc1e406ed2e6ef471bfb452d707c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 10:18:09 +0100 Subject: [PATCH 164/334] BaseEventIndexManager: Fix a typo. --- src/BaseEventIndexManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BaseEventIndexManager.js b/src/BaseEventIndexManager.js index 7cefb023d1..61c556a0ff 100644 --- a/src/BaseEventIndexManager.js +++ b/src/BaseEventIndexManager.js @@ -76,7 +76,7 @@ export default class BaseEventIndexManager { /** * Does our EventIndexManager support event indexing. * - * If an EventIndexManager imlpementor has runtime dependencies that + * If an EventIndexManager implementor has runtime dependencies that * optionally enable event indexing they may override this method to perform * the necessary runtime checks here. * From 5149164010f1237e9e07e3a1a03b5cc0738e9b64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 10:23:04 +0100 Subject: [PATCH 165/334] MatrixChat: Revert the unnecessary changes in the MatrixChat class. --- src/components/structures/MatrixChat.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index f78bb5c168..b45884e64f 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1222,7 +1222,7 @@ export default createReactClass({ /** * Called when the session is logged out */ - _onLoggedOut: async function() { + _onLoggedOut: function() { this.notifyNewScreen('login'); this.setStateForNewView({ view: VIEWS.LOGIN, @@ -1272,9 +1272,8 @@ export default createReactClass({ // particularly noticeable when there are lots of 'limited' /sync responses // such as when laptops unsleep. // https://github.com/vector-im/riot-web/issues/3307#issuecomment-282895568 - cli.setCanResetTimelineCallback(async function(roomId) { + cli.setCanResetTimelineCallback(function(roomId) { console.log("Request to reset timeline in room ", roomId, " viewing:", self.state.currentRoomId); - if (roomId !== self.state.currentRoomId) { // It is safe to remove events from rooms we are not viewing. return true; From 910c3ac08db4bbdf8097e998cb486e6cdfef1a46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 10:26:17 +0100 Subject: [PATCH 166/334] BaseEventIndexManager: Fix some type annotations. --- src/BaseEventIndexManager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/BaseEventIndexManager.js b/src/BaseEventIndexManager.js index 61c556a0ff..5e8ca668ad 100644 --- a/src/BaseEventIndexManager.js +++ b/src/BaseEventIndexManager.js @@ -159,8 +159,8 @@ export default class BaseEventIndexManager { */ async addHistoricEvents( events: [HistoricEvent], - checkpoint: CrawlerCheckpoint | null = null, - oldCheckpoint: CrawlerCheckpoint | null = null, + checkpoint: CrawlerCheckpoint | null, + oldCheckpoint: CrawlerCheckpoint | null, ): Promise { throw new Error("Unimplemented"); } From ddb536e94a69485360611458cf70341720a3f604 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 10:27:10 +0100 Subject: [PATCH 167/334] EventIndexPeg: Move a docstring to the correct place. --- src/EventIndexPeg.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index 54d9c40079..4d0e518ab8 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -30,7 +30,8 @@ class EventIndexPeg { this.index = null; } - /** Create a new EventIndex and initialize it if the platform supports it. + /** + * Create a new EventIndex and initialize it if the platform supports it. * * @return {Promise} A promise that will resolve to true if an * EventIndex was successfully initialized, false otherwise. From 050e52ce461de709c01b1697379e6f8ad7fac18a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 10:34:48 +0100 Subject: [PATCH 168/334] EventIndexPeg: Treat both cases of unavailable platform support the same. --- src/EventIndexPeg.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index 4d0e518ab8..f1841b3f2b 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -38,9 +38,7 @@ class EventIndexPeg { */ async init() { const indexManager = PlatformPeg.get().getEventIndexingManager(); - if (indexManager === null) return false; - - if (await indexManager.supportsEventIndexing() !== true) { + if (!indexManager || await indexManager.supportsEventIndexing() !== true) { console.log("EventIndex: Platform doesn't support event indexing,", "not initializing."); return false; From 3b06c684d23fd4e5cd012ccc197926b612fef63b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 10:35:57 +0100 Subject: [PATCH 169/334] EventIndexing: Don't capitalize homeserver. --- src/EventIndexing.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/EventIndexing.js b/src/EventIndexing.js index f2c3c5c433..05d5fd03da 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -59,8 +59,8 @@ export default class EventIndexer { return client.isRoomEncrypted(room.roomId); }; - // We only care to crawl the encrypted rooms, non-encrytped - // rooms can use the search provided by the Homeserver. + // We only care to crawl the encrypted rooms, non-encrypted. + // rooms can use the search provided by the homeserver. const encryptedRooms = rooms.filter(isRoomEncrypted); console.log("EventIndex: Adding initial crawler checkpoints"); @@ -189,7 +189,7 @@ export default class EventIndexer { while (!cancelled) { // This is a low priority task and we don't want to spam our - // Homeserver with /messages requests so we set a hefty timeout + // homeserver with /messages requests so we set a hefty timeout // here. await sleep(this._crawlerTimeout); @@ -210,7 +210,7 @@ export default class EventIndexer { console.log("EventIndex: crawling using checkpoint", checkpoint); // We have a checkpoint, let us fetch some messages, again, very - // conservatively to not bother our Homeserver too much. + // conservatively to not bother our homeserver too much. const eventMapper = client.getEventMapper(); // TODO we need to ensure to use member lazy loading with this // request so we get the correct profiles. From b4a6123295c896b49952302e4ced6ce166034419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 10:48:18 +0100 Subject: [PATCH 170/334] Searching: Move a comment to the correct place. --- src/Searching.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Searching.js b/src/Searching.js index cb641ec72a..4e6c8b9b4d 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -20,8 +20,9 @@ import MatrixClientPeg from "./MatrixClientPeg"; function serverSideSearch(term, roomId = undefined) { 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 = { - // XXX: it's unintuitive that the filter for searching doesn't have the same shape as the v2 filter API :( rooms: [roomId], }; } From a4ad8151f8415bf76bd1fd16b64ee167cc1bec0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 10:55:02 +0100 Subject: [PATCH 171/334] Searching: Use the short form to build the search arguments object. --- src/Searching.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Searching.js b/src/Searching.js index 4e6c8b9b4d..eb7137e221 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -28,8 +28,8 @@ function serverSideSearch(term, roomId = undefined) { } const searchPromise = MatrixClientPeg.get().searchRoomEvents({ - filter: filter, - term: term, + filter, + term, }); return searchPromise; From 0e3a0008df387bb036867177bb92451702f3fff4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 10:56:57 +0100 Subject: [PATCH 172/334] Searching: Remove the func suffix from our search functions. --- src/Searching.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Searching.js b/src/Searching.js index eb7137e221..601da56f86 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -35,11 +35,11 @@ function serverSideSearch(term, roomId = undefined) { return searchPromise; } -async function combinedSearchFunc(searchTerm) { +async function combinedSearch(searchTerm) { // Create two promises, one for the local search, one for the // server-side search. const serverSidePromise = serverSideSearch(searchTerm); - const localPromise = localSearchFunc(searchTerm); + const localPromise = localSearch(searchTerm); // Wait for both promises to resolve. await Promise.all([serverSidePromise, localPromise]); @@ -74,7 +74,7 @@ async function combinedSearchFunc(searchTerm) { return result; } -async function localSearchFunc(searchTerm, roomId = undefined) { +async function localSearch(searchTerm, roomId = undefined) { const searchArgs = { search_term: searchTerm, before_limit: 1, @@ -115,7 +115,7 @@ function eventIndexSearch(term, roomId = undefined) { if (MatrixClientPeg.get().isRoomEncrypted(roomId)) { // The search is for a single encrypted room, use our local // search method. - searchPromise = localSearchFunc(term, roomId); + searchPromise = localSearch(term, roomId); } else { // The search is for a single non-encrypted room, use the // server-side search. @@ -124,7 +124,7 @@ function eventIndexSearch(term, roomId = undefined) { } else { // Search across all rooms, combine a server side search and a // local search. - searchPromise = combinedSearchFunc(term); + searchPromise = combinedSearch(term); } return searchPromise; From 2bb331cdf0c635728d2a08f993e2cb186a89e381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 10:57:23 +0100 Subject: [PATCH 173/334] Searching: Fix a typo. --- src/Searching.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Searching.js b/src/Searching.js index 601da56f86..ca3e7f041f 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -52,7 +52,7 @@ async function combinedSearch(searchTerm) { const result = {}; // Our localResult and serverSideResult are both ordered by - // recency separetly, when we combine them the order might not + // 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; From d4d51dc61f75bcaab50184e066da23fc5cabfbdc Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 18 Nov 2019 10:03:05 +0000 Subject: [PATCH 174/334] Rip out the remainder of Bluebird --- .babelrc | 1 - package.json | 2 -- src/ContentMessages.js | 1 - src/Lifecycle.js | 5 ++--- src/Modal.js | 1 - src/Notifier.js | 2 +- src/Resend.js | 2 +- src/RoomNotifs.js | 1 - src/Rooms.js | 1 - src/ScalarAuthClient.js | 1 - src/ScalarMessaging.js | 8 ++++---- src/SlashCommands.js | 1 - src/Terms.js | 1 - src/ToWidgetPostMessageApi.js | 2 -- src/VectorConferenceHandler.js | 1 - src/autocomplete/Autocompleter.js | 1 - src/components/structures/GroupView.js | 5 ++--- src/components/structures/InteractiveAuth.js | 2 +- src/components/structures/MatrixChat.js | 10 ++++------ src/components/structures/MyGroups.js | 2 +- src/components/structures/RoomDirectory.js | 9 ++++----- src/components/structures/RoomView.js | 9 ++++----- src/components/structures/ScrollPanel.js | 1 - src/components/structures/TimelinePanel.js | 3 +-- src/components/structures/auth/ForgotPassword.js | 2 +- src/components/structures/auth/Login.js | 4 ++-- .../structures/auth/PostRegistration.js | 2 +- src/components/structures/auth/Registration.js | 3 +-- .../views/auth/InteractiveAuthEntryComponents.js | 2 +- .../views/context_menus/RoomTileContextMenu.js | 4 ++-- .../views/dialogs/AddressPickerDialog.js | 4 ++-- .../views/dialogs/CreateGroupDialog.js | 2 +- src/components/views/dialogs/KeyShareDialog.js | 2 +- src/components/views/dialogs/SetEmailDialog.js | 4 ++-- src/components/views/dialogs/SetMxIdDialog.js | 1 - src/components/views/elements/AppTile.js | 2 +- .../views/elements/EditableTextContainer.js | 5 ++--- src/components/views/elements/ErrorBoundary.js | 2 +- src/components/views/elements/ImageView.js | 2 +- .../views/elements/LanguageDropdown.js | 2 +- src/components/views/groups/GroupUserSettings.js | 2 +- src/components/views/messages/MAudioBody.js | 2 +- src/components/views/messages/MImageBody.js | 3 +-- src/components/views/messages/MVideoBody.js | 3 +-- src/components/views/right_panel/UserInfo.js | 2 +- .../views/room_settings/ColorSettings.js | 1 - src/components/views/rooms/Autocomplete.js | 1 - src/components/views/rooms/LinkPreviewWidget.js | 2 +- src/components/views/rooms/MemberInfo.js | 6 +++--- src/components/views/settings/ChangeAvatar.js | 2 +- src/components/views/settings/ChangePassword.js | 3 +-- src/components/views/settings/DevicesPanel.js | 2 +- src/components/views/settings/Notifications.js | 15 +++++++-------- .../settings/tabs/user/HelpUserSettingsTab.js | 2 +- src/createRoom.js | 1 - src/languageHandler.js | 1 - src/rageshake/rageshake.js | 2 -- src/rageshake/submit-rageshake.js | 1 - src/settings/handlers/DeviceSettingsHandler.js | 1 - src/settings/handlers/LocalEchoWrapper.js | 1 - .../handlers/RoomDeviceSettingsHandler.js | 1 - src/settings/handlers/SettingsHandler.js | 2 -- src/stores/FlairStore.js | 1 - src/stores/RoomViewStore.js | 2 +- src/utils/DecryptFile.js | 1 - src/utils/MultiInviter.js | 2 -- src/utils/promise.js | 3 --- .../views/dialogs/InteractiveAuthDialog-test.js | 1 - .../elements/MemberEventListSummary-test.js | 2 +- .../views/rooms/MessageComposerInput-test.js | 1 - test/components/views/rooms/RoomSettings-test.js | 1 - test/i18n-test/languageHandler-test.js | 2 +- test/stores/RoomViewStore-test.js | 2 -- test/test-utils.js | 1 - yarn.lock | 16 +++------------- 75 files changed, 71 insertions(+), 135 deletions(-) diff --git a/.babelrc b/.babelrc index 3fb847ad18..abe7e1ef3f 100644 --- a/.babelrc +++ b/.babelrc @@ -13,7 +13,6 @@ ], "transform-class-properties", "transform-object-rest-spread", - "transform-async-to-bluebird", "transform-runtime", "add-module-exports", "syntax-dynamic-import" diff --git a/package.json b/package.json index eb234e0573..620b323af7 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,6 @@ "dependencies": { "babel-plugin-syntax-dynamic-import": "^6.18.0", "babel-runtime": "^6.26.0", - "bluebird": "^3.5.0", "blueimp-canvas-to-blob": "^3.5.0", "browser-encrypt-attachment": "^0.3.0", "browser-request": "^0.3.3", @@ -120,7 +119,6 @@ "babel-eslint": "^10.0.1", "babel-loader": "^7.1.5", "babel-plugin-add-module-exports": "^0.2.1", - "babel-plugin-transform-async-to-bluebird": "^1.1.1", "babel-plugin-transform-builtin-extend": "^1.1.2", "babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-object-rest-spread": "^6.26.0", diff --git a/src/ContentMessages.js b/src/ContentMessages.js index dab8de2465..6908a6a18e 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -17,7 +17,6 @@ limitations under the License. 'use strict'; -import Promise from 'bluebird'; import extend from './extend'; import dis from './dispatcher'; import MatrixClientPeg from './MatrixClientPeg'; diff --git a/src/Lifecycle.js b/src/Lifecycle.js index ffd5baace4..c519e52872 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -16,7 +16,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; import Matrix from 'matrix-js-sdk'; import MatrixClientPeg from './MatrixClientPeg'; @@ -525,7 +524,7 @@ export function logout() { console.log("Failed to call logout API: token will not be invalidated"); onLoggedOut(); }, - ).done(); + ); } export function softLogout() { @@ -614,7 +613,7 @@ export function onLoggedOut() { // that can occur when components try to use a null client. dis.dispatch({action: 'on_logged_out'}, true); stopMatrixClient(); - _clearStorage().done(); + _clearStorage(); } /** diff --git a/src/Modal.js b/src/Modal.js index cb19731f01..4fc9fdcb02 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -23,7 +23,6 @@ import Analytics from './Analytics'; import sdk from './index'; import dis from './dispatcher'; import { _t } from './languageHandler'; -import Promise from "bluebird"; import {defer} from "./utils/promise"; const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; diff --git a/src/Notifier.js b/src/Notifier.js index cca0ea2b89..edb9850dfe 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -198,7 +198,7 @@ const Notifier = { if (enable) { // Attempt to get permission from user - plaf.requestNotificationPermission().done((result) => { + plaf.requestNotificationPermission().then((result) => { if (result !== 'granted') { // The permission request was dismissed or denied // TODO: Support alternative branding in messaging diff --git a/src/Resend.js b/src/Resend.js index 4eaee16d1b..51ec804c01 100644 --- a/src/Resend.js +++ b/src/Resend.js @@ -35,7 +35,7 @@ module.exports = { }, resend: function(event) { const room = MatrixClientPeg.get().getRoom(event.getRoomId()); - MatrixClientPeg.get().resendEvent(event, room).done(function(res) { + MatrixClientPeg.get().resendEvent(event, room).then(function(res) { dis.dispatch({ action: 'message_sent', event: event, diff --git a/src/RoomNotifs.js b/src/RoomNotifs.js index 2d5e4b3136..5bef4afd25 100644 --- a/src/RoomNotifs.js +++ b/src/RoomNotifs.js @@ -17,7 +17,6 @@ limitations under the License. import MatrixClientPeg from './MatrixClientPeg'; import PushProcessor from 'matrix-js-sdk/lib/pushprocessor'; -import Promise from 'bluebird'; export const ALL_MESSAGES_LOUD = 'all_messages_loud'; export const ALL_MESSAGES = 'all_messages'; diff --git a/src/Rooms.js b/src/Rooms.js index c8f90ec39a..239e348b58 100644 --- a/src/Rooms.js +++ b/src/Rooms.js @@ -15,7 +15,6 @@ limitations under the License. */ import MatrixClientPeg from './MatrixClientPeg'; -import Promise from 'bluebird'; /** * Given a room object, return the alias we should use for it, diff --git a/src/ScalarAuthClient.js b/src/ScalarAuthClient.js index 3623d47f8e..92f0ff6340 100644 --- a/src/ScalarAuthClient.js +++ b/src/ScalarAuthClient.js @@ -16,7 +16,6 @@ limitations under the License. */ import url from 'url'; -import Promise from 'bluebird'; import SettingsStore from "./settings/SettingsStore"; import { Service, startTermsFlow, TermsNotSignedError } from './Terms'; const request = require('browser-request'); diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index 910a6c4f13..c0ffc3022d 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -279,7 +279,7 @@ function inviteUser(event, roomId, userId) { } } - client.invite(roomId, userId).done(function() { + client.invite(roomId, userId).then(function() { sendResponse(event, { success: true, }); @@ -398,7 +398,7 @@ function setPlumbingState(event, roomId, status) { sendError(event, _t('You need to be logged in.')); return; } - client.sendStateEvent(roomId, "m.room.plumbing", { status: status }).done(() => { + client.sendStateEvent(roomId, "m.room.plumbing", { status: status }).then(() => { sendResponse(event, { success: true, }); @@ -414,7 +414,7 @@ function setBotOptions(event, roomId, userId) { sendError(event, _t('You need to be logged in.')); return; } - client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).done(() => { + client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).then(() => { sendResponse(event, { success: true, }); @@ -444,7 +444,7 @@ function setBotPower(event, roomId, userId, level) { }, ); - client.setPowerLevel(roomId, userId, level, powerEvent).done(() => { + client.setPowerLevel(roomId, userId, level, powerEvent).then(() => { sendResponse(event, { success: true, }); diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 1a491da54f..31e7ca4f39 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -28,7 +28,6 @@ import { linkifyAndSanitizeHtml } from './HtmlUtils'; import QuestionDialog from "./components/views/dialogs/QuestionDialog"; import WidgetUtils from "./utils/WidgetUtils"; import {textToHtmlRainbow} from "./utils/colour"; -import Promise from "bluebird"; import { getAddressType } from './UserAddress'; import { abbreviateUrl } from './utils/UrlUtils'; import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils'; diff --git a/src/Terms.js b/src/Terms.js index 685a39709c..14a7ccb65e 100644 --- a/src/Terms.js +++ b/src/Terms.js @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; import classNames from 'classnames'; import MatrixClientPeg from './MatrixClientPeg'; diff --git a/src/ToWidgetPostMessageApi.js b/src/ToWidgetPostMessageApi.js index def4af56ae..00309d252c 100644 --- a/src/ToWidgetPostMessageApi.js +++ b/src/ToWidgetPostMessageApi.js @@ -14,8 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from "bluebird"; - // const OUTBOUND_API_NAME = 'toWidget'; // Initiate requests using the "toWidget" postMessage API and handle responses diff --git a/src/VectorConferenceHandler.js b/src/VectorConferenceHandler.js index 37b3a7ddad..e0e333a371 100644 --- a/src/VectorConferenceHandler.js +++ b/src/VectorConferenceHandler.js @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; import {createNewMatrixCall, Room} from "matrix-js-sdk"; import CallHandler from './CallHandler'; import MatrixClientPeg from "./MatrixClientPeg"; diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index c385e13878..a26eb6033b 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -26,7 +26,6 @@ import RoomProvider from './RoomProvider'; import UserProvider from './UserProvider'; import EmojiProvider from './EmojiProvider'; import NotifProvider from './NotifProvider'; -import Promise from 'bluebird'; import {timeout} from "../utils/promise"; export type SelectionRange = { diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 776e7f0d6d..a0aa36803f 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -19,7 +19,6 @@ limitations under the License. import React from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; -import Promise from 'bluebird'; import MatrixClientPeg from '../../MatrixClientPeg'; import sdk from '../../index'; import dis from '../../dispatcher'; @@ -637,7 +636,7 @@ export default createReactClass({ title: _t('Error'), description: _t('Failed to upload image'), }); - }).done(); + }); }, _onJoinableChange: function(ev) { @@ -676,7 +675,7 @@ export default createReactClass({ this.setState({ avatarChanged: false, }); - }).done(); + }); }, _saveGroup: async function() { diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index 5e06d124c4..e1b02f653b 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -121,7 +121,7 @@ export default createReactClass({ this.setState({ errorText: msg, }); - }).done(); + }); this._intervalId = null; if (this.props.poll) { diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index b499cb6e42..455f039896 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -17,8 +17,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; - import React from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; @@ -542,7 +540,7 @@ export default createReactClass({ const Loader = sdk.getComponent("elements.Spinner"); const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); - MatrixClientPeg.get().leave(payload.room_id).done(() => { + MatrixClientPeg.get().leave(payload.room_id).then(() => { modal.close(); if (this.state.currentRoomId === payload.room_id) { dis.dispatch({action: 'view_next_room'}); @@ -863,7 +861,7 @@ export default createReactClass({ waitFor = this.firstSyncPromise.promise; } - waitFor.done(() => { + waitFor.then(() => { let presentedId = roomInfo.room_alias || roomInfo.room_id; const room = MatrixClientPeg.get().getRoom(roomInfo.room_id); if (room) { @@ -980,7 +978,7 @@ export default createReactClass({ const [shouldCreate, createOpts] = await modal.finished; if (shouldCreate) { - createRoom({createOpts}).done(); + createRoom({createOpts}); } }, @@ -1756,7 +1754,7 @@ export default createReactClass({ return; } - cli.sendEvent(roomId, event.getType(), event.getContent()).done(() => { + cli.sendEvent(roomId, event.getType(), event.getContent()).then(() => { dis.dispatch({action: 'message_sent'}); }, (err) => { dis.dispatch({action: 'message_send_failed'}); diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index 2de15a5444..63ae14ba09 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -47,7 +47,7 @@ export default createReactClass({ }, _fetch: function() { - this.context.matrixClient.getJoinedGroups().done((result) => { + this.context.matrixClient.getJoinedGroups().then((result) => { this.setState({groups: result.groups, error: null}); }, (err) => { if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') { diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 84f402e484..efca8d12a8 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -27,7 +27,6 @@ const dis = require('../../dispatcher'); import { linkifyAndSanitizeHtml } from '../../HtmlUtils'; import PropTypes from 'prop-types'; -import Promise from 'bluebird'; import { _t } from '../../languageHandler'; import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils'; import Analytics from '../../Analytics'; @@ -89,7 +88,7 @@ module.exports = createReactClass({ this.setState({protocolsLoading: false}); return; } - MatrixClientPeg.get().getThirdpartyProtocols().done((response) => { + MatrixClientPeg.get().getThirdpartyProtocols().then((response) => { this.protocols = response; this.setState({protocolsLoading: false}); }, (err) => { @@ -135,7 +134,7 @@ module.exports = createReactClass({ publicRooms: [], loading: true, }); - this.getMoreRooms().done(); + this.getMoreRooms(); }, getMoreRooms: function() { @@ -246,7 +245,7 @@ module.exports = createReactClass({ if (!alias) return; step = _t('delete the alias.'); return MatrixClientPeg.get().deleteAlias(alias); - }).done(() => { + }).then(() => { modal.close(); this.refreshRoomList(); }, (err) => { @@ -348,7 +347,7 @@ module.exports = createReactClass({ }); return; } - MatrixClientPeg.get().getThirdpartyLocation(protocolName, fields).done((resp) => { + MatrixClientPeg.get().getThirdpartyLocation(protocolName, fields).then((resp) => { if (resp.length > 0 && resp[0].alias) { this.showRoomAlias(resp[0].alias, true); } else { diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 4de573479d..ca558f2456 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -27,7 +27,6 @@ import React from 'react'; import createReactClass from 'create-react-class'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; -import Promise from 'bluebird'; import classNames from 'classnames'; import {Room} from "matrix-js-sdk"; import { _t } from '../../languageHandler'; @@ -1101,7 +1100,7 @@ module.exports = createReactClass({ } ContentMessages.sharedInstance().sendStickerContentToRoom(url, this.state.room.roomId, info, text, MatrixClientPeg.get()) - .done(undefined, (error) => { + .then(undefined, (error) => { if (error.name === "UnknownDeviceError") { // Let the staus bar handle this return; @@ -1145,7 +1144,7 @@ module.exports = createReactClass({ filter: filter, term: term, }); - this._handleSearchResult(searchPromise).done(); + this._handleSearchResult(searchPromise); }, _handleSearchResult: function(searchPromise) { @@ -1316,7 +1315,7 @@ module.exports = createReactClass({ }, onForgetClick: function() { - MatrixClientPeg.get().forget(this.state.room.roomId).done(function() { + MatrixClientPeg.get().forget(this.state.room.roomId).then(function() { dis.dispatch({ action: 'view_next_room' }); }, function(err) { const errCode = err.errcode || _t("unknown error code"); @@ -1333,7 +1332,7 @@ module.exports = createReactClass({ this.setState({ rejecting: true, }); - MatrixClientPeg.get().leave(this.state.roomId).done(function() { + MatrixClientPeg.get().leave(this.state.roomId).then(function() { dis.dispatch({ action: 'view_next_room' }); self.setState({ rejecting: false, diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 1d5c520285..8a67e70467 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -17,7 +17,6 @@ limitations under the License. import React from "react"; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; -import Promise from 'bluebird'; import { KeyCode } from '../../Keyboard'; import Timer from '../../utils/Timer'; import AutoHideScrollbar from "./AutoHideScrollbar"; diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 3dd5ea761e..7b0791ff1d 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -23,7 +23,6 @@ import React from 'react'; import createReactClass from 'create-react-class'; import ReactDOM from "react-dom"; import PropTypes from 'prop-types'; -import Promise from 'bluebird'; const Matrix = require("matrix-js-sdk"); const EventTimeline = Matrix.EventTimeline; @@ -462,7 +461,7 @@ const TimelinePanel = createReactClass({ // timeline window. // // see https://github.com/vector-im/vector-web/issues/1035 - this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).done(() => { + this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).then(() => { if (this.unmounted) { return; } const { events, liveEvents } = this._getEvents(); diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 46a5fa7bd7..6f68293caa 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -105,7 +105,7 @@ module.exports = createReactClass({ phase: PHASE_SENDING_EMAIL, }); this.reset = new PasswordReset(this.props.serverConfig.hsUrl, this.props.serverConfig.isUrl); - this.reset.resetPassword(email, password).done(() => { + this.reset.resetPassword(email, password).then(() => { this.setState({ phase: PHASE_EMAIL_SENT, }); diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index ad77ed49a5..2cdf5890cf 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -253,7 +253,7 @@ module.exports = createReactClass({ this.setState({ busy: false, }); - }).done(); + }); }, onUsernameChanged: function(username) { @@ -424,7 +424,7 @@ module.exports = createReactClass({ this.setState({ busy: false, }); - }).done(); + }); }, _isSupportedFlow: function(flow) { diff --git a/src/components/structures/auth/PostRegistration.js b/src/components/structures/auth/PostRegistration.js index 66075c80f7..760163585d 100644 --- a/src/components/structures/auth/PostRegistration.js +++ b/src/components/structures/auth/PostRegistration.js @@ -43,7 +43,7 @@ module.exports = createReactClass({ const cli = MatrixClientPeg.get(); this.setState({busy: true}); const self = this; - cli.getProfileInfo(cli.credentials.userId).done(function(result) { + cli.getProfileInfo(cli.credentials.userId).then(function(result) { self.setState({ avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(result.avatar_url), busy: false, diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 6321028457..3578d745f5 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -18,7 +18,6 @@ limitations under the License. */ import Matrix from 'matrix-js-sdk'; -import Promise from 'bluebird'; import React from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; @@ -371,7 +370,7 @@ module.exports = createReactClass({ if (pushers[i].kind === 'email') { const emailPusher = pushers[i]; emailPusher.data = { brand: this.props.brand }; - matrixClient.setPusher(emailPusher).done(() => { + matrixClient.setPusher(emailPusher).then(() => { console.log("Set email branding to " + this.props.brand); }, (error) => { console.error("Couldn't set email branding: " + error); diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js index d19ce95b33..cc3f9f96c4 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.js +++ b/src/components/views/auth/InteractiveAuthEntryComponents.js @@ -441,7 +441,7 @@ export const MsisdnAuthEntry = createReactClass({ this.props.fail(e); }).finally(() => { this.setState({requestingToken: false}); - }).done(); + }); }, /* diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js index fb056ee47f..97433e1f77 100644 --- a/src/components/views/context_menus/RoomTileContextMenu.js +++ b/src/components/views/context_menus/RoomTileContextMenu.js @@ -160,7 +160,7 @@ module.exports = createReactClass({ _onClickForget: function() { // FIXME: duplicated with RoomSettings (and dead code in RoomView) - MatrixClientPeg.get().forget(this.props.room.roomId).done(() => { + MatrixClientPeg.get().forget(this.props.room.roomId).then(() => { // Switch to another room view if we're currently viewing the // historical room if (RoomViewStore.getRoomId() === this.props.room.roomId) { @@ -190,7 +190,7 @@ module.exports = createReactClass({ this.setState({ roomNotifState: newState, }); - RoomNotifs.setRoomNotifsState(roomId, newState).done(() => { + RoomNotifs.setRoomNotifsState(roomId, newState).then(() => { // delay slightly so that the user can see their state change // before closing the menu return sleep(500).then(() => { diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index 24d8b96e0c..a40495893d 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -266,7 +266,7 @@ module.exports = createReactClass({ this.setState({ searchError: err.errcode ? err.message : _t('Something went wrong!'), }); - }).done(() => { + }).then(() => { this.setState({ busy: false, }); @@ -379,7 +379,7 @@ module.exports = createReactClass({ // Do a local search immediately this._doLocalSearch(query); } - }).done(() => { + }).then(() => { this.setState({ busy: false, }); diff --git a/src/components/views/dialogs/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.js index 11f4c21366..3430a12e71 100644 --- a/src/components/views/dialogs/CreateGroupDialog.js +++ b/src/components/views/dialogs/CreateGroupDialog.js @@ -93,7 +93,7 @@ export default createReactClass({ this.setState({createError: e}); }).finally(() => { this.setState({creating: false}); - }).done(); + }); }, _onCancel: function() { diff --git a/src/components/views/dialogs/KeyShareDialog.js b/src/components/views/dialogs/KeyShareDialog.js index a10c25a0fb..01e3479bb1 100644 --- a/src/components/views/dialogs/KeyShareDialog.js +++ b/src/components/views/dialogs/KeyShareDialog.js @@ -78,7 +78,7 @@ export default createReactClass({ true, ); } - }).done(); + }); }, componentWillUnmount: function() { diff --git a/src/components/views/dialogs/SetEmailDialog.js b/src/components/views/dialogs/SetEmailDialog.js index bedf713c4e..b527abffc9 100644 --- a/src/components/views/dialogs/SetEmailDialog.js +++ b/src/components/views/dialogs/SetEmailDialog.js @@ -62,7 +62,7 @@ export default createReactClass({ return; } this._addThreepid = new AddThreepid(); - this._addThreepid.addEmailAddress(emailAddress).done(() => { + this._addThreepid.addEmailAddress(emailAddress).then(() => { Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, { title: _t("Verification Pending"), description: _t( @@ -96,7 +96,7 @@ export default createReactClass({ }, verifyEmailAddress: function() { - this._addThreepid.checkEmailLinkClicked().done(() => { + this._addThreepid.checkEmailLinkClicked().then(() => { this.props.onFinished(true); }, (err) => { this.setState({emailBusy: false}); diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index 3bc6f5597e..598d0ce354 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -15,7 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; import React from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 260b63dfd4..453630413c 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -205,7 +205,7 @@ export default class AppTile extends React.Component { if (!this._scalarClient) { this._scalarClient = defaultManager.getScalarClient(); } - this._scalarClient.getScalarToken().done((token) => { + this._scalarClient.getScalarToken().then((token) => { // Append scalar_token as a query param if not already present this._scalarClient.scalarToken = token; const u = url.parse(this._addWurlParams(this.props.url)); diff --git a/src/components/views/elements/EditableTextContainer.js b/src/components/views/elements/EditableTextContainer.js index 3bf37df951..5cba98470c 100644 --- a/src/components/views/elements/EditableTextContainer.js +++ b/src/components/views/elements/EditableTextContainer.js @@ -17,7 +17,6 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import sdk from '../../../index'; -import Promise from 'bluebird'; /** * A component which wraps an EditableText, with a spinner while updates take @@ -51,7 +50,7 @@ export default class EditableTextContainer extends React.Component { this.setState({busy: true}); - this.props.getInitialValue().done( + this.props.getInitialValue().then( (result) => { if (this._unmounted) { return; } this.setState({ @@ -83,7 +82,7 @@ export default class EditableTextContainer extends React.Component { errorString: null, }); - this.props.onSubmit(value).done( + this.props.onSubmit(value).then( () => { if (this._unmounted) { return; } this.setState({ diff --git a/src/components/views/elements/ErrorBoundary.js b/src/components/views/elements/ErrorBoundary.js index e53e1ec0fa..e36464c4ef 100644 --- a/src/components/views/elements/ErrorBoundary.js +++ b/src/components/views/elements/ErrorBoundary.js @@ -54,7 +54,7 @@ export default class ErrorBoundary extends React.PureComponent { if (!PlatformPeg.get()) return; MatrixClientPeg.get().stopClient(); - MatrixClientPeg.get().store.deleteAllData().done(() => { + MatrixClientPeg.get().store.deleteAllData().then(() => { PlatformPeg.get().reload(); }); }; diff --git a/src/components/views/elements/ImageView.js b/src/components/views/elements/ImageView.js index 2772363bd0..b2f6d0abbb 100644 --- a/src/components/views/elements/ImageView.js +++ b/src/components/views/elements/ImageView.js @@ -84,7 +84,7 @@ export default class ImageView extends React.Component { title: _t('Error'), description: _t('You cannot delete this image. (%(code)s)', {code: code}), }); - }).done(); + }); }, }); }; diff --git a/src/components/views/elements/LanguageDropdown.js b/src/components/views/elements/LanguageDropdown.js index 365f9ded61..451c97d958 100644 --- a/src/components/views/elements/LanguageDropdown.js +++ b/src/components/views/elements/LanguageDropdown.js @@ -49,7 +49,7 @@ export default class LanguageDropdown extends React.Component { this.setState({langs}); }).catch(() => { this.setState({langs: ['en']}); - }).done(); + }); if (!this.props.value) { // If no value is given, we start with the first diff --git a/src/components/views/groups/GroupUserSettings.js b/src/components/views/groups/GroupUserSettings.js index 7d80bdd209..3cd5731b99 100644 --- a/src/components/views/groups/GroupUserSettings.js +++ b/src/components/views/groups/GroupUserSettings.js @@ -36,7 +36,7 @@ export default createReactClass({ }, componentWillMount: function() { - this.context.matrixClient.getJoinedGroups().done((result) => { + this.context.matrixClient.getJoinedGroups().then((result) => { this.setState({groups: result.groups || [], error: null}); }, (err) => { console.error(err); diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js index b4f26d0cbd..0246d28542 100644 --- a/src/components/views/messages/MAudioBody.js +++ b/src/components/views/messages/MAudioBody.js @@ -55,7 +55,7 @@ export default class MAudioBody extends React.Component { decryptFile(content.file).then(function(blob) { decryptedBlob = blob; return URL.createObjectURL(decryptedBlob); - }).done((url) => { + }).then((url) => { this.setState({ decryptedUrl: url, decryptedBlob: decryptedBlob, diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 640baa1966..b12957a7df 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -24,7 +24,6 @@ import MFileBody from './MFileBody'; import Modal from '../../../Modal'; import sdk from '../../../index'; import { decryptFile } from '../../../utils/DecryptFile'; -import Promise from 'bluebird'; import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; @@ -289,7 +288,7 @@ export default class MImageBody extends React.Component { this.setState({ error: err, }); - }).done(); + }); } // Remember that the user wanted to show this particular image diff --git a/src/components/views/messages/MVideoBody.js b/src/components/views/messages/MVideoBody.js index d277b6eae9..43e4f2dd75 100644 --- a/src/components/views/messages/MVideoBody.js +++ b/src/components/views/messages/MVideoBody.js @@ -20,7 +20,6 @@ import createReactClass from 'create-react-class'; import MFileBody from './MFileBody'; import MatrixClientPeg from '../../../MatrixClientPeg'; import { decryptFile } from '../../../utils/DecryptFile'; -import Promise from 'bluebird'; import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; @@ -115,7 +114,7 @@ module.exports = createReactClass({ this.setState({ error: err, }); - }).done(); + }); } }, diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 207bf29998..8c4d5a3586 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -870,7 +870,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room }, ).finally(() => { stopUpdating(); - }).done(); + }); }; const roomId = user.roomId; diff --git a/src/components/views/room_settings/ColorSettings.js b/src/components/views/room_settings/ColorSettings.js index aab6c04f53..952c49828b 100644 --- a/src/components/views/room_settings/ColorSettings.js +++ b/src/components/views/room_settings/ColorSettings.js @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index d4b51081f4..76a3a19e00 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -21,7 +21,6 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import flatMap from 'lodash/flatMap'; import type {Completion} from '../../../autocomplete/Autocompleter'; -import Promise from 'bluebird'; import { Room } from 'matrix-js-sdk'; import SettingsStore from "../../../settings/SettingsStore"; diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.js index d93fe76b46..3826c410bf 100644 --- a/src/components/views/rooms/LinkPreviewWidget.js +++ b/src/components/views/rooms/LinkPreviewWidget.js @@ -53,7 +53,7 @@ module.exports = createReactClass({ ); }, (error)=>{ console.error("Failed to get URL preview: " + error); - }).done(); + }); }, componentDidMount: function() { diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 2ea6392e96..cd6de64a5a 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -248,7 +248,7 @@ module.exports = createReactClass({ return client.getStoredDevicesForUser(member.userId); }).finally(function() { self._cancelDeviceList = null; - }).done(function(devices) { + }).then(function(devices) { if (cancelled) { // we got cancelled - presumably a different user now return; @@ -572,7 +572,7 @@ module.exports = createReactClass({ }, ).finally(()=>{ this.setState({ updating: this.state.updating - 1 }); - }).done(); + }); }, onPowerChange: async function(powerLevel) { @@ -629,7 +629,7 @@ module.exports = createReactClass({ this.setState({ updating: this.state.updating + 1 }); createRoom({dmUserId: this.props.member.userId}).finally(() => { this.setState({ updating: this.state.updating - 1 }); - }).done(); + }); }, onLeaveClick: function() { diff --git a/src/components/views/settings/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js index 32521006c7..904b17b15f 100644 --- a/src/components/views/settings/ChangeAvatar.js +++ b/src/components/views/settings/ChangeAvatar.js @@ -112,7 +112,7 @@ module.exports = createReactClass({ } }); - httpPromise.done(function() { + httpPromise.then(function() { self.setState({ phase: self.Phases.Display, avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(newUrl), diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 91292b19f9..a317c46cec 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -25,7 +25,6 @@ const Modal = require("../../../Modal"); const sdk = require("../../../index"); import dis from "../../../dispatcher"; -import Promise from 'bluebird'; import AccessibleButton from '../elements/AccessibleButton'; import { _t } from '../../../languageHandler'; @@ -174,7 +173,7 @@ module.exports = createReactClass({ newPassword: "", newPasswordConfirm: "", }); - }).done(); + }); }, _optionallySetEmail: function() { diff --git a/src/components/views/settings/DevicesPanel.js b/src/components/views/settings/DevicesPanel.js index 30f507ea18..cb5db10be4 100644 --- a/src/components/views/settings/DevicesPanel.js +++ b/src/components/views/settings/DevicesPanel.js @@ -52,7 +52,7 @@ export default class DevicesPanel extends React.Component { } _loadDevices() { - MatrixClientPeg.get().getDevices().done( + MatrixClientPeg.get().getDevices().then( (resp) => { if (this._unmounted) { return; } this.setState({devices: resp.devices || []}); diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index e3b4cfe122..6c71101eb8 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -16,7 +16,6 @@ limitations under the License. import React from 'react'; import createReactClass from 'create-react-class'; -import Promise from 'bluebird'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import MatrixClientPeg from '../../../MatrixClientPeg'; @@ -97,7 +96,7 @@ module.exports = createReactClass({ phase: this.phases.LOADING, }); - MatrixClientPeg.get().setPushRuleEnabled('global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked).done(function() { + MatrixClientPeg.get().setPushRuleEnabled('global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked).then(function() { self._refreshFromServer(); }); }, @@ -170,7 +169,7 @@ module.exports = createReactClass({ emailPusher.kind = null; emailPusherPromise = MatrixClientPeg.get().setPusher(emailPusher); } - emailPusherPromise.done(() => { + emailPusherPromise.then(() => { this._refreshFromServer(); }, (error) => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -274,7 +273,7 @@ module.exports = createReactClass({ } } - Promise.all(deferreds).done(function() { + Promise.all(deferreds).then(function() { self._refreshFromServer(); }, function(error) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -343,7 +342,7 @@ module.exports = createReactClass({ } } - Promise.all(deferreds).done(function(resps) { + Promise.all(deferreds).then(function(resps) { self._refreshFromServer(); }, function(error) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -398,7 +397,7 @@ module.exports = createReactClass({ }; // Then, add the new ones - Promise.all(removeDeferreds).done(function(resps) { + Promise.all(removeDeferreds).then(function(resps) { const deferreds = []; let pushRuleVectorStateKind = self.state.vectorContentRules.vectorState; @@ -434,7 +433,7 @@ module.exports = createReactClass({ } } - Promise.all(deferreds).done(function(resps) { + Promise.all(deferreds).then(function(resps) { self._refreshFromServer(); }, onError); }, onError); @@ -650,7 +649,7 @@ module.exports = createReactClass({ externalContentRules: self.state.externalContentRules, externalPushRules: self.state.externalPushRules, }); - }).done(); + }); MatrixClientPeg.get().getThreePids().then((r) => this.setState({threepids: r.threepids})); }, diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js index fbad327078..875f0bfc10 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js @@ -75,7 +75,7 @@ export default class HelpUserSettingsTab extends React.Component { // stopping in the middle of the logs. console.log("Clear cache & reload clicked"); MatrixClientPeg.get().stopClient(); - MatrixClientPeg.get().store.deleteAllData().done(() => { + MatrixClientPeg.get().store.deleteAllData().then(() => { PlatformPeg.get().reload(); }); }; diff --git a/src/createRoom.js b/src/createRoom.js index 120043247d..0ee90beba8 100644 --- a/src/createRoom.js +++ b/src/createRoom.js @@ -21,7 +21,6 @@ import { _t } from './languageHandler'; import dis from "./dispatcher"; import * as Rooms from "./Rooms"; -import Promise from 'bluebird'; import {getAddressType} from "./UserAddress"; /** diff --git a/src/languageHandler.js b/src/languageHandler.js index 179bb2d1d0..c56e5378df 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -19,7 +19,6 @@ limitations under the License. import request from 'browser-request'; import counterpart from 'counterpart'; -import Promise from 'bluebird'; import React from 'react'; import SettingsStore, {SettingLevel} from "./settings/SettingsStore"; diff --git a/src/rageshake/rageshake.js b/src/rageshake/rageshake.js index 820550af88..47bab38079 100644 --- a/src/rageshake/rageshake.js +++ b/src/rageshake/rageshake.js @@ -16,8 +16,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; - // This module contains all the code needed to log the console, persist it to // disk and submit bug reports. Rationale is as follows: // - Monkey-patching the console is preferable to having a log library because diff --git a/src/rageshake/submit-rageshake.js b/src/rageshake/submit-rageshake.js index e772912e48..457958eb82 100644 --- a/src/rageshake/submit-rageshake.js +++ b/src/rageshake/submit-rageshake.js @@ -17,7 +17,6 @@ limitations under the License. */ import pako from 'pako'; -import Promise from 'bluebird'; import MatrixClientPeg from '../MatrixClientPeg'; import PlatformPeg from '../PlatformPeg'; diff --git a/src/settings/handlers/DeviceSettingsHandler.js b/src/settings/handlers/DeviceSettingsHandler.js index 780815efd1..76c518b97b 100644 --- a/src/settings/handlers/DeviceSettingsHandler.js +++ b/src/settings/handlers/DeviceSettingsHandler.js @@ -15,7 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; import SettingsHandler from "./SettingsHandler"; import MatrixClientPeg from "../../MatrixClientPeg"; import {SettingLevel} from "../SettingsStore"; diff --git a/src/settings/handlers/LocalEchoWrapper.js b/src/settings/handlers/LocalEchoWrapper.js index e6964f9bf7..4cbe4891be 100644 --- a/src/settings/handlers/LocalEchoWrapper.js +++ b/src/settings/handlers/LocalEchoWrapper.js @@ -15,7 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from "bluebird"; import SettingsHandler from "./SettingsHandler"; /** diff --git a/src/settings/handlers/RoomDeviceSettingsHandler.js b/src/settings/handlers/RoomDeviceSettingsHandler.js index a0981ffbab..a9cf686c4c 100644 --- a/src/settings/handlers/RoomDeviceSettingsHandler.js +++ b/src/settings/handlers/RoomDeviceSettingsHandler.js @@ -15,7 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; import SettingsHandler from "./SettingsHandler"; import {SettingLevel} from "../SettingsStore"; diff --git a/src/settings/handlers/SettingsHandler.js b/src/settings/handlers/SettingsHandler.js index d1566d6bfa..7d987fc136 100644 --- a/src/settings/handlers/SettingsHandler.js +++ b/src/settings/handlers/SettingsHandler.js @@ -15,8 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from "bluebird"; - /** * Represents the base class for all level handlers. This class performs no logic * and should be overridden. diff --git a/src/stores/FlairStore.js b/src/stores/FlairStore.js index c8b4d75010..94b81c1ba5 100644 --- a/src/stores/FlairStore.js +++ b/src/stores/FlairStore.js @@ -15,7 +15,6 @@ limitations under the License. */ import EventEmitter from 'events'; -import Promise from 'bluebird'; const BULK_REQUEST_DEBOUNCE_MS = 200; diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index 6a405124f4..a3caf876ef 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -234,7 +234,7 @@ class RoomViewStore extends Store { }); MatrixClientPeg.get().joinRoom( this._state.roomAlias || this._state.roomId, payload.opts, - ).done(() => { + ).then(() => { // We don't actually need to do anything here: we do *not* // clear the 'joining' flag because the Room object and/or // our 'joined' member event may not have come down the sync diff --git a/src/utils/DecryptFile.js b/src/utils/DecryptFile.js index ea0e4c3fb0..f193bd7709 100644 --- a/src/utils/DecryptFile.js +++ b/src/utils/DecryptFile.js @@ -21,7 +21,6 @@ import encrypt from 'browser-encrypt-attachment'; import 'isomorphic-fetch'; // Grab the client so that we can turn mxc:// URLs into https:// URLS. import MatrixClientPeg from '../MatrixClientPeg'; -import Promise from 'bluebird'; // WARNING: We have to be very careful about what mime-types we allow into blobs, // as for performance reasons these are now rendered via URL.createObjectURL() diff --git a/src/utils/MultiInviter.js b/src/utils/MultiInviter.js index de5c2e7610..8b952a2b5b 100644 --- a/src/utils/MultiInviter.js +++ b/src/utils/MultiInviter.js @@ -15,11 +15,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; import MatrixClientPeg from '../MatrixClientPeg'; import {getAddressType} from '../UserAddress'; import GroupStore from '../stores/GroupStore'; -import Promise from 'bluebird'; import {_t} from "../languageHandler"; import sdk from "../index"; import Modal from "../Modal"; diff --git a/src/utils/promise.js b/src/utils/promise.js index e6e6ccb5c8..d7e8d2eae1 100644 --- a/src/utils/promise.js +++ b/src/utils/promise.js @@ -14,9 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -// This is only here to allow access to methods like done for the time being -import Promise from "bluebird"; - // @flow // Returns a promise which resolves with a given value after the given number of ms diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js index 7612b43b48..5f90e0f21c 100644 --- a/test/components/views/dialogs/InteractiveAuthDialog-test.js +++ b/test/components/views/dialogs/InteractiveAuthDialog-test.js @@ -15,7 +15,6 @@ limitations under the License. */ import expect from 'expect'; -import Promise from 'bluebird'; import React from 'react'; import ReactDOM from 'react-dom'; import ReactTestUtils from 'react-dom/test-utils'; diff --git a/test/components/views/elements/MemberEventListSummary-test.js b/test/components/views/elements/MemberEventListSummary-test.js index 95f7e7999a..a31cbdebb5 100644 --- a/test/components/views/elements/MemberEventListSummary-test.js +++ b/test/components/views/elements/MemberEventListSummary-test.js @@ -91,7 +91,7 @@ describe('MemberEventListSummary', function() { testUtils.beforeEach(this); sandbox = testUtils.stubClient(); - languageHandler.setLanguage('en').done(done); + languageHandler.setLanguage('en').then(done); languageHandler.setMissingEntryGenerator(function(key) { return key.split('|', 2)[1]; }); diff --git a/test/components/views/rooms/MessageComposerInput-test.js b/test/components/views/rooms/MessageComposerInput-test.js index 04a5c83ed0..60380eecd2 100644 --- a/test/components/views/rooms/MessageComposerInput-test.js +++ b/test/components/views/rooms/MessageComposerInput-test.js @@ -3,7 +3,6 @@ import ReactTestUtils from 'react-dom/test-utils'; import ReactDOM from 'react-dom'; import expect from 'expect'; import sinon from 'sinon'; -import Promise from 'bluebird'; import * as testUtils from '../../../test-utils'; import sdk from 'matrix-react-sdk'; const MessageComposerInput = sdk.getComponent('views.rooms.MessageComposerInput'); diff --git a/test/components/views/rooms/RoomSettings-test.js b/test/components/views/rooms/RoomSettings-test.js index dd91e812bc..1c0bfd95dc 100644 --- a/test/components/views/rooms/RoomSettings-test.js +++ b/test/components/views/rooms/RoomSettings-test.js @@ -3,7 +3,6 @@ // import ReactDOM from 'react-dom'; // import expect from 'expect'; // import jest from 'jest-mock'; -// import Promise from 'bluebird'; // import * as testUtils from '../../../test-utils'; // import sdk from 'matrix-react-sdk'; // const WrappedRoomSettings = testUtils.wrapInMatrixClientContext(sdk.getComponent('views.rooms.RoomSettings')); diff --git a/test/i18n-test/languageHandler-test.js b/test/i18n-test/languageHandler-test.js index 0d96bc15ab..8f21638703 100644 --- a/test/i18n-test/languageHandler-test.js +++ b/test/i18n-test/languageHandler-test.js @@ -11,7 +11,7 @@ describe('languageHandler', function() { testUtils.beforeEach(this); sandbox = testUtils.stubClient(); - languageHandler.setLanguage('en').done(done); + languageHandler.setLanguage('en').then(done); }); afterEach(function() { diff --git a/test/stores/RoomViewStore-test.js b/test/stores/RoomViewStore-test.js index be598de8da..77dfb37b0a 100644 --- a/test/stores/RoomViewStore-test.js +++ b/test/stores/RoomViewStore-test.js @@ -1,13 +1,11 @@ import expect from 'expect'; -import dis from '../../src/dispatcher'; import RoomViewStore from '../../src/stores/RoomViewStore'; import peg from '../../src/MatrixClientPeg'; import * as testUtils from '../test-utils'; -import Promise from 'bluebird'; const dispatch = testUtils.getDispatchForStore(RoomViewStore); diff --git a/test/test-utils.js b/test/test-utils.js index ff800132b9..64704fc610 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -1,7 +1,6 @@ "use strict"; import sinon from 'sinon'; -import Promise from 'bluebird'; import React from 'react'; import PropTypes from 'prop-types'; import peg from '../src/MatrixClientPeg'; diff --git a/yarn.lock b/yarn.lock index 3e43c29ef6..95d9adb573 100644 --- a/yarn.lock +++ b/yarn.lock @@ -899,7 +899,7 @@ babel-helper-explode-assignable-expression@^6.24.1: babel-traverse "^6.24.1" babel-types "^6.24.1" -babel-helper-function-name@^6.24.1, babel-helper-function-name@^6.8.0: +babel-helper-function-name@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9" integrity sha1-00dbjAPtmCQqJbSDUasYOZ01gKk= @@ -1042,16 +1042,6 @@ babel-plugin-syntax-trailing-function-commas@^6.22.0: resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3" integrity sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM= -babel-plugin-transform-async-to-bluebird@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-bluebird/-/babel-plugin-transform-async-to-bluebird-1.1.1.tgz#46ea3e7c5af629782ac9f1ed1b7cd38f8425afd4" - integrity sha1-Ruo+fFr2KXgqyfHtG3zTj4Qlr9Q= - dependencies: - babel-helper-function-name "^6.8.0" - babel-plugin-syntax-async-functions "^6.8.0" - babel-template "^6.9.0" - babel-traverse "^6.10.4" - babel-plugin-transform-async-to-generator@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761" @@ -1442,7 +1432,7 @@ babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.22.0, babel-runtim core-js "^2.4.0" regenerator-runtime "^0.11.0" -babel-template@^6.24.1, babel-template@^6.26.0, babel-template@^6.3.0, babel-template@^6.9.0: +babel-template@^6.24.1, babel-template@^6.26.0, babel-template@^6.3.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" integrity sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI= @@ -1453,7 +1443,7 @@ babel-template@^6.24.1, babel-template@^6.26.0, babel-template@^6.3.0, babel-tem babylon "^6.18.0" lodash "^4.17.4" -babel-traverse@^6.10.4, babel-traverse@^6.24.1, babel-traverse@^6.26.0: +babel-traverse@^6.24.1, babel-traverse@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" integrity sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4= From e144f1c368c6bd56935caf56df985fdf22ceceea Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 18 Nov 2019 10:37:29 +0000 Subject: [PATCH 175/334] remove Promise.config --- src/components/structures/MatrixChat.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 1fb1065e82..a2f2601e75 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -61,10 +61,6 @@ import { setTheme } from "../../theme"; import { storeRoomAliasInCache } from '../../RoomAliasCache'; import { defer } from "../../utils/promise"; -// Disable warnings for now: we use deprecated bluebird functions -// and need to migrate, but they spam the console with warnings. -Promise.config({warnings: false}); - /** constants for MatrixChat.state.view */ const VIEWS = { // a special initial state which is only used at startup, while we are From 579cbef7b0ed38f298fb35ad82b3d73096f080f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 14:29:03 +0100 Subject: [PATCH 176/334] EventIndexPeg: Rewrite the module documentation. --- src/EventIndexPeg.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index f1841b3f2b..a289c9e629 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -15,11 +15,8 @@ limitations under the License. */ /* - * Holds the current Platform object used by the code to do anything - * specific to the platform we're running on (eg. web, electron) - * Platforms are provided by the app layer. - * This allows the app layer to set a Platform without necessarily - * having to have a MatrixChat object + * Object holding the global EventIndex object. Can only be initialized if the + * platform supports event indexing. */ import PlatformPeg from "./PlatformPeg"; From 45e7aab41e3767026aa1e207640850f935f2aacb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 14:30:07 +0100 Subject: [PATCH 177/334] EventIndexing: Rename our EventIndexer class. --- src/EventIndexing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EventIndexing.js b/src/EventIndexing.js index 05d5fd03da..38d610bac7 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -20,7 +20,7 @@ import MatrixClientPeg from "./MatrixClientPeg"; /** * Event indexing class that wraps the platform specific event indexing. */ -export default class EventIndexer { +export default class EventIndex { constructor() { this.crawlerCheckpoints = []; // The time that the crawler will wait between /rooms/{room_id}/messages From b983eaa3f9321a245c0f2f63d16a153dc13c9b0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 14:36:08 +0100 Subject: [PATCH 178/334] EventIndex: Rename the file to be consistent with the class. --- src/{EventIndexing.js => EventIndex.js} | 0 src/EventIndexPeg.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/{EventIndexing.js => EventIndex.js} (100%) diff --git a/src/EventIndexing.js b/src/EventIndex.js similarity index 100% rename from src/EventIndexing.js rename to src/EventIndex.js diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index a289c9e629..7530dd1a99 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -20,7 +20,7 @@ limitations under the License. */ import PlatformPeg from "./PlatformPeg"; -import EventIndex from "./EventIndexing"; +import EventIndex from "./EventIndex"; class EventIndexPeg { constructor() { From c48ccf9761d1f481da21b661bf88cbebef04da0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 14:40:04 +0100 Subject: [PATCH 179/334] EventIndex: Remove some unnecessary checks if event indexing is supported. --- src/EventIndex.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/EventIndex.js b/src/EventIndex.js index 38d610bac7..e7aee6189e 100644 --- a/src/EventIndex.js +++ b/src/EventIndex.js @@ -40,7 +40,6 @@ export default class EventIndex { async onSync(state, prevState, data) { const indexManager = PlatformPeg.get().getEventIndexingManager(); - if (indexManager === null) return; if (prevState === null && state === "PREPARED") { // Load our stored checkpoints, if any. @@ -115,7 +114,6 @@ export default class EventIndex { async onRoomTimeline(ev, room, toStartOfTimeline, removed, data) { const indexManager = PlatformPeg.get().getEventIndexingManager(); - if (indexManager === null) return; // We only index encrypted rooms locally. if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; @@ -141,7 +139,6 @@ export default class EventIndex { async onEventDecrypted(ev, err) { const indexManager = PlatformPeg.get().getEventIndexingManager(); - if (indexManager === null) return; const eventId = ev.getId(); @@ -153,7 +150,6 @@ export default class EventIndex { async addLiveEventToIndex(ev) { const indexManager = PlatformPeg.get().getEventIndexingManager(); - if (indexManager === null) return; if (["m.room.message", "m.room.name", "m.room.topic"] .indexOf(ev.getType()) == -1) { @@ -349,7 +345,6 @@ export default class EventIndex { async onLimitedTimeline(room) { const indexManager = PlatformPeg.get().getEventIndexingManager(); - if (indexManager === null) return; if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; const timeline = room.getLiveTimeline(); From 8d7e7d0cc404c8d2df9d9602a5f526e3bb01924b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 14:40:38 +0100 Subject: [PATCH 180/334] EventIndex: Remove the unused deleteEventIndex method. We need to support the deletion of the event index even if it's not currently initialized, therefore the deletion ended up in the EventIndexPeg class. --- src/EventIndex.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/EventIndex.js b/src/EventIndex.js index e7aee6189e..6d8f265661 100644 --- a/src/EventIndex.js +++ b/src/EventIndex.js @@ -386,14 +386,6 @@ export default class EventIndex { return indexManager.closeEventIndex(); } - async deleteEventIndex() { - const indexManager = PlatformPeg.get().getEventIndexingManager(); - if (indexManager !== null) { - this.stopCrawler(); - await indexManager.deleteEventIndex(); - } - } - async search(searchArgs) { const indexManager = PlatformPeg.get().getEventIndexingManager(); return indexManager.searchEventIndex(searchArgs); From 4a6623bc00f9047256daaec382e3386b8f83741c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 15:04:22 +0100 Subject: [PATCH 181/334] EventIndex: Rework the crawler cancellation. --- src/EventIndex.js | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/EventIndex.js b/src/EventIndex.js index 6d8f265661..75e3cda4f2 100644 --- a/src/EventIndex.js +++ b/src/EventIndex.js @@ -29,7 +29,7 @@ export default class EventIndex { // The maximum number of events our crawler should fetch in a single // crawl. this._eventsPerCrawl = 100; - this._crawlerRef = null; + this._crawler = null; this.liveEventsForIndex = new Set(); } @@ -165,7 +165,7 @@ export default class EventIndex { indexManager.addEventToIndex(e, profile); } - async crawlerFunc(handle) { + async crawlerFunc() { // TODO either put this in a better place or find a library provided // method that does this. const sleep = async (ms) => { @@ -179,7 +179,9 @@ export default class EventIndex { const client = MatrixClientPeg.get(); const indexManager = PlatformPeg.get().getEventIndexingManager(); - handle.cancel = () => { + this._crawler = {}; + + this._crawler.cancel = () => { cancelled = true; }; @@ -340,6 +342,8 @@ export default class EventIndex { } } + this._crawler = null; + console.log("EventIndex: Stopping crawler function"); } @@ -366,18 +370,13 @@ export default class EventIndex { } startCrawler() { - if (this._crawlerRef !== null) return; - - const crawlerHandle = {}; - this.crawlerFunc(crawlerHandle); - this._crawlerRef = crawlerHandle; + if (this._crawler !== null) return; + this.crawlerFunc(); } stopCrawler() { - if (this._crawlerRef === null) return; - - this._crawlerRef.cancel(); - this._crawlerRef = null; + if (this._crawler === null) return; + this._crawler.cancel(); } async close() { From 21f00aaeb1c6c47f314c9aed1543b3ba41208811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 15:04:44 +0100 Subject: [PATCH 182/334] EventIndex: Fix some spelling errors. --- src/EventIndex.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/EventIndex.js b/src/EventIndex.js index 75e3cda4f2..c96fe25fc8 100644 --- a/src/EventIndex.js +++ b/src/EventIndex.js @@ -282,8 +282,8 @@ export default class EventIndex { // attributes? }; - // TODO if there ar no events at this point we're missing a lot - // decryption keys, do we wan't to retry this checkpoint at a later + // TODO if there are no events at this point we're missing a lot + // decryption keys, do we want to retry this checkpoint at a later // stage? const filteredEvents = matrixEvents.filter(isValidEvent); @@ -336,7 +336,7 @@ export default class EventIndex { } } catch (e) { console.log("EventIndex: Error durring a crawl", e); - // An error occured, put the checkpoint back so we + // An error occurred, put the checkpoint back so we // can retry. this.crawlerCheckpoints.push(checkpoint); } From 50cccd3212b384d3b26460544f2e8e54651f259f Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 15 Nov 2019 10:57:20 +0000 Subject: [PATCH 183/334] Add cross-signing feature flag Fixes https://github.com/vector-im/riot-web/issues/11407 --- src/MatrixClientPeg.js | 10 ++++++++++ src/i18n/strings/en_EN.json | 1 + src/settings/Settings.js | 6 ++++++ 3 files changed, 17 insertions(+) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index bebb254afc..ef0130ec15 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -220,6 +220,16 @@ class MatrixClientPeg { identityServer: new IdentityAuthClient(), }; + if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { + // TODO: Cross-signing keys are temporarily in memory only. A + // separate task in the cross-signing project will build from here. + const keys = []; + opts.cryptoCallbacks = { + getCrossSigningKey: k => keys[k], + saveCrossSigningKeys: newKeys => Object.assign(keys, newKeys), + }; + } + this.matrixClient = createMatrixClient(opts); // we're going to add eventlisteners for each matrix event tile, so the diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index dc9773ad21..8469f62d5c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -342,6 +342,7 @@ "Use the new, consistent UserInfo panel for Room Members and Group Members": "Use the new, consistent UserInfo panel for Room Members and Group Members", "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", "Send verification requests in direct message": "Send verification requests in direct message", + "Enable cross-signing to verify per-user instead of per-device": "Enable cross-signing to verify per-user instead of per-device", "Use the new, faster, composer for writing messages": "Use the new, faster, composer for writing messages", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", "Use compact timeline layout": "Use compact timeline layout", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 973d389ba6..c63217775a 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -146,6 +146,12 @@ export const SETTINGS = { supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_cross_signing": { + isFeature: true, + displayName: _td("Enable cross-signing to verify per-user instead of per-device"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "useCiderComposer": { displayName: _td("Use the new, faster, composer for writing messages"), supportedLevels: LEVELS_ACCOUNT_SETTINGS, From b8c0a0fe7238675dc818d152e0e63720a0a04bf6 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 15 Nov 2019 11:31:19 +0000 Subject: [PATCH 184/334] Reload automatically when changing cross-signing flag --- src/settings/Settings.js | 5 +++-- ...LowBandwidthController.js => ReloadOnChangeController.js} | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) rename src/settings/controllers/{LowBandwidthController.js => ReloadOnChangeController.js} (91%) diff --git a/src/settings/Settings.js b/src/settings/Settings.js index c63217775a..89bca043bd 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -23,7 +23,7 @@ import { } from "./controllers/NotificationControllers"; import CustomStatusController from "./controllers/CustomStatusController"; import ThemeController from './controllers/ThemeController'; -import LowBandwidthController from "./controllers/LowBandwidthController"; +import ReloadOnChangeController from "./controllers/ReloadOnChangeController"; // 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']; @@ -151,6 +151,7 @@ export const SETTINGS = { displayName: _td("Enable cross-signing to verify per-user instead of per-device"), supportedLevels: LEVELS_FEATURE, default: false, + controller: new ReloadOnChangeController(), }, "useCiderComposer": { displayName: _td("Use the new, faster, composer for writing messages"), @@ -433,7 +434,7 @@ export const SETTINGS = { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, displayName: _td('Low bandwidth mode'), default: false, - controller: new LowBandwidthController(), + controller: new ReloadOnChangeController(), }, "fallbackICEServerAllowed": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, diff --git a/src/settings/controllers/LowBandwidthController.js b/src/settings/controllers/ReloadOnChangeController.js similarity index 91% rename from src/settings/controllers/LowBandwidthController.js rename to src/settings/controllers/ReloadOnChangeController.js index c7796a425a..eadaee89ca 100644 --- a/src/settings/controllers/LowBandwidthController.js +++ b/src/settings/controllers/ReloadOnChangeController.js @@ -17,7 +17,7 @@ limitations under the License. import SettingController from "./SettingController"; import PlatformPeg from "../../PlatformPeg"; -export default class LowBandwidthController extends SettingController { +export default class ReloadOnChangeController extends SettingController { onChange(level, roomId, newValue) { PlatformPeg.get().reload(); } From 52e7d3505009732015f14be8743e58b4f650797f Mon Sep 17 00:00:00 2001 From: random Date: Mon, 18 Nov 2019 10:19:11 +0000 Subject: [PATCH 185/334] Translated using Weblate (Italian) Currently translated at 99.9% (1906 of 1908 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 8c7edbadd8..10227d447a 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -2270,5 +2270,17 @@ "You cancelled": "Hai annullato", "%(name)s cancelled": "%(name)s ha annullato", "%(name)s wants to verify": "%(name)s vuole verificare", - "You sent a verification request": "Hai inviato una richiesta di verifica" + "You sent a verification request": "Hai inviato una richiesta di verifica", + "Custom (%(level)s)": "Personalizzato (%(level)s)", + "Trusted": "Fidato", + "Not trusted": "Non fidato", + "Hide verified Sign-In's": "Nascondi accessi verificati", + "%(count)s verified Sign-In's|other": "%(count)s accessi verificati", + "%(count)s verified Sign-In's|one": "1 accesso verificato", + "Direct message": "Messaggio diretto", + "Unverify user": "Revoca verifica utente", + "%(role)s in %(roomName)s": "%(role)s in %(roomName)s", + "Messages in this room are end-to-end encrypted.": "I messaggi in questa stanza sono cifrati end-to-end.", + "Security": "Sicurezza", + "Verify": "Verifica" } From fd5e2398852b8b201c62fb7b0ac0d1b8f93b65e2 Mon Sep 17 00:00:00 2001 From: Walter Date: Mon, 18 Nov 2019 13:35:58 +0000 Subject: [PATCH 186/334] Translated using Weblate (Russian) Currently translated at 97.2% (1855 of 1908 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 | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index 01065a9e96..7806ea731b 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -2018,7 +2018,7 @@ "Create a private room": "Создать приватную комнату", "Topic (optional)": "Тема (опционально)", "Make this room public": "Сделать комнату публичной", - "Use the new, faster, composer for writing messages": "Используйте новый, более быстрый, редактор для написания сообщений.", + "Use the new, faster, composer for writing messages": "Используйте новый, более быстрый, редактор для написания сообщений", "Send read receipts for messages (requires compatible homeserver to disable)": "Отправлять подтверждения о прочтении сообщений (требуется отключение совместимого домашнего сервера)", "Show previews/thumbnails for images": "Показать превью / миниатюры для изображений", "Disconnect from the identity server and connect to instead?": "Отключиться от сервера идентификации и вместо этого подключиться к ?", @@ -2050,7 +2050,7 @@ "contact the administrators of identity server ": "связаться с администраторами сервера идентификации ", "wait and try again later": "Подождите и повторите попытку позже", "Error changing power level requirement": "Ошибка изменения требования к уровню прав", - "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "Произошла ошибка при изменении требований к уровню прав комнаты. Убедитесь, что у вас достаточно прав и попробуйте снова.", + "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "Произошла ошибка при изменении требований к уровню доступа комнаты. Убедитесь, что у вас достаточно прав и попробуйте снова.", "Error changing power level": "Ошибка изменения уровня прав", "An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "Произошла ошибка при изменении уровня прав пользователя. Убедитесь, что у вас достаточно прав и попробуйте снова.", "Unable to revoke sharing for email address": "Не удается отменить общий доступ к адресу электронной почты", @@ -2165,5 +2165,14 @@ "%(count)s unread messages including mentions.|one": "1 непрочитанное упоминание.", "%(count)s unread messages.|one": "1 непрочитанное сообщение.", "Unread messages.": "Непрочитанные сообщения.", - "Message Actions": "Сообщение действий" + "Message Actions": "Сообщение действий", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Это действие требует по умолчанию доступа к серверу идентификации для подтверждения адреса электронной почты или номера телефона, но у сервера нет никакого пользовательского соглашения.", + "Custom (%(level)s)": "Пользовательский (%(level)s)", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "Try out new ways to ignore people (experimental)": "Попробуйте новые способы игнорировать людей (экспериментальные)", + "Send verification requests in direct message": "Отправить запросы на подтверждение в прямом сообщении", + "My Ban List": "Мой список запрещенных", + "Ignored/Blocked": "Игнорируемые/Заблокированные", + "Error adding ignored user/server": "Ошибка добавления игнорируемого пользователя/сервера", + "Error subscribing to list": "Ошибка при подписке на список" } From f9d1fed74ac0f787ebae41d9d856e47129d6d6f5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 18 Nov 2019 19:00:22 +0000 Subject: [PATCH 187/334] re-add missing case of codepath --- src/components/structures/TimelinePanel.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 3dd5ea761e..e8e23c2f76 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -1076,6 +1076,7 @@ const TimelinePanel = createReactClass({ if (timeline) { // This is a hot-path optimization by skipping a promise tick // by repeating a no-op sync branch in TimelineSet.getTimelineForEvent & MatrixClient.getEventTimeline + this._timelineWindow.load(eventId, INITIAL_SIZE); // in this branch this method will happen in sync time onLoaded(); } else { const prom = this._timelineWindow.load(eventId, INITIAL_SIZE); From 6f8129419b5e3548209599fd1f6eb7baa537fa05 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Mon, 18 Nov 2019 19:47:10 +0000 Subject: [PATCH 188/334] Translated using Weblate (Hungarian) Currently translated at 100.0% (1918 of 1918 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 | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 3c049cc321..003af8240c 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2324,5 +2324,18 @@ "%(role)s in %(roomName)s": "%(role)s a szobában: %(roomName)s", "Messages in this room are end-to-end encrypted.": "Az üzenetek a szobában végponttól végpontig titkosítottak.", "Security": "Biztonság", - "Verify": "Ellenőriz" + "Verify": "Ellenőriz", + "Enable cross-signing to verify per-user instead of per-device": "Kereszt-aláírás engedélyezése eszköz alapú ellenőrzés helyett felhasználó alapú ellenőrzéshez", + "Any of the following data may be shared:": "Az alábbi adatok közül bármelyik megosztásra kerülhet:", + "Your display name": "Megjelenítési neved", + "Your avatar URL": "Profilképed URL-je", + "Your user ID": "Felhasználói azonosítód", + "Your theme": "Témád", + "Riot URL": "Riot URL", + "Room ID": "Szoba azonosító", + "Widget ID": "Kisalkalmazás azon.", + "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "Ennek a kisalkalmazásnak a használata adatot oszthat meg %(widgetDomain)s domain-nel és az Integrációs Menedzserrel.", + "Using this widget may share data with %(widgetDomain)s.": "Ennek a kisalkalmazásnak a használata adatot oszthat meg %(widgetDomain)s domain-nel.", + "Widget added by": "A kisalkalmazást hozzáadta", + "This widget may use cookies.": "Ez a kisalkalmazás sütiket használhat." } From f5ec9eb8f470eb0d55c98a9fcc36a3dd6d7e3e47 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 18 Nov 2019 13:16:36 -0700 Subject: [PATCH 189/334] Ensure widgets always have a sender associated with them Fixes https://github.com/vector-im/riot-web/issues/11419 --- src/components/views/elements/PersistentApp.js | 2 +- src/components/views/rooms/AppsDrawer.js | 2 +- src/utils/WidgetUtils.js | 8 ++++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/views/elements/PersistentApp.js b/src/components/views/elements/PersistentApp.js index d6931850be..391e7728f6 100644 --- a/src/components/views/elements/PersistentApp.js +++ b/src/components/views/elements/PersistentApp.js @@ -67,7 +67,7 @@ module.exports = createReactClass({ return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId(); }); const app = WidgetUtils.makeAppConfig( - appEvent.getStateKey(), appEvent.getContent(), appEvent.sender, persistentWidgetInRoomId, + appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(), persistentWidgetInRoomId, ); const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, persistentWidgetInRoomId); const AppTile = sdk.getComponent('elements.AppTile'); diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 2a0a7569fb..8e6319e315 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -107,7 +107,7 @@ module.exports = createReactClass({ this.props.room.roomId, WidgetUtils.getRoomWidgets(this.props.room), ); return widgets.map((ev) => { - return WidgetUtils.makeAppConfig(ev.getStateKey(), ev.getContent(), ev.sender); + return WidgetUtils.makeAppConfig(ev.getStateKey(), ev.getContent(), ev.getSender()); }); }, diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js index 36907da5ab..eb26ff1484 100644 --- a/src/utils/WidgetUtils.js +++ b/src/utils/WidgetUtils.js @@ -400,7 +400,7 @@ export default class WidgetUtils { return client.setAccountData('m.widgets', userWidgets); } - static makeAppConfig(appId, app, sender, roomId) { + static makeAppConfig(appId, app, senderUserId, roomId) { const myUserId = MatrixClientPeg.get().credentials.userId; const user = MatrixClientPeg.get().getUser(myUserId); const params = { @@ -413,6 +413,11 @@ export default class WidgetUtils { '$theme': SettingsStore.getValue("theme"), }; + if (!senderUserId) { + throw new Error("Widgets must be created by someone - provide a senderUserId"); + } + app.creatorUserId = senderUserId; + app.id = appId; app.name = app.name || app.type; @@ -425,7 +430,6 @@ export default class WidgetUtils { } app.url = encodeUri(app.url, params); - app.creatorUserId = (sender && sender.userId) ? sender.userId : null; return app; } From 8d25952dbbacdcf139b46977199491c449235768 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 18 Nov 2019 14:17:31 -0700 Subject: [PATCH 190/334] Add a bit more safety around breadcrumbs Fixes https://github.com/vector-im/riot-web/issues/11420 --- src/settings/handlers/AccountSettingsHandler.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/settings/handlers/AccountSettingsHandler.js b/src/settings/handlers/AccountSettingsHandler.js index f738bf7971..9c39d98990 100644 --- a/src/settings/handlers/AccountSettingsHandler.js +++ b/src/settings/handlers/AccountSettingsHandler.js @@ -126,6 +126,7 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa if (!content || !content['recent_rooms']) { content = this._getSettings(BREADCRUMBS_LEGACY_EVENT_TYPE); } + if (!content) content = {}; // If we still don't have content, make some content['recent_rooms'] = newValue; return MatrixClientPeg.get().setAccountData(BREADCRUMBS_EVENT_TYPE, content); @@ -167,7 +168,7 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa // This seems fishy - try and get the event for the new rooms const newType = this._getSettings(BREADCRUMBS_EVENT_TYPE); if (newType) val = newType['recent_rooms']; - else val = event.getContent()['rooms']; + else val = event.getContent()['rooms'] || []; } else if (event.getType() === BREADCRUMBS_EVENT_TYPE) { val = event.getContent()['recent_rooms']; } else { From 2f89f284965951013e8716df89aae5b8b622349f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 18 Nov 2019 14:25:04 -0700 Subject: [PATCH 191/334] Remove extraneous paranoia The value is nullchecked later on. --- 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 9c39d98990..7b05ad0c1b 100644 --- a/src/settings/handlers/AccountSettingsHandler.js +++ b/src/settings/handlers/AccountSettingsHandler.js @@ -168,7 +168,7 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa // This seems fishy - try and get the event for the new rooms const newType = this._getSettings(BREADCRUMBS_EVENT_TYPE); if (newType) val = newType['recent_rooms']; - else val = event.getContent()['rooms'] || []; + else val = event.getContent()['rooms']; } else if (event.getType() === BREADCRUMBS_EVENT_TYPE) { val = event.getContent()['recent_rooms']; } else { From b185eed46256edbfc1f287424f5f027dd4e38812 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 18 Nov 2019 17:56:33 -0700 Subject: [PATCH 192/334] Wire up the widget permission prompt to the cross-platform setting This doesn't have any backwards compatibility with anyone who has already clicked "Allow". We kinda want everyone to read the new prompt, so what better way to do it than effectively revoke all widget permissions? Part of https://github.com/vector-im/riot-web/issues/11262 --- src/components/views/elements/AppTile.js | 53 ++++++++++++------- .../views/elements/PersistentApp.js | 3 +- src/components/views/rooms/AppsDrawer.js | 3 +- src/utils/WidgetUtils.js | 3 +- 4 files changed, 41 insertions(+), 21 deletions(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index ffd9d73cca..db5978c792 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -34,7 +34,7 @@ import dis from '../../../dispatcher'; import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; import classNames from 'classnames'; import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; -import SettingsStore from "../../../settings/SettingsStore"; +import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:']; const ENABLE_REACT_PERF = false; @@ -69,8 +69,11 @@ export default class AppTile extends React.Component { * @return {Object} Updated component state to be set with setState */ _getNewState(newProps) { - const widgetPermissionId = [newProps.room.roomId, encodeURIComponent(newProps.url)].join('_'); - const hasPermissionToLoad = localStorage.getItem(widgetPermissionId); + // This is a function to make the impact of calling SettingsStore slightly less + const hasPermissionToLoad = () => { + const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", newProps.room.roomId); + return !!currentlyAllowedWidgets[newProps.eventId]; + }; const PersistedElement = sdk.getComponent("elements.PersistedElement"); return { @@ -78,10 +81,9 @@ export default class AppTile extends React.Component { // True while the iframe content is loading loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey), widgetUrl: this._addWurlParams(newProps.url), - widgetPermissionId: widgetPermissionId, // Assume that widget has permission to load if we are the user who // added it to the room, or if explicitly granted by the user - hasPermissionToLoad: hasPermissionToLoad === 'true' || newProps.userId === newProps.creatorUserId, + hasPermissionToLoad: newProps.userId === newProps.creatorUserId || hasPermissionToLoad(), error: null, deleting: false, widgetPageTitle: newProps.widgetPageTitle, @@ -446,24 +448,38 @@ export default class AppTile extends React.Component { }); } - /* TODO -- Store permission in account data so that it is persisted across multiple devices */ _grantWidgetPermission() { - console.warn('Granting permission to load widget - ', this.state.widgetUrl); - localStorage.setItem(this.state.widgetPermissionId, true); - this.setState({hasPermissionToLoad: true}); - // Now that we have permission, fetch the IM token - this.setScalarToken(); + const roomId = this.props.room.roomId; + console.info("Granting permission for widget to load: " + this.props.eventId); + const current = SettingsStore.getValue("allowedWidgets", roomId); + current[this.props.eventId] = true; + SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).then(() => { + this.setState({hasPermissionToLoad: true}); + + // Fetch a token for the integration manager, now that we're allowed to + this.setScalarToken(); + }).catch(err => { + console.error(err); + // We don't really need to do anything about this - the user will just hit the button again. + }); } _revokeWidgetPermission() { - console.warn('Revoking permission to load widget - ', this.state.widgetUrl); - localStorage.removeItem(this.state.widgetPermissionId); - this.setState({hasPermissionToLoad: false}); + const roomId = this.props.room.roomId; + console.info("Revoking permission for widget to load: " + this.props.eventId); + const current = SettingsStore.getValue("allowedWidgets", roomId); + current[this.props.eventId] = false; + SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).then(() => { + this.setState({hasPermissionToLoad: false}); - // Force the widget to be non-persistent - ActiveWidgetStore.destroyPersistentWidget(this.props.id); - const PersistedElement = sdk.getComponent("elements.PersistedElement"); - PersistedElement.destroyElement(this._persistKey); + // Force the widget to be non-persistent (able to be deleted/forgotten) + ActiveWidgetStore.destroyPersistentWidget(this.props.id); + const PersistedElement = sdk.getComponent("elements.PersistedElement"); + PersistedElement.destroyElement(this._persistKey); + }).catch(err => { + console.error(err); + // We don't really need to do anything about this - the user will just hit the button again. + }); } formatAppTileName() { @@ -720,6 +736,7 @@ AppTile.displayName ='AppTile'; AppTile.propTypes = { id: PropTypes.string.isRequired, + eventId: PropTypes.string, // required for room widgets url: PropTypes.string.isRequired, name: PropTypes.string.isRequired, room: PropTypes.object.isRequired, diff --git a/src/components/views/elements/PersistentApp.js b/src/components/views/elements/PersistentApp.js index 391e7728f6..47783a45c3 100644 --- a/src/components/views/elements/PersistentApp.js +++ b/src/components/views/elements/PersistentApp.js @@ -67,13 +67,14 @@ module.exports = createReactClass({ return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId(); }); const app = WidgetUtils.makeAppConfig( - appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(), persistentWidgetInRoomId, + appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(), persistentWidgetInRoomId, appEvent.getId(), ); const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, persistentWidgetInRoomId); const AppTile = sdk.getComponent('elements.AppTile'); return { - return WidgetUtils.makeAppConfig(ev.getStateKey(), ev.getContent(), ev.getSender()); + return WidgetUtils.makeAppConfig(ev.getStateKey(), ev.getContent(), ev.getSender(), ev.getRoomId(), ev.getId()); }); }, @@ -159,6 +159,7 @@ module.exports = createReactClass({ return ( Date: Mon, 18 Nov 2019 18:02:47 -0700 Subject: [PATCH 193/334] Appease the linter --- src/components/views/elements/PersistentApp.js | 3 ++- src/components/views/rooms/AppsDrawer.js | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/PersistentApp.js b/src/components/views/elements/PersistentApp.js index 47783a45c3..19e4be6083 100644 --- a/src/components/views/elements/PersistentApp.js +++ b/src/components/views/elements/PersistentApp.js @@ -67,7 +67,8 @@ module.exports = createReactClass({ return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId(); }); const app = WidgetUtils.makeAppConfig( - appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(), persistentWidgetInRoomId, appEvent.getId(), + appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(), + persistentWidgetInRoomId, appEvent.getId(), ); const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, persistentWidgetInRoomId); const AppTile = sdk.getComponent('elements.AppTile'); diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 618536ef7c..e53570dc5b 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -107,7 +107,9 @@ module.exports = createReactClass({ this.props.room.roomId, WidgetUtils.getRoomWidgets(this.props.room), ); return widgets.map((ev) => { - return WidgetUtils.makeAppConfig(ev.getStateKey(), ev.getContent(), ev.getSender(), ev.getRoomId(), ev.getId()); + return WidgetUtils.makeAppConfig( + ev.getStateKey(), ev.getContent(), ev.getSender(), ev.getRoomId(), ev.getId(), + ); }); }, From d2a99183595df253cdf4d97eee9c494e890fc4bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 19 Nov 2019 09:26:46 +0100 Subject: [PATCH 194/334] EventIndex: Remove some unused variables and some trailing whitespace. --- src/EventIndex.js | 4 ---- src/EventIndexPeg.js | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/EventIndex.js b/src/EventIndex.js index c96fe25fc8..7ed43ad31c 100644 --- a/src/EventIndex.js +++ b/src/EventIndex.js @@ -113,8 +113,6 @@ export default class EventIndex { } async onRoomTimeline(ev, room, toStartOfTimeline, removed, data) { - const indexManager = PlatformPeg.get().getEventIndexingManager(); - // We only index encrypted rooms locally. if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; @@ -138,8 +136,6 @@ export default class EventIndex { } async onEventDecrypted(ev, err) { - const indexManager = PlatformPeg.get().getEventIndexingManager(); - const eventId = ev.getId(); // If the event isn't in our live event set, ignore it. diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index 7530dd1a99..266b8f2d53 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -15,7 +15,7 @@ limitations under the License. */ /* - * Object holding the global EventIndex object. Can only be initialized if the + * Object holding the global EventIndex object. Can only be initialized if the * platform supports event indexing. */ From 92292003c8fcbe741fbe95e50bb54e5cee68fdb6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 19 Nov 2019 12:51:02 +0100 Subject: [PATCH 195/334] make shield on verification request scale correctly by not overriding `mask-size` using `mask` for `mask-image` --- res/css/views/messages/_MKeyVerificationRequest.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/views/messages/_MKeyVerificationRequest.scss b/res/css/views/messages/_MKeyVerificationRequest.scss index b4cde4e7ef..87a75dee82 100644 --- a/res/css/views/messages/_MKeyVerificationRequest.scss +++ b/res/css/views/messages/_MKeyVerificationRequest.scss @@ -25,7 +25,7 @@ limitations under the License. width: 12px; height: 16px; content: ""; - mask: url("$(res)/img/e2e/normal.svg"); + mask-image: url("$(res)/img/e2e/normal.svg"); mask-repeat: no-repeat; mask-size: 100%; margin-top: 4px; @@ -33,7 +33,7 @@ limitations under the License. } &.mx_KeyVerification_icon_verified::after { - mask: url("$(res)/img/e2e/verified.svg"); + mask-image: url("$(res)/img/e2e/verified.svg"); background-color: $accent-color; } From de15965c4a59495cc47e364833710410d0adfd19 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 19 Nov 2019 12:51:37 +0100 Subject: [PATCH 196/334] improve device list layout --- res/css/views/right_panel/_UserInfo.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index c68f3ffd37..df7d0a5f87 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -195,6 +195,8 @@ limitations under the License. .mx_UserInfo_devices { .mx_UserInfo_device { display: flex; + margin: 8px 0; + &.mx_UserInfo_device_verified { .mx_UserInfo_device_trusted { @@ -210,6 +212,7 @@ limitations under the License. .mx_UserInfo_device_name { flex: 1; margin-right: 5px; + word-break: break-word; } } From 39939de04ffbc29c66e68789f0d70372afcbc72e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 19 Nov 2019 12:51:46 +0100 Subject: [PATCH 197/334] =?UTF-8?q?remove=20white=20background=20on=20!=20?= =?UTF-8?q?and=20=E2=9C=85=20so=20it=20looks=20better=20on=20dark=20theme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- res/css/views/rooms/_E2EIcon.scss | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/res/css/views/rooms/_E2EIcon.scss b/res/css/views/rooms/_E2EIcon.scss index bc11ac6e1c..1ee5008888 100644 --- a/res/css/views/rooms/_E2EIcon.scss +++ b/res/css/views/rooms/_E2EIcon.scss @@ -22,21 +22,6 @@ limitations under the License. display: block; } -.mx_E2EIcon_verified::before, .mx_E2EIcon_warning::before { - content: ""; - display: block; - /* the symbols in the shield icons are cut out to make it themeable with css masking. - if they appear on a different background than white, the symbol wouldn't be white though, so we - add a rectangle here below the masked element to shine through the symbol cut-out. - hardcoding white and not using a theme variable as this would probably be white for any theme. */ - background-color: white; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; -} - .mx_E2EIcon_verified::after, .mx_E2EIcon_warning::after { content: ""; display: block; @@ -49,23 +34,11 @@ limitations under the License. mask-size: contain; } -.mx_E2EIcon_verified::before { - /* white rectangle below checkmark of shield */ - margin: 25% 28% 38% 25%; -} - - .mx_E2EIcon_verified::after { mask-image: url('$(res)/img/e2e/verified.svg'); background-color: $accent-color; } - -.mx_E2EIcon_warning::before { - /* white rectangle below "!" of shield */ - margin: 18% 40% 25% 40%; -} - .mx_E2EIcon_warning::after { mask-image: url('$(res)/img/e2e/warning.svg'); background-color: $warning-color; From 5f7b0fef334763df3a83aac563bf533574006101 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 19 Nov 2019 12:55:28 +0100 Subject: [PATCH 198/334] scale (new) icons to fit available size fixes https://github.com/vector-im/riot-web/issues/11399 --- res/css/views/rooms/_MemberDeviceInfo.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/rooms/_MemberDeviceInfo.scss b/res/css/views/rooms/_MemberDeviceInfo.scss index 951d1945b1..e73e6c58f1 100644 --- a/res/css/views/rooms/_MemberDeviceInfo.scss +++ b/res/css/views/rooms/_MemberDeviceInfo.scss @@ -25,6 +25,7 @@ limitations under the License. width: 12px; height: 12px; mask-repeat: no-repeat; + mask-size: 100%; } .mx_MemberDeviceInfo_icon_blacklisted { mask-image: url('$(res)/img/e2e/blacklisted.svg'); From 6017473caf8e23e31666924a17fb5f6ce60af42d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 19 Nov 2019 10:46:18 +0100 Subject: [PATCH 199/334] EventIndex: Move the event listener registration into the EventIndex class. --- src/EventIndex.js | 41 +++++++++++++++++++++++-- src/EventIndexPeg.js | 3 +- src/components/structures/MatrixChat.js | 26 ---------------- 3 files changed, 40 insertions(+), 30 deletions(-) diff --git a/src/EventIndex.js b/src/EventIndex.js index 7ed43ad31c..b6784cd331 100644 --- a/src/EventIndex.js +++ b/src/EventIndex.js @@ -31,11 +31,45 @@ export default class EventIndex { this._eventsPerCrawl = 100; this._crawler = null; this.liveEventsForIndex = new Set(); + + this.boundOnSync = async (state, prevState, data) => { + await this.onSync(state, prevState, data); + }; + this.boundOnRoomTimeline = async ( ev, room, toStartOfTimeline, removed, + data) => { + await this.onRoomTimeline(ev, room, toStartOfTimeline, removed, data); + }; + this.boundOnEventDecrypted = async (ev, err) => { + await this.onEventDecrypted(ev, err); + }; + this.boundOnTimelineReset = async (room, timelineSet, + resetAllTimelines) => await this.onTimelineReset(room); } async init() { const indexManager = PlatformPeg.get().getEventIndexingManager(); - return indexManager.initEventIndex(); + await indexManager.initEventIndex(); + + this.registerListeners(); + } + + registerListeners() { + const client = MatrixClientPeg.get(); + + client.on('sync', this.boundOnSync); + client.on('Room.timeline', this.boundOnRoomTimeline); + client.on('Event.decrypted', this.boundOnEventDecrypted); + client.on('Room.timelineReset', this.boundOnTimelineReset); + } + + removeListeners() { + const client = MatrixClientPeg.get(); + if (client === null) return; + + client.removeListener('sync', this.boundOnSync); + client.removeListener('Room.timeline', this.boundOnRoomTimeline); + client.removeListener('Event.decrypted', this.boundOnEventDecrypted); + client.removeListener('Room.timelineReset', this.boundOnTimelineReset); } async onSync(state, prevState, data) { @@ -343,7 +377,9 @@ export default class EventIndex { console.log("EventIndex: Stopping crawler function"); } - async onLimitedTimeline(room) { + async onTimelineReset(room) { + if (room === null) return; + const indexManager = PlatformPeg.get().getEventIndexingManager(); if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; @@ -377,6 +413,7 @@ export default class EventIndex { async close() { const indexManager = PlatformPeg.get().getEventIndexingManager(); + this.removeListeners(); this.stopCrawler(); return indexManager.closeEventIndex(); } diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index 266b8f2d53..74b7968c70 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -97,10 +97,9 @@ class EventIndexPeg { const indexManager = PlatformPeg.get().getEventIndexingManager(); if (indexManager !== null) { - this.stop(); + this.unset(); console.log("EventIndex: Deleting event index."); await indexManager.deleteEventIndex(); - this.index = null; } } } diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index b45884e64f..da67416400 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -31,7 +31,6 @@ import Analytics from "../../Analytics"; import { DecryptionFailureTracker } from "../../DecryptionFailureTracker"; import MatrixClientPeg from "../../MatrixClientPeg"; import PlatformPeg from "../../PlatformPeg"; -import EventIndexPeg from "../../EventIndexPeg"; import SdkConfig from "../../SdkConfig"; import * as RoomListSorter from "../../RoomListSorter"; import dis from "../../dispatcher"; @@ -1288,31 +1287,6 @@ export default createReactClass({ return self._loggedInView.child.canResetTimelineInRoom(roomId); }); - cli.on('sync', async (state, prevState, data) => { - const eventIndex = EventIndexPeg.get(); - if (eventIndex === null) return; - await eventIndex.onSync(state, prevState, data); - }); - - cli.on("Room.timeline", async (ev, room, toStartOfTimeline, removed, data) => { - const eventIndex = EventIndexPeg.get(); - if (eventIndex === null) return; - await eventIndex.onRoomTimeline(ev, room, toStartOfTimeline, removed, data); - }); - - cli.on("Event.decrypted", async (ev, err) => { - const eventIndex = EventIndexPeg.get(); - if (eventIndex === null) return; - await eventIndex.onEventDecrypted(ev, err); - }); - - cli.on("Room.timelineReset", async (room, timelineSet, resetAllTimelines) => { - const eventIndex = EventIndexPeg.get(); - if (eventIndex === null) return; - if (room === null) return; - await eventIndex.onLimitedTimeline(room); - }); - cli.on('sync', function(state, prevState, data) { // LifecycleStore and others cannot directly subscribe to matrix client for // events because flux only allows store state changes during flux dispatches. From 979803797fb58bb421c1e1a98cf90f263ae3af91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 19 Nov 2019 11:05:37 +0100 Subject: [PATCH 200/334] Lifecycle: Make the clear storage method async. --- src/Lifecycle.js | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 1d38934ade..1b69ca6ade 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -607,20 +607,20 @@ async function startMatrixClient(startSyncing=true) { * Stops a running client and all related services, and clears persistent * storage. Used after a session has been logged out. */ -export function onLoggedOut() { +export async function onLoggedOut() { _isLoggingOut = false; // Ensure that we dispatch a view change **before** stopping the client so // so that React components unmount first. This avoids React soft crashes // that can occur when components try to use a null client. dis.dispatch({action: 'on_logged_out'}); stopMatrixClient(); - _clearStorage().done(); + await _clearStorage(); } /** * @returns {Promise} promise which resolves once the stores have been cleared */ -function _clearStorage() { +async function _clearStorage() { Analytics.logout(); if (window.localStorage) { @@ -633,12 +633,8 @@ function _clearStorage() { baseUrl: "", }); - const clear = async () => { - await EventIndexPeg.deleteEventIndex(); - await cli.clearStores(); - }; - - return clear(); + await EventIndexPeg.deleteEventIndex(); + await cli.clearStores(); } /** @@ -662,7 +658,7 @@ export function stopMatrixClient(unsetClient=true) { if (unsetClient) { MatrixClientPeg.unset(); - EventIndexPeg.unset().done(); + EventIndexPeg.unset(); } } } From f776bdcc8b4306557df2bda71bce6ec097694abb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 19 Nov 2019 12:23:49 +0100 Subject: [PATCH 201/334] EventIndex: Hide the feature behind a labs flag. --- src/EventIndexPeg.js | 5 +++++ src/i18n/strings/en_EN.json | 3 ++- src/settings/Settings.js | 6 ++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index 74b7968c70..eb4caa2ca4 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -21,6 +21,7 @@ limitations under the License. import PlatformPeg from "./PlatformPeg"; import EventIndex from "./EventIndex"; +import SettingsStore from './settings/SettingsStore'; class EventIndexPeg { constructor() { @@ -34,6 +35,10 @@ class EventIndexPeg { * EventIndex was successfully initialized, false otherwise. */ async init() { + if (!SettingsStore.isFeatureEnabled("feature_event_indexing")) { + return false; + } + const indexManager = PlatformPeg.get().getEventIndexingManager(); if (!indexManager || await indexManager.supportsEventIndexing() !== true) { console.log("EventIndex: Platform doesn't support event indexing,", diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f524a22d4b..69c3f07f3f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1829,5 +1829,6 @@ "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.", "Failed to set direct chat tag": "Failed to set direct chat tag", "Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room", - "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room" + "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room", + "Enable local event indexing and E2EE search (requires restart)": "Enable local event indexing and E2EE search (requires restart)" } diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 3c33ae57fe..8abd845f0c 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -120,6 +120,12 @@ export const SETTINGS = { supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_event_indexing": { + isFeature: true, + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + displayName: _td("Enable local event indexing and E2EE search (requires restart)"), + default: false, + }, "useCiderComposer": { displayName: _td("Use the new, faster, composer for writing messages"), supportedLevels: LEVELS_ACCOUNT_SETTINGS, From e9df973c8273f7a0958a69e257cd2d9204ce8404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 19 Nov 2019 12:52:12 +0100 Subject: [PATCH 202/334] EventIndex: Move the event indexing files into a separate folder. --- src/BasePlatform.js | 2 +- src/Lifecycle.js | 2 +- src/Searching.js | 2 +- src/{ => indexing}/BaseEventIndexManager.js | 0 src/{ => indexing}/EventIndex.js | 4 ++-- src/{ => indexing}/EventIndexPeg.js | 6 +++--- 6 files changed, 8 insertions(+), 8 deletions(-) rename src/{ => indexing}/BaseEventIndexManager.js (100%) rename src/{ => indexing}/EventIndex.js (99%) rename src/{ => indexing}/EventIndexPeg.js (95%) diff --git a/src/BasePlatform.js b/src/BasePlatform.js index f6301fd173..14e34a1f40 100644 --- a/src/BasePlatform.js +++ b/src/BasePlatform.js @@ -19,7 +19,7 @@ limitations under the License. */ import dis from './dispatcher'; -import BaseEventIndexManager from './BaseEventIndexManager'; +import BaseEventIndexManager from './indexing/BaseEventIndexManager'; /** * Base class for classes that provide platform-specific functionality diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 1b69ca6ade..65fa0b29ce 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -20,7 +20,7 @@ import Promise from 'bluebird'; import Matrix from 'matrix-js-sdk'; import MatrixClientPeg from './MatrixClientPeg'; -import EventIndexPeg from './EventIndexPeg'; +import EventIndexPeg from './indexing/EventIndexPeg'; import createMatrixClient from './utils/createMatrixClient'; import Analytics from './Analytics'; import Notifier from './Notifier'; diff --git a/src/Searching.js b/src/Searching.js index ca3e7f041f..f8976c92e4 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import EventIndexPeg from "./EventIndexPeg"; +import EventIndexPeg from "./indexing/EventIndexPeg"; import MatrixClientPeg from "./MatrixClientPeg"; function serverSideSearch(term, roomId = undefined) { diff --git a/src/BaseEventIndexManager.js b/src/indexing/BaseEventIndexManager.js similarity index 100% rename from src/BaseEventIndexManager.js rename to src/indexing/BaseEventIndexManager.js diff --git a/src/EventIndex.js b/src/indexing/EventIndex.js similarity index 99% rename from src/EventIndex.js rename to src/indexing/EventIndex.js index b6784cd331..df81667c6e 100644 --- a/src/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import PlatformPeg from "./PlatformPeg"; -import MatrixClientPeg from "./MatrixClientPeg"; +import PlatformPeg from "../PlatformPeg"; +import MatrixClientPeg from "../MatrixClientPeg"; /** * Event indexing class that wraps the platform specific event indexing. diff --git a/src/EventIndexPeg.js b/src/indexing/EventIndexPeg.js similarity index 95% rename from src/EventIndexPeg.js rename to src/indexing/EventIndexPeg.js index eb4caa2ca4..c0bdd74ff4 100644 --- a/src/EventIndexPeg.js +++ b/src/indexing/EventIndexPeg.js @@ -19,9 +19,9 @@ limitations under the License. * platform supports event indexing. */ -import PlatformPeg from "./PlatformPeg"; -import EventIndex from "./EventIndex"; -import SettingsStore from './settings/SettingsStore'; +import PlatformPeg from "../PlatformPeg"; +import EventIndex from "../indexing/EventIndex"; +import SettingsStore from '../settings/SettingsStore'; class EventIndexPeg { constructor() { From 43884923e839fc900ab51fd8aef5a7ed903c6372 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 19 Nov 2019 14:07:14 +0100 Subject: [PATCH 203/334] merge the feature_user_info_panel flag into feature_dm_verification --- src/components/structures/RightPanel.js | 4 ++-- src/i18n/strings/en_EN.json | 3 +-- src/settings/Settings.js | 9 ++------- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index 48d272f6c9..895f6ae57e 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -185,7 +185,7 @@ export default class RightPanel extends React.Component { } else if (this.state.phase === RightPanel.Phase.GroupRoomList) { panel = ; } else if (this.state.phase === RightPanel.Phase.RoomMemberInfo) { - if (SettingsStore.isFeatureEnabled("feature_user_info_panel")) { + if (SettingsStore.isFeatureEnabled("feature_dm_verification")) { const onClose = () => { dis.dispatch({ action: "view_user", @@ -204,7 +204,7 @@ export default class RightPanel extends React.Component { } else if (this.state.phase === RightPanel.Phase.Room3pidMemberInfo) { panel = ; } else if (this.state.phase === RightPanel.Phase.GroupMemberInfo) { - if (SettingsStore.isFeatureEnabled("feature_user_info_panel")) { + if (SettingsStore.isFeatureEnabled("feature_dm_verification")) { const onClose = () => { dis.dispatch({ action: "view_user", diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9f13d133c4..473efdfb76 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -340,9 +340,8 @@ "Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)", "Render simple counters in room header": "Render simple counters in room header", "Multiple integration managers": "Multiple integration managers", - "Use the new, consistent UserInfo panel for Room Members and Group Members": "Use the new, consistent UserInfo panel for Room Members and Group Members", "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", - "Send verification requests in direct message": "Send verification requests in direct message", + "Send verification requests in direct message, including a new verification UX in the member panel.": "Send verification requests in direct message, including a new verification UX in the member panel.", "Enable cross-signing to verify per-user instead of per-device": "Enable cross-signing to verify per-user instead of per-device", "Use the new, faster, composer for writing messages": "Use the new, faster, composer for writing messages", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 89bca043bd..718a0daec3 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -120,12 +120,6 @@ export const SETTINGS = { supportedLevels: LEVELS_FEATURE, default: false, }, - "feature_user_info_panel": { - isFeature: true, - displayName: _td("Use the new, consistent UserInfo panel for Room Members and Group Members"), - supportedLevels: LEVELS_FEATURE, - default: false, - }, "feature_mjolnir": { isFeature: true, displayName: _td("Try out new ways to ignore people (experimental)"), @@ -142,7 +136,8 @@ export const SETTINGS = { }, "feature_dm_verification": { isFeature: true, - displayName: _td("Send verification requests in direct message"), + displayName: _td("Send verification requests in direct message," + + " including a new verification UX in the member panel."), supportedLevels: LEVELS_FEATURE, default: false, }, From 27d1e4fbbedb6ccf1bccdf17bd1cee74dee4c224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 19 Nov 2019 14:17:51 +0100 Subject: [PATCH 204/334] Fix the translations en_EN file by regenerating it. --- src/i18n/strings/en_EN.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 69c3f07f3f..6f116cbac2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -334,6 +334,7 @@ "Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)", "Render simple counters in room header": "Render simple counters in room header", "Multiple integration managers": "Multiple integration managers", + "Enable local event indexing and E2EE search (requires restart)": "Enable local event indexing and E2EE search (requires restart)", "Use the new, faster, composer for writing messages": "Use the new, faster, composer for writing messages", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", "Use compact timeline layout": "Use compact timeline layout", @@ -1829,6 +1830,5 @@ "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.", "Failed to set direct chat tag": "Failed to set direct chat tag", "Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room", - "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room", - "Enable local event indexing and E2EE search (requires restart)": "Enable local event indexing and E2EE search (requires restart)" + "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room" } From da2665f4a331d2c91ad07ff4e9ee59132cea3575 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Tue, 19 Nov 2019 06:47:53 +0000 Subject: [PATCH 205/334] Translated using Weblate (Albanian) Currently translated at 99.7% (1913 of 1918 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 2bf5732131..4d0ad6582b 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -2292,5 +2292,17 @@ "%(role)s in %(roomName)s": "%(role)s në %(roomName)s", "Messages in this room are end-to-end encrypted.": "Mesazhet në këtë dhomë janë të fshehtëzuara skaj-më-skaj.", "Security": "Siguri", - "Verify": "Verifikoje" + "Verify": "Verifikoje", + "Any of the following data may be shared:": "Mund të ndahen me të tjerët cilado prej të dhënave vijuese:", + "Your display name": "Emri juaj në ekran", + "Your avatar URL": "URL-ja e avatarit tuaj", + "Your user ID": "ID-ja juaj e përdoruesit", + "Your theme": "Tema juaj", + "Riot URL": "URL Riot-i", + "Room ID": "ID dhome", + "Widget ID": "ID widget-i", + "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "Përdorimi i këtij widget-i mund të sjellë ndarje të dhënash me %(widgetDomain)s & Përgjegjësin tuaj të Integrimeve.", + "Using this widget may share data with %(widgetDomain)s.": "Përdorimi i këtij widget-i mund të sjellë ndarje të dhënash me %(widgetDomain)s.", + "Widget added by": "Widget i shtuar nga", + "This widget may use cookies.": "Ky widget mund të përdorë cookies." } From 0c0437ebf501c4da485460343dff6a6d25ad0540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Tue, 19 Nov 2019 08:04:08 +0000 Subject: [PATCH 206/334] Translated using Weblate (French) Currently translated at 100.0% (1918 of 1918 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 | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index eef9438761..824da9d3ff 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2337,5 +2337,18 @@ "%(role)s in %(roomName)s": "%(role)s dans %(roomName)s", "Messages in this room are end-to-end encrypted.": "Les messages dans ce salon sont chiffrés de bout en bout.", "Security": "Sécurité", - "Verify": "Vérifier" + "Verify": "Vérifier", + "Enable cross-signing to verify per-user instead of per-device": "Activer la signature croisée pour vérifier par utilisateur et non par appareil", + "Any of the following data may be shared:": "Les données suivants peuvent être partagées :", + "Your display name": "Votre nom d’affichage", + "Your avatar URL": "L’URL de votre avatar", + "Your user ID": "Votre identifiant utilisateur", + "Your theme": "Votre thème", + "Riot URL": "URL de Riot", + "Room ID": "Identifiant du salon", + "Widget ID": "Identifiant du widget", + "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "L’utilisation de ce widget pourrait partager des données avec %(widgetDomain)s et votre gestionnaire d’intégrations.", + "Using this widget may share data with %(widgetDomain)s.": "L’utilisation de ce widget pourrait partager des données avec %(widgetDomain)s.", + "Widget added by": "Widget ajouté par", + "This widget may use cookies.": "Ce widget pourrait utiliser des cookies." } From dbee3a1215c2c3022ecc3ed87c9efeabf507dbae Mon Sep 17 00:00:00 2001 From: random Date: Tue, 19 Nov 2019 09:21:14 +0000 Subject: [PATCH 207/334] Translated using Weblate (Italian) Currently translated at 99.9% (1916 of 1918 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 | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 10227d447a..efab4595f6 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -2282,5 +2282,18 @@ "%(role)s in %(roomName)s": "%(role)s in %(roomName)s", "Messages in this room are end-to-end encrypted.": "I messaggi in questa stanza sono cifrati end-to-end.", "Security": "Sicurezza", - "Verify": "Verifica" + "Verify": "Verifica", + "Enable cross-signing to verify per-user instead of per-device": "Attiva la firma incrociata per verificare per-utente invece che per-dispositivo", + "Any of the following data may be shared:": "Possono essere condivisi tutti i seguenti dati:", + "Your display name": "Il tuo nome visualizzato", + "Your avatar URL": "L'URL del tuo avatar", + "Your user ID": "Il tuo ID utente", + "Your theme": "Il tuo tema", + "Riot URL": "URL di Riot", + "Room ID": "ID stanza", + "Widget ID": "ID widget", + "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "Usando questo widget i dati possono essere condivisi con %(widgetDomain)s e il tuo Gestore di Integrazione.", + "Using this widget may share data with %(widgetDomain)s.": "Usando questo widget i dati possono essere condivisi con %(widgetDomain)s.", + "Widget added by": "Widget aggiunto da", + "This widget may use cookies.": "Questo widget può usare cookie." } From afeab31ce6a3cd784606e4caa4624f30832dd3a8 Mon Sep 17 00:00:00 2001 From: fenuks Date: Tue, 19 Nov 2019 00:34:13 +0000 Subject: [PATCH 208/334] Translated using Weblate (Polish) Currently translated at 73.8% (1415 of 1918 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 | 46 +++++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index 31f82bc2dd..4054c48f97 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -320,7 +320,7 @@ "Mobile phone number (optional)": "Numer telefonu komórkowego (opcjonalne)", "Moderator": "Moderator", "%(serverName)s Matrix ID": "%(serverName)s Matrix ID", - "Name": "Imię", + "Name": "Nazwa", "Never send encrypted messages to unverified devices from this device": "Nigdy nie wysyłaj zaszyfrowanych wiadomości do niezweryfikowanych urządzeń z tego urządzenia", "Never send encrypted messages to unverified devices in this room from this device": "Nigdy nie wysyłaj niezaszyfrowanych wiadomości do niezweryfikowanych urządzeń z tego urządzenia", "New address (e.g. #foo:%(localDomain)s)": "Nowy adres (np. #foo:%(localDomain)s)", @@ -972,7 +972,7 @@ "Disinvite this user?": "Anulować zaproszenie tego użytkownika?", "Unignore": "Przestań ignorować", "Jump to read receipt": "Przeskocz do potwierdzenia odczytu", - "Share Link to User": "Udostępnij link do użytkownika", + "Share Link to User": "Udostępnij odnośnik do użytkownika", "At this time it is not possible to reply with a file so this will be sent without being a reply.": "W tej chwili nie można odpowiedzieć plikiem, więc zostanie wysłany nie będąc odpowiedzią.", "Unable to reply": "Nie udało się odpowiedzieć", "At this time it is not possible to reply with an emote.": "W tej chwili nie można odpowiedzieć emotikoną.", @@ -1556,7 +1556,7 @@ "Order rooms in the room list by most important first instead of most recent": "Kolejkuj pokoje na liście pokojów od najważniejszych niż od najnowszych", "Show hidden events in timeline": "Pokaż ukryte wydarzenia na linii czasowej", "Low bandwidth mode": "Tryb wolnej przepustowości", - "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Powzól na awaryjny serwer wspomagania połączeń turn.matrix.org, gdy Twój serwer domowy takiego nie oferuje (Twój adres IP będzie udostępniony podczas połączenia)", + "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Pozwól na awaryjny serwer wspomagania połączeń turn.matrix.org, gdy Twój serwer domowy takiego nie oferuje (Twój adres IP będzie udostępniony podczas połączenia)", "Messages containing my username": "Wiadomości zawierające moją nazwę użytkownika", "Encrypted messages in one-to-one chats": "Zaszyforwane wiadomości w rozmowach jeden-do-jednego", "Encrypted messages in group chats": "Zaszyfrowane wiadomości w rozmowach grupowych", @@ -1619,7 +1619,7 @@ "Disconnect Identity Server": "Odłącz Serwer Tożsamości", "Disconnect": "Odłącz", "Identity Server (%(server)s)": "Serwer tożsamości (%(server)s)", - "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "Używasz aby odkrywać i być odkrywanym przez isteniejące kontakty, które znasz. Możesz zmienić serwer tożsamości poniżej.", + "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "Używasz , aby odnajdywać i móc być odnajdywanym przez istniejące kontakty, które znasz. Możesz zmienić serwer tożsamości poniżej.", "Identity Server": "Serwer Tożsamości", "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "Nie używasz serwera tożsamości. Aby odkrywać i być odkrywanym przez istniejące kontakty które znasz, dodaj jeden poniżej.", "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Odłączenie się od serwera tożsamości oznacza, że nie będzie możliwości wykrycia przez innych użytkowników oraz nie będzie możliwości zaproszenia innych e-mailem lub za pomocą telefonu.", @@ -1653,5 +1653,41 @@ "You do not have the required permissions to use this command.": "Nie posiadasz wymaganych uprawnień do użycia tego polecenia.", "Changes the avatar of the current room": "Zmienia awatar dla obecnego pokoju", "Use an identity server": "Użyj serwera tożsamości", - "Show previews/thumbnails for images": "Pokaż podgląd/miniatury obrazów" + "Show previews/thumbnails for images": "Pokaż podgląd/miniatury obrazów", + "Trust": "Zaufaj", + "Custom (%(level)s)": "Własny (%(level)s)", + "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Użyj serwera tożsamości, by zaprosić z użyciem adresu e-mail. Kliknij dalej, żeby użyć domyślnego serwera tożsamości (%(defaultIdentityServerName)s), lub zmień w Ustawieniach.", + "Use an identity server to invite by email. Manage in Settings.": "Użyj serwera tożsamości, by zaprosić za pomocą adresu e-mail. Zarządzaj w ustawieniach.", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "Use the new, consistent UserInfo panel for Room Members and Group Members": "Użyj nowego, spójnego panelu informacji o użytkowniku dla członków pokoju i grup", + "Try out new ways to ignore people (experimental)": "Wypróbuj nowe sposoby na ignorowanie ludzi (eksperymentalne)", + "Send verification requests in direct message": "Wysyłaj prośby o weryfikację w bezpośredniej wiadomości", + "Use the new, faster, composer for writing messages": "Używaj nowego, szybszego kompozytora do pisania wiadomości", + "My Ban List": "Moja lista zablokowanych", + "This is your list of users/servers you have blocked - don't leave the room!": "To jest Twoja lista zablokowanych użytkowników/serwerów – nie opuszczaj tego pokoju!", + "Change identity server": "Zmień serwer tożsamości", + "Disconnect from the identity server and connect to instead?": "Rozłączyć się z serwerem tożsamości i połączyć się w jego miejsce z ?", + "Disconnect identity server": "Odłączanie serwera tożsamości", + "You should:": "Należy:", + "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "sprawdzić rozszerzenia przeglądarki, które mogą blokować serwer tożsamości (takie jak Privacy Badger)", + "contact the administrators of identity server ": "skontaktować się z administratorami serwera tożsamości ", + "wait and try again later": "zaczekaj i spróbuj ponownie później", + "Disconnect anyway": "Odłącz mimo to", + "You are still sharing your personal data on the identity server .": "W dalszym ciągu udostępniasz swoje dane osobowe na serwerze tożsamości .", + "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "Zalecamy, by usunąć swój adres e-mail i numer telefonu z serwera tożsamości przed odłączeniem.", + "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.": "Jeżeli nie chcesz używać do odnajdywania i bycia odnajdywanym przez osoby, które znasz, wpisz inny serwer tożsamości poniżej.", + "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Używanie serwera tożsamości jest opcjonalne. Jeżeli postanowisz nie używać serwera tożsamości, pozostali użytkownicy nie będą w stanie Cię odnaleźć ani nie będziesz mógł zaprosić innych po adresie e-mail czy numerze telefonu.", + "Do not use an identity server": "Nie używaj serwera tożsamości", + "Clear cache and reload": "Wyczyść pamięć podręczną i przeładuj", + "Something went wrong. Please try again or view your console for hints.": "Coś poszło nie tak. Spróbuj ponownie lub sprawdź konsolę przeglądarki dla wskazówek.", + "Please verify the room ID or alias and try again.": "Zweryfikuj poprawność ID pokoju lub nazwy zastępczej i spróbuj ponownie.", + "Please try again or view your console for hints.": "Spróbuj ponownie lub sprawdź konsolę przeglądarki dla wskazówek.", + "Personal ban list": "Osobista lista zablokowanych", + "Server or user ID to ignore": "ID serwera lub użytkownika do zignorowania", + "eg: @bot:* or example.org": "np: @bot:* lub przykład.pl", + "Composer": "Kompozytor", + "Autocomplete delay (ms)": "Opóźnienie autouzupełniania (ms)", + "Explore": "Przeglądaj", + "Filter": "Filtruj", + "Add room": "Dodaj pokój" } From 0eedab4154c18a39d9ae193a9eaf0ed2a34a3077 Mon Sep 17 00:00:00 2001 From: Alexis Date: Tue, 19 Nov 2019 05:46:11 +0000 Subject: [PATCH 209/334] Translated using Weblate (Portuguese) Currently translated at 33.3% (638 of 1918 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/pt/ --- src/i18n/strings/pt.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/pt.json b/src/i18n/strings/pt.json index 7cc80cfc78..5a56e807e4 100644 --- a/src/i18n/strings/pt.json +++ b/src/i18n/strings/pt.json @@ -842,5 +842,11 @@ "Collapse panel": "Colapsar o painel", "With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "Com o seu navegador atual, a aparência e sensação de uso da aplicação podem estar completamente incorretas, e algumas das funcionalidades poderão não funcionar. Se quiser tentar de qualquer maneira pode continuar, mas está por sua conta com algum problema que possa encontrar!", "Checking for an update...": "A procurar uma atualização...", - "There are advanced notifications which are not shown here": "Existem notificações avançadas que não são exibidas aqui" + "There are advanced notifications which are not shown here": "Existem notificações avançadas que não são exibidas aqui", + "Add Email Address": "Adicione adresso de e-mail", + "Add Phone Number": "Adicione número de telefone", + "The platform you're on": "A plataforma em que se encontra", + "The version of Riot.im": "A versão do RIOT.im", + "Whether or not you're logged in (we don't record your username)": "Tenha ou não, iniciado sessão (não iremos guardar o seu username)", + "Your language of choice": "O seu idioma de escolha" } From de0287213e240aff60a3e9e4fff9421dab42715f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 19 Nov 2019 14:42:35 +0100 Subject: [PATCH 210/334] use general warning icon instead of e2e one for room status --- src/components/structures/RoomStatusBar.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 21dd06767c..b0aa4cb59b 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -289,7 +289,7 @@ module.exports = createReactClass({ } return
    - +
    { title } @@ -306,7 +306,7 @@ module.exports = createReactClass({ if (this._shouldShowConnectionError()) { return (
    - /!\ + /!\
    { _t('Connectivity to the server has been lost.') } From 80ee68a42f468e5754cad43797e37a0a8e668811 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 19 Nov 2019 22:36:55 +0000 Subject: [PATCH 211/334] Use a settings watcher to set the theme Rather than listening for account data updates manually --- src/components/structures/MatrixChat.js | 20 +++++++++---------- .../tabs/user/GeneralUserSettingsTab.js | 3 +++ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 620e73bf93..c6efb56a9d 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -274,6 +274,7 @@ export default createReactClass({ componentDidMount: function() { this.dispatcherRef = dis.register(this.onAction); + this._themeWatchRef = SettingsStore.watchSetting("theme", null, this._onThemeChanged); this.focusComposer = false; @@ -360,6 +361,7 @@ export default createReactClass({ componentWillUnmount: function() { Lifecycle.stopMatrixClient(); dis.unregister(this.dispatcherRef); + SettingsStore.unwatchSetting(this._themeWatchRef); window.removeEventListener("focus", this.onFocus); window.removeEventListener('resize', this.handleResize); this.state.resizeNotifier.removeListener("middlePanelResized", this._dispatchTimelineResize); @@ -382,6 +384,13 @@ export default createReactClass({ } }, + _onThemeChanged: function(settingName, roomId, atLevel, newValue) { + dis.dispatch({ + action: 'set_theme', + value: newValue, + }); + }, + startPageChangeTimer() { // Tor doesn't support performance if (!performance || !performance.mark) return null; @@ -1376,17 +1385,6 @@ export default createReactClass({ }, null, true); }); - cli.on("accountData", function(ev) { - if (ev.getType() === 'im.vector.web.settings') { - if (ev.getContent() && ev.getContent().theme) { - dis.dispatch({ - action: 'set_theme', - value: ev.getContent().theme, - }); - } - } - }); - const dft = new DecryptionFailureTracker((total, errorCode) => { Analytics.trackEvent('E2E', 'Decryption failure', errorCode, total); }, (errorCode) => { diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 78961ad663..42324f1379 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -175,6 +175,9 @@ export default class GeneralUserSettingsTab extends React.Component { SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme); this.setState({theme: newTheme}); + // The settings watcher doesn't fire until the echo comes back from the + // server, so to make the theme change immediately we need to manually + // do the dispatch now dis.dispatch({action: 'set_theme', value: newTheme}); }; From a31d222570f7159d922ca0a94161574e92578678 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 19 Nov 2019 23:00:54 +0000 Subject: [PATCH 212/334] Add catch handler for theme setting --- .../views/settings/tabs/user/GeneralUserSettingsTab.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 42324f1379..d400e7a839 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -173,7 +173,13 @@ export default class GeneralUserSettingsTab extends React.Component { const newTheme = e.target.value; if (this.state.theme === newTheme) return; - SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme); + // doing getValue in the .catch will still return the value we failed to set, + // so remember what the value was before we tried to set it so we can revert + const oldTheme = SettingsStore.getValue('theme'); + SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme).catch(() => { + dis.dispatch({action: 'set_theme', value: oldTheme}); + this.setState({theme: oldTheme}); + }); this.setState({theme: newTheme}); // The settings watcher doesn't fire until the echo comes back from the // server, so to make the theme change immediately we need to manually From ab8a9dd0e9d70ed2e340cb2594fb13ab62377161 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Wed, 20 Nov 2019 03:58:41 +0000 Subject: [PATCH 213/334] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1917 of 1917 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 | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 5c6e69c864..1dfdc34f1a 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2330,5 +2330,19 @@ "%(role)s in %(roomName)s": "%(role)s 在 %(roomName)s", "Messages in this room are end-to-end encrypted.": "在此聊天室中的訊息為端到端加密。", "Security": "安全", - "Verify": "驗證" + "Verify": "驗證", + "Send verification requests in direct message, including a new verification UX in the member panel.": "在直接訊息中傳送驗證請求,包含成員面板中新的驗證使用者體驗。", + "Enable cross-signing to verify per-user instead of per-device": "啟用交叉簽章以驗證每個使用者而非每個裝置", + "Any of the following data may be shared:": "可能會分享以下資料:", + "Your display name": "您的顯示名稱", + "Your avatar URL": "您的大頭貼 URL", + "Your user ID": "您的使用 ID", + "Your theme": "您的佈景主題", + "Riot URL": "Riot URL", + "Room ID": "聊天室 ID", + "Widget ID": "小工具 ID", + "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "使用這個小工具可能會與 %(widgetDomain)s 以及您的整合管理員分享資料 。", + "Using this widget may share data with %(widgetDomain)s.": "使用這個小工具可能會與 %(widgetDomain)s 分享資料 。", + "Widget added by": "小工具新增由", + "This widget may use cookies.": "這個小工具可能會使用 cookies。" } From df868a6b0971be5b62c28563e9cb40308f3623ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Wed, 20 Nov 2019 08:43:16 +0000 Subject: [PATCH 214/334] Translated using Weblate (French) Currently translated at 100.0% (1917 of 1917 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 824da9d3ff..64272bb839 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2350,5 +2350,6 @@ "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "L’utilisation de ce widget pourrait partager des données avec %(widgetDomain)s et votre gestionnaire d’intégrations.", "Using this widget may share data with %(widgetDomain)s.": "L’utilisation de ce widget pourrait partager des données avec %(widgetDomain)s.", "Widget added by": "Widget ajouté par", - "This widget may use cookies.": "Ce widget pourrait utiliser des cookies." + "This widget may use cookies.": "Ce widget pourrait utiliser des cookies.", + "Send verification requests in direct message, including a new verification UX in the member panel.": "Envoyer les demandes de vérification en message direct, en incluant une nouvelle expérience de vérification dans le tableau des membres." } From 8df0aee12b15b963c0398049d54bf179bdb062c7 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Tue, 19 Nov 2019 18:58:32 +0000 Subject: [PATCH 215/334] Translated using Weblate (Hungarian) Currently translated at 100.0% (1917 of 1917 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 003af8240c..892f21dbb1 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2337,5 +2337,6 @@ "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "Ennek a kisalkalmazásnak a használata adatot oszthat meg %(widgetDomain)s domain-nel és az Integrációs Menedzserrel.", "Using this widget may share data with %(widgetDomain)s.": "Ennek a kisalkalmazásnak a használata adatot oszthat meg %(widgetDomain)s domain-nel.", "Widget added by": "A kisalkalmazást hozzáadta", - "This widget may use cookies.": "Ez a kisalkalmazás sütiket használhat." + "This widget may use cookies.": "Ez a kisalkalmazás sütiket használhat.", + "Send verification requests in direct message, including a new verification UX in the member panel.": "Ellenőrzés küldése közvetlen üzenetben, beleértve az új ellenőrzési felhasználói élményt a résztvevői panelen." } From f25236c3fb902b109ac1c480058843d17e2536f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=ED=83=9C=EC=84=AD?= Date: Wed, 20 Nov 2019 07:16:06 +0000 Subject: [PATCH 216/334] Translated using Weblate (Korean) Currently translated at 100.0% (1917 of 1917 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ko/ --- src/i18n/strings/ko.json | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index 8d34fab025..757edbfa4b 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -2181,5 +2181,19 @@ "Messages in this room are end-to-end encrypted.": "이 방의 메시지는 종단간 암호화되었습니다.", "Security": "보안", "Verify": "확인", - "You have ignored this user, so their message is hidden. Show anyways.": "이 사용자를 무시했습니다. 사용자의 메시지는 숨겨집니다. 무시하고 보이기." + "You have ignored this user, so their message is hidden. Show anyways.": "이 사용자를 무시했습니다. 사용자의 메시지는 숨겨집니다. 무시하고 보이기.", + "Send verification requests in direct message, including a new verification UX in the member panel.": "다이렉트 메시지에서 구성원 패널에 새 확인 UX가 적용된 확인 요청을 보냅니다.", + "Enable cross-signing to verify per-user instead of per-device": "기기 당 확인이 아닌 사람 당 확인을 위한 교차 서명 켜기", + "Any of the following data may be shared:": "다음 데이터가 공유됩니다:", + "Your display name": "당신의 표시 이름", + "Your avatar URL": "당신의 아바타 URL", + "Your user ID": "당신의 사용자 ID", + "Your theme": "당신의 테마", + "Riot URL": "Riot URL", + "Room ID": "방 ID", + "Widget ID": "위젯 ID", + "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "이 위젯을 사용하면 %(widgetDomain)s & 통합 관리자와 데이터를 공유합니다.", + "Using this widget may share data with %(widgetDomain)s.": "이 위젯을 사용하면 %(widgetDomain)s와(과) 데이터를 공유합니다.", + "Widget added by": "위젯을 추가했습니다", + "This widget may use cookies.": "이 위젯은 쿠키를 사용합니다." } From 870def5d858b2a7431cdf55b0fd4099a645bfdc9 Mon Sep 17 00:00:00 2001 From: fenuks Date: Tue, 19 Nov 2019 19:22:27 +0000 Subject: [PATCH 217/334] Translated using Weblate (Polish) Currently translated at 76.0% (1456 of 1917 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 | 48 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index 4054c48f97..f9c056b02b 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -1492,7 +1492,7 @@ "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.": "Możesz się zarejestrować, lecz niektóre funkcje nie będą dostępne dopóki Serwer Tożsamości nie będzie znów online. Jeśli ciągle widzisz to ostrzeżenie, sprawdź swoją konfigurację lub skontaktuj się z administratorem serwera.", "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.": "Możesz zresetować hasło, lecz niektóre funkcje nie będą dostępne dopóki Serwer Tożsamości nie będzie znów online. Jeśli ciągle widzisz to ostrzeżenie, sprawdź swoją konfigurację lub skontaktuj się z administratorem serwera.", "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.": "Możesz się zalogować, lecz niektóre funkcje nie będą dostępne dopóki Serwer Tożsamości nie będzie znów online. Jeśli ciągle widzisz to ostrzeżenie, sprawdź swoją konfigurację lub skontaktuj się z administratorem serwera.", - "No homeserver URL provided": "Nie podano URL serwera głównego.", + "No homeserver URL provided": "Nie podano URL serwera głównego", "The server does not support the room version specified.": "Serwer nie wspiera tej wersji pokoju.", "Name or Matrix ID": "Imię lub identyfikator Matrix", "Email, name or Matrix ID": "E-mail, imię lub Matrix ID", @@ -1528,7 +1528,7 @@ "%(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.", - "%(names)s and %(count)s others are typing …|one": "%(names)s i jedna osoba pisze.", + "%(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", "Your Riot is misconfigured": "Twój Riot jest źle skonfigurowany", @@ -1689,5 +1689,47 @@ "Autocomplete delay (ms)": "Opóźnienie autouzupełniania (ms)", "Explore": "Przeglądaj", "Filter": "Filtruj", - "Add room": "Dodaj pokój" + "Add room": "Dodaj pokój", + "A device's public name is visible to people you communicate with": "Publiczna nazwa urządzenia jest widoczna dla ludzi, z którymi się komunikujesz", + "Request media permissions": "Zapytaj o uprawnienia", + "Voice & Video": "Głos & Wideo", + "this room": "ten pokój", + "View older messages in %(roomName)s.": "Wyświetl starsze wiadomości w %(roomName)s.", + "Room information": "Informacje o pokoju", + "Internal room ID:": "Wewnętrzne ID pokoju:", + "Uploaded sound": "Przesłano dźwięk", + "Change history visibility": "Zmień widoczność historii", + "Upgrade the room": "Zaktualizuj pokój", + "Enable room encryption": "Włącz szyfrowanie pokoju", + "Select the roles required to change various parts of the room": "Wybierz role wymagane do zmieniania różnych części pokoju", + "Enable encryption?": "Włączyć szyfrowanie?", + "Your email address hasn't been verified yet": "Twój adres e-mail nie został jeszcze zweryfikowany", + "Verification code": "Kod weryfikacyjny", + "Remove %(email)s?": "Usunąć %(email)s?", + "Remove %(phone)s?": "Usunąć %(phone)s?", + "Some devices in this encrypted room are not trusted": "Niektóre urządzenia w tym zaszyfrowanym pokoju nie są zaufane", + "Loading …": "Ładowanie…", + "Loading room preview": "Wczytywanie podglądu pokoju", + "Try to join anyway": "Spróbuj dołączyć mimo tego", + "You can still join it because this is a public room.": "Możesz mimo to dołączyć, gdyż pokój jest publiczny.", + "This invite to %(roomName)s was sent to %(email)s which is not associated with your account": "To zaproszenie do %(roomName)s zostało wysłane na adres %(email)s, który nie jest przypisany do Twojego konta", + "Link this email with your account in Settings to receive invites directly in Riot.": "Połącz ten adres e-mail z Twoim kontem w Ustawieniach, aby otrzymywać zaproszenia bezpośrednio w Riot.", + "This invite to %(roomName)s was sent to %(email)s": "To zaproszenie do %(roomName)s zostało wysłane do %(email)s", + "Use an identity server in Settings to receive invites directly in Riot.": "Użyj serwera tożsamości w Ustawieniach, aby otrzymywać zaproszenia bezpośrednio w Riot.", + "Do you want to chat with %(user)s?": "Czy chcesz rozmawiać z %(user)s?", + "Do you want to join %(roomName)s?": "Czy chcesz dołączyć do %(roomName)s?", + " invited you": " zaprosił(a) CIę", + "You're previewing %(roomName)s. Want to join it?": "Przeglądasz %(roomName)s. Czy chcesz dołączyć do pokoju?", + "Not now": "Nie teraz", + "Don't ask me again": "Nie pytaj ponownie", + "%(count)s unread messages including mentions.|other": "%(count)s nieprzeczytanych wiadomości, wliczając wzmianki.", + "%(count)s unread messages including mentions.|one": "1 nieprzeczytana wzmianka.", + "%(count)s unread messages.|other": "%(count)s nieprzeczytanych wiadomości.", + "%(count)s unread messages.|one": "1 nieprzeczytana wiadomość.", + "Unread mentions.": "Nieprzeczytane wzmianki.", + "Unread messages.": "Nieprzeczytane wiadomości.", + "Join": "Dołącz", + "%(creator)s created and configured the room.": "%(creator)s stworzył(a) i skonfigurował(a) pokój.", + "Preview": "Przejrzyj", + "View": "Wyświetl" } From d277a1946ba6270e30a61b3ec3566e898df0c256 Mon Sep 17 00:00:00 2001 From: Karol Kosek Date: Tue, 19 Nov 2019 19:43:44 +0000 Subject: [PATCH 218/334] Translated using Weblate (Polish) Currently translated at 76.0% (1456 of 1917 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 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index f9c056b02b..a0ce517404 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -1731,5 +1731,6 @@ "Join": "Dołącz", "%(creator)s created and configured the room.": "%(creator)s stworzył(a) i skonfigurował(a) pokój.", "Preview": "Przejrzyj", - "View": "Wyświetl" + "View": "Wyświetl", + "Missing media permissions, click the button below to request.": "Brakuje uprawnień do mediów, kliknij przycisk poniżej, aby o nie zapytać." } From 8f796617257758f3b91752e8eef5f167de5c6578 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 20 Nov 2019 10:30:34 +0000 Subject: [PATCH 219/334] Update code style for our 90 char life We've been using 90 chars for JS code for quite a while now, but for some reason, the code style guide hasn't admitted that, so this adjusts it to match ESLint settings. --- code_style.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code_style.md b/code_style.md index e7844b939c..4b2338064c 100644 --- a/code_style.md +++ b/code_style.md @@ -22,7 +22,7 @@ number throgh from the original code to the final application. General Style ------------- - 4 spaces to indent, for consistency with Matrix Python. -- 120 columns per line, but try to keep JavaScript code around the 80 column mark. +- 120 columns per line, but try to keep JavaScript code around the 90 column mark. Inline JSX in particular can be nicer with more columns per line. - No trailing whitespace at end of lines. - Don't indent empty lines. From 2f5b0a9652629cb75bdf39926ff2f045511286ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 20 Nov 2019 12:30:03 +0100 Subject: [PATCH 220/334] EventIndex: Use property initializer style for the bound callbacks. --- src/indexing/EventIndex.js | 37 ++++++++++++------------------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index df81667c6e..e6a1d4007b 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -31,19 +31,6 @@ export default class EventIndex { this._eventsPerCrawl = 100; this._crawler = null; this.liveEventsForIndex = new Set(); - - this.boundOnSync = async (state, prevState, data) => { - await this.onSync(state, prevState, data); - }; - this.boundOnRoomTimeline = async ( ev, room, toStartOfTimeline, removed, - data) => { - await this.onRoomTimeline(ev, room, toStartOfTimeline, removed, data); - }; - this.boundOnEventDecrypted = async (ev, err) => { - await this.onEventDecrypted(ev, err); - }; - this.boundOnTimelineReset = async (room, timelineSet, - resetAllTimelines) => await this.onTimelineReset(room); } async init() { @@ -56,23 +43,23 @@ export default class EventIndex { registerListeners() { const client = MatrixClientPeg.get(); - client.on('sync', this.boundOnSync); - client.on('Room.timeline', this.boundOnRoomTimeline); - client.on('Event.decrypted', this.boundOnEventDecrypted); - client.on('Room.timelineReset', this.boundOnTimelineReset); + client.on('sync', this.onSync); + client.on('Room.timeline', this.onRoomTimeline); + client.on('Event.decrypted', this.onEventDecrypted); + client.on('Room.timelineReset', this.onTimelineReset); } removeListeners() { const client = MatrixClientPeg.get(); if (client === null) return; - client.removeListener('sync', this.boundOnSync); - client.removeListener('Room.timeline', this.boundOnRoomTimeline); - client.removeListener('Event.decrypted', this.boundOnEventDecrypted); - client.removeListener('Room.timelineReset', this.boundOnTimelineReset); + client.removeListener('sync', this.onSync); + client.removeListener('Room.timeline', this.onRoomTimeline); + client.removeListener('Event.decrypted', this.onEventDecrypted); + client.removeListener('Room.timelineReset', this.onTimelineReset); } - async onSync(state, prevState, data) { + onSync = async (state, prevState, data) => { const indexManager = PlatformPeg.get().getEventIndexingManager(); if (prevState === null && state === "PREPARED") { @@ -146,7 +133,7 @@ export default class EventIndex { } } - async onRoomTimeline(ev, room, toStartOfTimeline, removed, data) { + onRoomTimeline = async (ev, room, toStartOfTimeline, removed, data) => { // We only index encrypted rooms locally. if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; @@ -169,7 +156,7 @@ export default class EventIndex { } } - async onEventDecrypted(ev, err) { + onEventDecrypted = async (ev, err) => { const eventId = ev.getId(); // If the event isn't in our live event set, ignore it. @@ -377,7 +364,7 @@ export default class EventIndex { console.log("EventIndex: Stopping crawler function"); } - async onTimelineReset(room) { + onTimelineReset = async (room, timelineSet, resetAllTimelines) => { if (room === null) return; const indexManager = PlatformPeg.get().getEventIndexingManager(); From 0631faf902c5870b263d8f2745745c6ae2281a1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 20 Nov 2019 12:31:07 +0100 Subject: [PATCH 221/334] Settings: Fix the supportedLevels for event indexing feature. --- 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 8abd845f0c..2cf9509aca 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -122,7 +122,7 @@ export const SETTINGS = { }, "feature_event_indexing": { isFeature: true, - supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + supportedLevels: LEVELS_FEATURE, displayName: _td("Enable local event indexing and E2EE search (requires restart)"), default: false, }, From 4bd46f9d694f03aeaad667e35ab64083f7d4479f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 20 Nov 2019 12:47:20 +0100 Subject: [PATCH 222/334] EventIndex: Silence the linter complaining about missing docs. --- 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 e6a1d4007b..6bad992017 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -17,7 +17,7 @@ limitations under the License. import PlatformPeg from "../PlatformPeg"; import MatrixClientPeg from "../MatrixClientPeg"; -/** +/* * Event indexing class that wraps the platform specific event indexing. */ export default class EventIndex { From 5a700b518a5063a1484ee339daf2a4deb611d485 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 20 Nov 2019 13:41:06 +0000 Subject: [PATCH 223/334] Get theme automatically from system setting Uses CSS `prefers-color-scheme` to get the user's preferred colour scheme. Also bundles up some theme logic into its own class. --- src/components/structures/MatrixChat.js | 17 ++--- .../tabs/user/GeneralUserSettingsTab.js | 28 ++++++-- src/i18n/strings/en_EN.json | 1 + src/settings/Settings.js | 5 ++ src/theme.js | 67 ++++++++++++++++++- 5 files changed, 100 insertions(+), 18 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index c6efb56a9d..661a0c7077 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -59,7 +59,7 @@ import { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils"; import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils"; import DMRoomMap from '../../utils/DMRoomMap'; import { countRoomsWithNotif } from '../../RoomNotifs'; -import { setTheme } from "../../theme"; +import { ThemeWatcher } from "../../theme"; import { storeRoomAliasInCache } from '../../RoomAliasCache'; import { defer } from "../../utils/promise"; @@ -274,7 +274,8 @@ export default createReactClass({ componentDidMount: function() { this.dispatcherRef = dis.register(this.onAction); - this._themeWatchRef = SettingsStore.watchSetting("theme", null, this._onThemeChanged); + this._themeWatcher = new ThemeWatcher(); + this._themeWatcher.start(); this.focusComposer = false; @@ -361,7 +362,7 @@ export default createReactClass({ componentWillUnmount: function() { Lifecycle.stopMatrixClient(); dis.unregister(this.dispatcherRef); - SettingsStore.unwatchSetting(this._themeWatchRef); + this._themeWatcher.stop(); window.removeEventListener("focus", this.onFocus); window.removeEventListener('resize', this.handleResize); this.state.resizeNotifier.removeListener("middlePanelResized", this._dispatchTimelineResize); @@ -384,13 +385,6 @@ export default createReactClass({ } }, - _onThemeChanged: function(settingName, roomId, atLevel, newValue) { - dis.dispatch({ - action: 'set_theme', - value: newValue, - }); - }, - startPageChangeTimer() { // Tor doesn't support performance if (!performance || !performance.mark) return null; @@ -672,9 +666,6 @@ export default createReactClass({ }); break; } - case 'set_theme': - setTheme(payload.value); - break; case 'on_logging_in': // We are now logging in, so set the state to reflect that // NB. This does not touch 'ready' since if our dispatches diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index d400e7a839..50f37cea1f 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -27,7 +27,7 @@ import LanguageDropdown from "../../../elements/LanguageDropdown"; import AccessibleButton from "../../../elements/AccessibleButton"; import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog"; import PropTypes from "prop-types"; -import {enumerateThemes} from "../../../../../theme"; +import {enumerateThemes, ThemeWatcher} from "../../../../../theme"; import PlatformPeg from "../../../../../PlatformPeg"; import MatrixClientPeg from "../../../../../MatrixClientPeg"; import sdk from "../../../../.."; @@ -50,6 +50,7 @@ export default class GeneralUserSettingsTab extends React.Component { this.state = { language: languageHandler.getCurrentLanguage(), theme: SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme"), + useSystemTheme: SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme"), haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()), serverSupportsSeparateAddAndBind: null, idServerHasUnsignedTerms: false, @@ -177,16 +178,22 @@ export default class GeneralUserSettingsTab extends React.Component { // so remember what the value was before we tried to set it so we can revert const oldTheme = SettingsStore.getValue('theme'); SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme).catch(() => { - dis.dispatch({action: 'set_theme', value: oldTheme}); + dis.dispatch({action: 'recheck_theme'}); this.setState({theme: oldTheme}); }); this.setState({theme: newTheme}); // The settings watcher doesn't fire until the echo comes back from the // server, so to make the theme change immediately we need to manually // do the dispatch now - dis.dispatch({action: 'set_theme', value: newTheme}); + dis.dispatch({action: 'recheck_theme'}); }; + _onUseSystemThemeChanged = (checked) => { + this.setState({useSystemTheme: checked}); + dis.dispatch({action: 'recheck_theme'}); + } + + _onPasswordChangeError = (err) => { // TODO: Figure out a design that doesn't involve replacing the current dialog let errMsg = err.error || ""; @@ -297,11 +304,24 @@ export default class GeneralUserSettingsTab extends React.Component { _renderThemeSection() { const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); + + const themeWatcher = new ThemeWatcher(); + let systemThemeSection; + if (themeWatcher.isSystemThemeSupported()) { + systemThemeSection =
    + +
    ; + } return (
    {_t("Theme")} + {systemThemeSection} + value={this.state.theme} onChange={this._onThemeChange} + disabled={this.state.useSystemTheme} + > {Object.entries(enumerateThemes()).map(([theme, text]) => { return ; })} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 473efdfb76..5dfcd038ec 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -363,6 +363,7 @@ "Automatically replace plain text Emoji": "Automatically replace plain text Emoji", "Mirror local video feed": "Mirror local video feed", "Enable Community Filter Panel": "Enable Community Filter Panel", + "Match system theme": "Match system theme", "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 devices from this device": "Never send encrypted messages to unverified devices from this device", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 718a0daec3..8a3bc3ecbc 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -275,6 +275,11 @@ export const SETTINGS = { supportedLevels: LEVELS_ACCOUNT_SETTINGS, default: [], }, + "use_system_theme": { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: true, + displayName: _td("Match system theme"), + }, "webRtcAllowPeerToPeer": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, displayName: _td('Allow Peer-to-Peer for 1:1 calls'), diff --git a/src/theme.js b/src/theme.js index 8a15c606d7..8996fe28fd 100644 --- a/src/theme.js +++ b/src/theme.js @@ -19,8 +19,72 @@ import {_t} from "./languageHandler"; export const DEFAULT_THEME = "light"; import Tinter from "./Tinter"; +import dis from "./dispatcher"; import SettingsStore from "./settings/SettingsStore"; +export class ThemeWatcher { + static _instance = null; + + constructor() { + this._themeWatchRef = null; + this._systemThemeWatchRef = null; + this._dispatcherRef = null; + + // we have both here as each may either match or not match, so by having both + // we can get the tristate of dark/light/unsupported + this._preferDark = global.matchMedia("(prefers-color-scheme: dark)"); + this._preferLight = global.matchMedia("(prefers-color-scheme: light)"); + + this._currentTheme = this.getEffectiveTheme(); + } + + start() { + this._themeWatchRef = SettingsStore.watchSetting("theme", null, this._onChange); + this._systemThemeWatchRef = SettingsStore.watchSetting("use_system_theme", null, this._onChange); + this._preferDark.addEventListener('change', this._onChange); + this._preferLight.addEventListener('change', this._onChange); + this._dispatcherRef = dis.register(this._onAction); + } + + stop() { + this._preferDark.removeEventListener('change', this._onChange); + this._preferLight.removeEventListener('change', this._onChange); + SettingsStore.unwatchSetting(this._systemThemeWatchRef); + SettingsStore.unwatchSetting(this._themeWatchRef); + dis.unregister(this._dispatcherRef); + } + + _onChange = () => { + this.recheck(); + } + + _onAction = (payload) => { + if (payload.action === 'recheck_theme') { + this.recheck(); + } + } + + recheck() { + const oldTheme = this._currentTheme; + this._currentTheme = this.getEffectiveTheme(); + if (oldTheme !== this._currentTheme) { + setTheme(this._currentTheme); + } + } + + getEffectiveTheme() { + if (SettingsStore.getValue('use_system_theme')) { + if (this._preferDark.matches) return 'dark'; + if (this._preferLight.matches) return 'light'; + } + return SettingsStore.getValue('theme'); + } + + isSystemThemeSupported() { + return this._preferDark || this._preferLight; + } +} + export function enumerateThemes() { const BUILTIN_THEMES = { "light": _t("Light theme"), @@ -83,7 +147,8 @@ export function getBaseTheme(theme) { */ export function setTheme(theme) { if (!theme) { - theme = SettingsStore.getValue("theme"); + const themeWatcher = new ThemeWatcher(); + theme = themeWatcher.getEffectiveTheme(); } let stylesheetName = theme; if (theme.startsWith("custom-")) { From 71f5c8b2b045a85beb563be5ef5483a6c0137723 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 20 Nov 2019 13:47:54 +0000 Subject: [PATCH 224/334] Lint --- .../views/settings/tabs/user/GeneralUserSettingsTab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 50f37cea1f..dbe0a9a301 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -188,7 +188,7 @@ export default class GeneralUserSettingsTab extends React.Component { dis.dispatch({action: 'recheck_theme'}); }; - _onUseSystemThemeChanged = (checked) => { + _onUseSystemThemeChanged = (checked) => { this.setState({useSystemTheme: checked}); dis.dispatch({action: 'recheck_theme'}); } From a7444152213fc18e070e9e3ffca92f349c51fb47 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 20 Nov 2019 15:34:32 +0000 Subject: [PATCH 225/334] Add hack to work around mystery settings bug --- .../views/settings/tabs/user/GeneralUserSettingsTab.js | 5 ++++- src/theme.js | 8 +++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index dbe0a9a301..b518f7c81b 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -185,7 +185,10 @@ export default class GeneralUserSettingsTab extends React.Component { // The settings watcher doesn't fire until the echo comes back from the // server, so to make the theme change immediately we need to manually // do the dispatch now - dis.dispatch({action: 'recheck_theme'}); + // XXX: The local echoed value appears to be unreliable, in particular + // when settings custom themes(!) so adding forceTheme to override + // the value from settings. + dis.dispatch({action: 'recheck_theme', forceTheme: newTheme}); }; _onUseSystemThemeChanged = (checked) => { diff --git a/src/theme.js b/src/theme.js index 8996fe28fd..5e390bf2c8 100644 --- a/src/theme.js +++ b/src/theme.js @@ -60,13 +60,15 @@ export class ThemeWatcher { _onAction = (payload) => { if (payload.action === 'recheck_theme') { - this.recheck(); + // XXX forceTheme + this.recheck(payload.forceTheme); } } - recheck() { + // XXX: forceTheme param aded here as local echo appears to be unreliable + recheck(forceTheme) { const oldTheme = this._currentTheme; - this._currentTheme = this.getEffectiveTheme(); + this._currentTheme = forceTheme === undefined ? this.getEffectiveTheme() : forceTheme; if (oldTheme !== this._currentTheme) { setTheme(this._currentTheme); } From 518130c912dfd8cf196be96698fc4d342b79841e Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 20 Nov 2019 15:37:48 +0000 Subject: [PATCH 226/334] add bug link --- src/theme.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/theme.js b/src/theme.js index 5e390bf2c8..43bc813d34 100644 --- a/src/theme.js +++ b/src/theme.js @@ -66,6 +66,7 @@ export class ThemeWatcher { } // XXX: forceTheme param aded here as local echo appears to be unreliable + // https://github.com/vector-im/riot-web/issues/11443 recheck(forceTheme) { const oldTheme = this._currentTheme; this._currentTheme = forceTheme === undefined ? this.getEffectiveTheme() : forceTheme; From b69cee0c6756796ae6db514a2a328fd7b012ff02 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 20 Nov 2019 15:45:32 +0000 Subject: [PATCH 227/334] Remove getBaseTheme This was only used by vector/index.js, in the code removed by https://github.com/vector-im/riot-web/pull/11445 React SDK does a very similar thing in setTheme but also gets the rest of the custom theme name. Requires https://github.com/vector-im/riot-web/pull/11445 --- src/theme.js | 79 ++++++++++++++++++++++------------------------------ 1 file changed, 34 insertions(+), 45 deletions(-) diff --git a/src/theme.js b/src/theme.js index 43bc813d34..634e5cce7f 100644 --- a/src/theme.js +++ b/src/theme.js @@ -127,28 +127,14 @@ function getCustomTheme(themeName) { return customTheme; } -/** - * Gets the underlying theme name for the given theme. This is usually the theme or - * CSS resource that the theme relies upon to load. - * @param {string} theme The theme name to get the base of. - * @returns {string} The base theme (typically "light" or "dark"). - */ -export function getBaseTheme(theme) { - if (!theme) return "light"; - if (theme.startsWith("custom-")) { - const customTheme = getCustomTheme(theme.substr(7)); - return customTheme.is_dark ? "dark-custom" : "light-custom"; - } - - return theme; // it's probably a base theme -} - /** * Called whenever someone changes the theme + * Async function that returns once the theme has been set + * (ie. the CSS has been loaded) * * @param {string} theme new theme */ -export function setTheme(theme) { +export async function setTheme(theme) { if (!theme) { const themeWatcher = new ThemeWatcher(); theme = themeWatcher.getEffectiveTheme(); @@ -190,38 +176,41 @@ export function setTheme(theme) { styleElements[stylesheetName].disabled = false; - const switchTheme = function() { - // we re-enable our theme here just in case we raced with another - // theme set request as per https://github.com/vector-im/riot-web/issues/5601. - // We could alternatively lock or similar to stop the race, but - // this is probably good enough for now. - styleElements[stylesheetName].disabled = false; - Object.values(styleElements).forEach((a) => { - if (a == styleElements[stylesheetName]) return; - a.disabled = true; - }); - Tinter.setTheme(theme); - }; + return new Promise((resolve) => { + const switchTheme = function() { + // we re-enable our theme here just in case we raced with another + // theme set request as per https://github.com/vector-im/riot-web/issues/5601. + // We could alternatively lock or similar to stop the race, but + // this is probably good enough for now. + styleElements[stylesheetName].disabled = false; + Object.values(styleElements).forEach((a) => { + if (a == styleElements[stylesheetName]) return; + a.disabled = true; + }); + Tinter.setTheme(theme); + resolve(); + }; - // turns out that Firefox preloads the CSS for link elements with - // the disabled attribute, but Chrome doesn't. + // turns out that Firefox preloads the CSS for link elements with + // the disabled attribute, but Chrome doesn't. - let cssLoaded = false; + let cssLoaded = false; - styleElements[stylesheetName].onload = () => { - switchTheme(); - }; + styleElements[stylesheetName].onload = () => { + switchTheme(); + }; - for (let i = 0; i < document.styleSheets.length; i++) { - const ss = document.styleSheets[i]; - if (ss && ss.href === styleElements[stylesheetName].href) { - cssLoaded = true; - break; + for (let i = 0; i < document.styleSheets.length; i++) { + const ss = document.styleSheets[i]; + if (ss && ss.href === styleElements[stylesheetName].href) { + cssLoaded = true; + break; + } } - } - if (cssLoaded) { - styleElements[stylesheetName].onload = undefined; - switchTheme(); - } + if (cssLoaded) { + styleElements[stylesheetName].onload = undefined; + switchTheme(); + } + }); } From e36f4375b0bdece26b729679c61e530ca17a26bf Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 20 Nov 2019 16:12:14 +0000 Subject: [PATCH 228/334] Bugfix & clearer setting name --- src/i18n/strings/en_EN.json | 2 +- src/settings/Settings.js | 2 +- src/theme.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5dfcd038ec..c62b39cda0 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -363,7 +363,7 @@ "Automatically replace plain text Emoji": "Automatically replace plain text Emoji", "Mirror local video feed": "Mirror local video feed", "Enable Community Filter Panel": "Enable Community Filter Panel", - "Match system theme": "Match system theme", + "Match system dark mode setting": "Match system dark mode setting", "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 devices from this device": "Never send encrypted messages to unverified devices from this device", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 8a3bc3ecbc..59e60353b8 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -278,7 +278,7 @@ export const SETTINGS = { "use_system_theme": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, default: true, - displayName: _td("Match system theme"), + displayName: _td("Match system dark mode setting"), }, "webRtcAllowPeerToPeer": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, diff --git a/src/theme.js b/src/theme.js index 43bc813d34..fa7e3f783b 100644 --- a/src/theme.js +++ b/src/theme.js @@ -84,7 +84,7 @@ export class ThemeWatcher { } isSystemThemeSupported() { - return this._preferDark || this._preferLight; + return this._preferDark.matches || this._preferLight.matches; } } From 758dd4127fe434cbcb0d1fd88d4361877f4aa458 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 20 Nov 2019 16:36:27 +0000 Subject: [PATCH 229/334] upgrade nunito from 3.500 to 3.504 fixes https://github.com/vector-im/riot-web/issues/8092 by way of https://github.com/google/fonts/issues/632 --- res/fonts/Nunito/Nunito-Bold.ttf | Bin 168112 -> 176492 bytes res/fonts/Nunito/Nunito-Regular.ttf | Bin 165596 -> 172236 bytes res/fonts/Nunito/Nunito-SemiBold.ttf | Bin 166620 -> 175064 bytes .../Nunito/XRXQ3I6Li01BKofIMN44Y9vKUTo.ttf | Bin 47628 -> 0 bytes .../Nunito/XRXQ3I6Li01BKofIMN5cYtvKUTo.ttf | Bin 47796 -> 0 bytes res/fonts/Nunito/XRXV3I6Li01BKofINeaE.ttf | Bin 46796 -> 0 bytes res/fonts/Nunito/XRXW3I6Li01BKofA6sKUYevN.ttf | Bin 47220 -> 0 bytes res/fonts/Nunito/XRXW3I6Li01BKofAjsOUYevN.ttf | Bin 46556 -> 0 bytes res/fonts/Nunito/XRXX3I6Li01BKofIMNaDRss.ttf | Bin 48080 -> 0 bytes 9 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 res/fonts/Nunito/XRXQ3I6Li01BKofIMN44Y9vKUTo.ttf delete mode 100644 res/fonts/Nunito/XRXQ3I6Li01BKofIMN5cYtvKUTo.ttf delete mode 100644 res/fonts/Nunito/XRXV3I6Li01BKofINeaE.ttf delete mode 100644 res/fonts/Nunito/XRXW3I6Li01BKofA6sKUYevN.ttf delete mode 100644 res/fonts/Nunito/XRXW3I6Li01BKofAjsOUYevN.ttf delete mode 100644 res/fonts/Nunito/XRXX3I6Li01BKofIMNaDRss.ttf diff --git a/res/fonts/Nunito/Nunito-Bold.ttf b/res/fonts/Nunito/Nunito-Bold.ttf index c70de76bbd1bf407351094e9c0b092a65082d394..c8fabf7d920a82c54d8bac9cc3ec330e91401709 100644 GIT binary patch delta 68198 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 delta 60528 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 N$WwtOPO44r{{fVkFJ%A# diff --git a/res/fonts/Nunito/Nunito-Regular.ttf b/res/fonts/Nunito/Nunito-Regular.ttf index 064e805431b203c336af6b5c8de7b8708f7304bf..86ce522f609fee54bdbb16dc9337b712611da618 100644 GIT binary patch delta 63829 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 z%0D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 V!#yynga0;3C)ipvN2|W{{{UIbmn;AP delta 59523 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 diff --git a/res/fonts/Nunito/XRXQ3I6Li01BKofIMN44Y9vKUTo.ttf b/res/fonts/Nunito/XRXQ3I6Li01BKofIMN44Y9vKUTo.ttf deleted file mode 100644 index 4387fb67c41bde8b9d73ff8d830eac5718de3324..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47628 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൏#;KetQ>Yi-#CE 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 K=ik%v1OE@M+QcOQ diff --git a/res/fonts/Nunito/XRXQ3I6Li01BKofIMN5cYtvKUTo.ttf b/res/fonts/Nunito/XRXQ3I6Li01BKofIMN5cYtvKUTo.ttf deleted file mode 100644 index 68fb3ff5cbe2150feff0cd5439ce42b470392c4d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47796 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 Xpr=1K{=)dG@s~)R-0l1udfxGW+TqtT diff --git a/res/fonts/Nunito/XRXV3I6Li01BKofINeaE.ttf b/res/fonts/Nunito/XRXV3I6Li01BKofINeaE.ttf deleted file mode 100644 index c40e599260f0b1e1b926cca0885fd7959a22f813..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46796 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�%M1!f7PsX2f$<7;yR<4Pp4&R5~$mrS~ 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
  • <=_r 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 diff --git a/res/fonts/Nunito/XRXW3I6Li01BKofA6sKUYevN.ttf b/res/fonts/Nunito/XRXW3I6Li01BKofA6sKUYevN.ttf deleted file mode 100644 index 0c4fd17dfafb7e3bfba40095065f75dd60902230..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47220 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 diff --git a/res/fonts/Nunito/XRXW3I6Li01BKofAjsOUYevN.ttf b/res/fonts/Nunito/XRXW3I6Li01BKofAjsOUYevN.ttf deleted file mode 100644 index 339d59ac003965bb53004c6d81a9e67061571ea1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46556 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 T-*^7R`BNlMr=)*L%g_D~j~wAG diff --git a/res/fonts/Nunito/XRXX3I6Li01BKofIMNaDRss.ttf b/res/fonts/Nunito/XRXX3I6Li01BKofIMNaDRss.ttf deleted file mode 100644 index b5fcd891af7039f7b3fba5075e85cfed13cf88db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48080 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 From 93e4de986128267b96cf2d307b890c248ed9003b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 20 Nov 2019 11:00:39 -0700 Subject: [PATCH 230/334] Fix positioning and sizing of e2e icon in the composer This also removes the special case class for the composer because the input is now aligned regardless of icon. --- res/css/views/rooms/_MessageComposer.scss | 8 ++++---- src/components/views/rooms/MessageComposer.js | 6 +----- src/components/views/rooms/SlateMessageComposer.js | 6 +----- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index 14562fe7ed..a0c8048475 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -23,10 +23,6 @@ limitations under the License. padding-left: 84px; } -.mx_MessageComposer_wrapper.mx_MessageComposer_hasE2EIcon { - padding-left: 109px; -} - .mx_MessageComposer_replaced_wrapper { margin-left: auto; margin-right: auto; @@ -78,6 +74,10 @@ limitations under the License. .mx_MessageComposer_e2eIcon.mx_E2EIcon { position: absolute; left: 60px; + width: 16px; + height: 16px; + margin-right: 0; // Counteract the E2EIcon class + margin-left: 3px; // Counteract the E2EIcon class &::after { background-color: $composer-e2e-icon-color; diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 632ca53f82..b41b970fc6 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -353,13 +353,9 @@ export default class MessageComposer extends React.Component { ); } - const wrapperClasses = classNames({ - mx_MessageComposer_wrapper: true, - mx_MessageComposer_hasE2EIcon: !!this.props.e2eStatus, - }); return (
    -
    +
    { controls }
    diff --git a/src/components/views/rooms/SlateMessageComposer.js b/src/components/views/rooms/SlateMessageComposer.js index 4bb2f29e61..eb41f6729b 100644 --- a/src/components/views/rooms/SlateMessageComposer.js +++ b/src/components/views/rooms/SlateMessageComposer.js @@ -460,13 +460,9 @@ export default class SlateMessageComposer extends React.Component { const showFormatBar = this.state.showFormatting && this.state.inputState.isRichTextEnabled; - const wrapperClasses = classNames({ - mx_MessageComposer_wrapper: true, - mx_MessageComposer_hasE2EIcon: !!this.props.e2eStatus, - }); return (
    -
    +
    { controls }
    From f6394b1251cef54042914c421dc15306bc0d2980 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 20 Nov 2019 11:03:53 -0700 Subject: [PATCH 231/334] Remove stray colour correction on composer e2e icon We want it to match the room header --- res/css/views/rooms/_MessageComposer.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index a0c8048475..036756e2eb 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -78,10 +78,6 @@ limitations under the License. height: 16px; margin-right: 0; // Counteract the E2EIcon class margin-left: 3px; // Counteract the E2EIcon class - - &::after { - background-color: $composer-e2e-icon-color; - } } .mx_MessageComposer_noperm_error { From 8abc0953d518d7f7c47a1a1b74f92cdaf4a06567 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 20 Nov 2019 11:08:53 -0700 Subject: [PATCH 232/334] Remove the import my IDE should have removed for me --- src/components/views/rooms/MessageComposer.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index b41b970fc6..128f9be964 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -25,7 +25,6 @@ import RoomViewStore from '../../../stores/RoomViewStore'; import Stickerpicker from './Stickerpicker'; import { makeRoomPermalink } from '../../../utils/permalinks/Permalinks'; import ContentMessages from '../../../ContentMessages'; -import classNames from 'classnames'; import E2EIcon from './E2EIcon'; function ComposerAvatar(props) { From 966f84115dc16415269e5285d104f90e3596a988 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 20 Nov 2019 18:26:29 +0000 Subject: [PATCH 233/334] js-sdk rc.1 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index eb234e0573..57e8dd77e3 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "linkifyjs": "^2.1.6", "lodash": "^4.17.14", "lolex": "4.2", - "matrix-js-sdk": "2.4.3", + "matrix-js-sdk": "2.4.4-rc.1", "optimist": "^0.6.1", "pako": "^1.0.5", "png-chunks-extract": "^1.0.0", diff --git a/yarn.lock b/yarn.lock index 3e43c29ef6..eb72b11793 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5197,10 +5197,10 @@ mathml-tag-names@^2.0.1: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.1.tgz#6dff66c99d55ecf739ca53c492e626f1d12a33cc" integrity sha512-pWB896KPGSGkp1XtyzRBftpTzwSOL0Gfk0wLvxt4f2mgzjY19o0LxJ3U25vNWTzsh7da+KTbuXQoQ3lOJZ8WHw== -matrix-js-sdk@2.4.3: - version "2.4.3" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-2.4.3.tgz#23b78cc707a02eb0ce7eecb3aa50129e46dd5b6e" - integrity sha512-8qTqILd/NmTWF24tpaxmDIzkTk/bZhPD5N8h69PlvJ5Y6kMFctpRj+Tud5zZjl5/yhO07+g+JCyDzg+AagiM/A== +matrix-js-sdk@2.4.4-rc.1: + version "2.4.4-rc.1" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-2.4.4-rc.1.tgz#5fd33fd11be9eea23cd0d0b8eb79da7a4b6253bf" + integrity sha512-Kn94zZMXh2EmihYL3lWNp2lpT7RtqcaUxjkP7H9Mr113swSOXtKr8RWMrvopAIguC1pcLzL+lCk+N8rrML2A4Q== dependencies: another-json "^0.2.0" babel-runtime "^6.26.0" From 318a720e75e9e70831aa7f9cd775cb902274b657 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 20 Nov 2019 18:29:16 +0000 Subject: [PATCH 234/334] Prepare changelog for v1.7.3-rc.1 --- CHANGELOG.md | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c46530fad..2dad0accd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,97 @@ +Changes in [1.7.3-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.3-rc.1) (2019-11-20) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.2...v1.7.3-rc.1) + + * Fix positioning, size, and colour of the composer e2e icon + [\#3641](https://github.com/matrix-org/matrix-react-sdk/pull/3641) + * upgrade nunito from 3.500 to 3.504 + [\#3639](https://github.com/matrix-org/matrix-react-sdk/pull/3639) + * Wire up the widget permission prompt to the cross-platform setting + [\#3630](https://github.com/matrix-org/matrix-react-sdk/pull/3630) + * Get theme automatically from system setting + [\#3637](https://github.com/matrix-org/matrix-react-sdk/pull/3637) + * Update code style for our 90 char life + [\#3636](https://github.com/matrix-org/matrix-react-sdk/pull/3636) + * use general warning icon instead of e2e one for room status + [\#3633](https://github.com/matrix-org/matrix-react-sdk/pull/3633) + * Add support for platform specific event indexing and search + [\#3550](https://github.com/matrix-org/matrix-react-sdk/pull/3550) + * Update from Weblate + [\#3635](https://github.com/matrix-org/matrix-react-sdk/pull/3635) + * Use a settings watcher to set the theme + [\#3634](https://github.com/matrix-org/matrix-react-sdk/pull/3634) + * Merge the `feature_user_info_panel` flag into `feature_dm_verification` + [\#3632](https://github.com/matrix-org/matrix-react-sdk/pull/3632) + * Fix some styling regressions in member panel + [\#3631](https://github.com/matrix-org/matrix-react-sdk/pull/3631) + * Add a bit more safety around breadcrumbs + [\#3629](https://github.com/matrix-org/matrix-react-sdk/pull/3629) + * Ensure widgets always have a sender associated with them + [\#3628](https://github.com/matrix-org/matrix-react-sdk/pull/3628) + * re-add missing case of codepath + [\#3627](https://github.com/matrix-org/matrix-react-sdk/pull/3627) + * Implement the bulk of the new widget permission prompt design + [\#3622](https://github.com/matrix-org/matrix-react-sdk/pull/3622) + * Relax identity server discovery error handling + [\#3588](https://github.com/matrix-org/matrix-react-sdk/pull/3588) + * Add cross-signing feature flag + [\#3626](https://github.com/matrix-org/matrix-react-sdk/pull/3626) + * Attempt number two at ripping out Bluebird from rageshake.js + [\#3624](https://github.com/matrix-org/matrix-react-sdk/pull/3624) + * Update from Weblate + [\#3625](https://github.com/matrix-org/matrix-react-sdk/pull/3625) + * Remove Bluebird: phase 2.1 + [\#3618](https://github.com/matrix-org/matrix-react-sdk/pull/3618) + * Add better error handling to Synapse user deactivation + [\#3619](https://github.com/matrix-org/matrix-react-sdk/pull/3619) + * New design for member panel + [\#3620](https://github.com/matrix-org/matrix-react-sdk/pull/3620) + * Show server details on login for unreachable homeserver + [\#3617](https://github.com/matrix-org/matrix-react-sdk/pull/3617) + * Add a function to get the "base" theme for a theme + [\#3615](https://github.com/matrix-org/matrix-react-sdk/pull/3615) + * Remove Bluebird: phase 2 + [\#3616](https://github.com/matrix-org/matrix-react-sdk/pull/3616) + * Remove Bluebird: phase 1 + [\#3612](https://github.com/matrix-org/matrix-react-sdk/pull/3612) + * Move notification count to in front of the room name in the page title + [\#3613](https://github.com/matrix-org/matrix-react-sdk/pull/3613) + * Add some logging/recovery for lost rooms + [\#3614](https://github.com/matrix-org/matrix-react-sdk/pull/3614) + * Add Mjolnir ban list support + [\#3585](https://github.com/matrix-org/matrix-react-sdk/pull/3585) + * Improve room switching performance with alias cache + [\#3610](https://github.com/matrix-org/matrix-react-sdk/pull/3610) + * Fix draw order when hovering composer format buttons + [\#3609](https://github.com/matrix-org/matrix-react-sdk/pull/3609) + * Use a ternary operator instead of relying on AND semantics in + EditHistoryDialog + [\#3606](https://github.com/matrix-org/matrix-react-sdk/pull/3606) + * Update from Weblate + [\#3608](https://github.com/matrix-org/matrix-react-sdk/pull/3608) + * Fix HTML fallback in replies + [\#3607](https://github.com/matrix-org/matrix-react-sdk/pull/3607) + * Fix rounded corners for the formatting toolbar + [\#3605](https://github.com/matrix-org/matrix-react-sdk/pull/3605) + * Check for a message type before assuming it is a room message + [\#3604](https://github.com/matrix-org/matrix-react-sdk/pull/3604) + * Remove lint comments about no-descending-specificity + [\#3603](https://github.com/matrix-org/matrix-react-sdk/pull/3603) + * Show verification requests in the timeline + [\#3601](https://github.com/matrix-org/matrix-react-sdk/pull/3601) + * Match identity server registration to the IS r0.3.0 spec + [\#3602](https://github.com/matrix-org/matrix-react-sdk/pull/3602) + * Restore thumbs after variation selector removal + [\#3600](https://github.com/matrix-org/matrix-react-sdk/pull/3600) + * Fix breadcrumbs so the bar is a toolbar and the buttons are buttons. + [\#3599](https://github.com/matrix-org/matrix-react-sdk/pull/3599) + * Now that part of spacing is padding, make it smaller when collapsed + [\#3597](https://github.com/matrix-org/matrix-react-sdk/pull/3597) + * Remove variation selectors from quick reactions + [\#3598](https://github.com/matrix-org/matrix-react-sdk/pull/3598) + * Fix linkify imports + [\#3595](https://github.com/matrix-org/matrix-react-sdk/pull/3595) + Changes in [1.7.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.2) (2019-11-06) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.1...v1.7.2) From ef475bbdadd9c4ae1ab9057b6b18602cf39af137 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 20 Nov 2019 18:29:16 +0000 Subject: [PATCH 235/334] v1.7.3-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 57e8dd77e3..e5d2d7635c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "1.7.2", + "version": "1.7.3-rc.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From fccf9f138e53fd8286cd42066233a16eff814d00 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 20 Nov 2019 22:32:11 +0000 Subject: [PATCH 236/334] Add eslint-plugin-jest because we inherit js-sdk's eslintrc and it wants Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- package.json | 1 + yarn.lock | 72 +++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index eb234e0573..fe399e4c49 100644 --- a/package.json +++ b/package.json @@ -135,6 +135,7 @@ "eslint": "^5.12.0", "eslint-config-google": "^0.7.1", "eslint-plugin-babel": "^5.2.1", + "eslint-plugin-jest": "^23.0.4", "eslint-plugin-flowtype": "^2.30.0", "eslint-plugin-react": "^7.7.0", "eslint-plugin-react-hooks": "^2.0.1", diff --git a/yarn.lock b/yarn.lock index 3e43c29ef6..8bfaaa74c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -244,6 +244,11 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" +"@types/json-schema@^7.0.3": + version "7.0.3" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636" + integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A== + "@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" @@ -293,6 +298,28 @@ dependencies: "@types/yargs-parser" "*" +"@typescript-eslint/experimental-utils@^2.5.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.8.0.tgz#208b4164d175587e9b03ce6fea97d55f19c30ca9" + integrity sha512-jZ05E4SxCbbXseQGXOKf3ESKcsGxT8Ucpkp1jiVp55MGhOvZB2twmWKf894PAuVQTCgbPbJz9ZbRDqtUWzP8xA== + dependencies: + "@types/json-schema" "^7.0.3" + "@typescript-eslint/typescript-estree" "2.8.0" + eslint-scope "^5.0.0" + +"@typescript-eslint/typescript-estree@2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.8.0.tgz#fcc3fe6532840085d29b75432c8a59895876aeca" + integrity sha512-ksvjBDTdbAQ04cR5JyFSDX113k66FxH1tAXmi+dj6hufsl/G0eMc/f1GgLjEVPkYClDbRKv+rnBFuE5EusomUw== + dependencies: + debug "^4.1.1" + eslint-visitor-keys "^1.1.0" + glob "^7.1.6" + is-glob "^4.0.1" + lodash.unescape "4.0.1" + semver "^6.3.0" + tsutils "^3.17.1" + "@webassemblyjs/ast@1.8.5": version "1.8.5" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359" @@ -2891,6 +2918,13 @@ eslint-plugin-flowtype@^2.30.0: dependencies: lodash "^4.17.10" +eslint-plugin-jest@^23.0.4: + version "23.0.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-23.0.4.tgz#1ab81ffe3b16c5168efa72cbd4db14d335092aa0" + integrity sha512-OaP8hhT8chJNodUPvLJ6vl8gnalcsU/Ww1t9oR3HnGdEWjm/DdCCUXLOral+IPGAeWu/EwgVQCK/QtxALpH1Yw== + dependencies: + "@typescript-eslint/experimental-utils" "^2.5.0" + eslint-plugin-react-hooks@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.0.1.tgz#e898ec26a0a335af6f7b0ad1f0bedda7143ed756" @@ -2924,6 +2958,14 @@ eslint-scope@^4.0.3: esrecurse "^4.1.0" estraverse "^4.1.1" +eslint-scope@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9" + integrity sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw== + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + eslint-utils@^1.3.1: version "1.4.2" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.2.tgz#166a5180ef6ab7eb462f162fd0e6f2463d7309ab" @@ -2931,7 +2973,7 @@ eslint-utils@^1.3.1: dependencies: eslint-visitor-keys "^1.0.0" -eslint-visitor-keys@^1.0.0: +eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== @@ -3714,6 +3756,18 @@ glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + global-modules@2.0.0, global-modules@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" @@ -5038,6 +5092,11 @@ lodash.mergewith@^4.6.1: resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== +lodash.unescape@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c" + integrity sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw= + lodash@^4.1.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.2.1: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" @@ -7149,7 +7208,7 @@ selection-is-backward@^1.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@6.3.0: +semver@6.3.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== @@ -8086,11 +8145,18 @@ trough@^1.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.4.tgz#3b52b1f13924f460c3fbfd0df69b587dbcbc762e" integrity sha512-tdzBRDGWcI1OpPVmChbdSKhvSVurznZ8X36AYURAcl+0o2ldlCY2XPzyXNNxwJwwyIU+rIglTCG4kxtNKBQH7Q== -tslib@^1.9.0: +tslib@^1.8.1, tslib@^1.9.0: version "1.10.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== +tsutils@^3.17.1: + version "3.17.1" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" + integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g== + dependencies: + tslib "^1.8.1" + tty-browserify@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" From 62a2c7a51a58bbfb992565868da873ccbc65d682 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 20 Nov 2019 16:26:06 -0700 Subject: [PATCH 237/334] Re-add encryption warning to widget permission prompt --- src/components/views/elements/AppPermission.js | 5 ++++- src/components/views/elements/AppTile.js | 2 ++ src/i18n/strings/en_EN.json | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/AppPermission.js b/src/components/views/elements/AppPermission.js index 422427d4c4..c514dbc950 100644 --- a/src/components/views/elements/AppPermission.js +++ b/src/components/views/elements/AppPermission.js @@ -30,6 +30,7 @@ export default class AppPermission extends React.Component { creatorUserId: PropTypes.string.isRequired, roomId: PropTypes.string.isRequired, onPermissionGranted: PropTypes.func.isRequired, + isRoomEncrypted: PropTypes.bool, }; static defaultProps = { @@ -114,6 +115,8 @@ export default class AppPermission extends React.Component { : _t("Using this widget may share data with %(widgetDomain)s.", {widgetDomain: this.state.widgetDomain}, {helpIcon: () => warningTooltip}); + const encryptionWarning = this.props.isRoomEncrypted ? _t("Widgets are not encrypted.") : null; + return (
    @@ -128,7 +131,7 @@ export default class AppPermission extends React.Component { {warning}
    - {_t("This widget may use cookies.")} + {_t("This widget may use cookies.")} {encryptionWarning}
    diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index db5978c792..4b0079098a 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -585,12 +585,14 @@ export default class AppTile extends React.Component {
    ); if (!this.state.hasPermissionToLoad) { + const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId); appTileBody = (
    diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7709a4a398..8b71a1e182 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1195,6 +1195,7 @@ "Widget ID": "Widget ID", "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "Using this widget may share data with %(widgetDomain)s & your Integration Manager.", "Using this widget may share data with %(widgetDomain)s.": "Using this widget may share data with %(widgetDomain)s.", + "Widgets are not encrypted.": "Widgets are not encrypted.", "Widget added by": "Widget added by", "This widget may use cookies.": "This widget may use cookies.", "Delete Widget": "Delete Widget", From a40194194d10756414703abad5fc45cd5d180188 Mon Sep 17 00:00:00 2001 From: bkil Date: Thu, 21 Nov 2019 01:50:18 +0100 Subject: [PATCH 238/334] ReactionsRowButtonTooltip: fix null dereference if emoji owner left room Signed-off-by: bkil --- src/components/views/messages/ReactionsRowButtonTooltip.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/messages/ReactionsRowButtonTooltip.js b/src/components/views/messages/ReactionsRowButtonTooltip.js index b70724d516..d7e1ef3488 100644 --- a/src/components/views/messages/ReactionsRowButtonTooltip.js +++ b/src/components/views/messages/ReactionsRowButtonTooltip.js @@ -43,7 +43,8 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent { if (room) { const senders = []; for (const reactionEvent of reactionEvents) { - const { name } = room.getMember(reactionEvent.getSender()); + const member = room.getMember(reactionEvent.getSender()); + const name = member ? member.name : reactionEvent.getSender(); senders.push(name); } const shortName = unicodeToShortcode(content); From fd12eb28e76eab962d7748a63ce00907106c67c5 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 20 Nov 2019 19:17:42 -0700 Subject: [PATCH 239/334] Move many widget options to a context menu Part of https://github.com/vector-im/riot-web/issues/11262 --- res/css/_components.scss | 1 + .../context_menus/_WidgetContextMenu.scss | 36 +++++ res/css/views/rooms/_AppsDrawer.scss | 32 +---- res/img/feather-customised/widget/bin.svg | 65 --------- res/img/feather-customised/widget/camera.svg | 6 - res/img/feather-customised/widget/edit.svg | 6 - res/img/feather-customised/widget/refresh.svg | 6 - .../feather-customised/widget/x-circle.svg | 6 - .../views/context_menus/WidgetContextMenu.js | 134 ++++++++++++++++++ src/components/views/elements/AppTile.js | 105 +++++++------- src/i18n/strings/en_EN.json | 8 +- 11 files changed, 234 insertions(+), 171 deletions(-) create mode 100644 res/css/views/context_menus/_WidgetContextMenu.scss delete mode 100644 res/img/feather-customised/widget/bin.svg delete mode 100644 res/img/feather-customised/widget/camera.svg delete mode 100644 res/img/feather-customised/widget/edit.svg delete mode 100644 res/img/feather-customised/widget/refresh.svg delete mode 100644 res/img/feather-customised/widget/x-circle.svg create mode 100644 src/components/views/context_menus/WidgetContextMenu.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 40a2c576d0..45c0443cfb 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -48,6 +48,7 @@ @import "./views/context_menus/_StatusMessageContextMenu.scss"; @import "./views/context_menus/_TagTileContextMenu.scss"; @import "./views/context_menus/_TopLeftMenu.scss"; +@import "./views/context_menus/_WidgetContextMenu.scss"; @import "./views/dialogs/_AddressPickerDialog.scss"; @import "./views/dialogs/_Analytics.scss"; @import "./views/dialogs/_ChangelogDialog.scss"; diff --git a/res/css/views/context_menus/_WidgetContextMenu.scss b/res/css/views/context_menus/_WidgetContextMenu.scss new file mode 100644 index 0000000000..314c3be7bb --- /dev/null +++ b/res/css/views/context_menus/_WidgetContextMenu.scss @@ -0,0 +1,36 @@ +/* +Copyright 2019 The Matrix.org Foundaction 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_WidgetContextMenu { + padding: 6px; + + .mx_WidgetContextMenu_option { + padding: 3px 6px 3px 6px; + cursor: pointer; + white-space: nowrap; + } + + .mx_WidgetContextMenu_separator { + margin-top: 0; + margin-bottom: 0; + border-bottom-style: none; + border-left-style: none; + border-right-style: none; + border-top-style: solid; + border-top-width: 1px; + border-color: $menu-border-color; + } +} diff --git a/res/css/views/rooms/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss index 6f5e3abade..a3fe573ad0 100644 --- a/res/css/views/rooms/_AppsDrawer.scss +++ b/res/css/views/rooms/_AppsDrawer.scss @@ -153,40 +153,12 @@ $AppsDrawerBodyHeight: 273px; background-color: $accent-color; } -.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_reload { - mask-image: url('$(res)/img/feather-customised/widget/refresh.svg'); - mask-size: 100%; -} - .mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_popout { mask-image: url('$(res)/img/feather-customised/widget/external-link.svg'); } -.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_snapshot { - mask-image: url('$(res)/img/feather-customised/widget/camera.svg'); -} - -.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_edit { - mask-image: url('$(res)/img/feather-customised/widget/edit.svg'); -} - -.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_delete { - mask-image: url('$(res)/img/feather-customised/widget/bin.svg'); - background-color: $warning-color; -} - -.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_cancel { - mask-image: url('$(res)/img/feather-customised/widget/x-circle.svg'); -} - -/* delete ? */ -.mx_AppTileMenuBarWidget { - cursor: pointer; - width: 10px; - height: 10px; - padding: 1px; - transition-duration: 500ms; - border: 1px solid transparent; +.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_menu { + mask-image: url('$(res)/img/icon_context.svg'); } .mx_AppTileMenuBarWidgetDelete { diff --git a/res/img/feather-customised/widget/bin.svg b/res/img/feather-customised/widget/bin.svg deleted file mode 100644 index 7616d8931b..0000000000 --- a/res/img/feather-customised/widget/bin.svg +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - - - diff --git a/res/img/feather-customised/widget/camera.svg b/res/img/feather-customised/widget/camera.svg deleted file mode 100644 index 5502493068..0000000000 --- a/res/img/feather-customised/widget/camera.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/res/img/feather-customised/widget/edit.svg b/res/img/feather-customised/widget/edit.svg deleted file mode 100644 index 749e83f982..0000000000 --- a/res/img/feather-customised/widget/edit.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/res/img/feather-customised/widget/refresh.svg b/res/img/feather-customised/widget/refresh.svg deleted file mode 100644 index 0994bbdd52..0000000000 --- a/res/img/feather-customised/widget/refresh.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/res/img/feather-customised/widget/x-circle.svg b/res/img/feather-customised/widget/x-circle.svg deleted file mode 100644 index 951407b39c..0000000000 --- a/res/img/feather-customised/widget/x-circle.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/components/views/context_menus/WidgetContextMenu.js b/src/components/views/context_menus/WidgetContextMenu.js new file mode 100644 index 0000000000..43e7e172cc --- /dev/null +++ b/src/components/views/context_menus/WidgetContextMenu.js @@ -0,0 +1,134 @@ +/* +Copyright 2019 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 PropTypes from 'prop-types'; +import sdk from '../../../index'; +import {_t} from '../../../languageHandler'; + +export default class WidgetContextMenu extends React.Component { + static propTypes = { + onFinished: PropTypes.func, + + // Callback for when the revoke button is clicked. Required. + onRevokeClicked: PropTypes.func.isRequired, + + // Callback for when the snapshot button is clicked. Button not shown + // without a callback. + onSnapshotClicked: PropTypes.func, + + // Callback for when the reload button is clicked. Button not shown + // without a callback. + onReloadClicked: PropTypes.func, + + // Callback for when the edit button is clicked. Button not shown + // without a callback. + onEditClicked: PropTypes.func, + + // Callback for when the delete button is clicked. Button not shown + // without a callback. + onDeleteClicked: PropTypes.func, + }; + + proxyClick(fn) { + fn(); + if (this.props.onFinished) this.props.onFinished(); + } + + // XXX: It's annoying that our context menus require us to hit onFinished() to close :( + + onEditClicked = () => { + this.proxyClick(this.props.onEditClicked); + }; + + onReloadClicked = () => { + this.proxyClick(this.props.onReloadClicked); + }; + + onSnapshotClicked = () => { + this.proxyClick(this.props.onSnapshotClicked); + }; + + onDeleteClicked = () => { + this.proxyClick(this.props.onDeleteClicked); + }; + + onRevokeClicked = () => { + this.proxyClick(this.props.onRevokeClicked); + }; + + render() { + const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton"); + + const options = []; + + if (this.props.onEditClicked) { + options.push( + + {_t("Edit")} + , + ); + } + + if (this.props.onReloadClicked) { + options.push( + + {_t("Reload")} + , + ); + } + + if (this.props.onSnapshotClicked) { + options.push( + + {_t("Take picture")} + , + ); + } + + if (this.props.onDeleteClicked) { + options.push( + + {_t("Remove for everyone")} + , + ); + } + + // Push this last so it appears last. It's always present. + options.push( + + {_t("Remove for me")} + , + ); + + // Put separators between the options + if (options.length > 1) { + const length = options.length; + for (let i = 0; i < length - 1; i++) { + const sep =
    ; + + // Insert backwards so the insertions don't affect our math on where to place them. + // We also use our cached length to avoid worrying about options.length changing + options.splice(length - 1 - i, 0, sep); + } + } + + return
    {options}
    ; + } +} diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index db5978c792..0010e8022e 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -35,6 +35,7 @@ import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; import classNames from 'classnames'; import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; +import {createMenu} from "../../structures/ContextualMenu"; const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:']; const ENABLE_REACT_PERF = false; @@ -52,7 +53,7 @@ export default class AppTile extends React.Component { this._onLoaded = this._onLoaded.bind(this); this._onEditClick = this._onEditClick.bind(this); this._onDeleteClick = this._onDeleteClick.bind(this); - this._onCancelClick = this._onCancelClick.bind(this); + this._onRevokeClicked = this._onRevokeClicked.bind(this); this._onSnapshotClick = this._onSnapshotClick.bind(this); this.onClickMenuBar = this.onClickMenuBar.bind(this); this._onMinimiseClick = this._onMinimiseClick.bind(this); @@ -271,7 +272,7 @@ export default class AppTile extends React.Component { return WidgetUtils.canUserModifyWidgets(this.props.room.roomId); } - _onEditClick(e) { + _onEditClick() { console.log("Edit widget ID ", this.props.id); if (this.props.onEditClick) { this.props.onEditClick(); @@ -293,7 +294,7 @@ export default class AppTile extends React.Component { } } - _onSnapshotClick(e) { + _onSnapshotClick() { console.warn("Requesting widget snapshot"); ActiveWidgetStore.getWidgetMessaging(this.props.id).getScreenshot() .catch((err) => { @@ -360,13 +361,9 @@ export default class AppTile extends React.Component { } } - _onCancelClick() { - if (this.props.onDeleteClick) { - this.props.onDeleteClick(); - } else { - console.log("Revoke widget permissions - %s", this.props.id); - this._revokeWidgetPermission(); - } + _onRevokeClicked() { + console.log("Revoke widget permissions - %s", this.props.id); + this._revokeWidgetPermission(); } /** @@ -544,18 +541,59 @@ export default class AppTile extends React.Component { } } - _onPopoutWidgetClick(e) { + _onPopoutWidgetClick() { // Using Object.assign workaround as the following opens in a new window instead of a new tab. // window.open(this._getSafeUrl(), '_blank', 'noopener=yes'); Object.assign(document.createElement('a'), { target: '_blank', href: this._getSafeUrl(), rel: 'noopener'}).click(); } - _onReloadWidgetClick(e) { + _onReloadWidgetClick() { // Reload iframe in this way to avoid cross-origin restrictions this.refs.appFrame.src = this.refs.appFrame.src; } + _getMenuOptions(ev) { + // TODO: This block of code gets copy/pasted a lot. We should make that happen less. + const menuOptions = {}; + const buttonRect = ev.target.getBoundingClientRect(); + // The window X and Y offsets are to adjust position when zoomed in to page + const buttonLeft = buttonRect.left + window.pageXOffset; + const buttonTop = buttonRect.top + window.pageYOffset; + // Align the right edge of the menu to the left edge of the button + menuOptions.right = window.innerWidth - buttonLeft; + // Align the menu vertically on whichever side of the button has more + // space available. + if (buttonTop < window.innerHeight / 2) { + menuOptions.top = buttonTop; + } else { + menuOptions.bottom = window.innerHeight - buttonTop; + } + return menuOptions; + } + + _onContextMenuClick = (ev) => { + const WidgetContextMenu = sdk.getComponent('views.context_menus.WidgetContextMenu'); + const menuOptions = { + ...this._getMenuOptions(ev), + + // A revoke handler is always required + onRevokeClicked: this._onRevokeClicked, + }; + + const canUserModify = this._canUserModify(); + const showEditButton = Boolean(this._scalarClient && canUserModify); + const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify; + const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show; + + if (showEditButton) menuOptions.onEditClicked = this._onEditClick; + if (showDeleteButton) menuOptions.onDeleteClicked = this._onDeleteClick; + if (showPictureSnapshotButton) menuOptions.onSnapshotClicked = this._onSnapshotClick; + if (this.props.showReload) menuOptions.onReloadClicked = this._onReloadWidgetClick; + + createMenu(WidgetContextMenu, menuOptions); + }; + render() { let appTileBody; @@ -565,7 +603,7 @@ export default class AppTile extends React.Component { } // Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin - // because that would allow the iframe to prgramatically remove the sandbox attribute, but + // because that would allow the iframe to programmatically remove the sandbox attribute, but // this would only be for content hosted on the same origin as the riot client: anything // hosted on the same origin as the client will get the same access as if you clicked // a link to it. @@ -643,13 +681,6 @@ export default class AppTile extends React.Component { } } - // editing is done in scalar - const canUserModify = this._canUserModify(); - const showEditButton = Boolean(this._scalarClient && canUserModify); - const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify; - const showCancelButton = (this.props.showCancel === undefined || this.props.showCancel) && !showDeleteButton; - // Picture snapshot - only show button when apps are maximised. - const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show; const showMinimiseButton = this.props.showMinimise && this.props.show; const showMaximiseButton = this.props.showMinimise && !this.props.show; @@ -688,41 +719,17 @@ export default class AppTile extends React.Component { { this.props.showTitle && this._getTileTitle() } - { /* Reload widget */ } - { this.props.showReload && } { /* Popout widget */ } { this.props.showPopout && } - { /* Snapshot widget */ } - { showPictureSnapshotButton && } - { /* Edit widget */ } - { showEditButton && } - { /* Delete widget */ } - { showDeleteButton && } - { /* Cancel widget */ } - { showCancelButton && }
    } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7709a4a398..dbe5cb3e08 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1204,10 +1204,8 @@ "An error ocurred whilst trying to remove the widget from the room": "An error ocurred whilst trying to remove the widget from the room", "Minimize apps": "Minimize apps", "Maximize apps": "Maximize apps", - "Reload widget": "Reload widget", "Popout widget": "Popout widget", - "Picture": "Picture", - "Revoke widget access": "Revoke widget access", + "More options": "More options", "Create new room": "Create new room", "Unblacklist": "Unblacklist", "Blacklist": "Blacklist", @@ -1564,6 +1562,10 @@ "Hide": "Hide", "Home": "Home", "Sign in": "Sign in", + "Reload": "Reload", + "Take picture": "Take picture", + "Remove for everyone": "Remove for everyone", + "Remove for me": "Remove for me", "powered by Matrix": "powered by Matrix", "This homeserver would like to make sure you are not a robot.": "This homeserver would like to make sure you are not a robot.", "Custom Server Options": "Custom Server Options", From 66c51704cc7edb7f2bb758b5eba785f07681b200 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 20 Nov 2019 19:21:10 -0700 Subject: [PATCH 240/334] Fix indentation --- .../context_menus/_WidgetContextMenu.scss | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/res/css/views/context_menus/_WidgetContextMenu.scss b/res/css/views/context_menus/_WidgetContextMenu.scss index 314c3be7bb..60b7b93f99 100644 --- a/res/css/views/context_menus/_WidgetContextMenu.scss +++ b/res/css/views/context_menus/_WidgetContextMenu.scss @@ -17,20 +17,20 @@ limitations under the License. .mx_WidgetContextMenu { padding: 6px; - .mx_WidgetContextMenu_option { - padding: 3px 6px 3px 6px; - cursor: pointer; - white-space: nowrap; - } + .mx_WidgetContextMenu_option { + padding: 3px 6px 3px 6px; + cursor: pointer; + white-space: nowrap; + } - .mx_WidgetContextMenu_separator { - margin-top: 0; - margin-bottom: 0; - border-bottom-style: none; - border-left-style: none; - border-right-style: none; - border-top-style: solid; - border-top-width: 1px; - border-color: $menu-border-color; - } + .mx_WidgetContextMenu_separator { + margin-top: 0; + margin-bottom: 0; + border-bottom-style: none; + border-left-style: none; + border-right-style: none; + border-top-style: solid; + border-top-width: 1px; + border-color: $menu-border-color; + } } From b0eb54541cbe18c67c91f9017ec609478bf53ce1 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 20 Nov 2019 19:50:13 -0700 Subject: [PATCH 241/334] Rip out options to change your integration manager We are not supporting this due to the complexity involved in switching integration managers. We still support custom ones under the hood, just not to the common user. A later sprint on integrations will consider re-adding the option alongside fixing the various bugs out there. --- .../settings/_SetIntegrationManager.scss | 8 - .../views/settings/SetIntegrationManager.js | 153 +----------------- 2 files changed, 2 insertions(+), 159 deletions(-) diff --git a/res/css/views/settings/_SetIntegrationManager.scss b/res/css/views/settings/_SetIntegrationManager.scss index 99537f9eb4..454fb95cf7 100644 --- a/res/css/views/settings/_SetIntegrationManager.scss +++ b/res/css/views/settings/_SetIntegrationManager.scss @@ -14,10 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_SetIntegrationManager .mx_Field_input { - @mixin mx_Settings_fullWidthField; -} - .mx_SetIntegrationManager { margin-top: 10px; margin-bottom: 10px; @@ -31,7 +27,3 @@ limitations under the License. display: inline-block; padding-left: 5px; } - -.mx_SetIntegrationManager_tooltip { - @mixin mx_Settings_tooltip; -} diff --git a/src/components/views/settings/SetIntegrationManager.js b/src/components/views/settings/SetIntegrationManager.js index b1268c8048..2482b3c846 100644 --- a/src/components/views/settings/SetIntegrationManager.js +++ b/src/components/views/settings/SetIntegrationManager.js @@ -16,13 +16,7 @@ limitations under the License. import React from 'react'; import {_t} from "../../../languageHandler"; -import sdk from '../../../index'; -import Field from "../elements/Field"; import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; -import MatrixClientPeg from "../../../MatrixClientPeg"; -import {SERVICE_TYPES} from "matrix-js-sdk"; -import {IntegrationManagerInstance} from "../../../integrations/IntegrationManagerInstance"; -import Modal from "../../../Modal"; export default class SetIntegrationManager extends React.Component { constructor() { @@ -32,136 +26,10 @@ export default class SetIntegrationManager extends React.Component { this.state = { currentManager, - url: "", // user-entered text - error: null, - busy: false, - checking: false, }; } - _onUrlChanged = (ev) => { - const u = ev.target.value; - this.setState({url: u}); - }; - - _getTooltip = () => { - if (this.state.checking) { - const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner'); - return
    - - { _t("Checking server") } -
    ; - } else if (this.state.error) { - return {this.state.error}; - } else { - return null; - } - }; - - _canChange = () => { - return !!this.state.url && !this.state.busy; - }; - - _continueTerms = async (manager) => { - try { - await IntegrationManagers.sharedInstance().overwriteManagerOnAccount(manager); - this.setState({ - busy: false, - error: null, - currentManager: IntegrationManagers.sharedInstance().getPrimaryManager(), - url: "", // clear input - }); - } catch (e) { - console.error(e); - this.setState({ - busy: false, - error: _t("Failed to update integration manager"), - }); - } - }; - - _setManager = async (ev) => { - // Don't reload the page when the user hits enter in the form. - ev.preventDefault(); - ev.stopPropagation(); - - this.setState({busy: true, checking: true, error: null}); - - let offline = false; - let manager: IntegrationManagerInstance; - try { - manager = await IntegrationManagers.sharedInstance().tryDiscoverManager(this.state.url); - offline = !manager; // no manager implies offline - } catch (e) { - console.error(e); - offline = true; // probably a connection error - } - if (offline) { - this.setState({ - busy: false, - checking: false, - error: _t("Integration manager offline or not accessible."), - }); - return; - } - - // Test the manager (causes terms of service prompt if agreement is needed) - // We also cancel the tooltip at this point so it doesn't collide with the dialog. - this.setState({checking: false}); - try { - const client = manager.getScalarClient(); - await client.connect(); - } catch (e) { - console.error(e); - this.setState({ - busy: false, - error: _t("Terms of service not accepted or the integration manager is invalid."), - }); - return; - } - - // Specifically request the terms of service to see if there are any. - // The above won't trigger a terms of service check if there are no terms to - // sign, so when there's no terms at all we need to ensure we tell the user. - let hasTerms = true; - try { - const terms = await MatrixClientPeg.get().getTerms(SERVICE_TYPES.IM, manager.trimmedApiUrl); - hasTerms = terms && terms['policies'] && Object.keys(terms['policies']).length > 0; - } catch (e) { - // Assume errors mean there are no terms. This could be a 404, 500, etc - console.error(e); - hasTerms = false; - } - if (!hasTerms) { - this.setState({busy: false}); - const QuestionDialog = sdk.getComponent("views.dialogs.QuestionDialog"); - Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, { - title: _t("Integration manager has no terms of service"), - description: ( -
    - - {_t("The integration manager you have chosen does not have any terms of service.")} - - -  {_t("Only continue if you trust the owner of the server.")} - -
    - ), - button: _t("Continue"), - onFinished: async (confirmed) => { - if (!confirmed) return; - this._continueTerms(manager); - }, - }); - return; - } - - this._continueTerms(manager); - }; - render() { - const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton'); - const currentManager = this.state.currentManager; let managerName; let bodyText; @@ -181,7 +49,7 @@ export default class SetIntegrationManager extends React.Component { } return ( -
    +
    {_t("Integration Manager")} {managerName} @@ -189,24 +57,7 @@ export default class SetIntegrationManager extends React.Component { {bodyText} - - {_t("Change")} - +
    ); } } From 0a0e952691808ed17787bf2950825a9567cfd2d8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 20 Nov 2019 19:53:52 -0700 Subject: [PATCH 242/334] Update integration manager copy --- .../views/settings/SetIntegrationManager.js | 15 +++++++++------ src/i18n/strings/en_EN.json | 13 ++++--------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/components/views/settings/SetIntegrationManager.js b/src/components/views/settings/SetIntegrationManager.js index 2482b3c846..11dadb4918 100644 --- a/src/components/views/settings/SetIntegrationManager.js +++ b/src/components/views/settings/SetIntegrationManager.js @@ -36,26 +36,29 @@ export default class SetIntegrationManager extends React.Component { if (currentManager) { managerName = `(${currentManager.name})`; bodyText = _t( - "You are currently using %(serverName)s to manage your bots, widgets, " + + "Use an Integration Manager (%(serverName)s) to manage bots, widgets, " + "and sticker packs.", {serverName: currentManager.name}, { b: sub => {sub} }, ); } else { - bodyText = _t( - "Add which integration manager you want to manage your bots, widgets, " + - "and sticker packs.", - ); + bodyText = _t("Use an Integration Manager to manage bots, widgets, and sticker packs."); } return (
    - {_t("Integration Manager")} + {_t("Integrations")} {managerName}
    {bodyText} +
    +
    + {_t( + "Integration Managers receive configuration data, and can modify widgets, " + + "send room invites, and set power levels on your behalf.", + )}
    ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7709a4a398..7f1a5ab851 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -598,15 +598,10 @@ "Do not use an identity server": "Do not use an identity server", "Enter a new identity server": "Enter a new identity server", "Change": "Change", - "Failed to update integration manager": "Failed to update integration manager", - "Integration manager offline or not accessible.": "Integration manager offline or not accessible.", - "Terms of service not accepted or the integration manager is invalid.": "Terms of service not accepted or the integration manager is invalid.", - "Integration manager has no terms of service": "Integration manager has no terms of service", - "The integration manager you have chosen does not have any terms of service.": "The integration manager you have chosen does not have any terms of service.", - "You are currently using %(serverName)s to manage your bots, widgets, and sticker packs.": "You are currently using %(serverName)s to manage your bots, widgets, and sticker packs.", - "Add which integration manager you want to manage your bots, widgets, and sticker packs.": "Add which integration manager you want to manage your bots, widgets, and sticker packs.", - "Integration Manager": "Integration Manager", - "Enter a new integration manager": "Enter a new integration manager", + "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.", + "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Use an Integration Manager to manage bots, widgets, and sticker packs.", + "Integrations": "Integrations", + "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.", "Flair": "Flair", "Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?", "Success": "Success", From 3391cc0d9017065c777c681381b5dcc0c8f9e9fc Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 20 Nov 2019 20:05:32 -0700 Subject: [PATCH 243/334] Add the toggle switch for provisioning --- .../views/settings/_SetIntegrationManager.scss | 8 ++++++++ .../views/settings/SetIntegrationManager.js | 17 +++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/res/css/views/settings/_SetIntegrationManager.scss b/res/css/views/settings/_SetIntegrationManager.scss index 454fb95cf7..3e59ac73ac 100644 --- a/res/css/views/settings/_SetIntegrationManager.scss +++ b/res/css/views/settings/_SetIntegrationManager.scss @@ -27,3 +27,11 @@ limitations under the License. display: inline-block; padding-left: 5px; } + +.mx_SetIntegrationManager .mx_ToggleSwitch { + display: inline-block; + float: right; + top: 9px; + + @mixin mx_Settings_fullWidthField; +} diff --git a/src/components/views/settings/SetIntegrationManager.js b/src/components/views/settings/SetIntegrationManager.js index 11dadb4918..26c45e3d2a 100644 --- a/src/components/views/settings/SetIntegrationManager.js +++ b/src/components/views/settings/SetIntegrationManager.js @@ -17,6 +17,8 @@ limitations under the License. import React from 'react'; import {_t} from "../../../languageHandler"; import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; +import sdk from '../../../index'; +import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; export default class SetIntegrationManager extends React.Component { constructor() { @@ -26,10 +28,24 @@ export default class SetIntegrationManager extends React.Component { this.state = { currentManager, + provisioningEnabled: SettingsStore.getValue("integrationProvisioning"), }; } + onProvisioningToggled = () => { + const current = this.state.provisioningEnabled; + SettingsStore.setValue("integrationProvisioning", null, SettingLevel.ACCOUNT, !current).catch(err => { + console.error("Error changing integration manager provisioning"); + console.error(err); + + this.setState({provisioningEnabled: current}); + }); + this.setState({provisioningEnabled: !current}); + }; + render() { + const ToggleSwitch = sdk.getComponent("views.elements.ToggleSwitch"); + const currentManager = this.state.currentManager; let managerName; let bodyText; @@ -50,6 +66,7 @@ export default class SetIntegrationManager extends React.Component {
    {_t("Integrations")} {managerName} +
    {bodyText} From 81c9bdd9f33580cc10ced8ac18c7cc31e171f9d8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 20 Nov 2019 20:14:20 -0700 Subject: [PATCH 244/334] It's called an "Integration Manager" (singular) Fixes https://github.com/vector-im/riot-web/issues/11256 This was finally annoying me enough to fix it. --- res/css/_components.scss | 2 +- res/css/views/dialogs/_TermsDialog.scss | 4 ++-- ...sManager.scss => _IntegrationManager.scss} | 10 ++++----- src/CallHandler.js | 2 +- .../dialogs/TabbedIntegrationManagerDialog.js | 8 +++---- src/components/views/dialogs/TermsDialog.js | 2 +- src/components/views/rooms/Stickerpicker.js | 6 ++--- ...ationsManager.js => IntegrationManager.js} | 22 +++++++++---------- src/i18n/strings/en_EN.json | 16 +++++++------- .../IntegrationManagerInstance.js | 14 ++++++------ src/integrations/IntegrationManagers.js | 7 +++--- 11 files changed, 46 insertions(+), 47 deletions(-) rename res/css/views/settings/{_IntegrationsManager.scss => _IntegrationManager.scss} (83%) rename src/components/views/settings/{IntegrationsManager.js => IntegrationManager.js} (81%) diff --git a/res/css/_components.scss b/res/css/_components.scss index 40a2c576d0..dc360c5caa 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -172,7 +172,7 @@ @import "./views/rooms/_WhoIsTypingTile.scss"; @import "./views/settings/_DevicesPanel.scss"; @import "./views/settings/_EmailAddresses.scss"; -@import "./views/settings/_IntegrationsManager.scss"; +@import "./views/settings/_IntegrationManager.scss"; @import "./views/settings/_KeyBackupPanel.scss"; @import "./views/settings/_Notifications.scss"; @import "./views/settings/_PhoneNumbers.scss"; diff --git a/res/css/views/dialogs/_TermsDialog.scss b/res/css/views/dialogs/_TermsDialog.scss index aad679a5b3..beb507e778 100644 --- a/res/css/views/dialogs/_TermsDialog.scss +++ b/res/css/views/dialogs/_TermsDialog.scss @@ -16,10 +16,10 @@ limitations under the License. /* * To avoid visual glitching of two modals stacking briefly, we customise the - * terms dialog sizing when it will appear for the integrations manager so that + * terms dialog sizing when it will appear for the integration manager so that * it gets the same basic size as the IM's own modal. */ -.mx_TermsDialog_forIntegrationsManager .mx_Dialog { +.mx_TermsDialog_forIntegrationManager .mx_Dialog { width: 60%; height: 70%; box-sizing: border-box; diff --git a/res/css/views/settings/_IntegrationsManager.scss b/res/css/views/settings/_IntegrationManager.scss similarity index 83% rename from res/css/views/settings/_IntegrationsManager.scss rename to res/css/views/settings/_IntegrationManager.scss index 8b51eb272e..81b01ab8de 100644 --- a/res/css/views/settings/_IntegrationsManager.scss +++ b/res/css/views/settings/_IntegrationManager.scss @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_IntegrationsManager .mx_Dialog { +.mx_IntegrationManager .mx_Dialog { width: 60%; height: 70%; overflow: hidden; @@ -23,22 +23,22 @@ limitations under the License. max-height: initial; } -.mx_IntegrationsManager iframe { +.mx_IntegrationManager iframe { background-color: #fff; border: 0px; width: 100%; height: 100%; } -.mx_IntegrationsManager_loading h3 { +.mx_IntegrationManager_loading h3 { text-align: center; } -.mx_IntegrationsManager_error { +.mx_IntegrationManager_error { text-align: center; padding-top: 20px; } -.mx_IntegrationsManager_error h3 { +.mx_IntegrationManager_error h3 { color: $warning-color; } diff --git a/src/CallHandler.js b/src/CallHandler.js index bcdf7853fd..625ca8c551 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -382,7 +382,7 @@ function _onAction(payload) { } async function _startCallApp(roomId, type) { - // check for a working integrations manager. Technically we could put + // check for a working integration manager. Technically we could put // the state event in anyway, but the resulting widget would then not // work for us. Better that the user knows before everyone else in the // room sees it. diff --git a/src/components/views/dialogs/TabbedIntegrationManagerDialog.js b/src/components/views/dialogs/TabbedIntegrationManagerDialog.js index 5ef7aef9ab..e86a46fb36 100644 --- a/src/components/views/dialogs/TabbedIntegrationManagerDialog.js +++ b/src/components/views/dialogs/TabbedIntegrationManagerDialog.js @@ -82,10 +82,10 @@ export default class TabbedIntegrationManagerDialog extends React.Component { client.setTermsInteractionCallback((policyInfo, agreedUrls) => { // To avoid visual glitching of two modals stacking briefly, we customise the - // terms dialog sizing when it will appear for the integrations manager so that + // terms dialog sizing when it will appear for the integration manager so that // it gets the same basic size as the IM's own modal. return dialogTermsInteractionCallback( - policyInfo, agreedUrls, 'mx_TermsDialog_forIntegrationsManager', + policyInfo, agreedUrls, 'mx_TermsDialog_forIntegrationManager', ); }); @@ -139,7 +139,7 @@ export default class TabbedIntegrationManagerDialog extends React.Component { } _renderTab() { - const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); + const IntegrationManager = sdk.getComponent("views.settings.IntegrationManager"); let uiUrl = null; if (this.state.currentScalarClient) { uiUrl = this.state.currentScalarClient.getScalarInterfaceUrlForRoom( @@ -148,7 +148,7 @@ export default class TabbedIntegrationManagerDialog extends React.Component { this.props.integrationId, ); } - return {_t("Identity Server")}
    ({host})
    ; case Matrix.SERVICE_TYPES.IM: - return
    {_t("Integrations Manager")}
    ({host})
    ; + return
    {_t("Integration Manager")}
    ({host})
    ; } } diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js index 28e51ed12e..47239cf33f 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.js @@ -74,10 +74,10 @@ export default class Stickerpicker extends React.Component { this.forceUpdate(); return this.scalarClient; }).catch((e) => { - this._imError(_td("Failed to connect to integrations server"), e); + this._imError(_td("Failed to connect to integration manager"), e); }); } else { - this._imError(_td("No integrations server is configured to manage stickers with")); + this._imError(_td("No integration manager is configured to manage stickers with")); } } @@ -346,7 +346,7 @@ export default class Stickerpicker extends React.Component { } /** - * Launch the integrations manager on the stickers integration page + * Launch the integration manager on the stickers integration page */ _launchManageIntegrations() { // TODO: Open the right integration manager for the widget diff --git a/src/components/views/settings/IntegrationsManager.js b/src/components/views/settings/IntegrationManager.js similarity index 81% rename from src/components/views/settings/IntegrationsManager.js rename to src/components/views/settings/IntegrationManager.js index d463b043d5..97c469e9aa 100644 --- a/src/components/views/settings/IntegrationsManager.js +++ b/src/components/views/settings/IntegrationManager.js @@ -21,12 +21,12 @@ import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import dis from '../../../dispatcher'; -export default class IntegrationsManager extends React.Component { +export default class IntegrationManager extends React.Component { static propTypes = { - // false to display an error saying that there is no integrations manager configured + // false to display an error saying that there is no integration manager configured configured: PropTypes.bool.isRequired, - // false to display an error saying that we couldn't connect to the integrations manager + // false to display an error saying that we couldn't connect to the integration manager connected: PropTypes.bool.isRequired, // true to display a loading spinner @@ -72,9 +72,9 @@ export default class IntegrationsManager extends React.Component { render() { if (!this.props.configured) { return ( -
    -

    {_t("No integrations server configured")}

    -

    {_t("This Riot instance does not have an integrations server configured.")}

    +
    +

    {_t("No integration manager configured")}

    +

    {_t("This Riot instance does not have an integration manager configured.")}

    ); } @@ -82,8 +82,8 @@ export default class IntegrationsManager extends React.Component { if (this.props.loading) { const Spinner = sdk.getComponent("elements.Spinner"); return ( -
    -

    {_t("Connecting to integrations server...")}

    +
    +

    {_t("Connecting to integration manager...")}

    ); @@ -91,9 +91,9 @@ export default class IntegrationsManager extends React.Component { if (!this.props.connected) { return ( -
    -

    {_t("Cannot connect to integrations server")}

    -

    {_t("The integrations server is offline or it cannot reach your homeserver.")}

    +
    +

    {_t("Cannot connect to integration manager")}

    +

    {_t("The integration manager is offline or it cannot reach your homeserver.")}

    ); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7f1a5ab851..0735a8e4b3 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -507,11 +507,11 @@ "Failed to set display name": "Failed to set display name", "Disable Notifications": "Disable Notifications", "Enable Notifications": "Enable Notifications", - "No integrations server configured": "No integrations server configured", - "This Riot instance does not have an integrations server configured.": "This Riot instance does not have an integrations server configured.", - "Connecting to integrations server...": "Connecting to integrations server...", - "Cannot connect to integrations server": "Cannot connect to integrations server", - "The integrations server is offline or it cannot reach your homeserver.": "The integrations server is offline or it cannot reach your homeserver.", + "No integration manager configured": "No integration manager configured", + "This Riot instance does not have an integration manager configured.": "This Riot instance does not have an integration manager configured.", + "Connecting to integration manager...": "Connecting to integration manager...", + "Cannot connect to integration manager": "Cannot connect to integration manager", + "The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.", "Delete Backup": "Delete Backup", "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.", "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.", @@ -1019,8 +1019,8 @@ "numbered-list": "numbered-list", "Show Text Formatting Toolbar": "Show Text Formatting Toolbar", "Hide Text Formatting Toolbar": "Hide Text Formatting Toolbar", - "Failed to connect to integrations server": "Failed to connect to integrations server", - "No integrations server is configured to manage stickers with": "No integrations server is configured to manage stickers with", + "Failed to connect to integration manager": "Failed to connect to integration manager", + "No integration manager is configured to manage stickers with": "No integration manager is configured to manage stickers with", "You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled", "Add some now": "Add some now", "Stickerpack": "Stickerpack", @@ -1470,7 +1470,7 @@ "Missing session data": "Missing session data", "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.", "Your browser likely removed this data when running low on disk space.": "Your browser likely removed this data when running low on disk space.", - "Integrations Manager": "Integrations Manager", + "Integration Manager": "Integration Manager", "Find others by phone or email": "Find others by phone or email", "Be found by phone or email": "Be found by phone or email", "Use bots, bridges, widgets and sticker packs": "Use bots, bridges, widgets and sticker packs", diff --git a/src/integrations/IntegrationManagerInstance.js b/src/integrations/IntegrationManagerInstance.js index d36fa73d48..2b616c9fed 100644 --- a/src/integrations/IntegrationManagerInstance.js +++ b/src/integrations/IntegrationManagerInstance.js @@ -57,19 +57,19 @@ export class IntegrationManagerInstance { } async open(room: Room = null, screen: string = null, integrationId: string = null): void { - const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); + const IntegrationManager = sdk.getComponent("views.settings.IntegrationManager"); const dialog = Modal.createTrackedDialog( - 'Integration Manager', '', IntegrationsManager, - {loading: true}, 'mx_IntegrationsManager', + 'Integration Manager', '', IntegrationManager, + {loading: true}, 'mx_IntegrationManager', ); const client = this.getScalarClient(); client.setTermsInteractionCallback((policyInfo, agreedUrls) => { // To avoid visual glitching of two modals stacking briefly, we customise the - // terms dialog sizing when it will appear for the integrations manager so that + // terms dialog sizing when it will appear for the integration manager so that // it gets the same basic size as the IM's own modal. return dialogTermsInteractionCallback( - policyInfo, agreedUrls, 'mx_TermsDialog_forIntegrationsManager', + policyInfo, agreedUrls, 'mx_TermsDialog_forIntegrationManager', ); }); @@ -94,8 +94,8 @@ export class IntegrationManagerInstance { // Close the old dialog and open a new one dialog.close(); Modal.createTrackedDialog( - 'Integration Manager', '', IntegrationsManager, - newProps, 'mx_IntegrationsManager', + 'Integration Manager', '', IntegrationManager, + newProps, 'mx_IntegrationManager', ); } } diff --git a/src/integrations/IntegrationManagers.js b/src/integrations/IntegrationManagers.js index a0fbff56fb..96fd18b5b8 100644 --- a/src/integrations/IntegrationManagers.js +++ b/src/integrations/IntegrationManagers.js @@ -172,11 +172,10 @@ export class IntegrationManagers { } openNoManagerDialog(): void { - // TODO: Is it Integrations (plural) or Integration (singular). Singular is easier spoken. - const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); + const IntegrationManager = sdk.getComponent("views.settings.IntegrationManager"); Modal.createTrackedDialog( - "Integration Manager", "None", IntegrationsManager, - {configured: false}, 'mx_IntegrationsManager', + "Integration Manager", "None", IntegrationManager, + {configured: false}, 'mx_IntegrationManager', ); } From 94fed922cfe3c61ca3fd6169efdb1c4e54405778 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 20 Nov 2019 20:40:39 -0700 Subject: [PATCH 245/334] Intercept cases of disabled/no integration managers We already intercepted most of the cases where no integration manager was present, though there was a bug in many components where openAll() would be called regardless of an integration manager being available. The integration manager being disabled by the user is handled in the IntegrationManager classes rather than on click because we have quite a few calls to these functions. The StickerPicker is an exception because it does slightly different behaviour. This also removes the old "no integration manager configured" state from the IntegrationManager component as it is now replaced by a dialog. --- .../dialogs/IntegrationsDisabledDialog.js | 57 +++++++++++++++++++ .../dialogs/IntegrationsImpossibleDialog.js | 55 ++++++++++++++++++ src/components/views/rooms/Stickerpicker.js | 7 ++- .../views/settings/IntegrationManager.js | 13 ----- src/i18n/strings/en_EN.json | 7 ++- .../IntegrationManagerInstance.js | 6 ++ src/integrations/IntegrationManagers.js | 24 ++++++-- 7 files changed, 147 insertions(+), 22 deletions(-) create mode 100644 src/components/views/dialogs/IntegrationsDisabledDialog.js create mode 100644 src/components/views/dialogs/IntegrationsImpossibleDialog.js diff --git a/src/components/views/dialogs/IntegrationsDisabledDialog.js b/src/components/views/dialogs/IntegrationsDisabledDialog.js new file mode 100644 index 0000000000..3ab1123f8b --- /dev/null +++ b/src/components/views/dialogs/IntegrationsDisabledDialog.js @@ -0,0 +1,57 @@ +/* +Copyright 2019 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 PropTypes from 'prop-types'; +import {_t} from "../../../languageHandler"; +import sdk from "../../../index"; +import dis from '../../../dispatcher'; + +export default class IntegrationsDisabledDialog extends React.Component { + static propTypes = { + onFinished: PropTypes.func.isRequired, + }; + + _onAcknowledgeClick = () => { + this.props.onFinished(); + }; + + _onOpenSettingsClick = () => { + this.props.onFinished(); + dis.dispatch({action: "view_user_settings"}); + }; + + render() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + + return ( + +
    +

    {_t("Enable 'Manage Integrations' in Settings to do this.")}

    +
    + +
    + ); + } +} diff --git a/src/components/views/dialogs/IntegrationsImpossibleDialog.js b/src/components/views/dialogs/IntegrationsImpossibleDialog.js new file mode 100644 index 0000000000..9927f627f1 --- /dev/null +++ b/src/components/views/dialogs/IntegrationsImpossibleDialog.js @@ -0,0 +1,55 @@ +/* +Copyright 2019 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 PropTypes from 'prop-types'; +import {_t} from "../../../languageHandler"; +import sdk from "../../../index"; + +export default class IntegrationsImpossibleDialog extends React.Component { + static propTypes = { + onFinished: PropTypes.func.isRequired, + }; + + _onAcknowledgeClick = () => { + this.props.onFinished(); + }; + + render() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + + return ( + +
    +

    + {_t( + "Your Riot doesn't allow you to use an Integration Manager to do this. " + + "Please contact an admin.", + )} +

    +
    + +
    + ); + } +} diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js index 47239cf33f..d35285463a 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.js @@ -77,7 +77,7 @@ export default class Stickerpicker extends React.Component { this._imError(_td("Failed to connect to integration manager"), e); }); } else { - this._imError(_td("No integration manager is configured to manage stickers with")); + IntegrationManagers.sharedInstance().openNoManagerDialog(); } } @@ -293,6 +293,11 @@ export default class Stickerpicker extends React.Component { * @param {Event} e Event that triggered the function */ _onShowStickersClick(e) { + if (!SettingsStore.getValue("integrationProvisioning")) { + // Intercept this case and spawn a warning. + return IntegrationManagers.sharedInstance().showDisabledDialog(); + } + // XXX: Simplify by using a context menu that is positioned relative to the sticker picker button const buttonRect = e.target.getBoundingClientRect(); diff --git a/src/components/views/settings/IntegrationManager.js b/src/components/views/settings/IntegrationManager.js index 97c469e9aa..1ab17ca8a0 100644 --- a/src/components/views/settings/IntegrationManager.js +++ b/src/components/views/settings/IntegrationManager.js @@ -23,9 +23,6 @@ import dis from '../../../dispatcher'; export default class IntegrationManager extends React.Component { static propTypes = { - // false to display an error saying that there is no integration manager configured - configured: PropTypes.bool.isRequired, - // false to display an error saying that we couldn't connect to the integration manager connected: PropTypes.bool.isRequired, @@ -40,7 +37,6 @@ export default class IntegrationManager extends React.Component { }; static defaultProps = { - configured: true, connected: true, loading: false, }; @@ -70,15 +66,6 @@ export default class IntegrationManager extends React.Component { }; render() { - if (!this.props.configured) { - return ( -
    -

    {_t("No integration manager configured")}

    -

    {_t("This Riot instance does not have an integration manager configured.")}

    -
    - ); - } - if (this.props.loading) { const Spinner = sdk.getComponent("elements.Spinner"); return ( diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0735a8e4b3..375124b4dc 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -507,8 +507,6 @@ "Failed to set display name": "Failed to set display name", "Disable Notifications": "Disable Notifications", "Enable Notifications": "Enable Notifications", - "No integration manager configured": "No integration manager configured", - "This Riot instance does not have an integration manager configured.": "This Riot instance does not have an integration manager configured.", "Connecting to integration manager...": "Connecting to integration manager...", "Cannot connect to integration manager": "Cannot connect to integration manager", "The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.", @@ -1020,7 +1018,6 @@ "Show Text Formatting Toolbar": "Show Text Formatting Toolbar", "Hide Text Formatting Toolbar": "Hide Text Formatting Toolbar", "Failed to connect to integration manager": "Failed to connect to integration manager", - "No integration manager is configured to manage stickers with": "No integration manager is configured to manage stickers with", "You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled", "Add some now": "Add some now", "Stickerpack": "Stickerpack", @@ -1393,6 +1390,10 @@ "Verifying this user will mark their device as trusted, and also mark your device as trusted to them.": "Verifying this user will mark their device as trusted, and also mark your device as trusted to them.", "Waiting for partner to confirm...": "Waiting for partner to confirm...", "Incoming Verification Request": "Incoming Verification Request", + "Integrations are disabled": "Integrations are disabled", + "Enable 'Manage Integrations' in Settings to do this.": "Enable 'Manage Integrations' in Settings to do this.", + "Integrations not allowed": "Integrations not allowed", + "Your Riot doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "Your Riot doesn't allow you to use an Integration Manager to do this. Please contact an admin.", "You added a new device '%(displayName)s', which is requesting encryption keys.": "You added a new device '%(displayName)s', which is requesting encryption keys.", "Your unverified device '%(displayName)s' is requesting encryption keys.": "Your unverified device '%(displayName)s' is requesting encryption keys.", "Start verification": "Start verification", diff --git a/src/integrations/IntegrationManagerInstance.js b/src/integrations/IntegrationManagerInstance.js index 2b616c9fed..4958209351 100644 --- a/src/integrations/IntegrationManagerInstance.js +++ b/src/integrations/IntegrationManagerInstance.js @@ -20,6 +20,8 @@ import {dialogTermsInteractionCallback, TermsNotSignedError} from "../Terms"; import type {Room} from "matrix-js-sdk"; import Modal from '../Modal'; import url from 'url'; +import SettingsStore from "../settings/SettingsStore"; +import {IntegrationManagers} from "./IntegrationManagers"; export const KIND_ACCOUNT = "account"; export const KIND_CONFIG = "config"; @@ -57,6 +59,10 @@ export class IntegrationManagerInstance { } async open(room: Room = null, screen: string = null, integrationId: string = null): void { + if (!SettingsStore.getValue("integrationProvisioning")) { + return IntegrationManagers.sharedInstance().showDisabledDialog(); + } + const IntegrationManager = sdk.getComponent("views.settings.IntegrationManager"); const dialog = Modal.createTrackedDialog( 'Integration Manager', '', IntegrationManager, diff --git a/src/integrations/IntegrationManagers.js b/src/integrations/IntegrationManagers.js index 96fd18b5b8..60ceb49dc0 100644 --- a/src/integrations/IntegrationManagers.js +++ b/src/integrations/IntegrationManagers.js @@ -22,6 +22,10 @@ import type {MatrixClient, MatrixEvent, Room} from "matrix-js-sdk"; import WidgetUtils from "../utils/WidgetUtils"; import MatrixClientPeg from "../MatrixClientPeg"; import {AutoDiscovery} from "matrix-js-sdk"; +import {_t} from "../languageHandler"; +import dis from "../dispatcher"; +import React from 'react'; +import SettingsStore from "../settings/SettingsStore"; const HS_MANAGERS_REFRESH_INTERVAL = 8 * 60 * 60 * 1000; // 8 hours const KIND_PREFERENCE = [ @@ -172,14 +176,19 @@ export class IntegrationManagers { } openNoManagerDialog(): void { - const IntegrationManager = sdk.getComponent("views.settings.IntegrationManager"); - Modal.createTrackedDialog( - "Integration Manager", "None", IntegrationManager, - {configured: false}, 'mx_IntegrationManager', - ); + const IntegrationsImpossibleDialog = sdk.getComponent("dialogs.IntegrationsImpossibleDialog"); + Modal.createTrackedDialog('Integrations impossible', '', IntegrationsImpossibleDialog); } openAll(room: Room = null, screen: string = null, integrationId: string = null): void { + if (!SettingsStore.getValue("integrationProvisioning")) { + return this.showDisabledDialog(); + } + + if (this._managers.length === 0) { + return this.openNoManagerDialog(); + } + const TabbedIntegrationManagerDialog = sdk.getComponent("views.dialogs.TabbedIntegrationManagerDialog"); Modal.createTrackedDialog( 'Tabbed Integration Manager', '', TabbedIntegrationManagerDialog, @@ -187,6 +196,11 @@ export class IntegrationManagers { ); } + showDisabledDialog(): void { + const IntegrationsDisabledDialog = sdk.getComponent("dialogs.IntegrationsDisabledDialog"); + Modal.createTrackedDialog('Integrations disabled', '', IntegrationsDisabledDialog); + } + async overwriteManagerOnAccount(manager: IntegrationManagerInstance) { // TODO: TravisR - We should be logging out of scalar clients. await WidgetUtils.removeIntegrationManagerWidgets(); From 560c0afae3d0ea6cf9b7a0b2df508b339c9734d4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 20 Nov 2019 20:45:16 -0700 Subject: [PATCH 246/334] Appease the linter --- src/components/views/rooms/Stickerpicker.js | 1 + src/integrations/IntegrationManagers.js | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js index d35285463a..879b7c7582 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.js @@ -291,6 +291,7 @@ export default class Stickerpicker extends React.Component { * Show the sticker picker overlay * If no stickerpacks have been added, show a link to the integration manager add sticker packs page. * @param {Event} e Event that triggered the function + * @returns Nothing of use when the thing happens. */ _onShowStickersClick(e) { if (!SettingsStore.getValue("integrationProvisioning")) { diff --git a/src/integrations/IntegrationManagers.js b/src/integrations/IntegrationManagers.js index 60ceb49dc0..6c4d2ae4d4 100644 --- a/src/integrations/IntegrationManagers.js +++ b/src/integrations/IntegrationManagers.js @@ -22,9 +22,6 @@ import type {MatrixClient, MatrixEvent, Room} from "matrix-js-sdk"; import WidgetUtils from "../utils/WidgetUtils"; import MatrixClientPeg from "../MatrixClientPeg"; import {AutoDiscovery} from "matrix-js-sdk"; -import {_t} from "../languageHandler"; -import dis from "../dispatcher"; -import React from 'react'; import SettingsStore from "../settings/SettingsStore"; const HS_MANAGERS_REFRESH_INTERVAL = 8 * 60 * 60 * 1000; // 8 hours From a69d818a0de2a22a7267dee89e34b17a88d29ad2 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 20 Nov 2019 20:49:41 -0700 Subject: [PATCH 247/334] Our linter is seriously picky. --- src/components/views/rooms/Stickerpicker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js index 879b7c7582..25001a2b80 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.js @@ -291,7 +291,7 @@ export default class Stickerpicker extends React.Component { * Show the sticker picker overlay * If no stickerpacks have been added, show a link to the integration manager add sticker packs page. * @param {Event} e Event that triggered the function - * @returns Nothing of use when the thing happens. + * @return Nothing of use when the thing happens. */ _onShowStickersClick(e) { if (!SettingsStore.getValue("integrationProvisioning")) { From 670c14b2e3f00588c99916043b4bca4225aae54b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 20 Nov 2019 20:54:21 -0700 Subject: [PATCH 248/334] Circumvent the linter --- src/components/views/rooms/Stickerpicker.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js index 25001a2b80..7eabf27528 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.js @@ -287,11 +287,10 @@ export default class Stickerpicker extends React.Component { return stickersContent; } - /** + // Dev note: this isn't jsdoc because it's angry. + /* * Show the sticker picker overlay * If no stickerpacks have been added, show a link to the integration manager add sticker packs page. - * @param {Event} e Event that triggered the function - * @return Nothing of use when the thing happens. */ _onShowStickersClick(e) { if (!SettingsStore.getValue("integrationProvisioning")) { From 8c02893da7616a2ceadd976e7e98a93b20f5be1f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 21 Nov 2019 08:58:13 +0000 Subject: [PATCH 249/334] Update CIDER docs now that it is used for main composer as well --- docs/ciderEditor.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/ciderEditor.md b/docs/ciderEditor.md index e67c74a95c..00033b5b8c 100644 --- a/docs/ciderEditor.md +++ b/docs/ciderEditor.md @@ -2,8 +2,7 @@ The CIDER editor is a custom editor written for Riot. Most of the code can be found in the `/editor/` directory of the `matrix-react-sdk` project. -It is used to power the composer to edit messages, -and will soon be used as the main composer to send messages as well. +It is used to power the composer main composer (both to send and edit messages), and might be used for other usecases where autocomplete is desired (invite box, ...). ## High-level overview. From 5670a524693c3cc267065dd782967078a27a74d6 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Thu, 21 Nov 2019 02:52:36 +0000 Subject: [PATCH 250/334] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1919 of 1919 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 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 1dfdc34f1a..2d6d3f55bc 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2344,5 +2344,7 @@ "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "使用這個小工具可能會與 %(widgetDomain)s 以及您的整合管理員分享資料 。", "Using this widget may share data with %(widgetDomain)s.": "使用這個小工具可能會與 %(widgetDomain)s 分享資料 。", "Widget added by": "小工具新增由", - "This widget may use cookies.": "這個小工具可能會使用 cookies。" + "This widget may use cookies.": "這個小工具可能會使用 cookies。", + "Enable local event indexing and E2EE search (requires restart)": "啟用本機事件索引與端到端加密搜尋(需要重新啟動)", + "Match system dark mode setting": "與系統深色模式設定相符" } From 670d44ecd8bfdf41026cfbed45ca5d7f120f5137 Mon Sep 17 00:00:00 2001 From: Tuomas Hietala Date: Thu, 21 Nov 2019 11:36:43 +0000 Subject: [PATCH 251/334] Translated using Weblate (Finnish) Currently translated at 96.1% (1845 of 1919 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 | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index 80fbb9b138..81a8563e5b 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -2137,5 +2137,25 @@ "%(count)s unread messages including mentions.|one": "Yksi lukematon maininta.", "%(count)s unread messages.|one": "Yksi lukematon viesti.", "Unread messages.": "Lukemattomat viestit.", - "Message Actions": "Viestitoiminnot" + "Message Actions": "Viestitoiminnot", + "Custom (%(level)s)": "Mukautettu (%(level)s)", + "Match system dark mode setting": "Sovita järjestelmän tumman tilan asetukseen", + "None": "Ei mitään", + "Unsubscribe": "Lopeta tilaus", + "View rules": "Näytä säännöt", + "Subscribe": "Tilaa", + "Direct message": "Yksityisviesti", + "%(role)s in %(roomName)s": "%(role)s huoneessa %(roomName)s", + "Security": "Tietoturva", + "Any of the following data may be shared:": "Seuraavat tiedot saatetaan jakaa:", + "Your display name": "Näyttönimesi", + "Your avatar URL": "Kuvasi URL-osoite", + "Your user ID": "Käyttäjätunnuksesi", + "Your theme": "Teemasi", + "Riot URL": "Riotin URL-osoite", + "Room ID": "Huoneen tunnus", + "Widget ID": "Sovelman tunnus", + "Using this widget may share data with %(widgetDomain)s.": "Tämän sovelman käyttäminen voi jakaa tietoja verkkotunnukselle %(widgetDomain)s.", + "Widget added by": "Sovelman lisäsi", + "This widget may use cookies.": "Tämä sovelma saattaa käyttää evästeitä." } From ad941b96e09c863dd5c2a8add15c455ea9ce9218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Thu, 21 Nov 2019 11:17:02 +0000 Subject: [PATCH 252/334] Translated using Weblate (French) Currently translated at 100.0% (1919 of 1919 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 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 64272bb839..e58cb187e8 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2351,5 +2351,7 @@ "Using this widget may share data with %(widgetDomain)s.": "L’utilisation de ce widget pourrait partager des données avec %(widgetDomain)s.", "Widget added by": "Widget ajouté par", "This widget may use cookies.": "Ce widget pourrait utiliser des cookies.", - "Send verification requests in direct message, including a new verification UX in the member panel.": "Envoyer les demandes de vérification en message direct, en incluant une nouvelle expérience de vérification dans le tableau des membres." + "Send verification requests in direct message, including a new verification UX in the member panel.": "Envoyer les demandes de vérification en message direct, en incluant une nouvelle expérience de vérification dans le tableau des membres.", + "Enable local event indexing and E2EE search (requires restart)": "Activer l’indexation des événements locaux et la recherche des données chiffrées de bout en bout (nécessite un redémarrage)", + "Match system dark mode setting": "S’adapter aux paramètres de mode sombre du système" } From d786017d08deef229710db2c2cf7a65a0400efd3 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Wed, 20 Nov 2019 19:02:23 +0000 Subject: [PATCH 253/334] Translated using Weblate (Hungarian) Currently translated at 100.0% (1919 of 1919 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 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 892f21dbb1..e48161d798 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2338,5 +2338,7 @@ "Using this widget may share data with %(widgetDomain)s.": "Ennek a kisalkalmazásnak a használata adatot oszthat meg %(widgetDomain)s domain-nel.", "Widget added by": "A kisalkalmazást hozzáadta", "This widget may use cookies.": "Ez a kisalkalmazás sütiket használhat.", - "Send verification requests in direct message, including a new verification UX in the member panel.": "Ellenőrzés küldése közvetlen üzenetben, beleértve az új ellenőrzési felhasználói élményt a résztvevői panelen." + "Send verification requests in direct message, including a new verification UX in the member panel.": "Ellenőrzés küldése közvetlen üzenetben, beleértve az új ellenőrzési felhasználói élményt a résztvevői panelen.", + "Enable local event indexing and E2EE search (requires restart)": "Helyi esemény indexálás és végponttól végpontig titkosított események keresésének engedélyezése (újraindítás szükséges)", + "Match system dark mode setting": "Rendszer sötét témájához alkalmazkodás" } From 87167f42f89cd8c2f3e87f8d18ad95d717ad678e Mon Sep 17 00:00:00 2001 From: random Date: Thu, 21 Nov 2019 09:34:39 +0000 Subject: [PATCH 254/334] Translated using Weblate (Italian) Currently translated at 99.9% (1917 of 1919 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 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index efab4595f6..9faa48328c 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -2295,5 +2295,8 @@ "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "Usando questo widget i dati possono essere condivisi con %(widgetDomain)s e il tuo Gestore di Integrazione.", "Using this widget may share data with %(widgetDomain)s.": "Usando questo widget i dati possono essere condivisi con %(widgetDomain)s.", "Widget added by": "Widget aggiunto da", - "This widget may use cookies.": "Questo widget può usare cookie." + "This widget may use cookies.": "Questo widget può usare cookie.", + "Send verification requests in direct message, including a new verification UX in the member panel.": "Invia le richieste di verifica via messaggio diretto, inclusa una nuova esperienza utente per la verifica nel pannello membri.", + "Enable local event indexing and E2EE search (requires restart)": "Attiva l'indicizzazione di eventi locali e la ricerca E2EE (richiede riavvio)", + "Match system dark mode setting": "Combacia la modalità scura di sistema" } From 598901b48339e5547b1396db16e81d8b2ab5e932 Mon Sep 17 00:00:00 2001 From: Edgars Voroboks Date: Wed, 20 Nov 2019 18:58:42 +0000 Subject: [PATCH 255/334] Translated using Weblate (Latvian) Currently translated at 47.8% (918 of 1919 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/lv/ --- src/i18n/strings/lv.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/lv.json b/src/i18n/strings/lv.json index 80e173dc3f..1c7d2b0311 100644 --- a/src/i18n/strings/lv.json +++ b/src/i18n/strings/lv.json @@ -1144,5 +1144,8 @@ "You do not have permission to start a conference call in this room": "Šajā istabā nav atļaujas sākt konferences zvanu", "Replying With Files": "Atbildot ar failiem", "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Šobrīd nav iespējams atbildēt ar failu. Vai vēlaties augšupielādēt šo failu, neatbildot?", - "Your Riot is misconfigured": "Jūsu Riot ir nepareizi konfigurēts" + "Your Riot is misconfigured": "Jūsu Riot ir nepareizi konfigurēts", + "Add Email Address": "Pievienot e-pasta adresi", + "Add Phone Number": "Pievienot tālruņa numuru", + "Call failed due to misconfigured server": "Zvans neizdevās nekorekti nokonfigurēta servera dēļ" } From 5c6ef10c6b6c1f063e9eeee653cc8419ff172200 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 21 Nov 2019 15:55:30 +0000 Subject: [PATCH 256/334] Ignore media actions Hopefully the comment explains all Fixes https://github.com/vector-im/riot-web/issues/11118 --- src/CallHandler.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/CallHandler.js b/src/CallHandler.js index 625ca8c551..4ffc9fb7a2 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -495,6 +495,15 @@ async function _startCallApp(roomId, type) { // with the dispatcher once if (!global.mxCallHandler) { dis.register(_onAction); + // add empty handlers for media actions, otherwise the media keys + // end up causing the audio elements with our ring/ringback etc + // audio clips in to play. + navigator.mediaSession.setActionHandler('play', function() {}); + navigator.mediaSession.setActionHandler('pause', function() {}); + navigator.mediaSession.setActionHandler('seekbackward', function() {}); + navigator.mediaSession.setActionHandler('seekforward', function() {}); + navigator.mediaSession.setActionHandler('previoustrack', function() {}); + navigator.mediaSession.setActionHandler('nexttrack', function() {}); } const callHandler = { From f7f22444e8f0581ddb1e6520dcfd596a29e1b139 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 21 Nov 2019 09:03:07 -0700 Subject: [PATCH 257/334] Rename section heading for integrations in settings Misc design update --- src/components/views/settings/SetIntegrationManager.js | 2 +- src/i18n/strings/en_EN.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/SetIntegrationManager.js b/src/components/views/settings/SetIntegrationManager.js index 26c45e3d2a..e205f02e6c 100644 --- a/src/components/views/settings/SetIntegrationManager.js +++ b/src/components/views/settings/SetIntegrationManager.js @@ -64,7 +64,7 @@ export default class SetIntegrationManager extends React.Component { return (
    - {_t("Integrations")} + {_t("Manage integrations")} {managerName}
    diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a6173e15b7..618c9ad63a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -598,7 +598,7 @@ "Change": "Change", "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.", "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Use an Integration Manager to manage bots, widgets, and sticker packs.", - "Integrations": "Integrations", + "Manage integrations": "Manage integrations", "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.", "Flair": "Flair", "Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?", From f0fbb20ee50b8d822175207136d8ffa3f1ee107c Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 21 Nov 2019 16:11:42 +0000 Subject: [PATCH 258/334] Detect support for mediaSession Firefox doesn't support mediaSession so don't try setting handlers --- src/CallHandler.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/CallHandler.js b/src/CallHandler.js index 4ffc9fb7a2..9350fe4dd9 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -498,12 +498,14 @@ if (!global.mxCallHandler) { // add empty handlers for media actions, otherwise the media keys // end up causing the audio elements with our ring/ringback etc // audio clips in to play. - navigator.mediaSession.setActionHandler('play', function() {}); - navigator.mediaSession.setActionHandler('pause', function() {}); - navigator.mediaSession.setActionHandler('seekbackward', function() {}); - navigator.mediaSession.setActionHandler('seekforward', function() {}); - navigator.mediaSession.setActionHandler('previoustrack', function() {}); - navigator.mediaSession.setActionHandler('nexttrack', function() {}); + if (navigator.mediaSession) { + navigator.mediaSession.setActionHandler('play', function() {}); + navigator.mediaSession.setActionHandler('pause', function() {}); + navigator.mediaSession.setActionHandler('seekbackward', function() {}); + navigator.mediaSession.setActionHandler('seekforward', function() {}); + navigator.mediaSession.setActionHandler('previoustrack', function() {}); + navigator.mediaSession.setActionHandler('nexttrack', function() {}); + } } const callHandler = { From a55e5f77598f4ca7c433240b50229329f6a45e9c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 21 Nov 2019 09:12:07 -0700 Subject: [PATCH 259/334] Update copy for widgets not using message encryption Misc design update --- src/components/views/elements/AppPermission.js | 2 +- src/i18n/strings/en_EN.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/AppPermission.js b/src/components/views/elements/AppPermission.js index c514dbc950..8dc58643bd 100644 --- a/src/components/views/elements/AppPermission.js +++ b/src/components/views/elements/AppPermission.js @@ -115,7 +115,7 @@ export default class AppPermission extends React.Component { : _t("Using this widget may share data with %(widgetDomain)s.", {widgetDomain: this.state.widgetDomain}, {helpIcon: () => warningTooltip}); - const encryptionWarning = this.props.isRoomEncrypted ? _t("Widgets are not encrypted.") : null; + const encryptionWarning = this.props.isRoomEncrypted ? _t("Widgets do not use message encryption.") : null; return (
    diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a6173e15b7..56ae95d568 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1187,7 +1187,7 @@ "Widget ID": "Widget ID", "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "Using this widget may share data with %(widgetDomain)s & your Integration Manager.", "Using this widget may share data with %(widgetDomain)s.": "Using this widget may share data with %(widgetDomain)s.", - "Widgets are not encrypted.": "Widgets are not encrypted.", + "Widgets do not use message encryption.": "Widgets do not use message encryption.", "Widget added by": "Widget added by", "This widget may use cookies.": "This widget may use cookies.", "Delete Widget": "Delete Widget", From 08e08376a204e34926a525b34454427589878b25 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Thu, 21 Nov 2019 15:19:59 +0000 Subject: [PATCH 260/334] Translated using Weblate (Hungarian) Currently translated at 100.0% (1922 of 1922 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 | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index e48161d798..70a966ce3d 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2340,5 +2340,11 @@ "This widget may use cookies.": "Ez a kisalkalmazás sütiket használhat.", "Send verification requests in direct message, including a new verification UX in the member panel.": "Ellenőrzés küldése közvetlen üzenetben, beleértve az új ellenőrzési felhasználói élményt a résztvevői panelen.", "Enable local event indexing and E2EE search (requires restart)": "Helyi esemény indexálás és végponttól végpontig titkosított események keresésének engedélyezése (újraindítás szükséges)", - "Match system dark mode setting": "Rendszer sötét témájához alkalmazkodás" + "Match system dark mode setting": "Rendszer sötét témájához alkalmazkodás", + "Widgets are not encrypted.": "A kisalkalmazások nem titkosítottak.", + "More options": "További beállítások", + "Reload": "Újratölt", + "Take picture": "Fénykép készítés", + "Remove for everyone": "Visszavonás mindenkitől", + "Remove for me": "Visszavonás magamtól" } From 5d8476185f83e73ff06a59e89a71a4f53bf0a419 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 21 Nov 2019 17:00:35 +0000 Subject: [PATCH 261/334] Catch exceptions when we can't play audio ...or try to: the chrome debugger still breakpoints, even when we catch the exception. Related to, but probably does not fix https://github.com/vector-im/riot-web/issues/7657 --- src/CallHandler.js | 17 +++++++++++++++-- src/Notifier.js | 2 +- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/CallHandler.js b/src/CallHandler.js index 625ca8c551..c15fda1ef9 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -80,13 +80,26 @@ function play(audioId) { // which listens? const audio = document.getElementById(audioId); if (audio) { + const playAudio = async () => { + try { + // This still causes the chrome debugger to break on promise rejection if + // the promise is rejected, even though we're catching the exception. + await audio.play(); + } catch (e) { + // This is usually because the user hasn't interacted with the document, + // or chrome doesn't think so and is denying the request. Not sure what + // we can really do here... + // https://github.com/vector-im/riot-web/issues/7657 + console.log("Unable to play audio clip", e); + } + }; if (audioPromises[audioId]) { audioPromises[audioId] = audioPromises[audioId].then(()=>{ audio.load(); - return audio.play(); + return playAudio(); }); } else { - audioPromises[audioId] = audio.play(); + audioPromises[audioId] = playAudio(); } } } diff --git a/src/Notifier.js b/src/Notifier.js index edb9850dfe..dd691d8ca7 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -146,7 +146,7 @@ const Notifier = { } document.body.appendChild(audioElement); } - audioElement.play(); + await audioElement.play(); } catch (ex) { console.warn("Caught error when trying to fetch room notification sound:", ex); } From 3cddcad5de18f926dcd2b44e0b4dfe36968018d3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 21 Nov 2019 18:12:32 +0100 Subject: [PATCH 262/334] use correct icons with borders --- res/img/e2e/verified.svg | 14 +++----------- res/img/e2e/warning.svg | 15 ++++----------- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/res/img/e2e/verified.svg b/res/img/e2e/verified.svg index af6bb92297..464b443dcf 100644 --- a/res/img/e2e/verified.svg +++ b/res/img/e2e/verified.svg @@ -1,12 +1,4 @@ - - - + + + diff --git a/res/img/e2e/warning.svg b/res/img/e2e/warning.svg index 2501da6ab3..209ae0f71f 100644 --- a/res/img/e2e/warning.svg +++ b/res/img/e2e/warning.svg @@ -1,12 +1,5 @@ - - - + + + + From cb0e6ca5d280d16b951e843748936f337f05d802 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 21 Nov 2019 18:13:01 +0100 Subject: [PATCH 263/334] dont make e2e icons themable, as they have multiple colors --- res/css/views/rooms/_E2EIcon.scss | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/res/css/views/rooms/_E2EIcon.scss b/res/css/views/rooms/_E2EIcon.scss index 1ee5008888..cb99aa63f1 100644 --- a/res/css/views/rooms/_E2EIcon.scss +++ b/res/css/views/rooms/_E2EIcon.scss @@ -15,8 +15,8 @@ limitations under the License. */ .mx_E2EIcon { - width: 25px; - height: 25px; + width: 16px; + height: 16px; margin: 0 9px; position: relative; display: block; @@ -30,16 +30,14 @@ limitations under the License. bottom: 0; left: 0; right: 0; - mask-repeat: no-repeat; - mask-size: contain; + background-repeat: no-repeat; + background-size: contain; } .mx_E2EIcon_verified::after { - mask-image: url('$(res)/img/e2e/verified.svg'); - background-color: $accent-color; + background-image: url('$(res)/img/e2e/verified.svg'); } .mx_E2EIcon_warning::after { - mask-image: url('$(res)/img/e2e/warning.svg'); - background-color: $warning-color; + background-image: url('$(res)/img/e2e/warning.svg'); } From 854f27df3fbd0b8d65790a06c33d0ed301756a7f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 21 Nov 2019 18:14:25 +0100 Subject: [PATCH 264/334] remove obsolete style from message composer for e2e icon as it's now the default size for the e2e iconn --- res/css/views/rooms/_MessageComposer.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index 036756e2eb..12e45a07c9 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -74,8 +74,6 @@ limitations under the License. .mx_MessageComposer_e2eIcon.mx_E2EIcon { position: absolute; left: 60px; - width: 16px; - height: 16px; margin-right: 0; // Counteract the E2EIcon class margin-left: 3px; // Counteract the E2EIcon class } From f75e45a715db4314ffa23684412d24fbd86f02a4 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 21 Nov 2019 18:17:55 +0100 Subject: [PATCH 265/334] reduce margin on e2e icon in room header --- res/css/views/rooms/_RoomHeader.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index 5da8ff76b9..f1e4456cc1 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -17,6 +17,10 @@ limitations under the License. .mx_RoomHeader { flex: 0 0 52px; border-bottom: 1px solid $primary-hairline-color; + + .mx_E2EIcon { + margin: 0 5px; + } } .mx_RoomHeader_wrapper { From b239fde32dec454b29f9b11611e02862a79cd979 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 21 Nov 2019 17:31:57 +0000 Subject: [PATCH 266/334] Workaround for soft-crash with calls on startup Fixes https://github.com/vector-im/riot-web/issues/11458 --- src/components/views/voip/CallView.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/views/voip/CallView.js b/src/components/views/voip/CallView.js index a4d7927ac3..cf1f505197 100644 --- a/src/components/views/voip/CallView.js +++ b/src/components/views/voip/CallView.js @@ -90,6 +90,13 @@ module.exports = createReactClass({ } } else { call = CallHandler.getAnyActiveCall(); + // Ignore calls if we can't get the room associated with them. + // I think the underlying problem is that the js-sdk sends events + // for calls before it has made the rooms available in the store, + // although this isn't confirmed. + if (MatrixClientPeg.get().getRoom(call.roomId) === null) { + call = null; + } this.setState({ call: call }); } From a02a285058fdc38bebcc198aaefe06d1f28cd586 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 22 Nov 2019 10:24:51 +0000 Subject: [PATCH 267/334] Show m.room.create event before the ELS on room upgrade Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/MessagePanel.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index cf2a5b1738..39504666bf 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -411,6 +411,11 @@ module.exports = createReactClass({ readMarkerInSummary = true; } + // If this m.room.create event should be shown (room upgrade) then show it before the summary + if (this._shouldShowEvent(mxEv)) { + ret.push(...this._getTilesForEvent(prevEvent, mxEv, false)); + } + const summarisedEvents = []; // Don't add m.room.create here as we don't want it inside the summary for (;i + 1 < this.props.events.length; i++) { const collapsedMxEv = this.props.events[i + 1]; From 6d4abeef4515bab769d04359a2ded0ca70219d57 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 22 Nov 2019 12:07:25 +0000 Subject: [PATCH 268/334] Convert MessagePanel to React class I was about to add the getDerivedStateFromProps function to change how read markers worked, but doing that in an old style class means the statics object, so let;s just convert the thing. --- src/components/structures/MessagePanel.js | 132 +++++++++++----------- 1 file changed, 65 insertions(+), 67 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index cf2a5b1738..3781dd0ce7 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -1,6 +1,7 @@ /* Copyright 2016 OpenMarket Ltd Copyright 2018 New Vector Ltd +Copyright 2019 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. @@ -18,7 +19,6 @@ limitations under the License. /* global Velocity */ import React from 'react'; -import createReactClass from 'create-react-class'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import classNames from 'classnames'; @@ -37,10 +37,8 @@ const isMembershipChange = (e) => e.getType() === 'm.room.member' || e.getType() /* (almost) stateless UI component which builds the event tiles in the room timeline. */ -module.exports = createReactClass({ - displayName: 'MessagePanel', - - propTypes: { +export default class MessagePanel extends React.Component { + propTypes = { // true to give the component a 'display: none' style. hidden: PropTypes.bool, @@ -109,9 +107,9 @@ module.exports = createReactClass({ // whether to show reactions for an event showReactions: PropTypes.bool, - }, + }; - componentWillMount: function() { + componentDidMount() { // the event after which we put a visible unread marker on the last // render cycle; null if readMarkerVisible was false or the RM was // suppressed (eg because it was at the end of the timeline) @@ -168,37 +166,37 @@ module.exports = createReactClass({ SettingsStore.getValue("showHiddenEventsInTimeline"); this._isMounted = true; - }, + } - componentWillUnmount: function() { + componentWillUnmount() { this._isMounted = false; - }, + } /* get the DOM node representing the given event */ - getNodeForEventId: function(eventId) { + getNodeForEventId(eventId) { if (!this.eventNodes) { return undefined; } return this.eventNodes[eventId]; - }, + } /* return true if the content is fully scrolled down right now; else false. */ - isAtBottom: function() { + isAtBottom() { return this.refs.scrollPanel && this.refs.scrollPanel.isAtBottom(); - }, + } /* get the current scroll state. See ScrollPanel.getScrollState for * details. * * returns null if we are not mounted. */ - getScrollState: function() { + getScrollState() { if (!this.refs.scrollPanel) { return null; } return this.refs.scrollPanel.getScrollState(); - }, + } // returns one of: // @@ -206,7 +204,7 @@ module.exports = createReactClass({ // -1: read marker is above the window // 0: read marker is within the window // +1: read marker is below the window - getReadMarkerPosition: function() { + getReadMarkerPosition() { const readMarker = this.refs.readMarkerNode; const messageWrapper = this.refs.scrollPanel; @@ -226,45 +224,45 @@ module.exports = createReactClass({ } else { return 1; } - }, + } /* jump to the top of the content. */ - scrollToTop: function() { + scrollToTop() { if (this.refs.scrollPanel) { this.refs.scrollPanel.scrollToTop(); } - }, + } /* jump to the bottom of the content. */ - scrollToBottom: function() { + scrollToBottom() { if (this.refs.scrollPanel) { this.refs.scrollPanel.scrollToBottom(); } - }, + } /** * Page up/down. * * @param {number} mult: -1 to page up, +1 to page down */ - scrollRelative: function(mult) { + scrollRelative(mult) { if (this.refs.scrollPanel) { this.refs.scrollPanel.scrollRelative(mult); } - }, + } /** * Scroll up/down in response to a scroll key * * @param {KeyboardEvent} ev: the keyboard event to handle */ - handleScrollKey: function(ev) { + handleScrollKey(ev) { if (this.refs.scrollPanel) { this.refs.scrollPanel.handleScrollKey(ev); } - }, + } /* jump to the given event id. * @@ -276,33 +274,33 @@ module.exports = createReactClass({ * node (specifically, the bottom of it) will be positioned. If omitted, it * defaults to 0. */ - scrollToEvent: function(eventId, pixelOffset, offsetBase) { + scrollToEvent(eventId, pixelOffset, offsetBase) { if (this.refs.scrollPanel) { this.refs.scrollPanel.scrollToToken(eventId, pixelOffset, offsetBase); } - }, + } - scrollToEventIfNeeded: function(eventId) { + scrollToEventIfNeeded(eventId) { const node = this.eventNodes[eventId]; if (node) { node.scrollIntoView({block: "nearest", behavior: "instant"}); } - }, + } /* check the scroll state and send out pagination requests if necessary. */ - checkFillState: function() { + checkFillState() { if (this.refs.scrollPanel) { this.refs.scrollPanel.checkFillState(); } - }, + } - _isUnmounting: function() { + _isUnmounting() { return !this._isMounted; - }, + } // TODO: Implement granular (per-room) hide options - _shouldShowEvent: function(mxEv) { + _shouldShowEvent(mxEv) { if (mxEv.sender && MatrixClientPeg.get().isUserIgnored(mxEv.sender.userId)) { return false; // ignored = no show (only happens if the ignore happens after an event was received) } @@ -320,9 +318,9 @@ module.exports = createReactClass({ if (this.props.highlightedEventId === mxEv.getId()) return true; return !shouldHideEvent(mxEv); - }, + } - _getEventTiles: function() { + _getEventTiles() { const DateSeparator = sdk.getComponent('messages.DateSeparator'); const EventListSummary = sdk.getComponent('views.elements.EventListSummary'); const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary'); @@ -596,9 +594,9 @@ module.exports = createReactClass({ this.currentReadMarkerEventId = readMarkerVisible ? this.props.readMarkerEventId : null; return ret; - }, + } - _getTilesForEvent: function(prevEvent, mxEv, last) { + _getTilesForEvent(prevEvent, mxEv, last) { const EventTile = sdk.getComponent('rooms.EventTile'); const DateSeparator = sdk.getComponent('messages.DateSeparator'); const ret = []; @@ -691,20 +689,20 @@ module.exports = createReactClass({ ); return ret; - }, + } - _wantsDateSeparator: function(prevEvent, nextEventDate) { + _wantsDateSeparator(prevEvent, nextEventDate) { if (prevEvent == null) { // first event in the panel: depends if we could back-paginate from // here. return !this.props.suppressFirstDateSeparator; } return wantsDateSeparator(prevEvent.getDate(), nextEventDate); - }, + } // Get a list of read receipts that should be shown next to this event // Receipts are objects which have a 'userId', 'roomMember' and 'ts'. - _getReadReceiptsForEvent: function(event) { + _getReadReceiptsForEvent(event) { const myUserId = MatrixClientPeg.get().credentials.userId; // get list of read receipts, sorted most recent first @@ -728,12 +726,12 @@ module.exports = createReactClass({ }); }); return receipts; - }, + } // Get an object that maps from event ID to a list of read receipts that // should be shown next to that event. If a hidden event has read receipts, // they are folded into the receipts of the last shown event. - _getReadReceiptsByShownEvent: function() { + _getReadReceiptsByShownEvent() { const receiptsByEvent = {}; const receiptsByUserId = {}; @@ -786,9 +784,9 @@ module.exports = createReactClass({ } return receiptsByEvent; - }, + } - _getReadMarkerTile: function(visible) { + _getReadMarkerTile(visible) { let hr; if (visible) { hr =
    ); - }, + } - _startAnimation: function(ghostNode) { + _startAnimation = (ghostNode) => { if (this._readMarkerGhostNode) { Velocity.Utilities.removeData(this._readMarkerGhostNode); } @@ -816,9 +814,9 @@ module.exports = createReactClass({ {duration: 400, easing: 'easeInSine', delay: 1000}); } - }, + }; - _getReadMarkerGhostTile: function() { + _getReadMarkerGhostTile() { const hr =
    ); - }, + } - _collectEventNode: function(eventId, node) { + _collectEventNode = (eventId, node) => { this.eventNodes[eventId] = node; - }, + } // once dynamic content in the events load, make the scrollPanel check the // scroll offsets. - _onHeightChanged: function() { + _onHeightChanged = () => { const scrollPanel = this.refs.scrollPanel; if (scrollPanel) { scrollPanel.checkScroll(); } - }, + }; - _onTypingShown: function() { + _onTypingShown = () => { const scrollPanel = this.refs.scrollPanel; // this will make the timeline grow, so checkScroll scrollPanel.checkScroll(); if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) { scrollPanel.preventShrinking(); } - }, + }; - _onTypingHidden: function() { + _onTypingHidden = () => { const scrollPanel = this.refs.scrollPanel; if (scrollPanel) { // as hiding the typing notifications doesn't @@ -868,9 +866,9 @@ module.exports = createReactClass({ // reveal added padding to balance the notifs disappearing. scrollPanel.checkScroll(); } - }, + }; - updateTimelineMinHeight: function() { + updateTimelineMinHeight() { const scrollPanel = this.refs.scrollPanel; if (scrollPanel) { @@ -885,16 +883,16 @@ module.exports = createReactClass({ scrollPanel.preventShrinking(); } } - }, + } - onTimelineReset: function() { + onTimelineReset() { const scrollPanel = this.refs.scrollPanel; if (scrollPanel) { scrollPanel.clearPreventShrinking(); } - }, + } - render: function() { + render() { const ScrollPanel = sdk.getComponent("structures.ScrollPanel"); const WhoIsTypingTile = sdk.getComponent("rooms.WhoIsTypingTile"); const Spinner = sdk.getComponent("elements.Spinner"); @@ -941,5 +939,5 @@ module.exports = createReactClass({ { bottomSpinner } ); - }, -}); + } +} From fd5a5e13ee0c485716e7d758e2da63c9b434ee12 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 22 Nov 2019 12:59:51 +0000 Subject: [PATCH 269/334] Make addEventListener conditional Safari doesn't support addEventListener --- src/theme.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/theme.js b/src/theme.js index 92bf03ef0a..9208ff2045 100644 --- a/src/theme.js +++ b/src/theme.js @@ -41,14 +41,18 @@ export class ThemeWatcher { start() { this._themeWatchRef = SettingsStore.watchSetting("theme", null, this._onChange); this._systemThemeWatchRef = SettingsStore.watchSetting("use_system_theme", null, this._onChange); - this._preferDark.addEventListener('change', this._onChange); - this._preferLight.addEventListener('change', this._onChange); + if (this._preferDark.addEventListener) { + this._preferDark.addEventListener('change', this._onChange); + this._preferLight.addEventListener('change', this._onChange); + } this._dispatcherRef = dis.register(this._onAction); } stop() { - this._preferDark.removeEventListener('change', this._onChange); - this._preferLight.removeEventListener('change', this._onChange); + if (this._preferDark.addEventListener) { + this._preferDark.removeEventListener('change', this._onChange); + this._preferLight.removeEventListener('change', this._onChange); + } SettingsStore.unwatchSetting(this._systemThemeWatchRef); SettingsStore.unwatchSetting(this._themeWatchRef); dis.unregister(this._dispatcherRef); From 3f5a8faf376b33499258f071b1eefe69e8c2c5ee Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 22 Nov 2019 13:01:56 +0000 Subject: [PATCH 270/334] PropTypes should be static --- res/css/structures/_RoomView.scss | 1 + src/components/structures/MessagePanel.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss index 50d412ad58..8e47fe7509 100644 --- a/res/css/structures/_RoomView.scss +++ b/res/css/structures/_RoomView.scss @@ -221,6 +221,7 @@ hr.mx_RoomView_myReadMarker { position: relative; top: -1px; z-index: 1; + transition: width 1s easeInSine; } .mx_RoomView_callStatusBar .mx_UploadBar_uploadProgressInner { diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 3781dd0ce7..bd5630ab12 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -38,7 +38,7 @@ const isMembershipChange = (e) => e.getType() === 'm.room.member' || e.getType() /* (almost) stateless UI component which builds the event tiles in the room timeline. */ export default class MessagePanel extends React.Component { - propTypes = { + static propTypes = { // true to give the component a 'display: none' style. hidden: PropTypes.bool, From 0dbb639aea1ef008bcdede36899112f3b7bca1fa Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 22 Nov 2019 13:06:35 +0000 Subject: [PATCH 271/334] Accidentally committed --- res/css/structures/_RoomView.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss index 8e47fe7509..50d412ad58 100644 --- a/res/css/structures/_RoomView.scss +++ b/res/css/structures/_RoomView.scss @@ -221,7 +221,6 @@ hr.mx_RoomView_myReadMarker { position: relative; top: -1px; z-index: 1; - transition: width 1s easeInSine; } .mx_RoomView_callStatusBar .mx_UploadBar_uploadProgressInner { From 936c36dd586ebaacfe203f00759f7d01f1abe0dd Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Fri, 22 Nov 2019 03:23:27 +0000 Subject: [PATCH 272/334] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1918 of 1918 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 | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 2d6d3f55bc..2fd014554e 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2346,5 +2346,22 @@ "Widget added by": "小工具新增由", "This widget may use cookies.": "這個小工具可能會使用 cookies。", "Enable local event indexing and E2EE search (requires restart)": "啟用本機事件索引與端到端加密搜尋(需要重新啟動)", - "Match system dark mode setting": "與系統深色模式設定相符" + "Match system dark mode setting": "與系統深色模式設定相符", + "Connecting to integration manager...": "正在連線到整合管理員……", + "Cannot connect to integration manager": "無法連線到整合管理員", + "The integration manager is offline or it cannot reach your homeserver.": "整合管理員已離線或無法存取您的家伺服器。", + "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "使用整合管理員 (%(serverName)s) 以管理機器人、小工具與貼紙包。", + "Use an Integration Manager to manage bots, widgets, and sticker packs.": "使用整合管理員以管理機器人、小工具與貼紙包。", + "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "整合管理員接收設定資料,並可以修改小工具、傳送聊天室邀請並設定權限等級。", + "Failed to connect to integration manager": "連線到整合管理員失敗", + "Widgets do not use message encryption.": "小工具不使用訊息加密。", + "More options": "更多選項", + "Integrations are disabled": "整合已停用", + "Enable 'Manage Integrations' in Settings to do this.": "在設定中啟用「管理整合」以執行此動作。", + "Integrations not allowed": "不允許整合", + "Your Riot doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "您的 Riot 不允許您使用整合管理員來執行此動作。請聯絡管理員。", + "Reload": "重新載入", + "Take picture": "拍照", + "Remove for everyone": "對所有人移除", + "Remove for me": "對我移除" } From 27c64db613281d6d44c35fca40402c53df8e39c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Fri, 22 Nov 2019 08:35:43 +0000 Subject: [PATCH 273/334] Translated using Weblate (French) Currently translated at 100.0% (1918 of 1918 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 | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index e58cb187e8..0c13b3c722 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2353,5 +2353,22 @@ "This widget may use cookies.": "Ce widget pourrait utiliser des cookies.", "Send verification requests in direct message, including a new verification UX in the member panel.": "Envoyer les demandes de vérification en message direct, en incluant une nouvelle expérience de vérification dans le tableau des membres.", "Enable local event indexing and E2EE search (requires restart)": "Activer l’indexation des événements locaux et la recherche des données chiffrées de bout en bout (nécessite un redémarrage)", - "Match system dark mode setting": "S’adapter aux paramètres de mode sombre du système" + "Match system dark mode setting": "S’adapter aux paramètres de mode sombre du système", + "Connecting to integration manager...": "Connexion au gestionnaire d’intégrations…", + "Cannot connect to integration manager": "Impossible de se connecter au gestionnaire d’intégrations", + "The integration manager is offline or it cannot reach your homeserver.": "Le gestionnaire d’intégrations est hors ligne ou il ne peut pas joindre votre serveur d’accueil.", + "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Utilisez un gestionnaire d’intégrations (%(serverName)s) pour gérer les bots, les widgets et les packs de stickers.", + "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Utilisez un gestionnaire d’intégrations pour gérer les bots, les widgets et les packs de stickers.", + "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Les gestionnaires d’intégrations reçoivent les données de configuration et peuvent modifier les widgets, envoyer des invitations aux salons et définir les rangs à votre place.", + "Failed to connect to integration manager": "Échec de la connexion au gestionnaire d’intégrations", + "Widgets do not use message encryption.": "Les widgets n’utilisent pas le chiffrement des messages.", + "More options": "Plus d’options", + "Integrations are disabled": "Les intégrations sont désactivées", + "Enable 'Manage Integrations' in Settings to do this.": "Activez « Gérer les intégrations » dans les paramètres pour faire ça.", + "Integrations not allowed": "Les intégrations ne sont pas autorisées", + "Your Riot doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "Votre Riot ne vous autorise pas à utiliser un gestionnaire d’intégrations pour faire ça. Contactez un administrateur.", + "Reload": "Recharger", + "Take picture": "Prendre une photo", + "Remove for everyone": "Supprimer pour tout le monde", + "Remove for me": "Supprimer pour moi" } From fed1ed3b500d5e98bc04ab9ab52dd9f63949db06 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Thu, 21 Nov 2019 20:00:59 +0000 Subject: [PATCH 274/334] Translated using Weblate (Hungarian) Currently translated at 100.0% (1918 of 1918 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 70a966ce3d..7ed4eb253c 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2346,5 +2346,17 @@ "Reload": "Újratölt", "Take picture": "Fénykép készítés", "Remove for everyone": "Visszavonás mindenkitől", - "Remove for me": "Visszavonás magamtól" + "Remove for me": "Visszavonás magamtól", + "Connecting to integration manager...": "Kapcsolódás az integrációs menedzserhez...", + "Cannot connect to integration manager": "A kapcsolódás az integrációs menedzserhez sikertelen", + "The integration manager is offline or it cannot reach your homeserver.": "Az integrációs menedzser nem működik vagy nem éri el a matrix szerveredet.", + "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Használj Integrációs Menedzsert (%(serverName)s) a botok, kisalkalmazások és matrica csomagok kezeléséhez.", + "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Használj Integrációs Menedzsert a botok, kisalkalmazások és matrica csomagok kezeléséhez.", + "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integrációs Menedzser megkapja a konfigurációt, módosíthat kisalkalmazásokat, szobához meghívót küldhet és a hozzáférési szintet beállíthatja helyetted.", + "Failed to connect to integration manager": "Az integrációs menedzserhez nem sikerült csatlakozni", + "Widgets do not use message encryption.": "A kisalkalmazások nem használnak üzenet titkosítást.", + "Integrations are disabled": "Az integrációk le vannak tiltva", + "Enable 'Manage Integrations' in Settings to do this.": "Ehhez engedélyezd az „Integrációk Kezelésé”-t a Beállításokban.", + "Integrations not allowed": "Az integrációk nem engedélyezettek", + "Your Riot doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "A Riotod nem használhat ehhez Integrációs Menedzsert. Kérlek vedd fel a kapcsolatot az adminisztrátorral." } From 25ba4c5f7124d40fd5d83cddba30b1479f64dc0c Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 22 Nov 2019 13:11:36 +0000 Subject: [PATCH 275/334] Fix read markers init code needs to be a constructor or its run too late --- src/components/structures/MessagePanel.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index bd5630ab12..22be35db60 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -109,7 +109,8 @@ export default class MessagePanel extends React.Component { showReactions: PropTypes.bool, }; - componentDidMount() { + constructor() { + super(); // the event after which we put a visible unread marker on the last // render cycle; null if readMarkerVisible was false or the RM was // suppressed (eg because it was at the end of the timeline) @@ -165,6 +166,10 @@ export default class MessagePanel extends React.Component { this._showHiddenEventsInTimeline = SettingsStore.getValue("showHiddenEventsInTimeline"); + this._isMounted = false; + } + + componentDidMount() { this._isMounted = true; } From d1501a16515c003a949cd819be4a1f34c567f8ea Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 21 Nov 2019 18:17:55 +0100 Subject: [PATCH 276/334] reduce margin on e2e icon in room header --- res/css/views/rooms/_RoomHeader.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index 5da8ff76b9..f1e4456cc1 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -17,6 +17,10 @@ limitations under the License. .mx_RoomHeader { flex: 0 0 52px; border-bottom: 1px solid $primary-hairline-color; + + .mx_E2EIcon { + margin: 0 5px; + } } .mx_RoomHeader_wrapper { From de645a32a8d3024956270094bc01e4b6277d7bf9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 21 Nov 2019 18:14:25 +0100 Subject: [PATCH 277/334] remove obsolete style from message composer for e2e icon as it's now the default size for the e2e iconn --- res/css/views/rooms/_MessageComposer.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index 036756e2eb..12e45a07c9 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -74,8 +74,6 @@ limitations under the License. .mx_MessageComposer_e2eIcon.mx_E2EIcon { position: absolute; left: 60px; - width: 16px; - height: 16px; margin-right: 0; // Counteract the E2EIcon class margin-left: 3px; // Counteract the E2EIcon class } From 9c234d93172c77b532b696ce18f71fb77e70db66 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 21 Nov 2019 18:13:01 +0100 Subject: [PATCH 278/334] dont make e2e icons themable, as they have multiple colors --- res/css/views/rooms/_E2EIcon.scss | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/res/css/views/rooms/_E2EIcon.scss b/res/css/views/rooms/_E2EIcon.scss index 1ee5008888..cb99aa63f1 100644 --- a/res/css/views/rooms/_E2EIcon.scss +++ b/res/css/views/rooms/_E2EIcon.scss @@ -15,8 +15,8 @@ limitations under the License. */ .mx_E2EIcon { - width: 25px; - height: 25px; + width: 16px; + height: 16px; margin: 0 9px; position: relative; display: block; @@ -30,16 +30,14 @@ limitations under the License. bottom: 0; left: 0; right: 0; - mask-repeat: no-repeat; - mask-size: contain; + background-repeat: no-repeat; + background-size: contain; } .mx_E2EIcon_verified::after { - mask-image: url('$(res)/img/e2e/verified.svg'); - background-color: $accent-color; + background-image: url('$(res)/img/e2e/verified.svg'); } .mx_E2EIcon_warning::after { - mask-image: url('$(res)/img/e2e/warning.svg'); - background-color: $warning-color; + background-image: url('$(res)/img/e2e/warning.svg'); } From 35877a06a387e5148cdcb1aa2158283dbdeb5371 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 21 Nov 2019 18:12:32 +0100 Subject: [PATCH 279/334] use correct icons with borders --- res/img/e2e/verified.svg | 14 +++----------- res/img/e2e/warning.svg | 15 ++++----------- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/res/img/e2e/verified.svg b/res/img/e2e/verified.svg index af6bb92297..464b443dcf 100644 --- a/res/img/e2e/verified.svg +++ b/res/img/e2e/verified.svg @@ -1,12 +1,4 @@ - - - + + + diff --git a/res/img/e2e/warning.svg b/res/img/e2e/warning.svg index 2501da6ab3..209ae0f71f 100644 --- a/res/img/e2e/warning.svg +++ b/res/img/e2e/warning.svg @@ -1,12 +1,5 @@ - - - + + + + From 521cbbac74b392d61b1b85c31d6e0d06808929d3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 22 Nov 2019 12:59:51 +0000 Subject: [PATCH 280/334] Make addEventListener conditional Safari doesn't support addEventListener --- src/theme.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/theme.js b/src/theme.js index fa7e3f783b..e89af55924 100644 --- a/src/theme.js +++ b/src/theme.js @@ -41,14 +41,18 @@ export class ThemeWatcher { start() { this._themeWatchRef = SettingsStore.watchSetting("theme", null, this._onChange); this._systemThemeWatchRef = SettingsStore.watchSetting("use_system_theme", null, this._onChange); - this._preferDark.addEventListener('change', this._onChange); - this._preferLight.addEventListener('change', this._onChange); + if (this._preferDark.addEventListener) { + this._preferDark.addEventListener('change', this._onChange); + this._preferLight.addEventListener('change', this._onChange); + } this._dispatcherRef = dis.register(this._onAction); } stop() { - this._preferDark.removeEventListener('change', this._onChange); - this._preferLight.removeEventListener('change', this._onChange); + if (this._preferDark.addEventListener) { + this._preferDark.removeEventListener('change', this._onChange); + this._preferLight.removeEventListener('change', this._onChange); + } SettingsStore.unwatchSetting(this._systemThemeWatchRef); SettingsStore.unwatchSetting(this._themeWatchRef); dis.unregister(this._dispatcherRef); From 5ec4b6efcdf74197b8efd60e1014aa0d83d50d6b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 22 Nov 2019 10:24:51 +0000 Subject: [PATCH 281/334] Show m.room.create event before the ELS on room upgrade Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/MessagePanel.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index cf2a5b1738..39504666bf 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -411,6 +411,11 @@ module.exports = createReactClass({ readMarkerInSummary = true; } + // If this m.room.create event should be shown (room upgrade) then show it before the summary + if (this._shouldShowEvent(mxEv)) { + ret.push(...this._getTilesForEvent(prevEvent, mxEv, false)); + } + const summarisedEvents = []; // Don't add m.room.create here as we don't want it inside the summary for (;i + 1 < this.props.events.length; i++) { const collapsedMxEv = this.props.events[i + 1]; From e86d2b616e647f85bbfa56cb79bc952ca932ad40 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 20 Nov 2019 16:18:28 +0100 Subject: [PATCH 282/334] add ToastContainer --- res/css/_components.scss | 1 + res/css/structures/_ToastContainer.scss | 105 ++++++++++++++++++++ src/components/structures/LoggedInView.js | 2 + src/components/structures/ToastContainer.js | 85 ++++++++++++++++ 4 files changed, 193 insertions(+) create mode 100644 res/css/structures/_ToastContainer.scss create mode 100644 src/components/structures/ToastContainer.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 40a2c576d0..f7147b3b9f 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -25,6 +25,7 @@ @import "./structures/_TabbedView.scss"; @import "./structures/_TagPanel.scss"; @import "./structures/_TagPanelButtons.scss"; +@import "./structures/_ToastContainer.scss"; @import "./structures/_TopLeftMenuButton.scss"; @import "./structures/_UploadBar.scss"; @import "./structures/_ViewSource.scss"; diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss new file mode 100644 index 0000000000..54132d19bf --- /dev/null +++ b/res/css/structures/_ToastContainer.scss @@ -0,0 +1,105 @@ +/* +Copyright 2019 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_ToastContainer { + position: absolute; + top: 0; + left: 70px; + z-index: 101; + padding: 4px; + display: grid; + grid-template-rows: 1fr 14px 6px; + + &.mx_ToastContainer_stacked::before { + content: ""; + margin: 0 4px; + grid-row: 2 / 4; + grid-column: 1; + background-color: white; + box-shadow: 0px 4px 12px $menu-box-shadow-color; + border-radius: 8px; + } + + .mx_Toast_toast { + grid-row: 1 / 3; + grid-column: 1; + color: $primary-fg-color; + background-color: $primary-bg-color; + box-shadow: 0px 4px 12px $menu-box-shadow-color; + border-radius: 8px; + overflow: hidden; + } + + .mx_Toast_toast { + display: grid; + grid-template-columns: 20px 1fr; + column-gap: 10px; + row-gap: 4px; + padding: 8px; + padding-right: 16px; + + &.mx_Toast_hasIcon { + &::after { + content: ""; + width: 20px; + height: 20px; + grid-column: 1; + grid-row: 1; + mask-size: 100%; + mask-repeat: no-repeat; + } + + &.mx_Toast_icon_verification::after { + mask-image: url("$(res)/img/e2e/normal.svg"); + background-color: $primary-fg-color; + } + + h2, .mx_Toast_body { + grid-column: 2; + } + } + + h2 { + grid-column: 1 / 3; + grid-row: 1; + margin: 0; + font-size: 15px; + font-weight: 600; + } + + .mx_Toast_body { + grid-column: 1 / 3; + grid-row: 2; + } + + .mx_Toast_buttons { + display: flex; + + > :not(:last-child) { + margin-right: 8px; + } + } + + .mx_Toast_description { + max-width: 400px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + margin: 4px 0 11px 0; + font-size: 12px; + } + } +} diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 889b0cdc8b..d071ba1d79 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -525,6 +525,7 @@ const LoggedInView = createReactClass({ const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage'); const GroupView = sdk.getComponent('structures.GroupView'); const MyGroups = sdk.getComponent('structures.MyGroups'); + const ToastContainer = sdk.getComponent('structures.ToastContainer'); const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar'); const CookieBar = sdk.getComponent('globals.CookieBar'); const NewVersionBar = sdk.getComponent('globals.NewVersionBar'); @@ -628,6 +629,7 @@ const LoggedInView = createReactClass({ return (
    { topBar } +
    { + if (payload.action === "show_toast") { + this._addToast(payload.toast); + } + }; + + _addToast(toast) { + this.setState({toasts: this.state.toasts.concat(toast)}); + } + + dismissTopToast = () => { + const [, ...remaining] = this.state.toasts; + this.setState({toasts: remaining}); + }; + + render() { + const totalCount = this.state.toasts.length; + if (totalCount === 0) { + return null; + } + const isStacked = totalCount > 1; + const topToast = this.state.toasts[0]; + const {title, icon, key, component, props} = topToast; + + const containerClasses = classNames("mx_ToastContainer", { + "mx_ToastContainer_stacked": isStacked, + }); + + const toastClasses = classNames("mx_Toast_toast", { + "mx_Toast_hasIcon": icon, + [`mx_Toast_icon_${icon}`]: icon, + }); + + const countIndicator = isStacked ? _t(" (1/%(totalCount)s)", {totalCount}) : null; + + const toastProps = Object.assign({}, props, { + dismiss: this.dismissTopToast, + key, + }); + + return ( +
    +
    +

    {title}{countIndicator}

    +
    {React.createElement(component, toastProps)}
    +
    +
    + ); + } +} From 66cc68bae4dcee465946089df4a5d973a8e02497 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 20 Nov 2019 16:18:59 +0100 Subject: [PATCH 283/334] add new-styled button might merge it later on with accessible button --- res/css/_components.scss | 1 + res/css/views/elements/_FormButton.scss | 31 +++++++++++++++++++++ src/components/views/elements/FormButton.js | 27 ++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 res/css/views/elements/_FormButton.scss create mode 100644 src/components/views/elements/FormButton.js diff --git a/res/css/_components.scss b/res/css/_components.scss index f7147b3b9f..8d2b1cc91a 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -91,6 +91,7 @@ @import "./views/elements/_ErrorBoundary.scss"; @import "./views/elements/_EventListSummary.scss"; @import "./views/elements/_Field.scss"; +@import "./views/elements/_FormButton.scss"; @import "./views/elements/_IconButton.scss"; @import "./views/elements/_ImageView.scss"; @import "./views/elements/_InlineSpinner.scss"; diff --git a/res/css/views/elements/_FormButton.scss b/res/css/views/elements/_FormButton.scss new file mode 100644 index 0000000000..191dee5566 --- /dev/null +++ b/res/css/views/elements/_FormButton.scss @@ -0,0 +1,31 @@ +/* +Copyright 2019 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_FormButton { + line-height: 16px; + padding: 5px 15px; + font-size: 12px; + + &.mx_AccessibleButton_kind_primary { + color: $accent-color; + background-color: $accent-bg-color; + } + + &.mx_AccessibleButton_kind_danger { + color: $notice-primary-color; + background-color: $notice-primary-bg-color; + } +} diff --git a/src/components/views/elements/FormButton.js b/src/components/views/elements/FormButton.js new file mode 100644 index 0000000000..d667132c38 --- /dev/null +++ b/src/components/views/elements/FormButton.js @@ -0,0 +1,27 @@ +/* +Copyright 2019 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 AccessibleButton from "./AccessibleButton"; + +export default function FormButton(props) { + const {className, label, kind, ...restProps} = props; + const newClassName = (className || "") + " mx_FormButton"; + const allProps = Object.assign({}, restProps, {className: newClassName, kind: kind || "primary", children: [label]}); + return React.createElement(AccessibleButton, allProps); +} + +FormButton.propTypes = AccessibleButton.propTypes; From f1c62e7dab0ef27bb3a5fd9fa8a63833a8779148 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 20 Nov 2019 16:19:51 +0100 Subject: [PATCH 284/334] make colors slightly more opaque than in design as it is very light otherwise --- res/themes/light/css/_light.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index dcd7ce166e..0a3ef812b8 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -12,9 +12,9 @@ $monospace-font-family: Inconsolata, Twemoji, 'Apple Color Emoji', 'Segoe UI Emo // unified palette // try to use these colors when possible $accent-color: #03b381; -$accent-bg-color: rgba(115, 247, 91, 0.08); +$accent-bg-color: rgba(3, 179, 129, 0.16); $notice-primary-color: #ff4b55; -$notice-primary-bg-color: rgba(255, 75, 85, 0.08); +$notice-primary-bg-color: rgba(255, 75, 85, 0.16); $notice-secondary-color: #61708b; $header-panel-bg-color: #f3f8fd; From c705752317769c7bcd0ed00cef8beca7c15d82a9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 22 Nov 2019 16:00:39 +0100 Subject: [PATCH 285/334] add toast for verification requests this uses a verification requests as emitted by the js-sdk with the `crypto.verification.request` rather than a verifier as emitted by `crypto.verification.start` as this works for both to_device and timeline events with the changes made in the js-sdk pr. --- .../views/toasts/VerificationRequestToast.js | 123 ++++++++++++++++++ src/i18n/strings/en_EN.json | 3 + src/utils/KeyVerificationStateObserver.js | 14 +- 3 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 src/components/views/toasts/VerificationRequestToast.js diff --git a/src/components/views/toasts/VerificationRequestToast.js b/src/components/views/toasts/VerificationRequestToast.js new file mode 100644 index 0000000000..c6f7f3a363 --- /dev/null +++ b/src/components/views/toasts/VerificationRequestToast.js @@ -0,0 +1,123 @@ +/* +Copyright 2019 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 PropTypes from 'prop-types'; +import sdk from "../../../index"; +import { _t } from '../../../languageHandler'; +import Modal from "../../../Modal"; +import MatrixClientPeg from '../../../MatrixClientPeg'; +import {verificationMethods} from 'matrix-js-sdk/lib/crypto'; +import KeyVerificationStateObserver, {userLabelForEventRoom} from "../../../utils/KeyVerificationStateObserver"; +import dis from "../../../dispatcher"; + +export default class VerificationRequestToast extends React.PureComponent { + constructor(props) { + super(props); + const {event, timeout} = props.request; + // to_device requests don't have a timestamp, so consider them age=0 + const age = event.getTs() ? event.getLocalAge() : 0; + const remaining = Math.max(0, timeout - age); + const counter = Math.ceil(remaining / 1000); + this.state = {counter}; + if (this.props.requestObserver) { + this.props.requestObserver.setCallback(this._checkRequestIsPending); + } + } + + componentDidMount() { + if (this.props.requestObserver) { + this.props.requestObserver.attach(); + this._checkRequestIsPending(); + } + this._intervalHandle = setInterval(() => { + let {counter} = this.state; + counter -= 1; + if (counter <= 0) { + this.cancel(); + } else { + this.setState({counter}); + } + }, 1000); + } + + componentWillUnmount() { + clearInterval(this._intervalHandle); + if (this.props.requestObserver) { + this.props.requestObserver.detach(); + } + } + + _checkRequestIsPending = () => { + if (!this.props.requestObserver.pending) { + this.props.dismiss(); + } + } + + cancel = () => { + this.props.dismiss(); + try { + this.props.request.cancel(); + } catch (err) { + console.error("Error while cancelling verification request", err); + } + } + + accept = () => { + this.props.dismiss(); + const {event} = this.props.request; + // no room id for to_device requests + if (event.getRoomId()) { + dis.dispatch({ + action: 'view_room', + room_id: event.getRoomId(), + should_peek: false, + }); + } + + const verifier = this.props.request.beginKeyVerification(verificationMethods.SAS); + const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog'); + Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {verifier}); + }; + + render() { + const FormButton = sdk.getComponent("elements.FormButton"); + const {event} = this.props.request; + const userId = event.getSender(); + let nameLabel = event.getRoomId() ? userLabelForEventRoom(userId, event) : userId; + // for legacy to_device verification requests + if (nameLabel === userId) { + const client = MatrixClientPeg.get(); + const user = client.getUser(event.getSender()); + if (user && user.displayName) { + nameLabel = _t("%(name)s (%(userId)s)", {name: user.displayName, userId}); + } + } + return (
    +
    {nameLabel}
    +
    + + +
    +
    ); + } +} + +VerificationRequestToast.propTypes = { + dismiss: PropTypes.func.isRequired, + request: PropTypes.object.isRequired, + requestObserver: PropTypes.instanceOf(KeyVerificationStateObserver), +}; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7709a4a398..177d999148 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -481,6 +481,7 @@ "Headphones": "Headphones", "Folder": "Folder", "Pin": "Pin", + "Decline (%(counter)s)": "Decline (%(counter)s)", "Accept to continue:": "Accept to continue:", "Failed to upload profile picture!": "Failed to upload profile picture!", "Upload new:": "Upload new:", @@ -1694,6 +1695,7 @@ "Review terms and conditions": "Review terms and conditions", "Old cryptography data detected": "Old cryptography data detected", "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.", + "Verification Request": "Verification Request", "Logout": "Logout", "%(creator)s created and configured the room.": "%(creator)s created and configured the room.", "Your Communities": "Your Communities", @@ -1759,6 +1761,7 @@ "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", + " (1/%(totalCount)s)": " (1/%(totalCount)s)", "Guest": "Guest", "Your profile": "Your profile", "Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others", diff --git a/src/utils/KeyVerificationStateObserver.js b/src/utils/KeyVerificationStateObserver.js index 7de50ec4bf..2f7c0367ad 100644 --- a/src/utils/KeyVerificationStateObserver.js +++ b/src/utils/KeyVerificationStateObserver.js @@ -30,6 +30,18 @@ export default class KeyVerificationStateObserver { this._updateVerificationState(); } + get concluded() { + return this.accepted || this.done || this.cancelled; + } + + get pending() { + return !this.concluded; + } + + setCallback(callback) { + this._updateCallback = callback; + } + attach() { this._requestEvent.on("Event.relationsCreated", this._onRelationsCreated); for (const phaseName of SUB_EVENT_TYPES_OF_INTEREST) { @@ -83,7 +95,7 @@ export default class KeyVerificationStateObserver { _onRelationsUpdated = (event) => { this._updateVerificationState(); - this._updateCallback(); + this._updateCallback && this._updateCallback(); }; _updateVerificationState() { From 8cb362002bf00062b8a106cdc2a2cd14b001f93b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 22 Nov 2019 16:02:11 +0100 Subject: [PATCH 286/334] show a toast instead of dialog when feature flag is enabled --- src/components/structures/MatrixChat.js | 35 ++++++++++++++++++++----- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 661a0c7077..9d0dd7da8f 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -62,6 +62,7 @@ import { countRoomsWithNotif } from '../../RoomNotifs'; import { ThemeWatcher } from "../../theme"; import { storeRoomAliasInCache } from '../../RoomAliasCache'; import { defer } from "../../utils/promise"; +import KeyVerificationStateObserver from '../../utils/KeyVerificationStateObserver'; // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. @@ -1270,7 +1271,6 @@ export default createReactClass({ this.firstSyncComplete = false; this.firstSyncPromise = defer(); const cli = MatrixClientPeg.get(); - const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog'); // Allow the JS SDK to reap timeline events. This reduces the amount of // memory consumed as the JS SDK stores multiple distinct copies of room @@ -1469,12 +1469,35 @@ export default createReactClass({ } }); - cli.on("crypto.verification.start", (verifier) => { - Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, { - verifier, - }); - }); + if (SettingsStore.isFeatureEnabled("feature_dm_verification")) { + cli.on("crypto.verification.request", request => { + let requestObserver; + if (request.event.getRoomId()) { + requestObserver = new KeyVerificationStateObserver( + request.event, MatrixClientPeg.get()); + } + if (!requestObserver || requestObserver.pending) { + dis.dispatch({ + action: "show_toast", + toast: { + key: request.event.getId(), + title: _t("Verification Request"), + icon: "verification", + props: {request, requestObserver}, + component: sdk.getComponent("toasts.VerificationRequestToast"), + }, + }); + } + }); + } else { + cli.on("crypto.verification.start", (verifier) => { + const IncomingSasDialog = sdk.getComponent("views.dialogs.IncomingSasDialog"); + Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, { + verifier, + }); + }); + } // Fire the tinter right on startup to ensure the default theme is applied // A later sync can/will correct the tint to be the right value for the user const colorScheme = SettingsStore.getValue("roomColor"); From 0dfb0f54688e2bff8427dd38d7ce108666fc871a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 22 Nov 2019 16:25:30 +0100 Subject: [PATCH 287/334] fix lint --- src/components/views/elements/FormButton.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/FormButton.js b/src/components/views/elements/FormButton.js index d667132c38..f6b4c986f5 100644 --- a/src/components/views/elements/FormButton.js +++ b/src/components/views/elements/FormButton.js @@ -20,7 +20,8 @@ import AccessibleButton from "./AccessibleButton"; export default function FormButton(props) { const {className, label, kind, ...restProps} = props; const newClassName = (className || "") + " mx_FormButton"; - const allProps = Object.assign({}, restProps, {className: newClassName, kind: kind || "primary", children: [label]}); + const allProps = Object.assign({}, restProps, + {className: newClassName, kind: kind || "primary", children: [label]}); return React.createElement(AccessibleButton, allProps); } From 309633181d60b0732966e7c8fe4acd4255341af4 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 22 Nov 2019 16:32:50 +0100 Subject: [PATCH 288/334] use FormButton in verification request tile too and dedupe styles --- res/css/structures/_ToastContainer.scss | 4 ---- res/css/views/elements/_FormButton.scss | 5 +++++ .../messages/_MKeyVerificationRequest.scss | 17 ----------------- .../views/messages/MKeyVerificationRequest.js | 6 +++--- 4 files changed, 8 insertions(+), 24 deletions(-) diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss index 54132d19bf..ca8477dcc5 100644 --- a/res/css/structures/_ToastContainer.scss +++ b/res/css/structures/_ToastContainer.scss @@ -87,10 +87,6 @@ limitations under the License. .mx_Toast_buttons { display: flex; - - > :not(:last-child) { - margin-right: 8px; - } } .mx_Toast_description { diff --git a/res/css/views/elements/_FormButton.scss b/res/css/views/elements/_FormButton.scss index 191dee5566..1483fe2091 100644 --- a/res/css/views/elements/_FormButton.scss +++ b/res/css/views/elements/_FormButton.scss @@ -18,6 +18,11 @@ limitations under the License. line-height: 16px; padding: 5px 15px; font-size: 12px; + height: min-content; + + &:not(:last-child) { + margin-right: 8px; + } &.mx_AccessibleButton_kind_primary { color: $accent-color; diff --git a/res/css/views/messages/_MKeyVerificationRequest.scss b/res/css/views/messages/_MKeyVerificationRequest.scss index 87a75dee82..ee20751083 100644 --- a/res/css/views/messages/_MKeyVerificationRequest.scss +++ b/res/css/views/messages/_MKeyVerificationRequest.scss @@ -65,23 +65,6 @@ limitations under the License. .mx_KeyVerification_buttons { align-items: center; display: flex; - - .mx_AccessibleButton_kind_decline { - color: $notice-primary-color; - background-color: $notice-primary-bg-color; - } - - .mx_AccessibleButton_kind_accept { - color: $accent-color; - background-color: $accent-bg-color; - } - - [role=button] { - margin: 10px; - padding: 7px 15px; - border-radius: 5px; - height: min-content; - } } .mx_KeyVerification_state { diff --git a/src/components/views/messages/MKeyVerificationRequest.js b/src/components/views/messages/MKeyVerificationRequest.js index 21d82309ed..b2a1724fc6 100644 --- a/src/components/views/messages/MKeyVerificationRequest.js +++ b/src/components/views/messages/MKeyVerificationRequest.js @@ -111,10 +111,10 @@ export default class MKeyVerificationRequest extends React.Component { userLabelForEventRoom(fromUserId, mxEvent)}
    ); const isResolved = !(this.state.accepted || this.state.cancelled || this.state.done); if (isResolved) { - const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); + const FormButton = sdk.getComponent("elements.FormButton"); stateNode = (
    - {_t("Decline")} - {_t("Accept")} + +
    ); } } else if (isOwn) { // request sent by us From 8ce1ed472654297091d8fc4b76c89ffe772572e7 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 22 Nov 2019 16:36:22 +0100 Subject: [PATCH 289/334] moar lint --- res/css/structures/_ToastContainer.scss | 3 --- 1 file changed, 3 deletions(-) diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss index ca8477dcc5..4c5e746e66 100644 --- a/res/css/structures/_ToastContainer.scss +++ b/res/css/structures/_ToastContainer.scss @@ -41,9 +41,6 @@ limitations under the License. box-shadow: 0px 4px 12px $menu-box-shadow-color; border-radius: 8px; overflow: hidden; - } - - .mx_Toast_toast { display: grid; grid-template-columns: 20px 1fr; column-gap: 10px; From 32b6fccbfcfe9f8197354e69634d48d2da557ce5 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 22 Nov 2019 17:26:14 +0100 Subject: [PATCH 290/334] fix double date separator --- src/components/structures/MessagePanel.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 7eae3ff7a3..912b865b9f 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -416,7 +416,8 @@ export default class MessagePanel extends React.Component { // If this m.room.create event should be shown (room upgrade) then show it before the summary if (this._shouldShowEvent(mxEv)) { - ret.push(...this._getTilesForEvent(prevEvent, mxEv, false)); + // pass in the mxEv as prevEvent as well so no extra DateSeparator is rendered + ret.push(...this._getTilesForEvent(mxEv, mxEv, false)); } const summarisedEvents = []; // Don't add m.room.create here as we don't want it inside the summary From f1d096e7aa860bb673513f3d6fead7880343574c Mon Sep 17 00:00:00 2001 From: Slavi Pantaleev Date: Fri, 22 Nov 2019 15:16:53 +0000 Subject: [PATCH 291/334] Translated using Weblate (Bulgarian) Currently translated at 95.9% (1840 of 1918 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/bg/ --- src/i18n/strings/bg.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/bg.json b/src/i18n/strings/bg.json index 3697cc635c..0ec1d91b9b 100644 --- a/src/i18n/strings/bg.json +++ b/src/i18n/strings/bg.json @@ -2260,5 +2260,7 @@ "You cancelled": "Отказахте потвърждаването", "%(name)s cancelled": "%(name)s отказа", "%(name)s wants to verify": "%(name)s иска да извърши потвърждение", - "You sent a verification request": "Изпратихте заявка за потвърждение" + "You sent a verification request": "Изпратихте заявка за потвърждение", + "Custom (%(level)s)": "Собствен (%(level)s)", + "Try out new ways to ignore people (experimental)": "Опитайте нови начини да игнорирате хора (експериментално)" } From 9bb98e2b9c64bf7127ffcd7143cc387a8d1ae965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanislav=20Luke=C5=A1?= Date: Fri, 22 Nov 2019 13:42:28 +0000 Subject: [PATCH 292/334] Translated using Weblate (Czech) Currently translated at 95.8% (1837 of 1918 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/cs/ --- src/i18n/strings/cs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index e4e01b0116..62e6147506 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -1269,7 +1269,7 @@ "Security & Privacy": "Bezpečnost & Soukromí", "Encryption": "Šifrování", "Once enabled, encryption cannot be disabled.": "Když se šifrování zapne, už nepůjde vypnout.", - "Encrypted": "Šifrování je zapnuté", + "Encrypted": "Šifrování", "General": "Obecné", "General failure": "Nějaká chyba", "This homeserver does not support login using email address.": "Tento homeserver neumožňuje přihlášní pomocí emailu.", From fab7dfd8e81f709d05e012284200d59a5c1369e5 Mon Sep 17 00:00:00 2001 From: random Date: Fri, 22 Nov 2019 14:01:15 +0000 Subject: [PATCH 293/334] Translated using Weblate (Italian) Currently translated at 99.9% (1916 of 1918 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 | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 9faa48328c..eb5f5f76f2 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -2298,5 +2298,22 @@ "This widget may use cookies.": "Questo widget può usare cookie.", "Send verification requests in direct message, including a new verification UX in the member panel.": "Invia le richieste di verifica via messaggio diretto, inclusa una nuova esperienza utente per la verifica nel pannello membri.", "Enable local event indexing and E2EE search (requires restart)": "Attiva l'indicizzazione di eventi locali e la ricerca E2EE (richiede riavvio)", - "Match system dark mode setting": "Combacia la modalità scura di sistema" + "Match system dark mode setting": "Combacia la modalità scura di sistema", + "Connecting to integration manager...": "Connessione al gestore di integrazioni...", + "Cannot connect to integration manager": "Impossibile connettere al gestore di integrazioni", + "The integration manager is offline or it cannot reach your homeserver.": "Il gestore di integrazioni è offline o non riesce a raggiungere il tuo homeserver.", + "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Usa un gestore di integrazioni (%(serverName)s) per gestire bot, widget e pacchetti di adesivi.", + "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Usa un gestore di integrazioni per gestire bot, widget e pacchetti di adesivi.", + "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "I gestori di integrazione ricevono dati di configurazione e possono modificare widget, inviare inviti alla stanza, assegnare permessi a tuo nome.", + "Failed to connect to integration manager": "Connessione al gestore di integrazioni fallita", + "Widgets do not use message encryption.": "I widget non usano la cifratura dei messaggi.", + "More options": "Altre opzioni", + "Integrations are disabled": "Le integrazioni sono disattivate", + "Enable 'Manage Integrations' in Settings to do this.": "Attiva 'Gestisci integrazioni' nelle impostazioni per continuare.", + "Integrations not allowed": "Integrazioni non permesse", + "Your Riot doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "Il tuo Riot non ti permette di usare il gestore di integrazioni per questa azione. Contatta un amministratore.", + "Reload": "Ricarica", + "Take picture": "Scatta foto", + "Remove for everyone": "Rimuovi per tutti", + "Remove for me": "Rimuovi per me" } From 2b7a7f88b8dd2dba96b58e18d81c53939707b781 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 22 Nov 2019 17:26:14 +0100 Subject: [PATCH 294/334] fix double date separator --- src/components/structures/MessagePanel.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 39504666bf..aad01a2bff 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -413,7 +413,8 @@ module.exports = createReactClass({ // If this m.room.create event should be shown (room upgrade) then show it before the summary if (this._shouldShowEvent(mxEv)) { - ret.push(...this._getTilesForEvent(prevEvent, mxEv, false)); + // pass in the mxEv as prevEvent as well so no extra DateSeparator is rendered + ret.push(...this._getTilesForEvent(mxEv, mxEv, false)); } const summarisedEvents = []; // Don't add m.room.create here as we don't want it inside the summary From aae315038309df0533986e3f7a186c236a6a7998 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 22 Nov 2019 16:50:32 +0000 Subject: [PATCH 295/334] Null check on thumbnail_file --- src/components/views/messages/MVideoBody.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/messages/MVideoBody.js b/src/components/views/messages/MVideoBody.js index 43e4f2dd75..44954344ff 100644 --- a/src/components/views/messages/MVideoBody.js +++ b/src/components/views/messages/MVideoBody.js @@ -88,7 +88,7 @@ module.exports = createReactClass({ const content = this.props.mxEvent.getContent(); if (content.file !== undefined && this.state.decryptedUrl === null) { let thumbnailPromise = Promise.resolve(null); - if (content.info.thumbnail_file) { + if (content.info && content.info.thumbnail_file) { thumbnailPromise = decryptFile( content.info.thumbnail_file, ).then(function(blob) { From f23e5942e6e80465a8031aacd15878fc0360563d Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Fri, 22 Nov 2019 17:18:26 +0000 Subject: [PATCH 296/334] Prepare changelog for v1.7.3-rc.2 --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dad0accd2..a3f72685db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +Changes in [1.7.3-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.3-rc.2) (2019-11-22) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.3-rc.1...v1.7.3-rc.2) + + * Fix double date separator for room upgrade tiles + [\#3663](https://github.com/matrix-org/matrix-react-sdk/pull/3663) + * Show m.room.create event before the ELS on room upgrade + [\#3660](https://github.com/matrix-org/matrix-react-sdk/pull/3660) + * Make addEventListener conditional + [\#3659](https://github.com/matrix-org/matrix-react-sdk/pull/3659) + * Fix e2e icons + [\#3658](https://github.com/matrix-org/matrix-react-sdk/pull/3658) + Changes in [1.7.3-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.3-rc.1) (2019-11-20) ============================================================================================================= [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.2...v1.7.3-rc.1) From 730967fd3f381ed26db5f09a3cbaf9824fe14191 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Fri, 22 Nov 2019 17:18:27 +0000 Subject: [PATCH 297/334] v1.7.3-rc.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e5d2d7635c..099fc30b33 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "1.7.3-rc.1", + "version": "1.7.3-rc.2", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 8254261f261006740cc53ff6ea0602f14aac4562 Mon Sep 17 00:00:00 2001 From: Keunes Date: Fri, 22 Nov 2019 18:48:44 +0000 Subject: [PATCH 298/334] Translated using Weblate (Dutch) Currently translated at 93.4% (1794 of 1921 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 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index 825f3c6a48..7ec8197eaf 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -1193,7 +1193,7 @@ "Encrypted, not sent": "Versleuteld, niet verstuurd", "Demote yourself?": "Uzelf degraderen?", "Demote": "Degraderen", - "Share Link to User": "Koppeling met gebruiker delen", + "Share Link to User": "Link naar gebruiker delen", "deleted": "verwijderd", "underlined": "onderstreept", "inline-code": "code", From d867f41f569e01db0fbeba732ff97ba354fc3f64 Mon Sep 17 00:00:00 2001 From: Nathan Follens Date: Fri, 22 Nov 2019 18:48:50 +0000 Subject: [PATCH 299/334] Translated using Weblate (Dutch) Currently translated at 93.4% (1794 of 1921 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 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index 7ec8197eaf..347afc3583 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -1193,7 +1193,7 @@ "Encrypted, not sent": "Versleuteld, niet verstuurd", "Demote yourself?": "Uzelf degraderen?", "Demote": "Degraderen", - "Share Link to User": "Link naar gebruiker delen", + "Share Link to User": "Koppeling naar gebruiker delen", "deleted": "verwijderd", "underlined": "onderstreept", "inline-code": "code", From 95e02899b45735da73f299043e341f08287fc30e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Sat, 23 Nov 2019 10:08:24 +0000 Subject: [PATCH 300/334] Translated using Weblate (French) Currently translated at 100.0% (1921 of 1921 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 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 0c13b3c722..097dd0824f 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2370,5 +2370,9 @@ "Reload": "Recharger", "Take picture": "Prendre une photo", "Remove for everyone": "Supprimer pour tout le monde", - "Remove for me": "Supprimer pour moi" + "Remove for me": "Supprimer pour moi", + "Decline (%(counter)s)": "Refuser (%(counter)s)", + "Manage integrations": "Gérer les intégrations", + "Verification Request": "Demande de vérification", + " (1/%(totalCount)s)": " (1/%(totalCount)s)" } From 1c02fa573f315ba5dbb479b440c7506f606d7b92 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Fri, 22 Nov 2019 18:52:54 +0000 Subject: [PATCH 301/334] Translated using Weblate (Hungarian) Currently translated at 100.0% (1921 of 1921 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 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 7ed4eb253c..9e41b7381e 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2358,5 +2358,9 @@ "Integrations are disabled": "Az integrációk le vannak tiltva", "Enable 'Manage Integrations' in Settings to do this.": "Ehhez engedélyezd az „Integrációk Kezelésé”-t a Beállításokban.", "Integrations not allowed": "Az integrációk nem engedélyezettek", - "Your Riot doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "A Riotod nem használhat ehhez Integrációs Menedzsert. Kérlek vedd fel a kapcsolatot az adminisztrátorral." + "Your Riot doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "A Riotod nem használhat ehhez Integrációs Menedzsert. Kérlek vedd fel a kapcsolatot az adminisztrátorral.", + "Decline (%(counter)s)": "Elutasítás (%(counter)s)", + "Manage integrations": "Integrációk kezelése", + "Verification Request": "Ellenőrzési kérés", + " (1/%(totalCount)s)": " (1/%(totalCount)s)" } From 00fd329bfeeb3dab87e2ccbc2d08a64ebed35c85 Mon Sep 17 00:00:00 2001 From: Andrew Date: Sat, 23 Nov 2019 18:56:46 +0000 Subject: [PATCH 302/334] Translated using Weblate (Italian) Currently translated at 100.0% (1921 of 1921 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 | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index eb5f5f76f2..21c03a7802 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -2315,5 +2315,11 @@ "Reload": "Ricarica", "Take picture": "Scatta foto", "Remove for everyone": "Rimuovi per tutti", - "Remove for me": "Rimuovi per me" + "Remove for me": "Rimuovi per me", + "Trust": "Fidati", + "Decline (%(counter)s)": "Rifiuta (%(counter)s)", + "Manage integrations": "Gestisci integrazioni", + "Ignored/Blocked": "Ignorati/Bloccati", + "Verification Request": "Richiesta verifica", + " (1/%(totalCount)s)": " (1/%(totalCount)s)" } From 1607bc329bc819866e22f1d68814f897d3d7a336 Mon Sep 17 00:00:00 2001 From: dreboy30 Date: Sat, 23 Nov 2019 15:16:23 +0000 Subject: [PATCH 303/334] Translated using Weblate (Portuguese) Currently translated at 34.5% (663 of 1921 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/pt/ --- src/i18n/strings/pt.json | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/pt.json b/src/i18n/strings/pt.json index 5a56e807e4..7aa8851daa 100644 --- a/src/i18n/strings/pt.json +++ b/src/i18n/strings/pt.json @@ -350,7 +350,7 @@ "No devices with registered encryption keys": "Não há dispositivos com chaves de criptografia registradas", "No more results": "Não há mais resultados", "No results": "Sem resultados", - "OK": "Ok", + "OK": "OK", "Revoke Moderator": "Retirar status de moderador", "Search": "Pesquisar", "Search failed": "Busca falhou", @@ -847,6 +847,29 @@ "Add Phone Number": "Adicione número de telefone", "The platform you're on": "A plataforma em que se encontra", "The version of Riot.im": "A versão do RIOT.im", - "Whether or not you're logged in (we don't record your username)": "Tenha ou não, iniciado sessão (não iremos guardar o seu username)", - "Your language of choice": "O seu idioma de escolha" + "Whether or not you're logged in (we don't record your username)": "Tenha ou não, iniciado sessão (não iremos guardar o seu nome de utilizador)", + "Your language of choice": "O seu idioma que escolheu", + "Which officially provided instance you are using, if any": "Qual instância oficial está utilizando, se for o caso", + "Whether or not you're using the Richtext mode of the Rich Text Editor": "Se está a usar o modo de texto enriquecido do editor de texto enriquecido", + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Se usa a funcionalidade 'breadcrumbs' (avatares em cima da lista de salas", + "Your homeserver's URL": "O URL do seu servidor de início", + "Your identity server's URL": "O URL do seu servidor de identidade", + "e.g. %(exampleValue)s": "ex. %(exampleValue)s", + "Every page you use in the app": "Todas as páginas que usa na aplicação", + "e.g. ": "ex. ", + "Your User Agent": "O seu Agente de Utilizador", + "Your device resolution": "A resolução do seu dispositivo", + "The information being sent to us to help make Riot.im better includes:": "As informações que estão sendo enviadas para ajudar a melhorar o Riot.im incluem:", + "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Quando esta página contém informação de que permitam a sua identificação, como uma sala, ID de utilizador ou de grupo, estes dados são removidos antes de serem enviados ao servidor.", + "Call Failed": "A chamada falhou", + "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Há dispositivos desconhecidos nesta sala: se continuar sem os verificar, será possível que alguém espie a sua chamada.", + "Review Devices": "Rever dispositivos", + "Call Anyway": "Ligar na mesma", + "Answer Anyway": "Responder na mesma", + "Call": "Ligar", + "Answer": "Responder", + "Call failed due to misconfigured server": "Chamada falhada devido a um erro de configuração do servidor", + "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Peça ao administrador do seu servidor inicial (%(homeserverDomain)s) de configurar um servidor TURN para que as chamadas funcionem fiavelmente.", + "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternativamente, pode tentar usar o servidor público em turn.matrix.org, mas não será tão fiável e partilhará o seu IP com esse servidor. Também pode gerir isso nas definições.", + "Try using turn.matrix.org": "Tente utilizar turn.matrix.org" } From 4fb6e4b9833e4cbb69dd773515ff41d17eb13f89 Mon Sep 17 00:00:00 2001 From: dreboy30 Date: Sat, 23 Nov 2019 15:31:58 +0000 Subject: [PATCH 304/334] Translated using Weblate (Portuguese (Brazil)) Currently translated at 68.8% (1321 of 1921 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/pt_BR/ --- src/i18n/strings/pt_BR.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json index 072215663d..415d2fdd4b 100644 --- a/src/i18n/strings/pt_BR.json +++ b/src/i18n/strings/pt_BR.json @@ -652,7 +652,7 @@ "Whether or not you're using the Richtext mode of the Rich Text Editor": "Se você está usando o editor de texto visual", "Your homeserver's URL": "A URL do seu Servidor de Base (homeserver)", "Your identity server's URL": "A URL do seu servidor de identidade", - "The information being sent to us to help make Riot.im better includes:": "As informações que estão sendo usadas para ajudar a melhorar o Riot.im incluem:", + "The information being sent to us to help make Riot.im better includes:": "As informações que estão sendo enviadas para ajudar a melhorar o Riot.im incluem:", "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Quando esta página tem informação de identificação, como uma sala, ID de usuária/o ou de grupo, estes dados são removidos antes de serem enviados ao servidor.", "Call Failed": "A chamada falhou", "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Há dispositivos desconhecidos nesta sala: se você continuar sem verificá-los, será possível alguém espiar sua chamada.", From 11fec80370fa7cd8e081acbd19e631a88cfff131 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 24 Nov 2019 20:52:22 -0700 Subject: [PATCH 305/334] Hide tooltips with CSS when they aren't visible Fixes https://github.com/vector-im/riot-web/issues/11456 --- src/components/views/elements/Tooltip.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/Tooltip.js b/src/components/views/elements/Tooltip.js index bb5f9f0604..8ff3ce9bdb 100644 --- a/src/components/views/elements/Tooltip.js +++ b/src/components/views/elements/Tooltip.js @@ -100,7 +100,9 @@ module.exports = createReactClass({ const parent = ReactDOM.findDOMNode(this).parentNode; let style = {}; style = this._updatePosition(style); - style.display = "block"; + // Hide the entire container when not visible. This prevents flashing of the tooltip + // if it is not meant to be visible on first mount. + style.display = this.props.visible ? "block" : "none"; const tooltipClasses = classNames("mx_Tooltip", this.props.tooltipClassName, { "mx_Tooltip_visible": this.props.visible, From 56a7de5157dd70d281b80cdf59523844971bb7c8 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Mon, 25 Nov 2019 08:31:58 +0000 Subject: [PATCH 306/334] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1921 of 1921 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 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 2fd014554e..a5f8e5e04b 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2363,5 +2363,9 @@ "Reload": "重新載入", "Take picture": "拍照", "Remove for everyone": "對所有人移除", - "Remove for me": "對我移除" + "Remove for me": "對我移除", + "Decline (%(counter)s)": "拒絕 (%(counter)s)", + "Manage integrations": "管理整合", + "Verification Request": "驗證請求", + " (1/%(totalCount)s)": " (1/%(totalCount)s)" } From 422ab81185433c3d61cd261760dfcaee192b72e2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 25 Nov 2019 12:31:57 +0100 Subject: [PATCH 307/334] a11y adjustments for toasts --- src/components/structures/ToastContainer.js | 2 +- src/components/views/toasts/VerificationRequestToast.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/ToastContainer.js b/src/components/structures/ToastContainer.js index b8ced1e9de..83bbdac1a1 100644 --- a/src/components/structures/ToastContainer.js +++ b/src/components/structures/ToastContainer.js @@ -74,7 +74,7 @@ export default class ToastContainer extends React.Component { }); return ( -
    +

    {title}{countIndicator}

    {React.createElement(component, toastProps)}
    diff --git a/src/components/views/toasts/VerificationRequestToast.js b/src/components/views/toasts/VerificationRequestToast.js index c6f7f3a363..89af91c41f 100644 --- a/src/components/views/toasts/VerificationRequestToast.js +++ b/src/components/views/toasts/VerificationRequestToast.js @@ -108,7 +108,7 @@ export default class VerificationRequestToast extends React.PureComponent { } return (
    {nameLabel}
    -
    +
    From 694f2cb1dc676c06fd2961e566eeaf144c5cf603 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 25 Nov 2019 13:20:20 +0100 Subject: [PATCH 308/334] make sure the toast container is always in the document --- src/components/structures/ToastContainer.js | 43 ++++++++++----------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/src/components/structures/ToastContainer.js b/src/components/structures/ToastContainer.js index 83bbdac1a1..a8dca35747 100644 --- a/src/components/structures/ToastContainer.js +++ b/src/components/structures/ToastContainer.js @@ -50,35 +50,34 @@ export default class ToastContainer extends React.Component { render() { const totalCount = this.state.toasts.length; - if (totalCount === 0) { - return null; - } const isStacked = totalCount > 1; - const topToast = this.state.toasts[0]; - const {title, icon, key, component, props} = topToast; + let toast; + if (totalCount !== 0) { + const topToast = this.state.toasts[0]; + const {title, icon, key, component, props} = topToast; + const toastClasses = classNames("mx_Toast_toast", { + "mx_Toast_hasIcon": icon, + [`mx_Toast_icon_${icon}`]: icon, + }); + const countIndicator = isStacked ? _t(" (1/%(totalCount)s)", {totalCount}) : null; + + const toastProps = Object.assign({}, props, { + dismiss: this.dismissTopToast, + key, + }); + toast = (
    +

    {title}{countIndicator}

    +
    {React.createElement(component, toastProps)}
    +
    ); + } const containerClasses = classNames("mx_ToastContainer", { "mx_ToastContainer_stacked": isStacked, }); - const toastClasses = classNames("mx_Toast_toast", { - "mx_Toast_hasIcon": icon, - [`mx_Toast_icon_${icon}`]: icon, - }); - - const countIndicator = isStacked ? _t(" (1/%(totalCount)s)", {totalCount}) : null; - - const toastProps = Object.assign({}, props, { - dismiss: this.dismissTopToast, - key, - }); - return ( -
    -
    -

    {title}{countIndicator}

    -
    {React.createElement(component, toastProps)}
    -
    +
    + {toast}
    ); } From 942db34e9263c6e9b6f7236cfdb704f223a169a7 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 25 Nov 2019 13:27:15 +0000 Subject: [PATCH 309/334] released js-sdk --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 099fc30b33..a92222579c 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "linkifyjs": "^2.1.6", "lodash": "^4.17.14", "lolex": "4.2", - "matrix-js-sdk": "2.4.4-rc.1", + "matrix-js-sdk": "2.4.4", "optimist": "^0.6.1", "pako": "^1.0.5", "png-chunks-extract": "^1.0.0", diff --git a/yarn.lock b/yarn.lock index eb72b11793..44d9548b54 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5197,10 +5197,10 @@ mathml-tag-names@^2.0.1: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.1.tgz#6dff66c99d55ecf739ca53c492e626f1d12a33cc" integrity sha512-pWB896KPGSGkp1XtyzRBftpTzwSOL0Gfk0wLvxt4f2mgzjY19o0LxJ3U25vNWTzsh7da+KTbuXQoQ3lOJZ8WHw== -matrix-js-sdk@2.4.4-rc.1: - version "2.4.4-rc.1" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-2.4.4-rc.1.tgz#5fd33fd11be9eea23cd0d0b8eb79da7a4b6253bf" - integrity sha512-Kn94zZMXh2EmihYL3lWNp2lpT7RtqcaUxjkP7H9Mr113swSOXtKr8RWMrvopAIguC1pcLzL+lCk+N8rrML2A4Q== +matrix-js-sdk@2.4.4: + version "2.4.4" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-2.4.4.tgz#d5e2d6fbe938c4275a1423a5f09330d33517ce3f" + integrity sha512-wSaRFvhWvwEzVaEkyBGo5ReumvaM5OrC1MJ6SVlyoLwH/WRPEXcUlu+rUNw5TFVEAH4TAVHXf/SVRBiR0j5nSQ== dependencies: another-json "^0.2.0" babel-runtime "^6.26.0" From 92a50c73274160cc3476dfcfba1a2fb3c0421f43 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 25 Nov 2019 13:30:40 +0000 Subject: [PATCH 310/334] Prepare changelog for v1.7.3 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3f72685db..bc2341863a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +Changes in [1.7.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.3) (2019-11-25) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.3-rc.2...v1.7.3) + + * No changes since rc.2 + Changes in [1.7.3-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.3-rc.2) (2019-11-22) ============================================================================================================= [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.3-rc.1...v1.7.3-rc.2) From f62cd367450ce47329828ca44b50550c774c61c6 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 25 Nov 2019 13:30:40 +0000 Subject: [PATCH 311/334] v1.7.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a92222579c..1ecfe63c23 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "1.7.3-rc.2", + "version": "1.7.3", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 54294e4927b0392d6eb6a14a9282128405955eb1 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 25 Nov 2019 14:03:40 +0000 Subject: [PATCH 312/334] Clarify that cross-signing is in development In an attempt to clarify the state of this highly anticipated feature, this updates the labs flag name to match. Part of https://github.com/vector-im/riot-web/issues/11492 --- src/i18n/strings/en_EN.json | 2 +- src/settings/Settings.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ddc7c016cc..0a83237f45 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -342,7 +342,7 @@ "Multiple integration managers": "Multiple integration managers", "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", "Send verification requests in direct message, including a new verification UX in the member panel.": "Send verification requests in direct message, including a new verification UX in the member panel.", - "Enable cross-signing to verify per-user instead of per-device": "Enable cross-signing to verify per-user instead of per-device", + "Enable cross-signing to verify per-user instead of per-device (in development)": "Enable cross-signing to verify per-user instead of per-device (in development)", "Enable local event indexing and E2EE search (requires restart)": "Enable local event indexing and E2EE search (requires restart)", "Use the new, faster, composer for writing messages": "Use the new, faster, composer for writing messages", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 89693f7c50..5a3283c5f0 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -143,7 +143,7 @@ export const SETTINGS = { }, "feature_cross_signing": { isFeature: true, - displayName: _td("Enable cross-signing to verify per-user instead of per-device"), + displayName: _td("Enable cross-signing to verify per-user instead of per-device (in development)"), supportedLevels: LEVELS_FEATURE, default: false, controller: new ReloadOnChangeController(), From 21a15fdcb4ec1098df2a62ea3fabea5812c7dbd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 21 Nov 2019 10:38:21 +0100 Subject: [PATCH 313/334] EventIndex: Move the checkpoint loading logic into the init method. The checkpoints don't seem to be loaded anymore in the onSync method, the reason why this has stopped working is left unexplored since loading the checkpoints makes most sense during the initialization step anyways. --- src/indexing/EventIndex.js | 13 +++++-------- src/indexing/EventIndexPeg.js | 2 -- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index 6bad992017..cf7e2d8da2 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -35,7 +35,12 @@ export default class EventIndex { 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); this.registerListeners(); } @@ -62,14 +67,6 @@ export default class EventIndex { onSync = async (state, prevState, data) => { const indexManager = PlatformPeg.get().getEventIndexingManager(); - if (prevState === null && state === "PREPARED") { - // Load our stored checkpoints, if any. - this.crawlerCheckpoints = await indexManager.loadCheckpoints(); - console.log("EventIndex: Loaded checkpoints", - this.crawlerCheckpoints); - return; - } - if (prevState === "PREPARED" && state === "SYNCING") { const addInitialCheckpoints = async () => { const client = MatrixClientPeg.get(); diff --git a/src/indexing/EventIndexPeg.js b/src/indexing/EventIndexPeg.js index c0bdd74ff4..75f0fa66ba 100644 --- a/src/indexing/EventIndexPeg.js +++ b/src/indexing/EventIndexPeg.js @@ -55,8 +55,6 @@ class EventIndexPeg { return false; } - console.log("EventIndex: Successfully initialized the event index"); - this.index = index; return true; From 9df227dbd08b130e93ed636ba2421022bc7cdd27 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 25 Nov 2019 16:21:09 -0700 Subject: [PATCH 314/334] Update breadcrumbs when we do eventually see upgraded rooms Fixes https://github.com/vector-im/riot-web/issues/11469 --- src/components/views/rooms/RoomBreadcrumbs.js | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/RoomBreadcrumbs.js b/src/components/views/rooms/RoomBreadcrumbs.js index 6529b5b1da..a80602368f 100644 --- a/src/components/views/rooms/RoomBreadcrumbs.js +++ b/src/components/views/rooms/RoomBreadcrumbs.js @@ -31,6 +31,9 @@ import {_t} from "../../../languageHandler"; const MAX_ROOMS = 20; const MIN_ROOMS_BEFORE_ENABLED = 10; +// The threshold time in milliseconds to wait for an autojoined room to show up. +const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90 seconds + export default class RoomBreadcrumbs extends React.Component { constructor(props) { super(props); @@ -38,6 +41,10 @@ export default class RoomBreadcrumbs extends React.Component { this.onAction = this.onAction.bind(this); this._dispatcherRef = null; + + // The room IDs we're waiting to come down the Room handler and when we + // started waiting for them. Used to track a room over an upgrade/autojoin. + this._waitingRoomQueue = [/* { roomId, addedTs } */]; } componentWillMount() { @@ -54,7 +61,7 @@ export default class RoomBreadcrumbs extends React.Component { MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt); MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().on("Event.decrypted", this.onEventDecrypted); - MatrixClientPeg.get().on("Room", this.onRoomMembershipChanged); + MatrixClientPeg.get().on("Room", this.onRoom); } componentWillUnmount() { @@ -68,7 +75,7 @@ export default class RoomBreadcrumbs extends React.Component { client.removeListener("Room.receipt", this.onRoomReceipt); client.removeListener("Room.timeline", this.onRoomTimeline); client.removeListener("Event.decrypted", this.onEventDecrypted); - client.removeListener("Room", this.onRoomMembershipChanged); + client.removeListener("Room", this.onRoom); } } @@ -87,6 +94,12 @@ export default class RoomBreadcrumbs extends React.Component { onAction(payload) { switch (payload.action) { case 'view_room': + if (payload.auto_join && !MatrixClientPeg.get().getRoom(payload.room_id)) { + // Queue the room instead of pushing it immediately - we're probably just waiting + // for a join to complete (ie: joining the upgraded room). + this._waitingRoomQueue.push({roomId: payload.room_id, addedTs: (new Date).getTime()}); + break; + } this._appendRoomId(payload.room_id); break; @@ -153,7 +166,20 @@ export default class RoomBreadcrumbs extends React.Component { if (!this.state.enabled && this._shouldEnable()) { this.setState({enabled: true}); } - } + }; + + onRoom = (room) => { + // Always check for membership changes when we see new rooms + this.onRoomMembershipChanged(); + + const waitingRoom = this._waitingRoomQueue.find(r => r.roomId === room.roomId); + if (!waitingRoom) return; + this._waitingRoomQueue.splice(this._waitingRoomQueue.indexOf(waitingRoom), 1); + + const now = (new Date()).getTime(); + if ((now - waitingRoom.addedTs) > AUTOJOIN_WAIT_THRESHOLD_MS) return; // Too long ago. + this._appendRoomId(room.roomId); // add the room we've been waiting for + }; _shouldEnable() { const client = MatrixClientPeg.get(); From 1ff39f252406a531a6e3f918e1a03c9001cb52be Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 25 Nov 2019 16:51:48 -0700 Subject: [PATCH 315/334] Make the communities button behave more like a toggle Fixes https://github.com/vector-im/riot-web/issues/10771 Clicking the button should toggle between your last page (room in our case) and the communities stuff. --- src/components/structures/MatrixChat.js | 16 ++++++++++++++++ src/components/views/elements/GroupsButton.js | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index f5b64fe2ed..eb0481e1cd 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -627,6 +627,22 @@ export default createReactClass({ case 'view_invite': showRoomInviteDialog(payload.roomId); break; + case 'view_last_screen': + // This function does what we want, despite the name. The idea is that it shows + // the last room we were looking at or some reasonable default/guess. We don't + // have to worry about email invites or similar being re-triggered because the + // function will have cleared that state and not execute that path. + this._showScreenAfterLogin(); + break; + case 'toggle_my_groups': + // We just dispatch the page change rather than have to worry about + // what the logic is for each of these branches. + if (this.state.page_type === PageTypes.MyGroups) { + dis.dispatch({action: 'view_last_screen'}); + } else { + dis.dispatch({action: 'view_my_groups'}); + } + break; case 'notifier_enabled': { this.setState({showNotifierToolbar: Notifier.shouldShowToolbar()}); } diff --git a/src/components/views/elements/GroupsButton.js b/src/components/views/elements/GroupsButton.js index 3932c827c5..7b15e96424 100644 --- a/src/components/views/elements/GroupsButton.js +++ b/src/components/views/elements/GroupsButton.js @@ -22,7 +22,7 @@ import { _t } from '../../../languageHandler'; const GroupsButton = function(props) { const ActionButton = sdk.getComponent('elements.ActionButton'); return ( - Date: Tue, 26 Nov 2019 01:14:03 +0000 Subject: [PATCH 316/334] console.log doesn't take %s substitutions --- src/CallHandler.js | 4 ++-- src/Presence.js | 2 +- src/components/structures/MatrixChat.js | 2 +- src/components/structures/RoomView.js | 4 ++-- src/components/views/auth/CaptchaForm.js | 2 +- src/components/views/elements/AppTile.js | 2 +- src/components/views/messages/TextualBody.js | 4 ++-- src/components/views/rooms/EventTile.js | 2 +- src/components/views/rooms/MemberInfo.js | 2 +- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/CallHandler.js b/src/CallHandler.js index bcdf7853fd..427be14097 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -322,7 +322,7 @@ function _onAction(payload) { }); return; } else if (members.length === 2) { - console.log("Place %s call in %s", payload.type, payload.room_id); + console.info("Place %s call in %s", payload.type, payload.room_id); const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id); placeCall(call); } else { // > 2 @@ -337,7 +337,7 @@ function _onAction(payload) { } break; case 'place_conference_call': - console.log("Place conference call in %s", payload.room_id); + console.info("Place conference call in %s", payload.room_id); _startCallApp(payload.room_id, payload.type); break; case 'incoming_call': diff --git a/src/Presence.js b/src/Presence.js index ca3db9b762..8ef988f171 100644 --- a/src/Presence.js +++ b/src/Presence.js @@ -96,7 +96,7 @@ class Presence { try { await MatrixClientPeg.get().setPresence(this.state); - console.log("Presence: %s", newState); + console.info("Presence: %s", newState); } catch (err) { console.error("Failed to set presence: %s", err); this.state = oldState; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 661a0c7077..81098df319 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1315,7 +1315,7 @@ export default createReactClass({ if (state === "SYNCING" && prevState === "SYNCING") { return; } - console.log("MatrixClient sync state => %s", state); + console.info("MatrixClient sync state => %s", state); if (state !== "PREPARED") { return; } self.firstSyncComplete = true; diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 6dee60bec7..db7ae33c8a 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -358,7 +358,7 @@ module.exports = createReactClass({ if (this.props.autoJoin) { this.onJoinButtonClicked(); } else if (!room && shouldPeek) { - console.log("Attempting to peek into room %s", roomId); + console.info("Attempting to peek into room %s", roomId); this.setState({ peekLoading: true, isPeeking: true, // this will change to false if peeking fails @@ -1897,7 +1897,7 @@ module.exports = createReactClass({ highlightedEventId = this.state.initialEventId; } - // console.log("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview); + // console.info("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview); const messagePanel = ( = %s", them.powerLevel, me.powerLevel); + //console.info("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel); return can; } const editPowerLevel = ( From 4cec7c41b109714eae3f5b44535b0130ad2f0fd4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 26 Nov 2019 09:52:03 -0700 Subject: [PATCH 317/334] Fix override behaviour of system vs defined themes Fixes https://github.com/vector-im/riot-web/issues/11509 --- src/i18n/strings/en_EN.json | 2 +- src/settings/Settings.js | 2 +- src/theme.js | 23 ++++++++++++++++++++--- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0a83237f45..9136f432dd 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -364,7 +364,7 @@ "Automatically replace plain text Emoji": "Automatically replace plain text Emoji", "Mirror local video feed": "Mirror local video feed", "Enable Community Filter Panel": "Enable Community Filter Panel", - "Match system dark mode setting": "Match system dark mode setting", + "Match system theme": "Match system theme", "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 devices from this device": "Never send encrypted messages to unverified devices from this device", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 5a3283c5f0..b02ab82400 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -284,7 +284,7 @@ export const SETTINGS = { "use_system_theme": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, default: true, - displayName: _td("Match system dark mode setting"), + displayName: _td("Match system theme"), }, "webRtcAllowPeerToPeer": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, diff --git a/src/theme.js b/src/theme.js index 9208ff2045..045e573361 100644 --- a/src/theme.js +++ b/src/theme.js @@ -20,7 +20,7 @@ import {_t} from "./languageHandler"; export const DEFAULT_THEME = "light"; import Tinter from "./Tinter"; import dis from "./dispatcher"; -import SettingsStore from "./settings/SettingsStore"; +import SettingsStore, {SettingLevel} from "./settings/SettingsStore"; export class ThemeWatcher { static _instance = null; @@ -60,14 +60,14 @@ export class ThemeWatcher { _onChange = () => { this.recheck(); - } + }; _onAction = (payload) => { if (payload.action === 'recheck_theme') { // XXX forceTheme this.recheck(payload.forceTheme); } - } + }; // XXX: forceTheme param aded here as local echo appears to be unreliable // https://github.com/vector-im/riot-web/issues/11443 @@ -80,6 +80,23 @@ export class ThemeWatcher { } getEffectiveTheme() { + // If the user has specifically enabled the system matching option (excluding default), + // then use that over anything else. We pick the lowest possible level for the setting + // to ensure the ordering otherwise works. + const systemThemeExplicit = SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme", null, false, true); + if (systemThemeExplicit) { + if (this._preferDark.matches) return 'dark'; + if (this._preferLight.matches) return 'light'; + } + + // If the user has specifically enabled the theme (without the system matching option being + // enabled specifically and excluding the default), use that theme. We pick the lowest possible + // level for the setting to ensure the ordering otherwise works. + const themeExplicit = SettingsStore.getValueAt(SettingLevel.DEVICE, "theme", null, false, true); + if (themeExplicit) return themeExplicit; + + // If the user hasn't really made a preference in either direction, assume the defaults of the + // settings and use those. if (SettingsStore.getValue('use_system_theme')) { if (this._preferDark.matches) return 'dark'; if (this._preferLight.matches) return 'light'; From d50d8877e0d92a843ef0b2d54318438259b86f44 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 26 Nov 2019 09:56:04 -0700 Subject: [PATCH 318/334] Appease the linter --- src/theme.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/theme.js b/src/theme.js index 045e573361..3f50f8ba88 100644 --- a/src/theme.js +++ b/src/theme.js @@ -83,7 +83,8 @@ export class ThemeWatcher { // If the user has specifically enabled the system matching option (excluding default), // then use that over anything else. We pick the lowest possible level for the setting // to ensure the ordering otherwise works. - const systemThemeExplicit = SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme", null, false, true); + const systemThemeExplicit = SettingsStore.getValueAt( + SettingLevel.DEVICE, "use_system_theme", null, false, true); if (systemThemeExplicit) { if (this._preferDark.matches) return 'dark'; if (this._preferLight.matches) return 'light'; @@ -92,7 +93,8 @@ export class ThemeWatcher { // If the user has specifically enabled the theme (without the system matching option being // enabled specifically and excluding the default), use that theme. We pick the lowest possible // level for the setting to ensure the ordering otherwise works. - const themeExplicit = SettingsStore.getValueAt(SettingLevel.DEVICE, "theme", null, false, true); + const themeExplicit = SettingsStore.getValueAt( + SettingLevel.DEVICE, "theme", null, false, true); if (themeExplicit) return themeExplicit; // If the user hasn't really made a preference in either direction, assume the defaults of the From ff2ac63530646ca1d3c22b322153589fff9fb59a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 26 Nov 2019 09:52:03 -0700 Subject: [PATCH 319/334] Fix override behaviour of system vs defined themes Fixes https://github.com/vector-im/riot-web/issues/11509 --- src/i18n/strings/en_EN.json | 2 +- src/settings/Settings.js | 2 +- src/theme.js | 23 ++++++++++++++++++++--- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7709a4a398..f31f086d26 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -364,7 +364,7 @@ "Automatically replace plain text Emoji": "Automatically replace plain text Emoji", "Mirror local video feed": "Mirror local video feed", "Enable Community Filter Panel": "Enable Community Filter Panel", - "Match system dark mode setting": "Match system dark mode setting", + "Match system theme": "Match system theme", "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 devices from this device": "Never send encrypted messages to unverified devices from this device", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 89693f7c50..54b8715b6e 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -284,7 +284,7 @@ export const SETTINGS = { "use_system_theme": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, default: true, - displayName: _td("Match system dark mode setting"), + displayName: _td("Match system theme"), }, "webRtcAllowPeerToPeer": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, diff --git a/src/theme.js b/src/theme.js index e89af55924..2973d2d3fd 100644 --- a/src/theme.js +++ b/src/theme.js @@ -20,7 +20,7 @@ import {_t} from "./languageHandler"; export const DEFAULT_THEME = "light"; import Tinter from "./Tinter"; import dis from "./dispatcher"; -import SettingsStore from "./settings/SettingsStore"; +import SettingsStore, {SettingLevel} from "./settings/SettingsStore"; export class ThemeWatcher { static _instance = null; @@ -60,14 +60,14 @@ export class ThemeWatcher { _onChange = () => { this.recheck(); - } + }; _onAction = (payload) => { if (payload.action === 'recheck_theme') { // XXX forceTheme this.recheck(payload.forceTheme); } - } + }; // XXX: forceTheme param aded here as local echo appears to be unreliable // https://github.com/vector-im/riot-web/issues/11443 @@ -80,6 +80,23 @@ export class ThemeWatcher { } getEffectiveTheme() { + // If the user has specifically enabled the system matching option (excluding default), + // then use that over anything else. We pick the lowest possible level for the setting + // to ensure the ordering otherwise works. + const systemThemeExplicit = SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme", null, false, true); + if (systemThemeExplicit) { + if (this._preferDark.matches) return 'dark'; + if (this._preferLight.matches) return 'light'; + } + + // If the user has specifically enabled the theme (without the system matching option being + // enabled specifically and excluding the default), use that theme. We pick the lowest possible + // level for the setting to ensure the ordering otherwise works. + const themeExplicit = SettingsStore.getValueAt(SettingLevel.DEVICE, "theme", null, false, true); + if (themeExplicit) return themeExplicit; + + // If the user hasn't really made a preference in either direction, assume the defaults of the + // settings and use those. if (SettingsStore.getValue('use_system_theme')) { if (this._preferDark.matches) return 'dark'; if (this._preferLight.matches) return 'light'; From 810fff64bc65ecfe146442c050f7c514bd20d563 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 26 Nov 2019 09:56:04 -0700 Subject: [PATCH 320/334] Appease the linter --- src/theme.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/theme.js b/src/theme.js index 2973d2d3fd..77cf6e9593 100644 --- a/src/theme.js +++ b/src/theme.js @@ -83,7 +83,8 @@ export class ThemeWatcher { // If the user has specifically enabled the system matching option (excluding default), // then use that over anything else. We pick the lowest possible level for the setting // to ensure the ordering otherwise works. - const systemThemeExplicit = SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme", null, false, true); + const systemThemeExplicit = SettingsStore.getValueAt( + SettingLevel.DEVICE, "use_system_theme", null, false, true); if (systemThemeExplicit) { if (this._preferDark.matches) return 'dark'; if (this._preferLight.matches) return 'light'; @@ -92,7 +93,8 @@ export class ThemeWatcher { // If the user has specifically enabled the theme (without the system matching option being // enabled specifically and excluding the default), use that theme. We pick the lowest possible // level for the setting to ensure the ordering otherwise works. - const themeExplicit = SettingsStore.getValueAt(SettingLevel.DEVICE, "theme", null, false, true); + const themeExplicit = SettingsStore.getValueAt( + SettingLevel.DEVICE, "theme", null, false, true); if (themeExplicit) return themeExplicit; // If the user hasn't really made a preference in either direction, assume the defaults of the From a2e3f6496312d9def35d529e7ede9b5bfcc7622f Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 26 Nov 2019 19:06:02 +0000 Subject: [PATCH 321/334] Change read markers to use CSS transitions Removes one of the two places we use Velocity, so we're one step closer to getting rid of it for good. Should therefore fix the fact that Velocity is leaking data entries and therefore
    elements. Hopefully also makes the logic in getEventTiles incrementally simpler, if still somwewhat byzantine. --- res/css/structures/_RoomView.scss | 3 + src/components/structures/MessagePanel.js | 228 ++++++++---------- .../structures/MessagePanel-test.js | 118 +++++---- 3 files changed, 176 insertions(+), 173 deletions(-) diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss index 50d412ad58..5e826306c6 100644 --- a/res/css/structures/_RoomView.scss +++ b/res/css/structures/_RoomView.scss @@ -221,6 +221,9 @@ hr.mx_RoomView_myReadMarker { position: relative; top: -1px; z-index: 1; + transition: width 400ms easeInSine 1s, opacity 400ms easeInSine 1s; + width: 99%; + opacity: 1; } .mx_RoomView_callStatusBar .mx_UploadBar_uploadProgressInner { diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 912b865b9f..d1cc1b7caf 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -16,8 +16,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -/* global Velocity */ - import React from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; @@ -111,14 +109,12 @@ export default class MessagePanel extends React.Component { constructor() { super(); - // the event after which we put a visible unread marker on the last - // render cycle; null if readMarkerVisible was false or the RM was - // suppressed (eg because it was at the end of the timeline) - this.currentReadMarkerEventId = null; - // the event after which we are showing a disappearing read marker - // animation - this.currentGhostEventId = null; + this.state = { + // previous positions the read marker has been in, so we can + // display 'ghost' read markers that are animating away + ghostReadMarkers: [], + }; // opaque readreceipt info for each userId; used by ReadReceiptMarker // to manage its animations @@ -157,10 +153,6 @@ export default class MessagePanel extends React.Component { // displayed event in the current render cycle. this._readReceiptsByUserId = {}; - // Remember the read marker ghost node so we can do the cleanup that - // Velocity requires - this._readMarkerGhostNode = null; - // Cache hidden events setting on mount since Settings is expensive to // query, and we check this in a hot code path. this._showHiddenEventsInTimeline = @@ -177,6 +169,16 @@ export default class MessagePanel extends React.Component { this._isMounted = false; } + componentDidUpdate(prevProps, prevState) { + if (prevProps.readMarkerVisible && this.props.readMarkerEventId !== prevProps.readMarkerEventId) { + const ghostReadMarkers = this.state.ghostReadMarkers; + ghostReadMarkers.push(prevProps.readMarkerEventId); + this.setState({ + ghostReadMarkers, + }); + } + } + /* get the DOM node representing the given event */ getNodeForEventId(eventId) { if (!this.eventNodes) { @@ -325,6 +327,78 @@ export default class MessagePanel extends React.Component { return !shouldHideEvent(mxEv); } + _readMarkerForEvent(eventId, isLastEvent) { + const visible = !isLastEvent && this.props.readMarkerVisible; + + if (this.props.readMarkerEventId === eventId) { + let hr; + // if the read marker comes at the end of the timeline (except + // for local echoes, which are excluded from RMs, because they + // don't have useful event ids), we don't want to show it, but + // we still want to create the
  • for it so that the + // algorithms which depend on its position on the screen aren't + // confused. + if (visible) { + hr =
    ; + } + + return ( +
  • + { hr } +
  • + ); + } else if (this.state.ghostReadMarkers.includes(eventId)) { + // We render 'ghost' read markers in the DOM while they + // transition away. This allows the actual read marker + // to be in the right place straight away without having + // to wait for the transition to finish. + // There are probably much simpler ways to do this transition, + // possibly using react-transition-group which handles keeping + // elements in the DOM whilst they transition out, although our + // case is a little more complex because only some of the items + // transition (ie. the read markers do but the event tiles do not) + // and TransitionGroup requires that all its children are Transitions. + const hr =
    ; + + // give it a key which depends on the event id. That will ensure that + // we get a new DOM node (restarting the animation) when the ghost + // moves to a different event. + return ( +
  • + { hr } +
  • + ); + } + + return null; + } + + _collectGhostReadMarker = (node) => { + if (node) { + // now the element has appeared, change the style which will trigger the CSS transition + requestAnimationFrame(() => { + node.style.width = '10%'; + node.style.opacity = '0'; + }); + } + }; + + _onGhostTransitionEnd = (ev) => { + // we can now clean up the ghost element + const finishedEventId = ev.target.dataset.eventid; + this.setState({ + ghostReadMarkers: this.state.ghostReadMarkers.filter(eid => eid !== finishedEventId), + }); + }; + _getEventTiles() { const DateSeparator = sdk.getComponent('messages.DateSeparator'); const EventListSummary = sdk.getComponent('views.elements.EventListSummary'); @@ -332,7 +406,6 @@ export default class MessagePanel extends React.Component { this.eventNodes = {}; - let visible = false; let i; // first figure out which is the last event in the list which we're @@ -367,16 +440,6 @@ export default class MessagePanel extends React.Component { let prevEvent = null; // the last event we showed - // assume there is no read marker until proven otherwise - let readMarkerVisible = false; - - // if the readmarker has moved, cancel any active ghost. - if (this.currentReadMarkerEventId && this.props.readMarkerEventId && - this.props.readMarkerVisible && - this.currentReadMarkerEventId !== this.props.readMarkerEventId) { - this.currentGhostEventId = null; - } - this._readReceiptsByEvent = {}; if (this.props.showReadReceipts) { this._readReceiptsByEvent = this._getReadReceiptsByShownEvent(); @@ -401,7 +464,7 @@ export default class MessagePanel extends React.Component { return false; }; if (mxEv.getType() === "m.room.create") { - let readMarkerInSummary = false; + let summaryReadMarker = null; const ts1 = mxEv.getTs(); if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) { @@ -410,9 +473,7 @@ export default class MessagePanel extends React.Component { } // If RM event is the first in the summary, append the RM after the summary - if (mxEv.getId() === this.props.readMarkerEventId) { - readMarkerInSummary = true; - } + summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(mxEv.getId()); // If this m.room.create event should be shown (room upgrade) then show it before the summary if (this._shouldShowEvent(mxEv)) { @@ -427,9 +488,7 @@ export default class MessagePanel extends React.Component { // Ignore redacted/hidden member events if (!this._shouldShowEvent(collapsedMxEv)) { // If this hidden event is the RM and in or at end of a summary put RM after the summary. - if (collapsedMxEv.getId() === this.props.readMarkerEventId) { - readMarkerInSummary = true; - } + summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId()); continue; } @@ -438,9 +497,7 @@ export default class MessagePanel extends React.Component { } // If RM event is in the summary, mark it as such and the RM will be appended after the summary. - if (collapsedMxEv.getId() === this.props.readMarkerEventId) { - readMarkerInSummary = true; - } + summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId()); summarisedEvents.push(collapsedMxEv); } @@ -468,8 +525,8 @@ export default class MessagePanel extends React.Component { { eventTiles } ); - if (readMarkerInSummary) { - ret.push(this._getReadMarkerTile(visible)); + if (summaryReadMarker) { + ret.push(summaryReadMarker); } prevEvent = mxEv; @@ -480,7 +537,7 @@ export default class MessagePanel extends React.Component { // Wrap consecutive member events in a ListSummary, ignore if redacted if (isMembershipChange(mxEv) && wantTile) { - let readMarkerInMels = false; + let summaryReadMarker = null; const ts1 = mxEv.getTs(); // Ensure that the key of the MemberEventListSummary does not change with new // member events. This will prevent it from being re-created unnecessarily, and @@ -498,9 +555,7 @@ export default class MessagePanel extends React.Component { } // If RM event is the first in the MELS, append the RM after MELS - if (mxEv.getId() === this.props.readMarkerEventId) { - readMarkerInMels = true; - } + summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(mxEv.getId()); const summarisedEvents = [mxEv]; for (;i + 1 < this.props.events.length; i++) { @@ -509,9 +564,7 @@ export default class MessagePanel extends React.Component { // Ignore redacted/hidden member events if (!this._shouldShowEvent(collapsedMxEv)) { // If this hidden event is the RM and in or at end of a MELS put RM after MELS. - if (collapsedMxEv.getId() === this.props.readMarkerEventId) { - readMarkerInMels = true; - } + summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId()); continue; } @@ -521,9 +574,7 @@ export default class MessagePanel extends React.Component { } // If RM event is in MELS mark it as such and the RM will be appended after MELS. - if (collapsedMxEv.getId() === this.props.readMarkerEventId) { - readMarkerInMels = true; - } + summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId()); summarisedEvents.push(collapsedMxEv); } @@ -554,8 +605,8 @@ export default class MessagePanel extends React.Component { { eventTiles } ); - if (readMarkerInMels) { - ret.push(this._getReadMarkerTile(visible)); + if (summaryReadMarker) { + ret.push(summaryReadMarker); } prevEvent = mxEv; @@ -570,40 +621,10 @@ export default class MessagePanel extends React.Component { prevEvent = mxEv; } - let isVisibleReadMarker = false; - - if (eventId === this.props.readMarkerEventId) { - visible = this.props.readMarkerVisible; - - // if the read marker comes at the end of the timeline (except - // for local echoes, which are excluded from RMs, because they - // don't have useful event ids), we don't want to show it, but - // we still want to create the
  • for it so that the - // algorithms which depend on its position on the screen aren't - // confused. - if (i >= lastShownNonLocalEchoIndex) { - visible = false; - } - ret.push(this._getReadMarkerTile(visible)); - readMarkerVisible = visible; - isVisibleReadMarker = visible; - } - - // XXX: there should be no need for a ghost tile - we should just use a - // a dispatch (user_activity_end) to start the RM animation. - if (eventId === this.currentGhostEventId) { - // if we're showing an animation, continue to show it. - ret.push(this._getReadMarkerGhostTile()); - } else if (!isVisibleReadMarker && - eventId === this.currentReadMarkerEventId) { - // there is currently a read-up-to marker at this point, but no - // more. Show an animation of it disappearing. - ret.push(this._getReadMarkerGhostTile()); - this.currentGhostEventId = eventId; - } + const readMarker = this._readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex); + if (readMarker) ret.push(readMarker); } - this.currentReadMarkerEventId = readMarkerVisible ? this.props.readMarkerEventId : null; return ret; } @@ -797,53 +818,6 @@ export default class MessagePanel extends React.Component { return receiptsByEvent; } - _getReadMarkerTile(visible) { - let hr; - if (visible) { - hr =
    ; - } - - return ( -
  • - { hr } -
  • - ); - } - - _startAnimation = (ghostNode) => { - if (this._readMarkerGhostNode) { - Velocity.Utilities.removeData(this._readMarkerGhostNode); - } - this._readMarkerGhostNode = ghostNode; - - if (ghostNode) { - // eslint-disable-next-line new-cap - Velocity(ghostNode, {opacity: '0', width: '10%'}, - {duration: 400, easing: 'easeInSine', - delay: 1000}); - } - }; - - _getReadMarkerGhostTile() { - const hr =
    ; - - // give it a key which depends on the event id. That will ensure that - // we get a new DOM node (restarting the animation) when the ghost - // moves to a different event. - return ( -
  • - { hr } -
  • - ); - } - _collectEventNode = (eventId, node) => { this.eventNodes[eventId] = node; } diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js index f58f1b925c..7c52512bc2 100644 --- a/test/components/structures/MessagePanel-test.js +++ b/test/components/structures/MessagePanel-test.js @@ -1,5 +1,6 @@ /* Copyright 2016 OpenMarket Ltd +Copyright 2019 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. @@ -81,6 +82,7 @@ describe('MessagePanel', function() { // HACK: We assume all settings want to be disabled SettingsStore.getValue = sinon.stub().returns(false); + SettingsStore.getValue.withArgs('showDisplaynameChanges').returns(true); // This option clobbers the duration of all animations to be 1ms // which makes unit testing a lot simpler (the animation doesn't @@ -109,6 +111,44 @@ describe('MessagePanel', function() { return events; } + + // make a collection of events with some member events that should be collapsed + // with a MemberEventListSummary + function mkMelsEvents() { + const events = []; + const ts0 = Date.now(); + + let i = 0; + events.push(test_utils.mkMessage({ + event: true, room: "!room:id", user: "@user:id", + ts: ts0 + ++i*1000, + })); + + for (i = 0; i < 10; i++) { + events.push(test_utils.mkMembership({ + event: true, room: "!room:id", user: "@user:id", + target: { + userId: "@user:id", + name: "Bob", + getAvatarUrl: () => { + return "avatar.jpeg"; + }, + }, + ts: ts0 + i*1000, + mship: 'join', + prevMship: 'join', + name: 'A user', + })); + } + + events.push(test_utils.mkMessage({ + event: true, room: "!room:id", user: "@user:id", + ts: ts0 + ++i*1000, + })); + + return events; + } + it('should show the events', function() { const res = TestUtils.renderIntoDocument( , @@ -120,6 +160,23 @@ describe('MessagePanel', function() { expect(tiles.length).toEqual(10); }); + it('should collapse adjacent member events', function() { + const res = TestUtils.renderIntoDocument( + , + ); + + // just check we have the right number of tiles for now + const tiles = TestUtils.scryRenderedComponentsWithType( + res, sdk.getComponent('rooms.EventTile'), + ); + expect(tiles.length).toEqual(2); + + const summaryTiles = TestUtils.scryRenderedComponentsWithType( + res, sdk.getComponent('elements.MemberEventListSummary'), + ); + expect(summaryTiles.length).toEqual(1); + }); + it('should show the read-marker in the right place', function() { const res = TestUtils.renderIntoDocument( , + ); + + const summary = TestUtils.findRenderedDOMComponentWithClass(res, 'mx_EventListSummary'); + + // find the
  • which wraps the read marker + const rm = TestUtils.findRenderedDOMComponentWithClass(res, 'mx_RoomView_myReadMarker_container'); + + expect(rm.previousSibling).toEqual(summary); + }); + it('shows a ghost read-marker when the read-marker moves', function(done) { // fake the clock so that we can test the velocity animation. clock.install(); @@ -191,50 +263,4 @@ describe('MessagePanel', function() { }, 100); }, 100); }); - - it('shows only one ghost when the RM moves twice', function() { - const parentDiv = document.createElement('div'); - - // first render with the RM in one place - let mp = ReactDOM.render( - , parentDiv); - - const tiles = TestUtils.scryRenderedComponentsWithType( - mp, sdk.getComponent('rooms.EventTile')); - const tileContainers = tiles.map(function(t) { - return ReactDOM.findDOMNode(t).parentNode; - }); - - // now move the RM - mp = ReactDOM.render( - , parentDiv); - - // now there should be two RM containers - let found = TestUtils.scryRenderedDOMComponentsWithClass(mp, 'mx_RoomView_myReadMarker_container'); - expect(found.length).toEqual(2); - - // the first should be the ghost - expect(tileContainers.indexOf(found[0].previousSibling)).toEqual(4); - - // the second should be the real RM - expect(tileContainers.indexOf(found[1].previousSibling)).toEqual(6); - - // and move the RM again - mp = ReactDOM.render( - , parentDiv); - - // still two RM containers - found = TestUtils.scryRenderedDOMComponentsWithClass(mp, 'mx_RoomView_myReadMarker_container'); - expect(found.length).toEqual(2); - - // they should have moved - expect(tileContainers.indexOf(found[0].previousSibling)).toEqual(6); - expect(tileContainers.indexOf(found[1].previousSibling)).toEqual(8); - }); }); From c2c8b1b6e0d8f0e07e502e8a58f877427603c89b Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 25 Nov 2019 14:03:40 +0000 Subject: [PATCH 322/334] Clarify that cross-signing is in development In an attempt to clarify the state of this highly anticipated feature, this updates the labs flag name to match. Part of https://github.com/vector-im/riot-web/issues/11492 --- src/i18n/strings/en_EN.json | 2 +- src/settings/Settings.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f31f086d26..ad877f11e7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -342,7 +342,7 @@ "Multiple integration managers": "Multiple integration managers", "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", "Send verification requests in direct message, including a new verification UX in the member panel.": "Send verification requests in direct message, including a new verification UX in the member panel.", - "Enable cross-signing to verify per-user instead of per-device": "Enable cross-signing to verify per-user instead of per-device", + "Enable cross-signing to verify per-user instead of per-device (in development)": "Enable cross-signing to verify per-user instead of per-device (in development)", "Enable local event indexing and E2EE search (requires restart)": "Enable local event indexing and E2EE search (requires restart)", "Use the new, faster, composer for writing messages": "Use the new, faster, composer for writing messages", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 54b8715b6e..b02ab82400 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -143,7 +143,7 @@ export const SETTINGS = { }, "feature_cross_signing": { isFeature: true, - displayName: _td("Enable cross-signing to verify per-user instead of per-device"), + displayName: _td("Enable cross-signing to verify per-user instead of per-device (in development)"), supportedLevels: LEVELS_FEATURE, default: false, controller: new ReloadOnChangeController(), From bb7cc20b1a6cf181ea209fd382dd5a62a506a89c Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 27 Nov 2019 00:45:46 +0000 Subject: [PATCH 323/334] fix font smoothing to match figma as per https://github.com/vector-im/riot-web/issues/11425 with apologies to https://usabilitypost.com/2012/11/05/stop-fixing-font-smoothing/ :/ --- res/css/_common.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/res/css/_common.scss b/res/css/_common.scss index 5987275f7f..51d985efb7 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -30,6 +30,11 @@ body { color: $primary-fg-color; border: 0px; margin: 0px; + + // needed to match the designs correctly on macOS + // see https://github.com/vector-im/riot-web/issues/11425 + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } pre, code { From d9e322bbcaf2f4bf78b608266c35c6c5292bb4ef Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 27 Nov 2019 10:32:21 +0000 Subject: [PATCH 324/334] Upgrade to JS SDK 2.4.5 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 1ecfe63c23..6cdd42c9da 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "linkifyjs": "^2.1.6", "lodash": "^4.17.14", "lolex": "4.2", - "matrix-js-sdk": "2.4.4", + "matrix-js-sdk": "2.4.5", "optimist": "^0.6.1", "pako": "^1.0.5", "png-chunks-extract": "^1.0.0", diff --git a/yarn.lock b/yarn.lock index 44d9548b54..e43f12760b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5197,10 +5197,10 @@ mathml-tag-names@^2.0.1: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.1.tgz#6dff66c99d55ecf739ca53c492e626f1d12a33cc" integrity sha512-pWB896KPGSGkp1XtyzRBftpTzwSOL0Gfk0wLvxt4f2mgzjY19o0LxJ3U25vNWTzsh7da+KTbuXQoQ3lOJZ8WHw== -matrix-js-sdk@2.4.4: - version "2.4.4" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-2.4.4.tgz#d5e2d6fbe938c4275a1423a5f09330d33517ce3f" - integrity sha512-wSaRFvhWvwEzVaEkyBGo5ReumvaM5OrC1MJ6SVlyoLwH/WRPEXcUlu+rUNw5TFVEAH4TAVHXf/SVRBiR0j5nSQ== +matrix-js-sdk@2.4.5: + version "2.4.5" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-2.4.5.tgz#0a02f0a3e18c59a393b34b8d6ebc54226cce6465" + integrity sha512-Mh0fPoiqyXRksFNYS4/2s20xAklmYVIgSms3qFvLhno32LN43NizUoAMBYYGtyjt8BQi+U77lbNL0s5f2V7gPQ== dependencies: another-json "^0.2.0" babel-runtime "^6.26.0" From 25b5921ddfeeb4dbbfd4387b8f8482fcbdda82fe Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 27 Nov 2019 10:38:35 +0000 Subject: [PATCH 325/334] Prepare changelog for v1.7.4 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc2341863a..8fe6f80e43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +Changes in [1.7.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.4) (2019-11-27) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.3...v1.7.4) + +* Upgrade to JS SDK 2.5.4 to relax identity server discovery and E2EE debugging +* Fix override behaviour of system vs defined theme +* Clarify that cross-signing is in development + Changes in [1.7.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.3) (2019-11-25) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.3-rc.2...v1.7.3) From 1a98c0d04e74684dc47ee1c7d917ae56a5a9ba91 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 27 Nov 2019 10:38:35 +0000 Subject: [PATCH 326/334] v1.7.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6cdd42c9da..7b75390293 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "1.7.3", + "version": "1.7.4", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 54d6b6aa73656d9c00cc047ace964bc73ce011e7 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 27 Nov 2019 13:31:44 +0000 Subject: [PATCH 327/334] Flip JS SDK back to develop --- package.json | 4 ++-- yarn.lock | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 92bf2e452d..5b82d9b111 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "linkifyjs": "^2.1.6", "lodash": "^4.17.14", "lolex": "4.2", - "matrix-js-sdk": "2.4.5", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "optimist": "^0.6.1", "pako": "^1.0.5", "png-chunks-extract": "^1.0.0", @@ -133,8 +133,8 @@ "eslint": "^5.12.0", "eslint-config-google": "^0.7.1", "eslint-plugin-babel": "^5.2.1", - "eslint-plugin-jest": "^23.0.4", "eslint-plugin-flowtype": "^2.30.0", + "eslint-plugin-jest": "^23.0.4", "eslint-plugin-react": "^7.7.0", "eslint-plugin-react-hooks": "^2.0.1", "estree-walker": "^0.5.0", diff --git a/yarn.lock b/yarn.lock index 62b45fd715..073fc95b37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5246,10 +5246,9 @@ mathml-tag-names@^2.0.1: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.1.tgz#6dff66c99d55ecf739ca53c492e626f1d12a33cc" integrity sha512-pWB896KPGSGkp1XtyzRBftpTzwSOL0Gfk0wLvxt4f2mgzjY19o0LxJ3U25vNWTzsh7da+KTbuXQoQ3lOJZ8WHw== -matrix-js-sdk@2.4.5: +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": version "2.4.5" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-2.4.5.tgz#0a02f0a3e18c59a393b34b8d6ebc54226cce6465" - integrity sha512-Mh0fPoiqyXRksFNYS4/2s20xAklmYVIgSms3qFvLhno32LN43NizUoAMBYYGtyjt8BQi+U77lbNL0s5f2V7gPQ== + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/6ea8003df23d55e2b84911c3204005c42a9ffa9c" dependencies: another-json "^0.2.0" babel-runtime "^6.26.0" From d6821ecb990c7667e96fab58661fbc3cb89e76bd Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 27 Nov 2019 10:44:36 -0700 Subject: [PATCH 328/334] Fix multi-invite error dialog messaging Fixes https://github.com/vector-im/riot-web/issues/11515 --- src/RoomInvite.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 64aab36128..babed0e6b8 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -202,11 +202,14 @@ function _showAnyInviteErrors(addrs, room, inviter) { } } + // React 16 doesn't let us use `errorList.join(
    )` anymore, so this is our solution + let description =
    {errorList.map(e =>
    {e}
    )}
    ; + if (errorList.length > 0) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Failed to invite the following users to the room', '', ErrorDialog, { title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}), - description: errorList.join(
    ), + description, }); } } From 275bd33a6c59f25106063e3967c93b942c111872 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 27 Nov 2019 10:48:05 -0700 Subject: [PATCH 329/334] Move the description into the relevant branch --- src/RoomInvite.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/RoomInvite.js b/src/RoomInvite.js index babed0e6b8..c72ca4d662 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -202,10 +202,10 @@ function _showAnyInviteErrors(addrs, room, inviter) { } } - // React 16 doesn't let us use `errorList.join(
    )` anymore, so this is our solution - let description =
    {errorList.map(e =>
    {e}
    )}
    ; - if (errorList.length > 0) { + // React 16 doesn't let us use `errorList.join(
    )` anymore, so this is our solution + let description =
    {errorList.map(e =>
    {e}
    )}
    ; + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Failed to invite the following users to the room', '', ErrorDialog, { title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}), From 673e6c31625ded3f9215ec4cfbdefa09b1c97295 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 27 Nov 2019 12:26:43 -0700 Subject: [PATCH 330/334] Don't assume that diffs will have an appropriate child node Fixes https://github.com/vector-im/riot-web/issues/11497 This is a regression from react-sdk v1.5.0 where the diff feature was added in the first place. It only affects lists. --- src/utils/MessageDiffUtils.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/utils/MessageDiffUtils.js b/src/utils/MessageDiffUtils.js index 78f3faa0c5..de0d8fdc89 100644 --- a/src/utils/MessageDiffUtils.js +++ b/src/utils/MessageDiffUtils.js @@ -77,6 +77,8 @@ function findRefNodes(root, route, isAddition) { const end = isAddition ? route.length - 1 : route.length; for (let i = 0; i < end; ++i) { refParentNode = refNode; + // Lists don't have appropriate child nodes we can use. + if (!refNode.childNodes[route[i]]) continue; refNode = refNode.childNodes[route[i]]; } return {refNode, refParentNode}; From 7b013ecc697ca8fe6aebfd96bab3ff583e2357d0 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 27 Nov 2019 12:54:31 -0700 Subject: [PATCH 331/334] Fix persisted widgets getting stuck at loading screens The widget itself is rendered underneath the loading screen, so we just have to disable the loading state. This commit also removes the "is" attribute because React 16 includes unknown attributes: https://reactjs.org/blog/2017/09/08/dom-attributes-in-react-16.html Fixes https://github.com/vector-im/riot-web/issues/11536 --- src/components/views/elements/AppTile.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 4cfce0c5dd..9a29843d3b 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -36,6 +36,7 @@ import classNames from 'classnames'; import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; import {createMenu} from "../../structures/ContextualMenu"; +import PersistedElement from "./PersistedElement"; const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:']; const ENABLE_REACT_PERF = false; @@ -247,7 +248,8 @@ export default class AppTile extends React.Component { this.setScalarToken(); } } else if (nextProps.show && !this.props.show) { - if (this.props.waitForIframeLoad) { + // We assume that persisted widgets are loaded and don't need a spinner. + if (this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey)) { this.setState({ loading: true, }); @@ -652,12 +654,7 @@ export default class AppTile extends React.Component { appTileBody = (
    { this.state.loading && loadingElement } - { /* - The "is" attribute in the following iframe tag is needed in order to enable rendering of the - "allow" attribute, which is unknown to react 15. - */ }