From 65f957f023e9de9499f69f8f000917ede41118fb Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 6 Dec 2021 11:36:45 +0530 Subject: [PATCH 01/77] WIP --- src/matrix/net/HomeServerApi.js | 273 +++++++++++++++++++++++++++ src/matrix/room/timeline/Timeline.js | 4 +- 2 files changed, 275 insertions(+), 2 deletions(-) create mode 100644 src/matrix/net/HomeServerApi.js diff --git a/src/matrix/net/HomeServerApi.js b/src/matrix/net/HomeServerApi.js new file mode 100644 index 00000000..a5756e33 --- /dev/null +++ b/src/matrix/net/HomeServerApi.js @@ -0,0 +1,273 @@ +/* +Copyright 2020 Bruno Windels +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 {encodeQueryParams, encodeBody} from "./common.js"; +import {HomeServerRequest} from "./HomeServerRequest.js"; + +const CS_R0_PREFIX = "/_matrix/client/r0"; +const DEHYDRATION_PREFIX = "/_matrix/client/unstable/org.matrix.msc2697.v2"; + +export class HomeServerApi { + constructor({homeserver, accessToken, request, reconnector}) { + // store these both in a closure somehow so it's harder to get at in case of XSS? + // one could change the homeserver as well so the token gets sent there, so both must be protected from read/write + this._homeserver = homeserver; + this._accessToken = accessToken; + this._requestFn = request; + this._reconnector = reconnector; + } + + _url(csPath, prefix = CS_R0_PREFIX) { + return this._homeserver + prefix + csPath; + } + + _baseRequest(method, url, queryParams, body, options, accessToken) { + const queryString = encodeQueryParams(queryParams); + url = `${url}?${queryString}`; + let log; + if (options?.log) { + const parent = options?.log; + log = parent.child({ + t: "network", + url, + method, + }, parent.level.Info); + } + let encodedBody; + const headers = new Map(); + if (accessToken) { + headers.set("Authorization", `Bearer ${accessToken}`); + } + headers.set("Accept", "application/json"); + if (body) { + const encoded = encodeBody(body); + headers.set("Content-Type", encoded.mimeType); + headers.set("Content-Length", encoded.length); + encodedBody = encoded.body; + } + + const requestResult = this._requestFn(url, { + method, + headers, + body: encodedBody, + timeout: options?.timeout, + uploadProgress: options?.uploadProgress, + format: "json" // response format + }); + + const hsRequest = new HomeServerRequest(method, url, requestResult, log); + + if (this._reconnector) { + hsRequest.response().catch(err => { + // Some endpoints such as /sync legitimately time-out + // (which is also reported as a ConnectionError) and will re-attempt, + // but spinning up the reconnector in this case is ok, + // as all code ran on session and sync start should be reentrant + if (err.name === "ConnectionError") { + this._reconnector.onRequestFailed(this); + } + }); + } + + return hsRequest; + } + + _unauthedRequest(method, url, queryParams, body, options) { + return this._baseRequest(method, url, queryParams, body, options, null); + } + + _authedRequest(method, url, queryParams, body, options) { + return this._baseRequest(method, url, queryParams, body, options, this._accessToken); + } + + _post(csPath, queryParams, body, options) { + return this._authedRequest("POST", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options); + } + + _put(csPath, queryParams, body, options) { + return this._authedRequest("PUT", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options); + } + + _get(csPath, queryParams, body, options) { + return this._authedRequest("GET", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options); + } + + sync(since, filter, timeout, options = null) { + return this._get("/sync", {since, timeout, filter}, null, options); + } + + event(roomId, eventId) { + return this._get(`/rooms/${encodeURIComponent(roomId)}/event/${encodeURIComponent(eventId)}`); + } + + // params is from, dir and optionally to, limit, filter. + messages(roomId, params, options = null) { + return this._get(`/rooms/${encodeURIComponent(roomId)}/messages`, params, null, options); + } + + // params is at, membership and not_membership + members(roomId, params, options = null) { + return this._get(`/rooms/${encodeURIComponent(roomId)}/members`, params, null, options); + } + + send(roomId, eventType, txnId, content, options = null) { + return this._put(`/rooms/${encodeURIComponent(roomId)}/send/${encodeURIComponent(eventType)}/${encodeURIComponent(txnId)}`, {}, content, options); + } + + redact(roomId, eventId, txnId, content, options = null) { + return this._put(`/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${encodeURIComponent(txnId)}`, {}, content, options); + } + + receipt(roomId, receiptType, eventId, options = null) { + return this._post(`/rooms/${encodeURIComponent(roomId)}/receipt/${encodeURIComponent(receiptType)}/${encodeURIComponent(eventId)}`, + {}, {}, options); + } + + state(roomId, eventType, stateKey, options = null) { + return this._get(`/rooms/${encodeURIComponent(roomId)}/state/${encodeURIComponent(eventType)}/${encodeURIComponent(stateKey)}`, {}, null, options); + } + + getLoginFlows() { + return this._unauthedRequest("GET", this._url("/login"), null, null, null); + } + + passwordLogin(username, password, initialDeviceDisplayName, options = null) { + return this._unauthedRequest("POST", this._url("/login"), null, { + "type": "m.login.password", + "identifier": { + "type": "m.id.user", + "user": username + }, + "password": password, + "initial_device_display_name": initialDeviceDisplayName + }, options); + } + + tokenLogin(loginToken, txnId, initialDeviceDisplayName, options = null) { + return this._unauthedRequest("POST", this._url("/login"), null, { + "type": "m.login.token", + "identifier": { + "type": "m.id.user", + }, + "token": loginToken, + "txn_id": txnId, + "initial_device_display_name": initialDeviceDisplayName + }, options); + } + + createFilter(userId, filter, options = null) { + return this._post(`/user/${encodeURIComponent(userId)}/filter`, null, filter, options); + } + + versions(options = null) { + return this._unauthedRequest("GET", `${this._homeserver}/_matrix/client/versions`, null, null, options); + } + + uploadKeys(dehydratedDeviceId, payload, options = null) { + let path = "/keys/upload"; + if (dehydratedDeviceId) { + path = path + `/${encodeURIComponent(dehydratedDeviceId)}`; + } + return this._post(path, null, payload, options); + } + + queryKeys(queryRequest, options = null) { + return this._post("/keys/query", null, queryRequest, options); + } + + claimKeys(payload, options = null) { + return this._post("/keys/claim", null, payload, options); + } + + sendToDevice(type, payload, txnId, options = null) { + return this._put(`/sendToDevice/${encodeURIComponent(type)}/${encodeURIComponent(txnId)}`, null, payload, options); + } + + roomKeysVersion(version = null, options = null) { + let versionPart = ""; + if (version) { + versionPart = `/${encodeURIComponent(version)}`; + } + return this._get(`/room_keys/version${versionPart}`, null, null, options); + } + + roomKeyForRoomAndSession(version, roomId, sessionId, options = null) { + return this._get(`/room_keys/keys/${encodeURIComponent(roomId)}/${encodeURIComponent(sessionId)}`, {version}, null, options); + } + + uploadAttachment(blob, filename, options = null) { + return this._authedRequest("POST", `${this._homeserver}/_matrix/media/r0/upload`, {filename}, blob, options); + } + + setPusher(pusher, options = null) { + return this._post("/pushers/set", null, pusher, options); + } + + getPushers(options = null) { + return this._get("/pushers", null, null, options); + } + + join(roomId, options = null) { + return this._post(`/rooms/${encodeURIComponent(roomId)}/join`, null, null, options); + } + + joinIdOrAlias(roomIdOrAlias, options = null) { + return this._post(`/join/${encodeURIComponent(roomIdOrAlias)}`, null, null, options); + } + + leave(roomId, options = null) { + return this._post(`/rooms/${encodeURIComponent(roomId)}/leave`, null, null, options); + } + + forget(roomId, options = null) { + return this._post(`/rooms/${encodeURIComponent(roomId)}/forget`, null, null, options); + } + + logout(options = null) { + return this._post(`/logout`, null, null, options); + } + + getDehydratedDevice(options = {}) { + options.prefix = DEHYDRATION_PREFIX; + return this._get(`/dehydrated_device`, null, null, options); + } + + createDehydratedDevice(payload, options = {}) { + options.prefix = DEHYDRATION_PREFIX; + return this._put(`/dehydrated_device`, null, payload, options); + } + + claimDehydratedDevice(deviceId, options = {}) { + options.prefix = DEHYDRATION_PREFIX; + return this._post(`/dehydrated_device/claim`, null, {device_id: deviceId}, options); + } +} + +import {Request as MockRequest} from "../../mocks/Request.js"; + +export function tests() { + return { + "superficial happy path for GET": async assert => { + const hsApi = new HomeServerApi({ + request: () => new MockRequest().respond(200, 42), + homeserver: "https://hs.tld" + }); + const result = await hsApi._get("foo", null, null, null).response(); + assert.strictEqual(result, 42); + } + } +} diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index e77e7d8e..e018fa24 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -224,7 +224,7 @@ export class Timeline { } /** @package */ - replaceEntries(entries) { + replaceEntries(entries) { this._addLocalRelationsToNewRemoteEntries(entries); for (const entry of entries) { try { @@ -366,7 +366,7 @@ export class Timeline { } return eventEntry; } - + // tries to prepend `amount` entries to the `entries` list. /** * [loadAtTop description] From 35a13842af6502c1b7c17648270da6e8ce0ed287 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 7 Dec 2021 12:18:51 +0530 Subject: [PATCH 02/77] Implement context endpoint --- src/matrix/net/HomeServerApi.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix/net/HomeServerApi.js b/src/matrix/net/HomeServerApi.js index a5756e33..4d729cbf 100644 --- a/src/matrix/net/HomeServerApi.js +++ b/src/matrix/net/HomeServerApi.js @@ -110,8 +110,8 @@ export class HomeServerApi { return this._get("/sync", {since, timeout, filter}, null, options); } - event(roomId, eventId) { - return this._get(`/rooms/${encodeURIComponent(roomId)}/event/${encodeURIComponent(eventId)}`); + context(roomId, eventId, limit, filter) { + return this._get(`/rooms/${encodeURIComponent(roomId)}/context/${encodeURIComponent(eventId)}`, {filter, limit}); } // params is from, dir and optionally to, limit, filter. From f6cf3b378be35e1e1ad38ff30a860ab751fb26a6 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 7 Dec 2021 11:33:09 +0530 Subject: [PATCH 03/77] Strip reply fallback --- src/domain/session/room/timeline/deserialize.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/domain/session/room/timeline/deserialize.js b/src/domain/session/room/timeline/deserialize.js index 7da6255d..b66804a5 100644 --- a/src/domain/session/room/timeline/deserialize.js +++ b/src/domain/session/room/timeline/deserialize.js @@ -346,6 +346,10 @@ class Deserializer { } export function parseHTMLBody(platform, mediaRepository, allowReplies, html) { + if (allowReplies) { + // todo: might be better to remove mx-reply and children after parsing, need to think + html = html.replace(/.+<\/mx-reply>/, ""); + } const parseResult = platform.parseHTML(html); const deserializer = new Deserializer(parseResult, mediaRepository, allowReplies); const parts = deserializer.parseAnyNodes(parseResult.rootNodes); From e88ee319919bb900dda6a516065e4c6ff6214384 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 7 Dec 2021 11:33:37 +0530 Subject: [PATCH 04/77] Add getter for reply body --- .../room/timeline/tiles/BaseTextTile.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/domain/session/room/timeline/tiles/BaseTextTile.js b/src/domain/session/room/timeline/tiles/BaseTextTile.js index 60024ca6..aadcf6f5 100644 --- a/src/domain/session/room/timeline/tiles/BaseTextTile.js +++ b/src/domain/session/room/timeline/tiles/BaseTextTile.js @@ -17,6 +17,7 @@ limitations under the License. import {BaseMessageTile} from "./BaseMessageTile.js"; import {stringAsBody} from "../MessageBody.js"; import {createEnum} from "../../../../../utils/enum"; +import {avatarInitials, getIdentifierColorNumber} from "../../../../avatar.js"; export const BodyFormat = createEnum("Plain", "Html"); @@ -54,6 +55,24 @@ export class BaseTextTile extends BaseMessageTile { this._messageBody = this._parseBody(body, format); this._format = format; } + // console.log("messageBody", this._messageBody); return this._messageBody; } + + get replyPreviewBody() { + const entry = this._entry.relatedEntry; + if (!entry) { + return {}; + } + const format = entry.content.format === "org.matrix.custom.html" ? BodyFormat.Html : BodyFormat.Plain; + const body = entry.content["formatted_body"] ?? entry.content["body"]; + return { + body: this._parseBody(body, format), + sender: entry.displayName, + avatar: { + colorNumber: getIdentifierColorNumber(entry.sender), + initial: avatarInitials(entry.displayName) + } + }; + } } From 31573b35995efab127ad1fa6c7d98f72ddfd8c27 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 7 Dec 2021 11:33:55 +0530 Subject: [PATCH 05/77] Render reply --- .../web/ui/session/room/timeline/TextMessageView.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/platform/web/ui/session/room/timeline/TextMessageView.js b/src/platform/web/ui/session/room/timeline/TextMessageView.js index c1674501..2f8d10a7 100644 --- a/src/platform/web/ui/session/room/timeline/TextMessageView.js +++ b/src/platform/web/ui/session/room/timeline/TextMessageView.js @@ -35,6 +35,18 @@ export class TextMessageView extends BaseMessageView { } container.appendChild(time); }); + t.mapSideEffect(vm => vm.replyPreviewBody, ({ body, sender, avatar }) => { + if (!body) { + return; + } + const replyContainer = t.blockquote([ + t.a({ className: "link", href: "#" }, "In reply to"), + t.a({ className: "pill", href: "#" }, [tag.div({class: `avatar size-12 usercolor${avatar.colorNumber}`}, text(avatar.initial)), sender]), t.br()]); + for (const part of body.parts) { + replyContainer.appendChild(renderPart(part)); + } + container.insertBefore(replyContainer, container.firstChild); + }); return container; } } From 540aa6c54611239eded52aa5db307b1151b1a1e3 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 7 Dec 2021 16:09:46 +0530 Subject: [PATCH 06/77] Use contextEntry and pass avatarUrl --- src/domain/session/room/timeline/tiles/BaseTextTile.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/domain/session/room/timeline/tiles/BaseTextTile.js b/src/domain/session/room/timeline/tiles/BaseTextTile.js index aadcf6f5..0da6cd40 100644 --- a/src/domain/session/room/timeline/tiles/BaseTextTile.js +++ b/src/domain/session/room/timeline/tiles/BaseTextTile.js @@ -60,7 +60,7 @@ export class BaseTextTile extends BaseMessageTile { } get replyPreviewBody() { - const entry = this._entry.relatedEntry; + const entry = this._entry.contextEntry; if (!entry) { return {}; } @@ -71,7 +71,8 @@ export class BaseTextTile extends BaseMessageTile { sender: entry.displayName, avatar: { colorNumber: getIdentifierColorNumber(entry.sender), - initial: avatarInitials(entry.displayName) + initial: avatarInitials(entry.displayName), + avatarUrl: entry.avatarUrl } }; } From d6233e7c77eb9b47b22e5cf0dd312d7baffa7c2d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 7 Dec 2021 16:28:53 +0530 Subject: [PATCH 07/77] Render static avatar --- src/domain/session/room/timeline/tiles/BaseTextTile.js | 8 ++++---- .../web/ui/session/room/timeline/TextMessageView.js | 7 ++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/domain/session/room/timeline/tiles/BaseTextTile.js b/src/domain/session/room/timeline/tiles/BaseTextTile.js index 0da6cd40..7c0aa7b0 100644 --- a/src/domain/session/room/timeline/tiles/BaseTextTile.js +++ b/src/domain/session/room/timeline/tiles/BaseTextTile.js @@ -17,7 +17,7 @@ limitations under the License. import {BaseMessageTile} from "./BaseMessageTile.js"; import {stringAsBody} from "../MessageBody.js"; import {createEnum} from "../../../../../utils/enum"; -import {avatarInitials, getIdentifierColorNumber} from "../../../../avatar.js"; +import {avatarInitials, getAvatarHttpUrl, getIdentifierColorNumber} from "../../../../avatar.js"; export const BodyFormat = createEnum("Plain", "Html"); @@ -70,9 +70,9 @@ export class BaseTextTile extends BaseMessageTile { body: this._parseBody(body, format), sender: entry.displayName, avatar: { - colorNumber: getIdentifierColorNumber(entry.sender), - initial: avatarInitials(entry.displayName), - avatarUrl: entry.avatarUrl + avatarColorNumber: getIdentifierColorNumber(entry.sender), + avatarLetter: avatarInitials(entry.displayName), + avatarUrl: (size) => getAvatarHttpUrl(entry.avatarUrl, size, this.platform, this._mediaRepository) } }; } diff --git a/src/platform/web/ui/session/room/timeline/TextMessageView.js b/src/platform/web/ui/session/room/timeline/TextMessageView.js index 2f8d10a7..a70aaba2 100644 --- a/src/platform/web/ui/session/room/timeline/TextMessageView.js +++ b/src/platform/web/ui/session/room/timeline/TextMessageView.js @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { renderStaticAvatar } from "../../../avatar"; import {tag, text} from "../../../general/html"; import {BaseMessageView} from "./BaseMessageView.js"; @@ -39,9 +40,9 @@ export class TextMessageView extends BaseMessageView { if (!body) { return; } - const replyContainer = t.blockquote([ - t.a({ className: "link", href: "#" }, "In reply to"), - t.a({ className: "pill", href: "#" }, [tag.div({class: `avatar size-12 usercolor${avatar.colorNumber}`}, text(avatar.initial)), sender]), t.br()]); + const replyContainer = tag.blockquote([ + tag.a({ className: "link", href: "#" }, "In reply to"), + tag.a({ className: "pill", href: "#" }, [renderStaticAvatar(avatar, 12), sender]), tag.br()]); for (const part of body.parts) { replyContainer.appendChild(renderPart(part)); } From 61f4d0719f2ce57f0f7b0af6ee4fcecbae4c4f18 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 9 Dec 2021 22:52:42 +0530 Subject: [PATCH 08/77] Refactor code --- .../room/timeline/tiles/BaseTextTile.js | 32 ++++++++-- .../session/room/timeline/ReplyPreviewView.js | 61 +++++++++++++++++++ .../session/room/timeline/TextMessageView.js | 25 +++----- 3 files changed, 95 insertions(+), 23 deletions(-) create mode 100644 src/platform/web/ui/session/room/timeline/ReplyPreviewView.js diff --git a/src/domain/session/room/timeline/tiles/BaseTextTile.js b/src/domain/session/room/timeline/tiles/BaseTextTile.js index 7c0aa7b0..042747aa 100644 --- a/src/domain/session/room/timeline/tiles/BaseTextTile.js +++ b/src/domain/session/room/timeline/tiles/BaseTextTile.js @@ -55,25 +55,45 @@ export class BaseTextTile extends BaseMessageTile { this._messageBody = this._parseBody(body, format); this._format = format; } - // console.log("messageBody", this._messageBody); return this._messageBody; } get replyPreviewBody() { + if (!this._entry.contextEventId) { + return null; + } const entry = this._entry.contextEntry; - if (!entry) { - return {}; + const error = this._generateError(entry); + if (error?.name === "ContextEntryNotFound") { + return { error }; } const format = entry.content.format === "org.matrix.custom.html" ? BodyFormat.Html : BodyFormat.Plain; const body = entry.content["formatted_body"] ?? entry.content["body"]; return { - body: this._parseBody(body, format), - sender: entry.displayName, + body: body && this._parseBody(body, format), + senderName: entry.displayName, avatar: { avatarColorNumber: getIdentifierColorNumber(entry.sender), avatarLetter: avatarInitials(entry.displayName), avatarUrl: (size) => getAvatarHttpUrl(entry.avatarUrl, size, this.platform, this._mediaRepository) - } + }, + error }; } + + _generateError(entry) { + const createError = (name) => { const e = new Error(); e.name = name; return e;} + if (!entry) { + return createError("ContextEntryNotFound"); + } + else if (entry.decryptionError) { + return entry.decryptionError; + } + else if (entry.isRedacted) { + return createError("MessageRedacted"); + } + else if (!(entry.content["formatted_body"] || entry.content["body"])) { + return createError("MissingBody"); + } + } } diff --git a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js new file mode 100644 index 00000000..dbb07579 --- /dev/null +++ b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js @@ -0,0 +1,61 @@ +/* +Copyright 2021 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 {renderStaticAvatar} from "../../../avatar"; +import {tag} from "../../../general/html"; +import {TemplateView} from "../../../general/TemplateView"; +import {renderPart} from "./TextMessageView.js"; + +export class ReplyPreviewView extends TemplateView { + render(t, vm) { + const replyContainer = vm.error? this._renderError(vm) : this._renderReplyPreview(vm); + return replyContainer; + } + + _renderError({ error, avatar, senderName }) { + const errorMessage = this._getErrorMessage(error); + const reply = avatar && senderName? this._renderReplyHeader(avatar, senderName) : tag.blockquote(); + reply.append(tag.span({ className: "statusMessage" }, errorMessage), tag.br()); + return reply; + } + + _getErrorMessage(error) { + switch (error.name) { + case "ContextEntryNotFound": + case "MissingBody": + return "This message could not be fetched."; + case "MessageRedacted": + return "This message has been deleted."; + default: + return error.message; + } + } + + _renderReplyPreview({ body, avatar, senderName }) { + const reply = this._renderReplyHeader(avatar, senderName); + for (const part of body.parts) { + reply.appendChild(renderPart(part)); + } + return reply; + } + + _renderReplyHeader(avatar, displayName) { + return tag.blockquote([ + tag.a({ className: "link", href: "#" }, "In reply to"), + tag.a({ className: "pill", href: "#" }, [renderStaticAvatar(avatar, 12), displayName]), tag.br() + ]); + } +} diff --git a/src/platform/web/ui/session/room/timeline/TextMessageView.js b/src/platform/web/ui/session/room/timeline/TextMessageView.js index a70aaba2..b0733482 100644 --- a/src/platform/web/ui/session/room/timeline/TextMessageView.js +++ b/src/platform/web/ui/session/room/timeline/TextMessageView.js @@ -14,9 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { renderStaticAvatar } from "../../../avatar"; import {tag, text} from "../../../general/html"; import {BaseMessageView} from "./BaseMessageView.js"; +import {ReplyPreviewView} from "./ReplyPreviewView.js"; export class TextMessageView extends BaseMessageView { renderMessageBody(t, vm) { @@ -25,10 +25,11 @@ export class TextMessageView extends BaseMessageView { className: { "Timeline_messageBody": true, statusMessage: vm => vm.shape === "message-status", - }, - }); + } + }, t.mapView(vm => vm.replyPreviewBody, reply => reply ? new ReplyPreviewView(reply): null)); + t.mapSideEffect(vm => vm.body, body => { - while (container.lastChild) { + while (container.lastChild && container.lastChild.tagName !== "BLOCKQUOTE") { container.removeChild(container.lastChild); } for (const part of body.parts) { @@ -36,20 +37,10 @@ export class TextMessageView extends BaseMessageView { } container.appendChild(time); }); - t.mapSideEffect(vm => vm.replyPreviewBody, ({ body, sender, avatar }) => { - if (!body) { - return; - } - const replyContainer = tag.blockquote([ - tag.a({ className: "link", href: "#" }, "In reply to"), - tag.a({ className: "pill", href: "#" }, [renderStaticAvatar(avatar, 12), sender]), tag.br()]); - for (const part of body.parts) { - replyContainer.appendChild(renderPart(part)); - } - container.insertBefore(replyContainer, container.firstChild); - }); + return container; } + } function renderList(listBlock) { @@ -116,7 +107,7 @@ const formatFunction = { newline: () => tag.br() }; -function renderPart(part) { +export function renderPart(part) { const f = formatFunction[part.type]; if (!f) { return text(`[unknown part type ${part.type}]`); From 99f4eb6843cb9b973d3e12df3d28d7bd41759f5d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 9 Dec 2021 22:59:02 +0530 Subject: [PATCH 09/77] Minimize manual dom manipulation where possible --- .../web/ui/session/room/timeline/ReplyPreviewView.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js index dbb07579..7dbc9b0a 100644 --- a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js +++ b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js @@ -27,8 +27,8 @@ export class ReplyPreviewView extends TemplateView { _renderError({ error, avatar, senderName }) { const errorMessage = this._getErrorMessage(error); - const reply = avatar && senderName? this._renderReplyHeader(avatar, senderName) : tag.blockquote(); - reply.append(tag.span({ className: "statusMessage" }, errorMessage), tag.br()); + const children = [tag.span({ className: "statusMessage" }, errorMessage), tag.br()]; + const reply = avatar && senderName? this._renderReplyHeader(avatar, senderName, children) : tag.blockquote(children); return reply; } @@ -52,10 +52,12 @@ export class ReplyPreviewView extends TemplateView { return reply; } - _renderReplyHeader(avatar, displayName) { + _renderReplyHeader(avatar, displayName, children) { return tag.blockquote([ tag.a({ className: "link", href: "#" }, "In reply to"), - tag.a({ className: "pill", href: "#" }, [renderStaticAvatar(avatar, 12), displayName]), tag.br() + tag.a({ className: "pill", href: "#" }, [renderStaticAvatar(avatar, 12), displayName]), + tag.br(), + ...children ]); } } From 3aa29cfc659ccbc58a71cb8bdeffdee7af8bcb11 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 10 Dec 2021 12:40:55 +0530 Subject: [PATCH 10/77] Do not remove reply preview --- .../web/ui/session/room/timeline/ReplyPreviewView.js | 8 +++++--- .../web/ui/session/room/timeline/TextMessageView.js | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js index 7dbc9b0a..651db1c0 100644 --- a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js +++ b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js @@ -28,7 +28,8 @@ export class ReplyPreviewView extends TemplateView { _renderError({ error, avatar, senderName }) { const errorMessage = this._getErrorMessage(error); const children = [tag.span({ className: "statusMessage" }, errorMessage), tag.br()]; - const reply = avatar && senderName? this._renderReplyHeader(avatar, senderName, children) : tag.blockquote(children); + const reply = avatar && senderName ? this._renderReplyHeader(avatar, senderName, children) : + tag.blockquote({ className: "ReplyPreviewView" }, children); return reply; } @@ -52,8 +53,9 @@ export class ReplyPreviewView extends TemplateView { return reply; } - _renderReplyHeader(avatar, displayName, children) { - return tag.blockquote([ + _renderReplyHeader(avatar, displayName, children = []) { + return tag.blockquote({ className: "ReplyPreviewView" }, + [ tag.a({ className: "link", href: "#" }, "In reply to"), tag.a({ className: "pill", href: "#" }, [renderStaticAvatar(avatar, 12), displayName]), tag.br(), diff --git a/src/platform/web/ui/session/room/timeline/TextMessageView.js b/src/platform/web/ui/session/room/timeline/TextMessageView.js index b0733482..b16ab8a9 100644 --- a/src/platform/web/ui/session/room/timeline/TextMessageView.js +++ b/src/platform/web/ui/session/room/timeline/TextMessageView.js @@ -29,7 +29,7 @@ export class TextMessageView extends BaseMessageView { }, t.mapView(vm => vm.replyPreviewBody, reply => reply ? new ReplyPreviewView(reply): null)); t.mapSideEffect(vm => vm.body, body => { - while (container.lastChild && container.lastChild.tagName !== "BLOCKQUOTE") { + while (container.lastChild && container.lastChild.className !== "ReplyPreviewView") { container.removeChild(container.lastChild); } for (const part of body.parts) { From 545aae31d910047e19bbeb4f4f639911fdd0adb5 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 14 Dec 2021 11:33:54 +0530 Subject: [PATCH 11/77] WIP --- .../session/room/timeline/tiles/TextTile.js | 19 +++++++++++++++++ .../session/room/timeline/ReplyPreviewView.js | 21 ++++++++++++------- .../session/room/timeline/TextMessageView.js | 8 +++++-- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/domain/session/room/timeline/tiles/TextTile.js b/src/domain/session/room/timeline/tiles/TextTile.js index 1b235e20..145172e5 100644 --- a/src/domain/session/room/timeline/tiles/TextTile.js +++ b/src/domain/session/room/timeline/tiles/TextTile.js @@ -19,6 +19,12 @@ import {parsePlainBody} from "../MessageBody.js"; import {parseHTMLBody} from "../deserialize.js"; export class TextTile extends BaseTextTile { + + constructor(options) { + super(options); + this._replyTextTile = null; + } + _getContentString(key) { return this._getContent()?.[key] || ""; } @@ -59,4 +65,17 @@ export class TextTile extends BaseTextTile { } return messageBody; } + + get replyTextTile() { + if (!this._entry.contextEventId) { + return null; + } + if (!this._replyTextTile) { + const entry = this._entry.contextEntry; + if (entry) { + this._replyTextTile = new TextTile(this.childOptions({entry, roomVM: this._roomVM, timeline: this._timeline})); + } + } + return this._replyTextTile; + } } diff --git a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js index 651db1c0..f57c9f39 100644 --- a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js +++ b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js @@ -21,7 +21,13 @@ import {renderPart} from "./TextMessageView.js"; export class ReplyPreviewView extends TemplateView { render(t, vm) { - const replyContainer = vm.error? this._renderError(vm) : this._renderReplyPreview(vm); + const replyContainer = t.div({className: "ReplyPreviewView"}); + t.mapSideEffect(vm => vm.body, () => { + while (replyContainer.lastChild) { + replyContainer.removeChild(replyContainer.lastChild); + } + replyContainer.appendChild(vm.error? this._renderError(vm) : this._renderReplyPreview(vm)); + }) return replyContainer; } @@ -29,7 +35,7 @@ export class ReplyPreviewView extends TemplateView { const errorMessage = this._getErrorMessage(error); const children = [tag.span({ className: "statusMessage" }, errorMessage), tag.br()]; const reply = avatar && senderName ? this._renderReplyHeader(avatar, senderName, children) : - tag.blockquote({ className: "ReplyPreviewView" }, children); + tag.blockquote(children); return reply; } @@ -45,19 +51,20 @@ export class ReplyPreviewView extends TemplateView { } } - _renderReplyPreview({ body, avatar, senderName }) { - const reply = this._renderReplyHeader(avatar, senderName); + _renderReplyPreview(vm) { + const reply = this._renderReplyHeader(vm); + const body = vm.body; for (const part of body.parts) { reply.appendChild(renderPart(part)); } return reply; } - _renderReplyHeader(avatar, displayName, children = []) { - return tag.blockquote({ className: "ReplyPreviewView" }, + _renderReplyHeader(vm, children = []) { + return tag.blockquote( [ tag.a({ className: "link", href: "#" }, "In reply to"), - tag.a({ className: "pill", href: "#" }, [renderStaticAvatar(avatar, 12), displayName]), + tag.a({ className: "pill", href: "#" }, [renderStaticAvatar(vm, 12, undefined, true), vm.displayName]), tag.br(), ...children ]); diff --git a/src/platform/web/ui/session/room/timeline/TextMessageView.js b/src/platform/web/ui/session/room/timeline/TextMessageView.js index b16ab8a9..5d5a48a2 100644 --- a/src/platform/web/ui/session/room/timeline/TextMessageView.js +++ b/src/platform/web/ui/session/room/timeline/TextMessageView.js @@ -26,10 +26,10 @@ export class TextMessageView extends BaseMessageView { "Timeline_messageBody": true, statusMessage: vm => vm.shape === "message-status", } - }, t.mapView(vm => vm.replyPreviewBody, reply => reply ? new ReplyPreviewView(reply): null)); + }, t.mapView(vm => vm.replyTextTile, replyTextTile => replyTextTile ? new ReplyPreviewView(replyTextTile) : null)); t.mapSideEffect(vm => vm.body, body => { - while (container.lastChild && container.lastChild.className !== "ReplyPreviewView") { + while (this._shouldRemove(container.lastChild)) { container.removeChild(container.lastChild); } for (const part of body.parts) { @@ -41,6 +41,10 @@ export class TextMessageView extends BaseMessageView { return container; } + _shouldRemove(element) { + return element && element.className !== "ReplyPreviewView" && element.nodeName !== "#comment"; + } + } function renderList(listBlock) { From 67da746b48922d286476e3342e1296fd48a6ea88 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 14 Dec 2021 14:23:52 +0530 Subject: [PATCH 12/77] Render error --- .../session/room/timeline/tiles/TextTile.js | 20 ++++++++++++- .../session/room/timeline/ReplyPreviewView.js | 28 ++++++++++--------- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/src/domain/session/room/timeline/tiles/TextTile.js b/src/domain/session/room/timeline/tiles/TextTile.js index 145172e5..60ffa9ce 100644 --- a/src/domain/session/room/timeline/tiles/TextTile.js +++ b/src/domain/session/room/timeline/tiles/TextTile.js @@ -73,9 +73,27 @@ export class TextTile extends BaseTextTile { if (!this._replyTextTile) { const entry = this._entry.contextEntry; if (entry) { - this._replyTextTile = new TextTile(this.childOptions({entry, roomVM: this._roomVM, timeline: this._timeline})); + this._replyTextTile = new ReplyPreviewTile(this.childOptions({entry, roomVM: this._roomVM, timeline: this._timeline})); } } return this._replyTextTile; } } + +class ReplyPreviewTile extends TextTile { + constructor(options) { + super(options); + } + + get isRedacted() { + return this._entry.isRedacted; + } + + get decryptionError() { + return !!this._entry.decryptionError; + } + + get error() { + return this.isRedacted || this.decryptionError; + } +} diff --git a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js index f57c9f39..cedf59b2 100644 --- a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js +++ b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js @@ -31,23 +31,25 @@ export class ReplyPreviewView extends TemplateView { return replyContainer; } - _renderError({ error, avatar, senderName }) { - const errorMessage = this._getErrorMessage(error); + _renderError(vm) { + const errorMessage = this._getErrorMessage(vm); const children = [tag.span({ className: "statusMessage" }, errorMessage), tag.br()]; - const reply = avatar && senderName ? this._renderReplyHeader(avatar, senderName, children) : - tag.blockquote(children); + let reply; + try { + reply = this._renderReplyHeader(vm, children); + } + catch { + reply = tag.blockquote(children); + } return reply; } - _getErrorMessage(error) { - switch (error.name) { - case "ContextEntryNotFound": - case "MissingBody": - return "This message could not be fetched."; - case "MessageRedacted": - return "This message has been deleted."; - default: - return error.message; + _getErrorMessage(vm) { + if (vm.isRedacted) { + return "This message has been deleted."; + } + else if (vm.decryptionError) { + return "This message could not be decrypted." } } From 4a12acf1572216eb047be393fc281bf8d7794d42 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 14 Dec 2021 14:42:09 +0530 Subject: [PATCH 13/77] Improve error code --- src/domain/session/room/timeline/tiles/TextTile.js | 6 +++--- .../web/ui/session/room/timeline/ReplyPreviewView.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/domain/session/room/timeline/tiles/TextTile.js b/src/domain/session/room/timeline/tiles/TextTile.js index 60ffa9ce..29c2311b 100644 --- a/src/domain/session/room/timeline/tiles/TextTile.js +++ b/src/domain/session/room/timeline/tiles/TextTile.js @@ -90,10 +90,10 @@ class ReplyPreviewTile extends TextTile { } get decryptionError() { - return !!this._entry.decryptionError; + return this._entry.decryptionError; } - get error() { - return this.isRedacted || this.decryptionError; + get hasError() { + return this.isRedacted || !!this.decryptionError; } } diff --git a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js index cedf59b2..3837bf1e 100644 --- a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js +++ b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js @@ -26,7 +26,7 @@ export class ReplyPreviewView extends TemplateView { while (replyContainer.lastChild) { replyContainer.removeChild(replyContainer.lastChild); } - replyContainer.appendChild(vm.error? this._renderError(vm) : this._renderReplyPreview(vm)); + replyContainer.appendChild(vm.hasError? this._renderError(vm) : this._renderReplyPreview(vm)); }) return replyContainer; } @@ -49,7 +49,7 @@ export class ReplyPreviewView extends TemplateView { return "This message has been deleted."; } else if (vm.decryptionError) { - return "This message could not be decrypted." + return vm.decryptionError.message; } } From 73c5562fd348bf05d51e09d36f4dd87ae1dd4247 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 14 Dec 2021 14:57:31 +0530 Subject: [PATCH 14/77] Remove code from BaseTextTile --- .../room/timeline/tiles/BaseTextTile.js | 39 ------------------- 1 file changed, 39 deletions(-) diff --git a/src/domain/session/room/timeline/tiles/BaseTextTile.js b/src/domain/session/room/timeline/tiles/BaseTextTile.js index 042747aa..164443e3 100644 --- a/src/domain/session/room/timeline/tiles/BaseTextTile.js +++ b/src/domain/session/room/timeline/tiles/BaseTextTile.js @@ -17,7 +17,6 @@ limitations under the License. import {BaseMessageTile} from "./BaseMessageTile.js"; import {stringAsBody} from "../MessageBody.js"; import {createEnum} from "../../../../../utils/enum"; -import {avatarInitials, getAvatarHttpUrl, getIdentifierColorNumber} from "../../../../avatar.js"; export const BodyFormat = createEnum("Plain", "Html"); @@ -58,42 +57,4 @@ export class BaseTextTile extends BaseMessageTile { return this._messageBody; } - get replyPreviewBody() { - if (!this._entry.contextEventId) { - return null; - } - const entry = this._entry.contextEntry; - const error = this._generateError(entry); - if (error?.name === "ContextEntryNotFound") { - return { error }; - } - const format = entry.content.format === "org.matrix.custom.html" ? BodyFormat.Html : BodyFormat.Plain; - const body = entry.content["formatted_body"] ?? entry.content["body"]; - return { - body: body && this._parseBody(body, format), - senderName: entry.displayName, - avatar: { - avatarColorNumber: getIdentifierColorNumber(entry.sender), - avatarLetter: avatarInitials(entry.displayName), - avatarUrl: (size) => getAvatarHttpUrl(entry.avatarUrl, size, this.platform, this._mediaRepository) - }, - error - }; - } - - _generateError(entry) { - const createError = (name) => { const e = new Error(); e.name = name; return e;} - if (!entry) { - return createError("ContextEntryNotFound"); - } - else if (entry.decryptionError) { - return entry.decryptionError; - } - else if (entry.isRedacted) { - return createError("MessageRedacted"); - } - else if (!(entry.content["formatted_body"] || entry.content["body"])) { - return createError("MissingBody"); - } - } } From aa3bb9c6ef8ecf6178a0d86ddcc96d2977f52f4c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 14 Dec 2021 16:21:54 +0530 Subject: [PATCH 15/77] Remove allowReplies --- .../session/room/timeline/deserialize.js | 33 +++++++------------ .../session/room/timeline/tiles/TextTile.js | 2 +- 2 files changed, 12 insertions(+), 23 deletions(-) diff --git a/src/domain/session/room/timeline/deserialize.js b/src/domain/session/room/timeline/deserialize.js index b66804a5..bd362206 100644 --- a/src/domain/session/room/timeline/deserialize.js +++ b/src/domain/session/room/timeline/deserialize.js @@ -34,8 +34,7 @@ const baseUrl = 'https://matrix.to'; const linkPrefix = `${baseUrl}/#/`; class Deserializer { - constructor(result, mediaRepository, allowReplies) { - this.allowReplies = allowReplies; + constructor(result, mediaRepository) { this.result = result; this.mediaRepository = mediaRepository; } @@ -289,7 +288,7 @@ class Deserializer { } _isAllowedNode(node) { - return this.allowReplies || !this._ensureElement(node, "MX-REPLY"); + return !this._ensureElement(node, "MX-REPLY"); } _parseInlineNodes(nodes, into) { @@ -345,13 +344,11 @@ class Deserializer { } } -export function parseHTMLBody(platform, mediaRepository, allowReplies, html) { - if (allowReplies) { - // todo: might be better to remove mx-reply and children after parsing, need to think - html = html.replace(/.+<\/mx-reply>/, ""); - } +export function parseHTMLBody(platform, mediaRepository, html) { + // todo: might be better to remove mx-reply and children after parsing, need to think + html = html.replace(/.+<\/mx-reply>/, ""); const parseResult = platform.parseHTML(html); - const deserializer = new Deserializer(parseResult, mediaRepository, allowReplies); + const deserializer = new Deserializer(parseResult, mediaRepository); const parts = deserializer.parseAnyNodes(parseResult.rootNodes); return new MessageBody(html, parts); } @@ -405,8 +402,8 @@ export async function tests() { parseHTML: (html) => new HTMLParseResult(parse(html)) }; - function test(assert, input, output, replies=true) { - assert.deepEqual(parseHTMLBody(platform, null, replies, input), new MessageBody(input, output)); + function test(assert, input, output) { + assert.deepEqual(parseHTMLBody(platform, null, input), new MessageBody(input, output)); } return { @@ -504,23 +501,15 @@ export async function tests() { ]; test(assert, input, output); }, - "Replies are inserted when allowed": assert => { - const input = 'Hello, World!'; - const output = [ - new TextPart('Hello, '), - new FormatPart("em", [new TextPart('World')]), - new TextPart('!'), - ]; - test(assert, input, output); - }, - "Replies are stripped when not allowed": assert => { + "Reply fallback is always stripped": assert => { const input = 'Hello, World!'; + const strippedInput = 'Hello, !'; const output = [ new TextPart('Hello, '), new FormatPart("em", []), new TextPart('!'), ]; - test(assert, input, output, false); + assert.deepEqual(parseHTMLBody(platform, null, input), new MessageBody(strippedInput, output)); } /* Doesnt work: HTML library doesn't handle
 properly.
         "Text with code block": assert => {
diff --git a/src/domain/session/room/timeline/tiles/TextTile.js b/src/domain/session/room/timeline/tiles/TextTile.js
index 29c2311b..acd904a4 100644
--- a/src/domain/session/room/timeline/tiles/TextTile.js
+++ b/src/domain/session/room/timeline/tiles/TextTile.js
@@ -56,7 +56,7 @@ export class TextTile extends BaseTextTile {
     _parseBody(body, format) {
         let messageBody;
         if (format === BodyFormat.Html) {
-            messageBody = parseHTMLBody(this.platform, this._mediaRepository, this._entry.isReply, body);
+            messageBody = parseHTMLBody(this.platform, this._mediaRepository, body);
         } else {
             messageBody = parsePlainBody(body);
         }

From 54004eef4d4b695a44399e631a3713b7b0319608 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Wed, 15 Dec 2021 17:46:14 +0530
Subject: [PATCH 16/77] Integrate into update mechanism

---
 .../session/room/timeline/TilesCollection.js  |  2 +-
 .../room/timeline/tiles/BaseMessageTile.js    |  4 ++--
 .../session/room/timeline/tiles/SimpleTile.js | 14 ++++++++++++-
 .../session/room/timeline/tiles/TextTile.js   | 20 +------------------
 .../session/room/timeline/ReplyPreviewView.js |  2 +-
 5 files changed, 18 insertions(+), 24 deletions(-)

diff --git a/src/domain/session/room/timeline/TilesCollection.js b/src/domain/session/room/timeline/TilesCollection.js
index 54ab5ddd..33ae4472 100644
--- a/src/domain/session/room/timeline/TilesCollection.js
+++ b/src/domain/session/room/timeline/TilesCollection.js
@@ -150,7 +150,7 @@ export class TilesCollection extends BaseObservableList {
         const tileIdx = this._findTileIdx(entry);
         const tile = this._findTileAtIdx(entry, tileIdx);
         if (tile) {
-            const action = tile.updateEntry(entry, params);
+            const action = tile.updateEntry(entry, params, this._tileCreator);
             if (action.shouldReplace) {
                 const newTile = this._tileCreator(entry);
                 if (newTile) {
diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
index 25dbfa38..63f92056 100644
--- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js
+++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
@@ -102,8 +102,8 @@ export class BaseMessageTile extends SimpleTile {
         }
     }
 
-    updateEntry(entry, param) {
-        const action = super.updateEntry(entry, param);
+    updateEntry(entry, param, tileCreator) {
+        const action = super.updateEntry(entry, param, tileCreator);
         if (action.shouldUpdate) {
             this._updateReactions();
         }
diff --git a/src/domain/session/room/timeline/tiles/SimpleTile.js b/src/domain/session/room/timeline/tiles/SimpleTile.js
index 4c1c1de0..fabf7bf4 100644
--- a/src/domain/session/room/timeline/tiles/SimpleTile.js
+++ b/src/domain/session/room/timeline/tiles/SimpleTile.js
@@ -52,6 +52,10 @@ export class SimpleTile extends ViewModel {
         return this._entry.isPending && this._entry.pendingEvent.status !== SendStatus.Sent;
     }
 
+    get isRedacted() {
+        return this._entry.isRedacted;
+    }
+
     get canAbortSending() {
         return this._entry.isPending &&
             !this._entry.pendingEvent.hasStartedSending;
@@ -92,7 +96,15 @@ export class SimpleTile extends ViewModel {
     }
 
     // update received for already included (falls within sort keys) entry
-    updateEntry(entry, param) {
+    updateEntry(entry, param, tileCreator) {
+        const replyEntry = param?.reply ?? entry.contextEntry;
+        if (replyEntry) {
+            // this is an update to contextEntry used for replyPreview
+            const action = this._replyTextTile?.updateEntry(replyEntry);
+            if (action?.shouldReplace) {
+                this._replyTextTile = tileCreator(replyEntry);
+            }
+        }
         const renderedAsRedacted = this.shape === "redacted";
         if (!entry.isGap && entry.isRedacted !== renderedAsRedacted) {
             // recreate the tile if the entry becomes redacted
diff --git a/src/domain/session/room/timeline/tiles/TextTile.js b/src/domain/session/room/timeline/tiles/TextTile.js
index acd904a4..255e27fb 100644
--- a/src/domain/session/room/timeline/tiles/TextTile.js
+++ b/src/domain/session/room/timeline/tiles/TextTile.js
@@ -73,27 +73,9 @@ export class TextTile extends BaseTextTile {
         if (!this._replyTextTile) {
             const entry = this._entry.contextEntry;
             if (entry) {
-                this._replyTextTile =  new ReplyPreviewTile(this.childOptions({entry, roomVM: this._roomVM, timeline: this._timeline}));
+                this._replyTextTile = new TextTile(this.childOptions({entry, roomVM: this._roomVM, timeline: this._timeline}));
             }
         }
         return this._replyTextTile;
     }
 }
-
-class ReplyPreviewTile extends TextTile {
-    constructor(options) {
-        super(options);
-    }
-
-    get isRedacted() {
-        return this._entry.isRedacted;
-    }
-
-    get decryptionError() {
-        return this._entry.decryptionError;
-    }
-
-    get hasError() {
-        return this.isRedacted || !!this.decryptionError;
-    }
-}
diff --git a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
index 3837bf1e..15a69c3e 100644
--- a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
+++ b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
@@ -26,7 +26,7 @@ export class ReplyPreviewView extends TemplateView {
             while (replyContainer.lastChild) {
                 replyContainer.removeChild(replyContainer.lastChild);
             }
-            replyContainer.appendChild(vm.hasError? this._renderError(vm) : this._renderReplyPreview(vm));
+            replyContainer.appendChild(vm.isRedacted? this._renderError(vm) : this._renderReplyPreview(vm));
         })
         return replyContainer;
     }

From 91912bdb8d96e36755085719b5e7aea0d89b0b7b Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Thu, 16 Dec 2021 13:47:14 +0530
Subject: [PATCH 17/77] Create tile using tileCreator

---
 .../session/room/timeline/tiles/TextTile.js   |  3 ++-
 .../session/room/timeline/tilesCreator.js     |  5 ++--
 .../session/room/timeline/ReplyPreviewView.js | 24 ++++---------------
 3 files changed, 9 insertions(+), 23 deletions(-)

diff --git a/src/domain/session/room/timeline/tiles/TextTile.js b/src/domain/session/room/timeline/tiles/TextTile.js
index 255e27fb..9c70dbfa 100644
--- a/src/domain/session/room/timeline/tiles/TextTile.js
+++ b/src/domain/session/room/timeline/tiles/TextTile.js
@@ -22,6 +22,7 @@ export class TextTile extends BaseTextTile {
 
     constructor(options) {
         super(options);
+        this._tileCreator = options.tileCreator;
         this._replyTextTile = null;
     }
 
@@ -73,7 +74,7 @@ export class TextTile extends BaseTextTile {
         if (!this._replyTextTile) {
             const entry = this._entry.contextEntry;
             if (entry) {
-                this._replyTextTile = new TextTile(this.childOptions({entry, roomVM: this._roomVM, timeline: this._timeline}));
+                this._replyTextTile = this._tileCreator(entry);
             }
         }
         return this._replyTextTile;
diff --git a/src/domain/session/room/timeline/tilesCreator.js b/src/domain/session/room/timeline/tilesCreator.js
index 9dde00a2..8634ea6f 100644
--- a/src/domain/session/room/timeline/tilesCreator.js
+++ b/src/domain/session/room/timeline/tilesCreator.js
@@ -28,8 +28,8 @@ import {EncryptionEnabledTile} from "./tiles/EncryptionEnabledTile.js";
 import {MissingAttachmentTile} from "./tiles/MissingAttachmentTile.js";
 
 export function tilesCreator(baseOptions) {
-    return function tilesCreator(entry, emitUpdate) {
-        const options = Object.assign({entry, emitUpdate}, baseOptions);
+    const creator =  function tilesCreator(entry, emitUpdate) {
+        const options = Object.assign({entry, emitUpdate, tileCreator: creator}, baseOptions);
         if (entry.isGap) {
             return new GapTile(options);
         } else if (entry.isPending && entry.pendingEvent.isMissingAttachments) {
@@ -77,4 +77,5 @@ export function tilesCreator(baseOptions) {
             }
         }
     }   
+    return creator;
 }
diff --git a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
index 15a69c3e..ce013c5c 100644
--- a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
+++ b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
@@ -26,33 +26,17 @@ export class ReplyPreviewView extends TemplateView {
             while (replyContainer.lastChild) {
                 replyContainer.removeChild(replyContainer.lastChild);
             }
-            replyContainer.appendChild(vm.isRedacted? this._renderError(vm) : this._renderReplyPreview(vm));
+            replyContainer.appendChild(vm.isRedacted? this._renderRedaction(vm) : this._renderReplyPreview(vm));
         })
         return replyContainer;
     }
 
-    _renderError(vm) {
-        const errorMessage = this._getErrorMessage(vm);
-        const children = [tag.span({ className: "statusMessage" }, errorMessage), tag.br()];
-        let reply;
-        try {
-            reply = this._renderReplyHeader(vm, children);
-        }
-        catch {
-            reply = tag.blockquote(children);
-        }
+    _renderRedaction(vm) {
+        const children = [tag.span({ className: "statusMessage" }, vm.description), tag.br()];
+        const reply = this._renderReplyHeader(vm, children);
         return reply;
     }
 
-    _getErrorMessage(vm) {
-        if (vm.isRedacted) {
-            return "This message has been deleted.";
-        }
-        else if (vm.decryptionError) {
-            return vm.decryptionError.message;
-        }
-    }
-
     _renderReplyPreview(vm) {
         const reply = this._renderReplyHeader(vm);
         const body = vm.body;

From e0dc853d7449599b218e60b721daf568962487bb Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Thu, 16 Dec 2021 15:27:53 +0530
Subject: [PATCH 18/77] Fill matrix.to links

---
 src/domain/session/room/timeline/tiles/BaseMessageTile.js | 8 ++++++++
 .../web/ui/session/room/timeline/ReplyPreviewView.js      | 4 ++--
 2 files changed, 10 insertions(+), 2 deletions(-)

diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
index 63f92056..7c5d7e1b 100644
--- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js
+++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
@@ -33,6 +33,14 @@ export class BaseMessageTile extends SimpleTile {
         return this._room.mediaRepository;
     }
 
+    get roomId() {
+        return this._room.id;
+    }
+
+    get eventId() {
+        return this._entry.id;
+    }
+
     get displayName() {
         return this._entry.displayName || this.sender;
     }
diff --git a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
index ce013c5c..393a34f5 100644
--- a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
+++ b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
@@ -49,8 +49,8 @@ export class ReplyPreviewView extends TemplateView {
     _renderReplyHeader(vm, children = []) {
         return tag.blockquote(
             [
-            tag.a({ className: "link", href: "#" }, "In reply to"),
-            tag.a({ className: "pill", href: "#" }, [renderStaticAvatar(vm, 12, undefined, true), vm.displayName]),
+            tag.a({ className: "link", href: `https://matrix.to/#/${vm.roomId}/${vm.eventId}` }, "In reply to"),
+            tag.a({ className: "pill", href: `https://matrix.to/#/${vm.sender}` }, [renderStaticAvatar(vm, 12, undefined, true), vm.displayName]),
             tag.br(),
             ...children
         ]);

From df22db256bb31b207d0eb9c877941dff3fe3dec7 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Thu, 16 Dec 2021 15:32:57 +0530
Subject: [PATCH 19/77] No need to pass tileCreator as argument

---
 src/domain/session/room/timeline/TilesCollection.js       | 2 +-
 src/domain/session/room/timeline/tiles/BaseMessageTile.js | 4 ++--
 src/domain/session/room/timeline/tiles/SimpleTile.js      | 5 +++--
 src/domain/session/room/timeline/tiles/TextTile.js        | 1 -
 4 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/domain/session/room/timeline/TilesCollection.js b/src/domain/session/room/timeline/TilesCollection.js
index 33ae4472..54ab5ddd 100644
--- a/src/domain/session/room/timeline/TilesCollection.js
+++ b/src/domain/session/room/timeline/TilesCollection.js
@@ -150,7 +150,7 @@ export class TilesCollection extends BaseObservableList {
         const tileIdx = this._findTileIdx(entry);
         const tile = this._findTileAtIdx(entry, tileIdx);
         if (tile) {
-            const action = tile.updateEntry(entry, params, this._tileCreator);
+            const action = tile.updateEntry(entry, params);
             if (action.shouldReplace) {
                 const newTile = this._tileCreator(entry);
                 if (newTile) {
diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
index 7c5d7e1b..79e9ee12 100644
--- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js
+++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
@@ -110,8 +110,8 @@ export class BaseMessageTile extends SimpleTile {
         }
     }
 
-    updateEntry(entry, param, tileCreator) {
-        const action = super.updateEntry(entry, param, tileCreator);
+    updateEntry(entry, param) {
+        const action = super.updateEntry(entry, param);
         if (action.shouldUpdate) {
             this._updateReactions();
         }
diff --git a/src/domain/session/room/timeline/tiles/SimpleTile.js b/src/domain/session/room/timeline/tiles/SimpleTile.js
index fabf7bf4..4b00c525 100644
--- a/src/domain/session/room/timeline/tiles/SimpleTile.js
+++ b/src/domain/session/room/timeline/tiles/SimpleTile.js
@@ -21,6 +21,7 @@ import {SendStatus} from "../../../../../matrix/room/sending/PendingEvent.js";
 export class SimpleTile extends ViewModel {
     constructor(options) {
         super(options);
+        this._tileCreator = options.tileCreator;
         this._entry = options.entry;
     }
     // view model props for all subclasses
@@ -96,13 +97,13 @@ export class SimpleTile extends ViewModel {
     }
 
     // update received for already included (falls within sort keys) entry
-    updateEntry(entry, param, tileCreator) {
+    updateEntry(entry, param) {
         const replyEntry = param?.reply ?? entry.contextEntry;
         if (replyEntry) {
             // this is an update to contextEntry used for replyPreview
             const action = this._replyTextTile?.updateEntry(replyEntry);
             if (action?.shouldReplace) {
-                this._replyTextTile = tileCreator(replyEntry);
+                this._replyTextTile = this._tileCreator(replyEntry);
             }
         }
         const renderedAsRedacted = this.shape === "redacted";
diff --git a/src/domain/session/room/timeline/tiles/TextTile.js b/src/domain/session/room/timeline/tiles/TextTile.js
index 9c70dbfa..759bfc0d 100644
--- a/src/domain/session/room/timeline/tiles/TextTile.js
+++ b/src/domain/session/room/timeline/tiles/TextTile.js
@@ -22,7 +22,6 @@ export class TextTile extends BaseTextTile {
 
     constructor(options) {
         super(options);
-        this._tileCreator = options.tileCreator;
         this._replyTextTile = null;
     }
 

From bb45d0eae970eda6fc9bca7cee35b32a41c66cf1 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Thu, 16 Dec 2021 17:35:19 +0530
Subject: [PATCH 20/77] Render non-text messages as well

---
 .../room/timeline/tiles/BaseMessageTile.js    | 14 ++++++
 .../session/room/timeline/tiles/TextTile.js   | 13 -----
 .../session/room/timeline/ReplyPreviewView.js | 48 ++++++++++++++++---
 3 files changed, 55 insertions(+), 20 deletions(-)

diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
index 79e9ee12..6dee5fc2 100644
--- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js
+++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
@@ -24,6 +24,7 @@ export class BaseMessageTile extends SimpleTile {
         this._date = this._entry.timestamp ? new Date(this._entry.timestamp) : null;
         this._isContinuation = false;
         this._reactions = null;
+        this._replyTextTile = null;
         if (this._entry.annotations || this._entry.pendingAnnotations) {
             this._updateReactions();
         }
@@ -210,4 +211,17 @@ export class BaseMessageTile extends SimpleTile {
             this._reactions.update(annotations, pendingAnnotations);
         }
     }
+
+    get replyTextTile() {
+        if (!this._entry.contextEventId) {
+            return null;
+        }
+        if (!this._replyTextTile) {
+            const entry = this._entry.contextEntry;
+            if (entry) {
+                this._replyTextTile = this._tileCreator(entry);
+            }
+        }
+        return this._replyTextTile;
+    }
 }
diff --git a/src/domain/session/room/timeline/tiles/TextTile.js b/src/domain/session/room/timeline/tiles/TextTile.js
index 759bfc0d..9105f190 100644
--- a/src/domain/session/room/timeline/tiles/TextTile.js
+++ b/src/domain/session/room/timeline/tiles/TextTile.js
@@ -22,7 +22,6 @@ export class TextTile extends BaseTextTile {
 
     constructor(options) {
         super(options);
-        this._replyTextTile = null;
     }
 
     _getContentString(key) {
@@ -66,16 +65,4 @@ export class TextTile extends BaseTextTile {
         return messageBody;
     }
 
-    get replyTextTile() {
-        if (!this._entry.contextEventId) {
-            return null;
-        }
-        if (!this._replyTextTile) {
-            const entry = this._entry.contextEntry;
-            if (entry) {
-                this._replyTextTile = this._tileCreator(entry);
-            }
-        }
-        return this._replyTextTile;
-    }
 }
diff --git a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
index 393a34f5..144f9e84 100644
--- a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
+++ b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
@@ -17,7 +17,10 @@ limitations under the License.
 import {renderStaticAvatar} from "../../../avatar";
 import {tag} from "../../../general/html";
 import {TemplateView} from "../../../general/TemplateView";
-import {renderPart} from "./TextMessageView.js";
+import {FileView} from "./FileView";
+import {ImageView} from "./ImageView";
+import {TextMessageView} from "./TextMessageView.js";
+import {VideoView} from "./VideoView";
 
 export class ReplyPreviewView extends TemplateView {
     render(t, vm) {
@@ -26,7 +29,7 @@ export class ReplyPreviewView extends TemplateView {
             while (replyContainer.lastChild) {
                 replyContainer.removeChild(replyContainer.lastChild);
             }
-            replyContainer.appendChild(vm.isRedacted? this._renderRedaction(vm) : this._renderReplyPreview(vm));
+            replyContainer.appendChild(vm.isRedacted? this._renderRedaction(vm) : this._renderReplyPreview(t, vm));
         })
         return replyContainer;
     }
@@ -37,15 +40,46 @@ export class ReplyPreviewView extends TemplateView {
         return reply;
     }
 
-    _renderReplyPreview(vm) {
-        const reply = this._renderReplyHeader(vm);
-        const body = vm.body;
-        for (const part of body.parts) {
-            reply.appendChild(renderPart(part));
+    _renderReplyPreview(t, vm) {
+        let reply;
+        switch (vm.shape) {
+            case "image":
+            case "video":
+                reply = this._renderMediaPreview(t, vm);
+                break;
+            default:
+                reply = this._renderPreview(t, vm);
+                break;
         }
         return reply;
     }
 
+    _renderPreview(t, vm) {
+        const view = this._viewFromShape(vm);
+        const rendered = view.renderMessageBody(t, vm);
+        return this._renderReplyHeader(vm, [rendered]);
+    }
+
+    _renderMediaPreview(t, vm) {
+        const view = this._viewFromShape(vm);
+        const rendered = view.renderMedia(t, vm);
+        return this._renderReplyHeader(vm, [rendered]);
+    }
+
+    _viewFromShape(vm) {
+        const shape = vm.shape;
+        switch (shape) {
+            case "image":
+                return new ImageView(vm);
+            case "video":
+                return new VideoView(vm);
+            case "file":
+                return new FileView(vm);
+            case "message":
+                return new TextMessageView(vm);
+        }
+    }
+
     _renderReplyHeader(vm, children = []) {
         return tag.blockquote(
             [

From 13cba844451e8bffca37d5d466d9b06f8dd9e7b5 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Thu, 16 Dec 2021 17:40:47 +0530
Subject: [PATCH 21/77] Remove mapSideEffect

---
 .../web/ui/session/room/timeline/ReplyPreviewView.js | 12 +++++-------
 1 file changed, 5 insertions(+), 7 deletions(-)

diff --git a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
index 144f9e84..4f9ddac7 100644
--- a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
+++ b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
@@ -24,13 +24,11 @@ import {VideoView} from "./VideoView";
 
 export class ReplyPreviewView extends TemplateView {
     render(t, vm) {
-        const replyContainer = t.div({className: "ReplyPreviewView"});
-        t.mapSideEffect(vm => vm.body, () => {
-            while (replyContainer.lastChild) {
-                replyContainer.removeChild(replyContainer.lastChild);
-            }
-            replyContainer.appendChild(vm.isRedacted? this._renderRedaction(vm) : this._renderReplyPreview(t, vm));
-        })
+        const replyContainer = t.div({ className: "ReplyPreviewView" }, [
+            vm.isRedacted
+                ? this._renderRedaction(vm)
+                : this._renderReplyPreview(t, vm),
+        ]);
         return replyContainer;
     }
 

From 277364240695398483ef6788f91653842f50fc3f Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Thu, 16 Dec 2021 17:52:59 +0530
Subject: [PATCH 22/77] No need to handle redaction specially

---
 .../session/room/timeline/ReplyPreviewView.js | 45 ++++++-------------
 1 file changed, 14 insertions(+), 31 deletions(-)

diff --git a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
index 4f9ddac7..32252677 100644
--- a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
+++ b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
@@ -19,52 +19,35 @@ import {tag} from "../../../general/html";
 import {TemplateView} from "../../../general/TemplateView";
 import {FileView} from "./FileView";
 import {ImageView} from "./ImageView";
+import {RedactedView} from "./RedactedView";
 import {TextMessageView} from "./TextMessageView.js";
 import {VideoView} from "./VideoView";
 
 export class ReplyPreviewView extends TemplateView {
     render(t, vm) {
-        const replyContainer = t.div({ className: "ReplyPreviewView" }, [
-            vm.isRedacted
-                ? this._renderRedaction(vm)
-                : this._renderReplyPreview(t, vm),
-        ]);
-        return replyContainer;
-    }
-
-    _renderRedaction(vm) {
-        const children = [tag.span({ className: "statusMessage" }, vm.description), tag.br()];
-        const reply = this._renderReplyHeader(vm, children);
-        return reply;
+        return t.div({ className: "ReplyPreviewView" }, this._renderReplyPreview(t, vm));
     }
 
     _renderReplyPreview(t, vm) {
-        let reply;
+        const view = this._viewFromViewModel(vm);
+        const rendered = this._renderContent(t, vm, view);
+        return this._renderReplyHeader(vm, [rendered]);
+    }
+
+    _renderContent(t, vm, view) {
         switch (vm.shape) {
             case "image":
             case "video":
-                reply = this._renderMediaPreview(t, vm);
-                break;
+                return view.renderMedia(t, vm);
             default:
-                reply = this._renderPreview(t, vm);
-                break;
+                return view.renderMessageBody(t, vm);
         }
-        return reply;
     }
 
-    _renderPreview(t, vm) {
-        const view = this._viewFromShape(vm);
-        const rendered = view.renderMessageBody(t, vm);
-        return this._renderReplyHeader(vm, [rendered]);
-    }
-
-    _renderMediaPreview(t, vm) {
-        const view = this._viewFromShape(vm);
-        const rendered = view.renderMedia(t, vm);
-        return this._renderReplyHeader(vm, [rendered]);
-    }
-
-    _viewFromShape(vm) {
+    _viewFromViewModel(vm) {
+        if (vm.isRedacted) {
+            return new RedactedView(vm);
+        }
         const shape = vm.shape;
         switch (shape) {
             case "image":

From 89d696813948e2377609c056fd53ffc7d40bba42 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Thu, 16 Dec 2021 18:16:17 +0530
Subject: [PATCH 23/77] Show decryption error as well

---
 src/platform/web/ui/session/room/timeline/ReplyPreviewView.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
index 32252677..741200d8 100644
--- a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
+++ b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
@@ -57,6 +57,7 @@ export class ReplyPreviewView extends TemplateView {
             case "file":
                 return new FileView(vm);
             case "message":
+            case "message-status":
                 return new TextMessageView(vm);
         }
     }

From f01d5d95d988b94d1137d302445c637601c51666 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Fri, 17 Dec 2021 12:46:26 +0530
Subject: [PATCH 24/77] Reuse code from timeline view

---
 .../session/room/timeline/ReplyPreviewView.js | 28 +++----------------
 1 file changed, 4 insertions(+), 24 deletions(-)

diff --git a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
index 741200d8..b6febf28 100644
--- a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
+++ b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
@@ -17,11 +17,7 @@ limitations under the License.
 import {renderStaticAvatar} from "../../../avatar";
 import {tag} from "../../../general/html";
 import {TemplateView} from "../../../general/TemplateView";
-import {FileView} from "./FileView";
-import {ImageView} from "./ImageView";
-import {RedactedView} from "./RedactedView";
-import {TextMessageView} from "./TextMessageView.js";
-import {VideoView} from "./VideoView";
+import {viewClassForEntry} from "../TimelineView";
 
 export class ReplyPreviewView extends TemplateView {
     render(t, vm) {
@@ -29,7 +25,9 @@ export class ReplyPreviewView extends TemplateView {
     }
 
     _renderReplyPreview(t, vm) {
-        const view = this._viewFromViewModel(vm);
+        // todo: this should probably be called viewClassForTile instead
+        const viewClass = viewClassForEntry(vm);
+        const view = new viewClass(vm)
         const rendered = this._renderContent(t, vm, view);
         return this._renderReplyHeader(vm, [rendered]);
     }
@@ -44,24 +42,6 @@ export class ReplyPreviewView extends TemplateView {
         }
     }
 
-    _viewFromViewModel(vm) {
-        if (vm.isRedacted) {
-            return new RedactedView(vm);
-        }
-        const shape = vm.shape;
-        switch (shape) {
-            case "image":
-                return new ImageView(vm);
-            case "video":
-                return new VideoView(vm);
-            case "file":
-                return new FileView(vm);
-            case "message":
-            case "message-status":
-                return new TextMessageView(vm);
-        }
-    }
-
     _renderReplyHeader(vm, children = []) {
         return tag.blockquote(
             [

From 7f1b3e25e89a218988964023d7d82aa1efa91d40 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Fri, 17 Dec 2021 13:09:01 +0530
Subject: [PATCH 25/77] Use t instead of tag

---
 .../ui/session/room/timeline/ReplyPreviewView.js    | 13 ++++++-------
 1 file changed, 6 insertions(+), 7 deletions(-)

diff --git a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
index b6febf28..5581ae77 100644
--- a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
+++ b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
@@ -15,7 +15,6 @@ limitations under the License.
 */
 
 import {renderStaticAvatar} from "../../../avatar";
-import {tag} from "../../../general/html";
 import {TemplateView} from "../../../general/TemplateView";
 import {viewClassForEntry} from "../TimelineView";
 
@@ -29,7 +28,7 @@ export class ReplyPreviewView extends TemplateView {
         const viewClass = viewClassForEntry(vm);
         const view = new viewClass(vm)
         const rendered = this._renderContent(t, vm, view);
-        return this._renderReplyHeader(vm, [rendered]);
+        return this._renderReplyHeader(t, vm, [rendered]);
     }
 
     _renderContent(t, vm, view) {
@@ -42,12 +41,12 @@ export class ReplyPreviewView extends TemplateView {
         }
     }
 
-    _renderReplyHeader(vm, children = []) {
-        return tag.blockquote(
+    _renderReplyHeader(t, vm, children = []) {
+        return t.blockquote(
             [
-            tag.a({ className: "link", href: `https://matrix.to/#/${vm.roomId}/${vm.eventId}` }, "In reply to"),
-            tag.a({ className: "pill", href: `https://matrix.to/#/${vm.sender}` }, [renderStaticAvatar(vm, 12, undefined, true), vm.displayName]),
-            tag.br(),
+            t.a({ className: "link", href: `https://matrix.to/#/${vm.roomId}/${vm.eventId}` }, "In reply to"),
+            t.a({ className: "pill", href: `https://matrix.to/#/${vm.sender}` }, [renderStaticAvatar(vm, 12, undefined, true), vm.displayName]),
+            t.br(),
             ...children
         ]);
     }

From 1b9f970d7fed49887e48a5dee68b31841d318d28 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Fri, 17 Dec 2021 16:22:45 +0530
Subject: [PATCH 26/77] WIP: Render the whole view instead of messageBody

---
 .../web/ui/session/room/MessageComposer.js    |  2 +-
 .../web/ui/session/room/TimelineView.ts       | 19 +-------
 src/platform/web/ui/session/room/common.ts    | 43 +++++++++++++++++++
 .../session/room/timeline/BaseMessageView.js  |  2 +-
 .../session/room/timeline/ReplyPreviewView.js |  7 +--
 5 files changed, 51 insertions(+), 22 deletions(-)
 create mode 100644 src/platform/web/ui/session/room/common.ts

diff --git a/src/platform/web/ui/session/room/MessageComposer.js b/src/platform/web/ui/session/room/MessageComposer.js
index 0d3d9755..cfd389f3 100644
--- a/src/platform/web/ui/session/room/MessageComposer.js
+++ b/src/platform/web/ui/session/room/MessageComposer.js
@@ -17,7 +17,7 @@ limitations under the License.
 import {TemplateView} from "../../general/TemplateView";
 import {Popup} from "../../general/Popup.js";
 import {Menu} from "../../general/Menu.js";
-import {viewClassForEntry} from "./TimelineView"
+import {viewClassForEntry} from "./common"
 
 export class MessageComposer extends TemplateView {
     constructor(viewModel) {
diff --git a/src/platform/web/ui/session/room/TimelineView.ts b/src/platform/web/ui/session/room/TimelineView.ts
index da3dc537..68602c48 100644
--- a/src/platform/web/ui/session/room/TimelineView.ts
+++ b/src/platform/web/ui/session/room/TimelineView.ts
@@ -27,6 +27,7 @@ import {AnnouncementView} from "./timeline/AnnouncementView.js";
 import {RedactedView} from "./timeline/RedactedView.js";
 import {SimpleTile} from "../../../../../domain/session/room/timeline/tiles/SimpleTile.js";
 import {BaseObservableList as ObservableList} from "../../../../../observable/list/BaseObservableList";
+import {viewClassForEntry} from "./common";
 
 //import {TimelineViewModel} from "../../../../../domain/session/room/timeline/TimelineViewModel.js";
 export interface TimelineViewModel extends IObservableValue {
@@ -35,25 +36,9 @@ export interface TimelineViewModel extends IObservableValue {
     setVisibleTileRange(start?: SimpleTile, end?: SimpleTile);
 }
 
-type TileView = GapView | AnnouncementView | TextMessageView |
+export type TileView = GapView | AnnouncementView | TextMessageView |
     ImageView | VideoView | FileView | MissingAttachmentView | RedactedView;
-type TileViewConstructor = (this: TileView, SimpleTile) => void;
 
-export function viewClassForEntry(entry: SimpleTile): TileViewConstructor | undefined {
-    switch (entry.shape) {
-        case "gap": return GapView;
-        case "announcement": return AnnouncementView;
-        case "message":
-        case "message-status":
-            return TextMessageView;
-        case "image": return ImageView;
-        case "video": return VideoView;
-        case "file": return FileView;
-        case "missing-attachment": return MissingAttachmentView;
-        case "redacted":
-            return RedactedView;
-    }
-}
 
 function bottom(node: HTMLElement): number {
     return node.offsetTop + node.clientHeight;
diff --git a/src/platform/web/ui/session/room/common.ts b/src/platform/web/ui/session/room/common.ts
new file mode 100644
index 00000000..764e2b6e
--- /dev/null
+++ b/src/platform/web/ui/session/room/common.ts
@@ -0,0 +1,43 @@
+/*
+Copyright 2021 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 {TextMessageView} from "./timeline/TextMessageView.js";
+import {ImageView} from "./timeline/ImageView.js";
+import {VideoView} from "./timeline/VideoView.js";
+import {FileView} from "./timeline/FileView.js";
+import {MissingAttachmentView} from "./timeline/MissingAttachmentView.js";
+import {AnnouncementView} from "./timeline/AnnouncementView.js";
+import {RedactedView} from "./timeline/RedactedView.js";
+import {SimpleTile} from "../../../../../domain/session/room/timeline/tiles/SimpleTile.js";
+import {GapView} from "./timeline/GapView.js";
+import type {TileView} from "./TimelineView";
+
+type TileViewConstructor = (this: TileView, SimpleTile) => void;
+export function viewClassForEntry(entry: SimpleTile): TileViewConstructor | undefined {
+    switch (entry.shape) {
+        case "gap": return GapView;
+        case "announcement": return AnnouncementView;
+        case "message":
+        case "message-status":
+            return TextMessageView;
+        case "image": return ImageView;
+        case "video": return VideoView;
+        case "file": return FileView;
+        case "missing-attachment": return MissingAttachmentView;
+        case "redacted":
+            return RedactedView;
+    }
+}
diff --git a/src/platform/web/ui/session/room/timeline/BaseMessageView.js b/src/platform/web/ui/session/room/timeline/BaseMessageView.js
index 1fa14841..ca155785 100644
--- a/src/platform/web/ui/session/room/timeline/BaseMessageView.js
+++ b/src/platform/web/ui/session/room/timeline/BaseMessageView.js
@@ -54,7 +54,7 @@ export class BaseMessageView extends TemplateView {
             if (isContinuation && wasContinuation === false) {
                 li.removeChild(li.querySelector(".Timeline_messageAvatar"));
                 li.removeChild(li.querySelector(".Timeline_messageSender"));
-            } else if (!isContinuation) {
+            } else if (!isContinuation && this._interactive) {
                 const avatar = tag.a({href: vm.memberPanelLink, className: "Timeline_messageAvatar"}, [renderStaticAvatar(vm, 30)]);
                 const sender = tag.div({className: `Timeline_messageSender usercolor${vm.avatarColorNumber}`}, vm.displayName);
                 li.insertBefore(avatar, li.firstChild);
diff --git a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
index 5581ae77..00767bdf 100644
--- a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
+++ b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
@@ -16,7 +16,7 @@ limitations under the License.
 
 import {renderStaticAvatar} from "../../../avatar";
 import {TemplateView} from "../../../general/TemplateView";
-import {viewClassForEntry} from "../TimelineView";
+import {viewClassForEntry} from "../common";
 
 export class ReplyPreviewView extends TemplateView {
     render(t, vm) {
@@ -26,8 +26,9 @@ export class ReplyPreviewView extends TemplateView {
     _renderReplyPreview(t, vm) {
         // todo: this should probably be called viewClassForTile instead
         const viewClass = viewClassForEntry(vm);
-        const view = new viewClass(vm)
-        const rendered = this._renderContent(t, vm, view);
+        const view = new viewClass(vm, false)
+        // const rendered = this._renderContent(t, vm, view);
+        const rendered = view.render(t, vm);
         return this._renderReplyHeader(t, vm, [rendered]);
     }
 

From 4d63b411273c4da232499e07c2e2afbca77f7b15 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Sun, 19 Dec 2021 19:50:58 +0530
Subject: [PATCH 27/77] Make reply preview flush left

---
 .../web/ui/css/themes/element/timeline.css        | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/src/platform/web/ui/css/themes/element/timeline.css b/src/platform/web/ui/css/themes/element/timeline.css
index 6558c003..0a0e21cc 100644
--- a/src/platform/web/ui/css/themes/element/timeline.css
+++ b/src/platform/web/ui/css/themes/element/timeline.css
@@ -49,6 +49,21 @@ limitations under the License.
     margin-top: 4px;
 }
 
+.ReplyPreviewView .Timeline_message {
+    display: grid;
+    grid-template:
+        "body" auto
+        "body" 1fr;
+    margin-left: 0;
+    padding-left: 0;
+    padding-top: 0;
+    padding: 0;
+}
+
+.ReplyPreviewView .Timeline_message:not(.continuation) {
+    margin-top: 0;
+}
+
 @media screen and (max-width: 800px) {
     .Timeline_message {
         grid-template:

From 4df3654166a5ec65dde06e7d329f806a5aeebe79 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Mon, 20 Dec 2021 11:16:53 +0530
Subject: [PATCH 28/77] Prevent reply previews from being nested

---
 .../web/ui/session/room/timeline/TextMessageView.js    | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/src/platform/web/ui/session/room/timeline/TextMessageView.js b/src/platform/web/ui/session/room/timeline/TextMessageView.js
index 5d5a48a2..801f075e 100644
--- a/src/platform/web/ui/session/room/timeline/TextMessageView.js
+++ b/src/platform/web/ui/session/room/timeline/TextMessageView.js
@@ -26,7 +26,15 @@ export class TextMessageView extends BaseMessageView {
                 "Timeline_messageBody": true,
                 statusMessage: vm => vm.shape === "message-status",
             }
-        }, t.mapView(vm => vm.replyTextTile, replyTextTile => replyTextTile ? new ReplyPreviewView(replyTextTile) : null));
+        }, t.mapView(vm => vm.replyTextTile, replyTextTile => {
+            if (replyTextTile && this._interactive) {
+                // if this._interactive = false, this is already a reply preview, don't nest replies for now.
+                return new ReplyPreviewView(replyTextTile);
+            }
+            else {
+                return null;
+            }
+        }));
 
         t.mapSideEffect(vm => vm.body, body => {
             while (this._shouldRemove(container.lastChild)) {

From 687aa5a7e3250e0f5b9d42d390e8c4f411a0efd6 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Mon, 20 Dec 2021 11:25:26 +0530
Subject: [PATCH 29/77] Remove dead code

---
 .../web/ui/session/room/timeline/ReplyPreviewView.js  | 11 -----------
 1 file changed, 11 deletions(-)

diff --git a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
index 00767bdf..d2693238 100644
--- a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
+++ b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
@@ -27,21 +27,10 @@ export class ReplyPreviewView extends TemplateView {
         // todo: this should probably be called viewClassForTile instead
         const viewClass = viewClassForEntry(vm);
         const view = new viewClass(vm, false)
-        // const rendered = this._renderContent(t, vm, view);
         const rendered = view.render(t, vm);
         return this._renderReplyHeader(t, vm, [rendered]);
     }
 
-    _renderContent(t, vm, view) {
-        switch (vm.shape) {
-            case "image":
-            case "video":
-                return view.renderMedia(t, vm);
-            default:
-                return view.renderMessageBody(t, vm);
-        }
-    }
-
     _renderReplyHeader(t, vm, children = []) {
         return t.blockquote(
             [

From 46b69b3873422efb143cb5059158d0c4ef3663ad Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Mon, 20 Dec 2021 12:06:21 +0530
Subject: [PATCH 30/77] Render error

---
 src/domain/session/room/timeline/tiles/BaseMessageTile.js | 4 ++++
 .../web/ui/session/room/timeline/ReplyPreviewView.js      | 8 ++++++++
 .../web/ui/session/room/timeline/TextMessageView.js       | 7 +++++--
 3 files changed, 17 insertions(+), 2 deletions(-)

diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
index 6dee5fc2..b59dfa3a 100644
--- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js
+++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
@@ -91,6 +91,10 @@ export class BaseMessageTile extends SimpleTile {
         return this._entry.isUnverified;
     }
 
+    get isReply() {
+        return this._entry.isReply;
+    }
+
     _getContent() {
         return this._entry.content;
     }
diff --git a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
index d2693238..5342f24a 100644
--- a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
+++ b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
@@ -41,3 +41,11 @@ export class ReplyPreviewView extends TemplateView {
         ]);
     }
 }
+
+export class ReplyPreviewError extends TemplateView {
+    render(t) {
+        return t.blockquote({ className: "ReplyPreviewView" }, [
+            t.div({ className: "Timeline_messageBody statusMessage" }, "This reply could not be found.")
+        ]);
+    }
+}
diff --git a/src/platform/web/ui/session/room/timeline/TextMessageView.js b/src/platform/web/ui/session/room/timeline/TextMessageView.js
index 801f075e..b1a7c0f2 100644
--- a/src/platform/web/ui/session/room/timeline/TextMessageView.js
+++ b/src/platform/web/ui/session/room/timeline/TextMessageView.js
@@ -16,7 +16,7 @@ limitations under the License.
 
 import {tag, text} from "../../../general/html";
 import {BaseMessageView} from "./BaseMessageView.js";
-import {ReplyPreviewView} from "./ReplyPreviewView.js";
+import {ReplyPreviewError, ReplyPreviewView} from "./ReplyPreviewView.js";
 
 export class TextMessageView extends BaseMessageView {
     renderMessageBody(t, vm) {
@@ -27,7 +27,10 @@ export class TextMessageView extends BaseMessageView {
                 statusMessage: vm => vm.shape === "message-status",
             }
         }, t.mapView(vm => vm.replyTextTile, replyTextTile => {
-            if (replyTextTile && this._interactive) {
+            if (vm.isReply && !replyTextTile) {
+                return new ReplyPreviewError();
+            }
+            else if (replyTextTile && this._interactive) {
                 // if this._interactive = false, this is already a reply preview, don't nest replies for now.
                 return new ReplyPreviewView(replyTextTile);
             }

From dee22f712072e683362d3ed272dcfea9ae3d80ff Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Mon, 20 Dec 2021 17:21:59 +0530
Subject: [PATCH 31/77] Implement render flags

---
 src/platform/web/ui/session/room/MessageComposer.js        | 2 +-
 .../web/ui/session/room/timeline/BaseMessageView.js        | 7 ++++---
 .../web/ui/session/room/timeline/ReplyPreviewView.js       | 2 +-
 3 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/src/platform/web/ui/session/room/MessageComposer.js b/src/platform/web/ui/session/room/MessageComposer.js
index cfd389f3..bb175b7d 100644
--- a/src/platform/web/ui/session/room/MessageComposer.js
+++ b/src/platform/web/ui/session/room/MessageComposer.js
@@ -56,7 +56,7 @@ export class MessageComposer extends TemplateView {
                         className: "cancel",
                         onClick: () => this._clearReplyingTo()
                     }, "Close"),
-                    t.view(new View(rvm, false, "div"))
+                t.view(new View(rvm, { interactive: false }, "div"))
                 ])
         });
         const input = t.div({className: "MessageComposer_input"}, [
diff --git a/src/platform/web/ui/session/room/timeline/BaseMessageView.js b/src/platform/web/ui/session/room/timeline/BaseMessageView.js
index ca155785..f3f6e661 100644
--- a/src/platform/web/ui/session/room/timeline/BaseMessageView.js
+++ b/src/platform/web/ui/session/room/timeline/BaseMessageView.js
@@ -24,12 +24,13 @@ import {Menu} from "../../../general/Menu.js";
 import {ReactionsView} from "./ReactionsView.js";
 
 export class BaseMessageView extends TemplateView {
-    constructor(value, interactive = true, tagName = "li") {
+    constructor(value, renderFlags, tagName = "li") {
         super(value);
         this._menuPopup = null;
         this._tagName = tagName;
         // TODO An enum could be nice to make code easier to read at call sites.
-        this._interactive = interactive;
+        this._interactive = renderFlags?.interactive ?? true;
+        this._isReplyPreview = renderFlags?.reply;
     }
 
     render(t, vm) {
@@ -54,7 +55,7 @@ export class BaseMessageView extends TemplateView {
             if (isContinuation && wasContinuation === false) {
                 li.removeChild(li.querySelector(".Timeline_messageAvatar"));
                 li.removeChild(li.querySelector(".Timeline_messageSender"));
-            } else if (!isContinuation && this._interactive) {
+            } else if (!isContinuation && !this._isReplyPreview) {
                 const avatar = tag.a({href: vm.memberPanelLink, className: "Timeline_messageAvatar"}, [renderStaticAvatar(vm, 30)]);
                 const sender = tag.div({className: `Timeline_messageSender usercolor${vm.avatarColorNumber}`}, vm.displayName);
                 li.insertBefore(avatar, li.firstChild);
diff --git a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
index 5342f24a..8430986b 100644
--- a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
+++ b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
@@ -26,7 +26,7 @@ export class ReplyPreviewView extends TemplateView {
     _renderReplyPreview(t, vm) {
         // todo: this should probably be called viewClassForTile instead
         const viewClass = viewClassForEntry(vm);
-        const view = new viewClass(vm, false)
+        const view = new viewClass(vm, { reply: true, interactive: false });
         const rendered = view.render(t, vm);
         return this._renderReplyHeader(t, vm, [rendered]);
     }

From cba044eff13c16eda2b56378b2407138d80e2a29 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Tue, 21 Dec 2021 16:00:24 +0530
Subject: [PATCH 32/77] Remove comment

---
 src/domain/session/room/timeline/deserialize.js | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/domain/session/room/timeline/deserialize.js b/src/domain/session/room/timeline/deserialize.js
index bd362206..77eb670e 100644
--- a/src/domain/session/room/timeline/deserialize.js
+++ b/src/domain/session/room/timeline/deserialize.js
@@ -345,7 +345,6 @@ class Deserializer {
 }
 
 export function parseHTMLBody(platform, mediaRepository, html) {
-    // todo: might be better to remove mx-reply and children after parsing, need to think
     html = html.replace(/.+<\/mx-reply>/, "");
     const parseResult = platform.parseHTML(html);
     const deserializer = new Deserializer(parseResult, mediaRepository);

From 0c3f16e5f6555c389121ad76e2849a524beb1046 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Tue, 21 Dec 2021 16:08:17 +0530
Subject: [PATCH 33/77] Use 's' flag with regex if available

---
 src/domain/session/room/timeline/deserialize.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/domain/session/room/timeline/deserialize.js b/src/domain/session/room/timeline/deserialize.js
index 77eb670e..fd1e3449 100644
--- a/src/domain/session/room/timeline/deserialize.js
+++ b/src/domain/session/room/timeline/deserialize.js
@@ -345,7 +345,7 @@ class Deserializer {
 }
 
 export function parseHTMLBody(platform, mediaRepository, html) {
-    html = html.replace(/.+<\/mx-reply>/, "");
+    html = html.replace(/.+<\/mx-reply>/s, "");
     const parseResult = platform.parseHTML(html);
     const deserializer = new Deserializer(parseResult, mediaRepository);
     const parts = deserializer.parseAnyNodes(parseResult.rootNodes);

From d69059de68e09e88e03cebf5d0f851ded249f7bc Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Tue, 21 Dec 2021 16:11:06 +0530
Subject: [PATCH 34/77] Use different flag

---
 src/platform/web/ui/session/room/timeline/TextMessageView.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/platform/web/ui/session/room/timeline/TextMessageView.js b/src/platform/web/ui/session/room/timeline/TextMessageView.js
index b1a7c0f2..2ed495dd 100644
--- a/src/platform/web/ui/session/room/timeline/TextMessageView.js
+++ b/src/platform/web/ui/session/room/timeline/TextMessageView.js
@@ -30,8 +30,8 @@ export class TextMessageView extends BaseMessageView {
             if (vm.isReply && !replyTextTile) {
                 return new ReplyPreviewError();
             }
-            else if (replyTextTile && this._interactive) {
-                // if this._interactive = false, this is already a reply preview, don't nest replies for now.
+            else if (replyTextTile && !this._isReplyPreview) {
+                // if this._isReplyPreview = true, this is already a reply preview, don't nest replies for now.
                 return new ReplyPreviewView(replyTextTile);
             }
             else {

From f645065db7d54f4d427df5495f6b3c0ed0a18726 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Tue, 21 Dec 2021 16:32:58 +0530
Subject: [PATCH 35/77] Remove unused getter

---
 src/domain/session/room/timeline/tiles/SimpleTile.js | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/src/domain/session/room/timeline/tiles/SimpleTile.js b/src/domain/session/room/timeline/tiles/SimpleTile.js
index 4b00c525..3ba78548 100644
--- a/src/domain/session/room/timeline/tiles/SimpleTile.js
+++ b/src/domain/session/room/timeline/tiles/SimpleTile.js
@@ -53,10 +53,6 @@ export class SimpleTile extends ViewModel {
         return this._entry.isPending && this._entry.pendingEvent.status !== SendStatus.Sent;
     }
 
-    get isRedacted() {
-        return this._entry.isRedacted;
-    }
-
     get canAbortSending() {
         return this._entry.isPending &&
             !this._entry.pendingEvent.hasStartedSending;

From e352867f5a12b240aa985a8a36a4ebcf4007052a Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Tue, 21 Dec 2021 16:34:41 +0530
Subject: [PATCH 36/77] Remove unnecessary ctor

---
 src/domain/session/room/timeline/tiles/TextTile.js | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/src/domain/session/room/timeline/tiles/TextTile.js b/src/domain/session/room/timeline/tiles/TextTile.js
index 9105f190..4f430f5f 100644
--- a/src/domain/session/room/timeline/tiles/TextTile.js
+++ b/src/domain/session/room/timeline/tiles/TextTile.js
@@ -19,11 +19,6 @@ import {parsePlainBody} from "../MessageBody.js";
 import {parseHTMLBody} from "../deserialize.js";
 
 export class TextTile extends BaseTextTile {
-
-    constructor(options) {
-        super(options);
-    }
-
     _getContentString(key) {
         return this._getContent()?.[key] || "";
     }

From 2a124d4195cf6560f98d06909fc5176f922e9e0c Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Tue, 21 Dec 2021 17:32:18 +0530
Subject: [PATCH 37/77] simplify css

---
 src/platform/web/ui/css/themes/element/timeline.css | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/src/platform/web/ui/css/themes/element/timeline.css b/src/platform/web/ui/css/themes/element/timeline.css
index 0a0e21cc..123b2cfc 100644
--- a/src/platform/web/ui/css/themes/element/timeline.css
+++ b/src/platform/web/ui/css/themes/element/timeline.css
@@ -51,12 +51,8 @@ limitations under the License.
 
 .ReplyPreviewView .Timeline_message {
     display: grid;
-    grid-template:
-        "body" auto
-        "body" 1fr;
+    grid-template: "body" auto;
     margin-left: 0;
-    padding-left: 0;
-    padding-top: 0;
     padding: 0;
 }
 

From c34d574385dd2431c881511bd9fffd3a7bd453c7 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Tue, 21 Dec 2021 17:54:58 +0530
Subject: [PATCH 38/77] No need to export renderPart

---
 src/platform/web/ui/session/room/timeline/TextMessageView.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/platform/web/ui/session/room/timeline/TextMessageView.js b/src/platform/web/ui/session/room/timeline/TextMessageView.js
index 2ed495dd..574016cf 100644
--- a/src/platform/web/ui/session/room/timeline/TextMessageView.js
+++ b/src/platform/web/ui/session/room/timeline/TextMessageView.js
@@ -122,7 +122,7 @@ const formatFunction = {
     newline: () => tag.br()
 };
 
-export function renderPart(part) {
+function renderPart(part) {
     const f = formatFunction[part.type];
     if (!f) {
         return text(`[unknown part type ${part.type}]`);

From 0ae3c60d6de9c4e66320dde4f8007a8e5b9d040f Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Thu, 6 Jan 2022 12:46:37 +0530
Subject: [PATCH 39/77] Remove .js file from rebase

---
 src/matrix/net/HomeServerApi.js | 273 --------------------------------
 1 file changed, 273 deletions(-)
 delete mode 100644 src/matrix/net/HomeServerApi.js

diff --git a/src/matrix/net/HomeServerApi.js b/src/matrix/net/HomeServerApi.js
deleted file mode 100644
index 4d729cbf..00000000
--- a/src/matrix/net/HomeServerApi.js
+++ /dev/null
@@ -1,273 +0,0 @@
-/*
-Copyright 2020 Bruno Windels 
-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 {encodeQueryParams, encodeBody} from "./common.js";
-import {HomeServerRequest} from "./HomeServerRequest.js";
-
-const CS_R0_PREFIX = "/_matrix/client/r0";
-const DEHYDRATION_PREFIX = "/_matrix/client/unstable/org.matrix.msc2697.v2";
-
-export class HomeServerApi {
-    constructor({homeserver, accessToken, request, reconnector}) {
-        // store these both in a closure somehow so it's harder to get at in case of XSS?
-        // one could change the homeserver as well so the token gets sent there, so both must be protected from read/write
-        this._homeserver = homeserver;
-        this._accessToken = accessToken;
-        this._requestFn = request;
-        this._reconnector = reconnector;
-    }
-
-    _url(csPath, prefix = CS_R0_PREFIX) {
-        return this._homeserver + prefix + csPath;
-    }
-
-    _baseRequest(method, url, queryParams, body, options, accessToken) {
-        const queryString = encodeQueryParams(queryParams);
-        url = `${url}?${queryString}`;
-        let log;
-        if (options?.log) {
-            const parent = options?.log;
-            log = parent.child({
-                t: "network",
-                url,
-                method,
-            }, parent.level.Info);
-        }
-        let encodedBody;
-        const headers = new Map();
-        if (accessToken) {
-            headers.set("Authorization", `Bearer ${accessToken}`);
-        }
-        headers.set("Accept", "application/json");
-        if (body) {
-            const encoded = encodeBody(body);
-            headers.set("Content-Type", encoded.mimeType);
-            headers.set("Content-Length", encoded.length);
-            encodedBody = encoded.body;
-        }
-
-        const requestResult = this._requestFn(url, {
-            method,
-            headers,
-            body: encodedBody,
-            timeout: options?.timeout,
-            uploadProgress: options?.uploadProgress,
-            format: "json"  // response format
-        });
-
-        const hsRequest = new HomeServerRequest(method, url, requestResult, log);
-        
-        if (this._reconnector) {
-            hsRequest.response().catch(err => {
-                // Some endpoints such as /sync legitimately time-out
-                // (which is also reported as a ConnectionError) and will re-attempt,
-                // but spinning up the reconnector in this case is ok,
-                // as all code ran on session and sync start should be reentrant
-                if (err.name === "ConnectionError") {
-                    this._reconnector.onRequestFailed(this);
-                }
-            });
-        }
-
-        return hsRequest;
-    }
-
-    _unauthedRequest(method, url, queryParams, body, options) {
-        return this._baseRequest(method, url, queryParams, body, options, null);
-    }
-
-    _authedRequest(method, url, queryParams, body, options) {
-        return this._baseRequest(method, url, queryParams, body, options, this._accessToken);
-    }
-
-    _post(csPath, queryParams, body, options) {
-        return this._authedRequest("POST", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options);
-    }
-
-    _put(csPath, queryParams, body, options) {
-        return this._authedRequest("PUT", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options);
-    }
-
-    _get(csPath, queryParams, body, options) {
-        return this._authedRequest("GET", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options);
-    }
-
-    sync(since, filter, timeout, options = null) {
-        return this._get("/sync", {since, timeout, filter}, null, options);
-    }
-
-    context(roomId, eventId, limit, filter) {
-        return this._get(`/rooms/${encodeURIComponent(roomId)}/context/${encodeURIComponent(eventId)}`, {filter, limit});
-    }
-
-    // params is from, dir and optionally to, limit, filter.
-    messages(roomId, params, options = null) {
-        return this._get(`/rooms/${encodeURIComponent(roomId)}/messages`, params, null, options);
-    }
-
-    // params is at, membership and not_membership
-    members(roomId, params, options = null) {
-        return this._get(`/rooms/${encodeURIComponent(roomId)}/members`, params, null, options);
-    }
-
-    send(roomId, eventType, txnId, content, options = null) {
-        return this._put(`/rooms/${encodeURIComponent(roomId)}/send/${encodeURIComponent(eventType)}/${encodeURIComponent(txnId)}`, {}, content, options);
-    }
-
-    redact(roomId, eventId, txnId, content, options = null) {
-        return this._put(`/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${encodeURIComponent(txnId)}`, {}, content, options);
-    }
-
-    receipt(roomId, receiptType, eventId, options = null) {
-        return this._post(`/rooms/${encodeURIComponent(roomId)}/receipt/${encodeURIComponent(receiptType)}/${encodeURIComponent(eventId)}`,
-            {}, {}, options);
-    }
-
-    state(roomId, eventType, stateKey, options = null) {
-        return this._get(`/rooms/${encodeURIComponent(roomId)}/state/${encodeURIComponent(eventType)}/${encodeURIComponent(stateKey)}`, {}, null, options);
-    }
-
-    getLoginFlows() {
-        return this._unauthedRequest("GET", this._url("/login"), null, null, null);
-    }
-
-    passwordLogin(username, password, initialDeviceDisplayName, options = null) {
-        return this._unauthedRequest("POST", this._url("/login"), null, {
-          "type": "m.login.password",
-          "identifier": {
-            "type": "m.id.user",
-            "user": username
-          },
-          "password": password,
-          "initial_device_display_name": initialDeviceDisplayName
-        }, options);
-    }
-
-    tokenLogin(loginToken, txnId, initialDeviceDisplayName, options = null) {
-        return this._unauthedRequest("POST", this._url("/login"), null, {
-          "type": "m.login.token",
-          "identifier": {
-            "type": "m.id.user",
-          },
-          "token": loginToken,
-          "txn_id": txnId,
-          "initial_device_display_name": initialDeviceDisplayName
-        }, options);
-    }
-
-    createFilter(userId, filter, options = null) {
-        return this._post(`/user/${encodeURIComponent(userId)}/filter`, null, filter, options);
-    }
-
-    versions(options = null) {
-        return this._unauthedRequest("GET", `${this._homeserver}/_matrix/client/versions`, null, null, options);
-    }
-
-    uploadKeys(dehydratedDeviceId, payload, options = null) {
-        let path = "/keys/upload";
-        if (dehydratedDeviceId) {
-            path = path + `/${encodeURIComponent(dehydratedDeviceId)}`;
-        }
-        return this._post(path, null, payload, options);
-    }
-
-    queryKeys(queryRequest, options = null) {
-        return this._post("/keys/query", null, queryRequest, options);
-    }
-
-    claimKeys(payload, options = null) {
-        return this._post("/keys/claim", null, payload, options);
-    }
-
-    sendToDevice(type, payload, txnId, options = null) {
-        return this._put(`/sendToDevice/${encodeURIComponent(type)}/${encodeURIComponent(txnId)}`, null, payload, options);
-    }
-    
-    roomKeysVersion(version = null, options = null) {
-        let versionPart = "";
-        if (version) {
-            versionPart = `/${encodeURIComponent(version)}`;
-        }
-        return this._get(`/room_keys/version${versionPart}`, null, null, options);
-    }
-
-    roomKeyForRoomAndSession(version, roomId, sessionId, options = null) {
-        return this._get(`/room_keys/keys/${encodeURIComponent(roomId)}/${encodeURIComponent(sessionId)}`, {version}, null, options);
-    }
-
-    uploadAttachment(blob, filename, options = null) {
-        return this._authedRequest("POST", `${this._homeserver}/_matrix/media/r0/upload`, {filename}, blob, options);
-    }
-
-    setPusher(pusher, options = null) {
-        return this._post("/pushers/set", null, pusher, options);
-    }
-
-    getPushers(options = null) {
-        return this._get("/pushers", null, null, options);
-    }
-
-    join(roomId, options = null) {
-        return this._post(`/rooms/${encodeURIComponent(roomId)}/join`, null, null, options);
-    }
-
-    joinIdOrAlias(roomIdOrAlias, options = null) {
-        return this._post(`/join/${encodeURIComponent(roomIdOrAlias)}`, null, null, options);
-    }
-
-    leave(roomId, options = null) {
-        return this._post(`/rooms/${encodeURIComponent(roomId)}/leave`, null, null, options);
-    }
-
-    forget(roomId, options = null) {
-        return this._post(`/rooms/${encodeURIComponent(roomId)}/forget`, null, null, options);
-    }
-
-    logout(options = null) {
-        return this._post(`/logout`, null, null, options);
-    }
-
-    getDehydratedDevice(options = {}) {
-        options.prefix = DEHYDRATION_PREFIX;
-        return this._get(`/dehydrated_device`, null, null, options);
-    }
-
-    createDehydratedDevice(payload, options = {}) {
-        options.prefix = DEHYDRATION_PREFIX;
-        return this._put(`/dehydrated_device`, null, payload, options);
-    }
-
-    claimDehydratedDevice(deviceId, options = {}) {
-        options.prefix = DEHYDRATION_PREFIX;
-        return this._post(`/dehydrated_device/claim`, null, {device_id: deviceId}, options);
-    }
-}
-
-import {Request as MockRequest} from "../../mocks/Request.js";
-
-export function tests() {
-    return {
-        "superficial happy path for GET": async assert => {
-            const hsApi = new HomeServerApi({
-                request: () => new MockRequest().respond(200, 42),
-                homeserver: "https://hs.tld"
-            });
-            const result = await hsApi._get("foo", null, null, null).response();
-            assert.strictEqual(result, 42);
-        }
-    }
-}

From 88f9ad09a2ae8016757395112c35d7051ab7e6d2 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Thu, 6 Jan 2022 17:19:05 +0530
Subject: [PATCH 40/77] Move method as local function

---
 .../web/ui/session/room/timeline/TextMessageView.js        | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/src/platform/web/ui/session/room/timeline/TextMessageView.js b/src/platform/web/ui/session/room/timeline/TextMessageView.js
index 574016cf..0d9ea83b 100644
--- a/src/platform/web/ui/session/room/timeline/TextMessageView.js
+++ b/src/platform/web/ui/session/room/timeline/TextMessageView.js
@@ -39,8 +39,10 @@ export class TextMessageView extends BaseMessageView {
             }
         }));
 
+        const shouldRemove = (element) => element && element.className !== "ReplyPreviewView" && element.nodeName !== "#comment";
+
         t.mapSideEffect(vm => vm.body, body => {
-            while (this._shouldRemove(container.lastChild)) {
+            while (shouldRemove(container.lastChild)) {
                 container.removeChild(container.lastChild);
             }
             for (const part of body.parts) {
@@ -52,9 +54,6 @@ export class TextMessageView extends BaseMessageView {
         return container;
     }
 
-    _shouldRemove(element) {
-        return element && element.className !== "ReplyPreviewView" && element.nodeName !== "#comment";
-    }
 
 }
 

From cfefe6962a3f424eeb96905165bf799176822d4d Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Thu, 6 Jan 2022 17:20:32 +0530
Subject: [PATCH 41/77] Remove stray space

---
 src/matrix/room/timeline/Timeline.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js
index e018fa24..ef3c3fba 100644
--- a/src/matrix/room/timeline/Timeline.js
+++ b/src/matrix/room/timeline/Timeline.js
@@ -224,7 +224,7 @@ export class Timeline {
     }
 
     /** @package */
-     replaceEntries(entries) {
+    replaceEntries(entries) {
         this._addLocalRelationsToNewRemoteEntries(entries);
         for (const entry of entries) {
             try {

From 27a9f5dd027b7dbe9f4204edfe223f7bdebc61ac Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Thu, 6 Jan 2022 17:24:58 +0530
Subject: [PATCH 42/77] Use DOMPurify to remove mx-reply

---
 src/domain/session/room/timeline/deserialize.js | 1 -
 src/platform/web/parsehtml.js                   | 3 ++-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/domain/session/room/timeline/deserialize.js b/src/domain/session/room/timeline/deserialize.js
index fd1e3449..6003aef9 100644
--- a/src/domain/session/room/timeline/deserialize.js
+++ b/src/domain/session/room/timeline/deserialize.js
@@ -345,7 +345,6 @@ class Deserializer {
 }
 
 export function parseHTMLBody(platform, mediaRepository, html) {
-    html = html.replace(/.+<\/mx-reply>/s, "");
     const parseResult = platform.parseHTML(html);
     const deserializer = new Deserializer(parseResult, mediaRepository);
     const parts = deserializer.parseAnyNodes(parseResult.rootNodes);
diff --git a/src/platform/web/parsehtml.js b/src/platform/web/parsehtml.js
index ec30b2c9..21c8f39a 100644
--- a/src/platform/web/parsehtml.js
+++ b/src/platform/web/parsehtml.js
@@ -56,7 +56,8 @@ class HTMLParseResult {
 
 const sanitizeConfig = {
     ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp|xxx|mxc):|[^a-z]|[a-z+.-]+(?:[^a-z+.-:]|$))/i,
-    ADD_TAGS: ['mx-reply']
+    FORBID_TAGS: ['mx-reply'],
+    KEEP_CONTENT: false,
 }
 
 export function parseHTML(html) {

From af5a008d0f88549e241b562171d1a6dfab9496ea Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Thu, 6 Jan 2022 17:51:14 +0530
Subject: [PATCH 43/77] Move links to vm

---
 src/domain/session/room/timeline/tiles/BaseMessageTile.js | 8 ++++----
 .../web/ui/session/room/timeline/ReplyPreviewView.js      | 4 ++--
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
index b59dfa3a..ef0c2c39 100644
--- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js
+++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
@@ -34,12 +34,12 @@ export class BaseMessageTile extends SimpleTile {
         return this._room.mediaRepository;
     }
 
-    get roomId() {
-        return this._room.id;
+    get permaLink() {
+        return `https://matrix.to/#/${this._room.id}/${this._entry.id}`;
     }
 
-    get eventId() {
-        return this._entry.id;
+    get senderProfileLink() {
+        return `https://matrix.to/#/${this.sender}`;
     }
 
     get displayName() {
diff --git a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
index 8430986b..b38ff92b 100644
--- a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
+++ b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
@@ -34,8 +34,8 @@ export class ReplyPreviewView extends TemplateView {
     _renderReplyHeader(t, vm, children = []) {
         return t.blockquote(
             [
-            t.a({ className: "link", href: `https://matrix.to/#/${vm.roomId}/${vm.eventId}` }, "In reply to"),
-            t.a({ className: "pill", href: `https://matrix.to/#/${vm.sender}` }, [renderStaticAvatar(vm, 12, undefined, true), vm.displayName]),
+            t.a({ className: "link", href: vm.permaLink }, "In reply to"),
+            t.a({ className: "pill", href: vm.senderProfileLink }, [renderStaticAvatar(vm, 12, undefined, true), vm.displayName]),
             t.br(),
             ...children
         ]);

From e99cd41ed0deffe40d9206f1138bbc097a284972 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Thu, 6 Jan 2022 22:01:41 +0530
Subject: [PATCH 44/77] Change check

---
 src/platform/web/ui/session/room/timeline/TextMessageView.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/platform/web/ui/session/room/timeline/TextMessageView.js b/src/platform/web/ui/session/room/timeline/TextMessageView.js
index 0d9ea83b..489294c3 100644
--- a/src/platform/web/ui/session/room/timeline/TextMessageView.js
+++ b/src/platform/web/ui/session/room/timeline/TextMessageView.js
@@ -39,7 +39,7 @@ export class TextMessageView extends BaseMessageView {
             }
         }));
 
-        const shouldRemove = (element) => element && element.className !== "ReplyPreviewView" && element.nodeName !== "#comment";
+        const shouldRemove = (element) => element?.nodeType === Node.ELEMENT_NODE && element.className !== "ReplyPreviewView";
 
         t.mapSideEffect(vm => vm.body, body => {
             while (shouldRemove(container.lastChild)) {

From fee6447e220660f663c6b44ea2ee1d6073902762 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Thu, 6 Jan 2022 22:03:47 +0530
Subject: [PATCH 45/77] Don't call render()

---
 src/platform/web/ui/session/room/timeline/ReplyPreviewView.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
index b38ff92b..656d98f2 100644
--- a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
+++ b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
@@ -27,7 +27,7 @@ export class ReplyPreviewView extends TemplateView {
         // todo: this should probably be called viewClassForTile instead
         const viewClass = viewClassForEntry(vm);
         const view = new viewClass(vm, { reply: true, interactive: false });
-        const rendered = view.render(t, vm);
+        const rendered = t.view(view);
         return this._renderReplyHeader(t, vm, [rendered]);
     }
 

From b134fa7409f807d68635cacbac0ca94750214cf3 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Thu, 6 Jan 2022 22:07:17 +0530
Subject: [PATCH 46/77] Format swtich case properly

---
 src/platform/web/ui/session/room/common.ts | 18 ++++++++++++------
 1 file changed, 12 insertions(+), 6 deletions(-)

diff --git a/src/platform/web/ui/session/room/common.ts b/src/platform/web/ui/session/room/common.ts
index 764e2b6e..1018939d 100644
--- a/src/platform/web/ui/session/room/common.ts
+++ b/src/platform/web/ui/session/room/common.ts
@@ -28,15 +28,21 @@ import type {TileView} from "./TimelineView";
 type TileViewConstructor = (this: TileView, SimpleTile) => void;
 export function viewClassForEntry(entry: SimpleTile): TileViewConstructor | undefined {
     switch (entry.shape) {
-        case "gap": return GapView;
-        case "announcement": return AnnouncementView;
+        case "gap":
+            return GapView;
+        case "announcement":
+            return AnnouncementView;
         case "message":
         case "message-status":
             return TextMessageView;
-        case "image": return ImageView;
-        case "video": return VideoView;
-        case "file": return FileView;
-        case "missing-attachment": return MissingAttachmentView;
+        case "image":
+            return ImageView;
+        case "video":
+            return VideoView;
+        case "file":
+            return FileView;
+        case "missing-attachment":
+            return MissingAttachmentView;
         case "redacted":
             return RedactedView;
     }

From 273c44424f34e310877a1469d1e8ddfa41cd89f7 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Thu, 6 Jan 2022 22:17:31 +0530
Subject: [PATCH 47/77] Throw if viewClass returns undefined

---
 src/platform/web/ui/session/room/timeline/ReplyPreviewView.js | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
index 656d98f2..af8239d6 100644
--- a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
+++ b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
@@ -24,9 +24,11 @@ export class ReplyPreviewView extends TemplateView {
     }
 
     _renderReplyPreview(t, vm) {
-        // todo: this should probably be called viewClassForTile instead
         const viewClass = viewClassForEntry(vm);
         const view = new viewClass(vm, { reply: true, interactive: false });
+        if (!view) {
+            throw new Error(`Shape ${vm.shape} is unrecognized.`)
+        }
         const rendered = t.view(view);
         return this._renderReplyHeader(t, vm, [rendered]);
     }

From 086e0c03203f206c87f783d889195f06db10cd53 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Thu, 6 Jan 2022 22:34:35 +0530
Subject: [PATCH 48/77] Inline methods

---
 .../session/room/timeline/ReplyPreviewView.js | 32 ++++++++-----------
 1 file changed, 14 insertions(+), 18 deletions(-)

diff --git a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
index af8239d6..3c52fc71 100644
--- a/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
+++ b/src/platform/web/ui/session/room/timeline/ReplyPreviewView.js
@@ -20,27 +20,23 @@ import {viewClassForEntry} from "../common";
 
 export class ReplyPreviewView extends TemplateView {
     render(t, vm) {
-        return t.div({ className: "ReplyPreviewView" }, this._renderReplyPreview(t, vm));
-    }
-
-    _renderReplyPreview(t, vm) {
         const viewClass = viewClassForEntry(vm);
-        const view = new viewClass(vm, { reply: true, interactive: false });
-        if (!view) {
+        if (!viewClass) {
             throw new Error(`Shape ${vm.shape} is unrecognized.`)
         }
-        const rendered = t.view(view);
-        return this._renderReplyHeader(t, vm, [rendered]);
-    }
-
-    _renderReplyHeader(t, vm, children = []) {
-        return t.blockquote(
-            [
-            t.a({ className: "link", href: vm.permaLink }, "In reply to"),
-            t.a({ className: "pill", href: vm.senderProfileLink }, [renderStaticAvatar(vm, 12, undefined, true), vm.displayName]),
-            t.br(),
-            ...children
-        ]);
+        const view = new viewClass(vm, { reply: true, interactive: false });
+        return t.div(
+            { className: "ReplyPreviewView" },
+            t.blockquote([
+                t.a({ className: "link", href: vm.permaLink }, "In reply to"),
+                t.a({ className: "pill", href: vm.senderProfileLink }, [
+                    renderStaticAvatar(vm, 12, undefined, true),
+                    vm.displayName,
+                ]),
+                t.br(),
+                t.view(view),
+            ])
+        );
     }
 }
 

From 7f916532088cc1f584d64c8012263d1d9db0f657 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Thu, 6 Jan 2022 22:38:37 +0530
Subject: [PATCH 49/77] Rename replyTextTile -> replyTile

---
 .../session/room/timeline/tiles/BaseMessageTile.js     | 10 +++++-----
 src/domain/session/room/timeline/tiles/SimpleTile.js   |  4 ++--
 .../web/ui/session/room/timeline/TextMessageView.js    |  8 ++++----
 3 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
index ef0c2c39..174cdd0a 100644
--- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js
+++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
@@ -24,7 +24,7 @@ export class BaseMessageTile extends SimpleTile {
         this._date = this._entry.timestamp ? new Date(this._entry.timestamp) : null;
         this._isContinuation = false;
         this._reactions = null;
-        this._replyTextTile = null;
+        this._replyTile = null;
         if (this._entry.annotations || this._entry.pendingAnnotations) {
             this._updateReactions();
         }
@@ -216,16 +216,16 @@ export class BaseMessageTile extends SimpleTile {
         }
     }
 
-    get replyTextTile() {
+    get replyTile() {
         if (!this._entry.contextEventId) {
             return null;
         }
-        if (!this._replyTextTile) {
+        if (!this._replyTile) {
             const entry = this._entry.contextEntry;
             if (entry) {
-                this._replyTextTile = this._tileCreator(entry);
+                this._replyTile = this._tileCreator(entry);
             }
         }
-        return this._replyTextTile;
+        return this._replyTile;
     }
 }
diff --git a/src/domain/session/room/timeline/tiles/SimpleTile.js b/src/domain/session/room/timeline/tiles/SimpleTile.js
index 3ba78548..4174413a 100644
--- a/src/domain/session/room/timeline/tiles/SimpleTile.js
+++ b/src/domain/session/room/timeline/tiles/SimpleTile.js
@@ -97,9 +97,9 @@ export class SimpleTile extends ViewModel {
         const replyEntry = param?.reply ?? entry.contextEntry;
         if (replyEntry) {
             // this is an update to contextEntry used for replyPreview
-            const action = this._replyTextTile?.updateEntry(replyEntry);
+            const action = this._replyTile?.updateEntry(replyEntry);
             if (action?.shouldReplace) {
-                this._replyTextTile = this._tileCreator(replyEntry);
+                this._replyTile = this._tileCreator(replyEntry);
             }
         }
         const renderedAsRedacted = this.shape === "redacted";
diff --git a/src/platform/web/ui/session/room/timeline/TextMessageView.js b/src/platform/web/ui/session/room/timeline/TextMessageView.js
index 489294c3..fe34f850 100644
--- a/src/platform/web/ui/session/room/timeline/TextMessageView.js
+++ b/src/platform/web/ui/session/room/timeline/TextMessageView.js
@@ -26,13 +26,13 @@ export class TextMessageView extends BaseMessageView {
                 "Timeline_messageBody": true,
                 statusMessage: vm => vm.shape === "message-status",
             }
-        }, t.mapView(vm => vm.replyTextTile, replyTextTile => {
-            if (vm.isReply && !replyTextTile) {
+        }, t.mapView(vm => vm.replyTile, replyTile => {
+            if (vm.isReply && !replyTile) {
                 return new ReplyPreviewError();
             }
-            else if (replyTextTile && !this._isReplyPreview) {
+            else if (replyTile && !this._isReplyPreview) {
                 // if this._isReplyPreview = true, this is already a reply preview, don't nest replies for now.
-                return new ReplyPreviewView(replyTextTile);
+                return new ReplyPreviewView(replyTile);
             }
             else {
                 return null;

From f9f7f6cc6fa089d12096732afafc12ecde68c5d8 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Fri, 7 Jan 2022 19:47:11 +0530
Subject: [PATCH 50/77] Fix test

---
 src/domain/session/room/timeline/deserialize.js | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/domain/session/room/timeline/deserialize.js b/src/domain/session/room/timeline/deserialize.js
index 6003aef9..b59c2e59 100644
--- a/src/domain/session/room/timeline/deserialize.js
+++ b/src/domain/session/room/timeline/deserialize.js
@@ -501,13 +501,12 @@ export async function tests() {
         },
         "Reply fallback is always stripped": assert => {
             const input = 'Hello, World!';
-            const strippedInput = 'Hello, !';
             const output = [
                 new TextPart('Hello, '),
                 new FormatPart("em", []),
                 new TextPart('!'),
             ];
-            assert.deepEqual(parseHTMLBody(platform, null, input), new MessageBody(strippedInput, output));
+            assert.deepEqual(parseHTMLBody(platform, null, input), new MessageBody(input, output));
         }
         /* Doesnt work: HTML library doesn't handle 
 properly.
         "Text with code block": assert => {

From 28a534ee490fe7086b22b968ed58d1ca758e1409 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Mon, 10 Jan 2022 18:32:03 +0530
Subject: [PATCH 51/77] Fix reply nesting

---
 .../web/ui/session/room/timeline/TextMessageView.js      | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/src/platform/web/ui/session/room/timeline/TextMessageView.js b/src/platform/web/ui/session/room/timeline/TextMessageView.js
index fe34f850..c8651224 100644
--- a/src/platform/web/ui/session/room/timeline/TextMessageView.js
+++ b/src/platform/web/ui/session/room/timeline/TextMessageView.js
@@ -27,11 +27,14 @@ export class TextMessageView extends BaseMessageView {
                 statusMessage: vm => vm.shape === "message-status",
             }
         }, t.mapView(vm => vm.replyTile, replyTile => {
-            if (vm.isReply && !replyTile) {
+            if (this._isReplyPreview) {
+                // if this._isReplyPreview = true, this is already a reply preview, don't nest replies for now.
+                return null;
+            }
+            else if (vm.isReply && !replyTile) {
                 return new ReplyPreviewError();
             }
-            else if (replyTile && !this._isReplyPreview) {
-                // if this._isReplyPreview = true, this is already a reply preview, don't nest replies for now.
+            else if (replyTile) {
                 return new ReplyPreviewView(replyTile);
             }
             else {

From 455b747a1cfd45a56bc602ceb5bb0b42804f5150 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Tue, 11 Jan 2022 11:49:55 +0530
Subject: [PATCH 52/77] Don't check param for reply

---
 src/domain/session/room/timeline/tiles/SimpleTile.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/domain/session/room/timeline/tiles/SimpleTile.js b/src/domain/session/room/timeline/tiles/SimpleTile.js
index 4174413a..19b1d0eb 100644
--- a/src/domain/session/room/timeline/tiles/SimpleTile.js
+++ b/src/domain/session/room/timeline/tiles/SimpleTile.js
@@ -94,7 +94,7 @@ export class SimpleTile extends ViewModel {
 
     // update received for already included (falls within sort keys) entry
     updateEntry(entry, param) {
-        const replyEntry = param?.reply ?? entry.contextEntry;
+        const replyEntry = entry.contextEntry;
         if (replyEntry) {
             // this is an update to contextEntry used for replyPreview
             const action = this._replyTile?.updateEntry(replyEntry);

From 951af49e043876ddf1636907a9b38d02dad22ce2 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Thu, 13 Jan 2022 14:38:45 +0530
Subject: [PATCH 53/77] Emit change on reply tile

---
 src/domain/session/room/timeline/tiles/SimpleTile.js | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/domain/session/room/timeline/tiles/SimpleTile.js b/src/domain/session/room/timeline/tiles/SimpleTile.js
index 19b1d0eb..54740d59 100644
--- a/src/domain/session/room/timeline/tiles/SimpleTile.js
+++ b/src/domain/session/room/timeline/tiles/SimpleTile.js
@@ -101,6 +101,9 @@ export class SimpleTile extends ViewModel {
             if (action?.shouldReplace) {
                 this._replyTile = this._tileCreator(replyEntry);
             }
+            else {
+                this._replyTile?.emitChange();
+            }
         }
         const renderedAsRedacted = this.shape === "redacted";
         if (!entry.isGap && entry.isRedacted !== renderedAsRedacted) {

From ef5a377bc63607ef892beaf0be41da7e88bc4c56 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Thu, 13 Jan 2022 18:30:58 +0530
Subject: [PATCH 54/77] Hide reply option on pending tile

---
 src/platform/web/ui/session/room/timeline/BaseMessageView.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/platform/web/ui/session/room/timeline/BaseMessageView.js b/src/platform/web/ui/session/room/timeline/BaseMessageView.js
index f3f6e661..cf9742aa 100644
--- a/src/platform/web/ui/session/room/timeline/BaseMessageView.js
+++ b/src/platform/web/ui/session/room/timeline/BaseMessageView.js
@@ -105,7 +105,7 @@ export class BaseMessageView extends TemplateView {
 
     createMenuOptions(vm) {
         const options = [];
-        if (vm.canReact && vm.shape !== "redacted") {
+        if (vm.canReact && vm.shape !== "redacted" && !vm.isPending) {
             options.push(new QuickReactionsMenuOption(vm));
             options.push(Menu.option(vm.i18n`Reply`, () => vm.startReply()));
         }

From a77b9d9027539587695fb0d0e3fbd6bdcf722608 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Thu, 13 Jan 2022 20:03:44 +0530
Subject: [PATCH 55/77] Move update logic to BaseMessageTile

---
 .../session/room/timeline/tiles/BaseMessageTile.js    | 11 +++++++++++
 src/domain/session/room/timeline/tiles/SimpleTile.js  | 11 -----------
 2 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
index 174cdd0a..a594c722 100644
--- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js
+++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
@@ -116,6 +116,17 @@ export class BaseMessageTile extends SimpleTile {
     }
 
     updateEntry(entry, param) {
+        const replyEntry = entry.contextEntry;
+        if (replyEntry) {
+            // this is an update to contextEntry used for replyPreview
+            const action = this._replyTile?.updateEntry(replyEntry);
+            if (action?.shouldReplace) {
+                this._replyTile = this._tileCreator(replyEntry);
+            }
+            else {
+                this._replyTile?.emitChange();
+            }
+        }
         const action = super.updateEntry(entry, param);
         if (action.shouldUpdate) {
             this._updateReactions();
diff --git a/src/domain/session/room/timeline/tiles/SimpleTile.js b/src/domain/session/room/timeline/tiles/SimpleTile.js
index 54740d59..bed79012 100644
--- a/src/domain/session/room/timeline/tiles/SimpleTile.js
+++ b/src/domain/session/room/timeline/tiles/SimpleTile.js
@@ -94,17 +94,6 @@ export class SimpleTile extends ViewModel {
 
     // update received for already included (falls within sort keys) entry
     updateEntry(entry, param) {
-        const replyEntry = entry.contextEntry;
-        if (replyEntry) {
-            // this is an update to contextEntry used for replyPreview
-            const action = this._replyTile?.updateEntry(replyEntry);
-            if (action?.shouldReplace) {
-                this._replyTile = this._tileCreator(replyEntry);
-            }
-            else {
-                this._replyTile?.emitChange();
-            }
-        }
         const renderedAsRedacted = this.shape === "redacted";
         if (!entry.isGap && entry.isRedacted !== renderedAsRedacted) {
             // recreate the tile if the entry becomes redacted

From 58dd25b58dad707232081259580cfc9e3bfd8c35 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Thu, 13 Jan 2022 20:13:00 +0530
Subject: [PATCH 56/77] track reply-tile

---
 src/domain/session/room/timeline/tiles/BaseMessageTile.js | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
index a594c722..0691fbe7 100644
--- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js
+++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
@@ -121,6 +121,7 @@ export class BaseMessageTile extends SimpleTile {
             // this is an update to contextEntry used for replyPreview
             const action = this._replyTile?.updateEntry(replyEntry);
             if (action?.shouldReplace) {
+                this.disposeTracked(this._replyTile);
                 this._replyTile = this._tileCreator(replyEntry);
             }
             else {
@@ -234,7 +235,7 @@ export class BaseMessageTile extends SimpleTile {
         if (!this._replyTile) {
             const entry = this._entry.contextEntry;
             if (entry) {
-                this._replyTile = this._tileCreator(entry);
+                this._replyTile = this.track(this._tileCreator(entry));
             }
         }
         return this._replyTile;

From 846e637716efcfa184b1c3faf3413e85355080ba Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Thu, 13 Jan 2022 20:15:25 +0530
Subject: [PATCH 57/77] Remove stray newline

---
 src/domain/session/room/timeline/tiles/TextTile.js | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/domain/session/room/timeline/tiles/TextTile.js b/src/domain/session/room/timeline/tiles/TextTile.js
index 4f430f5f..fd410af5 100644
--- a/src/domain/session/room/timeline/tiles/TextTile.js
+++ b/src/domain/session/room/timeline/tiles/TextTile.js
@@ -59,5 +59,4 @@ export class TextTile extends BaseTextTile {
         }
         return messageBody;
     }
-
 }

From e1b9b1161deda9da7e9159f1c150a62a03a50438 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Thu, 13 Jan 2022 20:35:26 +0530
Subject: [PATCH 58/77] Split ifs and remove ?. abuse

---
 src/domain/session/room/timeline/tiles/BaseMessageTile.js | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
index 0691fbe7..ddd66fdf 100644
--- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js
+++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
@@ -117,15 +117,15 @@ export class BaseMessageTile extends SimpleTile {
 
     updateEntry(entry, param) {
         const replyEntry = entry.contextEntry;
-        if (replyEntry) {
+        if (replyEntry && this._replyTile) {
             // this is an update to contextEntry used for replyPreview
-            const action = this._replyTile?.updateEntry(replyEntry);
+            const action = this._replyTile.updateEntry(replyEntry);
             if (action?.shouldReplace) {
                 this.disposeTracked(this._replyTile);
                 this._replyTile = this._tileCreator(replyEntry);
             }
-            else {
-                this._replyTile?.emitChange();
+            if(action?.shouldUpdate) {
+                this._replyTile.emitChange();
             }
         }
         const action = super.updateEntry(entry, param);

From d639e169ecff5554963414baba7826f0fc893db4 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Thu, 13 Jan 2022 20:41:18 +0530
Subject: [PATCH 59/77] Move tileCreator to BaseMessageTile

---
 src/domain/session/room/timeline/tiles/BaseMessageTile.js | 1 +
 src/domain/session/room/timeline/tiles/SimpleTile.js      | 1 -
 2 files changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
index ddd66fdf..bd7472e9 100644
--- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js
+++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
@@ -21,6 +21,7 @@ import {getIdentifierColorNumber, avatarInitials, getAvatarHttpUrl} from "../../
 export class BaseMessageTile extends SimpleTile {
     constructor(options) {
         super(options);
+        this._tileCreator = options.tileCreator;
         this._date = this._entry.timestamp ? new Date(this._entry.timestamp) : null;
         this._isContinuation = false;
         this._reactions = null;
diff --git a/src/domain/session/room/timeline/tiles/SimpleTile.js b/src/domain/session/room/timeline/tiles/SimpleTile.js
index bed79012..4c1c1de0 100644
--- a/src/domain/session/room/timeline/tiles/SimpleTile.js
+++ b/src/domain/session/room/timeline/tiles/SimpleTile.js
@@ -21,7 +21,6 @@ import {SendStatus} from "../../../../../matrix/room/sending/PendingEvent.js";
 export class SimpleTile extends ViewModel {
     constructor(options) {
         super(options);
-        this._tileCreator = options.tileCreator;
         this._entry = options.entry;
     }
     // view model props for all subclasses

From 51215fda16a931a438d3995c675000b91f3f6e05 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Thu, 13 Jan 2022 20:43:51 +0530
Subject: [PATCH 60/77] Rename tileCreator -> tilesCreator

---
 src/domain/session/room/timeline/tiles/BaseMessageTile.js | 6 +++---
 src/domain/session/room/timeline/tilesCreator.js          | 2 +-
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
index bd7472e9..6e40cef8 100644
--- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js
+++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
@@ -21,7 +21,7 @@ import {getIdentifierColorNumber, avatarInitials, getAvatarHttpUrl} from "../../
 export class BaseMessageTile extends SimpleTile {
     constructor(options) {
         super(options);
-        this._tileCreator = options.tileCreator;
+        this._tilesCreator = options.tilesCreator;
         this._date = this._entry.timestamp ? new Date(this._entry.timestamp) : null;
         this._isContinuation = false;
         this._reactions = null;
@@ -123,7 +123,7 @@ export class BaseMessageTile extends SimpleTile {
             const action = this._replyTile.updateEntry(replyEntry);
             if (action?.shouldReplace) {
                 this.disposeTracked(this._replyTile);
-                this._replyTile = this._tileCreator(replyEntry);
+                this._replyTile = this._tilesCreator(replyEntry);
             }
             if(action?.shouldUpdate) {
                 this._replyTile.emitChange();
@@ -236,7 +236,7 @@ export class BaseMessageTile extends SimpleTile {
         if (!this._replyTile) {
             const entry = this._entry.contextEntry;
             if (entry) {
-                this._replyTile = this.track(this._tileCreator(entry));
+                this._replyTile = this.track(this._tilesCreator(entry));
             }
         }
         return this._replyTile;
diff --git a/src/domain/session/room/timeline/tilesCreator.js b/src/domain/session/room/timeline/tilesCreator.js
index 8634ea6f..945d9843 100644
--- a/src/domain/session/room/timeline/tilesCreator.js
+++ b/src/domain/session/room/timeline/tilesCreator.js
@@ -29,7 +29,7 @@ import {MissingAttachmentTile} from "./tiles/MissingAttachmentTile.js";
 
 export function tilesCreator(baseOptions) {
     const creator =  function tilesCreator(entry, emitUpdate) {
-        const options = Object.assign({entry, emitUpdate, tileCreator: creator}, baseOptions);
+        const options = Object.assign({entry, emitUpdate, tilesCreator: creator}, baseOptions);
         if (entry.isGap) {
             return new GapTile(options);
         } else if (entry.isPending && entry.pendingEvent.isMissingAttachments) {

From 41fffdf155c0f9ecaa5550df6096e87f7f3ddbf3 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Thu, 13 Jan 2022 20:47:24 +0530
Subject: [PATCH 61/77] Remove even more stray new lines

---
 src/platform/web/ui/session/room/timeline/TextMessageView.js | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/platform/web/ui/session/room/timeline/TextMessageView.js b/src/platform/web/ui/session/room/timeline/TextMessageView.js
index c8651224..510a676f 100644
--- a/src/platform/web/ui/session/room/timeline/TextMessageView.js
+++ b/src/platform/web/ui/session/room/timeline/TextMessageView.js
@@ -56,8 +56,6 @@ export class TextMessageView extends BaseMessageView {
 
         return container;
     }
-
-
 }
 
 function renderList(listBlock) {

From d18f4d341c5dfdbbac29b118da3d874ff1208666 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Fri, 14 Jan 2022 18:31:22 +0530
Subject: [PATCH 62/77] store replyFlags on this

---
 .../web/ui/session/room/timeline/BaseMessageView.js         | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/platform/web/ui/session/room/timeline/BaseMessageView.js b/src/platform/web/ui/session/room/timeline/BaseMessageView.js
index cf9742aa..a6fbb9be 100644
--- a/src/platform/web/ui/session/room/timeline/BaseMessageView.js
+++ b/src/platform/web/ui/session/room/timeline/BaseMessageView.js
@@ -29,10 +29,12 @@ export class BaseMessageView extends TemplateView {
         this._menuPopup = null;
         this._tagName = tagName;
         // TODO An enum could be nice to make code easier to read at call sites.
-        this._interactive = renderFlags?.interactive ?? true;
-        this._isReplyPreview = renderFlags?.reply;
+        this._renderFlags = renderFlags;
     }
 
+    get _interactive() { return this._renderFlags?.interactive ?? true; }
+    get _isReplyPreview() { return this._renderFlags?.reply; }
+
     render(t, vm) {
         const children = [this.renderMessageBody(t, vm)];
         if (this._interactive) {

From 0af9f1016605d3bee38b0122cfb569b361ace741 Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Fri, 14 Jan 2022 19:11:40 +0530
Subject: [PATCH 63/77] don't store tilesCreator

---
 .../session/room/timeline/TilesCollection.js  |  2 +-
 .../room/timeline/tiles/BaseMessageTile.js    | 19 ++++++-------------
 .../session/room/timeline/tilesCreator.js     |  5 ++---
 3 files changed, 9 insertions(+), 17 deletions(-)

diff --git a/src/domain/session/room/timeline/TilesCollection.js b/src/domain/session/room/timeline/TilesCollection.js
index 54ab5ddd..33ae4472 100644
--- a/src/domain/session/room/timeline/TilesCollection.js
+++ b/src/domain/session/room/timeline/TilesCollection.js
@@ -150,7 +150,7 @@ export class TilesCollection extends BaseObservableList {
         const tileIdx = this._findTileIdx(entry);
         const tile = this._findTileAtIdx(entry, tileIdx);
         if (tile) {
-            const action = tile.updateEntry(entry, params);
+            const action = tile.updateEntry(entry, params, this._tileCreator);
             if (action.shouldReplace) {
                 const newTile = this._tileCreator(entry);
                 if (newTile) {
diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
index 6e40cef8..0e1de27c 100644
--- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js
+++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
@@ -21,7 +21,6 @@ import {getIdentifierColorNumber, avatarInitials, getAvatarHttpUrl} from "../../
 export class BaseMessageTile extends SimpleTile {
     constructor(options) {
         super(options);
-        this._tilesCreator = options.tilesCreator;
         this._date = this._entry.timestamp ? new Date(this._entry.timestamp) : null;
         this._isContinuation = false;
         this._reactions = null;
@@ -116,17 +115,17 @@ export class BaseMessageTile extends SimpleTile {
         }
     }
 
-    updateEntry(entry, param) {
+    updateEntry(entry, param, tilesCreator) {
         const replyEntry = entry.contextEntry;
-        if (replyEntry && this._replyTile) {
+        if (replyEntry) {
             // this is an update to contextEntry used for replyPreview
-            const action = this._replyTile.updateEntry(replyEntry);
-            if (action?.shouldReplace) {
+            const action = this._replyTile?.updateEntry(replyEntry);
+            if (action?.shouldReplace || !this._replyTile) {
                 this.disposeTracked(this._replyTile);
-                this._replyTile = this._tilesCreator(replyEntry);
+                this._replyTile = tilesCreator(replyEntry);
             }
             if(action?.shouldUpdate) {
-                this._replyTile.emitChange();
+                this._replyTile?.emitChange();
             }
         }
         const action = super.updateEntry(entry, param);
@@ -233,12 +232,6 @@ export class BaseMessageTile extends SimpleTile {
         if (!this._entry.contextEventId) {
             return null;
         }
-        if (!this._replyTile) {
-            const entry = this._entry.contextEntry;
-            if (entry) {
-                this._replyTile = this.track(this._tilesCreator(entry));
-            }
-        }
         return this._replyTile;
     }
 }
diff --git a/src/domain/session/room/timeline/tilesCreator.js b/src/domain/session/room/timeline/tilesCreator.js
index 945d9843..9dde00a2 100644
--- a/src/domain/session/room/timeline/tilesCreator.js
+++ b/src/domain/session/room/timeline/tilesCreator.js
@@ -28,8 +28,8 @@ import {EncryptionEnabledTile} from "./tiles/EncryptionEnabledTile.js";
 import {MissingAttachmentTile} from "./tiles/MissingAttachmentTile.js";
 
 export function tilesCreator(baseOptions) {
-    const creator =  function tilesCreator(entry, emitUpdate) {
-        const options = Object.assign({entry, emitUpdate, tilesCreator: creator}, baseOptions);
+    return function tilesCreator(entry, emitUpdate) {
+        const options = Object.assign({entry, emitUpdate}, baseOptions);
         if (entry.isGap) {
             return new GapTile(options);
         } else if (entry.isPending && entry.pendingEvent.isMissingAttachments) {
@@ -77,5 +77,4 @@ export function tilesCreator(baseOptions) {
             }
         }
     }   
-    return creator;
 }

From dac2d5e685a39aabd9f61c6bed44c5fe98239a3c Mon Sep 17 00:00:00 2001
From: RMidhunSuresh 
Date: Fri, 14 Jan 2022 19:26:23 +0530
Subject: [PATCH 64/77] Pass everything down into updateEntry

---
 src/domain/session/room/timeline/tiles/BaseMessageTile.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
index 0e1de27c..1fadd855 100644
--- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js
+++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
@@ -119,7 +119,7 @@ export class BaseMessageTile extends SimpleTile {
         const replyEntry = entry.contextEntry;
         if (replyEntry) {
             // this is an update to contextEntry used for replyPreview
-            const action = this._replyTile?.updateEntry(replyEntry);
+            const action = this._replyTile?.updateEntry(replyEntry, param, tilesCreator);
             if (action?.shouldReplace || !this._replyTile) {
                 this.disposeTracked(this._replyTile);
                 this._replyTile = tilesCreator(replyEntry);

From 052ff02571037ab212576f2267c736654569bc95 Mon Sep 17 00:00:00 2001
From: Bruno Windels 
Date: Fri, 14 Jan 2022 15:47:22 +0100
Subject: [PATCH 65/77] move TileView type too so we don't have to repeat
 imports

---
 src/platform/web/ui/session/room/TimelineView.ts | 11 ++---------
 src/platform/web/ui/session/room/common.ts       |  4 +++-
 2 files changed, 5 insertions(+), 10 deletions(-)

diff --git a/src/platform/web/ui/session/room/TimelineView.ts b/src/platform/web/ui/session/room/TimelineView.ts
index 8e4c91a2..936b8c7c 100644
--- a/src/platform/web/ui/session/room/TimelineView.ts
+++ b/src/platform/web/ui/session/room/TimelineView.ts
@@ -14,20 +14,16 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+import type {TileView} from "./common";
+import {viewClassForEntry} from "./common";
 import {ListView} from "../../general/ListView";
 import {TemplateView, Builder} from "../../general/TemplateView";
 import {IObservableValue} from "../../general/BaseUpdateView";
-import {GapView} from "./timeline/GapView.js";
-import {TextMessageView} from "./timeline/TextMessageView.js";
-import {ImageView} from "./timeline/ImageView.js";
-import {VideoView} from "./timeline/VideoView.js";
-import {FileView} from "./timeline/FileView.js";
 import {MissingAttachmentView} from "./timeline/MissingAttachmentView.js";
 import {AnnouncementView} from "./timeline/AnnouncementView.js";
 import {RedactedView} from "./timeline/RedactedView.js";
 import {SimpleTile} from "../../../../../domain/session/room/timeline/tiles/SimpleTile.js";
 import {BaseObservableList as ObservableList} from "../../../../../observable/list/BaseObservableList";
-import {viewClassForEntry} from "./common";
 
 //import {TimelineViewModel} from "../../../../../domain/session/room/timeline/TimelineViewModel.js";
 export interface TimelineViewModel extends IObservableValue {
@@ -36,9 +32,6 @@ export interface TimelineViewModel extends IObservableValue {
     setVisibleTileRange(start?: SimpleTile, end?: SimpleTile);
 }
 
-export type TileView = GapView | AnnouncementView | TextMessageView |
-    ImageView | VideoView | FileView | MissingAttachmentView | RedactedView;
-
 function bottom(node: HTMLElement): number {
     return node.offsetTop + node.clientHeight;
 }
diff --git a/src/platform/web/ui/session/room/common.ts b/src/platform/web/ui/session/room/common.ts
index 0928a998..6214c3b8 100644
--- a/src/platform/web/ui/session/room/common.ts
+++ b/src/platform/web/ui/session/room/common.ts
@@ -24,7 +24,9 @@ import {AnnouncementView} from "./timeline/AnnouncementView.js";
 import {RedactedView} from "./timeline/RedactedView.js";
 import {SimpleTile} from "../../../../../domain/session/room/timeline/tiles/SimpleTile.js";
 import {GapView} from "./timeline/GapView.js";
-import type {TileView} from "./TimelineView";
+
+export type TileView = GapView | AnnouncementView | TextMessageView |
+    ImageView | VideoView | FileView | MissingAttachmentView | RedactedView;
 
 type TileViewConstructor = (this: TileView, SimpleTile) => void;
 export function viewClassForEntry(entry: SimpleTile): TileViewConstructor | undefined {

From b578f4ac840dbcf506b9ee66a411e5487a4f0977 Mon Sep 17 00:00:00 2001
From: Bruno Windels 
Date: Fri, 14 Jan 2022 15:50:19 +0100
Subject: [PATCH 66/77] actually add LocationView

---
 src/platform/web/ui/session/room/common.ts | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/platform/web/ui/session/room/common.ts b/src/platform/web/ui/session/room/common.ts
index 6214c3b8..5048211a 100644
--- a/src/platform/web/ui/session/room/common.ts
+++ b/src/platform/web/ui/session/room/common.ts
@@ -26,8 +26,9 @@ import {SimpleTile} from "../../../../../domain/session/room/timeline/tiles/Simp
 import {GapView} from "./timeline/GapView.js";
 
 export type TileView = GapView | AnnouncementView | TextMessageView |
-    ImageView | VideoView | FileView | MissingAttachmentView | RedactedView;
+    ImageView | VideoView | FileView | LocationView | MissingAttachmentView | RedactedView;
 
+// TODO: this is what works for a ctor but doesn't actually check we constrain the returned ctors to the types above
 type TileViewConstructor = (this: TileView, SimpleTile) => void;
 export function viewClassForEntry(entry: SimpleTile): TileViewConstructor | undefined {
     switch (entry.shape) {

From 1ea4a347e2dc011b826290dde8ddb4c4b81263dc Mon Sep 17 00:00:00 2001
From: Bruno Windels 
Date: Fri, 14 Jan 2022 15:53:17 +0100
Subject: [PATCH 67/77] encode url components

---
 src/domain/session/room/timeline/tiles/BaseMessageTile.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
index 1fadd855..65b50269 100644
--- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js
+++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
@@ -35,11 +35,11 @@ export class BaseMessageTile extends SimpleTile {
     }
 
     get permaLink() {
-        return `https://matrix.to/#/${this._room.id}/${this._entry.id}`;
+        return `https://matrix.to/#/${encodeURIComponent(this._room.id)}/${encodeURIComponent(this._entry.id)}`;
     }
 
     get senderProfileLink() {
-        return `https://matrix.to/#/${this.sender}`;
+        return `https://matrix.to/#/${encodeURIComponent(this.sender)}`;
     }
 
     get displayName() {

From ad335d5088bb972c77a7b61c8924e1eea5b93670 Mon Sep 17 00:00:00 2001
From: Bruno Windels 
Date: Fri, 14 Jan 2022 16:06:29 +0100
Subject: [PATCH 68/77] pass in tilesCreator everywhere, although not needed
 right now

---
 src/domain/session/room/timeline/ReactionsViewModel.js       | 2 +-
 src/domain/session/room/timeline/tiles/BaseMessageTile.js    | 2 +-
 src/domain/session/room/timeline/tiles/EncryptedEventTile.js | 4 ++--
 src/domain/session/room/timeline/tiles/GapTile.js            | 4 ++--
 src/domain/session/room/timeline/tiles/SimpleTile.js         | 2 +-
 5 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/src/domain/session/room/timeline/ReactionsViewModel.js b/src/domain/session/room/timeline/ReactionsViewModel.js
index de5ea2c3..a7c175b1 100644
--- a/src/domain/session/room/timeline/ReactionsViewModel.js
+++ b/src/domain/session/room/timeline/ReactionsViewModel.js
@@ -225,7 +225,7 @@ export function tests() {
                 return new BaseMessageTile({entry, roomVM: {room}, timeline, platform: {logger}});
             }
             return null;
-        }, (tile, params, entry) => tile?.updateEntry(entry, params));
+        }, (tile, params, entry) => tile?.updateEntry(entry, params, null));
         return tiles;
     }
 
diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
index 65b50269..091ef518 100644
--- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js
+++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
@@ -128,7 +128,7 @@ export class BaseMessageTile extends SimpleTile {
                 this._replyTile?.emitChange();
             }
         }
-        const action = super.updateEntry(entry, param);
+        const action = super.updateEntry(entry, param, tilesCreator);
         if (action.shouldUpdate) {
             this._updateReactions();
         }
diff --git a/src/domain/session/room/timeline/tiles/EncryptedEventTile.js b/src/domain/session/room/timeline/tiles/EncryptedEventTile.js
index b96e2d85..50f507eb 100644
--- a/src/domain/session/room/timeline/tiles/EncryptedEventTile.js
+++ b/src/domain/session/room/timeline/tiles/EncryptedEventTile.js
@@ -18,8 +18,8 @@ import {BaseTextTile} from "./BaseTextTile.js";
 import {UpdateAction} from "../UpdateAction.js";
 
 export class EncryptedEventTile extends BaseTextTile {
-    updateEntry(entry, params) {
-        const parentResult = super.updateEntry(entry, params);
+    updateEntry(entry, params, tilesCreator) {
+        const parentResult = super.updateEntry(entry, params, tilesCreator);
         // event got decrypted, recreate the tile and replace this one with it
         if (entry.eventType !== "m.room.encrypted") {
             // the "shape" parameter trigger tile recreation in TimelineView
diff --git a/src/domain/session/room/timeline/tiles/GapTile.js b/src/domain/session/room/timeline/tiles/GapTile.js
index c1fed69f..df0cedd9 100644
--- a/src/domain/session/room/timeline/tiles/GapTile.js
+++ b/src/domain/session/room/timeline/tiles/GapTile.js
@@ -81,8 +81,8 @@ export class GapTile extends SimpleTile {
         this._siblingChanged = true;
     }
 
-    updateEntry(entry, params) {
-        super.updateEntry(entry, params);
+    updateEntry(entry, params, tilesCreator) {
+        super.updateEntry(entry, params, tilesCreator);
         if (!entry.isGap) {
             return UpdateAction.Remove();
         } else {
diff --git a/src/domain/session/room/timeline/tiles/SimpleTile.js b/src/domain/session/room/timeline/tiles/SimpleTile.js
index 4c1c1de0..46e8aa3a 100644
--- a/src/domain/session/room/timeline/tiles/SimpleTile.js
+++ b/src/domain/session/room/timeline/tiles/SimpleTile.js
@@ -92,7 +92,7 @@ export class SimpleTile extends ViewModel {
     }
 
     // update received for already included (falls within sort keys) entry
-    updateEntry(entry, param) {
+    updateEntry(entry, param, tilesCreator) {
         const renderedAsRedacted = this.shape === "redacted";
         if (!entry.isGap && entry.isRedacted !== renderedAsRedacted) {
             // recreate the tile if the entry becomes redacted

From 5f99c2360cd0e7767e4cf4a33c55aa4557064f67 Mon Sep 17 00:00:00 2001
From: Bruno Windels 
Date: Fri, 14 Jan 2022 16:12:43 +0100
Subject: [PATCH 69/77] also try to create replyTile from ctor just in case
 update doesn't come

---
 .../room/timeline/tiles/BaseMessageTile.js        | 15 ++++++++++-----
 src/domain/session/room/timeline/tilesCreator.js  |  7 ++++---
 2 files changed, 14 insertions(+), 8 deletions(-)

diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
index 091ef518..ae447b3c 100644
--- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js
+++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
@@ -28,6 +28,7 @@ export class BaseMessageTile extends SimpleTile {
         if (this._entry.annotations || this._entry.pendingAnnotations) {
             this._updateReactions();
         }
+        this._updateReplyTileIfNeeded(options.tilesCreator);
     }
 
     get _mediaRepository() {
@@ -116,6 +117,15 @@ export class BaseMessageTile extends SimpleTile {
     }
 
     updateEntry(entry, param, tilesCreator) {
+        this._updateReplyTileIfNeeded(tilesCreator);
+        const action = super.updateEntry(entry, param, tilesCreator);
+        if (action.shouldUpdate) {
+            this._updateReactions();
+        }
+        return action;
+    }
+
+    _updateReplyTileIfNeeded(tilesCreator) {
         const replyEntry = entry.contextEntry;
         if (replyEntry) {
             // this is an update to contextEntry used for replyPreview
@@ -128,11 +138,6 @@ export class BaseMessageTile extends SimpleTile {
                 this._replyTile?.emitChange();
             }
         }
-        const action = super.updateEntry(entry, param, tilesCreator);
-        if (action.shouldUpdate) {
-            this._updateReactions();
-        }
-        return action;
     }
 
     startReply() {
diff --git a/src/domain/session/room/timeline/tilesCreator.js b/src/domain/session/room/timeline/tilesCreator.js
index 9dde00a2..dc9a850e 100644
--- a/src/domain/session/room/timeline/tilesCreator.js
+++ b/src/domain/session/room/timeline/tilesCreator.js
@@ -28,8 +28,8 @@ import {EncryptionEnabledTile} from "./tiles/EncryptionEnabledTile.js";
 import {MissingAttachmentTile} from "./tiles/MissingAttachmentTile.js";
 
 export function tilesCreator(baseOptions) {
-    return function tilesCreator(entry, emitUpdate) {
-        const options = Object.assign({entry, emitUpdate}, baseOptions);
+    const tilesCreator = function tilesCreator(entry, emitUpdate) {
+        const options = Object.assign({entry, emitUpdate, tilesCreator}, baseOptions);
         if (entry.isGap) {
             return new GapTile(options);
         } else if (entry.isPending && entry.pendingEvent.isMissingAttachments) {
@@ -76,5 +76,6 @@ export function tilesCreator(baseOptions) {
                     return null;
             }
         }
-    }   
+    };
+    return tilesCreator;
 }

From 23212289812fc23339db527fd0cc6ab94c132719 Mon Sep 17 00:00:00 2001
From: Bruno Windels 
Date: Fri, 14 Jan 2022 16:20:14 +0100
Subject: [PATCH 70/77] use this._entry here (once updated by
 super.updateEntry)

---
 src/domain/session/room/timeline/tiles/BaseMessageTile.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
index ae447b3c..b82a761a 100644
--- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js
+++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
@@ -117,16 +117,16 @@ export class BaseMessageTile extends SimpleTile {
     }
 
     updateEntry(entry, param, tilesCreator) {
-        this._updateReplyTileIfNeeded(tilesCreator);
         const action = super.updateEntry(entry, param, tilesCreator);
         if (action.shouldUpdate) {
             this._updateReactions();
         }
+        this._updateReplyTileIfNeeded(tilesCreator);
         return action;
     }
 
     _updateReplyTileIfNeeded(tilesCreator) {
-        const replyEntry = entry.contextEntry;
+        const replyEntry = this._entry.contextEntry;
         if (replyEntry) {
             // this is an update to contextEntry used for replyPreview
             const action = this._replyTile?.updateEntry(replyEntry, param, tilesCreator);

From 8201a85c475eee7ee7e7f88de78d5e0e21d80258 Mon Sep 17 00:00:00 2001
From: Bruno Windels 
Date: Fri, 14 Jan 2022 16:20:38 +0100
Subject: [PATCH 71/77] ensure these have a fn for tilesCreator

---
 src/domain/session/room/timeline/ReactionsViewModel.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/domain/session/room/timeline/ReactionsViewModel.js b/src/domain/session/room/timeline/ReactionsViewModel.js
index a7c175b1..fa48bec0 100644
--- a/src/domain/session/room/timeline/ReactionsViewModel.js
+++ b/src/domain/session/room/timeline/ReactionsViewModel.js
@@ -225,7 +225,7 @@ export function tests() {
                 return new BaseMessageTile({entry, roomVM: {room}, timeline, platform: {logger}});
             }
             return null;
-        }, (tile, params, entry) => tile?.updateEntry(entry, params, null));
+        }, (tile, params, entry) => tile?.updateEntry(entry, params, function () {}));
         return tiles;
     }
 

From 184a16a1944a9ad5e9052aca456af0c9185b1181 Mon Sep 17 00:00:00 2001
From: Bruno Windels 
Date: Fri, 14 Jan 2022 16:23:12 +0100
Subject: [PATCH 72/77] also define param

---
 src/domain/session/room/timeline/tiles/BaseMessageTile.js | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
index b82a761a..d04ee495 100644
--- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js
+++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js
@@ -28,7 +28,7 @@ export class BaseMessageTile extends SimpleTile {
         if (this._entry.annotations || this._entry.pendingAnnotations) {
             this._updateReactions();
         }
-        this._updateReplyTileIfNeeded(options.tilesCreator);
+        this._updateReplyTileIfNeeded(options.tilesCreator, undefined);
     }
 
     get _mediaRepository() {
@@ -121,11 +121,11 @@ export class BaseMessageTile extends SimpleTile {
         if (action.shouldUpdate) {
             this._updateReactions();
         }
-        this._updateReplyTileIfNeeded(tilesCreator);
+        this._updateReplyTileIfNeeded(tilesCreator, param);
         return action;
     }
 
-    _updateReplyTileIfNeeded(tilesCreator) {
+    _updateReplyTileIfNeeded(tilesCreator, param) {
         const replyEntry = this._entry.contextEntry;
         if (replyEntry) {
             // this is an update to contextEntry used for replyPreview

From 65929194b059a71d82ebf21e9742d672753a0ff7 Mon Sep 17 00:00:00 2001
From: Bruno Windels 
Date: Fri, 14 Jan 2022 16:23:55 +0100
Subject: [PATCH 73/77] fix lint warnings

---
 src/domain/session/room/timeline/tiles/SimpleTile.js | 2 +-
 src/platform/web/main.js                             | 1 -
 2 files changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/domain/session/room/timeline/tiles/SimpleTile.js b/src/domain/session/room/timeline/tiles/SimpleTile.js
index 46e8aa3a..4c1c1de0 100644
--- a/src/domain/session/room/timeline/tiles/SimpleTile.js
+++ b/src/domain/session/room/timeline/tiles/SimpleTile.js
@@ -92,7 +92,7 @@ export class SimpleTile extends ViewModel {
     }
 
     // update received for already included (falls within sort keys) entry
-    updateEntry(entry, param, tilesCreator) {
+    updateEntry(entry, param) {
         const renderedAsRedacted = this.shape === "redacted";
         if (!entry.isGap && entry.isRedacted !== renderedAsRedacted) {
             // recreate the tile if the entry becomes redacted
diff --git a/src/platform/web/main.js b/src/platform/web/main.js
index eb71244a..1729c17c 100644
--- a/src/platform/web/main.js
+++ b/src/platform/web/main.js
@@ -16,7 +16,6 @@ limitations under the License.
 */
 
 // import {RecordRequester, ReplayRequester} from "./matrix/net/request/replay";
-import {Client} from "../../matrix/Client.js";
 import {RootViewModel} from "../../domain/RootViewModel.js";
 import {createNavigation, createRouter} from "../../domain/navigation/index.js";
 // Don't use a default export here, as we use multiple entries during legacy build,

From 3243ce2a905a791dab2ce0ffda91f594726f3b3a Mon Sep 17 00:00:00 2001
From: Bruno Windels 
Date: Fri, 14 Jan 2022 18:15:46 +0100
Subject: [PATCH 74/77] fix unit test that failed after it finished

crashing the runner on node 16
---
 src/matrix/room/timeline/Timeline.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js
index ef3c3fba..67ce9ce2 100644
--- a/src/matrix/room/timeline/Timeline.js
+++ b/src/matrix/room/timeline/Timeline.js
@@ -742,7 +742,7 @@ export function tests() {
             const entryA = new EventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)) });
             const entryB = new EventEntry({ event: withReply("event_id_1", createEvent("m.room.message", "event_id_2", bob)), eventIndex: 2 });
             await timeline.load(new User(alice), "join", new NullLogItem());
-            timeline.entries.subscribe({ onAdd: () => null, });
+            timeline.entries.subscribe({ onAdd: () => null, onUpdate: () => null});
             timeline.addEntries([entryA, entryB]);
             assert.deepEqual(entryB.contextEntry, entryA);
         },

From 7197e5427f12c5119f65f6a48ca66a7d4a8ed8e0 Mon Sep 17 00:00:00 2001
From: Bruno Windels 
Date: Fri, 14 Jan 2022 18:16:52 +0100
Subject: [PATCH 75/77] don't emit an update when the context entry is loaded
 sync

also load context entries in parallel
---
 src/matrix/room/timeline/Timeline.js | 41 ++++++++++++++++++++--------
 1 file changed, 30 insertions(+), 11 deletions(-)

diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js
index 67ce9ce2..0530fdad 100644
--- a/src/matrix/room/timeline/Timeline.js
+++ b/src/matrix/room/timeline/Timeline.js
@@ -258,8 +258,8 @@ export class Timeline {
         this._addLocalRelationsToNewRemoteEntries(newEntries);
         this._updateEntriesFetchedFromHomeserver(newEntries);
         this._moveEntryToRemoteEntries(newEntries);
-        this._remoteEntries.setManySorted(newEntries);
         this._loadContextEntriesWhereNeeded(newEntries);
+        this._remoteEntries.setManySorted(newEntries);
     }
 
     /**
@@ -320,22 +320,41 @@ export class Timeline {
                 continue;
             }
             const id = entry.contextEventId;
-            let contextEvent = this._findLoadedEventById(id);
-            if (!contextEvent) {
-                contextEvent = await this._getEventFromStorage(id) ?? await this._getEventFromHomeserver(id);
-                if (contextEvent) {
-                    // this entry was created from storage/hs, so it's not tracked by remoteEntries
-                    // we track them here so that we can update reply previews later
-                    this._contextEntriesNotInTimeline.set(id, contextEvent);
-                }
-            }
+            // before looking into remoteEntries, check the entries
+            // that about to be added first
+            let contextEvent = entries.find(e => e.id === id);
+            contextEvent = this._findLoadedEventById(id);
             if (contextEvent) {
                 entry.setContextEntry(contextEvent);
-                this._emitUpdateForEntry(entry, "contextEntry");
+                // we don't emit an update here, as the add or update
+                // that the callee will emit hasn't been emitted yet.
+            } else {
+                // we don't await here, which is not ideal,
+                // but one of our callers, addEntries, is not async
+                // so there is not much point.
+                // Also, we want to run the entry fetching in parallel.
+                this._loadContextEntryNotInTimeline(entry);
             }
         }
     }
 
+    async _loadContextEntryNotInTimeline(entry) {
+        const id = entry.contextEventId;
+        let contextEvent = await this._getEventFromStorage(id);
+        if (!contextEvent) {
+            contextEvent = await this._getEventFromHomeserver(id);
+        }
+        if (contextEvent) {
+            // this entry was created from storage/hs, so it's not tracked by remoteEntries
+            // we track them here so that we can update reply previews later
+            this._contextEntriesNotInTimeline.set(id, contextEvent);
+            entry.setContextEntry(contextEvent);
+            // here, we awaited something, so from now on we do have to emit
+            // an update if we set the context entry.
+            this._emitUpdateForEntry(entry, "contextEntry");
+        }
+    }
+
     /**
      * Fetches an entry with the given event-id from localEntries, remoteEntries or contextEntriesNotInTimeline.
      * @param {string} eventId event-id of the entry

From 3d00881508673d9cca15f51a351f895c5c6ec955 Mon Sep 17 00:00:00 2001
From: Bruno Windels 
Date: Fri, 14 Jan 2022 19:05:30 +0100
Subject: [PATCH 76/77] don't look in remoteEntries when already found

---
 src/matrix/room/timeline/Timeline.js | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js
index 0530fdad..cb342da0 100644
--- a/src/matrix/room/timeline/Timeline.js
+++ b/src/matrix/room/timeline/Timeline.js
@@ -323,7 +323,9 @@ export class Timeline {
             // before looking into remoteEntries, check the entries
             // that about to be added first
             let contextEvent = entries.find(e => e.id === id);
-            contextEvent = this._findLoadedEventById(id);
+            if (!contextEvent) {
+                contextEvent = this._findLoadedEventById(id);
+            }
             if (contextEvent) {
                 entry.setContextEntry(contextEvent);
                 // we don't emit an update here, as the add or update

From a8a8355ea4efcc9b6dbb341ae6654a0eb57a5853 Mon Sep 17 00:00:00 2001
From: Bruno Windels 
Date: Fri, 14 Jan 2022 19:05:53 +0100
Subject: [PATCH 77/77] fix unit test

---
 src/matrix/room/timeline/Timeline.js | 22 +++++++++++++---------
 1 file changed, 13 insertions(+), 9 deletions(-)

diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js
index cb342da0..90ec29eb 100644
--- a/src/matrix/room/timeline/Timeline.js
+++ b/src/matrix/room/timeline/Timeline.js
@@ -490,6 +490,7 @@ import {EventEntry} from "./entries/EventEntry.js";
 import {User} from "../../User.js";
 import {PendingEvent} from "../sending/PendingEvent.js";
 import {createAnnotation} from "./relations.js";
+import {redactEvent} from "./common.js";
 
 export function tests() {
     const fragmentIdComparer = new FragmentIdComparer([]);
@@ -763,7 +764,9 @@ export function tests() {
             const entryA = new EventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)) });
             const entryB = new EventEntry({ event: withReply("event_id_1", createEvent("m.room.message", "event_id_2", bob)), eventIndex: 2 });
             await timeline.load(new User(alice), "join", new NullLogItem());
-            timeline.entries.subscribe({ onAdd: () => null, onUpdate: () => null});
+            timeline.entries.subscribe({
+                onAdd() {},
+            });
             timeline.addEntries([entryA, entryB]);
             assert.deepEqual(entryB.contextEntry, entryA);
         },
@@ -823,21 +826,22 @@ export function tests() {
         "redaction of context entry triggers updates in other entries": async assert => {
             const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {},
                 fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock(), hsApi});
-            const entryB = new EventEntry({ event: withReply("event_id_1", createEvent("m.room.message", "event_id_2", bob)), eventIndex: 2 });
-            const entryC = new EventEntry({ event: withReply("event_id_1", createEvent("m.room.message", "event_id_3", bob)), eventIndex: 3 });
+            const entryA = new EventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)), eventIndex: 1, fragmentId: 1 });
+            const entryB = new EventEntry({ event: withReply("event_id_1", createEvent("m.room.message", "event_id_2", bob)), eventIndex: 2, fragmentId: 1 });
+            const entryC = new EventEntry({ event: withReply("event_id_1", createEvent("m.room.message", "event_id_3", bob)), eventIndex: 3, fragmentId: 1 });
             await timeline.load(new User(alice), "join", new NullLogItem());
             const bin = [];
             timeline.entries.subscribe({
-                onUpdate: (index) => {
-                    const e = timeline.remoteEntries[index];
+                onUpdate: (index, e) => {
                     bin.push(e.id);
                 },
                 onAdd: () => null,
             });
-            timeline.addEntries([entryB, entryC]);
-            await poll(() => timeline._remoteEntries.array.length === 2 && timeline._contextEntriesNotInTimeline.get("event_id_1"));
-            const redactingEntry = new EventEntry({ event: withRedacts("event_id_1", "foo", createEvent("m.room.redaction", "event_id_3", alice)) });
-            timeline.addEntries([redactingEntry]);
+            timeline.addEntries([entryA, entryB, entryC]);
+            const eventAClone = JSON.parse(JSON.stringify(entryA.event));
+            redactEvent(withRedacts("event_id_1", "foo", createEvent("m.room.redaction", "event_id_4", alice)), eventAClone);
+            const redactedEntry = new EventEntry({ event: eventAClone, eventIndex: 1, fragmentId: 1 });
+            timeline.replaceEntries([redactedEntry]);
             assert.strictEqual(bin.includes("event_id_2"), true);
             assert.strictEqual(bin.includes("event_id_3"), true);
         },