From 0354bf9b6d432354c4c11f376a1c82b4a9705e0e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 8 Jun 2020 17:11:58 -0600 Subject: [PATCH 01/23] Reimplement breadcrumbs for new room list This all-new component handles breadcrumbs a bit more smoothly for the app by always listening to changes even if the component isn't present. This allows the breadcrumbs to remain up to date for when the user re-enables breadcrumbs. The new behaviour is that we turn breadcrumbs on once the user has a room, and we don't turn it back off for them. This also introduces a new animation which is more stable and not laggy, though instead of sliding the breadcrumbs pop. This might be undesirable - to be reviewed. --- res/css/_components.scss | 1 + res/css/structures/_LeftPanel2.scss | 2 +- res/css/views/rooms/_RoomBreadcrumbs2.scss | 53 ++++++ src/components/structures/LeftPanel2.tsx | 34 +++- .../views/rooms/RoomBreadcrumbs2.tsx | 90 ++++++++++ src/i18n/strings/en_EN.json | 1 + src/settings/SettingsStore.js | 2 + src/stores/AsyncStoreWithClient.ts | 53 ++++++ src/stores/BreadcrumbsStore.ts | 154 ++++++++++++++++++ 9 files changed, 384 insertions(+), 6 deletions(-) create mode 100644 res/css/views/rooms/_RoomBreadcrumbs2.scss create mode 100644 src/components/views/rooms/RoomBreadcrumbs2.tsx create mode 100644 src/stores/AsyncStoreWithClient.ts create mode 100644 src/stores/BreadcrumbsStore.ts diff --git a/res/css/_components.scss b/res/css/_components.scss index 62bec5ad62..8958aee2fc 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -175,6 +175,7 @@ @import "./views/rooms/_PresenceLabel.scss"; @import "./views/rooms/_ReplyPreview.scss"; @import "./views/rooms/_RoomBreadcrumbs.scss"; +@import "./views/rooms/_RoomBreadcrumbs2.scss"; @import "./views/rooms/_RoomDropTarget.scss"; @import "./views/rooms/_RoomHeader.scss"; @import "./views/rooms/_RoomList.scss"; diff --git a/res/css/structures/_LeftPanel2.scss b/res/css/structures/_LeftPanel2.scss index 822a5ac399..502ed18a87 100644 --- a/res/css/structures/_LeftPanel2.scss +++ b/res/css/structures/_LeftPanel2.scss @@ -76,9 +76,9 @@ $roomListMinimizedWidth: 50px; } .mx_LeftPanel2_breadcrumbsContainer { - // TODO: Improve CSS for breadcrumbs (currently shoved into the view rather than placed) width: 100%; overflow: hidden; + margin-top: 8px; } } diff --git a/res/css/views/rooms/_RoomBreadcrumbs2.scss b/res/css/views/rooms/_RoomBreadcrumbs2.scss new file mode 100644 index 0000000000..aa0b0ecb08 --- /dev/null +++ b/res/css/views/rooms/_RoomBreadcrumbs2.scss @@ -0,0 +1,53 @@ +/* +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. +*/ + +@keyframes breadcrumb-popin { + 0% { + // Ideally we'd use `width` instead of `opacity`, but we only + // have 16 nanoseconds to render the frame, and width is expensive. + opacity: 0; + transform: scale(0); + } + 100% { + opacity: 1; + transform: scale(1); + } +} + +.mx_RoomBreadcrumbs2 { + // Create a flexbox for the crumbs + display: flex; + flex-direction: row; + align-items: flex-start; + width: 100%; + + .mx_RoomBreadcrumbs2_crumb { + margin-right: 8px; + width: 32px; + + // React loves to add elements, so only target the one we want to animate + &:first-child { + animation: breadcrumb-popin 0.3s; + } + } + + .mx_RoomBreadcrumbs2_placeholder { + font-weight: 600; + font-size: $font-14px; + line-height: 32px; // specifically to match the height this is not scaled + height: 32px; + } +} diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index c66c0a6799..b42da0be09 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -26,7 +26,9 @@ import TopLeftMenuButton from "./TopLeftMenuButton"; import { Action } from "../../dispatcher/actions"; import { MatrixClientPeg } from "../../MatrixClientPeg"; import BaseAvatar from '../views/avatars/BaseAvatar'; -import RoomBreadcrumbs from "../views/rooms/RoomBreadcrumbs"; +import RoomBreadcrumbs2 from "../views/rooms/RoomBreadcrumbs2"; +import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore"; +import { UPDATE_EVENT } from "../../stores/AsyncStore"; /******************************************************************* * CAUTION * @@ -43,6 +45,7 @@ interface IProps { interface IState { searchExpanded: boolean; searchFilter: string; // TODO: Move search into room list? + showBreadcrumbs: boolean; } export default class LeftPanel2 extends React.Component { @@ -60,7 +63,14 @@ export default class LeftPanel2 extends React.Component { this.state = { searchExpanded: false, searchFilter: "", + showBreadcrumbs: BreadcrumbsStore.instance.visible, }; + + BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); + } + + public componentWillUnmount() { + BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); } private onSearch = (term: string): void => { @@ -85,6 +95,13 @@ export default class LeftPanel2 extends React.Component { } } + private onBreadcrumbsUpdate = () => { + const newVal = BreadcrumbsStore.instance.visible; + if (newVal !== this.state.showBreadcrumbs) { + this.setState({showBreadcrumbs: newVal}); + } + }; + private renderHeader(): React.ReactNode { // TODO: Update when profile info changes // TODO: Presence @@ -100,6 +117,16 @@ export default class LeftPanel2 extends React.Component { displayName = myUser.rawDisplayName; avatarUrl = myUser.avatarUrl; } + + let breadcrumbs; + if (this.state.showBreadcrumbs) { + breadcrumbs = ( +
+ +
+ ); + } + return (
@@ -116,9 +143,7 @@ export default class LeftPanel2 extends React.Component { {displayName}
-
- -
+ {breadcrumbs}
); } @@ -152,7 +177,6 @@ export default class LeftPanel2 extends React.Component { onBlur={() => {/*TODO*/}} />; - // TODO: Breadcrumbs // TODO: Conference handling / calls const containerClasses = classNames({ diff --git a/src/components/views/rooms/RoomBreadcrumbs2.tsx b/src/components/views/rooms/RoomBreadcrumbs2.tsx new file mode 100644 index 0000000000..195757ccf0 --- /dev/null +++ b/src/components/views/rooms/RoomBreadcrumbs2.tsx @@ -0,0 +1,90 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import { BreadcrumbsStore } from "../../../stores/BreadcrumbsStore"; +import AccessibleButton from "../elements/AccessibleButton"; +import RoomAvatar from "../avatars/RoomAvatar"; +import { _t } from "../../../languageHandler"; +import { Room } from "matrix-js-sdk/src/models/room"; +import defaultDispatcher from "../../../dispatcher/dispatcher"; +import Analytics from "../../../Analytics"; +import { UPDATE_EVENT } from "../../../stores/AsyncStore"; + +/******************************************************************* + * CAUTION * + ******************************************************************* + * This is a work in progress implementation and isn't complete or * + * even useful as a component. Please avoid using it until this * + * warning disappears. * + *******************************************************************/ + +interface IProps { +} + +interface IState { +} + +export default class RoomBreadcrumbs2 extends React.PureComponent { + private isMounted = true; + + constructor(props: IProps) { + super(props); + + BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); + } + + public componentWillUnmount() { + this.isMounted = false; + BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); + } + + private onBreadcrumbsUpdate = () => { + if (!this.isMounted) return; + this.forceUpdate(); // we have no state, so this is the best we can do + }; + + private viewRoom = (room: Room, index: number) => { + Analytics.trackEvent("Breadcrumbs", "click_node", index); + defaultDispatcher.dispatch({action: "view_room", room_id: room.roomId}); + }; + + public render(): React.ReactElement { + // TODO: Decorate crumbs with icons + const tiles = BreadcrumbsStore.instance.rooms.map((r, i) => { + return ( + this.viewRoom(r, i)} + aria-label={_t("Room %(name)s", {name: r.name})} + > + + + ) + }); + + if (tiles.length === 0) { + tiles.push( +
+ {_t("No recently visited rooms")} +
+ ); + } + + return
{tiles}
; + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index cf6dc2431a..75caf5b593 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1069,6 +1069,7 @@ "Replying": "Replying", "Room %(name)s": "Room %(name)s", "Recent rooms": "Recent rooms", + "No recently visited rooms": "No recently visited rooms", "No rooms to show": "No rooms to show", "Unnamed room": "Unnamed room", "World readable": "World readable", diff --git a/src/settings/SettingsStore.js b/src/settings/SettingsStore.js index 4b18a27c6c..dcdde46631 100644 --- a/src/settings/SettingsStore.js +++ b/src/settings/SettingsStore.js @@ -181,6 +181,8 @@ export default class SettingsStore { * @param {String} roomId The room ID to monitor for changes in. Use null for all rooms. */ static monitorSetting(settingName, roomId) { + roomId = roomId || null; // the thing wants null specifically to work, so appease it. + if (!this._monitors[settingName]) this._monitors[settingName] = {}; const registerWatcher = () => { diff --git a/src/stores/AsyncStoreWithClient.ts b/src/stores/AsyncStoreWithClient.ts new file mode 100644 index 0000000000..ce7fd45eec --- /dev/null +++ b/src/stores/AsyncStoreWithClient.ts @@ -0,0 +1,53 @@ +/* +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 { MatrixClient } from "matrix-js-sdk/src/client"; +import { AsyncStore } from "./AsyncStore"; +import { ActionPayload } from "../dispatcher/payloads"; + + +export abstract class AsyncStoreWithClient extends AsyncStore { + protected matrixClient: MatrixClient; + + protected abstract async onAction(payload: ActionPayload); + + protected async onReady() { + // Default implementation is to do nothing. + } + + protected async onNotReady() { + // Default implementation is to do nothing. + } + + protected async onDispatch(payload: ActionPayload) { + await this.onAction(payload); + + if (payload.action === 'MatrixActions.sync') { + // Filter out anything that isn't the first PREPARED sync. + if (!(payload.prevState === 'PREPARED' && payload.state !== 'PREPARED')) { + return; + } + + this.matrixClient = payload.matrixClient; + await this.onReady(); + } else if (payload.action === 'on_client_not_viable' || payload.action === 'on_logged_out') { + if (this.matrixClient) { + await this.onNotReady(); + this.matrixClient = null; + } + } + } +} diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts new file mode 100644 index 0000000000..783b38e62f --- /dev/null +++ b/src/stores/BreadcrumbsStore.ts @@ -0,0 +1,154 @@ +/* +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 SettingsStore, { SettingLevel } from "../settings/SettingsStore"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { ActionPayload } from "../dispatcher/payloads"; +import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; +import defaultDispatcher from "../dispatcher/dispatcher"; +import { arrayHasDiff } from "../utils/arrays"; + +const MAX_ROOMS = 20; // arbitrary +const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90s, the time we wait for an autojoined room to show up + +interface IState { + enabled?: boolean; + rooms?: Room[]; +} + +export class BreadcrumbsStore extends AsyncStoreWithClient { + private static internalInstance = new BreadcrumbsStore(); + + private waitingRooms: { roomId: string, addedTs: number }[] = []; + + private constructor() { + super(defaultDispatcher); + + SettingsStore.monitorSetting("breadcrumb_rooms", null); + SettingsStore.monitorSetting("breadcrumbs", null); + } + + public static get instance(): BreadcrumbsStore { + return BreadcrumbsStore.internalInstance; + } + + public get rooms(): Room[] { + return this.state.rooms || []; + } + + public get visible(): boolean { + return this.state.enabled; + } + + protected async onAction(payload: ActionPayload) { + if (!this.matrixClient) return; + + if (payload.action === 'setting_updated') { + if (payload.settingName === 'breadcrumb_rooms') { + await this.updateRooms(); + } else if (payload.settingName === 'breadcrumbs') { + await this.updateState({enabled: SettingsStore.getValue("breadcrumbs", null)}); + } + } else if (payload.action === 'view_room') { + if (payload.auto_join && !this.matrixClient.getRoom(payload.room_id)) { + // Queue the room instead of pushing it immediately. We're probably just + // waiting for a room join to complete. + this.waitingRooms.push({roomId: payload.room_id, addedTs: Date.now()}); + } else { + await this.appendRoom(this.matrixClient.getRoom(payload.room_id)); + } + } + } + + protected async onReady() { + await this.updateRooms(); + await this.updateState({enabled: SettingsStore.getValue("breadcrumbs", null)}); + + this.matrixClient.on("Room.myMembership", this.onMyMembership); + this.matrixClient.on("Room", this.onRoom); + } + + protected async onNotReady() { + this.matrixClient.removeListener("Room.myMembership", this.onMyMembership); + this.matrixClient.removeListener("Room", this.onRoom); + } + + private onMyMembership = async (room: Room) => { + // We turn on breadcrumbs by default once the user has at least 1 room to show. + if (!this.state.enabled) { + await SettingsStore.setValue("breadcrumbs", null, SettingLevel.ACCOUNT, true); + } + }; + + private onRoom = async (room: Room) => { + const waitingRoom = this.waitingRooms.find(r => r.roomId === room.roomId); + if (!waitingRoom) return; + this.waitingRooms.splice(this.waitingRooms.indexOf(waitingRoom), 1); + + if ((Date.now() - waitingRoom.addedTs) > AUTOJOIN_WAIT_THRESHOLD_MS) return; // Too long ago. + await this.appendRoom(room); + }; + + private async updateRooms() { + let roomIds = SettingsStore.getValue("breadcrumb_rooms"); + if (!roomIds || roomIds.length === 0) roomIds = []; + + const rooms = roomIds.map(r => this.matrixClient.getRoom(r)).filter(r => !!r); + const currentRooms = this.state.rooms || []; + if (!arrayHasDiff(rooms, currentRooms)) return; // no change (probably echo) + await this.updateState({rooms}); + } + + private async appendRoom(room: Room) { + const rooms = this.state.rooms.slice(); // cheap clone + + // If the room is upgraded, use that room instead. We'll also splice out + // any children of the room. + const history = this.matrixClient.getRoomUpgradeHistory(room.roomId); + if (history.length > 1) { + room = history[history.length - 1]; // Last room is most recent in history + + // Take out any room that isn't the most recent room + for (let i = 0; i < history.length - 1; i++) { + const idx = rooms.findIndex(r => r.roomId === history[i].roomId); + if (idx !== -1) rooms.splice(idx, 1); + } + } + + // Remove the existing room, if it is present + const existingIdx = rooms.findIndex(r => r.roomId === room.roomId); + if (existingIdx !== -1) { + rooms.splice(existingIdx, 1); + } + + // Splice the room to the start of the list + rooms.splice(0, 0, room); + + if (rooms.length > MAX_ROOMS) { + // This looks weird, but it's saying to start at the MAX_ROOMS point in the + // list and delete everything after it. + rooms.splice(MAX_ROOMS, rooms.length - MAX_ROOMS); + } + + // Update the breadcrumbs + await this.updateState({rooms}); + const roomIds = rooms.map(r => r.roomId); + if (roomIds.length > 0) { + await SettingsStore.setValue("breadcrumb_rooms", null, SettingLevel.ACCOUNT, roomIds); + } + } + +} From 04566e12b202c50d10045fda65bdaf0e288bfecc Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 8 Jun 2020 17:14:40 -0600 Subject: [PATCH 02/23] Fix indentation in styles --- res/css/views/rooms/_RoomBreadcrumbs2.scss | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/res/css/views/rooms/_RoomBreadcrumbs2.scss b/res/css/views/rooms/_RoomBreadcrumbs2.scss index aa0b0ecb08..2db0fdca08 100644 --- a/res/css/views/rooms/_RoomBreadcrumbs2.scss +++ b/res/css/views/rooms/_RoomBreadcrumbs2.scss @@ -15,16 +15,16 @@ limitations under the License. */ @keyframes breadcrumb-popin { - 0% { - // Ideally we'd use `width` instead of `opacity`, but we only - // have 16 nanoseconds to render the frame, and width is expensive. - opacity: 0; - transform: scale(0); - } - 100% { - opacity: 1; - transform: scale(1); - } + 0% { + // Ideally we'd use `width` instead of `opacity`, but we only + // have 16 nanoseconds to render the frame, and width is expensive. + opacity: 0; + transform: scale(0); + } + 100% { + opacity: 1; + transform: scale(1); + } } .mx_RoomBreadcrumbs2 { From eff97e6c205ecdcce0bc40714fe51ca6f4e6d960 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 8 Jun 2020 18:18:34 -0600 Subject: [PATCH 03/23] Fix the tests --- src/stores/BreadcrumbsStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts index 783b38e62f..5944091d00 100644 --- a/src/stores/BreadcrumbsStore.ts +++ b/src/stores/BreadcrumbsStore.ts @@ -113,7 +113,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { } private async appendRoom(room: Room) { - const rooms = this.state.rooms.slice(); // cheap clone + const rooms = (this.state.rooms || []).slice(); // cheap clone // If the room is upgraded, use that room instead. We'll also splice out // any children of the room. From 5083811deb8a2a1fc53acd331c667a0926bc5bce Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 8 Jun 2020 18:26:43 -0600 Subject: [PATCH 04/23] Appease the tests --- src/stores/BreadcrumbsStore.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts index 5944091d00..f0f2dad91b 100644 --- a/src/stores/BreadcrumbsStore.ts +++ b/src/stores/BreadcrumbsStore.ts @@ -68,7 +68,9 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { // waiting for a room join to complete. this.waitingRooms.push({roomId: payload.room_id, addedTs: Date.now()}); } else { - await this.appendRoom(this.matrixClient.getRoom(payload.room_id)); + // The tests might not result in a valid room object. + const room = this.matrixClient.getRoom(payload.room_id); + if (room) await this.appendRoom(room); } } } From 708c65cd965f9da3fd8ada635a71a116d710b292 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 8 Jun 2020 19:08:18 -0600 Subject: [PATCH 05/23] Disable new breadcrumb store when old room list is in use --- src/stores/BreadcrumbsStore.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts index f0f2dad91b..332fa7fe2e 100644 --- a/src/stores/BreadcrumbsStore.ts +++ b/src/stores/BreadcrumbsStore.ts @@ -20,6 +20,7 @@ import { ActionPayload } from "../dispatcher/payloads"; import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; import defaultDispatcher from "../dispatcher/dispatcher"; import { arrayHasDiff } from "../utils/arrays"; +import { RoomListStoreTempProxy } from "./room-list/RoomListStoreTempProxy"; const MAX_ROOMS = 20; // arbitrary const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90s, the time we wait for an autojoined room to show up @@ -56,6 +57,9 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { protected async onAction(payload: ActionPayload) { if (!this.matrixClient) return; + // TODO: Remove when new room list is made the default + if (!RoomListStoreTempProxy.isUsingNewStore()) return; + if (payload.action === 'setting_updated') { if (payload.settingName === 'breadcrumb_rooms') { await this.updateRooms(); @@ -76,6 +80,9 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { } protected async onReady() { + // TODO: Remove when new room list is made the default + if (!RoomListStoreTempProxy.isUsingNewStore()) return; + await this.updateRooms(); await this.updateState({enabled: SettingsStore.getValue("breadcrumbs", null)}); @@ -84,6 +91,9 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { } protected async onNotReady() { + // TODO: Remove when new room list is made the default + if (!RoomListStoreTempProxy.isUsingNewStore()) return; + this.matrixClient.removeListener("Room.myMembership", this.onMyMembership); this.matrixClient.removeListener("Room", this.onRoom); } From 5c4229433661dfb2cddb4a502412797354b9f3d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 9 Jun 2020 13:53:27 +0200 Subject: [PATCH 06/23] EventIndex: Store and restore the encryption info for encrypted events. --- src/Searching.js | 20 ++++++++++++++++++++ src/indexing/EventIndex.js | 33 +++++++++++++++++++++++++++++---- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/Searching.js b/src/Searching.js index 663328fe41..fb59cc563d 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -107,6 +107,26 @@ async function localSearch(searchTerm, roomId = undefined) { const result = MatrixClientPeg.get()._processRoomEventsSearch( emptyResult, response); + // Restore our encryption info so we can properly re-verify the events. + for (let i = 0; i < result.results.length; i++) { + const timeline = result.results[i].context.getTimeline(); + + for (let j = 0; j < timeline.length; j++) { + const ev = timeline[j]; + if (ev.event.curve25519Key) { + ev.makeEncrypted( + "m.room.encrypted", + { algorithm: ev.event.algorithm }, + ev.event.curve25519Key, + ev.event.ed25519Key, + ); + ev._forwardingCurve25519KeyChain = ev.event.forwardingCurve25519KeyChain; + + delete ev.event.curve25519Key; + } + } + } + return result; } diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index fac7c92b65..d4e8ab0117 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -290,6 +290,33 @@ export default class EventIndex extends EventEmitter { return validEventType && validMsgType && hasContentValue; } + eventToJson(ev) { + const jsonEvent = ev.toJSON(); + const e = ev.isEncrypted() ? jsonEvent.decrypted : jsonEvent; + + if (ev.isEncrypted()) { + // Let us store some additional data so we can re-verify the event. + // The js-sdk checks if an event is encrypted using the algorithm, + // the sender key and ed25519 signing key are used to find the + // correct device that sent the event which allows us to check the + // verification state of the event, either directly or using cross + // signing. + e.curve25519Key = ev.getSenderKey(); + e.ed25519Key = ev.getClaimedEd25519Key(); + e.algorithm = ev.getWireContent().algorithm; + e.forwardingCurve25519KeyChain = ev.getForwardingCurve25519KeyChain(); + } else { + // Make sure that unencrypted events don't contain any of that data, + // despite what the server might give to us. + delete e.curve25519Key; + delete e.ed25519Key; + delete e.algorithm; + delete e.forwardingCurve25519KeyChain; + } + + return e; + } + /** * Queue up live events to be added to the event index. * @@ -300,8 +327,7 @@ export default class EventIndex extends EventEmitter { if (!this.isValidEvent(ev)) return; - const jsonEvent = ev.toJSON(); - const e = ev.isEncrypted() ? jsonEvent.decrypted : jsonEvent; + const e = this.eventToJson(ev); const profile = { displayname: ev.sender.rawDisplayName, @@ -477,8 +503,7 @@ export default class EventIndex extends EventEmitter { // Let us convert the events back into a format that EventIndex can // consume. const events = filteredEvents.map((ev) => { - const jsonEvent = ev.toJSON(); - const e = ev.isEncrypted() ? jsonEvent.decrypted : jsonEvent; + const e = this.eventToJson(ev); let profile = {}; if (e.sender in profiles) profile = profiles[e.sender]; From 1467191a5d8d3fd9aefa5639aa77aebdd81d0dd9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 9 Jun 2020 15:06:54 -0600 Subject: [PATCH 07/23] Update the CSS transition for breadcrumbs The actual transition length might need adjusting, but this is fairly close to what was requested. --- package.json | 2 + res/css/views/rooms/_RoomBreadcrumbs2.scss | 34 ++++++------- .../views/rooms/RoomBreadcrumbs2.tsx | 49 ++++++++++++++++--- tsconfig.json | 3 +- yarn.lock | 29 ++++++++++- 5 files changed, 89 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index 93d59a4fa6..966119d1eb 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "react-dom": "^16.9.0", "react-focus-lock": "^2.2.1", "react-resizable": "^1.10.1", + "react-transition-group": "^4.4.1", "resize-observer-polyfill": "^1.5.0", "sanitize-html": "^1.18.4", "text-encoding-utf-8": "^1.0.1", @@ -126,6 +127,7 @@ "@types/qrcode": "^1.3.4", "@types/react": "^16.9", "@types/react-dom": "^16.9.8", + "@types/react-transition-group": "^4.4.0", "@types/zxcvbn": "^4.4.0", "babel-eslint": "^10.0.3", "babel-jest": "^24.9.0", diff --git a/res/css/views/rooms/_RoomBreadcrumbs2.scss b/res/css/views/rooms/_RoomBreadcrumbs2.scss index 2db0fdca08..68cf7d7500 100644 --- a/res/css/views/rooms/_RoomBreadcrumbs2.scss +++ b/res/css/views/rooms/_RoomBreadcrumbs2.scss @@ -14,34 +14,32 @@ See the License for the specific language governing permissions and limitations under the License. */ -@keyframes breadcrumb-popin { - 0% { - // Ideally we'd use `width` instead of `opacity`, but we only - // have 16 nanoseconds to render the frame, and width is expensive. - opacity: 0; - transform: scale(0); - } - 100% { - opacity: 1; - transform: scale(1); - } -} - .mx_RoomBreadcrumbs2 { + width: 100%; + // Create a flexbox for the crumbs display: flex; flex-direction: row; align-items: flex-start; - width: 100%; .mx_RoomBreadcrumbs2_crumb { margin-right: 8px; width: 32px; + } - // React loves to add elements, so only target the one we want to animate - &:first-child { - animation: breadcrumb-popin 0.3s; - } + // These classes come from the CSSTransition component. There's many more classes we + // could care about, but this is all we worried about for now. The animation works by + // first triggering the enter state with the newest breadcrumb off screen (-40px) then + // sliding it into view. + &.mx_RoomBreadcrumbs2-enter { + margin-left: -40px; // 32px for the avatar, 8px for the margin + } + &.mx_RoomBreadcrumbs2-enter-active { + margin-left: 0; + + // Timing function is as-requested by design. + // NOTE: The transition time MUST match the value passed to CSSTransition! + transition: margin-left 300ms cubic-bezier(0.66, 0.02, 0.36, 1); } .mx_RoomBreadcrumbs2_placeholder { diff --git a/src/components/views/rooms/RoomBreadcrumbs2.tsx b/src/components/views/rooms/RoomBreadcrumbs2.tsx index 195757ccf0..197170018a 100644 --- a/src/components/views/rooms/RoomBreadcrumbs2.tsx +++ b/src/components/views/rooms/RoomBreadcrumbs2.tsx @@ -23,6 +23,7 @@ import { Room } from "matrix-js-sdk/src/models/room"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import Analytics from "../../../Analytics"; import { UPDATE_EVENT } from "../../../stores/AsyncStore"; +import { CSSTransition, TransitionGroup } from "react-transition-group"; /******************************************************************* * CAUTION * @@ -36,6 +37,14 @@ interface IProps { } interface IState { + // Both of these control the animation for the breadcrumbs. For details on the + // actual animation, see the CSS. + // + // doAnimation is to lie to the CSSTransition component (see onBreadcrumbsUpdate + // for info). skipFirst is used to try and reduce jerky animation - also see the + // breadcrumb update function for info on that. + doAnimation: boolean; + skipFirst: boolean; } export default class RoomBreadcrumbs2 extends React.PureComponent { @@ -44,6 +53,11 @@ export default class RoomBreadcrumbs2 extends React.PureComponent { if (!this.isMounted) return; - this.forceUpdate(); // we have no state, so this is the best we can do + + // We need to trick the CSSTransition component into updating, which means we need to + // tell it to not animate, then to animate a moment later. This causes two updates + // which means two renders. The skipFirst change is so that our don't-animate state + // doesn't show the breadcrumb we're about to reveal as it causes a visual jump/jerk. + // The second update, on the next available tick, causes the "enter" animation to start + // again and this time we want to show the newest breadcrumb because it'll be hidden + // off screen for the animation. + this.setState({doAnimation: false, skipFirst: true}); + setTimeout(() => this.setState({doAnimation: true, skipFirst: false}), 0); }; private viewRoom = (room: Room, index: number) => { @@ -77,14 +100,26 @@ export default class RoomBreadcrumbs2 extends React.PureComponent - {_t("No recently visited rooms")} + if (tiles.length > 0) { + // NOTE: The CSSTransition timeout MUST match the timeout in our CSS! + return ( + +
+ {tiles.slice(this.state.skipFirst ? 1 : 0)} +
+
+ ); + } else { + return ( +
+
+ {_t("No recently visited rooms")} +
); } - - return
{tiles}
; } } diff --git a/tsconfig.json b/tsconfig.json index 8a01ca335e..db040d1f31 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,8 @@ "types": [ "node", "react", - "flux" + "flux", + "react-transition-group" ] }, "include": [ diff --git a/yarn.lock b/yarn.lock index 333c5ccf20..32f8d3093e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -968,7 +968,7 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": version "7.10.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.2.tgz#d103f21f2602497d38348a32e008637d506db839" integrity sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg== @@ -1352,6 +1352,13 @@ dependencies: "@types/react" "*" +"@types/react-transition-group@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.0.tgz#882839db465df1320e4753e6e9f70ca7e9b4d46d" + integrity sha512-/QfLHGpu+2fQOqQaXh8MG9q03bFENooTb/it4jr5kKaZlDQfWvjqWZg48AwzPVMBHlRuTRAY7hRHCEOXz5kV6w== + dependencies: + "@types/react" "*" + "@types/react@*", "@types/react@^16.9": version "16.9.35" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.35.tgz#a0830d172e8aadd9bd41709ba2281a3124bbd368" @@ -2835,7 +2842,7 @@ cssstyle@^1.0.0: dependencies: cssom "0.3.x" -csstype@^2.2.0: +csstype@^2.2.0, csstype@^2.6.7: version "2.6.10" resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.10.tgz#e63af50e66d7c266edb6b32909cfd0aabe03928b" integrity sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w== @@ -3054,6 +3061,14 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-helpers@^5.0.1: + version "5.1.4" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.1.4.tgz#4609680ab5c79a45f2531441f1949b79d6587f4b" + integrity sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^2.6.7" + dom-serializer@0, dom-serializer@^0.2.1: version "0.2.2" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" @@ -7136,6 +7151,16 @@ react-test-renderer@^16.0.0-0, react-test-renderer@^16.9.0: react-is "^16.8.6" scheduler "^0.19.1" +react-transition-group@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9" + integrity sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react@^16.9.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e" From db23aaad8366f04bbecf4daf2be7acfc9bd5a5b2 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 9 Jun 2020 15:22:37 -0600 Subject: [PATCH 08/23] Destroy old CommunityFilterConditions when they aren't needed Fixes https://github.com/vector-im/riot-web/issues/13971 --- src/stores/room-list/TagWatcher.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/stores/room-list/TagWatcher.ts b/src/stores/room-list/TagWatcher.ts index 1fb5223e00..22302b695d 100644 --- a/src/stores/room-list/TagWatcher.ts +++ b/src/stores/room-list/TagWatcher.ts @@ -74,6 +74,11 @@ export class TagWatcher { this.store.removeFilter(filter); } + // Destroy any and all old filter conditions to prevent resource leaks + for (const filter of this.filters.values()) { + filter.destroy(); + } + this.filters = newFilters; } }; From fed52f274e796fc28586ee49614b51fd0128df25 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 9 Jun 2020 15:23:34 -0600 Subject: [PATCH 09/23] Fix custom theme use with new room list Fixes https://github.com/vector-im/riot-web/issues/13968 We were grabbing "custom-" instead of the actual theme name. --- src/components/structures/UserMenuButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/UserMenuButton.tsx b/src/components/structures/UserMenuButton.tsx index d8f96d4a91..5fbab796a6 100644 --- a/src/components/structures/UserMenuButton.tsx +++ b/src/components/structures/UserMenuButton.tsx @@ -80,7 +80,7 @@ export default class UserMenuButton extends React.Component { private isUserOnDarkTheme(): boolean { const theme = SettingsStore.getValue("theme"); if (theme.startsWith("custom-")) { - return getCustomTheme(theme.substring(0, 7)).is_dark; + return getCustomTheme(theme.substring("custom-".length)).is_dark; } return theme === "dark"; } From c3608006314eba7defd1c27b0a550c2eb15ad3f6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 9 Jun 2020 15:26:13 -0600 Subject: [PATCH 10/23] Add token.remove() handling to room list temp proxy Fixes https://github.com/vector-im/riot-web/issues/13930 --- .../room-list/RoomListStoreTempProxy.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/stores/room-list/RoomListStoreTempProxy.ts b/src/stores/room-list/RoomListStoreTempProxy.ts index 0268cf0a46..13129f4bdb 100644 --- a/src/stores/room-list/RoomListStoreTempProxy.ts +++ b/src/stores/room-list/RoomListStoreTempProxy.ts @@ -31,11 +31,14 @@ export class RoomListStoreTempProxy { return SettingsStore.isFeatureEnabled("feature_new_room_list"); } - public static addListener(handler: () => void) { + public static addListener(handler: () => void): RoomListStoreTempToken { if (RoomListStoreTempProxy.isUsingNewStore()) { - return RoomListStore.instance.on(UPDATE_EVENT, handler); + const offFn = () => RoomListStore.instance.off(UPDATE_EVENT, handler); + RoomListStore.instance.on(UPDATE_EVENT, handler); + return new RoomListStoreTempToken(offFn); } else { - return OldRoomListStore.addListener(handler); + const token = OldRoomListStore.addListener(handler); + return new RoomListStoreTempToken(() => token.remove()); } } @@ -47,3 +50,13 @@ export class RoomListStoreTempProxy { } } } + + +export class RoomListStoreTempToken { + constructor(private offFn: () => void) { + } + + public remove(): void { + this.offFn(); + } +} From 84174cc44061d713c9fafad100790249a9a4b574 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 9 Jun 2020 15:30:03 -0600 Subject: [PATCH 11/23] Remove 1 extra line --- src/stores/room-list/RoomListStoreTempProxy.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/stores/room-list/RoomListStoreTempProxy.ts b/src/stores/room-list/RoomListStoreTempProxy.ts index 13129f4bdb..0a173d53a9 100644 --- a/src/stores/room-list/RoomListStoreTempProxy.ts +++ b/src/stores/room-list/RoomListStoreTempProxy.ts @@ -51,7 +51,6 @@ export class RoomListStoreTempProxy { } } - export class RoomListStoreTempToken { constructor(private offFn: () => void) { } From b84af372b9800225f06d45077da87a2d13182e20 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 9 Jun 2020 15:35:07 -0600 Subject: [PATCH 12/23] Bump animation time for breadcrumbs up to 640ms This matches the design, unlike the 300ms which was too fast. --- res/css/views/rooms/_RoomBreadcrumbs2.scss | 2 +- src/components/views/rooms/RoomBreadcrumbs2.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/views/rooms/_RoomBreadcrumbs2.scss b/res/css/views/rooms/_RoomBreadcrumbs2.scss index 68cf7d7500..ac5a9fc34e 100644 --- a/res/css/views/rooms/_RoomBreadcrumbs2.scss +++ b/res/css/views/rooms/_RoomBreadcrumbs2.scss @@ -39,7 +39,7 @@ limitations under the License. // Timing function is as-requested by design. // NOTE: The transition time MUST match the value passed to CSSTransition! - transition: margin-left 300ms cubic-bezier(0.66, 0.02, 0.36, 1); + transition: margin-left 640ms cubic-bezier(0.66, 0.02, 0.36, 1); } .mx_RoomBreadcrumbs2_placeholder { diff --git a/src/components/views/rooms/RoomBreadcrumbs2.tsx b/src/components/views/rooms/RoomBreadcrumbs2.tsx index 197170018a..1b912b39d6 100644 --- a/src/components/views/rooms/RoomBreadcrumbs2.tsx +++ b/src/components/views/rooms/RoomBreadcrumbs2.tsx @@ -104,7 +104,7 @@ export default class RoomBreadcrumbs2 extends React.PureComponent
From 5f8b7187cf2859471b6d88a445dd979288dc0280 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 9 Jun 2020 19:48:31 -0600 Subject: [PATCH 13/23] Update resize handle for new designs The diff should have information on what this does and how it is supposed to work. --- res/css/views/rooms/_RoomSublist2.scss | 71 +++++++++++++++++-- src/components/views/rooms/RoomSublist2.tsx | 76 +++++++++++++-------- src/i18n/strings/en_EN.json | 3 +- src/stores/room-list/ListLayout.ts | 19 +++++- 4 files changed, 132 insertions(+), 37 deletions(-) diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index cfb9bc3b6d..b550b2f002 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -16,10 +16,6 @@ limitations under the License. // TODO: Rename to mx_RoomSublist during replacement of old component -// TODO: Just use the 3 selectors we need from this instead of importing it. -// We're going to end up with heavy modifications anyways. -@import "../../../../node_modules/react-resizable/css/styles.css"; - .mx_RoomSublist2 { // The sublist is a column of rows, essentially display: flex; @@ -63,18 +59,83 @@ limitations under the License. } .mx_RoomSublist2_resizeBox { + margin-bottom: 4px; // for the resize handle + position: relative; + // Create another flexbox column for the tiles display: flex; flex-direction: column; overflow: hidden; .mx_RoomSublist2_showMoreButton { - height: 44px; // 1 room tile high cursor: pointer; + font-size: $font-13px; + line-height: $font-18px; + color: $roomtile2-preview-color; + + // This is the same color as the left panel background because it needs + // to occlude the lastmost tile in the list. + background-color: $header-panel-bg-color; + + // Update the render() function for RoomSublist2 if these change + // Update the ListLayout class for minVisibleTiles if these change. + // + // At 24px high and 8px padding on the top this equates to 0.65 of + // a tile due to how the padding calculations work. + height: 24px; + padding-top: 8px; + + // We force this to the bottom so it will overlap rooms as needed. + // We account for the space it takes up (24px) in the code through padding. + position: absolute; + bottom: 4px; // the height of the resize handle + left: 0; + right: 0; // We create a flexbox to cheat at alignment display: flex; align-items: center; + + .mx_RoomSublist2_showMoreButtonChevron { + position: relative; + width: 16px; + height: 16px; + margin-left: 12px; + margin-right: 18px; + mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background: $roomtile2-preview-color; + } + } + + // Class name comes from the ResizableBox component + // The hover state needs to use the whole sublist, not just the resizable box, + // so that selector is below and one level higher. + .react-resizable-handle { + cursor: ns-resize; + border-radius: 2px; + + // This is positioned directly below the 'show more' button. + position: absolute; + bottom: 0; + left: 0; + right: 0; + + // This is to visually align the bar in the list. Should be 12px from + // either side of the list. We define this after the positioning to + // trick the browser. + margin-left: 4px; + margin-right: 8px; } } + + // The aforementioned selector for the hover state. + &:hover .react-resizable-handle { + opacity: 0.2; + + // Update the render() function for RoomSublist2 if this changes + border: 2px solid $primary-fg-color; + } } diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index cd27156cbd..4ad803d046 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -178,46 +178,61 @@ export default class RoomSublist2 extends React.Component { let content = null; if (tiles.length > 0) { + const layout = this.props.layout; // to shorten calls + // TODO: Lazy list rendering // TODO: Whatever scrolling magic needs to happen here - const layout = this.props.layout; // to shorten calls - const minTilesPx = layout.tilesToPixels(Math.min(tiles.length, layout.minVisibleTiles)); - const maxTilesPx = layout.tilesToPixels(tiles.length); - const tilesPx = layout.tilesToPixels(Math.min(tiles.length, layout.visibleTiles)); + + const nVisible = Math.floor(layout.visibleTiles); + const visibleTiles = tiles.slice(0, nVisible); + + // If we're hiding rooms, show a 'show more' button to the user. This button + // floats above the resize handle, if we have one present + let showMoreButton = null; + if (tiles.length > nVisible) { + // we have a cutoff condition - add the button to show all + const numMissing = tiles.length - visibleTiles.length; + showMoreButton = ( +
+ + {/* set by CSS masking */} + + + {_t("Show %(count)s more", {count: numMissing})} + +
+ ); + } + + // Figure out if we need a handle let handles = ['s']; if (layout.visibleTiles >= tiles.length && tiles.length <= layout.minVisibleTiles) { handles = []; // no handles, we're at a minimum } - // TODO: This might need adjustment, however for now it is fine as a round. - const nVisible = Math.round(layout.visibleTiles); - const visibleTiles = tiles.slice(0, nVisible); + // We have to account for padding so we can accommodate a 'show more' button and + // the resize handle, which are pinned to the bottom of the container. This is the + // easiest way to have a resize handle below the button as otherwise we're writing + // our own resize handling and that doesn't sound fun. + // + // The layout class has some helpers for dealing with padding, as we don't want to + // apply it in all cases. If we apply it in all cases, the resizing feels like it + // goes backwards and can become wildly incorrect (visibleTiles says 18 when there's + // only mathematically 7 possible). - // If we're hiding rooms, show a 'show more' button to the user. This button - // replaces the last visible tile, so will always show 2+ rooms. We do this - // because if it said "show 1 more room" we had might as well show that room - // instead. We also replace the last item so we don't have to adjust our math - // on pixel heights, etc. It's much easier to pretend the button is a tile. - if (tiles.length > nVisible) { - // we have a cutoff condition - add the button to show all + const showMoreHeight = 32; // As defined by CSS + const resizeHandleHeight = 4; // As defined by CSS - // we +1 to account for the room we're about to hide with our 'show more' button - // this results in the button always being 1+, and not needing an i18n `count`. - const numMissing = (tiles.length - visibleTiles.length) + 1; + // The padding is variable though, so figure out what we need padding for. + let padding = 0; + if (showMoreButton) padding += showMoreHeight; + if (handles.length > 0) padding += resizeHandleHeight; + + const minTilesPx = layout.calculateTilesToPixelsMin(tiles.length, layout.minVisibleTiles, padding); + const maxTilesPx = layout.tilesToPixelsWithPadding(tiles.length, padding); + const tilesWithoutPadding = Math.min(tiles.length, layout.visibleTiles); + const tilesPx = layout.calculateTilesToPixelsMin(tiles.length, tilesWithoutPadding, padding); - // TODO: CSS TBD - // TODO: Make this an actual tile - // TODO: This is likely to pop out of the list, consider that. - visibleTiles.splice(visibleTiles.length - 1, 1, ( -
- {_t("Show %(n)s more", {n: numMissing})} -
- )); - } content = ( { className="mx_RoomSublist2_resizeBox" > {visibleTiles} + {showMoreButton} ) } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8a1d112e5d..f4880804b4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1134,7 +1134,8 @@ "Securely back up your keys to avoid losing them. Learn more.": "Securely back up your keys to avoid losing them. Learn more.", "Not now": "Not now", "Don't ask me again": "Don't ask me again", - "Show %(n)s more": "Show %(n)s more", + "Show %(count)s more|one": "Show %(count)s more", + "Show %(count)s more|other": "Show %(count)s more", "Options": "Options", "%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.", "%(count)s unread messages including mentions.|one": "1 unread mention.", diff --git a/src/stores/room-list/ListLayout.ts b/src/stores/room-list/ListLayout.ts index a6abb7d37a..afcab8e12a 100644 --- a/src/stores/room-list/ListLayout.ts +++ b/src/stores/room-list/ListLayout.ts @@ -52,7 +52,24 @@ export class ListLayout { } public get minVisibleTiles(): number { - return 3; + // the .65 comes from the CSS where the show more button is + // mathematically 65% of a tile when floating. + return 4.65; + } + + public calculateTilesToPixelsMin(maxTiles: number, n: number, possiblePadding: number): number { + // Only apply the padding if we're about to use maxTiles as we need to + // plan for the padding. If we're using n, the padding is already accounted + // for by the resizing stuff. + let padding = 0; + if (maxTiles < n) { + padding = possiblePadding; + } + return this.tilesToPixels(Math.min(maxTiles, n)) + padding; + } + + public tilesToPixelsWithPadding(n: number, padding: number): number { + return this.tilesToPixels(n) + padding; } public tilesToPixels(n: number): number { From 8ec6d4ce7627fb6865e21526ce958359cfd8274b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 9 Jun 2020 19:53:07 -0600 Subject: [PATCH 14/23] Appease i18n --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f4880804b4..1771e0d16c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1134,8 +1134,8 @@ "Securely back up your keys to avoid losing them. Learn more.": "Securely back up your keys to avoid losing them. Learn more.", "Not now": "Not now", "Don't ask me again": "Don't ask me again", - "Show %(count)s more|one": "Show %(count)s more", "Show %(count)s more|other": "Show %(count)s more", + "Show %(count)s more|one": "Show %(count)s more", "Options": "Options", "%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.", "%(count)s unread messages including mentions.|one": "1 unread mention.", From f70ada4d6d592285d2d627cda4b0dc4110502538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 10 Jun 2020 12:58:08 +0200 Subject: [PATCH 15/23] Searching: Delete all the fields that we use to get back event verification. --- src/Searching.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Searching.js b/src/Searching.js index fb59cc563d..9631afc36b 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -123,6 +123,9 @@ async function localSearch(searchTerm, roomId = undefined) { ev._forwardingCurve25519KeyChain = ev.event.forwardingCurve25519KeyChain; delete ev.event.curve25519Key; + delete ev.event.ed25519Key; + delete ev.event.algorithm; + delete ev.event.forwardingCurve25519KeyChain; } } } From 0fb6846c9dd12d61ce07405b5eb53a4747576509 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 10 Jun 2020 13:03:00 +0100 Subject: [PATCH 16/23] Radio buttons --- res/css/_components.scss | 1 + .../views/elements/_StyledRadioButton.scss | 95 +++++++++++++++++++ .../views/elements/StyledRadioButton.tsx | 38 ++++++++ 3 files changed, 134 insertions(+) create mode 100644 res/css/views/elements/_StyledRadioButton.scss create mode 100644 src/components/views/elements/StyledRadioButton.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index f0073eff81..fb804ad09d 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -118,6 +118,7 @@ @import "./views/elements/_Slider.scss"; @import "./views/elements/_Spinner.scss"; @import "./views/elements/_StyledCheckbox.scss"; +@import "./views/elements/_StyledRadioButton.scss"; @import "./views/elements/_SyntaxHighlight.scss"; @import "./views/elements/_TextWithTooltip.scss"; @import "./views/elements/_ToggleSwitch.scss"; diff --git a/res/css/views/elements/_StyledRadioButton.scss b/res/css/views/elements/_StyledRadioButton.scss new file mode 100644 index 0000000000..4fcb598498 --- /dev/null +++ b/res/css/views/elements/_StyledRadioButton.scss @@ -0,0 +1,95 @@ +/* +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. +*/ + +/** +* This component expects the parent to specify a positive padding and +* width +*/ + +.mx_RadioButton { + + $radio-circle-color: $muted-fg-color; + $active-radio-circle-color: $accent-color; + position: relative; + + display: flex; + align-items: center; + flex-grow: 1; + + > span { + flex-grow: 1; + + display: flex; + + margin-left: 10px; + margin-right: 10px; + } + + .mx_RadioButton_spacer { + flex-shrink: 0; + flex-grow: 0; + + height: $font-16px; + width: $font-16px; + } + + > input[type=radio] { + // Remove the OS's representation + margin: 0; + padding: 0; + display: none; + + + div { + flex-shrink: 0; + flex-grow: 0; + + display: flex; + align-items: center; + justify-content: center; + + box-sizing: border-box; + height: $font-16px; + width: $font-16px; + + border: $font-1-5px solid $radio-circle-color; + border-radius: $font-16px; + + > div { + box-sizing: border-box; + + height: $font-8px; + width: $font-8px; + + border-radius: $font-8px; + } + } + } + + > input[type=radio]:checked { + + div { + border-color: $active-radio-circle-color; + + > div { + background: $active-radio-circle-color; + } + } + } + + > input[type=radio]:disabled { + + div { + > div { + display: none; + } + } + } +} diff --git a/src/components/views/elements/StyledRadioButton.tsx b/src/components/views/elements/StyledRadioButton.tsx new file mode 100644 index 0000000000..3636d46b7a --- /dev/null +++ b/src/components/views/elements/StyledRadioButton.tsx @@ -0,0 +1,38 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. +Licensed under the Apache License, Version 2.0 (the 'License'); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an 'AS IS' BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import classnames from 'classnames'; + +interface IProps extends React.InputHTMLAttributes { +} + +interface IState { +} + +export default class StyledRadioButton extends React.PureComponent { + public static readonly defaultProps = { + className: '', + } + + public render() { + const { children, className, ...otherProps } = this.props; + return