Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Make widgets not reload (persistent) between center and top container #7575

Merged
merged 11 commits into from
Jan 24, 2022
60 changes: 38 additions & 22 deletions src/components/views/elements/AppTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import classNames from 'classnames';
import { MatrixCapabilities } from "matrix-widget-api";
import { Room } from "matrix-js-sdk/src/models/room";
import { logger } from "matrix-js-sdk/src/logger";
import { EventSubscription } from 'fbemitter';

import { MatrixClientPeg } from '../../../MatrixClientPeg';
import AccessibleButton from './AccessibleButton';
Expand All @@ -46,6 +47,7 @@ import { IApp } from "../../../stores/WidgetStore";
import { WidgetLayoutStore, Container } from "../../../stores/widgets/WidgetLayoutStore";
import { OwnProfileStore } from '../../../stores/OwnProfileStore';
import { UPDATE_EVENT } from '../../../stores/AsyncStore';
import RoomViewStore from '../../../stores/RoomViewStore';

interface IProps {
app: IApp;
Expand Down Expand Up @@ -116,6 +118,7 @@ export default class AppTile extends React.Component<IProps, IState> {
private persistKey: string;
private sgWidget: StopGapWidget;
private dispatcherRef: string;
private roomStoreToken: EventSubscription;

constructor(props: IProps) {
super(props);
Expand All @@ -134,9 +137,6 @@ export default class AppTile extends React.Component<IProps, IState> {
}

this.state = this.getNewState(props);
this.watchUserReady();

this.allowedWidgetsWatchRef = SettingsStore.watchSetting("allowedWidgets", null, this.onAllowedWidgetsChange);
}

private watchUserReady = () => {
Expand All @@ -162,6 +162,29 @@ export default class AppTile extends React.Component<IProps, IState> {
return !!currentlyAllowedWidgets[props.app.eventId];
};

private onWidgetLayoutChange = () => {
const room = MatrixClientPeg.get().getRoom(this.props.room.roomId);
const app = this.props.app;
const isActiveWidget = ActiveWidgetStore.instance.getWidgetPersistence(app.id);
const isVisibleOnScreen = WidgetLayoutStore.instance.isVisibleOnScreen(room, app.id);
if (!isVisibleOnScreen && !isActiveWidget) {
ActiveWidgetStore.instance.destroyPersistentWidget(app.id);
PersistedElement.destroyElement(this.persistKey);
this.sgWidget?.stopMessaging();
}
};

private onRoomViewStoreUpdate = () => {
if (this.props.room.roomId == RoomViewStore.getRoomId()) return;
const app = this.props.app;
const isActiveWidget = ActiveWidgetStore.instance.getWidgetPersistence(app.id);
if (!isActiveWidget) {
ActiveWidgetStore.instance.destroyPersistentWidget(app.id);
PersistedElement.destroyElement(this.persistKey);
this.sgWidget?.stopMessaging();
}
};

/**
* Set initial component state when the App wUrl (widget URL) is being updated.
* Component props *must* be passed (rather than relying on this.props).
Expand Down Expand Up @@ -194,7 +217,7 @@ export default class AppTile extends React.Component<IProps, IState> {
// Force the widget to be non-persistent (able to be deleted/forgotten)
ActiveWidgetStore.instance.destroyPersistentWidget(this.props.app.id);
PersistedElement.destroyElement(this.persistKey);
if (this.sgWidget) this.sgWidget.stop();
this.sgWidget?.stopMessaging();
}

this.setState({ hasPermissionToLoad });
Expand All @@ -217,7 +240,11 @@ export default class AppTile extends React.Component<IProps, IState> {
if (this.sgWidget && this.state.hasPermissionToLoad) {
this.startWidget();
}
this.watchUserReady();

WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(this.props.room), this.onWidgetLayoutChange);
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
this.allowedWidgetsWatchRef = SettingsStore.watchSetting("allowedWidgets", null, this.onAllowedWidgetsChange);
// Widget action listeners
this.dispatcherRef = dis.register(this.onAction);
}
Expand All @@ -226,24 +253,14 @@ export default class AppTile extends React.Component<IProps, IState> {
// Widget action listeners
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);

// if it's not remaining on screen, get rid of the PersistedElement container
if (!ActiveWidgetStore.instance.getWidgetPersistence(this.props.app.id)) {
ActiveWidgetStore.instance.destroyPersistentWidget(this.props.app.id);
PersistedElement.destroyElement(this.persistKey);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the main change that fixes the issue. Instead of destroying it here we destroy it if the widget is not visible on screen anymore of if we switch room.
(this pr now fixes other bugs so i though it would be good to highlight this with a comment)


if (this.sgWidget) {
this.sgWidget.stop();
}

WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(this.props.room), this.onWidgetLayoutChange);
this.roomStoreToken?.remove();
SettingsStore.unwatchSetting(this.allowedWidgetsWatchRef);
OwnProfileStore.instance.removeListener(UPDATE_EVENT, this.onUserReady);
}

private resetWidget(newProps: IProps): void {
if (this.sgWidget) {
this.sgWidget.stop();
}
this.sgWidget?.stopMessaging();
try {
this.sgWidget = new StopGapWidget(newProps);
this.sgWidget.on("preparing", this.onWidgetPreparing);
Expand All @@ -265,9 +282,7 @@ export default class AppTile extends React.Component<IProps, IState> {
this.iframe = ref;
if (ref) {
try {
if (this.sgWidget) {
this.sgWidget.start(ref);
}
this.sgWidget?.startMessaging(ref);
} catch (e) {
logger.error("Failed to start widget", e);
}
Expand Down Expand Up @@ -320,7 +335,7 @@ export default class AppTile extends React.Component<IProps, IState> {
PersistedElement.destroyElement(this.persistKey);
ActiveWidgetStore.instance.destroyPersistentWidget(this.props.app.id);

if (this.sgWidget) this.sgWidget.stop({ forceDestroy: true });
this.sgWidget?.stopMessaging({ forceDestroy: true });
}

private onWidgetPreparing = (): void => {
Expand Down Expand Up @@ -527,7 +542,7 @@ export default class AppTile extends React.Component<IProps, IState> {
// Also wrap the PersistedElement in a div to fix the height, otherwise
// AppTile's border is in the wrong place

// For persistent apps in PiP we want the zIndex to be higher then for other persistent apps (100)
// For persisted apps in PiP we want the zIndex to be higher then for other persisted apps (100)
// otherwise there are issues that the PiP view is drawn UNDER another widget (Persistent app) when dragged around.
const zIndexAboveOtherPersistentElements = 101;

Expand Down Expand Up @@ -564,6 +579,7 @@ export default class AppTile extends React.Component<IProps, IState> {
/>
);
}

let maxMinButton;
if (!this.props.hideMaximiseButton) {
const widgetIsMaximised = WidgetLayoutStore.instance.
Expand Down
4 changes: 2 additions & 2 deletions src/components/views/rooms/AppsDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
private onAction = (action: ActionPayload): void => {
const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer';
switch (action.action) {
case 'appsDrawer':
case "appsDrawer":
// Note: these booleans are awkward because localstorage is fundamentally
// string-based. We also do exact equality on the strings later on.
if (action.show) {
Expand Down Expand Up @@ -279,7 +279,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
drawer = <PersistentVResizer
room={this.props.room}
minHeight={100}
maxHeight={(this.props.maxHeight || !widgetIsMaxmised) ? this.props.maxHeight - 50 : undefined}
maxHeight={this.props.maxHeight - 50}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all changes to this file are unrelated. If you want me to I can make a different pr. It was just super convinient and simple to include them right after I found them without switching branches.

handleClass="mx_AppsContainer_resizerHandle"
handleWrapperClass="mx_AppsContainer_resizerHandleContainer"
className="mx_AppsContainer_resizer"
Expand Down
18 changes: 4 additions & 14 deletions src/components/views/voip/PipView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
import PictureInPictureDragger from './PictureInPictureDragger';
import dis from '../../../dispatcher/dispatcher';
import { Action } from "../../../dispatcher/actions";
import { Container, WidgetLayoutStore } from '../../../stores/widgets/WidgetLayoutStore';
import { WidgetLayoutStore } from '../../../stores/widgets/WidgetLayoutStore';
import CallViewHeader from './CallView/CallViewHeader';
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from '../../../stores/ActiveWidgetStore';
import { UPDATE_EVENT } from '../../../stores/AsyncStore';
Expand Down Expand Up @@ -242,9 +242,7 @@ export default class PipView extends React.Component<IProps, IState> {

let userIsPartOfTheRoom = false;
let fromAnotherRoom = false;
let notInRightPanel = false;
let notInCenterContainer = false;
let notInTopContainer = false;
let notVisible = false;
if (wId) {
const persistentWidgetInRoomId = ActiveWidgetStore.instance.getRoomId(wId);
const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId);
Expand All @@ -254,24 +252,16 @@ export default class PipView extends React.Component<IProps, IState> {
if (!persistentWidgetInRoom) return null;

const wls = WidgetLayoutStore.instance;

notVisible = !wls.isVisibleOnScreen(persistentWidgetInRoom, wId);
userIsPartOfTheRoom = persistentWidgetInRoom.getMyMembership() == "join";
fromAnotherRoom = this.state.viewedRoomId !== persistentWidgetInRoomId;

notInRightPanel =
!(RightPanelStore.instance.currentCard.phase == RightPanelPhases.Widget &&
wId == RightPanelStore.instance.currentCard.state?.widgetId);
notInCenterContainer =
!wls.getContainerWidgets(persistentWidgetInRoom, Container.Center).some((app) => app.id == wId);
notInTopContainer =
!wls.getContainerWidgets(persistentWidgetInRoom, Container.Top).some(app => app.id == wId);
}

// The widget should only be shown as a persistent app (in a floating pip container) if it is not visible on screen
// either, because we are viewing a different room OR because it is in none of the possible containers of the room view.
const showWidgetInPip =
(fromAnotherRoom && userIsPartOfTheRoom) ||
(notInRightPanel && notInCenterContainer && notInTopContainer && userIsPartOfTheRoom);
(notVisible && userIsPartOfTheRoom);

this.setState({ showWidgetInPip });
}
Expand Down
60 changes: 28 additions & 32 deletions src/stores/right-panel/RightPanelStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import { EventSubscription } from 'fbemitter';
import { logger } from "matrix-js-sdk/src/logger";

import defaultDispatcher from '../../dispatcher/dispatcher';
import { pendingVerificationRequestForUser } from '../../verification';
import SettingsStore from "../../settings/SettingsStore";
import { RightPanelPhases } from "./RightPanelStorePhases";
import { ActionPayload } from "../../dispatcher/payloads";
import { Action } from '../../dispatcher/actions';
import { SettingLevel } from "../../settings/SettingLevel";
import { UPDATE_EVENT } from '../AsyncStore';
import { ReadyWatchingStore } from '../ReadyWatchingStore';
Expand All @@ -32,21 +32,14 @@ import {
IRightPanelForRoom,
} from './RightPanelStoreIPanelState';
import { MatrixClientPeg } from "../../MatrixClientPeg";
// import RoomViewStore from '../RoomViewStore';

import RoomViewStore from '../RoomViewStore';
jryans marked this conversation as resolved.
Show resolved Hide resolved
const GROUP_PHASES = [
RightPanelPhases.GroupMemberList,
RightPanelPhases.GroupRoomList,
RightPanelPhases.GroupRoomInfo,
RightPanelPhases.GroupMemberInfo,
];

const MEMBER_INFO_PHASES = [
RightPanelPhases.RoomMemberInfo,
RightPanelPhases.Room3pidMemberInfo,
RightPanelPhases.EncryptionPanel,
];

/**
* A class for tracking the state of the right panel between layouts and
* sessions. This state includes a history for each room. Each history element
Expand All @@ -68,6 +61,8 @@ export default class RightPanelStore extends ReadyWatchingStore {
[roomId: string]: IRightPanelForRoom;
} = {};

private roomStoreToken: EventSubscription;

private constructor() {
super(defaultDispatcher);
this.dispatcherRefRightPanelStore = defaultDispatcher.register(this.onDispatch);
Expand All @@ -76,8 +71,9 @@ export default class RightPanelStore extends ReadyWatchingStore {
protected async onReady(): Promise<any> {
this.isReady = true;
// TODO RightPanelStore (will be addressed when dropping groups): This should be used instead of the onDispatch callback when groups are removed.
jryans marked this conversation as resolved.
Show resolved Hide resolved
// RoomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
MatrixClientPeg.get().on("crypto.verification.request", this.onVerificationRequestUpdate);
this.viewedRoomId = RoomViewStore.getRoomId();
this.loadCacheFromSettings();
this.emitAndUpdateSettings();
}
Expand All @@ -91,8 +87,7 @@ export default class RightPanelStore extends ReadyWatchingStore {
protected async onNotReady(): Promise<any> {
this.isReady = false;
MatrixClientPeg.get().off("crypto.verification.request", this.onVerificationRequestUpdate);
// TODO RightPanelStore (will be addressed when dropping groups): User this instead of the dispatcher.
// RoomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
this.roomStoreToken.remove();
}

// Getters
Expand Down Expand Up @@ -373,45 +368,46 @@ export default class RightPanelStore extends ReadyWatchingStore {
};

onRoomViewStoreUpdate = () => {
// TODO: use this function instead of the onDispatch (the whole onDispatch can get removed!) as soon groups are removed
// this.viewedRoomId = RoomViewStore.getRoomId();
// this.isViewingRoom = true; // Is viewing room will of course be removed when removing groups
// // load values from byRoomCache with the viewedRoomId.
// this.loadCacheFromSettings();
// TODO: only use this function instead of the onDispatch (the whole onDispatch can get removed!) as soon groups are removed
this.viewedRoomId = RoomViewStore.getRoomId();
this.isViewingRoom = true; // Is viewing room will of course be removed when removing groups
// load values from byRoomCache with the viewedRoomId.
this.loadCacheFromSettings();
this.emitAndUpdateSettings();
};

onDispatch = (payload: ActionPayload) => {
switch (payload.action) {
case 'view_group':
case Action.ViewRoom: {
case 'view_group': {
if (payload.room_id === this.viewedRoomId) break; // skip this transition, probably a permalink

// Put group in the same/similar view to what was open from the previously viewed room
// Is contradictory to the new "per room" philosophy but it is the legacy behavior for groups.
if ((this.isViewingRoom ? Action.ViewRoom : "view_group") != payload.action) {
if (payload.action == Action.ViewRoom && MEMBER_INFO_PHASES.includes(this.currentCard?.phase)) {
// switch from group to room
this.setRightPanelCache({ phase: RightPanelPhases.RoomMemberList, state: {} });
} else if (
payload.action == "view_group" &&
this.currentCard?.phase === RightPanelPhases.GroupMemberInfo
) {
// switch from room to group
this.setRightPanelCache({ phase: RightPanelPhases.GroupMemberList, state: {} });
}

if (
this.currentCard?.phase === RightPanelPhases.GroupMemberInfo
) {
// switch from room to group
this.setRightPanelCache({ phase: RightPanelPhases.GroupMemberList, state: {} });
}

// Update the current room here, so that all the other functions dont need to be room dependant.
// The right panel store always will return the state for the current room.
this.viewedRoomId = payload.room_id;
this.isViewingRoom = payload.action == Action.ViewRoom;
this.isViewingRoom = false;
// load values from byRoomCache with the viewedRoomId.
if (this.isReady) {
// we need the client to be ready to get the events form the ids of the settings
// the loading will be done in the onReady function (to catch up with the changes done here before it was ready)
// all the logic in this case is not necessary anymore as soon as groups are dropped and we use: onRoomViewStoreUpdate
this.loadCacheFromSettings();
this.emitAndUpdateSettings();

/*
DO NOT EMIT. Emitting breaks iframe refs by triggering a render
for the room view and calling the iframe ref changed
function
*/
jryans marked this conversation as resolved.
Show resolved Hide resolved
// this.emitAndUpdateSettings();
}
break;
}
Expand Down
16 changes: 13 additions & 3 deletions src/stores/widgets/StopGapWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,12 @@ export class StopGapWidget extends EventEmitter {
});
}
};

public start(iframe: HTMLIFrameElement) {
/**
* This starts the messaging for the widget if it is not in the state `started` yet.
* @param iframe the iframe the widget should use
* @returns
jryans marked this conversation as resolved.
Show resolved Hide resolved
*/
public startMessaging(iframe: HTMLIFrameElement): any {
if (this.started) return;
const allowedCapabilities = this.appTileProps.whitelistCapabilities || [];
const driver = new StopGapWidgetDriver(allowedCapabilities, this.mockWidget, this.kind, this.roomId);
Expand Down Expand Up @@ -407,7 +411,13 @@ export class StopGapWidget extends EventEmitter {
}
}

public stop(opts = { forceDestroy: false }) {
/**
* Stops the widget messaging for if it is started. Skips stopping if it is an active
* widget.
* @param opts
* @returns
jryans marked this conversation as resolved.
Show resolved Hide resolved
*/
public stopMessaging(opts = { forceDestroy: false }) {
if (!opts?.forceDestroy && ActiveWidgetStore.instance.getPersistentWidgetId() === this.mockWidget.id) {
logger.log("Skipping destroy - persistent widget");
return;
Expand Down
Loading