mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-15 20:54:59 +08:00
Add maximise widget functionality (#7098)
Co-authored-by: J. Ryan Stinnett <jryans@gmail.com>
This commit is contained in:
parent
2f4f3f2a8c
commit
556cfc7ed8
@ -30,6 +30,8 @@ $MessageTimestamp_width_hover: calc($MessageTimestamp_width - 2 * $selected-mess
|
||||
$slider-dot-size: 1em;
|
||||
$slider-selection-dot-size: 2.4em;
|
||||
|
||||
$container-border-width: 8px;
|
||||
|
||||
:root {
|
||||
font-size: 10px;
|
||||
|
||||
|
@ -134,7 +134,7 @@ limitations under the License.
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
mask-repeat: no-repeat;
|
||||
mask-image: url('$(res)/img/feather-customised/maximise.svg');
|
||||
mask-image: url("$(res)/img/element-icons/maximise-expand.svg");
|
||||
background: $muted-fg-color;
|
||||
}
|
||||
}
|
||||
|
@ -23,9 +23,8 @@ limitations under the License.
|
||||
}
|
||||
|
||||
.mx_MainSplit > .mx_RightPanel_ResizeWrapper {
|
||||
padding: 5px;
|
||||
// margin left to not allow the handle to not encroach on the space for the scrollbar
|
||||
margin-left: 8px;
|
||||
// no padding on the left. The spacing is taken care of by the main split content.
|
||||
padding: 5px 5px 5px 0px;
|
||||
height: calc(100vh - 51px); // height of .mx_RoomHeader.light-panel
|
||||
|
||||
&:hover .mx_RightPanel_ResizeHandle {
|
||||
|
@ -22,7 +22,7 @@ limitations under the License.
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 8px;
|
||||
padding: 8px 0;
|
||||
padding: $container-border-width 0;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
contain: strict;
|
||||
|
@ -78,7 +78,7 @@ limitations under the License.
|
||||
}
|
||||
|
||||
.mx_HostSignup_maximize_button {
|
||||
mask: url('$(res)/img/feather-customised/maximise.svg');
|
||||
mask: url("$(res)/img/element-icons/maximise-expand.svg");
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: cover;
|
||||
@ -92,7 +92,7 @@ limitations under the License.
|
||||
}
|
||||
|
||||
.mx_HostSignup_minimize_button {
|
||||
mask: url('$(res)/img/feather-customised/minimise.svg');
|
||||
mask: url("$(res)/img/element-icons/minimise-collapse.svg");
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: cover;
|
||||
|
@ -32,11 +32,6 @@ limitations under the License.
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
.mx_MatrixChat > .mx_ResizeHandle.mx_ResizeHandle_horizontal {
|
||||
margin: 0 -10px 0 0;
|
||||
padding: 0 8px 0 0;
|
||||
}
|
||||
|
||||
.mx_ResizeHandle.mx_ResizeHandle_horizontal > div {
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
|
@ -29,19 +29,19 @@ limitations under the License.
|
||||
}
|
||||
|
||||
.mx_ViewSourceEvent_toggle {
|
||||
width: 12px;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: 0 center;
|
||||
mask-size: auto 12px;
|
||||
width: 12px;
|
||||
visibility: hidden;
|
||||
background-color: $accent;
|
||||
mask-image: url('$(res)/img/feather-customised/maximise.svg');
|
||||
mask-image: url("$(res)/img/element-icons/maximise-expand.svg");
|
||||
}
|
||||
|
||||
&.mx_ViewSourceEvent_expanded .mx_ViewSourceEvent_toggle {
|
||||
mask-position: 0 bottom;
|
||||
margin-bottom: 7px;
|
||||
mask-image: url('$(res)/img/feather-customised/minimise.svg');
|
||||
margin-bottom: 5px;
|
||||
mask-image: url("$(res)/img/element-icons/minimise-collapse.svg");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,6 +133,8 @@ limitations under the License.
|
||||
}
|
||||
|
||||
.mx_RoomSummaryCard_app_pinToggle,
|
||||
.mx_RoomSummaryCard_app_maximise,
|
||||
.mx_RoomSummaryCard_app_minimise,
|
||||
.mx_RoomSummaryCard_app_options {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@ -174,6 +176,21 @@ limitations under the License.
|
||||
mask-image: url('$(res)/img/element-icons/room/pin-upright.svg');
|
||||
}
|
||||
}
|
||||
.mx_RoomSummaryCard_app_maximise {
|
||||
right: 48px;
|
||||
&::before {
|
||||
mask-size: 14px;
|
||||
mask-image: url("$(res)/img/element-icons/maximise-expand.svg");
|
||||
}
|
||||
}
|
||||
.mx_RoomSummaryCard_app_minimise {
|
||||
right: 48px;
|
||||
&::before {
|
||||
mask-size: 14px;
|
||||
mask-image: url("$(res)/img/element-icons/minimise-collapse.svg");
|
||||
background-color: $accent;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomSummaryCard_app_options {
|
||||
right: 48px;
|
||||
@ -182,6 +199,9 @@ limitations under the License.
|
||||
&::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/ellipsis.svg');
|
||||
}
|
||||
&.mx_RoomSummaryCard_maximised_widget {
|
||||
right: 72px;
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_RoomSummaryCard_Button_pinned {
|
||||
|
@ -20,6 +20,8 @@ limitations under the License.
|
||||
|
||||
.mx_AppTileFullWidth {
|
||||
max-width: unset;
|
||||
width: auto !important;
|
||||
margin: 0px $container-border-width 0px $container-border-width;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
|
@ -18,11 +18,12 @@ limitations under the License.
|
||||
$MiniAppTileHeight: 200px;
|
||||
|
||||
.mx_AppsDrawer {
|
||||
margin: 5px 5px 5px 18px;
|
||||
margin: 5px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
flex-grow: 1;
|
||||
|
||||
.mx_AppsContainer_resizerHandleContainer {
|
||||
width: 100%;
|
||||
@ -100,11 +101,11 @@ $MiniAppTileHeight: 200px;
|
||||
min-height: 0;
|
||||
|
||||
.mx_AppTile:first-of-type {
|
||||
border-left-width: 8px;
|
||||
border-left-width: $container-border-width;
|
||||
border-radius: 10px 0 0 10px;
|
||||
}
|
||||
.mx_AppTile:last-of-type {
|
||||
border-right-width: 8px;
|
||||
border-right-width: $container-border-width;
|
||||
border-radius: 0 10px 10px 0;
|
||||
}
|
||||
|
||||
@ -142,7 +143,7 @@ $MinWidth: 240px;
|
||||
.mx_AppTile {
|
||||
width: 50%;
|
||||
min-width: $MinWidth;
|
||||
border: 8px solid $widget-menu-bar-bg-color;
|
||||
border: $container-border-width solid $widget-menu-bar-bg-color;
|
||||
border-left-width: 5px;
|
||||
border-right-width: 5px;
|
||||
display: flex;
|
||||
@ -155,7 +156,7 @@ $MinWidth: 240px;
|
||||
width: 100% !important; // to override the inline style set by the resizer
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 5px solid $widget-menu-bar-bg-color;
|
||||
border: $container-border-width solid $widget-menu-bar-bg-color;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -224,15 +225,28 @@ $MinWidth: 240px;
|
||||
mask-position: 0 center;
|
||||
mask-size: auto 12px;
|
||||
background-color: $topleftmenu-color;
|
||||
margin: 0 3px;
|
||||
}
|
||||
margin: 0 5px;
|
||||
|
||||
.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_popout {
|
||||
mask-image: url('$(res)/img/feather-customised/widget/external-link.svg');
|
||||
}
|
||||
&.mx_AppTileMenuBar_iconButton_minWidget {
|
||||
width: 10px;
|
||||
height: 12px;
|
||||
mask-size: auto 10px;
|
||||
mask-image: url("$(res)/img/element-icons/minimise-collapse.svg");
|
||||
}
|
||||
|
||||
.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_menu {
|
||||
mask-image: url('$(res)/img/element-icons/room/ellipsis.svg');
|
||||
&.mx_AppTileMenuBar_iconButton_maxWidget {
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
mask-image: url("$(res)/img/element-icons/maximise-expand.svg");
|
||||
}
|
||||
|
||||
&.mx_AppTileMenuBar_iconButton_popout {
|
||||
mask-image: url('$(res)/img/feather-customised/widget/external-link.svg');
|
||||
}
|
||||
|
||||
&.mx_AppTileMenuBar_iconButton_menu {
|
||||
mask-image: url('$(res)/img/element-icons/room/ellipsis.svg');
|
||||
}
|
||||
}
|
||||
|
||||
.mx_AppTileBody {
|
||||
|
@ -546,13 +546,13 @@ $left-gutter: 64px;
|
||||
mask-size: 75%;
|
||||
mask-position: center;
|
||||
mask-repeat: no-repeat;
|
||||
mask-image: url($collapse-button-url);
|
||||
mask-image: url("$(res)/img/element-icons/minimise-collapse.svg");
|
||||
}
|
||||
.mx_EventTile_expandButton {
|
||||
mask-size: 75%;
|
||||
mask-position: center;
|
||||
mask-repeat: no-repeat;
|
||||
mask-image: url($expand-button-url);
|
||||
mask-image: url("$(res)/img/element-icons/maximise-expand.svg");
|
||||
}
|
||||
|
||||
.mx_EventTile_body .mx_EventTile_pre_container:focus-within .mx_EventTile_copyButton,
|
||||
|
3
res/img/element-icons/maximise-expand.svg
Normal file
3
res/img/element-icons/maximise-expand.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 4.34778V0.777778C14 0.35 13.65 0 13.2222 0H9.65222C8.96 0 8.61 0.84 9.1 1.33L10.3367 2.56667L2.55889 10.3444L1.32222 9.10778C0.84 8.61778 0 8.96 0 9.65222V13.2222C0 13.65 0.35 14 0.777778 14H4.34778C5.04 14 5.39 13.16 4.9 12.67L3.66333 11.4333L11.4411 3.65556L12.6778 4.89222C13.16 5.38222 14 5.04 14 4.34778Z" fill="#737D8C"/>
|
||||
</svg>
|
After Width: | Height: | Size: 444 B |
3
res/img/element-icons/minimise-collapse.svg
Normal file
3
res/img/element-icons/minimise-collapse.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.7855 1.25024L10.4345 4.60126L11.5953 5.76207C12.0553 6.22202 11.7267 7.01049 11.077 7.01049H7.73326C7.33172 7.01049 7.00319 6.68196 7.00319 6.28042V2.9221C7.00319 2.27234 7.79166 1.94381 8.25161 2.40375L9.41242 3.56456L12.7634 0.213545C13.0482 -0.0711818 13.5081 -0.0711818 13.7928 0.213545C14.0703 0.505573 14.0703 0.965517 13.7855 1.25024ZM1.25024 13.7855L4.60126 10.4345L5.76207 11.5953C6.22202 12.0553 7.01049 11.7267 7.01049 11.077V7.73326C7.01049 7.33172 6.68196 7.00319 6.28042 7.00319H2.9221C2.27234 7.00319 1.94381 7.79166 2.40375 8.25161L3.56456 9.41242L0.213545 12.7634C-0.0711818 13.0482 -0.0711818 13.5081 0.213545 13.7928C0.505573 14.0703 0.965517 14.0703 1.25024 13.7855Z" fill="#737D8C"/>
|
||||
</svg>
|
After Width: | Height: | Size: 821 B |
@ -1,63 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="11"
|
||||
height="11"
|
||||
viewBox="0 0 11 11"
|
||||
version="1.1"
|
||||
id="svg6"
|
||||
sodipodi:docname="maximise.svg"
|
||||
inkscape:version="0.92.3 (2405546, 2018-03-11)">
|
||||
<metadata
|
||||
id="metadata12">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs10" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="750"
|
||||
inkscape:window-height="480"
|
||||
id="namedview8"
|
||||
showgrid="false"
|
||||
inkscape:zoom="21.454545"
|
||||
inkscape:cx="5.5"
|
||||
inkscape:cy="5.5"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg6" />
|
||||
<g
|
||||
fill="none"
|
||||
fill-rule="evenodd"
|
||||
stroke="#7AC9A1"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="g4"
|
||||
style="stroke:#000000;stroke-opacity:1">
|
||||
<path
|
||||
d="M7 1h3v3M4 10H1V7M10 1L6.5 4.5M1 10l3.5-3.5"
|
||||
id="path2"
|
||||
style="stroke:#000000;stroke-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.7 KiB |
@ -1,65 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="10.2101"
|
||||
height="10.2101"
|
||||
viewBox="0 0 10.2101 10.210099"
|
||||
version="1.1"
|
||||
id="svg6"
|
||||
sodipodi:docname="minimise.svg"
|
||||
inkscape:version="0.92.3 (2405546, 2018-03-11)">
|
||||
<metadata
|
||||
id="metadata12">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs10" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1016"
|
||||
id="namedview8"
|
||||
showgrid="false"
|
||||
fit-margin-top="0.1"
|
||||
fit-margin-left="0.1"
|
||||
fit-margin-right="0.1"
|
||||
fit-margin-bottom="0.1"
|
||||
inkscape:zoom="26.222222"
|
||||
inkscape:cx="-1.5686788"
|
||||
inkscape:cy="5.0287789"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg6" />
|
||||
<g
|
||||
id="g4"
|
||||
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
|
||||
transform="translate(-0.39494988,0.60505012)">
|
||||
<path
|
||||
d="m 1.5,5.5 h 3 v 3 m 5,-5 h -3 v -3 m 0,3 L 10,0 M 1,9 4.5,5.5"
|
||||
id="path2"
|
||||
inkscape:connector-curvature="0"
|
||||
style="stroke:#000000;stroke-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.9 KiB |
@ -208,8 +208,6 @@ $event-highlight-bg-color: $yellow-background;
|
||||
$event-timestamp-color: #acacac;
|
||||
|
||||
$copy-button-url: "$(res)/img/feather-customised/clipboard.svg";
|
||||
$collapse-button-url: "$(res)/img/feather-customised/minimise.svg";
|
||||
$expand-button-url: "$(res)/img/feather-customised/maximise.svg";
|
||||
|
||||
// e2e
|
||||
$e2e-verified-color: #76cfa5; // N.B. *NOT* the same as $accent
|
||||
|
@ -296,8 +296,6 @@ $focus-brightness: 105%;
|
||||
// Icon URLs
|
||||
// ********************
|
||||
$copy-button-url: "$(res)/img/feather-customised/clipboard.svg";
|
||||
$collapse-button-url: "$(res)/img/feather-customised/minimise.svg";
|
||||
$expand-button-url: "$(res)/img/feather-customised/maximise.svg";
|
||||
// ********************
|
||||
|
||||
// Mixins
|
||||
|
@ -95,6 +95,7 @@ import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';
|
||||
import { dispatchShowThreadEvent } from '../../dispatcher/dispatch-actions/threads';
|
||||
import { fetchInitialEvent } from "../../utils/EventUtils";
|
||||
import { ComposerType } from "../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import AppsDrawer from '../views/rooms/AppsDrawer';
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function(msg: string) {};
|
||||
@ -119,6 +120,13 @@ interface IRoomProps extends MatrixClientProps {
|
||||
onRegistered?(credentials: IMatrixClientCreds): void;
|
||||
}
|
||||
|
||||
// This defines the content of the mainSplit.
|
||||
// If the mainSplit does not contain the Timeline, the chat is shown in the right panel.
|
||||
enum MainSplitContentType {
|
||||
Timeline,
|
||||
MaximisedWidget,
|
||||
// Video
|
||||
}
|
||||
export interface IRoomState {
|
||||
room?: Room;
|
||||
roomId?: string;
|
||||
@ -188,6 +196,7 @@ export interface IRoomState {
|
||||
rejecting?: boolean;
|
||||
rejectError?: Error;
|
||||
hasPinnedWidgets?: boolean;
|
||||
mainSplitContentType?: MainSplitContentType;
|
||||
dragCounter: number;
|
||||
// whether or not a spaces context switch brought us here,
|
||||
// if it did we don't want the room to be marked as read as soon as it is loaded.
|
||||
@ -254,6 +263,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
showAvatarChanges: true,
|
||||
showDisplaynameChanges: true,
|
||||
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
|
||||
mainSplitContentType: MainSplitContentType.Timeline,
|
||||
dragCounter: 0,
|
||||
timelineRenderingType: TimelineRenderingType.Room,
|
||||
liveTimeline: undefined,
|
||||
@ -306,18 +316,35 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
}
|
||||
|
||||
private onWidgetStoreUpdate = () => {
|
||||
if (this.state.room) {
|
||||
this.checkWidgets(this.state.room);
|
||||
}
|
||||
if (!this.state.room) return;
|
||||
this.checkWidgets(this.state.room);
|
||||
};
|
||||
|
||||
private onWidgetEchoStoreUpdate = () => {
|
||||
if (!this.state.room) return;
|
||||
this.checkWidgets(this.state.room);
|
||||
};
|
||||
|
||||
private onWidgetLayoutChange = () => {
|
||||
if (!this.state.room) return;
|
||||
this.checkWidgets(this.state.room);
|
||||
};
|
||||
|
||||
private checkWidgets = (room) => {
|
||||
this.setState({
|
||||
hasPinnedWidgets: WidgetLayoutStore.instance.getContainerWidgets(room, Container.Top).length > 0,
|
||||
showApps: this.shouldShowApps(room),
|
||||
hasPinnedWidgets: WidgetLayoutStore.instance.hasPinnedWidgets(this.state.room),
|
||||
mainSplitContentType: this.getMainSplitContentType(),
|
||||
showApps: this.shouldShowApps(this.state.room),
|
||||
});
|
||||
};
|
||||
|
||||
private getMainSplitContentType = () => {
|
||||
// TODO-video check if video should be displayed in main panel
|
||||
return (WidgetLayoutStore.instance.hasMaximisedWidget(this.state.room))
|
||||
? MainSplitContentType.MaximisedWidget
|
||||
: MainSplitContentType.Timeline;
|
||||
};
|
||||
|
||||
private onReadReceiptsChange = () => {
|
||||
this.setState({
|
||||
showReadReceipts: SettingsStore.getValue("showReadReceipts", this.state.roomId),
|
||||
@ -504,18 +531,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
}
|
||||
}
|
||||
|
||||
private onWidgetEchoStoreUpdate = () => {
|
||||
if (!this.state.room) return;
|
||||
this.setState({
|
||||
hasPinnedWidgets: WidgetLayoutStore.instance.getContainerWidgets(this.state.room, Container.Top).length > 0,
|
||||
showApps: this.shouldShowApps(this.state.room),
|
||||
});
|
||||
};
|
||||
|
||||
private onWidgetLayoutChange = () => {
|
||||
this.onWidgetEchoStoreUpdate(); // we cheat here by calling the thing that matters
|
||||
};
|
||||
|
||||
private setupRoom(room: Room, roomId: string, joining: boolean, shouldPeek: boolean) {
|
||||
// if this is an unknown room then we're in one of three states:
|
||||
// - This is a room we can peek into (search engine) (we can /peek)
|
||||
@ -972,7 +987,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
if (this.unmounted) return;
|
||||
// Attach a widget store listener only when we get a room
|
||||
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(room), this.onWidgetLayoutChange);
|
||||
this.onWidgetLayoutChange(); // provoke an update
|
||||
|
||||
this.calculatePeekRules(room);
|
||||
this.updatePreviewUrlVisibility(room);
|
||||
@ -2094,6 +2108,38 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
|
||||
const showChatEffects = SettingsStore.getValue('showChatEffects');
|
||||
|
||||
// Decide what to show in the main split
|
||||
let mainSplitBody = <React.Fragment>
|
||||
{ auxPanel }
|
||||
<div className={timelineClasses}>
|
||||
{ fileDropTarget }
|
||||
{ topUnreadMessagesBar }
|
||||
{ jumpToBottom }
|
||||
{ messagePanel }
|
||||
{ searchResultsPanel }
|
||||
</div>
|
||||
{ statusBarArea }
|
||||
{ previewBar }
|
||||
{ messageComposer }
|
||||
</React.Fragment>;
|
||||
|
||||
switch (this.state.mainSplitContentType) {
|
||||
case MainSplitContentType.Timeline:
|
||||
// keep the timeline in as the mainSplitBody
|
||||
break;
|
||||
case MainSplitContentType.MaximisedWidget:
|
||||
if (!SettingsStore.getValue("feature_maximised_widgets")) break;
|
||||
mainSplitBody = <AppsDrawer
|
||||
room={this.state.room}
|
||||
userId={this.context.credentials.userId}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
showApps={true}
|
||||
/>;
|
||||
break;
|
||||
// TODO-video MainSplitContentType.Video:
|
||||
// break;
|
||||
}
|
||||
|
||||
return (
|
||||
<RoomContext.Provider value={this.state}>
|
||||
<main className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}>
|
||||
@ -2115,17 +2161,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
/>
|
||||
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
|
||||
<div className="mx_RoomView_body">
|
||||
{ auxPanel }
|
||||
<div className={timelineClasses}>
|
||||
{ fileDropTarget }
|
||||
{ topUnreadMessagesBar }
|
||||
{ jumpToBottom }
|
||||
{ messagePanel }
|
||||
{ searchResultsPanel }
|
||||
</div>
|
||||
{ statusBarArea }
|
||||
{ previewBar }
|
||||
{ messageComposer }
|
||||
{ mainSplitBody }
|
||||
</div>
|
||||
</MainSplit>
|
||||
</ErrorBoundary>
|
||||
|
@ -40,7 +40,7 @@ import WidgetAvatar from "../avatars/WidgetAvatar";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { IApp } from "../../../stores/WidgetStore";
|
||||
|
||||
import { WidgetLayoutStore, Container } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
interface IProps {
|
||||
app: IApp;
|
||||
// If room is not specified then it is an account level widget
|
||||
@ -400,6 +400,14 @@ export default class AppTile extends React.Component<IProps, IState> {
|
||||
{ target: '_blank', href: this.sgWidget.popoutUrl, rel: 'noreferrer noopener' }).click();
|
||||
};
|
||||
|
||||
private onMaxMinWidgetClick = (): void => {
|
||||
const targetContainer =
|
||||
WidgetLayoutStore.instance.isInContainer(this.props.room, this.props.app, Container.Center)
|
||||
? Container.Right
|
||||
: Container.Center;
|
||||
WidgetLayoutStore.instance.moveToContainer(this.props.room, this.props.app, targetContainer);
|
||||
};
|
||||
|
||||
private onContextMenuClick = (): void => {
|
||||
this.setState({ menuDisplayed: true });
|
||||
};
|
||||
@ -522,6 +530,23 @@ export default class AppTile extends React.Component<IProps, IState> {
|
||||
/>
|
||||
);
|
||||
}
|
||||
let maxMinButton;
|
||||
if (SettingsStore.getValue("feature_maximised_widgets")) {
|
||||
const widgetIsMaximised = WidgetLayoutStore.instance.
|
||||
isInContainer(this.props.room, this.props.app, Container.Center);
|
||||
maxMinButton = <AccessibleButton
|
||||
className={
|
||||
"mx_AppTileMenuBar_iconButton"
|
||||
+ (widgetIsMaximised
|
||||
? " mx_AppTileMenuBar_iconButton_minWidget"
|
||||
: " mx_AppTileMenuBar_iconButton_maxWidget")
|
||||
}
|
||||
title={
|
||||
widgetIsMaximised ? _t('Close'): _t('Maximise widget')
|
||||
}
|
||||
onClick={this.onMaxMinWidgetClick}
|
||||
/>;
|
||||
}
|
||||
|
||||
return <React.Fragment>
|
||||
<div className={appTileClasses} id={this.props.app.id}>
|
||||
@ -531,6 +556,7 @@ export default class AppTile extends React.Component<IProps, IState> {
|
||||
{ this.props.showTitle && this.getTileTitle() }
|
||||
</span>
|
||||
<span className="mx_AppTileMenuBarWidgets">
|
||||
{ maxMinButton }
|
||||
{ (this.props.showPopout && !this.state.requiresClient) && <AccessibleButton
|
||||
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_popout"
|
||||
title={_t('Popout widget')}
|
||||
|
@ -138,14 +138,28 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
|
||||
mx_RoomSummaryCard_Button_pinned: isPinned,
|
||||
});
|
||||
|
||||
const isMaximised = WidgetLayoutStore.instance.isInContainer(room, app, Container.Center);
|
||||
const toggleMaximised = isMaximised
|
||||
? () => { WidgetLayoutStore.instance.moveToContainer(room, app, Container.Right); }
|
||||
: () => { WidgetLayoutStore.instance.moveToContainer(room, app, Container.Center); };
|
||||
|
||||
const maximiseTitle = isMaximised ? _t("Close") : _t("Maximise widget");
|
||||
|
||||
let openTitle = "";
|
||||
if (isPinned) {
|
||||
openTitle = _t("Unpin this widget to view it in this panel");
|
||||
} else if (isMaximised) {
|
||||
openTitle =_t("Close this widget to view it in this panel");
|
||||
}
|
||||
|
||||
return <div className={classes} ref={handle}>
|
||||
<AccessibleTooltipButton
|
||||
className="mx_RoomSummaryCard_icon_app"
|
||||
onClick={onOpenWidgetClick}
|
||||
// only show a tooltip if the widget is pinned
|
||||
title={isPinned ? _t("Unpin a widget to view it in this panel") : ""}
|
||||
forceHide={!isPinned}
|
||||
disabled={isPinned}
|
||||
title={openTitle}
|
||||
forceHide={!(isPinned || isMaximised)}
|
||||
disabled={isPinned || isMaximised}
|
||||
yOffset={-48}
|
||||
>
|
||||
<WidgetAvatar app={app} />
|
||||
@ -154,7 +168,10 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
|
||||
</AccessibleTooltipButton>
|
||||
|
||||
<ContextMenuTooltipButton
|
||||
className="mx_RoomSummaryCard_app_options"
|
||||
className={classNames({
|
||||
"mx_RoomSummaryCard_app_options": true,
|
||||
"mx_RoomSummaryCard_maximised_widget": SettingsStore.getValue("feature_maximised_widgets"),
|
||||
})}
|
||||
isExpanded={menuDisplayed}
|
||||
onClick={openMenu}
|
||||
title={_t("Options")}
|
||||
@ -168,6 +185,13 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
|
||||
disabled={cannotPin}
|
||||
yOffset={-24}
|
||||
/>
|
||||
{ SettingsStore.getValue("feature_maximised_widgets") &&
|
||||
<AccessibleTooltipButton
|
||||
className={isMaximised ? "mx_RoomSummaryCard_app_minimise" : "mx_RoomSummaryCard_app_maximise"}
|
||||
onClick={toggleMaximised}
|
||||
title={maximiseTitle}
|
||||
yOffset={-24}
|
||||
/> }
|
||||
|
||||
{ contextMenu }
|
||||
</div>;
|
||||
|
@ -47,7 +47,8 @@ interface IProps {
|
||||
}
|
||||
|
||||
interface IState {
|
||||
apps: IApp[];
|
||||
// @ts-ignore - TS wants a string key, but we know better
|
||||
apps: {[id: Container]: IApp[]};
|
||||
resizingVertical: boolean; // true when changing the height of the apps drawer
|
||||
resizingHorizontal: boolean; // true when chagning the distribution of the width between widgets
|
||||
resizing: boolean;
|
||||
@ -118,7 +119,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
|
||||
this.resizeContainer.classList.remove("mx_AppsDrawer_resizing");
|
||||
WidgetLayoutStore.instance.setResizerDistributions(
|
||||
this.props.room, Container.Top,
|
||||
this.state.apps.slice(1).map((_, i) => this.resizer.forHandleAt(i).size),
|
||||
this.topApps().slice(1).map((_, i) => this.resizer.forHandleAt(i).size),
|
||||
);
|
||||
this.setState({ resizingHorizontal: false });
|
||||
},
|
||||
@ -148,7 +149,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
|
||||
if (prevProps.userId !== this.props.userId || prevProps.room !== this.props.room) {
|
||||
// Room has changed, update apps
|
||||
this.updateApps();
|
||||
} else if (this.getAppsHash(this.state.apps) !== this.getAppsHash(prevState.apps)) {
|
||||
} else if (this.getAppsHash(this.topApps()) !== this.getAppsHash(prevState.apps[Container.Top])) {
|
||||
this.loadResizerPreferences();
|
||||
}
|
||||
}
|
||||
@ -163,7 +164,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
|
||||
|
||||
private loadResizerPreferences = (): void => {
|
||||
const distributions = WidgetLayoutStore.instance.getResizerDistributions(this.props.room, Container.Top);
|
||||
if (this.state.apps && (this.state.apps.length - 1) === distributions.length) {
|
||||
if (this.state.apps && (this.topApps().length - 1) === distributions.length) {
|
||||
distributions.forEach((size, i) => {
|
||||
const distributor = this.resizer.forHandleAt(i);
|
||||
if (distributor) {
|
||||
@ -200,8 +201,16 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private getApps = (): IApp[] => WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top);
|
||||
// @ts-ignore - TS wants a string key, but we know better
|
||||
private getApps = (): { [id: Container]: IApp[] } => {
|
||||
// @ts-ignore
|
||||
const appsDict: { [id: Container]: IApp[] } = {};
|
||||
appsDict[Container.Top] = WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top);
|
||||
appsDict[Container.Center] = WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Center);
|
||||
return appsDict;
|
||||
};
|
||||
private topApps = (): IApp[] => this.state.apps[Container.Top];
|
||||
private centerApps = (): IApp[] => this.state.apps[Container.Center];
|
||||
|
||||
private updateApps = (): void => {
|
||||
this.setState({
|
||||
@ -211,8 +220,9 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
|
||||
|
||||
public render(): JSX.Element {
|
||||
if (!this.props.showApps) return <div />;
|
||||
|
||||
const apps = this.state.apps.map((app, index, arr) => {
|
||||
const widgetIsMaxmised: boolean = this.centerApps().length > 0;
|
||||
const appsToDisplay = widgetIsMaxmised ? this.centerApps() : this.topApps();
|
||||
const apps = appsToDisplay.map((app, index, arr) => {
|
||||
return (<AppTile
|
||||
key={app.id}
|
||||
app={app}
|
||||
@ -242,33 +252,42 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
|
||||
|
||||
const classes = classNames({
|
||||
mx_AppsDrawer: true,
|
||||
mx_AppsDrawer_maximise: widgetIsMaxmised,
|
||||
mx_AppsDrawer_fullWidth: apps.length < 2,
|
||||
mx_AppsDrawer_resizing: this.state.resizing,
|
||||
mx_AppsDrawer_2apps: apps.length === 2,
|
||||
mx_AppsDrawer_3apps: apps.length === 3,
|
||||
});
|
||||
const appConatiners =
|
||||
<div className="mx_AppsContainer" ref={this.collectResizer}>
|
||||
{ apps.map((app, i) => {
|
||||
if (i < 1) return app;
|
||||
return <React.Fragment key={app.key}>
|
||||
<ResizeHandle reverse={i > apps.length / 2} />
|
||||
{ app }
|
||||
</React.Fragment>;
|
||||
}) }
|
||||
</div>;
|
||||
|
||||
let drawer;
|
||||
if (widgetIsMaxmised) {
|
||||
drawer = appConatiners;
|
||||
} else {
|
||||
drawer = <PersistentVResizer
|
||||
room={this.props.room}
|
||||
minHeight={100}
|
||||
maxHeight={(this.props.maxHeight || !widgetIsMaxmised) ? this.props.maxHeight - 50 : undefined}
|
||||
handleClass="mx_AppsContainer_resizerHandle"
|
||||
handleWrapperClass="mx_AppsContainer_resizerHandleContainer"
|
||||
className="mx_AppsContainer_resizer"
|
||||
resizeNotifier={this.props.resizeNotifier}>
|
||||
{ appConatiners }
|
||||
</PersistentVResizer>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<PersistentVResizer
|
||||
room={this.props.room}
|
||||
minHeight={100}
|
||||
maxHeight={this.props.maxHeight ? this.props.maxHeight - 50 : undefined}
|
||||
handleClass="mx_AppsContainer_resizerHandle"
|
||||
handleWrapperClass="mx_AppsContainer_resizerHandleContainer"
|
||||
className="mx_AppsContainer_resizer"
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
>
|
||||
<div className="mx_AppsContainer" ref={this.collectResizer}>
|
||||
{ apps.map((app, i) => {
|
||||
if (i < 1) return app;
|
||||
return <React.Fragment key={app.key}>
|
||||
<ResizeHandle reverse={i > apps.length / 2} />
|
||||
{ app }
|
||||
</React.Fragment>;
|
||||
}) }
|
||||
</div>
|
||||
</PersistentVResizer>
|
||||
{ drawer }
|
||||
{ spinner }
|
||||
</div>
|
||||
);
|
||||
|
@ -1873,7 +1873,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",
|
||||
|
@ -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());
|
||||
|
125
test/stores/WidgetLayoutStore-test.ts
Normal file
125
test/stores/WidgetLayoutStore-test.ts
Normal file
@ -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 = <Room>{
|
||||
roomId: roomId,
|
||||
currentState: {
|
||||
getStateEvents: (_l, _x) => {
|
||||
return {
|
||||
getId: ()=>"$layoutEventId",
|
||||
getContent: () => null,
|
||||
};
|
||||
},
|
||||
} };
|
||||
|
||||
const mockApps = [
|
||||
<IApp> { roomId: roomId, id: "1" },
|
||||
<IApp> { roomId: roomId, id: "2" },
|
||||
<IApp> { roomId: roomId, id: "3" },
|
||||
<IApp> { roomId: roomId, id: "4" },
|
||||
];
|
||||
|
||||
// fake the WidgetStore.instance to just return an object with `getApps`
|
||||
jest.spyOn(WidgetStore, 'instance', 'get').mockReturnValue(<WidgetStore>{ 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]]));
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user