Skip to content

Commit 84349f9

Browse files
Sticker widget UI/UX/permissions improvements
1 parent 3e04b24 commit 84349f9

14 files changed

Lines changed: 334 additions & 116 deletions

File tree

apps/web/res/css/views/rooms/_Stickers.pcss

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
.mx_Stickers_content_container {
66
overflow: hidden;
7-
height: 300px;
7+
height: 450px;
88
}
99

1010
#mx_persistedElement_stickerPicker {
@@ -21,8 +21,8 @@
2121
}
2222

2323
iframe {
24-
/* Sticker picker depends on the fixed height previously used for all tiles */
25-
height: 283px; /* height of the popout minus the AppTile menu bar */
24+
/* Sticker picker depends on a fixed popover height */
25+
height: 433px; /* 450px popover minus the AppTile menu bar */
2626
}
2727
}
2828

@@ -44,3 +44,31 @@
4444
cursor: pointer;
4545
color: $accent;
4646
}
47+
48+
.mx_Stickers_sidebar {
49+
display: flex;
50+
flex: 1;
51+
width: 100%;
52+
min-height: 0;
53+
overflow: hidden;
54+
55+
.mx_AppTileFullWidth {
56+
width: 100% !important;
57+
max-width: unset;
58+
}
59+
}
60+
61+
.mx_Stickers_sidebarPlaceholder {
62+
display: flex;
63+
flex: 1;
64+
width: 100%;
65+
min-height: 0;
66+
}
67+
68+
.mx_Stickers_loading {
69+
display: flex;
70+
flex: 1;
71+
min-height: 0;
72+
align-items: center;
73+
justify-content: center;
74+
}

apps/web/src/components/structures/RightPanel.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { Action } from "../../dispatcher/actions";
3434
import { type XOR } from "../../@types/common";
3535
import ExtensionsCard from "../views/right_panel/ExtensionsCard";
3636
import MemberListView from "../views/rooms/MemberList/MemberListView";
37+
import StickerpickerCard from "../views/right_panel/StickerpickerCard";
3738

3839
interface BaseProps {
3940
overwriteCard?: IRightPanelCard; // used to display a custom card and ignoring the RightPanelStore (used for UserView)
@@ -277,6 +278,11 @@ export default class RightPanel extends React.Component<Props, IState> {
277278
card = <WidgetCard room={this.props.room} widgetId={cardState.widgetId} onClose={this.onClose} />;
278279
}
279280
break;
281+
case RightPanelPhases.StickerPicker:
282+
if (!!this.props.room) {
283+
card = <StickerpickerCard onClose={this.onClose} />;
284+
}
285+
break;
280286
}
281287

282288
return (

apps/web/src/components/views/context_menus/WidgetContextMenu.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ interface IProps extends Omit<ComponentProps<typeof IconizedContextMenu>, "child
3737
showUnpin?: boolean;
3838
// override delete handler
3939
onDeleteClick?(this: void): void;
40+
// override attach to sidebar handler
41+
onAttachToSidebarClick?(this: void): void;
4042
// override edit handler
4143
onEditClick?(this: void): void;
4244
}
@@ -89,6 +91,7 @@ export const WidgetContextMenu: React.FC<IProps> = ({
8991
app,
9092
userWidget,
9193
onDeleteClick,
94+
onAttachToSidebarClick,
9295
onEditClick,
9396
showUnpin,
9497
...props
@@ -190,6 +193,18 @@ export const WidgetContextMenu: React.FC<IProps> = ({
190193
);
191194
}
192195

196+
let attachToSidebarButton: JSX.Element | undefined;
197+
if (onAttachToSidebarClick) {
198+
const onClick = (): void => {
199+
onAttachToSidebarClick();
200+
onFinished();
201+
};
202+
203+
attachToSidebarButton = (
204+
<IconizedContextMenuOption onClick={onClick} label={_t("widget|context_menu|attach_to_sidebar")} />
205+
);
206+
}
207+
193208
let revokeButton: JSX.Element | undefined;
194209
if (showRevokeButton(cli, roomId, app, userWidget)) {
195210
const opts: ApprovalOpts = { approved: undefined };
@@ -245,6 +260,7 @@ export const WidgetContextMenu: React.FC<IProps> = ({
245260
{streamAudioStreamButton}
246261
{editButton}
247262
{revokeButton}
263+
{attachToSidebarButton}
248264
{deleteButton}
249265
{snapshotButton}
250266
{moveLeftButton}

apps/web/src/components/views/elements/AppTile.tsx

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import Spinner from "./Spinner";
4040
import dis from "../../../dispatcher/dispatcher";
4141
import ActiveWidgetStore from "../../../stores/ActiveWidgetStore";
4242
import SettingsStore from "../../../settings/SettingsStore";
43-
import { ContextMenuButton } from "../../structures/ContextMenu";
43+
import { ContextMenuButton, toLeftOrRightOf } from "../../structures/ContextMenu";
4444
import PersistedElement, { getPersistKey } from "./PersistedElement";
4545
import { WidgetType } from "../../../widgets/WidgetType";
4646
import { ElementWidget, WidgetMessaging, WidgetMessagingEvent } from "../../../stores/widgets/WidgetMessaging";
@@ -63,7 +63,7 @@ import { toWidgetDescriptor } from "../../../modules/WidgetLifecycleApi";
6363
import { parseUrl } from "../../../utils/UrlUtils";
6464
import RightPanelStore from "../../../stores/right-panel/RightPanelStore.ts";
6565
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases.ts";
66-
import { WidgetContextMenu } from "../../../viewmodels/right-panel/WidgetContextMenuViewModel.tsx";
66+
import { WidgetContextMenu } from "../context_menus/WidgetContextMenu";
6767

6868
// Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin
6969
// because that would allow the iframe to programmatically remove the sandbox attribute, but
@@ -100,6 +100,8 @@ interface IProps {
100100
onEditClick?: () => void;
101101
// Optional onDeleteClickHandler (overrides default behaviour)
102102
onDeleteClick?: () => void;
103+
// Optional onAttachToSidebarClickHandler
104+
onAttachToSidebarClick?: () => void;
103105
// Optionally hide the tile title
104106
showTitle?: boolean;
105107
// Optionally handle minimise button pointer events (default false)
@@ -870,26 +872,27 @@ export default class AppTile extends React.Component<IProps, IState> {
870872
</AccessibleButton>
871873
)}
872874
<I18nContext.Provider value={window.mxModuleApi.i18n}>
873-
<WidgetContextMenu
874-
trigger={
875-
<ContextMenuButton
876-
className="mx_AppTileMenuBar_widgets_button"
877-
label={_t("common|options")}
878-
isExpanded={this.state.menuDisplayed}
879-
ref={this.contextMenuButton}
880-
onClick={this.onContextMenuClick}
881-
>
882-
<OverflowHorizontalIcon className="mx_Icon mx_Icon_12" />
883-
</ContextMenuButton>
884-
}
885-
app={this.props.app}
886-
onFinished={this.closeContextMenu}
887-
showUnpin={!this.props.userWidget}
888-
userWidget={this.props.userWidget}
889-
onEditClick={this.props.onEditClick}
890-
onDeleteClick={this.props.onDeleteClick}
891-
menuDisplayed={this.state.menuDisplayed}
892-
/>
875+
<ContextMenuButton
876+
className="mx_AppTileMenuBar_widgets_button"
877+
label={_t("common|options")}
878+
isExpanded={this.state.menuDisplayed}
879+
ref={this.contextMenuButton}
880+
onClick={this.onContextMenuClick}
881+
>
882+
<OverflowHorizontalIcon className="mx_Icon mx_Icon_12" />
883+
</ContextMenuButton>
884+
{this.state.menuDisplayed && this.contextMenuButton.current && (
885+
<WidgetContextMenu
886+
app={this.props.app}
887+
onFinished={this.closeContextMenu}
888+
showUnpin={!this.props.userWidget}
889+
userWidget={this.props.userWidget}
890+
onEditClick={this.props.onEditClick}
891+
onDeleteClick={this.props.onDeleteClick}
892+
onAttachToSidebarClick={this.props.onAttachToSidebarClick}
893+
{...toLeftOrRightOf(this.contextMenuButton.current.getBoundingClientRect())}
894+
/>
895+
)}
893896
</I18nContext.Provider>
894897
</span>
895898
</div>

apps/web/src/components/views/right_panel/ExtensionsCard.tsx

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
import BaseCard from "./BaseCard";
2121
import WidgetUtils, { useWidgets } from "../../../utils/WidgetUtils";
2222
import { _t } from "../../../languageHandler";
23-
import { useContextMenu } from "../../structures/ContextMenu";
23+
import { toLeftOrRightOf, useContextMenu } from "../../structures/ContextMenu";
2424
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
2525
import { type IApp } from "../../../stores/WidgetStore";
2626
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
@@ -31,7 +31,7 @@ import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
3131
import EmptyState from "./EmptyState";
3232
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents.ts";
3333
import { UIComponent } from "../../../settings/UIFeature.ts";
34-
import { WidgetContextMenu } from "../../../viewmodels/right-panel/WidgetContextMenuViewModel.tsx";
34+
import { WidgetContextMenu } from "../context_menus/WidgetContextMenu";
3535

3636
interface Props {
3737
room: Room;
@@ -107,21 +107,23 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
107107
</AccessibleButton>
108108

109109
{canModifyWidget && (
110-
<WidgetContextMenu
111-
app={app}
112-
onFinished={closeMenu}
113-
menuDisplayed={menuDisplayed}
114-
trigger={
115-
<AccessibleButton
116-
ref={handle}
117-
className="mx_ExtensionsCard_app_options"
118-
onClick={openMenu}
119-
title={_t("common|options")}
120-
>
121-
<OverflowHorizontalIcon />
122-
</AccessibleButton>
123-
}
124-
/>
110+
<>
111+
<AccessibleButton
112+
ref={handle}
113+
className="mx_ExtensionsCard_app_options"
114+
onClick={openMenu}
115+
title={_t("common|options")}
116+
>
117+
<OverflowHorizontalIcon />
118+
</AccessibleButton>
119+
{menuDisplayed && handle.current && (
120+
<WidgetContextMenu
121+
app={app}
122+
onFinished={closeMenu}
123+
{...toLeftOrRightOf(handle.current.getBoundingClientRect())}
124+
/>
125+
)}
126+
</>
125127
)}
126128

127129
<AccessibleButton
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
Copyright 2026 New Vector Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import React, { useContext } from "react";
9+
import { IconButton } from "@vector-im/compound-web";
10+
import { SidebarIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
11+
12+
import BaseCard from "./BaseCard";
13+
import { _t } from "../../../languageHandler";
14+
import Stickerpicker from "../rooms/Stickerpicker";
15+
import RoomContext from "../../../contexts/RoomContext";
16+
import dis from "../../../dispatcher/dispatcher";
17+
import { setStickerpickerAttachedToSidebar } from "../rooms/StickerpickerSidebarStore";
18+
import Heading from "../typography/Heading";
19+
20+
interface IProps {
21+
onClose(this: void): void;
22+
}
23+
24+
const StickerpickerCard: React.FC<IProps> = ({ onClose }) => {
25+
const roomContext = useContext(RoomContext);
26+
const room = roomContext.room;
27+
if (!room) return null;
28+
29+
const onDetachClick = (): void => {
30+
setStickerpickerAttachedToSidebar(false);
31+
dis.dispatch({
32+
action: "stickerpicker_detach_from_sidebar",
33+
roomId: room.roomId,
34+
});
35+
onClose();
36+
};
37+
38+
const header = (
39+
<div className="mx_BaseCard_header_title">
40+
<Heading size="4" className="mx_BaseCard_header_title_heading" as="h1">
41+
{_t("common|sticker")}
42+
</Heading>
43+
<IconButton
44+
size="28px"
45+
onClick={onDetachClick}
46+
tooltip={_t("stickers|detach_from_sidebar")}
47+
kind="secondary"
48+
>
49+
<SidebarIcon />
50+
</IconButton>
51+
</div>
52+
);
53+
54+
return (
55+
<BaseCard header={header} className="mx_WidgetCard" onClose={onClose} withoutScrollContainer>
56+
<Stickerpicker
57+
room={room}
58+
threadId={roomContext.threadId ?? null}
59+
isStickerPickerOpen={true}
60+
setStickerPickerOpen={() => onClose()}
61+
displayMode="sidebar"
62+
/>
63+
</BaseCard>
64+
);
65+
};
66+
67+
export default StickerpickerCard;

apps/web/src/components/views/right_panel/WidgetCard.tsx

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ import BaseCard from "./BaseCard";
1414
import WidgetUtils, { useWidgets } from "../../../utils/WidgetUtils";
1515
import AppTile from "../elements/AppTile";
1616
import { _t } from "../../../languageHandler";
17-
import { ContextMenuButton, useContextMenu } from "../../structures/ContextMenu";
17+
import { ContextMenuButton, toLeftOrRightOf, useContextMenu } from "../../structures/ContextMenu";
1818
import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
1919
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
2020
import Heading from "../typography/Heading";
21-
import { WidgetContextMenu } from "../../../viewmodels/right-panel/WidgetContextMenuViewModel";
21+
import { WidgetContextMenu } from "../context_menus/WidgetContextMenu";
2222

2323
interface IProps {
2424
room: Room;
@@ -46,20 +46,22 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
4646
if (!app || !isRight) return null;
4747

4848
const contextMenu: JSX.Element = (
49-
<WidgetContextMenu
50-
trigger={
51-
<ContextMenuButton
52-
className="mx_BaseCard_header_title_button--option"
53-
ref={handle}
54-
onClick={openMenu}
55-
isExpanded={menuDisplayed}
56-
label={_t("common|options")}
49+
<>
50+
<ContextMenuButton
51+
className="mx_BaseCard_header_title_button--option"
52+
ref={handle}
53+
onClick={openMenu}
54+
isExpanded={menuDisplayed}
55+
label={_t("common|options")}
56+
/>
57+
{menuDisplayed && handle.current && (
58+
<WidgetContextMenu
59+
onFinished={closeMenu}
60+
app={app}
61+
{...toLeftOrRightOf(handle.current.getBoundingClientRect())}
5762
/>
58-
}
59-
onFinished={closeMenu}
60-
app={app}
61-
menuDisplayed={menuDisplayed}
62-
/>
63+
)}
64+
</>
6365
);
6466

6567
const header = (

0 commit comments

Comments
 (0)