mirror of
https://github.com/element-hq/element-web
synced 2024-11-25 02:35:48 +03:00
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:
parent
2548a438ae
commit
cfb583d193
4 changed files with 111 additions and 49 deletions
|
@ -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());
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue