From cfb583d193d2dab9f5b2244bf12e4258741a7705 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 18 Jan 2021 18:48:07 -0700 Subject: [PATCH] Calculate widget widths in the WidgetLayoutStore Note that this ditches all previously set width values, however this is probably acceptable for now. Trying to remain backwards compatible gets tricky on top of already tricky code, and the impact of Element forgetting widths is not as severe as forgetting which widgets were/are pinned. --- src/components/views/rooms/AppsDrawer.js | 39 +++----- src/stores/widgets/WidgetLayoutStore.ts | 115 ++++++++++++++++++----- src/utils/WidgetUtils.ts | 2 +- src/utils/numbers.ts | 4 + 4 files changed, 111 insertions(+), 49 deletions(-) diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 924cda0b64..5982c52d98 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -102,11 +102,10 @@ export default class AppsDrawer extends React.Component { }, onResizeStop: () => { this._resizeContainer.classList.remove("mx_AppsDrawer_resizing"); - // persist to localStorage - localStorage.setItem(this._getStorageKey(), JSON.stringify([ - this.state.apps.map(app => app.id), - ...this.state.apps.slice(1).map((_, i) => this.resizer.forHandleAt(i).size), - ])); + WidgetLayoutStore.instance.setResizerDistributions( + this.props.room, Container.Top, + this.state.apps.slice(1).map((_, i) => this.resizer.forHandleAt(i).size), + ); }, }; // pass a truthy container for now, we won't call attach until we update it @@ -128,8 +127,6 @@ export default class AppsDrawer extends React.Component { this._loadResizerPreferences(); }; - _getStorageKey = () => `mx_apps_drawer-${this.props.room.roomId}`; - _getAppsHash = (apps) => apps.map(app => app.id).join("~"); componentDidUpdate(prevProps, prevState) { @@ -147,24 +144,16 @@ export default class AppsDrawer extends React.Component { }; _loadResizerPreferences = () => { - try { - const [[...lastIds], ...sizes] = JSON.parse(localStorage.getItem(this._getStorageKey())); - // Every app was included in the last split, reuse the last sizes - if (this.state.apps.length <= lastIds.length && this.state.apps.every((app, i) => lastIds[i] === app.id)) { - sizes.forEach((size, i) => { - const distributor = this.resizer.forHandleAt(i); - if (distributor) { - distributor.size = size; - distributor.finish(); - } - }); - return; - } - } catch (e) { - // this is expected - } - - if (this.state.apps) { + const distributions = WidgetLayoutStore.instance.getResizerDistributions(this.props.room, Container.Top); + if (this.state.apps && (this.state.apps.length - 1) === distributions.length) { + distributions.forEach((size, i) => { + const distributor = this.resizer.forHandleAt(i); + if (distributor) { + distributor.size = size; + distributor.finish(); + } + }); + } else if (this.state.apps) { const distributors = this.resizer.getDistributors(); distributors.forEach(d => d.item.clearSize()); distributors.forEach(d => d.start()); diff --git a/src/stores/widgets/WidgetLayoutStore.ts b/src/stores/widgets/WidgetLayoutStore.ts index fddfaf65b3..7d22f6729e 100644 --- a/src/stores/widgets/WidgetLayoutStore.ts +++ b/src/stores/widgets/WidgetLayoutStore.ts @@ -18,12 +18,11 @@ import SettingsStore from "../../settings/SettingsStore"; import { Room } from "matrix-js-sdk/src/models/room"; import WidgetStore, { IApp } from "../WidgetStore"; import { WidgetType } from "../../widgets/WidgetType"; -import { clamp, defaultNumber } from "../../utils/numbers"; -import { EventEmitter } from "events"; -import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; +import { clamp, defaultNumber, sum } from "../../utils/numbers"; import defaultDispatcher from "../../dispatcher/dispatcher"; import { ReadyWatchingStore } from "../ReadyWatchingStore"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { SettingLevel } from "../../settings/SettingLevel"; export const WIDGET_LAYOUT_EVENT_TYPE = "io.element.widgets.layout"; @@ -117,13 +116,11 @@ export class WidgetLayoutStore extends ReadyWatchingStore { } protected async onReady(): Promise { - this.byRoom = {}; - for (const room of this.matrixClient.getVisibleRooms()) { - this.recalculateRoom(room); - } + this.updateAllRooms(); this.matrixClient.on("RoomState.events", this.updateRoomFromState); - // TODO: Register settings listeners + SettingsStore.watchSetting("Widgets.pinned", null, this.updateFromSettings); + SettingsStore.watchSetting("Widgets.layout", null, this.updateFromSettings); // TODO: Register WidgetStore listener } @@ -131,10 +128,26 @@ export class WidgetLayoutStore extends ReadyWatchingStore { this.byRoom = {}; } + private updateAllRooms() { + this.byRoom = {}; + for (const room of this.matrixClient.getVisibleRooms()) { + this.recalculateRoom(room); + } + } + private updateRoomFromState = (ev: MatrixEvent) => { if (ev.getType() !== WIDGET_LAYOUT_EVENT_TYPE) return; const room = this.matrixClient.getRoom(ev.getRoomId()); - this.recalculateRoom(room); + if (room) this.recalculateRoom(room); + }; + + private updateFromSettings = (settingName: string, roomId: string, /* and other stuff */) => { + if (roomId) { + const room = this.matrixClient.getRoom(roomId); + if (room) this.recalculateRoom(room); + } else { + this.updateAllRooms(); + } }; private recalculateRoom(room: Room) { @@ -145,6 +158,8 @@ export class WidgetLayoutStore extends ReadyWatchingStore { return; } + const beforeChanges = JSON.stringify(this.byRoom[room.roomId]); + const layoutEv = room.currentState.getStateEvents(WIDGET_LAYOUT_EVENT_TYPE, ""); const legacyPinned = SettingsStore.getValue("Widgets.pinned", room.roomId); let userLayout = SettingsStore.getValue("Widgets.layout", room.roomId); @@ -239,36 +254,90 @@ export class WidgetLayoutStore extends ReadyWatchingStore { for (const width of widths) { remainingWidth -= width; } - if (topWidgets.length > 1 && remainingWidth < MIN_WIDGET_WIDTH_PCT) { - const toReclaim = MIN_WIDGET_WIDTH_PCT - remainingWidth; - for (let i = 0; i < widths.length - 1; i++) { - widths[i] = widths[i] - (toReclaim / (widths.length - 1)); - } - widths[widths.length - 1] = MIN_WIDGET_WIDTH_PCT; - } if (doAutobalance) { for (let i = 0; i < widths.length; i++) { widths[i] = 100 / widths.length; } } + // TODO: There is probably a more efficient way to do this. + // All we're doing is making sure that our widths sum up to 100 and take + // any excess width off all widgets equally to keep the proportions. + let toReclaim = sum(...widths) - 100; + while (toReclaim > 0 && topWidgets.length > 0) { + for (let i = 0; i < widths.length; i++) { + if (toReclaim <= 0) break; + const w = widths[i]; + const adjusted = clamp(w - 1, MIN_WIDGET_WIDTH_PCT, 100); + if (adjusted !== w) { + toReclaim -= 1; + widths[i] = adjusted; + } + } + } + // Finally, fill in our cache and update - this.byRoom[room.roomId] = { - [Container.Top]: { + this.byRoom[room.roomId] = {}; + if (topWidgets.length) { + this.byRoom[room.roomId][Container.Top] = { ordered: topWidgets, distributions: widths, height: maxHeight, - }, - [Container.Right]: { + }; + } + if (rightWidgets.length) { + this.byRoom[room.roomId][Container.Right] = { ordered: rightWidgets, - }, - }; - this.emitFor(room); + }; + } + + const afterChanges = JSON.stringify(this.byRoom[room.roomId]); + if (afterChanges !== beforeChanges) { + this.emitFor(room); + } } public getContainerWidgets(room: Room, container: Container): IApp[] { return this.byRoom[room.roomId]?.[container]?.ordered || []; } + + public getResizerDistributions(room: Room, container: Container): string[] { // yes, string. + let distributions = this.byRoom[room.roomId]?.[container]?.distributions; + if (!distributions || distributions.length < 2) return []; + + // The distributor actually expects to be fed N-1 sizes and expands the middle section + // instead of the edges. Therefore, we need to return [0] when there's two widgets or + // [0, 2] when there's three (skipping [1] because it's irrelevant). + + if (distributions.length === 2) distributions = [distributions[0]]; + if (distributions.length === 3) distributions = [distributions[0], distributions[2]]; + return distributions.map(d => `${d.toFixed(1)}%`); // actual percents - these are decoded later + } + + public setResizerDistributions(room: Room, container: Container, distributions: string[]) { + if (container !== Container.Top) return; // ignore - not relevant + + const numbers = distributions.map(d => Number(Number(d.substring(0, d.length - 1)).toFixed(1))); + const widgets = this.getContainerWidgets(room, container); + + // From getResizerDistributions, we need to fill in the middle size if applicable. + const remaining = 100 - sum(...numbers); + if (numbers.length === 2) numbers.splice(1, 0, remaining); + if (numbers.length === 1) numbers.push(remaining); + + const localLayout = {}; + widgets.forEach((w, i) => { + localLayout[w.id] = { + width: numbers[i], + index: i, + }; + }); + const layoutEv = room.currentState.getStateEvents(WIDGET_LAYOUT_EVENT_TYPE, ""); + SettingsStore.setValue("Widgets.layout", room.roomId, SettingLevel.ROOM_ACCOUNT, { + overrides: layoutEv?.getId(), + widgets: localLayout, + }); + } } window.mxWidgetLayoutStore = WidgetLayoutStore.instance; diff --git a/src/utils/WidgetUtils.ts b/src/utils/WidgetUtils.ts index 986c68342c..815900d97a 100644 --- a/src/utils/WidgetUtils.ts +++ b/src/utils/WidgetUtils.ts @@ -28,7 +28,7 @@ import {WidgetType} from "../widgets/WidgetType"; import {objectClone} from "./objects"; import {_t} from "../languageHandler"; import {Capability, IWidgetData, MatrixCapabilities} from "matrix-widget-api"; -import {IApp} from "../stores/WidgetStore"; // TODO @@ +import {IApp} from "../stores/WidgetStore"; // How long we wait for the state event echo to come back from the server // before waitFor[Room/User]Widget rejects its promise diff --git a/src/utils/numbers.ts b/src/utils/numbers.ts index 1bf48c5117..e26db0d5aa 100644 --- a/src/utils/numbers.ts +++ b/src/utils/numbers.ts @@ -28,3 +28,7 @@ export function defaultNumber(i: unknown, def: number): number { export function clamp(i: number, min: number, max: number): number { return Math.min(Math.max(i, min), max); } + +export function sum(...i: number[]): number { + return [...i].reduce((p, c) => c + p, 0); +}