);
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index ea7a144e53d..7223e9c7ab5 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1871,7 +1871,9 @@
"Threads": "Threads",
"Room Info": "Room Info",
"You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets",
- "Unpin a widget to view it in this panel": "Unpin a widget to view it in this panel",
+ "Maximise widget": "Maximise widget",
+ "Unpin this widget to view it in this panel": "Unpin this widget to view it in this panel",
+ "Close this widget to view it in this panel": "Close this widget to view it in this panel",
"Set my room layout for everyone": "Set my room layout for everyone",
"Widgets": "Widgets",
"Edit widgets, bridges & bots": "Edit widgets, bridges & bots",
diff --git a/src/stores/widgets/WidgetLayoutStore.ts b/src/stores/widgets/WidgetLayoutStore.ts
index 7efc5fb195b..823dd450d22 100644
--- a/src/stores/widgets/WidgetLayoutStore.ts
+++ b/src/stores/widgets/WidgetLayoutStore.ts
@@ -38,8 +38,7 @@ export enum Container {
// changes needed", though this may change in the future.
Right = "right",
- // ... more as needed. Note that most of this code assumes that there
- // are only two containers, and that only the top container is special.
+ Center = "center"
}
export interface IStoredLayout {
@@ -174,7 +173,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
}
};
- private recalculateRoom(room: Room) {
+ public recalculateRoom(room: Room) {
const widgets = WidgetStore.instance.getApps(room.roomId);
if (!widgets?.length) {
this.byRoom[room.roomId] = {};
@@ -195,18 +194,26 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
}
const roomLayout: ILayoutStateEvent = layoutEv ? layoutEv.getContent() : null;
-
- // We essentially just need to find the top container's widgets because we
- // only have two containers. Anything not in the top widget by the end of this
- // function will go into the right container.
+ // We filter for the center container first.
+ // (An error is raised, if there are multiple widgets marked for the center container)
+ // For the right and top container multiple widgets are allowed.
const topWidgets: IApp[] = [];
const rightWidgets: IApp[] = [];
+ const centerWidgets: IApp[] = [];
for (const widget of widgets) {
const stateContainer = roomLayout?.widgets?.[widget.id]?.container;
const manualContainer = userLayout?.widgets?.[widget.id]?.container;
const isLegacyPinned = !!legacyPinned?.[widget.id];
const defaultContainer = WidgetType.JITSI.matches(widget.type) ? Container.Top : Container.Right;
-
+ if ((manualContainer) ? manualContainer === Container.Center : stateContainer === Container.Center) {
+ if (centerWidgets.length) {
+ console.error("Tried to push a second widget into the center container");
+ } else {
+ centerWidgets.push(widget);
+ }
+ // The widget won't need to be put in any other container.
+ continue;
+ }
let targetContainer = defaultContainer;
if (!!manualContainer || !!stateContainer) {
targetContainer = (manualContainer) ? manualContainer : stateContainer;
@@ -323,6 +330,11 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
ordered: rightWidgets,
};
}
+ if (centerWidgets.length) {
+ this.byRoom[room.roomId][Container.Center] = {
+ ordered: centerWidgets,
+ };
+ }
const afterChanges = JSON.stringify(this.byRoom[room.roomId]);
if (afterChanges !== beforeChanges) {
@@ -339,7 +351,11 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
}
public canAddToContainer(room: Room, container: Container): boolean {
- return this.getContainerWidgets(room, container).length < MAX_PINNED;
+ switch (container) {
+ case Container.Top: return this.getContainerWidgets(room, container).length < MAX_PINNED;
+ case Container.Right: return this.getContainerWidgets(room, container).length < MAX_PINNED;
+ case Container.Center: return this.getContainerWidgets(room, container).length < 1;
+ }
}
public getResizerDistributions(room: Room, container: Container): string[] { // yes, string.
@@ -423,11 +439,42 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
public moveToContainer(room: Room, widget: IApp, toContainer: Container) {
const allWidgets = this.getAllWidgets(room);
if (!allWidgets.some(([w]) => w.id === widget.id)) return; // invalid
+ // Prepare other containers (potentially move widgets to obay the following rules)
+ switch (toContainer) {
+ case Container.Right:
+ // new "right" widget
+ break;
+ case Container.Center:
+ // new "center" widget => all other widgets go into "right"
+ for (const w of this.getContainerWidgets(room, Container.Top)) {
+ this.moveToContainer(room, w, Container.Right);
+ }
+ for (const w of this.getContainerWidgets(room, Container.Center)) {
+ this.moveToContainer(room, w, Container.Right);
+ }
+ break;
+ case Container.Top:
+ // new "top" widget => the center widget moves into "right"
+ if (this.hasMaximisedWidget(room)) {
+ this.moveToContainer(room, this.getContainerWidgets(room, Container.Center)[0], Container.Right);
+ }
+ break;
+ }
+
+ // move widgets into requested container.
this.updateUserLayout(room, {
[widget.id]: { container: toContainer },
});
}
+ public hasMaximisedWidget(room: Room) {
+ return this.getContainerWidgets(room, Container.Center).length > 0;
+ }
+
+ public hasPinnedWidgets(room: Room) {
+ return this.getContainerWidgets(room, Container.Top).length > 0;
+ }
+
public canCopyLayoutToRoom(room: Room): boolean {
if (!this.matrixClient) return false; // not ready yet
return room.currentState.maySendStateEvent(WIDGET_LAYOUT_EVENT_TYPE, this.matrixClient.getUserId());
diff --git a/test/stores/WidgetLayoutStore-test.ts b/test/stores/WidgetLayoutStore-test.ts
new file mode 100644
index 00000000000..61399862ab4
--- /dev/null
+++ b/test/stores/WidgetLayoutStore-test.ts
@@ -0,0 +1,125 @@
+/*
+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 "../skinned-sdk"; // Must be first for skinning to work
+import WidgetStore, { IApp } from "../../src/stores/WidgetStore";
+import { Container, WidgetLayoutStore } from "../../src/stores/widgets/WidgetLayoutStore";
+import { Room } from "matrix-js-sdk";
+import { stubClient } from "../test-utils";
+
+// setup test env values
+const roomId = "!room:server";
+const mockRoom = {
+ roomId: roomId,
+ currentState: {
+ getStateEvents: (_l, _x) => {
+ return {
+ getId: ()=>"$layoutEventId",
+ getContent: () => null,
+ };
+ },
+ } };
+
+const mockApps = [
+ { roomId: roomId, id: "1" },
+ { roomId: roomId, id: "2" },
+ { roomId: roomId, id: "3" },
+ { roomId: roomId, id: "4" },
+];
+
+// fake the WidgetStore.instance to just return an object with `getApps`
+jest.spyOn(WidgetStore, 'instance', 'get').mockReturnValue({ getApps: (_room) => mockApps });
+
+describe("WidgetLayoutStore", () => {
+ // we need to init a client so it does not error, when asking for DeviceStorage handlers (SettingsStore.setValue("Widgets.layout"))
+ stubClient();
+
+ const store = WidgetLayoutStore.instance;
+
+ it("all widgets should be in the right container by default", async () => {
+ store.recalculateRoom(mockRoom);
+ expect(store.getContainerWidgets(mockRoom, Container.Right).length).toStrictEqual(mockApps.length);
+ });
+ it("add widget to top container", async () => {
+ store.recalculateRoom(mockRoom);
+ store.moveToContainer(mockRoom, mockApps[0], Container.Top);
+ expect(store.getContainerWidgets(mockRoom, Container.Top)).toStrictEqual([mockApps[0]]);
+ });
+ it("add three widgets to top container", async () => {
+ store.recalculateRoom(mockRoom);
+ store.moveToContainer(mockRoom, mockApps[0], Container.Top);
+ store.moveToContainer(mockRoom, mockApps[1], Container.Top);
+ store.moveToContainer(mockRoom, mockApps[2], Container.Top);
+ expect(new Set(store.getContainerWidgets(mockRoom, Container.Top)))
+ .toEqual(new Set([mockApps[0], mockApps[1], mockApps[2]]));
+ });
+ it("cannot add more than three widgets to top container", async () => {
+ store.recalculateRoom(mockRoom);
+ store.moveToContainer(mockRoom, mockApps[0], Container.Top);
+ store.moveToContainer(mockRoom, mockApps[1], Container.Top);
+ store.moveToContainer(mockRoom, mockApps[2], Container.Top);
+ expect(store.canAddToContainer(mockRoom, Container.Top))
+ .toEqual(false);
+ });
+ it("remove pins when maximising (other widget)", async () => {
+ store.recalculateRoom(mockRoom);
+ store.moveToContainer(mockRoom, mockApps[0], Container.Top);
+ store.moveToContainer(mockRoom, mockApps[1], Container.Top);
+ store.moveToContainer(mockRoom, mockApps[2], Container.Top);
+ store.moveToContainer(mockRoom, mockApps[3], Container.Center);
+ expect(store.getContainerWidgets(mockRoom, Container.Top))
+ .toEqual([]);
+ expect(new Set(store.getContainerWidgets(mockRoom, Container.Right)))
+ .toEqual(new Set([mockApps[0], mockApps[1], mockApps[2]]));
+ expect(store.getContainerWidgets(mockRoom, Container.Center))
+ .toEqual([mockApps[3]]);
+ });
+ it("remove pins when maximising (one of the pinned widgets)", async () => {
+ store.recalculateRoom(mockRoom);
+ store.moveToContainer(mockRoom, mockApps[0], Container.Top);
+ store.moveToContainer(mockRoom, mockApps[1], Container.Top);
+ store.moveToContainer(mockRoom, mockApps[2], Container.Top);
+ store.moveToContainer(mockRoom, mockApps[0], Container.Center);
+ expect(store.getContainerWidgets(mockRoom, Container.Top))
+ .toEqual([]);
+ expect(store.getContainerWidgets(mockRoom, Container.Center))
+ .toEqual([mockApps[0]]);
+ expect(new Set(store.getContainerWidgets(mockRoom, Container.Right)))
+ .toEqual(new Set([mockApps[1], mockApps[2], mockApps[3]]));
+ });
+ it("remove maximised when pinning (other widget)", async () => {
+ store.recalculateRoom(mockRoom);
+ store.moveToContainer(mockRoom, mockApps[0], Container.Center);
+ store.moveToContainer(mockRoom, mockApps[1], Container.Top);
+ expect(store.getContainerWidgets(mockRoom, Container.Top))
+ .toEqual([mockApps[1]]);
+ expect(store.getContainerWidgets(mockRoom, Container.Center))
+ .toEqual([]);
+ expect(new Set(store.getContainerWidgets(mockRoom, Container.Right)))
+ .toEqual(new Set([mockApps[2], mockApps[3], mockApps[0]]));
+ });
+ it("remove maximised when pinning (same widget)", async () => {
+ store.recalculateRoom(mockRoom);
+ store.moveToContainer(mockRoom, mockApps[0], Container.Center);
+ store.moveToContainer(mockRoom, mockApps[0], Container.Top);
+ expect(store.getContainerWidgets(mockRoom, Container.Top))
+ .toEqual([mockApps[0]]);
+ expect(store.getContainerWidgets(mockRoom, Container.Center))
+ .toEqual([]);
+ expect(new Set(store.getContainerWidgets(mockRoom, Container.Right)))
+ .toEqual(new Set([mockApps[2], mockApps[3], mockApps[1]]));
+ });
+});