From f16090caa2f9dc87c4d395e6b4389e90b929733e Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 20 Jan 2020 16:47:20 -0500 Subject: [PATCH 01/15] initial attempt to hide pre-join UTDs --- src/components/structures/TimelinePanel.js | 73 ++++++++++++++++++++-- 1 file changed, 67 insertions(+), 6 deletions(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 30b02bfcca4..a12fb0209bd 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -178,6 +178,11 @@ const TimelinePanel = createReactClass({ backPaginating: false, forwardPaginating: false, + // the user's membership in the room prior to the first message in the + // current timeline window. + // FIXME: should be set to "leave" initially if the user is peeking + userRoomMembership: "join", + // cache of matrixClient.getSyncState() (but from the 'sync' event) clientSyncState: MatrixClientPeg.get().getSyncState(), @@ -360,17 +365,72 @@ const TimelinePanel = createReactClass({ debuglog("TimelinePanel: Initiating paginate; backwards:"+backwards); this.setState({[paginatingKey]: true}); - return this._timelineWindow.paginate(dir, PAGINATE_SIZE).then((r) => { + return this._timelineWindow.paginate(dir, PAGINATE_SIZE).then((canPaginate) => { if (this.unmounted) { return; } - debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+r); + debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+canPaginate); + + let { events, liveEvents } = this._getEvents(); + + const myUserId = MatrixClientPeg.get().credentials.userId; + let userRoomMembership = this.state.userRoomMembership; + let stopBackPaginating = false; + // FIXME: start at new events + for (let i = events.length - 1; i >= 0; i--) { + const event = events[i]; + debuglog(event); + if (event.getStateKey() === myUserId + && event.getType() === "m.room.member") { + const prevContent = event.getPrevContent(); + debuglog("prevcontent", prevContent); + userRoomMembership = prevContent.membership || "leave"; + } else if (userRoomMembership === "leave" && event.isDecryptionFailure()) { + // reached an undecryptable message when the user wasn't in + // the room -- don't try to load any more + stopBackPaginating = true; + if (backwards) { + canPaginate = false; + } + events = events.slice(i); + events[0]._setClearData({ + clearEvent: { + type: "m.room.message", + content: { + msgtype: "m.bad.encrypted", + body: "** This room has encrypted messages that were sent before you joined the room. You will not be able to read these messages. **" + } + } + }) + liveEvents = liveEvents.slice(i); + break; + } else if (userRoomMembership === "leave" && event.isBeingDecrypted()) { + // reached an undecryptable message when the user wasn't in + // the room -- don't try to load any more + stopBackPaginating = true; + if (backwards) { + canPaginate = false; + } + events = events.slice(i); + events[0]._setClearData({ + clearEvent: { + type: "m.room.message", + content: { + msgtype: "m.bad.encrypted", + body: "** This room has encrypted messages that were sent before you joined the room. You will not be able to read these messages. **" + } + } + }) + liveEvents = liveEvents.slice(i); + break; + } + } - const { events, liveEvents } = this._getEvents(); const newState = { [paginatingKey]: false, - [canPaginateKey]: r, + [canPaginateKey]: canPaginate, events, liveEvents, + userRoomMembership, }; // moving the window in this direction may mean that we can now @@ -378,7 +438,8 @@ const TimelinePanel = createReactClass({ const otherDirection = backwards ? EventTimeline.FORWARDS : EventTimeline.BACKWARDS; const canPaginateOtherWayKey = backwards ? 'canForwardPaginate' : 'canBackPaginate'; if (!this.state[canPaginateOtherWayKey] && - this._timelineWindow.canPaginate(otherDirection)) { + this._timelineWindow.canPaginate(otherDirection) && + (backwards || !stopBackPaginating)) { debuglog('TimelinePanel: can now', otherDirection, 'paginate again'); newState[canPaginateOtherWayKey] = true; } @@ -390,7 +451,7 @@ const TimelinePanel = createReactClass({ // itself into the right place return new Promise((resolve) => { this.setState(newState, () => { - resolve(r); + resolve(canPaginate); }); }); }); From 361add03eabab0533b558e97761552891d9a12ef Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 20 Jan 2020 17:01:01 -0500 Subject: [PATCH 02/15] lint --- src/components/structures/TimelinePanel.js | 33 +++++++--------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index a12fb0209bd..71644be74b2 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -384,9 +384,12 @@ const TimelinePanel = createReactClass({ const prevContent = event.getPrevContent(); debuglog("prevcontent", prevContent); userRoomMembership = prevContent.membership || "leave"; - } else if (userRoomMembership === "leave" && event.isDecryptionFailure()) { + } else if (userRoomMembership === "leave" && + (event.isDecryptionFailure() || event.isBeingDecrypted())) { // reached an undecryptable message when the user wasn't in // the room -- don't try to load any more + // (for now, we assume that events that are being decrypted will + // fail if they were sent while the use wasn't in the room) stopBackPaginating = true; if (backwards) { canPaginate = false; @@ -397,28 +400,12 @@ const TimelinePanel = createReactClass({ type: "m.room.message", content: { msgtype: "m.bad.encrypted", - body: "** This room has encrypted messages that were sent before you joined the room. You will not be able to read these messages. **" - } - } - }) - liveEvents = liveEvents.slice(i); - break; - } else if (userRoomMembership === "leave" && event.isBeingDecrypted()) { - // reached an undecryptable message when the user wasn't in - // the room -- don't try to load any more - stopBackPaginating = true; - if (backwards) { - canPaginate = false; - } - events = events.slice(i); - events[0]._setClearData({ - clearEvent: { - type: "m.room.message", - content: { - msgtype: "m.bad.encrypted", - body: "** This room has encrypted messages that were sent before you joined the room. You will not be able to read these messages. **" - } - } + body: + "** This room has encrypted messages that were sent before " + + "you joined the room. You will not be able to read " + + "these messages. **", + }, + }, }) liveEvents = liveEvents.slice(i); break; From 374305231b98a4d820b86d1f609deda34ea53049 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 21 Jan 2020 23:36:59 -0500 Subject: [PATCH 03/15] move UTD hiding to message panel --- src/components/structures/MessagePanel.js | 128 ++++++++++++++++++++- src/components/structures/TimelinePanel.js | 60 +--------- 2 files changed, 132 insertions(+), 56 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 4ad75eb7009..c94e205262c 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -29,6 +29,8 @@ import SettingsStore from '../../settings/SettingsStore'; import {_t} from "../../languageHandler"; import {haveTileForEvent} from "../views/rooms/EventTile"; +import {MatrixEvent} from 'matrix-js-sdk'; + const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const continuedTypes = ['m.sticker', 'm.room.message']; @@ -115,6 +117,7 @@ export default class MessagePanel extends React.Component { // previous positions the read marker has been in, so we can // display 'ghost' read markers that are animating away ghostReadMarkers: [], + preventBackPaginating: false, }; // opaque readreceipt info for each userId; used by ReadReceiptMarker @@ -405,6 +408,7 @@ export default class MessagePanel extends React.Component { const DateSeparator = sdk.getComponent('messages.DateSeparator'); const EventListSummary = sdk.getComponent('views.elements.EventListSummary'); const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary'); + const EventTile = sdk.getComponent('rooms.EventTile'); this.eventNodes = {}; @@ -419,6 +423,7 @@ export default class MessagePanel extends React.Component { let lastShownEvent; let lastShownNonLocalEchoIndex = -1; + for (i = this.props.events.length-1; i >= 0; i--) { const mxEv = this.props.events[i]; if (!this._shouldShowEvent(mxEv)) { @@ -440,6 +445,68 @@ export default class MessagePanel extends React.Component { const ret = []; + let firstShownEventIndex = checkForPreJoinUISI( + this.props.events, this.props.room, this.props.ourUserId + ); + + if (firstShownEventIndex > 0) { + if (!this.state.preventBackPaginating) { + this.setState({ + preventBackPaginating: true, + }); + } + const hiddenEvent = this.props.events[firstShownEventIndex-1]; + const eventId = hiddenEvent.getId(); + const event = new MatrixEvent({ + type: "m.room.message", + content: { + msgtype: "m.text", + body: + "** This room has encrypted messages that were sent before " + + "you joined the room. You will not be able to read " + + "these messages. **", + }, + event_id: hiddenEvent.getId(), + origin_server_ts: hiddenEvent.getTs(), + room_id: hiddenEvent.getRoomId(), + sender: "invalid" + }); + // FIXME: use a special tile for the error message + ret.push( +
  • + +
  • , + ); + } else { + if (this.state.preventBackPaginating) { + this.setState({ + preventBackPaginating: false, + }); + } + firstShownEventIndex = 0; + } + let prevEvent = null; // the last event we showed this._readReceiptsByEvent = {}; @@ -447,7 +514,7 @@ export default class MessagePanel extends React.Component { this._readReceiptsByEvent = this._getReadReceiptsByShownEvent(); } - for (i = 0; i < this.props.events.length; i++) { + for (i = firstShownEventIndex; i < this.props.events.length; i++) { const mxEv = this.props.events[i]; const eventId = mxEv.getId(); const last = (mxEv === lastShownEvent); @@ -883,6 +950,15 @@ export default class MessagePanel extends React.Component { } } + onFillRequest(backwards) { + console.warn("***", this, this.state, this.props); + // FIXME: why are this.state and this.props undefined??? + if (this.state.preventBackPaginating) { + return Promise.resolve(false); + } + return this.props.onFillRequest(backwards); + } + render() { const ScrollPanel = sdk.getComponent("structures.ScrollPanel"); const WhoIsTypingTile = sdk.getComponent("rooms.WhoIsTypingTile"); @@ -921,7 +997,7 @@ export default class MessagePanel extends React.Component { className={className} onScroll={this.props.onScroll} onResize={this.onResize} - onFillRequest={this.props.onFillRequest} + onFillRequest={this.onFillRequest} onUnfillRequest={this.props.onUnfillRequest} style={style} stickyBottom={this.props.stickyBottom} @@ -935,3 +1011,51 @@ export default class MessagePanel extends React.Component { ); } } + +function checkForPreJoinUISI(events, room, userId) { + if (events.length === 0) { + return -1; + } + + let i; + let userMembership = "leave"; + // find the last event that is in a timeline (events may not be in a + // timeline if they have not been sent yet) and get the user's membership + // at that point. + for (i = events.length - 1; i >= 0; i--) { + const timeline = room.getTimelineForEvent(events[i].getId()); + if (timeline) { + const userMembershipEvent = timeline.getState("f").getMember(userId); // FIXME: EventTimeline.FORWARDS + userMembership = userMembershipEvent ? userMembershipEvent.membership : "leave"; + const timelineEvents = timeline.getEvents(); + for (let j = timelineEvents.length - 1; j >= 0; j--) { + const event = timelineEvents[i]; + if (event.getStateKey() === userId + && event.getType() === "m.room.member") { + const prevContent = event.getPrevContent(); + userMembership = prevContent.membership || "leave"; + } + } + break; + } + } + + // now go through the rest of the events and find the first undecryptable + // one that was sent when the user wasn't in the room + for (; i >= 0; i--) { + const event = events[i]; + if (event.getStateKey() === userId + && event.getType() === "m.room.member") { + const prevContent = event.getPrevContent(); + userMembership = prevContent.membership || "leave"; + } else if (userMembership === "leave" && + (event.isDecryptionFailure() || event.isBeingDecrypted())) { + // reached an undecryptable message when the user wasn't in + // the room -- don't try to load any more + // (for now, we assume that events that are being decrypted will + // fail if they were sent while the use wasn't in the room) + return i + 1; + } + } + return 0; +} diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 71644be74b2..30b02bfcca4 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -178,11 +178,6 @@ const TimelinePanel = createReactClass({ backPaginating: false, forwardPaginating: false, - // the user's membership in the room prior to the first message in the - // current timeline window. - // FIXME: should be set to "leave" initially if the user is peeking - userRoomMembership: "join", - // cache of matrixClient.getSyncState() (but from the 'sync' event) clientSyncState: MatrixClientPeg.get().getSyncState(), @@ -365,59 +360,17 @@ const TimelinePanel = createReactClass({ debuglog("TimelinePanel: Initiating paginate; backwards:"+backwards); this.setState({[paginatingKey]: true}); - return this._timelineWindow.paginate(dir, PAGINATE_SIZE).then((canPaginate) => { + return this._timelineWindow.paginate(dir, PAGINATE_SIZE).then((r) => { if (this.unmounted) { return; } - debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+canPaginate); - - let { events, liveEvents } = this._getEvents(); - - const myUserId = MatrixClientPeg.get().credentials.userId; - let userRoomMembership = this.state.userRoomMembership; - let stopBackPaginating = false; - // FIXME: start at new events - for (let i = events.length - 1; i >= 0; i--) { - const event = events[i]; - debuglog(event); - if (event.getStateKey() === myUserId - && event.getType() === "m.room.member") { - const prevContent = event.getPrevContent(); - debuglog("prevcontent", prevContent); - userRoomMembership = prevContent.membership || "leave"; - } else if (userRoomMembership === "leave" && - (event.isDecryptionFailure() || event.isBeingDecrypted())) { - // reached an undecryptable message when the user wasn't in - // the room -- don't try to load any more - // (for now, we assume that events that are being decrypted will - // fail if they were sent while the use wasn't in the room) - stopBackPaginating = true; - if (backwards) { - canPaginate = false; - } - events = events.slice(i); - events[0]._setClearData({ - clearEvent: { - type: "m.room.message", - content: { - msgtype: "m.bad.encrypted", - body: - "** This room has encrypted messages that were sent before " - + "you joined the room. You will not be able to read " - + "these messages. **", - }, - }, - }) - liveEvents = liveEvents.slice(i); - break; - } - } + debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+r); + const { events, liveEvents } = this._getEvents(); const newState = { [paginatingKey]: false, - [canPaginateKey]: canPaginate, + [canPaginateKey]: r, events, liveEvents, - userRoomMembership, }; // moving the window in this direction may mean that we can now @@ -425,8 +378,7 @@ const TimelinePanel = createReactClass({ const otherDirection = backwards ? EventTimeline.FORWARDS : EventTimeline.BACKWARDS; const canPaginateOtherWayKey = backwards ? 'canForwardPaginate' : 'canBackPaginate'; if (!this.state[canPaginateOtherWayKey] && - this._timelineWindow.canPaginate(otherDirection) && - (backwards || !stopBackPaginating)) { + this._timelineWindow.canPaginate(otherDirection)) { debuglog('TimelinePanel: can now', otherDirection, 'paginate again'); newState[canPaginateOtherWayKey] = true; } @@ -438,7 +390,7 @@ const TimelinePanel = createReactClass({ // itself into the right place return new Promise((resolve) => { this.setState(newState, () => { - resolve(canPaginate); + resolve(r); }); }); }); From fcfee87f3332da273c1421b9663f35e8609838d5 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 21 Jan 2020 23:42:04 -0500 Subject: [PATCH 04/15] lint --- src/components/structures/MessagePanel.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index c94e205262c..639a09b5d90 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -446,7 +446,7 @@ export default class MessagePanel extends React.Component { const ret = []; let firstShownEventIndex = checkForPreJoinUISI( - this.props.events, this.props.room, this.props.ourUserId + this.props.events, this.props.room, this.props.ourUserId, ); if (firstShownEventIndex > 0) { @@ -469,7 +469,7 @@ export default class MessagePanel extends React.Component { event_id: hiddenEvent.getId(), origin_server_ts: hiddenEvent.getTs(), room_id: hiddenEvent.getRoomId(), - sender: "invalid" + sender: "invalid", }); // FIXME: use a special tile for the error message ret.push( From feb36ac62bb73b7c0da76317d0523ef45c46323d Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 22 Jan 2020 00:25:50 -0500 Subject: [PATCH 05/15] unbreak it --- src/components/structures/MessagePanel.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 639a09b5d90..f1b22714939 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -950,9 +950,7 @@ export default class MessagePanel extends React.Component { } } - onFillRequest(backwards) { - console.warn("***", this, this.state, this.props); - // FIXME: why are this.state and this.props undefined??? + onFillRequest = (backwards) => { if (this.state.preventBackPaginating) { return Promise.resolve(false); } From 8e0fe2a687f2836ef2a5452bea1449c02d1d2964 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 22 Jan 2020 15:39:37 -0500 Subject: [PATCH 06/15] use a separate component for warning message --- res/css/_components.scss | 1 + res/css/views/messages/_PreJoinUISI.scss | 35 +++++++++++++++ src/components/structures/MessagePanel.js | 46 +++----------------- src/components/views/messages/PreJoinUISI.js | 30 +++++++++++++ src/i18n/strings/en_EN.json | 3 +- 5 files changed, 75 insertions(+), 40 deletions(-) create mode 100644 res/css/views/messages/_PreJoinUISI.scss create mode 100644 src/components/views/messages/PreJoinUISI.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 7a9ebfdf26b..ddf7ea85450 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -131,6 +131,7 @@ @import "./views/messages/_MessageActionBar.scss"; @import "./views/messages/_MessageTimestamp.scss"; @import "./views/messages/_MjolnirBody.scss"; +@import "./views/messages/_PreJoinUISI.scss"; @import "./views/messages/_ReactionsRow.scss"; @import "./views/messages/_ReactionsRowButton.scss"; @import "./views/messages/_ReactionsRowButtonTooltip.scss"; diff --git a/res/css/views/messages/_PreJoinUISI.scss b/res/css/views/messages/_PreJoinUISI.scss new file mode 100644 index 00000000000..ff8a056564d --- /dev/null +++ b/res/css/views/messages/_PreJoinUISI.scss @@ -0,0 +1,35 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_PreJoinUISI { + background-color: $info-plinth-bg-color; + padding-left: 20px; + padding-right: 20px; + padding-top: 10px; + padding-bottom: 10px; +} + +.mx_PreJoinUISI_image { + float: left; + margin-right: 20px; + width: 34px; + height: 34px; + + background-color: $primary-fg-color; + mask: url('$(res)/img/warning.svg'); + mask-repeat: no-repeat; + mask-position: center; +} diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index f1b22714939..eb00b320479 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -1,7 +1,7 @@ /* Copyright 2016 OpenMarket Ltd Copyright 2018 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019-2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -408,7 +408,8 @@ export default class MessagePanel extends React.Component { const DateSeparator = sdk.getComponent('messages.DateSeparator'); const EventListSummary = sdk.getComponent('views.elements.EventListSummary'); const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary'); - const EventTile = sdk.getComponent('rooms.EventTile'); + + const PreJoinUISI = sdk.getComponent('messages.PreJoinUISI'); this.eventNodes = {}; @@ -457,45 +458,12 @@ export default class MessagePanel extends React.Component { } const hiddenEvent = this.props.events[firstShownEventIndex-1]; const eventId = hiddenEvent.getId(); - const event = new MatrixEvent({ - type: "m.room.message", - content: { - msgtype: "m.text", - body: - "** This room has encrypted messages that were sent before " - + "you joined the room. You will not be able to read " - + "these messages. **", - }, - event_id: hiddenEvent.getId(), - origin_server_ts: hiddenEvent.getTs(), - room_id: hiddenEvent.getRoomId(), - sender: "invalid", - }); - // FIXME: use a special tile for the error message ret.push(
  • - +
  • , ); } else { @@ -504,7 +472,6 @@ export default class MessagePanel extends React.Component { preventBackPaginating: false, }); } - firstShownEventIndex = 0; } let prevEvent = null; // the last event we showed @@ -1011,8 +978,9 @@ export default class MessagePanel extends React.Component { } function checkForPreJoinUISI(events, room, userId) { - if (events.length === 0) { - return -1; + if (events.length === 0 || + !MatrixClientPeg.get().isRoomEncrypted(room.roomId)) { + return 0; } let i; diff --git a/src/components/views/messages/PreJoinUISI.js b/src/components/views/messages/PreJoinUISI.js new file mode 100644 index 00000000000..86fb3d98407 --- /dev/null +++ b/src/components/views/messages/PreJoinUISI.js @@ -0,0 +1,30 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; + +import { _t } from '../../../languageHandler'; + +export default class PreJoinUISI extends React.Component { + static propTypes = {}; + + render = () => { + return
    +
    + {_t("This room has encrypted messages that were sent before you joined the room. You will not be able to read these messages.")} +
    + }; +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 42c87172b82..12582dd8438 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2016,5 +2016,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", + "This room has encrypted messages that were sent before you joined the room. You will not be able to read these messages.": "This room has encrypted messages that were sent before you joined the room. You will not be able to read these messages." } From bfca2411ea688afecc80c92b0ab8a33ad15edc25 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 22 Jan 2020 15:49:17 -0500 Subject: [PATCH 07/15] lint --- src/components/structures/MessagePanel.js | 6 ++---- src/i18n/strings/en_EN.json | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index eb00b320479..e430e609bae 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -29,8 +29,6 @@ import SettingsStore from '../../settings/SettingsStore'; import {_t} from "../../languageHandler"; import {haveTileForEvent} from "../views/rooms/EventTile"; -import {MatrixEvent} from 'matrix-js-sdk'; - const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const continuedTypes = ['m.sticker', 'm.room.message']; @@ -446,7 +444,7 @@ export default class MessagePanel extends React.Component { const ret = []; - let firstShownEventIndex = checkForPreJoinUISI( + const firstShownEventIndex = checkForPreJoinUISI( this.props.events, this.props.room, this.props.ourUserId, ); @@ -463,7 +461,7 @@ export default class MessagePanel extends React.Component { ref={this._collectEventNode.bind(this, eventId)} data-scroll-tokens={eventId} > - + , ); } else { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 12582dd8438..a304d52d11b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1165,6 +1165,7 @@ "%(name)s wants to verify": "%(name)s wants to verify", "You sent a verification request": "You sent a verification request", "Error decrypting video": "Error decrypting video", + "This room has encrypted messages that were sent before you joined the room. You will not be able to read these messages.": "This room has encrypted messages that were sent before you joined the room. You will not be able to read these messages.", "Show all": "Show all", "Reactions": "Reactions", " reacted with %(content)s": " reacted with %(content)s", @@ -2016,6 +2017,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", - "This room has encrypted messages that were sent before you joined the room. You will not be able to read these messages.": "This room has encrypted messages that were sent before you joined the room. You will not be able to read these messages." + "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room" } From e3e89e260277eefc980311bcde5c5d82ea20cb79 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 23 Jan 2020 12:42:17 -0500 Subject: [PATCH 08/15] lint and use correct constant --- src/components/structures/MessagePanel.js | 4 +++- src/components/views/messages/PreJoinUISI.js | 5 +++-- src/i18n/strings/en_EN.json | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index e430e609bae..d842a2da19f 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -20,6 +20,7 @@ import React, {createRef} from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import classNames from 'classnames'; +import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline'; import shouldHideEvent from '../../shouldHideEvent'; import {wantsDateSeparator} from '../../DateUtils'; import * as sdk from '../../index'; @@ -989,7 +990,8 @@ function checkForPreJoinUISI(events, room, userId) { for (i = events.length - 1; i >= 0; i--) { const timeline = room.getTimelineForEvent(events[i].getId()); if (timeline) { - const userMembershipEvent = timeline.getState("f").getMember(userId); // FIXME: EventTimeline.FORWARDS + const userMembershipEvent = + timeline.getState(EventTimeline.FORWARDS).getMember(userId); userMembership = userMembershipEvent ? userMembershipEvent.membership : "leave"; const timelineEvents = timeline.getEvents(); for (let j = timelineEvents.length - 1; j >= 0; j--) { diff --git a/src/components/views/messages/PreJoinUISI.js b/src/components/views/messages/PreJoinUISI.js index 86fb3d98407..c532b279b62 100644 --- a/src/components/views/messages/PreJoinUISI.js +++ b/src/components/views/messages/PreJoinUISI.js @@ -24,7 +24,8 @@ export default class PreJoinUISI extends React.Component { render = () => { return
    - {_t("This room has encrypted messages that were sent before you joined the room. You will not be able to read these messages.")} -
    + {_t("This room has encrypted messages that were sent before you joined the room. " + + "You will not be able to read these messages.")} +
    ; }; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a304d52d11b..ac5486245dc 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1165,7 +1165,7 @@ "%(name)s wants to verify": "%(name)s wants to verify", "You sent a verification request": "You sent a verification request", "Error decrypting video": "Error decrypting video", - "This room has encrypted messages that were sent before you joined the room. You will not be able to read these messages.": "This room has encrypted messages that were sent before you joined the room. You will not be able to read these messages.", + "This room has encrypted messages that were sent before you joined the room. You will not be able to read these messages.": "This room has encrypted messages that were sent before you joined the room. You will not be able to read these messages.", "Show all": "Show all", "Reactions": "Reactions", " reacted with %(content)s": " reacted with %(content)s", From 1bebba99a875659e268794540462ddd7cc6c844c Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 23 Jan 2020 13:38:26 -0500 Subject: [PATCH 09/15] just hide pre-join UTDs, as per feedback from Nad, and other fixes --- res/css/_components.scss | 1 - res/css/views/messages/_PreJoinUISI.scss | 35 -------------------- src/components/structures/MessagePanel.js | 29 ++++++++-------- src/components/views/messages/PreJoinUISI.js | 31 ----------------- 4 files changed, 13 insertions(+), 83 deletions(-) delete mode 100644 res/css/views/messages/_PreJoinUISI.scss delete mode 100644 src/components/views/messages/PreJoinUISI.js diff --git a/res/css/_components.scss b/res/css/_components.scss index ddf7ea85450..7a9ebfdf26b 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -131,7 +131,6 @@ @import "./views/messages/_MessageActionBar.scss"; @import "./views/messages/_MessageTimestamp.scss"; @import "./views/messages/_MjolnirBody.scss"; -@import "./views/messages/_PreJoinUISI.scss"; @import "./views/messages/_ReactionsRow.scss"; @import "./views/messages/_ReactionsRowButton.scss"; @import "./views/messages/_ReactionsRowButtonTooltip.scss"; diff --git a/res/css/views/messages/_PreJoinUISI.scss b/res/css/views/messages/_PreJoinUISI.scss deleted file mode 100644 index ff8a056564d..00000000000 --- a/res/css/views/messages/_PreJoinUISI.scss +++ /dev/null @@ -1,35 +0,0 @@ -/* -Copyright 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_PreJoinUISI { - background-color: $info-plinth-bg-color; - padding-left: 20px; - padding-right: 20px; - padding-top: 10px; - padding-bottom: 10px; -} - -.mx_PreJoinUISI_image { - float: left; - margin-right: 20px; - width: 34px; - height: 34px; - - background-color: $primary-fg-color; - mask: url('$(res)/img/warning.svg'); - mask-repeat: no-repeat; - mask-position: center; -} diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index d842a2da19f..5b0d51d2dec 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -408,8 +408,6 @@ export default class MessagePanel extends React.Component { const EventListSummary = sdk.getComponent('views.elements.EventListSummary'); const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary'); - const PreJoinUISI = sdk.getComponent('messages.PreJoinUISI'); - this.eventNodes = {}; let i; @@ -455,16 +453,6 @@ export default class MessagePanel extends React.Component { preventBackPaginating: true, }); } - const hiddenEvent = this.props.events[firstShownEventIndex-1]; - const eventId = hiddenEvent.getId(); - ret.push( -
  • - -
  • , - ); } else { if (this.state.preventBackPaginating) { this.setState({ @@ -976,6 +964,18 @@ export default class MessagePanel extends React.Component { } } +/** + * Check for undecryptable messages that were sent while the user was not in + * the room. + * + * @param {Array} events The timeline events to check + * @param {Room} room The room that the events are in + * @param {string} userId The user's ID + * + * @return Number The index within `events` of the event after the most recent + * undecryptable event that was sent while the user was not in the room. If no + * such events were found, then it returns 0. + */ function checkForPreJoinUISI(events, room, userId) { if (events.length === 0 || !MatrixClientPeg.get().isRoomEncrypted(room.roomId)) { @@ -1014,12 +1014,9 @@ function checkForPreJoinUISI(events, room, userId) { && event.getType() === "m.room.member") { const prevContent = event.getPrevContent(); userMembership = prevContent.membership || "leave"; - } else if (userMembership === "leave" && - (event.isDecryptionFailure() || event.isBeingDecrypted())) { + } else if (userMembership === "leave" && event.isDecryptionFailure()) { // reached an undecryptable message when the user wasn't in // the room -- don't try to load any more - // (for now, we assume that events that are being decrypted will - // fail if they were sent while the use wasn't in the room) return i + 1; } } diff --git a/src/components/views/messages/PreJoinUISI.js b/src/components/views/messages/PreJoinUISI.js deleted file mode 100644 index c532b279b62..00000000000 --- a/src/components/views/messages/PreJoinUISI.js +++ /dev/null @@ -1,31 +0,0 @@ -/* -Copyright 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from 'react'; - -import { _t } from '../../../languageHandler'; - -export default class PreJoinUISI extends React.Component { - static propTypes = {}; - - render = () => { - return
    -
    - {_t("This room has encrypted messages that were sent before you joined the room. " + - "You will not be able to read these messages.")} -
    ; - }; -} From f1a0430ef72f648a14b603218143efed6b19dbe3 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 23 Jan 2020 13:48:31 -0500 Subject: [PATCH 10/15] fix jsdoc and i18n --- src/components/structures/MessagePanel.js | 2 +- src/i18n/strings/en_EN.json | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 5b0d51d2dec..0f7541a7415 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -972,7 +972,7 @@ export default class MessagePanel extends React.Component { * @param {Room} room The room that the events are in * @param {string} userId The user's ID * - * @return Number The index within `events` of the event after the most recent + * @return {Number} The index within `events` of the event after the most recent * undecryptable event that was sent while the user was not in the room. If no * such events were found, then it returns 0. */ diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ac5486245dc..42c87172b82 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1165,7 +1165,6 @@ "%(name)s wants to verify": "%(name)s wants to verify", "You sent a verification request": "You sent a verification request", "Error decrypting video": "Error decrypting video", - "This room has encrypted messages that were sent before you joined the room. You will not be able to read these messages.": "This room has encrypted messages that were sent before you joined the room. You will not be able to read these messages.", "Show all": "Show all", "Reactions": "Reactions", " reacted with %(content)s": " reacted with %(content)s", From 63a56f96897d5f40f8fd202a9d44591104777368 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 24 Jan 2020 11:54:21 -0500 Subject: [PATCH 11/15] use the right index for the loop --- src/components/structures/MessagePanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 0f7541a7415..56c079aea95 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -995,7 +995,7 @@ function checkForPreJoinUISI(events, room, userId) { userMembership = userMembershipEvent ? userMembershipEvent.membership : "leave"; const timelineEvents = timeline.getEvents(); for (let j = timelineEvents.length - 1; j >= 0; j--) { - const event = timelineEvents[i]; + const event = timelineEvents[j]; if (event.getStateKey() === userId && event.getType() === "m.room.member") { const prevContent = event.getPrevContent(); From a41bb69b41cc3abe42b6391685dddbbd4b3977c3 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 27 Jan 2020 16:12:28 -0500 Subject: [PATCH 12/15] move logic (back) to TimelinePanel --- src/components/structures/MessagePanel.js | 30 +------- src/components/structures/TimelinePanel.js | 87 +++++++++++++++++++++- 2 files changed, 86 insertions(+), 31 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 56c079aea95..afc788f73a3 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -116,7 +116,6 @@ export default class MessagePanel extends React.Component { // previous positions the read marker has been in, so we can // display 'ghost' read markers that are animating away ghostReadMarkers: [], - preventBackPaginating: false, }; // opaque readreceipt info for each userId; used by ReadReceiptMarker @@ -443,24 +442,6 @@ export default class MessagePanel extends React.Component { const ret = []; - const firstShownEventIndex = checkForPreJoinUISI( - this.props.events, this.props.room, this.props.ourUserId, - ); - - if (firstShownEventIndex > 0) { - if (!this.state.preventBackPaginating) { - this.setState({ - preventBackPaginating: true, - }); - } - } else { - if (this.state.preventBackPaginating) { - this.setState({ - preventBackPaginating: false, - }); - } - } - let prevEvent = null; // the last event we showed this._readReceiptsByEvent = {}; @@ -468,7 +449,7 @@ export default class MessagePanel extends React.Component { this._readReceiptsByEvent = this._getReadReceiptsByShownEvent(); } - for (i = firstShownEventIndex; i < this.props.events.length; i++) { + for (i = 0; i < this.props.events.length; i++) { const mxEv = this.props.events[i]; const eventId = mxEv.getId(); const last = (mxEv === lastShownEvent); @@ -904,13 +885,6 @@ export default class MessagePanel extends React.Component { } } - onFillRequest = (backwards) => { - if (this.state.preventBackPaginating) { - return Promise.resolve(false); - } - return this.props.onFillRequest(backwards); - } - render() { const ScrollPanel = sdk.getComponent("structures.ScrollPanel"); const WhoIsTypingTile = sdk.getComponent("rooms.WhoIsTypingTile"); @@ -949,7 +923,7 @@ export default class MessagePanel extends React.Component { className={className} onScroll={this.props.onScroll} onResize={this.onResize} - onFillRequest={this.onFillRequest} + onFillRequest={this.props.onFillRequest} onUnfillRequest={this.props.onUnfillRequest} style={style} stickyBottom={this.props.stickyBottom} diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 30b02bfcca4..cbfa8ae14eb 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -142,6 +142,9 @@ const TimelinePanel = createReactClass({ liveEvents: [], timelineLoading: true, // track whether our room timeline is loading + // the index of the first event that is to be shown + firstVisibleEventIndex: 0, + // canBackPaginate == false may mean: // // * we haven't (successfully) loaded the timeline yet, or: @@ -330,10 +333,12 @@ const TimelinePanel = createReactClass({ // We can now paginate in the unpaginated direction const canPaginateKey = (backwards) ? 'canBackPaginate' : 'canForwardPaginate'; const { events, liveEvents } = this._getEvents(); + const firstVisibleEventIndex = this._checkForPreJoinUISI(events); this.setState({ [canPaginateKey]: true, events, liveEvents, + firstVisibleEventIndex, }); } }, @@ -366,11 +371,13 @@ const TimelinePanel = createReactClass({ debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+r); const { events, liveEvents } = this._getEvents(); + const firstVisibleEventIndex = this._checkForPreJoinUISI(events); const newState = { [paginatingKey]: false, [canPaginateKey]: r, events, liveEvents, + firstVisibleEventIndex, }; // moving the window in this direction may mean that we can now @@ -390,7 +397,7 @@ const TimelinePanel = createReactClass({ // itself into the right place return new Promise((resolve) => { this.setState(newState, () => { - resolve(r); + resolve(r && (!backwards || firstVisibleEventIndex === 0)); }); }); }); @@ -466,10 +473,12 @@ const TimelinePanel = createReactClass({ const { events, liveEvents } = this._getEvents(); const lastLiveEvent = liveEvents[liveEvents.length - 1]; + const firstVisibleEventIndex = this._checkForPreJoinUISI(events); const updatedState = { events, liveEvents, + firstVisibleEventIndex, }; let callRMUpdated; @@ -1097,7 +1106,9 @@ const TimelinePanel = createReactClass({ // the results if so. if (this.unmounted) return; - this.setState(this._getEvents()); + const newState = this._getEvents(); + newState.firstVisibleEventIndex = this._checkForPreJoinUISI(newState.events); + this.setState(newState); }, // get the list of events from the timeline window and the pending event list @@ -1119,6 +1130,73 @@ const TimelinePanel = createReactClass({ }; }, + /** + * Check for undecryptable messages that were sent while the user was not in + * the room. + * + * @param {Array} events The timeline events to check + * @param {Room} room The room that the events are in + * + * @return {Number} The index within `events` of the event after the most recent + * undecryptable event that was sent while the user was not in the room. If no + * such events were found, then it returns 0. + */ + _checkForPreJoinUISI: function(events) { + const room = this.props.timelineSet.room; + + if (events.length === 0 || !room || + !MatrixClientPeg.get().isRoomEncrypted(room.roomId)) { + return 0; + } + + const userId = MatrixClientPeg.get().credentials.userId; + + let i; + let userMembership = "leave"; + // find the last event that is in a timeline (events may not be in a + // timeline if they have not been sent yet) and get the user's membership + // at that point. + for (i = events.length - 1; i >= 0; i--) { + const timeline = room.getTimelineForEvent(events[i].getId()); + if (timeline) { + const userMembershipEvent = + timeline.getState(EventTimeline.FORWARDS).getMember(userId); + userMembership = userMembershipEvent ? userMembershipEvent.membership : "leave"; + const timelineEvents = timeline.getEvents(); + for (let j = timelineEvents.length - 1; j >= 0; j--) { + const event = timelineEvents[j]; + if (event.getId() === events[i].getId()) { + break; + } else if (event.getStateKey() === userId + && event.getType() === "m.room.member") { + const prevContent = event.getPrevContent(); + userMembership = prevContent.membership || "leave"; + } + } + break; + } + } + + // now go through the rest of the events and find the first undecryptable + // one that was sent when the user wasn't in the room + for (; i >= 0; i--) { + const event = events[i]; + if (event.getStateKey() === userId + && event.getType() === "m.room.member") { + const prevContent = event.getPrevContent(); + userMembership = prevContent.membership || "leave"; + } else if (userMembership === "leave" && + (event.isDecryptionFailure() || event.isBeingDecrypted())) { + // reached an undecryptable message when the user wasn't in + // the room -- don't try to load any more + // Note: for now, we assume that events that are being decrypted are + // not decryptable + return i + 1; + } + } + return 0; + }, + _indexForEventId: function(evId) { for (let i = 0; i < this.state.events.length; ++i) { if (evId == this.state.events[i].getId()) { @@ -1309,6 +1387,9 @@ const TimelinePanel = createReactClass({ this.state.forwardPaginating || ['PREPARED', 'CATCHUP'].includes(this.state.clientSyncState) ); + const events = this.state.firstVisibleEventIndex + ? this.state.events.slice(this.state.firstVisibleEventIndex) + : this.state.events; return (