Move all widget pinning logic to the WidgetLayoutStore
							parent
							
								
									5b5c338200
								
							
						
					
					
						commit
						1768d6ea5f
					
				|  | @ -78,6 +78,7 @@ import {UPDATE_EVENT} from "../../stores/AsyncStore"; | |||
| import Notifier from "../../Notifier"; | ||||
| import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast"; | ||||
| import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore"; | ||||
| import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore"; | ||||
| 
 | ||||
| const DEBUG = false; | ||||
| let debuglog = function(msg: string) {}; | ||||
|  | @ -280,8 +281,8 @@ export default class RoomView extends React.Component<IProps, IState> { | |||
| 
 | ||||
|     private checkWidgets = (room) => { | ||||
|         this.setState({ | ||||
|             hasPinnedWidgets: WidgetStore.instance.getPinnedApps(room.roomId).length > 0, | ||||
|         }) | ||||
|             hasPinnedWidgets: WidgetLayoutStore.instance.getContainerWidgets(room, Container.Top).length > 0, | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     private onReadReceiptsChange = () => { | ||||
|  |  | |||
|  | @ -20,7 +20,7 @@ import {MatrixCapabilities} from "matrix-widget-api"; | |||
| import IconizedContextMenu, {IconizedContextMenuOption, IconizedContextMenuOptionList} from "./IconizedContextMenu"; | ||||
| import {ChevronFace} from "../../structures/ContextMenu"; | ||||
| import {_t} from "../../../languageHandler"; | ||||
| import WidgetStore, {IApp} from "../../../stores/WidgetStore"; | ||||
| import {IApp} from "../../../stores/WidgetStore"; | ||||
| import WidgetUtils from "../../../utils/WidgetUtils"; | ||||
| import {WidgetMessagingStore} from "../../../stores/widgets/WidgetMessagingStore"; | ||||
| import RoomContext from "../../../contexts/RoomContext"; | ||||
|  | @ -30,6 +30,7 @@ import Modal from "../../../Modal"; | |||
| import QuestionDialog from "../dialogs/QuestionDialog"; | ||||
| import {WidgetType} from "../../../widgets/WidgetType"; | ||||
| import MatrixClientContext from "../../../contexts/MatrixClientContext"; | ||||
| import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; | ||||
| 
 | ||||
| interface IProps extends React.ComponentProps<typeof IconizedContextMenu> { | ||||
|     app: IApp; | ||||
|  | @ -56,7 +57,7 @@ const WidgetContextMenu: React.FC<IProps> = ({ | |||
|     let unpinButton; | ||||
|     if (showUnpin) { | ||||
|         const onUnpinClick = () => { | ||||
|             WidgetStore.instance.unpinWidget(room.roomId, app.id); | ||||
|             WidgetLayoutStore.instance.moveToContainer(room, app, Container.Right); | ||||
|             onFinished(); | ||||
|         }; | ||||
| 
 | ||||
|  | @ -137,13 +138,13 @@ const WidgetContextMenu: React.FC<IProps> = ({ | |||
|         revokeButton = <IconizedContextMenuOption onClick={onRevokeClick} label={_t("Revoke permissions")} />; | ||||
|     } | ||||
| 
 | ||||
|     const pinnedWidgets = WidgetStore.instance.getPinnedApps(roomId); | ||||
|     const pinnedWidgets = WidgetLayoutStore.instance.getContainerWidgets(room, Container.Top); | ||||
|     const widgetIndex = pinnedWidgets.findIndex(widget => widget.id === app.id); | ||||
| 
 | ||||
|     let moveLeftButton; | ||||
|     if (showUnpin && widgetIndex > 0) { | ||||
|         const onClick = () => { | ||||
|             WidgetStore.instance.movePinnedWidget(roomId, app.id, -1); | ||||
|             WidgetLayoutStore.instance.moveWithinContainer(room, Container.Top, app, -1); | ||||
|             onFinished(); | ||||
|         }; | ||||
| 
 | ||||
|  | @ -153,7 +154,7 @@ const WidgetContextMenu: React.FC<IProps> = ({ | |||
|     let moveRightButton; | ||||
|     if (showUnpin && widgetIndex < pinnedWidgets.length - 1) { | ||||
|         const onClick = () => { | ||||
|             WidgetStore.instance.movePinnedWidget(roomId, app.id, 1); | ||||
|             WidgetLayoutStore.instance.moveWithinContainer(room, Container.Top, app, 1); | ||||
|             onFinished(); | ||||
|         }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -19,6 +19,8 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event"; | |||
| import { _t } from "../../../languageHandler"; | ||||
| import WidgetStore from "../../../stores/WidgetStore"; | ||||
| import EventTileBubble from "./EventTileBubble"; | ||||
| import { MatrixClientPeg } from "../../../MatrixClientPeg"; | ||||
| import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; | ||||
| 
 | ||||
| interface IProps { | ||||
|     mxEvent: MatrixEvent; | ||||
|  | @ -33,9 +35,12 @@ export default class MJitsiWidgetEvent extends React.PureComponent<IProps> { | |||
|         const url = this.props.mxEvent.getContent()['url']; | ||||
|         const prevUrl = this.props.mxEvent.getPrevContent()['url']; | ||||
|         const senderName = this.props.mxEvent.sender?.name || this.props.mxEvent.getSender(); | ||||
|         const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); | ||||
|         const widgetId = this.props.mxEvent.getStateKey(); | ||||
|         const widget = WidgetStore.instance.getRoom(room.roomId).widgets.find(w => w.id === widgetId); | ||||
| 
 | ||||
|         let joinCopy = _t('Join the conference at the top of this room'); | ||||
|         if (!WidgetStore.instance.isPinned(this.props.mxEvent.getRoomId(), this.props.mxEvent.getStateKey())) { | ||||
|         if (widget && WidgetLayoutStore.instance.isInContainer(room, widget, Container.Right)) { | ||||
|             joinCopy = _t('Join the conference from the room information card on the right'); | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
|  | @ -37,13 +37,14 @@ import SettingsStore from "../../../settings/SettingsStore"; | |||
| import TextWithTooltip from "../elements/TextWithTooltip"; | ||||
| import WidgetAvatar from "../avatars/WidgetAvatar"; | ||||
| import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; | ||||
| import WidgetStore, {IApp, MAX_PINNED} from "../../../stores/WidgetStore"; | ||||
| import WidgetStore, {IApp} from "../../../stores/WidgetStore"; | ||||
| import { E2EStatus } from "../../../utils/ShieldUtils"; | ||||
| import RoomContext from "../../../contexts/RoomContext"; | ||||
| import {UIFeature} from "../../../settings/UIFeature"; | ||||
| import {ChevronFace, ContextMenuTooltipButton, useContextMenu} from "../../structures/ContextMenu"; | ||||
| import WidgetContextMenu from "../context_menus/WidgetContextMenu"; | ||||
| import {useRoomMemberCount} from "../../../hooks/useRoomMembers"; | ||||
| import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; | ||||
| 
 | ||||
| interface IProps { | ||||
|     room: Room; | ||||
|  | @ -78,6 +79,7 @@ export const useWidgets = (room: Room) => { | |||
| 
 | ||||
|     useEffect(updateApps, [room]); | ||||
|     useEventEmitter(WidgetStore.instance, room.roomId, updateApps); | ||||
|     useEventEmitter(WidgetLayoutStore.instance, WidgetLayoutStore.emissionForRoom(room), updateApps); | ||||
| 
 | ||||
|     return apps; | ||||
| }; | ||||
|  | @ -102,10 +104,10 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => { | |||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     const isPinned = WidgetStore.instance.isPinned(room.roomId, app.id); | ||||
|     const isPinned = WidgetLayoutStore.instance.isInContainer(room, app, Container.Top); | ||||
|     const togglePin = isPinned | ||||
|         ? () => { WidgetStore.instance.unpinWidget(room.roomId, app.id); } | ||||
|         : () => { WidgetStore.instance.pinWidget(room.roomId, app.id); }; | ||||
|         ? () => { WidgetLayoutStore.instance.moveToContainer(room, app, Container.Right); } | ||||
|         : () => { WidgetLayoutStore.instance.moveToContainer(room, app, Container.Top); }; | ||||
| 
 | ||||
|     const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>(); | ||||
|     let contextMenu; | ||||
|  | @ -120,7 +122,7 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => { | |||
|         />; | ||||
|     } | ||||
| 
 | ||||
|     const cannotPin = !isPinned && !WidgetStore.instance.canPin(room.roomId, app.id); | ||||
|     const cannotPin = !isPinned && !WidgetLayoutStore.instance.canAddToContainer(room, Container.Top); | ||||
| 
 | ||||
|     let pinTitle: string; | ||||
|     if (cannotPin) { | ||||
|  |  | |||
|  | @ -14,22 +14,22 @@ See the License for the specific language governing permissions and | |||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| import React, {useContext, useEffect} from "react"; | ||||
| import {Room} from "matrix-js-sdk/src/models/room"; | ||||
| import React, { useContext, useEffect } from "react"; | ||||
| import { Room } from "matrix-js-sdk/src/models/room"; | ||||
| 
 | ||||
| import MatrixClientContext from "../../../contexts/MatrixClientContext"; | ||||
| import BaseCard from "./BaseCard"; | ||||
| import WidgetUtils from "../../../utils/WidgetUtils"; | ||||
| import AppTile from "../elements/AppTile"; | ||||
| import {_t} from "../../../languageHandler"; | ||||
| import {useWidgets} from "./RoomSummaryCard"; | ||||
| import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; | ||||
| import { _t } from "../../../languageHandler"; | ||||
| import { useWidgets } from "./RoomSummaryCard"; | ||||
| import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; | ||||
| import defaultDispatcher from "../../../dispatcher/dispatcher"; | ||||
| import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; | ||||
| import {Action} from "../../../dispatcher/actions"; | ||||
| import WidgetStore from "../../../stores/WidgetStore"; | ||||
| import {ChevronFace, ContextMenuButton, useContextMenu} from "../../structures/ContextMenu"; | ||||
| import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; | ||||
| import { Action } from "../../../dispatcher/actions"; | ||||
| import { ChevronFace, ContextMenuButton, useContextMenu } from "../../structures/ContextMenu"; | ||||
| import WidgetContextMenu from "../context_menus/WidgetContextMenu"; | ||||
| import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; | ||||
| 
 | ||||
| interface IProps { | ||||
|     room: Room; | ||||
|  | @ -42,7 +42,7 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => { | |||
| 
 | ||||
|     const apps = useWidgets(room); | ||||
|     const app = apps.find(a => a.id === widgetId); | ||||
|     const isPinned = app && WidgetStore.instance.isPinned(room.roomId, app.id); | ||||
|     const isPinned = app && WidgetLayoutStore.instance.isInContainer(room, app, Container.Top); | ||||
| 
 | ||||
|     const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(); | ||||
| 
 | ||||
|  |  | |||
|  | @ -21,16 +21,12 @@ import { IWidget } from "matrix-widget-api"; | |||
| import { ActionPayload } from "../dispatcher/payloads"; | ||||
| import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; | ||||
| import defaultDispatcher from "../dispatcher/dispatcher"; | ||||
| import SettingsStore from "../settings/SettingsStore"; | ||||
| import WidgetEchoStore from "../stores/WidgetEchoStore"; | ||||
| import RoomViewStore from "../stores/RoomViewStore"; | ||||
| import ActiveWidgetStore from "../stores/ActiveWidgetStore"; | ||||
| import WidgetUtils from "../utils/WidgetUtils"; | ||||
| import {SettingLevel} from "../settings/SettingLevel"; | ||||
| import {WidgetType} from "../widgets/WidgetType"; | ||||
| import {UPDATE_EVENT} from "./AsyncStore"; | ||||
| import { MatrixClientPeg } from "../MatrixClientPeg"; | ||||
| import { arrayDiff, arrayHasDiff, arrayUnion } from "../utils/arrays"; | ||||
| 
 | ||||
| interface IState {} | ||||
| 
 | ||||
|  | @ -41,15 +37,10 @@ export interface IApp extends IWidget { | |||
|     avatar_url: string; // MSC2765 https://github.com/matrix-org/matrix-doc/pull/2765
 | ||||
| } | ||||
| 
 | ||||
| type PinnedWidgets = Record<string, boolean>; | ||||
| 
 | ||||
| interface IRoomWidgets { | ||||
|     widgets: IApp[]; | ||||
|     pinned: PinnedWidgets; | ||||
| } | ||||
| 
 | ||||
| export const MAX_PINNED = 3; | ||||
| 
 | ||||
| function widgetUid(app: IApp): string { | ||||
|     return `${app.roomId ?? MatrixClientPeg.get().getUserId()}::${app.id}`; | ||||
| } | ||||
|  | @ -65,7 +56,6 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> { | |||
|     private constructor() { | ||||
|         super(defaultDispatcher, {}); | ||||
| 
 | ||||
|         SettingsStore.watchSetting("Widgets.pinned", null, this.onPinnedWidgetsChange); | ||||
|         WidgetEchoStore.on("update", this.onWidgetEchoStoreUpdate); | ||||
|     } | ||||
| 
 | ||||
|  | @ -76,7 +66,6 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> { | |||
|     private initRoom(roomId: string) { | ||||
|         if (!this.roomMap.has(roomId)) { | ||||
|             this.roomMap.set(roomId, { | ||||
|                 pinned: {}, // ordered
 | ||||
|                 widgets: [], | ||||
|             }); | ||||
|         } | ||||
|  | @ -85,16 +74,6 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> { | |||
|     protected async onReady(): Promise<any> { | ||||
|         this.matrixClient.on("RoomState.events", this.onRoomStateEvents); | ||||
|         this.matrixClient.getRooms().forEach((room: Room) => { | ||||
|             const pinned = SettingsStore.getValue("Widgets.pinned", room.roomId); | ||||
| 
 | ||||
|             if (pinned || WidgetUtils.getRoomWidgets(room).length) { | ||||
|                 this.initRoom(room.roomId); | ||||
|             } | ||||
| 
 | ||||
|             if (pinned) { | ||||
|                 this.getRoom(room.roomId).pinned = pinned; | ||||
|             } | ||||
| 
 | ||||
|             this.loadRoomWidgets(room); | ||||
|         }); | ||||
|         this.emit(UPDATE_EVENT); | ||||
|  | @ -128,7 +107,7 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> { | |||
| 
 | ||||
|     private loadRoomWidgets(room: Room) { | ||||
|         if (!room) return; | ||||
|         const roomInfo = this.roomMap.get(room.roomId); | ||||
|         const roomInfo = this.roomMap.get(room.roomId) || <IRoomWidgets>{}; | ||||
|         roomInfo.widgets = []; | ||||
| 
 | ||||
|         // first clean out old widgets from the map which originate from this room
 | ||||
|  | @ -138,6 +117,7 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> { | |||
|             this.widgetMap.delete(widgetUid(app)); | ||||
|         }); | ||||
| 
 | ||||
|         let edited = false; | ||||
|         this.generateApps(room).forEach(app => { | ||||
|             // Sanity check for https://github.com/vector-im/element-web/issues/15705
 | ||||
|             const existingApp = this.widgetMap.get(widgetUid(app)); | ||||
|  | @ -150,12 +130,16 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> { | |||
| 
 | ||||
|             this.widgetMap.set(widgetUid(app), app); | ||||
|             roomInfo.widgets.push(app); | ||||
|             edited = true; | ||||
|         }); | ||||
|         if (edited && !this.roomMap.has(room.roomId)) { | ||||
|             this.roomMap.set(room.roomId, roomInfo); | ||||
|         } | ||||
|         this.emit(room.roomId); | ||||
|     } | ||||
| 
 | ||||
|     private onRoomStateEvents = (ev: MatrixEvent) => { | ||||
|         if (ev.getType() !== "im.vector.modular.widgets") return; | ||||
|         if (ev.getType() !== "im.vector.modular.widgets") return; // TODO: Support m.widget too
 | ||||
|         const roomId = ev.getRoomId(); | ||||
|         this.initRoom(roomId); | ||||
|         this.loadRoomWidgets(this.matrixClient.getRoom(roomId)); | ||||
|  | @ -166,156 +150,6 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> { | |||
|         return this.roomMap.get(roomId); | ||||
|     }; | ||||
| 
 | ||||
|     private onPinnedWidgetsChange = (settingName: string, roomId: string) => { | ||||
|         this.initRoom(roomId); | ||||
| 
 | ||||
|         const pinned: PinnedWidgets = SettingsStore.getValue(settingName, roomId); | ||||
| 
 | ||||
|         // Sanity check for https://github.com/vector-im/element-web/issues/15705
 | ||||
|         const roomInfo = this.getRoom(roomId); | ||||
|         const remappedPinned: PinnedWidgets = {}; | ||||
|         for (const widgetId of Object.keys(pinned)) { | ||||
|             const isPinned = pinned[widgetId]; | ||||
|             if (!roomInfo.widgets?.some(w => w.id === widgetId)) { | ||||
|                 console.warn(`Skipping pinned widget update for ${widgetId} in ${roomId} -- wrong room`); | ||||
|             } else { | ||||
|                 remappedPinned[widgetId] = isPinned; | ||||
|             } | ||||
|         } | ||||
|         roomInfo.pinned = remappedPinned; | ||||
| 
 | ||||
|         this.emit(roomId); | ||||
|         this.emit(UPDATE_EVENT); | ||||
|     }; | ||||
| 
 | ||||
|     public isPinned(roomId: string, widgetId: string) { | ||||
|         return !!this.getPinnedApps(roomId).find(w => w.id === widgetId); | ||||
|     } | ||||
| 
 | ||||
|     // dev note: we don't need the widgetId on this function, but the contract makes more sense
 | ||||
|     // when we require it.
 | ||||
|     public canPin(roomId: string, widgetId: string) { | ||||
|         return this.getPinnedApps(roomId).length < MAX_PINNED; | ||||
|     } | ||||
| 
 | ||||
|     public pinWidget(roomId: string, widgetId: string) { | ||||
|         const roomInfo = this.getRoom(roomId); | ||||
|         if (!roomInfo) return; | ||||
| 
 | ||||
|         // When pinning, first confirm all the widgets (Jitsi) which were autopinned so that the order is correct
 | ||||
|         const autoPinned = this.getPinnedApps(roomId).filter(app => !roomInfo.pinned[app.id]); | ||||
|         autoPinned.forEach(app => { | ||||
|             this.setPinned(roomId, app.id, true); | ||||
|         }); | ||||
| 
 | ||||
|         this.setPinned(roomId, widgetId, true); | ||||
| 
 | ||||
|         // Show the apps drawer upon the user pinning a widget
 | ||||
|         if (RoomViewStore.getRoomId() === roomId) { | ||||
|             defaultDispatcher.dispatch({ | ||||
|                 action: "appsDrawer", | ||||
|                 show: true, | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public unpinWidget(roomId: string, widgetId: string) { | ||||
|         this.setPinned(roomId, widgetId, false); | ||||
|     } | ||||
| 
 | ||||
|     private setPinned(roomId: string, widgetId: string, value: boolean) { | ||||
|         const roomInfo = this.getRoom(roomId); | ||||
|         if (!roomInfo) return; | ||||
|         if (roomInfo.pinned[widgetId] === false && value) { | ||||
|             // delete this before write to maintain the correct object insertion order
 | ||||
|             delete roomInfo.pinned[widgetId]; | ||||
|         } | ||||
|         roomInfo.pinned[widgetId] = value; | ||||
| 
 | ||||
|         // Clean up the pinned record
 | ||||
|         Object.keys(roomInfo).forEach(wId => { | ||||
|             if (!roomInfo.widgets.some(w => w.id === wId) || !roomInfo.pinned[wId]) { | ||||
|                 delete roomInfo.pinned[wId]; | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         SettingsStore.setValue("Widgets.pinned", roomId, SettingLevel.ROOM_ACCOUNT, roomInfo.pinned); | ||||
|         this.emit(roomId); | ||||
|         this.emit(UPDATE_EVENT); | ||||
|     } | ||||
| 
 | ||||
|     public movePinnedWidget(roomId: string, widgetId: string, delta: 1 | -1) { | ||||
|         // TODO simplify this by changing the storage medium of pinned to an array once the Jitsi default-on goes away
 | ||||
|         const roomInfo = this.getRoom(roomId); | ||||
|         if (!roomInfo || roomInfo.pinned[widgetId] === false) return; | ||||
| 
 | ||||
|         const pinnedApps = this.getPinnedApps(roomId).map(app => app.id); | ||||
|         const i = pinnedApps.findIndex(id => id === widgetId); | ||||
| 
 | ||||
|         if (delta > 0) { | ||||
|             pinnedApps.splice(i, 2, pinnedApps[i + 1], pinnedApps[i]); | ||||
|         } else { | ||||
|             pinnedApps.splice(i - 1, 2, pinnedApps[i], pinnedApps[i - 1]); | ||||
|         } | ||||
| 
 | ||||
|         const reorderedPinned: IRoomWidgets["pinned"] = {}; | ||||
|         pinnedApps.forEach(id => { | ||||
|             reorderedPinned[id] = true; | ||||
|         }); | ||||
|         Object.keys(roomInfo.pinned).forEach(id => { | ||||
|             if (reorderedPinned[id] === undefined) { | ||||
|                 reorderedPinned[id] = roomInfo.pinned[id]; | ||||
|             } | ||||
|         }); | ||||
|         roomInfo.pinned = reorderedPinned; | ||||
| 
 | ||||
|         SettingsStore.setValue("Widgets.pinned", roomId, SettingLevel.ROOM_ACCOUNT, roomInfo.pinned); | ||||
|         this.emit(roomId); | ||||
|         this.emit(UPDATE_EVENT); | ||||
|     } | ||||
| 
 | ||||
|     public getPinnedApps(roomId: string): IApp[] { | ||||
|         // returns the apps in the order they were pinned with, up to the maximum
 | ||||
|         const roomInfo = this.getRoom(roomId); | ||||
|         if (!roomInfo) return []; | ||||
| 
 | ||||
|         // Show Jitsi widgets even if the user already had the maximum pinned, instead of their latest pinned,
 | ||||
|         // except if the user already explicitly unpinned the Jitsi widget
 | ||||
|         const priorityWidget = roomInfo.widgets.find(widget => { | ||||
|             return roomInfo.pinned[widget.id] === undefined && WidgetType.JITSI.matches(widget.type); | ||||
|         }); | ||||
| 
 | ||||
|         const order = Object.keys(roomInfo.pinned).filter(k => roomInfo.pinned[k]); | ||||
|         const apps = order | ||||
|             .map(wId => Array.from(this.widgetMap.values()) | ||||
|                 .find(w2 => w2.roomId === roomId && w2.id === wId)) | ||||
|             .filter(Boolean) | ||||
|             .slice(0, priorityWidget ? MAX_PINNED - 1 : MAX_PINNED); | ||||
|         if (priorityWidget) { | ||||
|             apps.push(priorityWidget); | ||||
|         } | ||||
| 
 | ||||
|         // Sanity check for https://github.com/vector-im/element-web/issues/15705
 | ||||
|         // We union the app IDs the above generated with the roomInfo's known widgets to
 | ||||
|         // get a list of IDs which both exist. We then diff that against the generated app
 | ||||
|         // IDs above to ensure that all of the app IDs are captured by the union with the
 | ||||
|         // room - if we grabbed a widget that wasn't part of the roomInfo's list, it wouldn't
 | ||||
|         // be in the union and thus result in a diff.
 | ||||
|         const appIds = apps.map(a => widgetUid(a)); | ||||
|         const roomAppIds = roomInfo.widgets.map(a => widgetUid(a)); | ||||
|         const roomAppIdsUnion = arrayUnion(appIds, roomAppIds); | ||||
|         const missingSomeApps = arrayHasDiff(roomAppIdsUnion, appIds); | ||||
|         if (missingSomeApps) { | ||||
|             const diff = arrayDiff(roomAppIdsUnion, appIds); | ||||
|             console.warn( | ||||
|                 `${roomId} appears to have a conflict for which widgets belong to it. ` + | ||||
|                 `Widget UIDs are: `, [...diff.added, ...diff.removed], | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         return apps; | ||||
|     } | ||||
| 
 | ||||
|     public getApps(roomId: string): IApp[] { | ||||
|         const roomInfo = this.getRoom(roomId); | ||||
|         return roomInfo?.widgets || []; | ||||
|  |  | |||
|  | @ -23,6 +23,8 @@ import defaultDispatcher from "../../dispatcher/dispatcher"; | |||
| import { ReadyWatchingStore } from "../ReadyWatchingStore"; | ||||
| import { MatrixEvent } from "matrix-js-sdk/src/models/event"; | ||||
| import { SettingLevel } from "../../settings/SettingLevel"; | ||||
| import { arrayFastClone } from "../../utils/arrays"; | ||||
| import { UPDATE_EVENT } from "../AsyncStore"; | ||||
| 
 | ||||
| export const WIDGET_LAYOUT_EVENT_TYPE = "io.element.widgets.layout"; | ||||
| 
 | ||||
|  | @ -79,7 +81,7 @@ interface ILayoutSettings extends ILayoutStateEvent { | |||
| } | ||||
| 
 | ||||
| // Dev note: "Pinned" widgets are ones in the top container.
 | ||||
| const MAX_PINNED = 3; | ||||
| export const MAX_PINNED = 3; | ||||
| 
 | ||||
| // These two are whole percentages and don't really mean anything. Later values will decide
 | ||||
| // minimum, but these help determine proportions during our calculations here. In fact, these
 | ||||
|  | @ -126,19 +128,19 @@ export class WidgetLayoutStore extends ReadyWatchingStore { | |||
|         this.matrixClient.on("RoomState.events", this.updateRoomFromState); | ||||
|         SettingsStore.watchSetting("Widgets.pinned", null, this.updateFromSettings); | ||||
|         SettingsStore.watchSetting("Widgets.layout", null, this.updateFromSettings); | ||||
|         // TODO: Register WidgetStore listener
 | ||||
|         WidgetStore.instance.on(UPDATE_EVENT, this.updateAllRooms); | ||||
|     } | ||||
| 
 | ||||
|     protected async onNotReady(): Promise<any> { | ||||
|         this.byRoom = {}; | ||||
|     } | ||||
| 
 | ||||
|     private updateAllRooms() { | ||||
|     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; | ||||
|  | @ -222,8 +224,8 @@ export class WidgetLayoutStore extends ReadyWatchingStore { | |||
|             const defaultA = WidgetType.JITSI.matches(a.type) ? Number.MIN_SAFE_INTEGER : Number.MAX_SAFE_INTEGER; | ||||
|             const defaultB = WidgetType.JITSI.matches(b.type) ? Number.MIN_SAFE_INTEGER : Number.MAX_SAFE_INTEGER; | ||||
| 
 | ||||
|             const orderA = defaultNumber(userLayoutA, defaultNumber(layoutA?.index, defaultA)); | ||||
|             const orderB = defaultNumber(userLayoutB, defaultNumber(layoutB?.index, defaultB)); | ||||
|             const orderA = defaultNumber(userLayoutA?.index, defaultNumber(layoutA?.index, defaultA)); | ||||
|             const orderB = defaultNumber(userLayoutB?.index, defaultNumber(layoutB?.index, defaultB)); | ||||
| 
 | ||||
|             if (orderA === orderB) { | ||||
|                 // We just need a tiebreak
 | ||||
|  | @ -308,6 +310,14 @@ export class WidgetLayoutStore extends ReadyWatchingStore { | |||
|         return this.byRoom[room.roomId]?.[container]?.ordered || []; | ||||
|     } | ||||
| 
 | ||||
|     public isInContainer(room: Room, widget: IApp, container: Container): boolean { | ||||
|         return this.getContainerWidgets(room, container).some(w => w.id === widget.id); | ||||
|     } | ||||
| 
 | ||||
|     public canAddToContainer(room: Room, container: Container): boolean { | ||||
|         return this.getContainerWidgets(room, container).length < MAX_PINNED; | ||||
|     } | ||||
| 
 | ||||
|     public getResizerDistributions(room: Room, container: Container): string[] { // yes, string.
 | ||||
|         let distributions = this.byRoom[room.roomId]?.[container]?.distributions; | ||||
|         if (!distributions || distributions.length < 2) return []; | ||||
|  | @ -335,6 +345,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore { | |||
|         const localLayout = {}; | ||||
|         widgets.forEach((w, i) => { | ||||
|             localLayout[w.id] = { | ||||
|                 container: container, | ||||
|                 width: numbers[i], | ||||
|                 index: i, | ||||
|                 height: this.byRoom[room.roomId]?.[container]?.height || MIN_WIDGET_HEIGHT_PCT, | ||||
|  | @ -353,6 +364,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore { | |||
|         const localLayout = {}; | ||||
|         widgets.forEach((w, i) => { | ||||
|             localLayout[w.id] = { | ||||
|                 container: container, | ||||
|                 width: widths[i], | ||||
|                 index: i, | ||||
|                 height: height, | ||||
|  | @ -361,7 +373,66 @@ export class WidgetLayoutStore extends ReadyWatchingStore { | |||
|         this.updateUserLayout(room, localLayout); | ||||
|     } | ||||
| 
 | ||||
|     public moveWithinContainer(room: Room, container: Container, widget: IApp, delta: number) { | ||||
|         const widgets = arrayFastClone(this.getContainerWidgets(room, container)); | ||||
|         const currentIdx = widgets.findIndex(w => w.id === widget.id); | ||||
|         if (currentIdx < 0) return; // no change needed
 | ||||
| 
 | ||||
|         widgets.splice(currentIdx, 1); // remove existing widget
 | ||||
|         const newIdx = clamp(currentIdx + delta, 0, widgets.length); | ||||
|         widgets.splice(newIdx, 0, widget); | ||||
| 
 | ||||
|         const widths = this.byRoom[room.roomId]?.[container]?.distributions; | ||||
|         const height = this.byRoom[room.roomId]?.[container]?.height; | ||||
|         const localLayout = {}; | ||||
|         widgets.forEach((w, i) => { | ||||
|             localLayout[w.id] = { | ||||
|                 container: container, | ||||
|                 width: widths[i], | ||||
|                 index: i, | ||||
|                 height: height, | ||||
|             }; | ||||
|         }); | ||||
|         this.updateUserLayout(room, localLayout); | ||||
|     } | ||||
| 
 | ||||
|     public moveToContainer(room: Room, widget: IApp, toContainer: Container) { | ||||
|         const allWidgets = this.getAllWidgets(room); | ||||
|         if (!allWidgets.some(([w])=> w.id === widget.id)) return; // invalid
 | ||||
|         this.updateUserLayout(room, {[widget.id]:{container: toContainer}}); | ||||
|     } | ||||
| 
 | ||||
|     private getAllWidgets(room: Room): [IApp, Container][] { | ||||
|         const containers = this.byRoom[room.roomId]; | ||||
|         if (!containers) return []; | ||||
| 
 | ||||
|         const ret = []; | ||||
|         for (const container of Object.keys(containers)) { | ||||
|             const widgets = containers[container].ordered; | ||||
|             for (const widget of widgets) { | ||||
|                 ret.push([widget, container]); | ||||
|             } | ||||
|         } | ||||
|         return ret; | ||||
|     } | ||||
| 
 | ||||
|     private updateUserLayout(room: Room, newLayout: IWidgetLayouts) { | ||||
|         // Polyfill any missing widgets
 | ||||
|         const allWidgets = this.getAllWidgets(room); | ||||
|         for (const [widget, container] of allWidgets) { | ||||
|             const containerWidgets = this.getContainerWidgets(room, container); | ||||
|             const idx = containerWidgets.findIndex(w => w.id === widget.id); | ||||
|             const widths = this.byRoom[room.roomId]?.[container]?.distributions; | ||||
|             if (!newLayout[widget.id]) { | ||||
|                 newLayout[widget.id] = { | ||||
|                     container: container, | ||||
|                     index: idx, | ||||
|                     height: this.byRoom[room.roomId]?.[container]?.height, | ||||
|                     width: widths?.[idx], | ||||
|                 }; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         const layoutEv = room.currentState.getStateEvents(WIDGET_LAYOUT_EVENT_TYPE, ""); | ||||
|         SettingsStore.setValue("Widgets.layout", room.roomId, SettingLevel.ROOM_ACCOUNT, { | ||||
|             overrides: layoutEv?.getId(), | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Travis Ralston
						Travis Ralston