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 001/124] 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 002/124] 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 003/124] 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 004/124] 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 005/124] 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 006/124] 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 007/124] 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 008/124] 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 009/124] 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 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 010/124] 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 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 011/124] 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 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 012/124] 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 013/124] 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 014/124] 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 015/124] 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 016/124] 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 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 017/124] 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 018/124] 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 019/124] 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 020/124] 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 021/124] 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 022/124] 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 023/124] 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 024/124] 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 025/124] 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 026/124] 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 027/124] 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 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 028/124] 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 029/124] 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 030/124] 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 031/124] 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 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 032/124] 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 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 033/124] 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 034/124] 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 035/124] 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 036/124] 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 6b726a8e13786f6f10222cac3c1655f47c213354 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 15 Nov 2019 14:25:53 -0700 Subject: [PATCH 037/124] 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 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 038/124] 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 039/124] 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 040/124] 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 041/124] 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 042/124] 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 043/124] 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 044/124] 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 045/124] 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 046/124] 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 047/124] 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 048/124] 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 049/124] 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 050/124] 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 051/124] 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 052/124] 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 053/124] 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 054/124] 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 055/124] 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 056/124] 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 057/124] 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 058/124] 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 52e7d3505009732015f14be8743e58b4f650797f Mon Sep 17 00:00:00 2001 From: random Date: Mon, 18 Nov 2019 10:19:11 +0000 Subject: [PATCH 059/124] 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 060/124] 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 061/124] 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 062/124] 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 063/124] 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 064/124] 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 065/124] 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 066/124] 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 067/124] 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 068/124] 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 069/124] 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 070/124] 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 071/124] =?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 072/124] 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 073/124] 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 074/124] 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 075/124] 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 076/124] 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 077/124] 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 078/124] 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 079/124] 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 080/124] 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 081/124] 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 082/124] 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 083/124] 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 084/124] 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 085/124] 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 086/124] 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 087/124] 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 088/124] 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 089/124] 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 090/124] 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 091/124] 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 092/124] 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 093/124] 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 094/124] 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 095/124] 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 096/124] 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 097/124] 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 098/124] 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 099/124] 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 100/124] 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 101/124] 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 102/124] 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 103/124] 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 zcmce<2Vfjmxi5asneBadc4xNtrd8KUT6L{$NiLFmWLtJzD}p&27&8@1QL1) zH3Ucl0ylvW0$jMjy{En%AdA+vrE6BNzx1zXns9syA%UMST|d3}-A{dUJ0TBt5F-72 z_4@X%GnJ2CO~{QNT)*SA13M4J_sk}6e25Ts%4rv#pBywBZz1H}SK<7fdk^h9aN+JN zFD2yW#|d#N`*xm(>j>`sKoon+zOydbn|S2c_Yy*JeD&G;_w3wlU%&DpLaw_X=X>{~ z!IHJegxv5)9B20*IRE0mjYqD+c^pf%vksoN^KV0eDk0Z=8t=Cp*m?0GcDkzv$LIq| zp1t$Hp8VUFe3Ov(1o4ia96EU3`8Qtu!zn`U`U?P1I&|)yL;bz04x+0+$Mqj3HnMJZ zik`_HBNB0w011%{X(BzOpA3^_Bve(Fj0_I+^>&wv*<@H%d>)&D5?e&f(#&0=2&WP0XW`MLTP_}!!bu3uy3H&)8@%-U~%^e_DbM+OH6Hys&_$1XfFFtBM5 zyP0@=CU)e=k=V?IM@B}DY#E8M$6mi#x?nJtip5gXr^XYB#B@9vA5jP6a+kkp>F(a$ zOPiXu#7ePP3BVz)+1J=t**8g;#K_nKF=~+Lipl$|Yc^H=zy(SSbT5%4Ws(6mVq_|E zg61Tdnt8}y$jBL^In!YCV-&PHp|2m@GIr6-k$D!MmiCUCuDV`wPhq4{B7;u!(qQ14$Jp6K-=d{sBSmg z>uwIU3=Or!+FeKQV$VhV{s`ZW{*boL!}*puc>Z0$jS{#3 z0FDCSN-c!$Ar@j_7Q-GZHJeRaiOJ-jB#cShjZ~5X5&+Zu53bw_91KhnqcLfmnyLB- zX)fV?#Y#plr!pyYPXf_fbt^FnTVkBOz{ZdUZ zU+tTlohhG^@Y36zMU_4jjinMt4?SCCTfP=ndcGG{djh`0ZWjWizQjG!QBDbX&}7GvRZ~hf$G1KKfMW%wn1|J~7uBbD50}z4e0HtFZ)YW0}}< z=Ug;8QhVC-v5B$6<%!WnU+_FcH%|}lI3pEjD>cFKs4nj{7!a{|eRiPc7JR(cp0 z+98!&X zYt}i%{KS~eIv2@c-Z)QD!AN?#F@s>;mC6a`mJZI(T<+{eW`iYHniyz~TWzu{8tG2h zLk63Txh}tD!F02o^K%+6vLwwWi|sbaWp&90Yg{oqEaTqK-qL^qTsCp4`Ua@VLxLPT zB`Og{DPt>uj)HdllSUlUtp-YIfKK_mgs1^;&=y+xA@aiOr`|-&yC>yyyqy1N%Ap5h5irQC$*% zkhFu%u@!i`iBdD8`+!Z-Rtw16KgpR~owCDWLc*nRQ!$@TCSrJin$7gu1a<5E;ZPP* z5;R`ngzgjl1Dy+M0*TYVX0afwc!5)xf&=Mr6Oe|14Sm*`siOAFe$MAzD1=IwmUvwvq3V%{#V z+C7*xw-wmJ8HXLz8mf=qNcrRer^NV^j57t!Sx$063>Td29Bu~fIq~EW3HD?%DU&%U zKqq*)rVt5!u0ictH?{R8leL%pEEkKW1P`Y>IKVbP+7XSs8p~v3Y(*>)3ik5xa9({c z`xdm5k?J6$Mib@`TnY@2Gb#eb!ng;+pxYr~!0!wO!M&lip+D`UHJ#1m^F@m}4A}xX zY|^^JiGBh1o@d{4h{>*xP>&&!Z#ung|LvuCqds zc<^3&MKt)Mw!GctS@W)a7k+j}HX29 z@b@{a67h>0Wv90hwKYUOh+yjOEQ1$PRvgfzuH-rH^_9qj9=`DOzi0Wu}miZ@+R7AJA!BycoE z<7nt8@Bh{mI)z>Wccfg*RPO}u7T4KEX8%qnBu*I|nK^}J1cmjWeNf^QrnSGSwhC?wZEjdS7!fC{?zcCmw7v(v%j8wi4I|uGRapnE(^E`P?Azb6SfkVbAWN|lZiK% zF_@KR6U+gCw+?Fz!EBCIl1*)fEX&8@NtQA1-Ox^15cj~4WN z1>TkO)9dm^cX1FzJcbINK?p_SV6f>Oo&&l0H18o!m% zS1&153qSx0(ZzWWfC1=0ifx;Rj+eMIISw6NTJc`!+;%1=UJJl9(;!z>KLQ52g5%lI z{}opIIae5-{ks%{1(zWQA4>Xckdk#wQwSs`pmNw~I3W=oe^w`=<4HU>{6MP_!dSU+v2omWhVB z7Lg05Jai(>vSDz9=+Q64GC}%ZwY9M{Sj1E`6Ref!ebKZ=jqIxux1iE^qeTAB?ASBG z*Pnt8?nWU%qJlAgKb#G0Z=oMyxrhU`Q?Q5pr zvu}_1yX)Nj+jpP4e}{GL9sAG!><;bkzB{fzbjh99AG%oM6fd)<1ch^-54Wx(nkgsdPK4VTF>(SIksAIR>c;Gyai=4u)MNfT@09QTegl;=0 zxWXoCK*tS)D;aDnSG}PzQ93WiPIiUh$0s|iIB!CqRHC*m!I`)eN>l# zZzzE2JE{)9i%Cq3t*&pxV4A~5mkfJgm}8JevKM41IQ9JS-ryoJ!Z$dt4brcwuu$S^ zEE+~v0=Z0f-qLRzCD%guNMWd8sHXESJMWv81saI?%R4HS2Rsk3wq2X|p1pfs``A?b z&oesOp47O?Qc&Et;gKkk&T3njQaH2t%yA<)3d_l05Sn6)3+oW~Ld1%(LTf&o<-Faf zNk{nc@`X00>xjI~tf=`0Hp9-b7DC>HwOd)d?QK(_roFeXK7B_neEK;W$Jym|2NNFe z&K8NSd)FS2&zd{;U-L+ltK-Hw?jx}774~*uT?$^E!D?TC8l^b;i+O?wTfy;;dm{J1 zaMu-e-CveT!?J41H1%Y1K$u{2ZobzXcAA1W=RYwkidukm%TKX==3*HAKF!mT+X+14 zdL4J6ynf)KuZFUoBR3C?-0xlKbOqBs`SE1bum*`77F0qnpXXwrl1BeYFX_qTTQkCcqT~ylkjq_Ajx`@%1He^nJTT2Q z`c~99yRNrp z6aG{nVUH#{TVsWgJJ7UdQFTXmG3^Tn63$?vr#(>$dIN=(!@CY*JSq5t--4O&lL?R) zlLYm1^~%RVt~;*`kV`S{w>!2mDZxF*#i^gjnKJyrDr8he7VNpKajFbg5vY`6%@2Sh z(LdtPgU!(WjF}jVhJyX!VBgVSLxo0PmIJmAfoc<^shW@2jY5g$-4JvsK`@GL;C0}Y zxk7=zPT_)}uFC0t(fMOj=BMvqF^!a>bj|uzo6IpusP-|(dv38_f3-7APvhEE?QVNu z{o3hm^g?lT`#X<7e)Il3AY>%x;9N$6Rm`<>ZCY!8bwN9qky`sp3);Di)Y@O>?HF%) z_Eq*1XmTOa^*|6>OkJ^Y%#B&FnfXJ`RUUw+UE*pB*IomD7=m0H79cf%mxA!}Wp^*2Ue`DWFsfvHL_L4^qUiDj3+2`4 z{*2z2Ac?QT41~z?hk`Ck!wd)nfPvpAdgt5baio69%*+GDVp1T9h9h%ox{f870M;2+ z)7}>pmXAi$EbuLRF&OsU`(ySb?`Q3)+DnO4Izg>83x1vt)n*|s(QDC1K@kHQMbPgG zOjaO(HiC%t_ME9*Kp$6Q^N(PBnWa5!5`vB9Xnb`f11 zjgHo)LlK$YRQpaenn=?3vB#o;P`G9bsg3-3RGu+-`wJn$Wy_ z+ZUP|Z|-_wguJ;+HSz`ZD{`y?|Ulz2#GS~jM1?@kcYkz^a!v14ni|bZC?g6?al;BovTkZWrg28QyoZ=gMw8jdv4C6ghi%m8zz-wY=dv@C$+F!_ zEtbTj+X}cBCx9>L-I#P!r*p1L(6Zx; z77Yw6np!kDF*Z;g7#$v}^c2$2oeJr6%7geo7Jgl>hUoT_?jkG*eN-s;3=Xc4-WMV@VD>=(YeHYbPmx` zpnZYlGxg}4Qa6c}IaN~cWEVJtkOpFse|T`Xdhyzh>4>8GY>wWcB`cN;#k-tiM2yZ?*IMVN=bw+#{k3uaBI`;g z3Ddbx*|A{S>5&C98=|S>19i)Nmu+r91yCBWlnv;Vc5BrhMBLkE1(DU|D7TA|LQ*$} zj-fSO_j-eigv5M<^O~6SFC^N0E~gRgoS(lzL?RnSIQbh5*%0^eW;nUh;F^vRPQv5e zt5qMolph)!85{=TvL_PZqo3gP>dX{1`Yjdy5Q-l?vSrn>Nlv(cMi$WMhu+pnhRJw! zaYr_6flU~My3M`62AGHv7bQj$LoSIV8MXs1QLrDANtokMG`qUGhP#GJv0T16DZ({6 zox*zWS;?Xc5wqz_x*`gkQyE5@Q8HZ~=v(Lrfw) zDVxanxqX}BL#+#-I&;z(t)|z&yvYh7L;tG!YCo37M>G9Vr^73UA~PM6hlWQEE!%sr z_Jsd`(cVrjG`wC&jhO5Y51zGb)n((Co^#~#;Zd%oh@J{9g)FoDaW=yzo4CK zDcbeVEokRjiq`%Y-i}dQXJ4ZqWsgJSeA|N_8$yoUkmELV+=AG!<_PdlN^t8E*s$;j z@oX3#R@8KcH@b>PC#z|ZL8CnjE|KFNr9~m(E=CVBUOAJ=q%&6J$UuojTp39+!Ujj= zSvSmi;2BXFZatc5q0PXiy9YKzSS}Jtvq)z+GT22e2et*1neGVNS?&n5bk#l;i^pSh zS`BoWcb;xy%ol1i!YbrC^%!dt601zEt2#1_nG69xlY~;80Srp;=pY;hrJCb|1!*w` zO`8@dfg~S2=>#6BNEO*1+QkT}Ba&g3kbO0OSHq<Sgwt=3fg>4$_ zhF+)xbt9rdouQ9RQ$Lg!c*Xpd9hDKIOof}IgCc<^L4DzMUG&A7jk4`|&JF)`aPivK-MUz?9VhHr~@z{-=}{=KyyviiUQgMwCkVK+fO)7Jjai5evfhPpk4nL?fTib&8{H76L8srp>VkPv8QNXCg5^R zELGPZc(gF2$5MTIEEQ}KWZU4~GMiUtyJO2uh^2->yBYJgEun`MwG|lxtLY=0WNWK7 zZZ!klpdG(TUk4^8A)}lMs4S_vEf7t0nT@;`?*8VQF*kAriXGF&kqc4PT$V>wbm^rIedV@Ri5m0C zknFG<%?-H=oU_AEU^YqIQ(yo?YoK7|1ZM?wPjo_~U(U(lGS24kU%ntXx!~0rq$`m1 zF!y!1S73zA9n29l6bKbY3j3ecu>JFqa2h!bEBDxZ+FbYzx#rytpEm9!D~7@eiFWVm zS>}yy=%FueoDSIk7RSFoo1HV){*?cEuCG`7L9dI%qY=;YmQtxjU={cq;FBR|RvpPi z2*Kzeu&s_%vXJ~D8htEdJMNT#$#Jd!4;dQgZV8&C`Di3A(ackWz0zI5m6TXcPfg$2jjDf;zS`e0q3S^3nUyTz>iAH{Afz-3cF0fHCIN|`=i z1Tcm=!%n{|V2M%#yT0oRtAl?voj<4uyiq0YK6&EJa})B9)D?x_JoH zL#i&NA&SuC|CKxD1KWBuTh0T1hcvs^c{9&FzGz?w)*W}pEM0r~c&YH+P&^T41JOhx z>eAdX<12+bM(;f=JKp=n)lu$D(mT%ssu;mSU()|~0~q1FDAFNn5M_UU$aHIT_Zxnjqv}cEo*y%wz5Jrl37=1TO@CpfSDX5F%GX$nRYwY{gVgk`;E0+_(H0D70`Jt86=8)B0U5i&}J zrUX`_tICLo=xsuj%pVFF@<1ThA|fK3^K#)?7oc-86DAS8DiX~^77ZD_b=g=5g=36` z*ReZj8zl4K;19V-q}N4b(MTlMgg_1Y8h|zYHT1RP4dTMW64ppu^W1fi_H@3K z&hrGthPW`#$kv0xT#)k~XVL}18?it}iwNJpC6fyJ9Io+ITX6M~j`MdFry~*7>u`+> zwX}Ej^epZ;caKH!d((2%QtS>z+^$$(>-e-Pd*gD%l8r{t8dz*{VaIw_VTRdmGC8W3`{+Pw@PK1MDw>wfUHVvhtStT0#m)fb^)S%}@ypa5)kn0>^VA5q-xe76d_4V-j(*r{ zIj+-%VJX07Lt5dSP6u_4Gi{g(WXHmIn0)LhO{Y7ybzY~#x{`b$cT8@fW~*y?qZlse zG~fpg!1C#ZC`m{OXmH^;W#M(5YfgcBi%e={%F_+q!$u!s`@%fGER+;l+@@3f_7^#8 z|C7nFZ3%4F1lOPV2)#moIt0|?{vb_%I<~#8KWXh;f6}h6>rYxc*Ppa@O@E@0dap7k z{p*S2`hp3}$85zRw``A%>mMIGryq}cl#KaFM^RC8)|;iK5W@Ory^lWajR&00+O%qZ__RwX0LsZRxknwsB0D@$dHr7^pABPh!}puxEhXy1L3%sex1J- z^ayIDQs+Htg?>Jy#QnEl@5%*3zANtZt4hNE{>$AslFz?|{v{qz{C9r9>knoFcfZe{ zL@L6EZuiN-ta9tyIVGiLCj=fuNwgY@c;VThE4X_{%sJGY(up!TIO5MIbIAIn*Z$KQ za|aUU(Idx9`_XUsys?1Gbs4>Le&&JW;@twrFV=DVeZof2+Bq(3?axuPqpwI*Lp?-y!}#^ZEw~P=9Ll*e&%4QTUL#`m6)M<~mhNMwW%}jFVC`Nt;-~6Q z;7g=S@U!;&-)vL;9DUw>QgtjC=_;od!8;|F5@p$&x_S)q_*_p0;fJ@!N0c>bfLrC+0a~QbB=VlDvnlxD> z=4>k#%ksI!Y6P}kv}<`7A|C_oowF! zsGtOhoC}E9L9Y#-hcO^FkFjuEAEi?QiBty`rVF$&Sp&^Jomq2La^CBpl&@I7ZS~Sq zwy(}+;^9O(7N$p5R;M=eZC!INo+{A4v2NWdPSrWaLoiJY2%wed&QzlH(Y$hqRTd{STQ)V6r1G(cV6}IPVr~YM?biG z>pky3Z5ue#Msy&JF-JvKs{>dkmO()=fp*HB=g==OmD_-NSX+y~h~Q)5%G~um&!x&` zd5*txbJ#2)(H1F2pNU1QXJj`zJxVqmCq{&Q%&a#?Y&@V`Io49e^Bj@1d`Dgy4Dn$+k5R{ITcK#>AsnvvnK9G z@8Xyv-n#*^s5&x0C7%6@#pgy7@1q`qfx{O@>^=mM1W$qyt4Cj;ljYOd?r!87adlI0 z8Q#OXC{%Pc9HAu*^|jUK^{gXp7<`h=^vBha3)YOEpABKbn6r6J`=aqhi~Ib+xY|6n zqQAUyZdldbl_kS;)9QiIUA@_eH->b;aCo4%+!0lk*kEs|V{V8oYkEdcK5XO&I;YY2tBT>uLG2N;zq%WOQE=rJK&6irE{+H0XGG*f%h6V9?!ckPac1J88@ z!)p@Aty>*a!Xf%4mpdJ=?V~mbEJ0<%$f9c}E2>M=SkT-`c^nO_i_a!zk;l<4hlC@N zK?{u;w~BaR!z=>-+esS=OYn>k&2_BNUc+2!7FC^d3D}e@7^{%b?!(pEr$Yedu91(rK&_6 z;Q;N9CEG*MFLL7#qk5UG0$!#_E02#etX$-I>GjNM3&u%I`^>PAL@Eey(rDBY)#+3k z+$Y_dZYkz*dorKPXRVk6#5D!P>jTqSOkI&faGB}m5+^kpKGhbQ=^ok9GkjqDs#_-C zwsYuUJRI`7z3J73Jrm=V-X)8A*=bi_TI%fGH9B!<^h38D{?eXoR89oqwqUgO+T>tm zY-+f&2s5;D_9Zrj848d@H7c|u9;Xrd25cR{f#h6)FYOWnL(9#DMn_YS%RITU9P-pY zZBKcXl%00h{&OG|r^tg3E8+0bM`eG4>(J)TNR(S830M*RfLjpSP&?WGV9?G{TMrv1 z09amPU>{Gl5YXZ&1@lX1A~4P0;0kVnRsGp)D3oo_wv~#ZOekGy)>bD7PA#Z>Zheg) z?ED-|^>PH7m}8wwE?s-sjZ=#|$|L8kJn!&W--An#YPR^|?9}3=E2bvK>EbJfPc84= zvS_={zp7_wbLWofp{?@kmG1WLfu8M0Ux81v+~3!MaxF|`*H`9o1#v3A=2hojz!b1A zlVD$l?Hpm|`4^zS5LdAFH79VTjwA~>Sz|m{tF55R>`&i3`L-Ry2jd}uAFB$hw^>6o zJ;0rj1LOTm$14?%E8o^J#FrEpGLJd6chBJpG7V>s(w+3v5WzAjRSQ54WY@EO^DzP> z#R^F+JA59OCaLQZQ#bP=Dl%zUzx&snx^`;amNkoem-I@zPCe(U)3zMCdhMroer5-7 zaulp%9r7O|q_tYIa~`2DtbzmQ+GAxSmc2n`(S#DGcwJvSRy5Le;rJ)Iuy#IYb$El3 zor+(LFg3QJ7*t9vKldBMo`|#C5sTD*&G{DMnRFUG$ZNWRE*IghLtijntV`AlUjPPq zl7m$2t_x@00|=B8h(!$qNMN8Ns;d4*QN8aBDX~yY=i7E$u@7;5E=?a0!G>WsZa!7lr_BCLl9#) z3)mZn2X9HWdW?Kx;4?^B*oQh*nxLvnC%`uW`IaapnsP`rCov_L71GJ4OD7Fmj+IRr zEh}47pC0ZsdD|ib;~U3TZ|XdCBwkhhGSH;CWjK>WRfTwjZmjfpePct_b=#+>&KUuE z1U+G6=tw3WNv0FG}=>OT613!sX!w^SkVswv?MO-5Ji7%I*lSp&x7C7M}b(~Iz zoL}&z198jUcXmeNzoPd9WksodHx>4KoYdSD3_=n)#4LXbHb)Bch4oshCW;7!^kEbX zSr;{P5(uP|P6vbOmUMG5A4~<4O(h{*>NJFRYr;jR@_fCZQC+kM1FDh(+uPV$iX}Hq zPOK|tZ;eM2=>bai`Wy;`QC=y^)Ru1jugZe=(?iEIfAp_}9-PU(f&SfE?;oA- zUy3TCe?7c{#YwpD9M|Ovz-e{UL@_R4zVnQMLs{?z0qK9he5-Gg`O;_K1oy>gV;Ij> z>~W0to%PYyXG6@Dg1LGOXaA*swhjxNZNt6S2uK~NpKZ9e4`)$P3isYtKihC`8fPyO z_ugJVE9p?ilR13f!dE;^_RD`}0W#m01^^C?dcuVl$nN`XV;^2@iJm z+NMAVOS0Kp!y%u`d-M|YZF=?((ye%Yn%s$}WXMOc0F&#@w=%3v($9b71n1wWpHJa_ z?fiQd+%L|fbFFj}{RaFF-DFGER-y)@n`(w8cb|v2%Yo2?7BiQ>+}_;FSFQtVBh?^3 zN3T2n6pFp!P2Hr-GkcInH7D^3g}UtmC$tWRirK_vpsLmtSG6ADH&^Iq6~(`+#~kyi zww6|-&*qDndv{@poPSrZIpz)8T3hU@FKX`D#Rfw@Uzi%#+U?#@VACeMJ>+%R)){Ho z=L>1g!NA6i3U6I&#HfR_U!?8q71Xf^Rs(kAvT|b$?bl-R&M##7Jjt4DhU333twS^u z@PrJX({}6;gDHT@4@VOQvmEjHaCbL(lsri{K*^0)Q6B*Cfma7ES^=rm_{rUy613l5 z{n?y9Z7Y?MNozTud8$ENZVf1I}qaW7ybQ?gxe>d0eo&yL)൏#;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~ z6^+5ls)X5VldZn8m92?f%|+-{6tlDrz3O0>p6U>yO8i2*j8o5EXr*gue>XS}G6E7&DL7jt?b9%N!1y)z#XVs+)+D=@_cX zB&zEa2$Zjh<<=ytzuj8XxvJb}m2F;2A`)HYvD-b-D1pLflPv&6bG*e4e60i|&O@#M zNy3dqNl2^qN<>HUvdF*ED%JWjny42O2(o*6qx!J+KKV{tKlD{-8_=W=w+4A-Y{sD znX0z>SK$Fg>}2A zgvJEI!!2IsDUq?o+5Ta zwc75AR#nGh)m72Lmj(t02heZgiQ9@;=3j(msw6JOE>$F#(pFNl3(nqdIV_``U&1mN z%0llbk{0P3om%!4S;$2!n_CXSy4P*$PXfW>Ks5fzGZ2ih?#F~+glO;w#CU*jm!5=X z{BEFJIkTK((xbdQ-wNLcWF{}?W73xv$xPK&B#B8)_CWjei3KqwG81u)ze8s7ZOaHv z^sNSOpA>z&RqGpH=vx#$Ux#%T9kf$`BEHU$TSa+A3}mlnpal$LrBX;JNu^r;|JzcC z|Mo43A&j^ZV>(azGG^m_+K9E;Ky4Pa+oUH@dz)6P!2`7^)b0?_x?QU+xi^XJKqke# z@7HQe?ybhXyTrX8&}wxmr14~e^fpm@hgMtiWEDrytM_QNB~N~nIaHsFImEs1UFP05 zfd(QR$L3$;J<@+7KY=oV#BLB`DPJlgGJ#b4flM5-C#GnvEk90a(Wb&R9GTb$GcTR5}aCp`!et7# z&Od?Nu@|x1TrlSo!&rx6hQ-$WSSmrYR9qXRF+Ho~cZMv#$6uRQFMZB{O~}G?RaU*r z=WycIUiMw~03U!|QI!khSwgEvKubIvaW7O@6FPXvIusX+c|?m42!>>zm;g^h^_4&@b-{j(6OT((-oD3`5KVw20+My0MuX_?W7 zR4DLw0t5tx>cZOz7z7l8?EHJ!1MGaL2v||&0LD?&iI8#7iXC>%g&im05RbH!B=Kjh>gsUjGxKt_wx8dOL25F&t22E*ZCq1WkrR2!Cpy(232meHPQ z0l$luSRxj<3Xm&x-EA${7b%I&CKFK^i@3N+osT$zLW)@GEWIQzHq50?)wF1(K!^}prqW9)F~&`} zlYc+E$KjQo7eufUz$x>}4@bDi1|D2F=#*vWhU`JAy||icu{+j>tGO&L`la3oZfvCg`_hMeFOXvJnw3^3}xaXfcZr zIGUFR>xPthR%a5w>Hl~saempEt&2tb+p}X$9-H0euDPUp@^H4HzR|#)Hi)YbQOiSa zXa7`N{fcCLGsPyHK36KcZMc1XTk~kM&HiX46pBVep$MoYg!KWR6OyNea=?nQ6zCJ! zFHmgxu^W&Qi57+#0#cblq2LyKbA4?pQ5F=lVm^b4N%~=68di#tARC0N5*1G|vhS%K zK6E$~udZ*x7_4?@C|;qC5L;%ofxpJsX>?tOzhA|fQ@O}@cCkpV(kkKDJ%c(|5#V~|q z_O*_Vw2_GTEHQWo*-UTMe6}VN~ ztIoptHjsNj_rH2NNxyFf(iqLsuN+RFTW6iC!tHGS8^~azeYsL6jr&%sniQ4|VgCid zk#S^Sq0-hy`p-!tTdk0(7U{qqN-q8&7O1!i8`@nqgr00RIK4QVVc%rmL3_yAdsDBso-rXn}CvW49h0}& z+S(IIu{Wn)FiQdqtYyk;g7W}jLcwPhMhd?@_$f9Z!Kt;Ck3k0>YHq7elg}+(-PSxr z@wLYK`l_n>`o?1(m)qUm{A_Isdq!ZbC6|)5&o;Na-7Zh|*O?A15zIW7rG7K?yF=@D zIr4`2IwlU})N#&DHn-!Q2f-yhkSEwz zIu|e#s>32tcD}%5WQ0uu2TPH$`F)xxb?`h#!0ypaOz1rJfFu3PGv5fh?I!b6-~H}W z7Nf(3{dWj!huHVDr|a=>B39a^P@rf=*rfr5TJaLi6eDJ_D`@xVw`|dS>_Nx5=YGaz z*W17Ho$q`FCs**Bzxc&B1#YpIbid&V5T-_5XQ5byx=N{4_c5Z@H@3e7YIV_}J`edy z|IqU^>R)wpq<1$ zwHY_5^PNhsG?_Nq{0><^FrbH_>oLCUusdw;^1AJ2^Sf@qCa-R*1y8~I**EYWKg|mE zbygTO%qCnB6~St%6BFN|Onj#{?erp(>5vz17#uXn4!>>OY#!g{wp+|Myvu6Bog47@o7>1^cm&f66N2ba`Xi4#eR-n!hWyP zR|Ob6z{ZjS`$=|b%8R0f8cpx6s0x;o+_I5vCvB?=mQ|-})^}NjahG27WPlSwM$ z(RjONH!dd|$!^3}!>ZjVJw=jGJiEL`qIZeD|Nmg0a<*gsM*b&hFY6Z3_5fU+ZY%-O zV}-D!gKc4e(n*_+i|vOMPd6ML`blIW(_HI?h1|`$5~=b`J@Iqlye8#O1!}t@ibxJY z4EX~=;Zy)-kdRum6##!83&w-}QLo$WojiDOlFq{ols$aLb^x?A+~;)rbsK1p(sAeY z`}e=gHBQ%Krs$b_1CVXCjm0pwZ(~)}YW`!z2j#Ej&MIIk*JB}3Mppm=+ z)SCn?UDfMMdRn@w)sAA_E2R)CZHs$Hz1g!|cviJ&Z!|IEK|B?&oW#kbgD*n7e_S2p zn>~h-$=+(02q@7-uT%yiis-F~kn6_7xg_x`+NK;^^dJ|4`ldAm^V8Z9$nY;0Xm4k% zeRca_UoWkbi#NvO4YW{~1VN`@Z$+j9yjyVn5%LkkBdau>raw@T`?1@#z(tC$1X}kw z&2`!Ol+|UkSkvw6*LS3?R;$ZW;}2$QtlVR@m}^_c*L9|Cw%-rNW2+k*2jlUE<;n*G~6ec-XdH5V^i4iFE>g z+weeNB3tJ4rZx<8Om-O8yZt`TdP7Yd*++g`A~w9Dt|jc26Kng3pIVS>vQcL->rjtclao%?y&G>ymES9>DAakGid&9SA zn=#wZen#7jIc46 zyV#r*;VYn%8y~_xkxgn`M6(Un;ST zKpeQ*fGkoqcK6IA66$u?PeDe<1XbV z_TcUM)5bD4zB_c<;Bk5^C-bDAcHvJ-P*0Y1<=R0tq#D89i_bSOJrXNOBQhCxferhO zW044Bk!++sohZ^0sXt0EVu5y)*<47(Qk+EEZh)hLu&~_j6uHSyfsQ<<_-6%a5P|TI z^E8#+Otf_~G;YrcCsJc>{*5;4=va>Z5W$T-@CvUHeziUats+1cGB$A0W3N^Y8-im& zHMCisL=GW*3}q5-A+aD+XCPhxhq9cgkp(Xx&s#z<42ra+O3grPiy$yk;P3&Le_Byj zT_@Ltoi3BfY4G(XBRv@uU>jDa+2khKsRk;xl6D2j#<4*bZ3_>_3nl0#;EB?pMpg_sHGjojcpI@M!A+bW z@H+DVmRoi{?ZigQwEAyEtp2M(oDA7SdYuc?s3~2^~~w~Q-U82D{k(o72jyHa9zWGr;-Z13L~ zl=;ip7iC(;Hu6{;Sau(!b4xqKejI-o?Atk`b53CKYyT*@A4jeqkN({5}uXu`0S+j6bT z+#!5W>Njze7cIUEz)G~W)znl}xZQ0l+xmODYFcYr8Zs5>ic~B@d%)MFB2Kvd^a^t+ zeu_XU;h)+n3dzF|BuO^CqPAZXPRql#0$jT+H;{JO=7&o%uwurB{Ec+YdK0WJ+?g>A1rt!OhIg27ZW{?AXxJq8On7Ff?DQ zRdG&MTWN~O^m{8eLYpeRG1vYUUWR-0dhAU_H-bmtg_x%D^`OM{+t*J|Y{uwD;Wwzo z8u%18S*Sc?bAF`N#?^SE!Zk{wiE8egFj3%kP)t3+QUpga#wBgqL`y#Pg@Z?*xcutV zyLxxDw)g3!veA}-&FwS&jl(u>mShLwjJ5hmq@t&0?4OSwxc8iG_Z+=zD`L5Bd-nG2 z`n9Rf#-@>ESM6QrJ#qZHr_Mk3 z{FccbpSI)-`!PH zRq26^qJy^v--v>n2Hu(?)+?!q#9csL9UXOb9V)Pwu>gp5N&8_&^jkEw6;q{_y z!yv^EpedM)rl9zCTTlUz+|h|U2`&%2+q3!W6uI=8mI_-elZ>FsyJJC6Cu(@Gf{hG~V1053$53b#h-J;}c z&N)w-YN@O4O4anlQw?(`Gnubt);D$Kz)`c{s4u~LUcu^e*uoWw>Efb5R zvBM!@Eem^-9bQUhrwy@zGZ>uWyDzR@qzi8H^=@x&-m&6GE}zfo^j_w5K8DXsJl(Rr zr+a&g&-upJoha9Z-+0dHBN+{EH+t|@wgQ-luR#s>N0I$N%cJxL`lcK$y1^Hsu7n*S z5E#UxW=SGk)7spSNhk5C9)Ob!=%N{@C@t_i2=iGiNvD}mix*qYLGqp%gM`Frn5J34 z67`p=!}7U1_OFt$HHtkL>1*iht!UYU z9q+7au7Eo<-cgMVARTLgr8Ngj%LP?rkR1|w0G1jV%|?UyIPw&XW|N3mL!k!uSN#^m zXLKgZK^xQsuA9UOJL$vn(DCs|gm&183`Pd}db>JWn&68`xU1b&p@4{SI@Ac=(l2u? z@#2U%heBS2bBV7}vA{6@ZTFpX&VBp#-HX3_C*QMa(|abT=lVZAd&M2}cXxL4dnYGv-MsnM$;tO_o;|j2@3E{^wj;IKGd?;t z?vc$%F|#2~re6f5RcxIGr+#j*P{a&oDCifv=pseV!~rN|cwlGU{Qwib0A(={pa5(E zDDp(;*!aKz!D(RKz}hv#-LNtM3`wtgWd#=N##&a|E;$$j$93 z1=?*ki^Jo)!t1(8EO46j_*SiGS!vb>yfcwRvie4%yClDluMrp)W?cfm08;qFl%+L< zKgAU`z@KIIXCl2)R=I$d)uyp~;$-g`NT&yCYv^xHs--2BZfQyL`%(k7wF4>TZ#$F) zQW)Aux7#_7tReIVSx{m#Rb@kVCAPWzm!B@i-cxR@*?KJK(KXW6VsCucgXOp-PZHmy zDMs8O3bp(x1R_NWRM=VW_u`dyeEdhG9jQB~5F>+DQu0fC8Fu*+FXcz@$v88;weS~D zaPG&Yzc88s_HHR4y$x1aRKPZXn2r*CmaZk~aV(vP(=O*iIam16!Y=KKob^fpuft<{ z@Ij+&_vs%u2C=og@$pj@r^n52F1*d{fxXMIses#2z{fxe&q}-IU&fAkloQt?-kByZ z0U}j6!SaJkhH^s3MY>hV@?(|qOLeW%gto$bMFqYFS`oODz6y$KX&1gDT3+rClq+XH zB~Z?{N!#Z?jP>BQMY#Y5LanOR@Q@vAW;I z%EV}(hcC+cq=#tV3D6i);TyhyJ18GBI`Lsh>63`&`h$0w%oc}{`ZyuQSfzMkwRoaW zi><1O5K6|c=$xXUl5IqajOiA$)oDB?2i*bRyQLWX4yL<;_!y!GDX4{CO7Zzuv5S~r zn`xggchOmXEkXWRDkXN>Sn^y*9c9|nrWcVHYuZVeyvyXZ%a)BBr8vHsX}vKNy3vYk zSoby8Acx5y)y;ofdP&Uh-=^2I%yKEzOKJ5Bz4V>f-a7g~EFDr^cO88bwywKb`yMPH zSoo&2ftjUmz-MwhK~VYJYZ!bJ+yYs0Xd>)UNFCT>BzB5#@*7=CXOR5rV?g(h&yi0^4|tc`piW zsScue;Ti01@dw)C;t$dhw-YHLE+=FTC|FlM|FZ7wVuqoOy;bUc%ap4lipAs#$`un$ zqxrV57unid zrN3Z~>3JU5&O`+eE5{Vtl9tfbtwxK(w3j~Z5_+5T7dwRW`5r_W+}i|Q{bl|YJV)eM zoQ0-Jo>xLouR83qZJNRs(>B@WuuV)zf05xM-061j#826F&N+n3x&Sllj_s%MNw~*K5
  • <=_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 104/124] 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 105/124] 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 106/124] 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 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 107/124] 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 108/124] 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 109/124] 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 110/124] 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 111/124] 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 112/124] 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 113/124] 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 114/124] 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 115/124] 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 116/124] 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 117/124] 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 118/124] 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 119/124] 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 120/124] 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 5c6ef10c6b6c1f063e9eeee653cc8419ff172200 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 21 Nov 2019 15:55:30 +0000 Subject: [PATCH 121/124] 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 122/124] 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 123/124] 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 124/124] 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",