Merge pull request #5553 from matrix-org/travis/widget-layout
Support room-defined widget layouts
This commit is contained in:
commit
a779951512
22 changed files with 855 additions and 272 deletions
|
@ -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) {
|
||||
|
@ -184,9 +186,18 @@ const AppsSection: React.FC<IAppsSectionProps> = ({ room }) => {
|
|||
}
|
||||
};
|
||||
|
||||
let copyLayoutBtn = null;
|
||||
if (apps.length > 0 && WidgetLayoutStore.instance.canCopyLayoutToRoom(room)) {
|
||||
copyLayoutBtn = (
|
||||
<AccessibleButton kind="link" onClick={() => WidgetLayoutStore.instance.copyLayoutToRoom(room)}>
|
||||
{ _t("Set my room layout for everyone") }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return <Group className="mx_RoomSummaryCard_appsGroup" title={_t("Widgets")}>
|
||||
{ apps.map(app => <AppRow key={app.id} app={app} room={room} />) }
|
||||
|
||||
{ copyLayoutBtn }
|
||||
<AccessibleButton kind="link" onClick={onManageIntegrations}>
|
||||
{ apps.length > 0 ? _t("Edit widgets, bridges & bots") : _t("Add widgets, bridges & bots") }
|
||||
</AccessibleButton>
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -28,12 +28,13 @@ import WidgetUtils from '../../../utils/WidgetUtils';
|
|||
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
|
||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {useLocalStorageState} from "../../../hooks/useLocalStorageState";
|
||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||
import WidgetStore from "../../../stores/WidgetStore";
|
||||
import ResizeHandle from "../elements/ResizeHandle";
|
||||
import Resizer from "../../../resizer/resizer";
|
||||
import PercentageDistributor from "../../../resizer/distributors/percentage";
|
||||
import {Container, WidgetLayoutStore} from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import {clamp, percentageOf, percentageWithin} from "../../../utils/numbers";
|
||||
import {useStateCallback} from "../../../hooks/useStateCallback";
|
||||
|
||||
export default class AppsDrawer extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -62,13 +63,13 @@ export default class AppsDrawer extends React.Component {
|
|||
|
||||
componentDidMount() {
|
||||
ScalarMessaging.startListening();
|
||||
WidgetStore.instance.on(this.props.room.roomId, this._updateApps);
|
||||
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(this.props.room), this._updateApps);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
ScalarMessaging.stopListening();
|
||||
WidgetStore.instance.off(this.props.room.roomId, this._updateApps);
|
||||
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(this.props.room), this._updateApps);
|
||||
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
|
||||
if (this._resizeContainer) {
|
||||
this.resizer.detach();
|
||||
|
@ -102,11 +103,10 @@ export default class AppsDrawer extends React.Component {
|
|||
},
|
||||
onResizeStop: () => {
|
||||
this._resizeContainer.classList.remove("mx_AppsDrawer_resizing");
|
||||
// persist to localStorage
|
||||
localStorage.setItem(this._getStorageKey(), JSON.stringify([
|
||||
this.state.apps.map(app => app.id),
|
||||
...this.state.apps.slice(1).map((_, i) => this.resizer.forHandleAt(i).size),
|
||||
]));
|
||||
WidgetLayoutStore.instance.setResizerDistributions(
|
||||
this.props.room, Container.Top,
|
||||
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
|
||||
|
@ -128,8 +128,6 @@ export default class AppsDrawer extends React.Component {
|
|||
this._loadResizerPreferences();
|
||||
};
|
||||
|
||||
_getStorageKey = () => `mx_apps_drawer-${this.props.room.roomId}`;
|
||||
|
||||
_getAppsHash = (apps) => apps.map(app => app.id).join("~");
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
|
@ -147,24 +145,16 @@ export default class AppsDrawer extends React.Component {
|
|||
};
|
||||
|
||||
_loadResizerPreferences = () => {
|
||||
try {
|
||||
const [[...lastIds], ...sizes] = JSON.parse(localStorage.getItem(this._getStorageKey()));
|
||||
// Every app was included in the last split, reuse the last sizes
|
||||
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);
|
||||
if (distributor) {
|
||||
distributor.size = size;
|
||||
distributor.finish();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
// this is expected
|
||||
}
|
||||
|
||||
if (this.state.apps) {
|
||||
const distributions = WidgetLayoutStore.instance.getResizerDistributions(this.props.room, Container.Top);
|
||||
if (this.state.apps && (this.state.apps.length - 1) === distributions.length) {
|
||||
distributions.forEach((size, i) => {
|
||||
const distributor = this.resizer.forHandleAt(i);
|
||||
if (distributor) {
|
||||
distributor.size = size;
|
||||
distributor.finish();
|
||||
}
|
||||
});
|
||||
} else if (this.state.apps) {
|
||||
const distributors = this.resizer.getDistributors();
|
||||
distributors.forEach(d => d.item.clearSize());
|
||||
distributors.forEach(d => d.start());
|
||||
|
@ -190,7 +180,7 @@ export default class AppsDrawer extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
_getApps = () => WidgetStore.instance.getPinnedApps(this.props.room.roomId);
|
||||
_getApps = () => WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top);
|
||||
|
||||
_updateApps = () => {
|
||||
this.setState({
|
||||
|
@ -248,7 +238,7 @@ export default class AppsDrawer extends React.Component {
|
|||
return (
|
||||
<div className={classes}>
|
||||
<PersistentVResizer
|
||||
id={"apps-drawer_" + this.props.room.roomId}
|
||||
room={this.props.room}
|
||||
minHeight={100}
|
||||
maxHeight={this.props.maxHeight ? this.props.maxHeight - 50 : undefined}
|
||||
handleClass="mx_AppsContainer_resizerHandle"
|
||||
|
@ -273,7 +263,7 @@ export default class AppsDrawer extends React.Component {
|
|||
}
|
||||
|
||||
const PersistentVResizer = ({
|
||||
id,
|
||||
room,
|
||||
minHeight,
|
||||
maxHeight,
|
||||
className,
|
||||
|
@ -282,7 +272,24 @@ const PersistentVResizer = ({
|
|||
resizeNotifier,
|
||||
children,
|
||||
}) => {
|
||||
const [height, setHeight] = useLocalStorageState("pvr_" + id, 280); // old fixed height was 273px
|
||||
let defaultHeight = WidgetLayoutStore.instance.getContainerHeight(room, Container.Top);
|
||||
|
||||
// Arbitrary defaults to avoid NaN problems. 100 px or 3/4 of the visible window.
|
||||
if (!minHeight) minHeight = 100;
|
||||
if (!maxHeight) maxHeight = (window.innerHeight / 4) * 3;
|
||||
|
||||
// Convert from percentage to height. Note that the default height is 280px.
|
||||
if (defaultHeight) {
|
||||
defaultHeight = clamp(defaultHeight, 0, 100);
|
||||
defaultHeight = percentageWithin(defaultHeight / 100, minHeight, maxHeight);
|
||||
} else {
|
||||
defaultHeight = 280;
|
||||
}
|
||||
|
||||
const [height, setHeight] = useStateCallback(defaultHeight, newHeight => {
|
||||
newHeight = percentageOf(newHeight, minHeight, maxHeight) * 100;
|
||||
WidgetLayoutStore.instance.setContainerHeight(room, Container.Top, newHeight);
|
||||
});
|
||||
|
||||
return <Resizable
|
||||
size={{height: Math.min(height, maxHeight)}}
|
||||
|
|
|
@ -37,6 +37,7 @@ import {E2E_STATE} from "./E2EIcon";
|
|||
import {toRem} from "../../../utils/units";
|
||||
import {WidgetType} from "../../../widgets/WidgetType";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import {WIDGET_LAYOUT_EVENT_TYPE} from "../../../stores/widgets/WidgetLayoutStore";
|
||||
|
||||
const eventTileTypes = {
|
||||
'm.room.message': 'messages.MessageEvent',
|
||||
|
@ -65,6 +66,7 @@ const stateEventTileTypes = {
|
|||
'm.room.server_acl': 'messages.TextualEvent',
|
||||
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
|
||||
'im.vector.modular.widgets': 'messages.TextualEvent',
|
||||
[WIDGET_LAYOUT_EVENT_TYPE]: 'messages.TextualEvent',
|
||||
'm.room.tombstone': 'messages.TextualEvent',
|
||||
'm.room.join_rules': 'messages.TextualEvent',
|
||||
'm.room.guest_access': 'messages.TextualEvent',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue