Extract Extensions into their own right panel tab (#12844)
* Extract useIsVideoRoom hook Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Move useWidgets hook to WidgetUtils Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Extract Extensions into their own right panel tab Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Remove unused components & classes Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update screenshots Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
fae5bf1612
commit
b55653ddf0
25 changed files with 820 additions and 475 deletions
|
@ -19,8 +19,6 @@ import type { ElementAppPage } from "../../pages/ElementAppPage";
|
||||||
export async function openIntegrationManager(app: ElementAppPage) {
|
export async function openIntegrationManager(app: ElementAppPage) {
|
||||||
const { page } = app;
|
const { page } = app;
|
||||||
await app.toggleRoomInfoPanel();
|
await app.toggleRoomInfoPanel();
|
||||||
await page
|
await page.getByRole("tab", { name: "Extensions" }).click();
|
||||||
.locator(".mx_RoomSummaryCard_appsGroup")
|
await page.getByRole("button", { name: "Add extensions" }).click();
|
||||||
.getByRole("button", { name: "Add widgets, bridges & bots" })
|
|
||||||
.click();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,8 @@ test.describe("RightPanel", () => {
|
||||||
test("should handle clicking add widgets", async ({ page, app }) => {
|
test("should handle clicking add widgets", async ({ page, app }) => {
|
||||||
await viewRoomSummaryByName(page, app, ROOM_NAME);
|
await viewRoomSummaryByName(page, app, ROOM_NAME);
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Add widgets, bridges & bots" }).click();
|
await page.getByRole("tab", { name: "Extensions" }).click();
|
||||||
|
await page.getByRole("button", { name: "Add extensions" }).click();
|
||||||
await expect(page.locator(".mx_IntegrationManager")).toBeVisible();
|
await expect(page.locator(".mx_IntegrationManager")).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |
Binary file not shown.
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
@ -260,6 +260,7 @@
|
||||||
@import "./views/right_panel/_BaseCard.pcss";
|
@import "./views/right_panel/_BaseCard.pcss";
|
||||||
@import "./views/right_panel/_EmptyState.pcss";
|
@import "./views/right_panel/_EmptyState.pcss";
|
||||||
@import "./views/right_panel/_EncryptionInfo.pcss";
|
@import "./views/right_panel/_EncryptionInfo.pcss";
|
||||||
|
@import "./views/right_panel/_ExtensionsCard.pcss";
|
||||||
@import "./views/right_panel/_PinnedMessagesCard.pcss";
|
@import "./views/right_panel/_PinnedMessagesCard.pcss";
|
||||||
@import "./views/right_panel/_RightPanelTabs.pcss";
|
@import "./views/right_panel/_RightPanelTabs.pcss";
|
||||||
@import "./views/right_panel/_RoomSummaryCard.pcss";
|
@import "./views/right_panel/_RoomSummaryCard.pcss";
|
||||||
|
|
|
@ -98,50 +98,6 @@ limitations under the License.
|
||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_BaseCard_Group {
|
|
||||||
margin: $spacing-20 0 $spacing-16;
|
|
||||||
|
|
||||||
& > * {
|
|
||||||
margin-left: $spacing-12;
|
|
||||||
margin-right: $spacing-12;
|
|
||||||
}
|
|
||||||
|
|
||||||
> h2 {
|
|
||||||
color: $tertiary-content;
|
|
||||||
font: var(--cpd-font-body-sm-medium);
|
|
||||||
margin: $spacing-12;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_BaseCard_Button {
|
|
||||||
padding: 10px;
|
|
||||||
padding-inline-start: $spacing-12;
|
|
||||||
margin: 0;
|
|
||||||
position: relative;
|
|
||||||
font: var(--cpd-font-heading-sm-medium);
|
|
||||||
height: 20px;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
.mx_BaseCard_Button_sublabel {
|
|
||||||
color: $tertiary-content;
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: rgba(141, 151, 165, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_AccessibleButton_disabled {
|
|
||||||
padding-right: $spacing-12;
|
|
||||||
&::after {
|
|
||||||
content: unset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_BaseCard_footer {
|
.mx_BaseCard_footer {
|
||||||
padding-top: $spacing-4;
|
padding-top: $spacing-4;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
145
res/css/views/right_panel/_ExtensionsCard.pcss
Normal file
145
res/css/views/right_panel/_ExtensionsCard.pcss
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_ExtensionsCard {
|
||||||
|
--cpd-separator-inset: var(--cpd-space-4x);
|
||||||
|
--cpd-separator-spacing: var(--cpd-space-4x);
|
||||||
|
|
||||||
|
.mx_BaseCard_header {
|
||||||
|
/* Hide the line between the header and the body of the card */
|
||||||
|
border-block-end: none;
|
||||||
|
|
||||||
|
/* Styling for the "Add extensions" button */
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AutoHideScrollbar {
|
||||||
|
padding: 0 var(--cpd-space-4x);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ExtensionsCard_container {
|
||||||
|
text-align: center;
|
||||||
|
margin: $spacing-20 var(--cpd-space-4x) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ExtensionsCard_Button {
|
||||||
|
/* this button is special so we have to override some of the original styling */
|
||||||
|
/* as we will be applying it in its children */
|
||||||
|
padding: 0;
|
||||||
|
height: auto;
|
||||||
|
color: $tertiary-content;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.mx_WidgetAvatar {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ExtensionsCard_icon_app {
|
||||||
|
padding: var(--cpd-space-2x) var(--cpd-space-12x) var(--cpd-space-2x) var(--cpd-space-3x);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 var(--cpd-space-3x);
|
||||||
|
color: $primary-content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ExtensionsCard_app_pinToggle,
|
||||||
|
.mx_ExtensionsCard_app_options {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
height: 100%; /* to give bigger interactive zone */
|
||||||
|
width: 24px;
|
||||||
|
padding: var(--cpd-space-3x) var(--cpd-space-1x);
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-width: 24px; /* prevent flexbox crushing */
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
top: var(--cpd-space-2x); /* equal to padding-top of parent */
|
||||||
|
left: 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
background-color: rgba(141, 151, 165, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: 16px;
|
||||||
|
background-color: $icon-button-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ExtensionsCard_app_pinToggle {
|
||||||
|
right: 8px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
mask-image: url("$(res)/img/element-icons/room/pin-upright.svg");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ExtensionsCard_app_options {
|
||||||
|
right: 32px; /* 24 + 8 */
|
||||||
|
&::before {
|
||||||
|
mask-image: url("$(res)/img/element-icons/room/ellipsis.svg");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_ExtensionsCard_Button_pinned {
|
||||||
|
&::after {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ExtensionsCard_app_pinToggle::before {
|
||||||
|
background-color: $accent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
top: var(--cpd-space-2x); /* re-align based on the height change */
|
||||||
|
pointer-events: none; /* pass through to the real button */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Set layout for everyone button */
|
||||||
|
a[data-kind="primary"] {
|
||||||
|
margin-top: var(--cpd-space-10x);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EmptyState::before {
|
||||||
|
/* Overlap the Add extensions button */
|
||||||
|
top: -76px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,24 +33,6 @@ limitations under the License.
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomSummaryCard_aboutGroup {
|
|
||||||
.mx_RoomSummaryCard_Button {
|
|
||||||
padding-left: 44px;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 8px;
|
|
||||||
left: 10px;
|
|
||||||
height: 24px;
|
|
||||||
width: 24px;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-position: center;
|
|
||||||
background-color: $icon-button-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomSummaryCard_topic {
|
.mx_RoomSummaryCard_topic {
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
color: var(--cpd-color-text-secondary);
|
color: var(--cpd-color-text-secondary);
|
||||||
|
@ -99,131 +81,6 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomSummaryCard_appsGroup {
|
|
||||||
.mx_RoomSummaryCard_Button {
|
|
||||||
/* this button is special so we have to override some of the original styling */
|
|
||||||
/* as we will be applying it in its children */
|
|
||||||
padding: 0;
|
|
||||||
height: auto;
|
|
||||||
color: $tertiary-content;
|
|
||||||
|
|
||||||
.mx_RoomSummaryCard_icon_app {
|
|
||||||
padding: 10px 48px 10px 12px; /* based on typical mx_RoomSummaryCard_Button padding */
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
span {
|
|
||||||
/* Center aligned and Spacing matched with the About section above the Widgets section */
|
|
||||||
margin-right: 10px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
color: $primary-content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomSummaryCard_app_pinToggle,
|
|
||||||
.mx_RoomSummaryCard_app_maximiseToggle,
|
|
||||||
.mx_RoomSummaryCard_app_options {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
height: 100%; /* to give bigger interactive zone */
|
|
||||||
width: 24px;
|
|
||||||
padding: 12px 4px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
min-width: 24px; /* prevent flexbox crushing */
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
&::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
height: 24px;
|
|
||||||
width: 24px;
|
|
||||||
top: 8px; /* equal to padding-top of parent */
|
|
||||||
left: 0;
|
|
||||||
border-radius: 12px;
|
|
||||||
background-color: rgba(141, 151, 165, 0.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
height: 16px;
|
|
||||||
width: 16px;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-position: center;
|
|
||||||
mask-size: 16px;
|
|
||||||
background-color: $icon-button-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomSummaryCard_app_pinToggle {
|
|
||||||
right: 8px;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
mask-image: url("$(res)/img/element-icons/room/pin-upright.svg");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.mx_RoomSummaryCard_app_maximiseToggle {
|
|
||||||
right: 32px; /* 24 + 8 */
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
mask-size: 14px;
|
|
||||||
mask-image: url("$(res)/img/element-icons/maximise-expand.svg");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomSummaryCard_app_options {
|
|
||||||
right: 56px; /* 2*24 + 8 */
|
|
||||||
display: none;
|
|
||||||
&::before {
|
|
||||||
mask-image: url("$(res)/img/element-icons/room/ellipsis.svg");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_RoomSummaryCard_Button_pinned {
|
|
||||||
&::after {
|
|
||||||
opacity: 0.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomSummaryCard_app_pinToggle::before {
|
|
||||||
background-color: $accent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_RoomSummaryCard_Button_maximised {
|
|
||||||
&::after {
|
|
||||||
opacity: 0.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomSummaryCard_app_maximiseToggle::before {
|
|
||||||
background-color: $accent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.mx_RoomSummaryCard_icon_app {
|
|
||||||
padding-right: 72px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomSummaryCard_app_options {
|
|
||||||
display: unset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
top: 8px; /* re-align based on the height change */
|
|
||||||
pointer-events: none; /* pass through to the real button */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AccessibleButton_kind_link {
|
.mx_AccessibleButton_kind_link {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
|
|
@ -64,14 +64,6 @@ $accent-1400: var(--cpd-color-green-1400);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Add padding, so the outline is not chopped off on the left */
|
|
||||||
.mx_BaseCard {
|
|
||||||
padding-left: 4px !important; /* Remove 4 to allow 4 in mx_BaseCard_Group */
|
|
||||||
}
|
|
||||||
.mx_BaseCard_Group {
|
|
||||||
padding-left: 4px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_BasicMessageComposer .mx_BasicMessageComposer_inputEmpty > :first-child::before {
|
.mx_BasicMessageComposer .mx_BasicMessageComposer_inputEmpty > :first-child::before {
|
||||||
color: $secondary-content;
|
color: $secondary-content;
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
|
|
|
@ -43,6 +43,7 @@ import { IRightPanelCard, IRightPanelCardState } from "../../stores/right-panel/
|
||||||
import { Action } from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
import { XOR } from "../../@types/common";
|
import { XOR } from "../../@types/common";
|
||||||
import { RightPanelTabs } from "../views/right_panel/RightPanelTabs";
|
import { RightPanelTabs } from "../views/right_panel/RightPanelTabs";
|
||||||
|
import ExtensionsCard from "../views/right_panel/ExtensionsCard";
|
||||||
|
|
||||||
interface BaseProps {
|
interface BaseProps {
|
||||||
overwriteCard?: IRightPanelCard; // used to display a custom card and ignoring the RightPanelStore (used for UserView)
|
overwriteCard?: IRightPanelCard; // used to display a custom card and ignoring the RightPanelStore (used for UserView)
|
||||||
|
@ -306,6 +307,12 @@ export default class RightPanel extends React.Component<Props, IState> {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case RightPanelPhases.Extensions:
|
||||||
|
if (!!this.props.room) {
|
||||||
|
card = <ExtensionsCard room={this.props.room} onClose={this.onClose} />;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case RightPanelPhases.Widget:
|
case RightPanelPhases.Widget:
|
||||||
if (!!this.props.room && !!cardState?.widgetId) {
|
if (!!this.props.room && !!cardState?.widgetId) {
|
||||||
card = <WidgetCard room={this.props.room} widgetId={cardState.widgetId} onClose={this.onClose} />;
|
card = <WidgetCard room={this.props.room} widgetId={cardState.widgetId} onClose={this.onClose} />;
|
||||||
|
@ -315,7 +322,7 @@ export default class RightPanel extends React.Component<Props, IState> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="mx_RightPanel" id="mx_RightPanel">
|
<aside className="mx_RightPanel" id="mx_RightPanel">
|
||||||
{phase && <RightPanelTabs phase={phase} />}
|
{phase && <RightPanelTabs room={this.props.room} phase={phase} />}
|
||||||
{card}
|
{card}
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
|
|
|
@ -41,26 +41,11 @@ interface IProps {
|
||||||
onKeyDown?(ev: KeyboardEvent): void;
|
onKeyDown?(ev: KeyboardEvent): void;
|
||||||
cardState?: any;
|
cardState?: any;
|
||||||
ref?: Ref<HTMLDivElement>;
|
ref?: Ref<HTMLDivElement>;
|
||||||
// Ref for the 'close' button the the card
|
// Ref for the 'close' button the card
|
||||||
closeButtonRef?: Ref<HTMLButtonElement>;
|
closeButtonRef?: Ref<HTMLButtonElement>;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IGroupProps {
|
|
||||||
className?: string;
|
|
||||||
title: string;
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Group: React.FC<IGroupProps> = ({ className, title, children }) => {
|
|
||||||
return (
|
|
||||||
<div className={classNames("mx_BaseCard_Group", className)}>
|
|
||||||
<h2>{title}</h2>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const BaseCard: React.FC<IProps> = forwardRef<HTMLDivElement, IProps>(
|
const BaseCard: React.FC<IProps> = forwardRef<HTMLDivElement, IProps>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
|
214
src/components/views/right_panel/ExtensionsCard.tsx
Normal file
214
src/components/views/right_panel/ExtensionsCard.tsx
Normal file
|
@ -0,0 +1,214 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 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 React, { useEffect, useMemo, useState } from "react";
|
||||||
|
import { Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { Button, Link, Separator, Text } from "@vector-im/compound-web";
|
||||||
|
import { Icon as PlusIcon } from "@vector-im/compound-design-tokens/icons/plus.svg";
|
||||||
|
import { Icon as ExtensionsIcon } from "@vector-im/compound-design-tokens/icons/extensions.svg";
|
||||||
|
|
||||||
|
import BaseCard from "./BaseCard";
|
||||||
|
import WidgetUtils, { useWidgets } from "../../../utils/WidgetUtils";
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
|
||||||
|
import { WidgetContextMenu } from "../context_menus/WidgetContextMenu";
|
||||||
|
import UIStore from "../../../stores/UIStore";
|
||||||
|
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||||
|
import { IApp } from "../../../stores/WidgetStore";
|
||||||
|
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||||
|
import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||||
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
|
import WidgetAvatar from "../avatars/WidgetAvatar";
|
||||||
|
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
|
||||||
|
import EmptyState from "./EmptyState";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
room: Room;
|
||||||
|
onClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IAppRowProps {
|
||||||
|
app: IApp;
|
||||||
|
room: Room;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
|
||||||
|
const name = WidgetUtils.getWidgetName(app);
|
||||||
|
const [canModifyWidget, setCanModifyWidget] = useState<boolean>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCanModifyWidget(WidgetUtils.canUserModifyWidgets(room.client, room.roomId));
|
||||||
|
}, [room.client, room.roomId]);
|
||||||
|
|
||||||
|
const onOpenWidgetClick = (): void => {
|
||||||
|
RightPanelStore.instance.pushCard({
|
||||||
|
phase: RightPanelPhases.Widget,
|
||||||
|
state: { widgetId: app.id },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPinned = WidgetLayoutStore.instance.isInContainer(room, app, Container.Top);
|
||||||
|
const togglePin = isPinned
|
||||||
|
? () => {
|
||||||
|
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Right);
|
||||||
|
}
|
||||||
|
: () => {
|
||||||
|
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Top);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
|
||||||
|
let contextMenu;
|
||||||
|
if (menuDisplayed) {
|
||||||
|
const rect = handle.current?.getBoundingClientRect();
|
||||||
|
const rightMargin = rect?.right ?? 0;
|
||||||
|
const topMargin = rect?.top ?? 0;
|
||||||
|
contextMenu = (
|
||||||
|
<WidgetContextMenu
|
||||||
|
chevronFace={ChevronFace.None}
|
||||||
|
right={UIStore.instance.windowWidth - rightMargin}
|
||||||
|
bottom={UIStore.instance.windowHeight - topMargin}
|
||||||
|
onFinished={closeMenu}
|
||||||
|
app={app}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cannotPin = !isPinned && !WidgetLayoutStore.instance.canAddToContainer(room, Container.Top);
|
||||||
|
|
||||||
|
let pinTitle: string;
|
||||||
|
if (cannotPin) {
|
||||||
|
pinTitle = _t("right_panel|pinned_messages|limits", { count: MAX_PINNED });
|
||||||
|
} else {
|
||||||
|
pinTitle = isPinned ? _t("action|unpin") : _t("action|pin");
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMaximised = WidgetLayoutStore.instance.isInContainer(room, app, Container.Center);
|
||||||
|
|
||||||
|
let openTitle = "";
|
||||||
|
if (isPinned) {
|
||||||
|
openTitle = _t("widget|unpin_to_view_right_panel");
|
||||||
|
} else if (isMaximised) {
|
||||||
|
openTitle = _t("widget|close_to_view_right_panel");
|
||||||
|
}
|
||||||
|
|
||||||
|
const classes = classNames("mx_BaseCard_Button mx_ExtensionsCard_Button", {
|
||||||
|
mx_ExtensionsCard_Button_pinned: isPinned,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes} ref={handle}>
|
||||||
|
<AccessibleButton
|
||||||
|
className="mx_ExtensionsCard_icon_app"
|
||||||
|
onClick={onOpenWidgetClick}
|
||||||
|
// only show a tooltip if the widget is pinned
|
||||||
|
title={!(isPinned || isMaximised) ? undefined : openTitle}
|
||||||
|
disabled={isPinned || isMaximised}
|
||||||
|
>
|
||||||
|
<WidgetAvatar app={app} size="24px" />
|
||||||
|
<Text size="md" weight="medium" className="mx_lineClamp">
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
</AccessibleButton>
|
||||||
|
|
||||||
|
{canModifyWidget && (
|
||||||
|
<ContextMenuTooltipButton
|
||||||
|
className="mx_ExtensionsCard_app_options"
|
||||||
|
isExpanded={menuDisplayed}
|
||||||
|
onClick={openMenu}
|
||||||
|
title={_t("common|options")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<AccessibleButton
|
||||||
|
className="mx_ExtensionsCard_app_pinToggle"
|
||||||
|
onClick={togglePin}
|
||||||
|
title={pinTitle}
|
||||||
|
disabled={cannotPin}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{contextMenu}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A right panel card displaying a list of widgets in the room and allowing the user to manage them.
|
||||||
|
* @param room the room to manage widgets for
|
||||||
|
* @param onClose callback when the card is closed
|
||||||
|
*/
|
||||||
|
const ExtensionsCard: React.FC<Props> = ({ room, onClose }) => {
|
||||||
|
const apps = useWidgets(room);
|
||||||
|
// Filter out virtual widgets
|
||||||
|
const realApps = useMemo(() => apps.filter((app) => app.eventId !== undefined), [apps]);
|
||||||
|
|
||||||
|
const onManageIntegrations = (): void => {
|
||||||
|
const managers = IntegrationManagers.sharedInstance();
|
||||||
|
if (!managers.hasManager()) {
|
||||||
|
managers.openNoManagerDialog();
|
||||||
|
} else {
|
||||||
|
// noinspection JSIgnoredPromiseFromCall
|
||||||
|
managers.getPrimaryManager()?.open(room);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// The button is in the header to keep it outside the scrollable region
|
||||||
|
const header = (
|
||||||
|
<Button size="sm" onClick={onManageIntegrations} kind="secondary" Icon={PlusIcon}>
|
||||||
|
{_t("right_panel|add_integrations")}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
let body: JSX.Element;
|
||||||
|
if (realApps.length < 1) {
|
||||||
|
body = (
|
||||||
|
<EmptyState
|
||||||
|
Icon={ExtensionsIcon}
|
||||||
|
title={_t("right_panel|extensions_empty_title")}
|
||||||
|
description={_t("right_panel|extensions_empty_description", {
|
||||||
|
addIntegrations: _t("right_panel|add_integrations"),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let copyLayoutBtn: JSX.Element | null = null;
|
||||||
|
if (WidgetLayoutStore.instance.canCopyLayoutToRoom(room)) {
|
||||||
|
copyLayoutBtn = (
|
||||||
|
<Link onClick={() => WidgetLayoutStore.instance.copyLayoutToRoom(room)}>
|
||||||
|
{_t("widget|set_room_layout")}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
body = (
|
||||||
|
<>
|
||||||
|
<Separator />
|
||||||
|
{realApps.map((app) => (
|
||||||
|
<AppRow key={app.id} app={app} room={room} />
|
||||||
|
))}
|
||||||
|
{copyLayoutBtn}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseCard header={header} className="mx_ExtensionsCard" onClose={onClose} hideHeaderButtons>
|
||||||
|
{body}
|
||||||
|
</BaseCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExtensionsCard;
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React, { useRef } from "react";
|
import React, { useRef } from "react";
|
||||||
import { NavBar, NavItem } from "@vector-im/compound-web";
|
import { NavBar, NavItem } from "@vector-im/compound-web";
|
||||||
|
import { Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||||
|
@ -24,17 +25,27 @@ import PosthogTrackers from "../../../PosthogTrackers";
|
||||||
import { useDispatcher } from "../../../hooks/useDispatcher";
|
import { useDispatcher } from "../../../hooks/useDispatcher";
|
||||||
import dispatcher from "../../../dispatcher/dispatcher";
|
import dispatcher from "../../../dispatcher/dispatcher";
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
import { UIComponent, UIFeature } from "../../../settings/UIFeature";
|
||||||
|
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||||
|
import { useIsVideoRoom } from "../../../utils/video-rooms";
|
||||||
|
|
||||||
function shouldShowTabsForPhase(phase?: RightPanelPhases): boolean {
|
function shouldShowTabsForPhase(phase?: RightPanelPhases): boolean {
|
||||||
const tabs = [RightPanelPhases.RoomSummary, RightPanelPhases.RoomMemberList, RightPanelPhases.ThreadPanel];
|
const tabs = [
|
||||||
|
RightPanelPhases.RoomSummary,
|
||||||
|
RightPanelPhases.RoomMemberList,
|
||||||
|
RightPanelPhases.ThreadPanel,
|
||||||
|
RightPanelPhases.Extensions,
|
||||||
|
];
|
||||||
return !!phase && tabs.includes(phase);
|
return !!phase && tabs.includes(phase);
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
room?: Room;
|
||||||
phase: RightPanelPhases;
|
phase: RightPanelPhases;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RightPanelTabs: React.FC<Props> = ({ phase }): JSX.Element | null => {
|
export const RightPanelTabs: React.FC<Props> = ({ phase, room }): JSX.Element | null => {
|
||||||
const threadsTabRef = useRef<HTMLButtonElement | null>(null);
|
const threadsTabRef = useRef<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
useDispatcher(dispatcher, (payload) => {
|
useDispatcher(dispatcher, (payload) => {
|
||||||
|
@ -45,6 +56,8 @@ export const RightPanelTabs: React.FC<Props> = ({ phase }): JSX.Element | null =
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isVideoRoom = useIsVideoRoom(room);
|
||||||
|
|
||||||
if (!shouldShowTabsForPhase(phase)) return null;
|
if (!shouldShowTabsForPhase(phase)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -81,6 +94,20 @@ export const RightPanelTabs: React.FC<Props> = ({ phase }): JSX.Element | null =
|
||||||
>
|
>
|
||||||
{_t("common|threads")}
|
{_t("common|threads")}
|
||||||
</NavItem>
|
</NavItem>
|
||||||
|
{SettingsStore.getValue(UIFeature.Widgets) &&
|
||||||
|
!isVideoRoom &&
|
||||||
|
shouldShowComponent(UIComponent.AddIntegrations) && (
|
||||||
|
<NavItem
|
||||||
|
aria-controls="thread-panel"
|
||||||
|
id="extensions-panel-tab"
|
||||||
|
onClick={() => {
|
||||||
|
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.Extensions }, true);
|
||||||
|
}}
|
||||||
|
active={phase === RightPanelPhases.Extensions}
|
||||||
|
>
|
||||||
|
{_t("common|extensions")}
|
||||||
|
</NavItem>
|
||||||
|
)}
|
||||||
</NavBar>
|
</NavBar>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,16 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {
|
import React, { ChangeEvent, SyntheticEvent, useContext, useEffect, useRef, useState } from "react";
|
||||||
ChangeEvent,
|
|
||||||
SyntheticEvent,
|
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import {
|
import {
|
||||||
MenuItem,
|
MenuItem,
|
||||||
|
@ -55,35 +46,23 @@ import { EventType, JoinRule, Room, RoomStateEvent } from "matrix-js-sdk/src/mat
|
||||||
|
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
|
import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
|
||||||
import BaseCard, { Group } from "./BaseCard";
|
import BaseCard from "./BaseCard";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import RoomAvatar from "../avatars/RoomAvatar";
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
|
||||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||||
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import ShareDialog from "../dialogs/ShareDialog";
|
import ShareDialog from "../dialogs/ShareDialog";
|
||||||
import { useEventEmitter, useEventEmitterState } from "../../../hooks/useEventEmitter";
|
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||||
import WidgetUtils from "../../../utils/WidgetUtils";
|
|
||||||
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
|
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
|
||||||
import WidgetAvatar from "../avatars/WidgetAvatar";
|
|
||||||
import WidgetStore, { IApp } from "../../../stores/WidgetStore";
|
|
||||||
import { E2EStatus } from "../../../utils/ShieldUtils";
|
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||||
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||||
import { UIComponent, UIFeature } from "../../../settings/UIFeature";
|
|
||||||
import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
|
|
||||||
import { WidgetContextMenu } from "../context_menus/WidgetContextMenu";
|
|
||||||
import { useFeatureEnabled } from "../../../hooks/useSettings";
|
import { useFeatureEnabled } from "../../../hooks/useSettings";
|
||||||
import { usePinnedEvents } from "./PinnedMessagesCard";
|
import { usePinnedEvents } from "./PinnedMessagesCard";
|
||||||
import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
|
||||||
import RoomName from "../elements/RoomName";
|
import RoomName from "../elements/RoomName";
|
||||||
import UIStore from "../../../stores/UIStore";
|
|
||||||
import ExportDialog from "../dialogs/ExportDialog";
|
import ExportDialog from "../dialogs/ExportDialog";
|
||||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||||
import PosthogTrackers from "../../../PosthogTrackers";
|
import PosthogTrackers from "../../../PosthogTrackers";
|
||||||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
|
||||||
import { PollHistoryDialog } from "../dialogs/PollHistoryDialog";
|
import { PollHistoryDialog } from "../dialogs/PollHistoryDialog";
|
||||||
import { Flex } from "../../utils/Flex";
|
import { Flex } from "../../utils/Flex";
|
||||||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
|
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
|
||||||
|
@ -111,182 +90,6 @@ interface IProps {
|
||||||
focusRoomSearch?: boolean;
|
focusRoomSearch?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IAppsSectionProps {
|
|
||||||
room: Room;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useWidgets = (room: Room): IApp[] => {
|
|
||||||
const [apps, setApps] = useState<IApp[]>(() => WidgetStore.instance.getApps(room.roomId));
|
|
||||||
|
|
||||||
const updateApps = useCallback(() => {
|
|
||||||
// Copy the array so that we always trigger a re-render, as some updates mutate the array of apps/settings
|
|
||||||
setApps([...WidgetStore.instance.getApps(room.roomId)]);
|
|
||||||
}, [room]);
|
|
||||||
|
|
||||||
useEffect(updateApps, [room, updateApps]);
|
|
||||||
useEventEmitter(WidgetStore.instance, room.roomId, updateApps);
|
|
||||||
useEventEmitter(WidgetLayoutStore.instance, WidgetLayoutStore.emissionForRoom(room), updateApps);
|
|
||||||
|
|
||||||
return apps;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IAppRowProps {
|
|
||||||
app: IApp;
|
|
||||||
room: Room;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
|
|
||||||
const name = WidgetUtils.getWidgetName(app);
|
|
||||||
const dataTitle = WidgetUtils.getWidgetDataTitle(app);
|
|
||||||
const subtitle = dataTitle && " - " + dataTitle;
|
|
||||||
const [canModifyWidget, setCanModifyWidget] = useState<boolean>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setCanModifyWidget(WidgetUtils.canUserModifyWidgets(room.client, room.roomId));
|
|
||||||
}, [room.client, room.roomId]);
|
|
||||||
|
|
||||||
const onOpenWidgetClick = (): void => {
|
|
||||||
RightPanelStore.instance.pushCard({
|
|
||||||
phase: RightPanelPhases.Widget,
|
|
||||||
state: { widgetId: app.id },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const isPinned = WidgetLayoutStore.instance.isInContainer(room, app, Container.Top);
|
|
||||||
const togglePin = isPinned
|
|
||||||
? () => {
|
|
||||||
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Right);
|
|
||||||
}
|
|
||||||
: () => {
|
|
||||||
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Top);
|
|
||||||
};
|
|
||||||
|
|
||||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
|
|
||||||
let contextMenu;
|
|
||||||
if (menuDisplayed) {
|
|
||||||
const rect = handle.current?.getBoundingClientRect();
|
|
||||||
const rightMargin = rect?.right ?? 0;
|
|
||||||
const topMargin = rect?.top ?? 0;
|
|
||||||
contextMenu = (
|
|
||||||
<WidgetContextMenu
|
|
||||||
chevronFace={ChevronFace.None}
|
|
||||||
right={UIStore.instance.windowWidth - rightMargin}
|
|
||||||
bottom={UIStore.instance.windowHeight - topMargin}
|
|
||||||
onFinished={closeMenu}
|
|
||||||
app={app}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const cannotPin = !isPinned && !WidgetLayoutStore.instance.canAddToContainer(room, Container.Top);
|
|
||||||
|
|
||||||
let pinTitle: string;
|
|
||||||
if (cannotPin) {
|
|
||||||
pinTitle = _t("right_panel|pinned_messages|limits", { count: MAX_PINNED });
|
|
||||||
} else {
|
|
||||||
pinTitle = isPinned ? _t("action|unpin") : _t("action|pin");
|
|
||||||
}
|
|
||||||
|
|
||||||
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("action|close") : _t("action|maximise");
|
|
||||||
|
|
||||||
let openTitle = "";
|
|
||||||
if (isPinned) {
|
|
||||||
openTitle = _t("widget|unpin_to_view_right_panel");
|
|
||||||
} else if (isMaximised) {
|
|
||||||
openTitle = _t("widget|close_to_view_right_panel");
|
|
||||||
}
|
|
||||||
|
|
||||||
const classes = classNames("mx_BaseCard_Button mx_RoomSummaryCard_Button", {
|
|
||||||
mx_RoomSummaryCard_Button_pinned: isPinned,
|
|
||||||
mx_RoomSummaryCard_Button_maximised: isMaximised,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classes} ref={handle}>
|
|
||||||
<AccessibleButton
|
|
||||||
className="mx_RoomSummaryCard_icon_app"
|
|
||||||
onClick={onOpenWidgetClick}
|
|
||||||
// only show a tooltip if the widget is pinned
|
|
||||||
title={!(isPinned || isMaximised) ? undefined : openTitle}
|
|
||||||
disabled={isPinned || isMaximised}
|
|
||||||
>
|
|
||||||
<WidgetAvatar app={app} size="20px" />
|
|
||||||
<span>{name}</span>
|
|
||||||
{subtitle}
|
|
||||||
</AccessibleButton>
|
|
||||||
|
|
||||||
{canModifyWidget && (
|
|
||||||
<ContextMenuTooltipButton
|
|
||||||
className="mx_RoomSummaryCard_app_options"
|
|
||||||
isExpanded={menuDisplayed}
|
|
||||||
onClick={openMenu}
|
|
||||||
title={_t("common|options")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<AccessibleButton
|
|
||||||
className="mx_RoomSummaryCard_app_pinToggle"
|
|
||||||
onClick={togglePin}
|
|
||||||
title={pinTitle}
|
|
||||||
disabled={cannotPin}
|
|
||||||
/>
|
|
||||||
<AccessibleButton
|
|
||||||
className="mx_RoomSummaryCard_app_maximiseToggle"
|
|
||||||
onClick={toggleMaximised}
|
|
||||||
title={maximiseTitle}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{contextMenu}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AppsSection: React.FC<IAppsSectionProps> = ({ room }) => {
|
|
||||||
const apps = useWidgets(room);
|
|
||||||
// Filter out virtual widgets
|
|
||||||
const realApps = useMemo(() => apps.filter((app) => app.eventId !== undefined), [apps]);
|
|
||||||
|
|
||||||
const onManageIntegrations = (): void => {
|
|
||||||
const managers = IntegrationManagers.sharedInstance();
|
|
||||||
if (!managers.hasManager()) {
|
|
||||||
managers.openNoManagerDialog();
|
|
||||||
} else {
|
|
||||||
// noinspection JSIgnoredPromiseFromCall
|
|
||||||
managers.getPrimaryManager()?.open(room);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let copyLayoutBtn: JSX.Element | null = null;
|
|
||||||
if (realApps.length > 0 && WidgetLayoutStore.instance.canCopyLayoutToRoom(room)) {
|
|
||||||
copyLayoutBtn = (
|
|
||||||
<AccessibleButton kind="link" onClick={() => WidgetLayoutStore.instance.copyLayoutToRoom(room)}>
|
|
||||||
{_t("widget|set_room_layout")}
|
|
||||||
</AccessibleButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Group className="mx_RoomSummaryCard_appsGroup" title={_t("right_panel|widgets_section")}>
|
|
||||||
{realApps.map((app) => (
|
|
||||||
<AppRow key={app.id} app={app} room={room} />
|
|
||||||
))}
|
|
||||||
{copyLayoutBtn}
|
|
||||||
<AccessibleButton kind="link" onClick={onManageIntegrations}>
|
|
||||||
{realApps.length > 0 ? _t("right_panel|edit_integrations") : _t("right_panel|add_integrations")}
|
|
||||||
</AccessibleButton>
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRoomFilesClick = (): void => {
|
const onRoomFilesClick = (): void => {
|
||||||
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.FilePanel }, true);
|
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.FilePanel }, true);
|
||||||
};
|
};
|
||||||
|
@ -622,10 +425,6 @@ const RoomSummaryCard: React.FC<IProps> = ({
|
||||||
onSelect={onLeaveRoomClick}
|
onSelect={onLeaveRoomClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{SettingsStore.getValue(UIFeature.Widgets) &&
|
|
||||||
!isVideoRoom &&
|
|
||||||
shouldShowComponent(UIComponent.AddIntegrations) && <AppsSection room={room} />}
|
|
||||||
</BaseCard>
|
</BaseCard>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,10 +19,9 @@ import { Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import BaseCard from "./BaseCard";
|
import BaseCard from "./BaseCard";
|
||||||
import WidgetUtils from "../../../utils/WidgetUtils";
|
import WidgetUtils, { useWidgets } from "../../../utils/WidgetUtils";
|
||||||
import AppTile from "../elements/AppTile";
|
import AppTile from "../elements/AppTile";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { useWidgets } from "./RoomSummaryCard";
|
|
||||||
import { ChevronFace, ContextMenuButton, useContextMenu } from "../../structures/ContextMenu";
|
import { ChevronFace, ContextMenuButton, useContextMenu } from "../../structures/ContextMenu";
|
||||||
import { WidgetContextMenu } from "../context_menus/WidgetContextMenu";
|
import { WidgetContextMenu } from "../context_menus/WidgetContextMenu";
|
||||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||||
|
|
|
@ -54,7 +54,7 @@ import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../../LegacyCallHa
|
||||||
import { useFeatureEnabled, useSettingValue } from "../../../hooks/useSettings";
|
import { useFeatureEnabled, useSettingValue } from "../../../hooks/useSettings";
|
||||||
import SdkConfig from "../../../SdkConfig";
|
import SdkConfig from "../../../SdkConfig";
|
||||||
import { useEventEmitterState, useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
|
import { useEventEmitterState, useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||||
import { useWidgets } from "../right_panel/RoomSummaryCard";
|
import { useWidgets } from "../../../utils/WidgetUtils";
|
||||||
import { WidgetType } from "../../../widgets/WidgetType";
|
import { WidgetType } from "../../../widgets/WidgetType";
|
||||||
import { useCall, useLayout } from "../../../hooks/useCall";
|
import { useCall, useLayout } from "../../../hooks/useCall";
|
||||||
import { getJoinedNonFunctionalMembers } from "../../../utils/room/getJoinedNonFunctionalMembers";
|
import { getJoinedNonFunctionalMembers } from "../../../utils/room/getJoinedNonFunctionalMembers";
|
||||||
|
|
|
@ -22,7 +22,7 @@ import { useFeatureEnabled } from "../useSettings";
|
||||||
import SdkConfig from "../../SdkConfig";
|
import SdkConfig from "../../SdkConfig";
|
||||||
import { useEventEmitter, useEventEmitterState } from "../useEventEmitter";
|
import { useEventEmitter, useEventEmitterState } from "../useEventEmitter";
|
||||||
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../LegacyCallHandler";
|
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../LegacyCallHandler";
|
||||||
import { useWidgets } from "../../components/views/right_panel/RoomSummaryCard";
|
import { useWidgets } from "../../utils/WidgetUtils";
|
||||||
import { WidgetType } from "../../widgets/WidgetType";
|
import { WidgetType } from "../../widgets/WidgetType";
|
||||||
import { useCall, useConnectionState, useParticipantCount } from "../useCall";
|
import { useCall, useConnectionState, useParticipantCount } from "../useCall";
|
||||||
import { useRoomMemberCount } from "../useRoomMembers";
|
import { useRoomMemberCount } from "../useRoomMembers";
|
||||||
|
|
|
@ -474,6 +474,7 @@
|
||||||
"encrypted": "Encrypted",
|
"encrypted": "Encrypted",
|
||||||
"encryption_enabled": "Encryption enabled",
|
"encryption_enabled": "Encryption enabled",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
|
"extensions": "Extensions",
|
||||||
"faq": "FAQ",
|
"faq": "FAQ",
|
||||||
"favourites": "Favourites",
|
"favourites": "Favourites",
|
||||||
"feedback": "Feedback",
|
"feedback": "Feedback",
|
||||||
|
@ -1830,10 +1831,11 @@
|
||||||
"restore_failed_error": "Unable to restore backup"
|
"restore_failed_error": "Unable to restore backup"
|
||||||
},
|
},
|
||||||
"right_panel": {
|
"right_panel": {
|
||||||
"add_integrations": "Add widgets, bridges & bots",
|
"add_integrations": "Add extensions",
|
||||||
"add_topic": "Add topic",
|
"add_topic": "Add topic",
|
||||||
"edit_integrations": "Edit widgets, bridges & bots",
|
|
||||||
"export_chat_button": "Export chat",
|
"export_chat_button": "Export chat",
|
||||||
|
"extensions_empty_description": "Select “%(addIntegrations)s” to browse and add extensions to this room",
|
||||||
|
"extensions_empty_title": "Boost productivity with more tools, widgets and bots",
|
||||||
"files_button": "Files",
|
"files_button": "Files",
|
||||||
"info": "Info",
|
"info": "Info",
|
||||||
"pinned_messages": {
|
"pinned_messages": {
|
||||||
|
@ -4067,7 +4069,7 @@
|
||||||
"title": "Allow this widget to verify your identity"
|
"title": "Allow this widget to verify your identity"
|
||||||
},
|
},
|
||||||
"popout": "Popout widget",
|
"popout": "Popout widget",
|
||||||
"set_room_layout": "Set my room layout for everyone",
|
"set_room_layout": "Set layout for everyone",
|
||||||
"shared_data_avatar": "Your profile picture URL",
|
"shared_data_avatar": "Your profile picture URL",
|
||||||
"shared_data_device_id": "Your device ID",
|
"shared_data_device_id": "Your device ID",
|
||||||
"shared_data_lang": "Your language",
|
"shared_data_lang": "Your language",
|
||||||
|
|
|
@ -28,6 +28,7 @@ export enum RightPanelPhases {
|
||||||
Widget = "Widget",
|
Widget = "Widget",
|
||||||
PinnedMessages = "PinnedMessages",
|
PinnedMessages = "PinnedMessages",
|
||||||
Timeline = "Timeline",
|
Timeline = "Timeline",
|
||||||
|
Extensions = "Extensions",
|
||||||
|
|
||||||
Room3pidMemberInfo = "Room3pidMemberInfo",
|
Room3pidMemberInfo = "Room3pidMemberInfo",
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { base32 } from "rfc4648";
|
import { base32 } from "rfc4648";
|
||||||
import { IWidget, IWidgetData } from "matrix-widget-api";
|
import { IWidget, IWidgetData } from "matrix-widget-api";
|
||||||
import { Room, ClientEvent, MatrixClient, RoomStateEvent, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
import { Room, ClientEvent, MatrixClient, RoomStateEvent, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
|
@ -32,8 +33,10 @@ import { WidgetType } from "../widgets/WidgetType";
|
||||||
import { Jitsi } from "../widgets/Jitsi";
|
import { Jitsi } from "../widgets/Jitsi";
|
||||||
import { objectClone } from "./objects";
|
import { objectClone } from "./objects";
|
||||||
import { _t } from "../languageHandler";
|
import { _t } from "../languageHandler";
|
||||||
import { IApp, isAppWidget } from "../stores/WidgetStore";
|
import WidgetStore, { IApp, isAppWidget } from "../stores/WidgetStore";
|
||||||
import { parseUrl } from "./UrlUtils";
|
import { parseUrl } from "./UrlUtils";
|
||||||
|
import { useEventEmitter } from "../hooks/useEventEmitter";
|
||||||
|
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
|
||||||
|
|
||||||
// How long we wait for the state event echo to come back from the server
|
// How long we wait for the state event echo to come back from the server
|
||||||
// before waitFor[Room/User]Widget rejects its promise
|
// before waitFor[Room/User]Widget rejects its promise
|
||||||
|
@ -562,3 +565,22 @@ export default class WidgetUtils {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to get the widgets for a room and update when they change
|
||||||
|
* @param room the room to get widgets for
|
||||||
|
*/
|
||||||
|
export const useWidgets = (room: Room): IApp[] => {
|
||||||
|
const [apps, setApps] = useState<IApp[]>(() => WidgetStore.instance.getApps(room.roomId));
|
||||||
|
|
||||||
|
const updateApps = useCallback(() => {
|
||||||
|
// Copy the array so that we always trigger a re-render, as some updates mutate the array of apps/settings
|
||||||
|
setApps([...WidgetStore.instance.getApps(room.roomId)]);
|
||||||
|
}, [room]);
|
||||||
|
|
||||||
|
useEffect(updateApps, [room, updateApps]);
|
||||||
|
useEventEmitter(WidgetStore.instance, room.roomId, updateApps);
|
||||||
|
useEventEmitter(WidgetLayoutStore.instance, WidgetLayoutStore.emissionForRoom(room), updateApps);
|
||||||
|
|
||||||
|
return apps;
|
||||||
|
};
|
||||||
|
|
159
test/components/views/right_panel/ExtensionsCard-test.tsx
Normal file
159
test/components/views/right_panel/ExtensionsCard-test.tsx
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 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 React from "react";
|
||||||
|
import { mocked, Mocked } from "jest-mock";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { MatrixWidgetType } from "matrix-widget-api";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
|
||||||
|
import ExtensionsCard from "../../../../src/components/views/right_panel/ExtensionsCard";
|
||||||
|
import { stubClient } from "../../../test-utils";
|
||||||
|
import { IApp } from "../../../../src/stores/WidgetStore";
|
||||||
|
import WidgetUtils, { useWidgets } from "../../../../src/utils/WidgetUtils";
|
||||||
|
import { WidgetLayoutStore } from "../../../../src/stores/widgets/WidgetLayoutStore";
|
||||||
|
import { IntegrationManagers } from "../../../../src/integrations/IntegrationManagers";
|
||||||
|
|
||||||
|
jest.mock("../../../../src/utils/WidgetUtils");
|
||||||
|
|
||||||
|
describe("<ExtensionsCard />", () => {
|
||||||
|
let client: Mocked<MatrixClient>;
|
||||||
|
let room: Room;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
client = mocked(stubClient());
|
||||||
|
room = new Room("!room:server", client, client.getSafeUserId());
|
||||||
|
mocked(WidgetUtils.getWidgetName).mockImplementation((app) => app?.name ?? "No Name");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render empty state", () => {
|
||||||
|
mocked(useWidgets).mockReturnValue([]);
|
||||||
|
const { asFragment } = render(<ExtensionsCard room={room} onClose={jest.fn()} />);
|
||||||
|
expect(screen.getByText("Boost productivity with more tools, widgets and bots")).toBeInTheDocument();
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render widgets", async () => {
|
||||||
|
mocked(useWidgets).mockReturnValue([
|
||||||
|
{
|
||||||
|
id: "id",
|
||||||
|
roomId: room.roomId,
|
||||||
|
eventId: "$event1",
|
||||||
|
creatorUserId: client.getSafeUserId(),
|
||||||
|
type: MatrixWidgetType.Custom,
|
||||||
|
name: "Custom Widget",
|
||||||
|
url: "http://url1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "jitsi",
|
||||||
|
roomId: room.roomId,
|
||||||
|
eventId: "$event2",
|
||||||
|
creatorUserId: client.getSafeUserId(),
|
||||||
|
type: MatrixWidgetType.JitsiMeet,
|
||||||
|
name: "Jitsi",
|
||||||
|
url: "http://jitsi",
|
||||||
|
},
|
||||||
|
] satisfies IApp[]);
|
||||||
|
|
||||||
|
const { asFragment } = render(<ExtensionsCard room={room} onClose={jest.fn()} />);
|
||||||
|
expect(screen.getByText("Custom Widget")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Jitsi")).toBeInTheDocument();
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show context menu on widget row", async () => {
|
||||||
|
jest.spyOn(WidgetUtils, "canUserModifyWidgets").mockReturnValue(true);
|
||||||
|
mocked(useWidgets).mockReturnValue([
|
||||||
|
{
|
||||||
|
id: "id",
|
||||||
|
roomId: room.roomId,
|
||||||
|
eventId: "$event1",
|
||||||
|
creatorUserId: client.getSafeUserId(),
|
||||||
|
type: MatrixWidgetType.Custom,
|
||||||
|
name: "Custom Widget",
|
||||||
|
url: "http://url1",
|
||||||
|
},
|
||||||
|
] satisfies IApp[]);
|
||||||
|
|
||||||
|
const { container } = render(<ExtensionsCard room={room} onClose={jest.fn()} />);
|
||||||
|
await userEvent.click(container.querySelector(".mx_ExtensionsCard_app_options")!);
|
||||||
|
expect(document.querySelector(".mx_IconizedContextMenu")).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show set room layout button", async () => {
|
||||||
|
jest.spyOn(WidgetLayoutStore.instance, "canCopyLayoutToRoom").mockReturnValue(true);
|
||||||
|
mocked(useWidgets).mockReturnValue([
|
||||||
|
{
|
||||||
|
id: "id",
|
||||||
|
roomId: room.roomId,
|
||||||
|
eventId: "$event1",
|
||||||
|
creatorUserId: client.getSafeUserId(),
|
||||||
|
type: MatrixWidgetType.Custom,
|
||||||
|
name: "Custom Widget",
|
||||||
|
url: "http://url1",
|
||||||
|
},
|
||||||
|
] satisfies IApp[]);
|
||||||
|
|
||||||
|
render(<ExtensionsCard room={room} onClose={jest.fn()} />);
|
||||||
|
expect(screen.getByText("Set layout for everyone")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show widget as pinned", async () => {
|
||||||
|
jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(true);
|
||||||
|
mocked(useWidgets).mockReturnValue([
|
||||||
|
{
|
||||||
|
id: "id",
|
||||||
|
roomId: room.roomId,
|
||||||
|
eventId: "$event1",
|
||||||
|
creatorUserId: client.getSafeUserId(),
|
||||||
|
type: MatrixWidgetType.Custom,
|
||||||
|
name: "Custom Widget",
|
||||||
|
url: "http://url1",
|
||||||
|
},
|
||||||
|
] satisfies IApp[]);
|
||||||
|
|
||||||
|
render(<ExtensionsCard room={room} onClose={jest.fn()} />);
|
||||||
|
expect(screen.getByText("Custom Widget").closest(".mx_ExtensionsCard_Button_pinned")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show cannot pin warning", async () => {
|
||||||
|
jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(false);
|
||||||
|
jest.spyOn(WidgetLayoutStore.instance, "canAddToContainer").mockReturnValue(false);
|
||||||
|
mocked(useWidgets).mockReturnValue([
|
||||||
|
{
|
||||||
|
id: "id",
|
||||||
|
roomId: room.roomId,
|
||||||
|
eventId: "$event1",
|
||||||
|
creatorUserId: client.getSafeUserId(),
|
||||||
|
type: MatrixWidgetType.Custom,
|
||||||
|
name: "Custom Widget",
|
||||||
|
url: "http://url1",
|
||||||
|
},
|
||||||
|
] satisfies IApp[]);
|
||||||
|
|
||||||
|
render(<ExtensionsCard room={room} onClose={jest.fn()} />);
|
||||||
|
expect(screen.getByLabelText("You can only pin up to 3 widgets")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should should open integration manager on click", async () => {
|
||||||
|
jest.spyOn(IntegrationManagers.sharedInstance(), "hasManager").mockReturnValue(false);
|
||||||
|
const spy = jest.spyOn(IntegrationManagers.sharedInstance(), "openNoManagerDialog");
|
||||||
|
render(<ExtensionsCard room={room} onClose={jest.fn()} />);
|
||||||
|
await userEvent.click(screen.getByText("Add extensions"));
|
||||||
|
expect(spy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
|
@ -38,8 +38,8 @@ describe("<RightPanelTabs />", () => {
|
||||||
const { container } = render(<RightPanelTabs phase={RightPanelPhases.RoomMemberList} />);
|
const { container } = render(<RightPanelTabs phase={RightPanelPhases.RoomMemberList} />);
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
// Assert that the active tab is Info
|
// Assert that the active tab is Info
|
||||||
expect(container.querySelectorAll("[aria-selected='true'").length).toEqual(1);
|
expect(container.querySelectorAll("[aria-selected='true']").length).toEqual(1);
|
||||||
expect(container.querySelector("[aria-selected='true'")).toHaveAccessibleName("People");
|
expect(container.querySelector("[aria-selected='true']")).toHaveAccessibleName("People");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Renders nothing for some phases, eg: FilePanel", () => {
|
it("Renders nothing for some phases, eg: FilePanel", () => {
|
||||||
|
|
|
@ -0,0 +1,194 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<ExtensionsCard /> should render empty state 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="mx_BaseCard mx_ExtensionsCard"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_BaseCard_header"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="_button_zt6rp_17 _has-icon_zt6rp_61"
|
||||||
|
data-kind="secondary"
|
||||||
|
data-size="sm"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
height="20"
|
||||||
|
width="20"
|
||||||
|
/>
|
||||||
|
Add extensions
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_AutoHideScrollbar"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_Flex mx_EmptyState"
|
||||||
|
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-4x);"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
height="32px"
|
||||||
|
width="32px"
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
class="_typography_yh5dq_162 _font-body-lg-semibold_yh5dq_83"
|
||||||
|
>
|
||||||
|
Boost productivity with more tools, widgets and bots
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="_typography_yh5dq_162 _font-body-md-regular_yh5dq_59"
|
||||||
|
>
|
||||||
|
Select “Add extensions” to browse and add extensions to this room
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<ExtensionsCard /> should render widgets 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="mx_BaseCard mx_ExtensionsCard"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_BaseCard_header"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="_button_zt6rp_17 _has-icon_zt6rp_61"
|
||||||
|
data-kind="secondary"
|
||||||
|
data-size="sm"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
height="20"
|
||||||
|
width="20"
|
||||||
|
/>
|
||||||
|
Add extensions
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_AutoHideScrollbar"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_separator_144s5_17"
|
||||||
|
data-kind="primary"
|
||||||
|
data-orientation="horizontal"
|
||||||
|
role="separator"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="mx_BaseCard_Button mx_ExtensionsCard_Button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_ExtensionsCard_icon_app"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-label="Avatar"
|
||||||
|
class="_avatar_mcap2_17 mx_BaseAvatar mx_WidgetAvatar"
|
||||||
|
data-color="1"
|
||||||
|
data-testid="avatar-img"
|
||||||
|
data-type="round"
|
||||||
|
style="--cpd-avatar-size: 24px;"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
class="_image_mcap2_50"
|
||||||
|
data-type="round"
|
||||||
|
height="24px"
|
||||||
|
loading="lazy"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
src="image-file-stub"
|
||||||
|
width="24px"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<p
|
||||||
|
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 mx_lineClamp"
|
||||||
|
>
|
||||||
|
Custom Widget
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
aria-label="Pin"
|
||||||
|
class="mx_AccessibleButton mx_ExtensionsCard_app_pinToggle"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_BaseCard_Button mx_ExtensionsCard_Button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_ExtensionsCard_icon_app"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-label="Avatar"
|
||||||
|
class="_avatar_mcap2_17 mx_BaseAvatar mx_WidgetAvatar"
|
||||||
|
data-color="1"
|
||||||
|
data-testid="avatar-img"
|
||||||
|
data-type="round"
|
||||||
|
style="--cpd-avatar-size: 24px;"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
class="_image_mcap2_50"
|
||||||
|
data-type="round"
|
||||||
|
height="24px"
|
||||||
|
loading="lazy"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
src="image-file-stub"
|
||||||
|
width="24px"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<p
|
||||||
|
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 mx_lineClamp"
|
||||||
|
>
|
||||||
|
Jitsi
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
aria-label="Pin"
|
||||||
|
class="mx_AccessibleButton mx_ExtensionsCard_app_pinToggle"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<ExtensionsCard /> should show context menu on widget row 1`] = `
|
||||||
|
<ul
|
||||||
|
class="mx_IconizedContextMenu"
|
||||||
|
role="none"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
aria-label="Remove for everyone"
|
||||||
|
class="mx_AccessibleButton mx_IconizedContextMenu_item"
|
||||||
|
role="menuitem"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_IconizedContextMenu_label"
|
||||||
|
>
|
||||||
|
Remove for everyone
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
`;
|
|
@ -54,6 +54,20 @@ exports[`<RightPanelTabs /> Component renders the correct tabs 1`] = `
|
||||||
Threads
|
Threads
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
<li
|
||||||
|
class="_nav-tab_135dy_33"
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-controls="thread-panel"
|
||||||
|
aria-selected="false"
|
||||||
|
class="_nav-item_135dy_55"
|
||||||
|
id="extensions-panel-tab"
|
||||||
|
role="tab"
|
||||||
|
>
|
||||||
|
Extensions
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
@ -113,6 +127,20 @@ exports[`<RightPanelTabs /> Correct tab is active 1`] = `
|
||||||
Threads
|
Threads
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
<li
|
||||||
|
class="_nav-tab_135dy_33"
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-controls="thread-panel"
|
||||||
|
aria-selected="false"
|
||||||
|
class="_nav-item_135dy_55"
|
||||||
|
id="extensions-panel-tab"
|
||||||
|
role="tab"
|
||||||
|
>
|
||||||
|
Extensions
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -414,20 +414,6 @@ exports[`<RoomSummaryCard /> has button to edit topic 1`] = `
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="mx_BaseCard_Group mx_RoomSummaryCard_appsGroup"
|
|
||||||
>
|
|
||||||
<h2>
|
|
||||||
Widgets
|
|
||||||
</h2>
|
|
||||||
<div
|
|
||||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
Add widgets, bridges & bots
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -820,20 +806,6 @@ exports[`<RoomSummaryCard /> renders the room summary 1`] = `
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="mx_BaseCard_Group mx_RoomSummaryCard_appsGroup"
|
|
||||||
>
|
|
||||||
<h2>
|
|
||||||
Widgets
|
|
||||||
</h2>
|
|
||||||
<div
|
|
||||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
Add widgets, bridges & bots
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1253,20 +1225,6 @@ exports[`<RoomSummaryCard /> renders the room topic in the summary 1`] = `
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="mx_BaseCard_Group mx_RoomSummaryCard_appsGroup"
|
|
||||||
>
|
|
||||||
<h2>
|
|
||||||
Widgets
|
|
||||||
</h2>
|
|
||||||
<div
|
|
||||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
Add widgets, bridges & bots
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue