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.
This commit is contained in:
Travis Ralston 2021-01-18 18:48:07 -07:00
parent 2548a438ae
commit cfb583d193
4 changed files with 111 additions and 49 deletions

View file

@ -102,11 +102,10 @@ export default class AppsDrawer extends React.Component {
}, },
onResizeStop: () => { onResizeStop: () => {
this._resizeContainer.classList.remove("mx_AppsDrawer_resizing"); this._resizeContainer.classList.remove("mx_AppsDrawer_resizing");
// persist to localStorage WidgetLayoutStore.instance.setResizerDistributions(
localStorage.setItem(this._getStorageKey(), JSON.stringify([ this.props.room, Container.Top,
this.state.apps.map(app => app.id), this.state.apps.slice(1).map((_, i) => this.resizer.forHandleAt(i).size),
...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 // 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(); this._loadResizerPreferences();
}; };
_getStorageKey = () => `mx_apps_drawer-${this.props.room.roomId}`;
_getAppsHash = (apps) => apps.map(app => app.id).join("~"); _getAppsHash = (apps) => apps.map(app => app.id).join("~");
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
@ -147,24 +144,16 @@ export default class AppsDrawer extends React.Component {
}; };
_loadResizerPreferences = () => { _loadResizerPreferences = () => {
try { const distributions = WidgetLayoutStore.instance.getResizerDistributions(this.props.room, Container.Top);
const [[...lastIds], ...sizes] = JSON.parse(localStorage.getItem(this._getStorageKey())); if (this.state.apps && (this.state.apps.length - 1) === distributions.length) {
// Every app was included in the last split, reuse the last sizes distributions.forEach((size, i) => {
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); const distributor = this.resizer.forHandleAt(i);
if (distributor) { if (distributor) {
distributor.size = size; distributor.size = size;
distributor.finish(); distributor.finish();
} }
}); });
return; } else if (this.state.apps) {
}
} catch (e) {
// this is expected
}
if (this.state.apps) {
const distributors = this.resizer.getDistributors(); const distributors = this.resizer.getDistributors();
distributors.forEach(d => d.item.clearSize()); distributors.forEach(d => d.item.clearSize());
distributors.forEach(d => d.start()); distributors.forEach(d => d.start());

View file

@ -18,12 +18,11 @@ import SettingsStore from "../../settings/SettingsStore";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import WidgetStore, { IApp } from "../WidgetStore"; import WidgetStore, { IApp } from "../WidgetStore";
import { WidgetType } from "../../widgets/WidgetType"; import { WidgetType } from "../../widgets/WidgetType";
import { clamp, defaultNumber } from "../../utils/numbers"; import { clamp, defaultNumber, sum } from "../../utils/numbers";
import { EventEmitter } from "events";
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
import defaultDispatcher from "../../dispatcher/dispatcher"; import defaultDispatcher from "../../dispatcher/dispatcher";
import { ReadyWatchingStore } from "../ReadyWatchingStore"; import { ReadyWatchingStore } from "../ReadyWatchingStore";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { SettingLevel } from "../../settings/SettingLevel";
export const WIDGET_LAYOUT_EVENT_TYPE = "io.element.widgets.layout"; export const WIDGET_LAYOUT_EVENT_TYPE = "io.element.widgets.layout";
@ -117,13 +116,11 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
} }
protected async onReady(): Promise<any> { protected async onReady(): Promise<any> {
this.byRoom = {}; this.updateAllRooms();
for (const room of this.matrixClient.getVisibleRooms()) {
this.recalculateRoom(room);
}
this.matrixClient.on("RoomState.events", this.updateRoomFromState); 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 // TODO: Register WidgetStore listener
} }
@ -131,10 +128,26 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
this.byRoom = {}; this.byRoom = {};
} }
private updateAllRooms() {
this.byRoom = {};
for (const room of this.matrixClient.getVisibleRooms()) {
this.recalculateRoom(room);
}
}
private updateRoomFromState = (ev: MatrixEvent) => { private updateRoomFromState = (ev: MatrixEvent) => {
if (ev.getType() !== WIDGET_LAYOUT_EVENT_TYPE) return; if (ev.getType() !== WIDGET_LAYOUT_EVENT_TYPE) return;
const room = this.matrixClient.getRoom(ev.getRoomId()); 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) { private recalculateRoom(room: Room) {
@ -145,6 +158,8 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
return; return;
} }
const beforeChanges = JSON.stringify(this.byRoom[room.roomId]);
const layoutEv = room.currentState.getStateEvents(WIDGET_LAYOUT_EVENT_TYPE, ""); const layoutEv = room.currentState.getStateEvents(WIDGET_LAYOUT_EVENT_TYPE, "");
const legacyPinned = SettingsStore.getValue("Widgets.pinned", room.roomId); const legacyPinned = SettingsStore.getValue("Widgets.pinned", room.roomId);
let userLayout = SettingsStore.getValue<ILayoutSettings>("Widgets.layout", room.roomId); let userLayout = SettingsStore.getValue<ILayoutSettings>("Widgets.layout", room.roomId);
@ -239,36 +254,90 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
for (const width of widths) { for (const width of widths) {
remainingWidth -= width; 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) { if (doAutobalance) {
for (let i = 0; i < widths.length; i++) { for (let i = 0; i < widths.length; i++) {
widths[i] = 100 / widths.length; 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 // Finally, fill in our cache and update
this.byRoom[room.roomId] = { this.byRoom[room.roomId] = {};
[Container.Top]: { if (topWidgets.length) {
this.byRoom[room.roomId][Container.Top] = {
ordered: topWidgets, ordered: topWidgets,
distributions: widths, distributions: widths,
height: maxHeight, height: maxHeight,
},
[Container.Right]: {
ordered: rightWidgets,
},
}; };
}
if (rightWidgets.length) {
this.byRoom[room.roomId][Container.Right] = {
ordered: rightWidgets,
};
}
const afterChanges = JSON.stringify(this.byRoom[room.roomId]);
if (afterChanges !== beforeChanges) {
this.emitFor(room); this.emitFor(room);
} }
}
public getContainerWidgets(room: Room, container: Container): IApp[] { public getContainerWidgets(room: Room, container: Container): IApp[] {
return this.byRoom[room.roomId]?.[container]?.ordered || []; 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; window.mxWidgetLayoutStore = WidgetLayoutStore.instance;

View file

@ -28,7 +28,7 @@ import {WidgetType} from "../widgets/WidgetType";
import {objectClone} from "./objects"; import {objectClone} from "./objects";
import {_t} from "../languageHandler"; import {_t} from "../languageHandler";
import {Capability, IWidgetData, MatrixCapabilities} from "matrix-widget-api"; 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 // How long we wait for the state event echo to come back from the server
// before waitFor[Room/User]Widget rejects its promise // before waitFor[Room/User]Widget rejects its promise

View file

@ -28,3 +28,7 @@ export function defaultNumber(i: unknown, def: number): number {
export function clamp(i: number, min: number, max: number): number { export function clamp(i: number, min: number, max: number): number {
return Math.min(Math.max(i, min), max); return Math.min(Math.max(i, min), max);
} }
export function sum(...i: number[]): number {
return [...i].reduce((p, c) => c + p, 0);
}