Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/feat/room-list-widgets
Conflicts: src/components/views/elements/AppTile.js
|
@ -1,3 +1,12 @@
|
||||||
|
Changes in [3.6.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.6.1) (2020-10-20)
|
||||||
|
===================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.6.0...v3.6.1)
|
||||||
|
|
||||||
|
* [Release] Adjust for new widget messaging APIs
|
||||||
|
[\#5342](https://github.com/matrix-org/matrix-react-sdk/pull/5342)
|
||||||
|
* [Release] Fix Jitsi OpenIDC auth
|
||||||
|
[\#5335](https://github.com/matrix-org/matrix-react-sdk/pull/5335)
|
||||||
|
|
||||||
Changes in [3.6.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.6.0) (2020-10-12)
|
Changes in [3.6.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.6.0) (2020-10-12)
|
||||||
===================================================================================================
|
===================================================================================================
|
||||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.6.0-rc.1...v3.6.0)
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.6.0-rc.1...v3.6.0)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "3.6.0",
|
"version": "3.6.1",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
|
@ -32,9 +32,7 @@ do
|
||||||
echo "Upgrading $i to $latestver..."
|
echo "Upgrading $i to $latestver..."
|
||||||
yarn add -E $i@$latestver
|
yarn add -E $i@$latestver
|
||||||
git add -u
|
git add -u
|
||||||
# The `-e` flag opens the editor and gives you a chance to check
|
git commit -m "Upgrade $i to $latestver"
|
||||||
# the upgrade for correctness.
|
|
||||||
git commit -m "Upgrade $i to $latestver" -e
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
|
@ -52,11 +52,11 @@
|
||||||
@import "./views/avatars/_DecoratedRoomAvatar.scss";
|
@import "./views/avatars/_DecoratedRoomAvatar.scss";
|
||||||
@import "./views/avatars/_MemberStatusMessageAvatar.scss";
|
@import "./views/avatars/_MemberStatusMessageAvatar.scss";
|
||||||
@import "./views/avatars/_PulsedAvatar.scss";
|
@import "./views/avatars/_PulsedAvatar.scss";
|
||||||
|
@import "./views/avatars/_WidgetAvatar.scss";
|
||||||
@import "./views/context_menus/_IconizedContextMenu.scss";
|
@import "./views/context_menus/_IconizedContextMenu.scss";
|
||||||
@import "./views/context_menus/_MessageContextMenu.scss";
|
@import "./views/context_menus/_MessageContextMenu.scss";
|
||||||
@import "./views/context_menus/_StatusMessageContextMenu.scss";
|
@import "./views/context_menus/_StatusMessageContextMenu.scss";
|
||||||
@import "./views/context_menus/_TagTileContextMenu.scss";
|
@import "./views/context_menus/_TagTileContextMenu.scss";
|
||||||
@import "./views/context_menus/_WidgetContextMenu.scss";
|
|
||||||
@import "./views/dialogs/_AddressPickerDialog.scss";
|
@import "./views/dialogs/_AddressPickerDialog.scss";
|
||||||
@import "./views/dialogs/_Analytics.scss";
|
@import "./views/dialogs/_Analytics.scss";
|
||||||
@import "./views/dialogs/_BugReportDialog.scss";
|
@import "./views/dialogs/_BugReportDialog.scss";
|
||||||
|
|
|
@ -79,7 +79,6 @@ limitations under the License.
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MatrixChat > .mx_LeftPanel2:hover + .mx_ResizeHandle_horizontal,
|
|
||||||
.mx_MatrixChat > .mx_ResizeHandle_horizontal:hover {
|
.mx_MatrixChat > .mx_ResizeHandle_horizontal:hover {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
|
|
@ -230,6 +230,10 @@ limitations under the License.
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.mx_UserMenu_contextMenu_hostingLink {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_IconizedContextMenu_icon {
|
.mx_IconizedContextMenu_icon {
|
||||||
|
|
|
@ -14,10 +14,6 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ActionPayload } from "../payloads";
|
.mx_WidgetAvatar {
|
||||||
import { Action } from "../actions";
|
border-radius: 4px;
|
||||||
|
|
||||||
export interface AppTileActionPayload extends ActionPayload {
|
|
||||||
action: Action.AppTileDelete | Action.AppTileRevoke;
|
|
||||||
widgetId: string;
|
|
||||||
}
|
}
|
|
@ -1,36 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2019 The Matrix.org Foundaction 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_WidgetContextMenu {
|
|
||||||
padding: 6px;
|
|
||||||
|
|
||||||
.mx_WidgetContextMenu_option {
|
|
||||||
padding: 3px 6px 3px 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_WidgetContextMenu_separator {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
border-bottom-style: none;
|
|
||||||
border-left-style: none;
|
|
||||||
border-right-style: none;
|
|
||||||
border-top-style: solid;
|
|
||||||
border-top-width: 1px;
|
|
||||||
border-color: $menu-border-color;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -128,6 +128,13 @@ limitations under the License.
|
||||||
mask-size: 20px;
|
mask-size: 20px;
|
||||||
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
|
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.mx_AccessibleButton_disabled {
|
||||||
|
padding-right: 12px;
|
||||||
|
&::after {
|
||||||
|
content: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -110,28 +110,107 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_RoomSummaryCard_appsGroup {
|
.mx_RoomSummaryCard_appsGroup {
|
||||||
.mx_RoomSummaryCard_Button {
|
.mx_RoomSummaryCard_Button {
|
||||||
padding-left: 12px;
|
// 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-fg-color;
|
color: $tertiary-fg-color;
|
||||||
|
|
||||||
span {
|
.mx_RoomSummaryCard_icon_app {
|
||||||
color: $primary-fg-color;
|
padding: 10px 48px 10px 12px; // based on typical mx_RoomSummaryCard_Button padding
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.mx_BaseAvatar_image {
|
||||||
|
vertical-align: top;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: $primary-fg-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
.mx_RoomSummaryCard_app_pinToggle,
|
||||||
vertical-align: top;
|
.mx_RoomSummaryCard_app_options {
|
||||||
margin-right: 12px;
|
position: absolute;
|
||||||
border-radius: 4px;
|
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: 24px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/pin-upright.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomSummaryCard_app_options {
|
||||||
|
right: 48px;
|
||||||
|
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-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.mx_RoomSummaryCard_icon_app {
|
||||||
|
padding-right: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomSummaryCard_app_options {
|
||||||
|
display: unset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: unset;
|
content: unset;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomSummaryCard_icon_app_pinned::after {
|
&::after {
|
||||||
mask-image: url('$(res)/img/element-icons/room/pin-upright.svg');
|
top: 8px; // re-align based on the height change
|
||||||
background-color: $accent-color;
|
pointer-events: none; // pass through to the real button
|
||||||
transform: unset;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,34 +24,35 @@ limitations under the License.
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_WidgetCard_noEdit {
|
.mx_BaseCard_header {
|
||||||
.mx_AccessibleButton_kind_secondary {
|
display: inline-flex;
|
||||||
margin: 0 12px;
|
|
||||||
|
|
||||||
&:first-child {
|
& > h2 {
|
||||||
// expand the Pin to room primary action
|
margin-right: 0;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.mx_WidgetCard_optionsButton {
|
.mx_WidgetCard_optionsButton {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 18px;
|
margin-right: 44px;
|
||||||
width: 26px;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
height: 20px;
|
||||||
top: 6px;
|
width: 20px;
|
||||||
left: 20px;
|
min-width: 20px; // prevent crushing by the flexbox
|
||||||
mask-repeat: no-repeat;
|
padding: 0;
|
||||||
mask-position: center;
|
|
||||||
mask-size: contain;
|
&::before {
|
||||||
mask-image: url('$(res)/img/element-icons/room/ellipsis.svg');
|
content: "";
|
||||||
background-color: $secondary-fg-color;
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
top: 0;
|
||||||
|
left: 4px;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/ellipsis.svg');
|
||||||
|
background-color: $secondary-fg-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,53 +47,100 @@ $MiniAppTileHeight: 200px;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
background: $primary-fg-color;
|
background: $primary-fg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_ResizeHandle_horizontal::before {
|
||||||
|
position: absolute;
|
||||||
|
left: 3px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(0, -50%);
|
||||||
|
|
||||||
|
height: 64px; // to match width of the ones on roomlist
|
||||||
|
width: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
content: '';
|
||||||
|
|
||||||
|
background-color: $primary-fg-color;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_AppsContainer_resizer {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_AppsContainer {
|
.mx_AppsContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin-bottom: 8px;
|
width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
.mx_AppTile:first-of-type {
|
||||||
|
border-left-width: 8px;
|
||||||
|
border-radius: 10px 0 0 10px;
|
||||||
|
}
|
||||||
|
.mx_AppTile:last-of-type {
|
||||||
|
border-right-width: 8px;
|
||||||
|
border-radius: 0 10px 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ResizeHandle_horizontal {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppsDrawer_minimised .mx_AppsContainer {
|
// TODO this should be 300px but that's too large
|
||||||
// override the re-resizable inline styles
|
$MinWidth: 240px;
|
||||||
height: inherit !important;
|
|
||||||
min-height: inherit !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AddWidget_button {
|
.mx_AppsDrawer_2apps .mx_AppTile {
|
||||||
order: 2;
|
width: 50%;
|
||||||
cursor: pointer;
|
|
||||||
padding: 0;
|
&:nth-child(3) {
|
||||||
margin: -3px auto 5px 0;
|
flex-grow: 1;
|
||||||
color: $accent-color;
|
width: 0 !important;
|
||||||
font-size: $font-12px;
|
min-width: $MinWidth !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.mx_AppsDrawer_3apps .mx_AppTile {
|
||||||
|
width: 33%;
|
||||||
|
|
||||||
|
&:nth-child(3) {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 0 !important;
|
||||||
|
min-width: $MinWidth !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppTile {
|
.mx_AppTile {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
border: 5px solid $widget-menu-bar-bg-color;
|
min-width: $MinWidth;
|
||||||
border-radius: 4px;
|
border: 8px solid $widget-menu-bar-bg-color;
|
||||||
|
border-left-width: 5px;
|
||||||
|
border-right-width: 5px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
box-sizing: border-box;
|
||||||
& + .mx_AppTile {
|
background-color: $widget-menu-bar-bg-color;
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppTileFullWidth {
|
.mx_AppTileFullWidth {
|
||||||
width: 100%;
|
width: 100% !important; // to override the inline style set by the resizer
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: 5px solid $widget-menu-bar-bg-color;
|
border: 5px solid $widget-menu-bar-bg-color;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
background-color: $widget-menu-bar-bg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppTile_mini {
|
.mx_AppTile_mini {
|
||||||
|
@ -105,12 +152,6 @@ $MiniAppTileHeight: 200px;
|
||||||
height: $MiniAppTileHeight;
|
height: $MiniAppTileHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppTile.mx_AppTile_minimised,
|
|
||||||
.mx_AppTileFullWidth.mx_AppTile_minimised,
|
|
||||||
.mx_AppTile_mini.mx_AppTile_minimised {
|
|
||||||
height: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AppTile .mx_AppTile_persistedWrapper,
|
.mx_AppTile .mx_AppTile_persistedWrapper,
|
||||||
.mx_AppTileFullWidth .mx_AppTile_persistedWrapper,
|
.mx_AppTileFullWidth .mx_AppTile_persistedWrapper,
|
||||||
.mx_AppTile_mini .mx_AppTile_persistedWrapper {
|
.mx_AppTile_mini .mx_AppTile_persistedWrapper {
|
||||||
|
@ -130,19 +171,20 @@ $MiniAppTileHeight: 200px;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
cursor: pointer;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
padding-top: 2px;
|
||||||
|
padding-bottom: 8px;
|
||||||
.mx_AppTileMenuBar_expanded {
|
|
||||||
padding-bottom: 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppTileMenuBarTitle {
|
.mx_AppTileMenuBarTitle {
|
||||||
display: flex;
|
line-height: 20px;
|
||||||
flex-direction: row;
|
white-space: nowrap;
|
||||||
align-items: center;
|
overflow: hidden;
|
||||||
pointer-events: none;
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
.mx_WidgetAvatar {
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppTileMenuBarTitle > :last-child {
|
.mx_AppTileMenuBarTitle > :last-child {
|
||||||
|
@ -166,37 +208,20 @@ $MiniAppTileHeight: 200px;
|
||||||
margin: 0 3px;
|
margin: 0 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_minimise {
|
|
||||||
mask-image: url('$(res)/img/feather-customised/widget/minimise.svg');
|
|
||||||
background-color: $accent-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_maximise {
|
|
||||||
mask-image: url('$(res)/img/feather-customised/widget/maximise.svg');
|
|
||||||
background-color: $accent-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_popout {
|
.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_popout {
|
||||||
mask-image: url('$(res)/img/feather-customised/widget/external-link.svg');
|
mask-image: url('$(res)/img/feather-customised/widget/external-link.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_menu {
|
.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_menu {
|
||||||
mask-image: url('$(res)/img/icon_context.svg');
|
mask-image: url('$(res)/img/element-icons/room/ellipsis.svg');
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AppTileMenuBarWidgetDelete {
|
|
||||||
filter: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AppTileMenuBarWidget:hover {
|
|
||||||
border: 1px solid $primary-fg-color;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppTileBody {
|
.mx_AppTileBody {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: $widget-body-bg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppTileBody_mini {
|
.mx_AppTileBody_mini {
|
||||||
|
@ -231,7 +256,6 @@ $MiniAppTileHeight: 200px;
|
||||||
|
|
||||||
.mx_AppPermissionWarning {
|
.mx_AppPermissionWarning {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background-color: $widget-menu-bar-bg-color;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -296,6 +320,10 @@ $MiniAppTileHeight: 200px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
|
// match bg of border so that the cut corners have the right fill
|
||||||
|
background-color: $widget-body-bg-color !important;
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppLoading .mx_Spinner {
|
.mx_AppLoading .mx_Spinner {
|
||||||
|
@ -323,10 +351,6 @@ $MiniAppTileHeight: 200px;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppsDrawer_minimised .mx_AppsContainer_resizerHandle {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Avoid apptile iframes capturing mouse event focus when resizing */
|
/* Avoid apptile iframes capturing mouse event focus when resizing */
|
||||||
.mx_AppsDrawer_resizing iframe {
|
.mx_AppsDrawer_resizing iframe {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
|
@ -241,6 +241,13 @@ limitations under the License.
|
||||||
width: 26px;
|
width: 26px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_RoomHeader_appsButton::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/apps.svg');
|
||||||
|
}
|
||||||
|
.mx_RoomHeader_appsButton_highlight::before {
|
||||||
|
background-color: $accent-color;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_RoomHeader_searchButton::before {
|
.mx_RoomHeader_searchButton::before {
|
||||||
mask-image: url('$(res)/img/element-icons/room/search-inset.svg');
|
mask-image: url('$(res)/img/element-icons/room/search-inset.svg');
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,10 @@
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_AppTileMenuBar {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
iframe {
|
iframe {
|
||||||
// Sticker picker depends on the fixed height previously used for all tiles
|
// Sticker picker depends on the fixed height previously used for all tiles
|
||||||
height: 273px;
|
height: 273px;
|
||||||
|
|
6
res/img/element-icons/room/apps.svg
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="14" y="2" width="8" height="8" rx="2" fill="#0DBD8B"/>
|
||||||
|
<rect x="14" y="14" width="8" height="8" rx="2" fill="#0DBD8B"/>
|
||||||
|
<rect x="2" y="14" width="8" height="8" rx="2" fill="#0DBD8B"/>
|
||||||
|
<rect x="2" y="2" width="8" height="8" rx="2" fill="#0DBD8B"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 359 B |
|
@ -1,11 +1,21 @@
|
||||||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<rect width="20" height="20" fill="url(#paint0_linear)"/>
|
<g clip-path="url(#clip0)">
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 3V9.5H0.00390625L0.00390625 10.5H2V17H0.00390625L0.00390625 18H2V20H3V18H9.5039V20.0005H10.5039V18H17V20H18V18H20.0039V17H18V10.5H20.0039V9.5H18V3H20.0039V2H18V0L17 0V2H10.5039V0.000488281L9.5039 0.000488281V2H3V0L2 0V2H0.00390625L0.00390625 3H2ZM17 3H10.5039V9.5H17V3ZM17 10.5H10.5039V17H17V10.5ZM9.5039 10.5V17H3V10.5H9.5039ZM9.5039 3V9.5H3V3H9.5039Z" fill="white" fill-opacity="0.3" style="mix-blend-mode:lighten"/>
|
<rect width="20" height="20" rx="4" fill="url(#paint0_linear)"/>
|
||||||
<circle opacity="0.8" cx="10.0039" cy="10" r="7.5" stroke="white"/>
|
<path d="M2.49609 0V20" stroke="white" stroke-opacity="0.5" style="mix-blend-mode:lighten"/>
|
||||||
<defs>
|
<path d="M20 2.5L1.60531e-06 2.5" stroke="white" stroke-opacity="0.5" style="mix-blend-mode:lighten"/>
|
||||||
<linearGradient id="paint0_linear" x1="10" y1="0" x2="10" y2="20" gradientUnits="userSpaceOnUse">
|
<path d="M20 10L1.60531e-06 10" stroke="white" stroke-opacity="0.5" style="mix-blend-mode:lighten"/>
|
||||||
<stop stop-color="#60A6FF"/>
|
<path d="M20 17.5H1.60531e-06" stroke="white" stroke-opacity="0.5" style="mix-blend-mode:lighten"/>
|
||||||
<stop offset="1" stop-color="#418DED"/>
|
<path d="M10 0.000488281V20.0005" stroke="white" stroke-opacity="0.5" style="mix-blend-mode:lighten"/>
|
||||||
</linearGradient>
|
<path d="M17.4961 0V20" stroke="white" stroke-opacity="0.5" style="mix-blend-mode:lighten"/>
|
||||||
</defs>
|
<circle opacity="0.8" cx="10" cy="10" r="7.5" stroke="white"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear" x1="10" y1="0" x2="10" y2="20" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#60A6FF"/>
|
||||||
|
<stop offset="1" stop-color="#418DED"/>
|
||||||
|
</linearGradient>
|
||||||
|
<clipPath id="clip0">
|
||||||
|
<rect width="20" height="20.0005" rx="4" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 900 B After Width: | Height: | Size: 1.3 KiB |
|
@ -1,6 +1,6 @@
|
||||||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<rect x="1.99461" y="1.00002" width="18" height="18" rx="2" fill="white" stroke="#FF4B55" stroke-width="2"/>
|
<rect width="20" height="20" rx="4" fill="#FF4B55"/>
|
||||||
<rect x="2.96777" y="2" width="16.9843" height="5" fill="#FF4B55"/>
|
<path d="M2 7H18V16C18 17.1046 17.1046 18 16 18H4C2.89543 18 2 17.1046 2 16V7Z" fill="white"/>
|
||||||
<rect x="4.96533" y="9" width="2.99723" height="3" rx="0.25" fill="#FF4B55"/>
|
<rect x="3.96826" y="8.99951" width="2.99723" height="3" rx="0.25" fill="#FF4B55"/>
|
||||||
<rect x="11.9585" y="13.0005" width="2.99723" height="3" rx="0.25" fill="#FF4B55"/>
|
<rect x="10.9614" y="13" width="2.99723" height="3" rx="0.25" fill="#FF4B55"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 430 B |
|
@ -1,5 +1,5 @@
|
||||||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<rect x="1.49609" y="0.500488" width="19" height="19" rx="3.5" fill="#17191C" stroke="#17191C"/>
|
<rect x="1" y="1" width="18" height="18" rx="3" fill="#17191C" stroke="#17191C" stroke-width="2"/>
|
||||||
<path d="M18.9961 10.0005C18.9961 14.4188 15.4144 18.0005 10.9961 18.0005C6.57782 18.0005 2.99609 14.4188 2.99609 10.0005C2.99609 5.58221 6.57782 2.00049 10.9961 2.00049C15.4144 2.00049 18.9961 5.58221 18.9961 10.0005Z" fill="white"/>
|
<path d="M18 10C18 14.4183 14.4183 18 10 18C5.58172 18 2 14.4183 2 10C2 5.58172 5.58172 2 10 2C14.4183 2 18 5.58172 18 10Z" fill="white"/>
|
||||||
<path d="M10.9961 6.00049V9.81299L13.4961 11.5005" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M10 6V9.8125L12.5 11.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 569 B After Width: | Height: | Size: 469 B |
|
@ -1,4 +1,4 @@
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<rect x="-0.000976562" y="0.000488281" width="20" height="20" rx="4" fill="#FCC639"/>
|
<rect width="20" height="20" rx="4" fill="#FCC639"/>
|
||||||
<path d="M1.99902 7.00049H17.999V16.5005C17.999 17.3289 17.3274 18.0005 16.499 18.0005H3.49902C2.6706 18.0005 1.99902 17.3289 1.99902 16.5005V7.00049Z" fill="white"/>
|
<path d="M2 7H18V16C18 17.1046 17.1046 18 16 18H4C2.89543 18 2 17.1046 2 16V7Z" fill="white"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 364 B After Width: | Height: | Size: 259 B |
5
res/img/element-icons/room/default_video.svg
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="20" height="20" rx="4" fill="#5ABFF2"/>
|
||||||
|
<path d="M3 7.875C3 6.83947 3.83947 6 4.875 6H11.1875C12.223 6 13.0625 6.83947 13.0625 7.875V12.875C13.0625 13.9105 12.223 14.75 11.1875 14.75H4.875C3.83947 14.75 3 13.9105 3 12.875V7.875Z" fill="white"/>
|
||||||
|
<path d="M14.375 8.44644L16.1208 7.11039C16.4806 6.83502 17 7.09158 17 7.54468V13.0396C17 13.5199 16.4251 13.7669 16.0767 13.4363L14.375 11.8214V8.44644Z" fill="white"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 543 B |
|
@ -1,3 +0,0 @@
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 6C2 3.79086 3.79086 2 6 2H18C20.2091 2 22 3.79086 22 6V18C22 20.2091 20.2091 22 18 22H6C3.79086 22 2 20.2091 2 18V6ZM11 8C11 9.65685 9.65685 11 8 11C6.34315 11 5 9.65685 5 8C5 6.34315 6.34315 5 8 5C9.65685 5 11 6.34315 11 8ZM8 19C9.65685 19 11 17.6569 11 16C11 14.3431 9.65685 13 8 13C6.34315 13 5 14.3431 5 16C5 17.6569 6.34315 19 8 19ZM19 16C19 17.6569 17.6569 19 16 19C14.3431 19 13 17.6569 13 16C13 14.3431 14.3431 13 16 13C17.6569 13 19 14.3431 19 16ZM16 11C17.6569 11 19 9.65685 19 8C19 6.34315 17.6569 5 16 5C14.3431 5 13 6.34315 13 8C13 9.65685 14.3431 11 16 11Z" fill="black"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 742 B |
|
@ -1,5 +0,0 @@
|
||||||
<svg width="3" height="15" viewBox="0 0 3 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 3C2.32843 3 3 2.32843 3 1.5C3 0.671573 2.32843 0 1.5 0C0.671573 0 0 0.671573 0 1.5C0 2.32843 0.671573 3 1.5 3Z" fill="#9FA9BA"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 9C2.32843 9 3 8.32843 3 7.5C3 6.67157 2.32843 6 1.5 6C0.671573 6 0 6.67157 0 7.5C0 8.32843 0.671573 9 1.5 9Z" fill="#9FA9BA"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 15C2.32843 15 3 14.3284 3 13.5C3 12.6716 2.32843 12 1.5 12C0.671573 12 0 12.6716 0 13.5C0 14.3284 0.671573 15 1.5 15Z" fill="#9FA9BA"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 655 B |
|
@ -131,6 +131,7 @@ $notice-secondary-color: $roomlist-header-color;
|
||||||
$panel-divider-color: transparent;
|
$panel-divider-color: transparent;
|
||||||
|
|
||||||
$widget-menu-bar-bg-color: $header-panel-bg-color;
|
$widget-menu-bar-bg-color: $header-panel-bg-color;
|
||||||
|
$widget-body-bg-color: rgba(141, 151, 165, 0.2);
|
||||||
|
|
||||||
// event tile lifecycle
|
// event tile lifecycle
|
||||||
$event-sending-color: $text-secondary-color;
|
$event-sending-color: $text-secondary-color;
|
||||||
|
|
|
@ -126,6 +126,7 @@ $roomtile-selected-bg-color: #1A1D23;
|
||||||
$panel-divider-color: $header-panel-border-color;
|
$panel-divider-color: $header-panel-border-color;
|
||||||
|
|
||||||
$widget-menu-bar-bg-color: $header-panel-bg-color;
|
$widget-menu-bar-bg-color: $header-panel-bg-color;
|
||||||
|
$widget-body-bg-color: #1A1D23;
|
||||||
|
|
||||||
// event tile lifecycle
|
// event tile lifecycle
|
||||||
$event-sending-color: $text-secondary-color;
|
$event-sending-color: $text-secondary-color;
|
||||||
|
|
|
@ -208,6 +208,7 @@ $panel-divider-color: #dee1f3;
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
$widget-menu-bar-bg-color: $secondary-accent-color;
|
$widget-menu-bar-bg-color: $secondary-accent-color;
|
||||||
|
$widget-body-bg-color: #fff;
|
||||||
|
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
|
|
|
@ -208,6 +208,7 @@ $pinned-color: $notice-secondary-color;
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
$widget-menu-bar-bg-color: $secondary-accent-color;
|
$widget-menu-bar-bg-color: $secondary-accent-color;
|
||||||
|
$widget-body-bg-color: #FFF;
|
||||||
|
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
|
|
|
@ -360,7 +360,7 @@ function combineEvents(previousSearchResult, localEvents = undefined, serverEven
|
||||||
let oldestEventFrom = previousSearchResult.oldestEventFrom;
|
let oldestEventFrom = previousSearchResult.oldestEventFrom;
|
||||||
response.highlights = previousSearchResult.highlights;
|
response.highlights = previousSearchResult.highlights;
|
||||||
|
|
||||||
if (localEvents && serverEvents) {
|
if (localEvents && serverEvents && serverEvents.results) {
|
||||||
// This is a first search call, combine the events from the server and
|
// This is a first search call, combine the events from the server and
|
||||||
// the local index. Note where our oldest event came from, we shall
|
// the local index. Note where our oldest event came from, we shall
|
||||||
// fetch the next batch of events from the other source.
|
// fetch the next batch of events from the other source.
|
||||||
|
@ -379,7 +379,7 @@ function combineEvents(previousSearchResult, localEvents = undefined, serverEven
|
||||||
oldestEventFrom = "local";
|
oldestEventFrom = "local";
|
||||||
}
|
}
|
||||||
combineEventSources(previousSearchResult, response, localEvents.results, cachedEvents);
|
combineEventSources(previousSearchResult, response, localEvents.results, cachedEvents);
|
||||||
} else if (serverEvents) {
|
} else if (serverEvents && serverEvents.results) {
|
||||||
// This is a pagination call fetching more events from the server,
|
// This is a pagination call fetching more events from the server,
|
||||||
// meaning that our oldest event was in the local index.
|
// meaning that our oldest event was in the local index.
|
||||||
// Change the source of the oldest event if our server event is older
|
// Change the source of the oldest event if our server event is older
|
||||||
|
@ -454,7 +454,7 @@ function combineResponses(previousSearchResult, localEvents = undefined, serverE
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreEncryptionInfo(searchResultSlice) {
|
function restoreEncryptionInfo(searchResultSlice = []) {
|
||||||
for (let i = 0; i < searchResultSlice.length; i++) {
|
for (let i = 0; i < searchResultSlice.length; i++) {
|
||||||
const timeline = searchResultSlice[i].context.getTimeline();
|
const timeline = searchResultSlice[i].context.getTimeline();
|
||||||
|
|
||||||
|
@ -517,7 +517,7 @@ async function combinedPagination(searchResult) {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const oldResultCount = searchResult.results.length;
|
const oldResultCount = searchResult.results ? searchResult.results.length : 0;
|
||||||
|
|
||||||
// Let the client process the combined result.
|
// Let the client process the combined result.
|
||||||
const result = client._processRoomEventsSearch(searchResult, response);
|
const result = client._processRoomEventsSearch(searchResult, response);
|
||||||
|
|
|
@ -416,8 +416,9 @@ export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None
|
||||||
return menuOptions;
|
return menuOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useContextMenu = (): [boolean, RefObject<HTMLElement>, () => void, () => void, (val: boolean) => void] => {
|
type ContextMenuTuple<T> = [boolean, RefObject<T>, () => void, () => void, (val: boolean) => void];
|
||||||
const button = useRef<HTMLElement>(null);
|
export const useContextMenu = <T extends any = HTMLElement>(): ContextMenuTuple<T> => {
|
||||||
|
const button = useRef<T>(null);
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const open = () => {
|
const open = () => {
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
|
|
|
@ -398,7 +398,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
const roomList = <RoomList
|
const roomList = <RoomList
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
resizeNotifier={null}
|
resizeNotifier={null}
|
||||||
collapsed={false}
|
|
||||||
onFocus={this.onFocus}
|
onFocus={this.onFocus}
|
||||||
onBlur={this.onBlur}
|
onBlur={this.onBlur}
|
||||||
isMinimized={this.props.isMinimized}
|
isMinimized={this.props.isMinimized}
|
||||||
|
|
|
@ -52,6 +52,7 @@ import RoomListStore from "../../stores/room-list/RoomListStore";
|
||||||
import NonUrgentToastContainer from "./NonUrgentToastContainer";
|
import NonUrgentToastContainer from "./NonUrgentToastContainer";
|
||||||
import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload";
|
import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload";
|
||||||
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||||
|
import { ICollapseConfig } from "../../resizer/distributors/collapse";
|
||||||
|
|
||||||
// We need to fetch each pinned message individually (if we don't already have it)
|
// We need to fetch each pinned message individually (if we don't already have it)
|
||||||
// so each pinned message may trigger a request. Limit the number per room for sanity.
|
// so each pinned message may trigger a request. Limit the number per room for sanity.
|
||||||
|
@ -205,13 +206,8 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
_createResizer() {
|
_createResizer() {
|
||||||
const classNames = {
|
|
||||||
handle: "mx_ResizeHandle",
|
|
||||||
vertical: "mx_ResizeHandle_vertical",
|
|
||||||
reverse: "mx_ResizeHandle_reverse",
|
|
||||||
};
|
|
||||||
let size;
|
let size;
|
||||||
const collapseConfig = {
|
const collapseConfig: ICollapseConfig = {
|
||||||
toggleSize: 260 - 50,
|
toggleSize: 260 - 50,
|
||||||
onCollapsed: (collapsed) => {
|
onCollapsed: (collapsed) => {
|
||||||
if (collapsed) {
|
if (collapsed) {
|
||||||
|
@ -234,7 +230,11 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const resizer = new Resizer(this._resizeContainer.current, CollapseDistributor, collapseConfig);
|
const resizer = new Resizer(this._resizeContainer.current, CollapseDistributor, collapseConfig);
|
||||||
resizer.setClassNames(classNames);
|
resizer.setClassNames({
|
||||||
|
handle: "mx_ResizeHandle",
|
||||||
|
vertical: "mx_ResizeHandle_vertical",
|
||||||
|
reverse: "mx_ResizeHandle_reverse",
|
||||||
|
});
|
||||||
return resizer;
|
return resizer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -72,6 +72,8 @@ import TintableSvg from "../views/elements/TintableSvg";
|
||||||
import {XOR} from "../../@types/common";
|
import {XOR} from "../../@types/common";
|
||||||
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||||
import { CallState, CallType, MatrixCall } from "matrix-js-sdk/lib/webrtc/call";
|
import { CallState, CallType, MatrixCall } from "matrix-js-sdk/lib/webrtc/call";
|
||||||
|
import WidgetStore from "../../stores/WidgetStore";
|
||||||
|
import {UPDATE_EVENT} from "../../stores/AsyncStore";
|
||||||
|
|
||||||
const DEBUG = false;
|
const DEBUG = false;
|
||||||
let debuglog = function(msg: string) {};
|
let debuglog = function(msg: string) {};
|
||||||
|
@ -180,6 +182,7 @@ export interface IState {
|
||||||
e2eStatus?: E2EStatus;
|
e2eStatus?: E2EStatus;
|
||||||
rejecting?: boolean;
|
rejecting?: boolean;
|
||||||
rejectError?: Error;
|
rejectError?: Error;
|
||||||
|
hasPinnedWidgets?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class RoomView extends React.Component<IProps, IState> {
|
export default class RoomView extends React.Component<IProps, IState> {
|
||||||
|
@ -250,7 +253,9 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||||
this.rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelStoreUpdate);
|
this.rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelStoreUpdate);
|
||||||
|
|
||||||
WidgetEchoStore.on('update', this.onWidgetEchoStoreUpdate);
|
WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
|
||||||
|
WidgetStore.instance.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
|
||||||
|
|
||||||
this.showReadReceiptsWatchRef = SettingsStore.watchSetting("showReadReceipts", null,
|
this.showReadReceiptsWatchRef = SettingsStore.watchSetting("showReadReceipts", null,
|
||||||
this.onReadReceiptsChange);
|
this.onReadReceiptsChange);
|
||||||
this.layoutWatcherRef = SettingsStore.watchSetting("useIRCLayout", null, this.onLayoutChange);
|
this.layoutWatcherRef = SettingsStore.watchSetting("useIRCLayout", null, this.onLayoutChange);
|
||||||
|
@ -262,6 +267,18 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
this.onRoomViewStoreUpdate(true);
|
this.onRoomViewStoreUpdate(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onWidgetStoreUpdate = () => {
|
||||||
|
if (this.state.room) {
|
||||||
|
this.checkWidgets(this.state.room);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkWidgets = (room) => {
|
||||||
|
this.setState({
|
||||||
|
hasPinnedWidgets: WidgetStore.instance.getPinnedApps(room.roomId).length > 0,
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
private onReadReceiptsChange = () => {
|
private onReadReceiptsChange = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
showReadReceipts: SettingsStore.getValue("showReadReceipts", this.state.roomId),
|
showReadReceipts: SettingsStore.getValue("showReadReceipts", this.state.roomId),
|
||||||
|
@ -584,7 +601,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
this.rightPanelStoreToken.remove();
|
this.rightPanelStoreToken.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
WidgetEchoStore.removeListener('update', this.onWidgetEchoStoreUpdate);
|
WidgetEchoStore.removeListener(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
|
||||||
|
WidgetStore.instance.removeListener(UPDATE_EVENT, this.onWidgetStoreUpdate);
|
||||||
|
|
||||||
if (this.showReadReceiptsWatchRef) {
|
if (this.showReadReceiptsWatchRef) {
|
||||||
SettingsStore.unwatchSetting(this.showReadReceiptsWatchRef);
|
SettingsStore.unwatchSetting(this.showReadReceiptsWatchRef);
|
||||||
|
@ -823,6 +841,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
this.calculateRecommendedVersion(room);
|
this.calculateRecommendedVersion(room);
|
||||||
this.updateE2EStatus(room);
|
this.updateE2EStatus(room);
|
||||||
this.updatePermissions(room);
|
this.updatePermissions(room);
|
||||||
|
this.checkWidgets(room);
|
||||||
};
|
};
|
||||||
|
|
||||||
private async calculateRecommendedVersion(room: Room) {
|
private async calculateRecommendedVersion(room: Room) {
|
||||||
|
@ -1258,7 +1277,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.state.searchResults.next_batch) {
|
if (!this.state.searchResults.next_batch) {
|
||||||
if (this.state.searchResults.results.length == 0) {
|
if (!this.state.searchResults?.results?.length) {
|
||||||
ret.push(<li key="search-top-marker">
|
ret.push(<li key="search-top-marker">
|
||||||
<h2 className="mx_RoomView_topMarker">{ _t("No results") }</h2>
|
<h2 className="mx_RoomView_topMarker">{ _t("No results") }</h2>
|
||||||
</li>,
|
</li>,
|
||||||
|
@ -1282,7 +1301,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
let lastRoomId;
|
let lastRoomId;
|
||||||
|
|
||||||
for (let i = this.state.searchResults.results.length - 1; i >= 0; i--) {
|
for (let i = (this.state.searchResults?.results?.length || 0) - 1; i >= 0; i--) {
|
||||||
const result = this.state.searchResults.results[i];
|
const result = this.state.searchResults.results[i];
|
||||||
|
|
||||||
const mxEv = result.context.getEvent();
|
const mxEv = result.context.getEvent();
|
||||||
|
@ -1352,6 +1371,13 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
dis.fire(Action.FocusComposer);
|
dis.fire(Action.FocusComposer);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onAppsClick = () => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: "appsDrawer",
|
||||||
|
show: !this.state.showApps,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
private onLeaveClick = () => {
|
private onLeaveClick = () => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'leave_room',
|
action: 'leave_room',
|
||||||
|
@ -1944,7 +1970,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
if (this.state.searchResults) {
|
if (this.state.searchResults) {
|
||||||
// show searching spinner
|
// show searching spinner
|
||||||
if (this.state.searchResults.results === undefined) {
|
if (this.state.searchResults.count === undefined) {
|
||||||
searchResultsPanel = (
|
searchResultsPanel = (
|
||||||
<div className="mx_RoomView_messagePanel mx_RoomView_messagePanelSearchSpinner" />
|
<div className="mx_RoomView_messagePanel mx_RoomView_messagePanelSearchSpinner" />
|
||||||
);
|
);
|
||||||
|
@ -2054,6 +2080,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
||||||
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
||||||
e2eStatus={this.state.e2eStatus}
|
e2eStatus={this.state.e2eStatus}
|
||||||
|
onAppsClick={this.state.hasPinnedWidgets ? this.onAppsClick : null}
|
||||||
|
appsShown={this.state.showApps}
|
||||||
/>
|
/>
|
||||||
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
|
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
|
||||||
<div className="mx_RoomView_body">
|
<div className="mx_RoomView_body">
|
||||||
|
|
|
@ -257,7 +257,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
const signupLink = getHostingLink("user-context-menu");
|
const signupLink = getHostingLink("user-context-menu");
|
||||||
if (signupLink) {
|
if (signupLink) {
|
||||||
hostingLink = (
|
hostingLink = (
|
||||||
<div className="mx_UserMenu_contextMenu_header">
|
<div className="mx_UserMenu_contextMenu_header mx_UserMenu_contextMenu_hostingLink">
|
||||||
{_t(
|
{_t(
|
||||||
"<a>Upgrade</a> to your own domain", {},
|
"<a>Upgrade</a> to your own domain", {},
|
||||||
{
|
{
|
||||||
|
|
58
src/components/views/avatars/WidgetAvatar.tsx
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 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, {ComponentProps, useContext} from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
||||||
|
|
||||||
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
import {IApp} from "../../../stores/WidgetStore";
|
||||||
|
import BaseAvatar, {BaseAvatarType} from "./BaseAvatar";
|
||||||
|
|
||||||
|
interface IProps extends Omit<ComponentProps<BaseAvatarType>, "name" | "url" | "urls"> {
|
||||||
|
app: IApp;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WidgetAvatar: React.FC<IProps> = ({ app, className, width = 20, height = 20, ...props }) => {
|
||||||
|
const cli = useContext(MatrixClientContext);
|
||||||
|
|
||||||
|
let iconUrls = [require("../../../../res/img/element-icons/room/default_app.svg")];
|
||||||
|
// heuristics for some better icons until Widgets support their own icons
|
||||||
|
if (app.type.includes("jitsi")) {
|
||||||
|
iconUrls = [require("../../../../res/img/element-icons/room/default_video.svg")];
|
||||||
|
} else if (app.type.includes("meeting") || app.type.includes("calendar")) {
|
||||||
|
iconUrls = [require("../../../../res/img/element-icons/room/default_cal.svg")];
|
||||||
|
} else if (app.type.includes("pad") || app.type.includes("doc") || app.type.includes("calc")) {
|
||||||
|
iconUrls = [require("../../../../res/img/element-icons/room/default_doc.svg")];
|
||||||
|
} else if (app.type.includes("clock")) {
|
||||||
|
iconUrls = [require("../../../../res/img/element-icons/room/default_clock.svg")];
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseAvatar
|
||||||
|
{...props}
|
||||||
|
name={app.id}
|
||||||
|
className={classNames("mx_WidgetAvatar", className)}
|
||||||
|
// MSC2765
|
||||||
|
url={app.avatar_url ? getHttpUriForMxc(cli.getHomeserverUrl(), app.avatar_url, 20, 20, "crop") : undefined}
|
||||||
|
urls={iconUrls}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WidgetAvatar;
|
|
@ -1,142 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2019 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 PropTypes from 'prop-types';
|
|
||||||
import {_t} from '../../../languageHandler';
|
|
||||||
import {MenuItem} from "../../structures/ContextMenu";
|
|
||||||
|
|
||||||
export default class WidgetContextMenu extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onFinished: PropTypes.func,
|
|
||||||
|
|
||||||
// Callback for when the revoke button is clicked. Required.
|
|
||||||
onRevokeClicked: PropTypes.func.isRequired,
|
|
||||||
|
|
||||||
// Callback for when the unpin button is clicked. If absent, unpin will be hidden.
|
|
||||||
onUnpinClicked: PropTypes.func,
|
|
||||||
|
|
||||||
// Callback for when the snapshot button is clicked. Button not shown
|
|
||||||
// without a callback.
|
|
||||||
onSnapshotClicked: PropTypes.func,
|
|
||||||
|
|
||||||
// Callback for when the reload button is clicked. Button not shown
|
|
||||||
// without a callback.
|
|
||||||
onReloadClicked: PropTypes.func,
|
|
||||||
|
|
||||||
// Callback for when the edit button is clicked. Button not shown
|
|
||||||
// without a callback.
|
|
||||||
onEditClicked: PropTypes.func,
|
|
||||||
|
|
||||||
// Callback for when the delete button is clicked. Button not shown
|
|
||||||
// without a callback.
|
|
||||||
onDeleteClicked: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
proxyClick(fn) {
|
|
||||||
fn();
|
|
||||||
if (this.props.onFinished) this.props.onFinished();
|
|
||||||
}
|
|
||||||
|
|
||||||
// XXX: It's annoying that our context menus require us to hit onFinished() to close :(
|
|
||||||
|
|
||||||
onEditClicked = () => {
|
|
||||||
this.proxyClick(this.props.onEditClicked);
|
|
||||||
};
|
|
||||||
|
|
||||||
onReloadClicked = () => {
|
|
||||||
this.proxyClick(this.props.onReloadClicked);
|
|
||||||
};
|
|
||||||
|
|
||||||
onSnapshotClicked = () => {
|
|
||||||
this.proxyClick(this.props.onSnapshotClicked);
|
|
||||||
};
|
|
||||||
|
|
||||||
onDeleteClicked = () => {
|
|
||||||
this.proxyClick(this.props.onDeleteClicked);
|
|
||||||
};
|
|
||||||
|
|
||||||
onRevokeClicked = () => {
|
|
||||||
this.proxyClick(this.props.onRevokeClicked);
|
|
||||||
};
|
|
||||||
|
|
||||||
onUnpinClicked = () => this.proxyClick(this.props.onUnpinClicked);
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const options = [];
|
|
||||||
|
|
||||||
if (this.props.onEditClicked) {
|
|
||||||
options.push(
|
|
||||||
<MenuItem className='mx_WidgetContextMenu_option' onClick={this.onEditClicked} key='edit'>
|
|
||||||
{_t("Edit")}
|
|
||||||
</MenuItem>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.onUnpinClicked) {
|
|
||||||
options.push(
|
|
||||||
<MenuItem className="mx_WidgetContextMenu_option" onClick={this.onUnpinClicked} key="unpin">
|
|
||||||
{_t("Unpin")}
|
|
||||||
</MenuItem>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.onReloadClicked) {
|
|
||||||
options.push(
|
|
||||||
<MenuItem className='mx_WidgetContextMenu_option' onClick={this.onReloadClicked} key='reload'>
|
|
||||||
{_t("Reload")}
|
|
||||||
</MenuItem>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.onSnapshotClicked) {
|
|
||||||
options.push(
|
|
||||||
<MenuItem className='mx_WidgetContextMenu_option' onClick={this.onSnapshotClicked} key='snap'>
|
|
||||||
{_t("Take picture")}
|
|
||||||
</MenuItem>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.onDeleteClicked) {
|
|
||||||
options.push(
|
|
||||||
<MenuItem className='mx_WidgetContextMenu_option' onClick={this.onDeleteClicked} key='delete'>
|
|
||||||
{_t("Remove for everyone")}
|
|
||||||
</MenuItem>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push this last so it appears last. It's always present.
|
|
||||||
options.push(
|
|
||||||
<MenuItem className='mx_WidgetContextMenu_option' onClick={this.onRevokeClicked} key='revoke'>
|
|
||||||
{_t("Remove for me")}
|
|
||||||
</MenuItem>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Put separators between the options
|
|
||||||
if (options.length > 1) {
|
|
||||||
const length = options.length;
|
|
||||||
for (let i = 0; i < length - 1; i++) {
|
|
||||||
const sep = <hr key={i} className="mx_WidgetContextMenu_separator" />;
|
|
||||||
|
|
||||||
// Insert backwards so the insertions don't affect our math on where to place them.
|
|
||||||
// We also use our cached length to avoid worrying about options.length changing
|
|
||||||
options.splice(length - 1 - i, 0, sep);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className="mx_WidgetContextMenu">{options}</div>;
|
|
||||||
}
|
|
||||||
}
|
|
177
src/components/views/context_menus/WidgetContextMenu.tsx
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 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, {useContext} from "react";
|
||||||
|
import {MatrixCapabilities} from "matrix-widget-api";
|
||||||
|
|
||||||
|
import IconizedContextMenu, {IconizedContextMenuOption, IconizedContextMenuOptionList} from "./IconizedContextMenu";
|
||||||
|
import {ChevronFace} from "../../structures/ContextMenu";
|
||||||
|
import {_t} from "../../../languageHandler";
|
||||||
|
import WidgetStore, {IApp} from "../../../stores/WidgetStore";
|
||||||
|
import WidgetUtils from "../../../utils/WidgetUtils";
|
||||||
|
import {WidgetMessagingStore} from "../../../stores/widgets/WidgetMessagingStore";
|
||||||
|
import RoomContext from "../../../contexts/RoomContext";
|
||||||
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
import {SettingLevel} from "../../../settings/SettingLevel";
|
||||||
|
import Modal from "../../../Modal";
|
||||||
|
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||||
|
import {WidgetType} from "../../../widgets/WidgetType";
|
||||||
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
|
||||||
|
interface IProps extends React.ComponentProps<typeof IconizedContextMenu> {
|
||||||
|
app: IApp;
|
||||||
|
userWidget?: boolean;
|
||||||
|
showUnpin?: boolean;
|
||||||
|
// override delete handler
|
||||||
|
onDeleteClick?(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WidgetContextMenu: React.FC<IProps> = ({
|
||||||
|
onFinished,
|
||||||
|
app,
|
||||||
|
userWidget,
|
||||||
|
onDeleteClick,
|
||||||
|
showUnpin,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const cli = useContext(MatrixClientContext);
|
||||||
|
const {room, roomId} = useContext(RoomContext);
|
||||||
|
|
||||||
|
const widgetMessaging = WidgetMessagingStore.instance.getMessagingForId(app.id);
|
||||||
|
const canModify = userWidget || WidgetUtils.canUserModifyWidgets(roomId);
|
||||||
|
|
||||||
|
let unpinButton;
|
||||||
|
if (showUnpin) {
|
||||||
|
const onUnpinClick = () => {
|
||||||
|
WidgetStore.instance.unpinWidget(app.id);
|
||||||
|
onFinished();
|
||||||
|
};
|
||||||
|
|
||||||
|
unpinButton = <IconizedContextMenuOption onClick={onUnpinClick} label={_t("Unpin")} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
let editButton;
|
||||||
|
if (canModify && WidgetUtils.isManagedByManager(app)) {
|
||||||
|
const onEditClick = () => {
|
||||||
|
WidgetUtils.editWidget(room, app);
|
||||||
|
onFinished();
|
||||||
|
};
|
||||||
|
|
||||||
|
editButton = <IconizedContextMenuOption onClick={onEditClick} label={_t("Edit")} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
let snapshotButton;
|
||||||
|
if (widgetMessaging?.hasCapability(MatrixCapabilities.Screenshots)) {
|
||||||
|
const onSnapshotClick = () => {
|
||||||
|
widgetMessaging?.takeScreenshot().then(data => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'picture_snapshot',
|
||||||
|
file: data.screenshot,
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
console.error("Failed to take screenshot: ", err);
|
||||||
|
});
|
||||||
|
onFinished();
|
||||||
|
};
|
||||||
|
|
||||||
|
snapshotButton = <IconizedContextMenuOption onClick={onSnapshotClick} label={_t("Take a picture")} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
let deleteButton;
|
||||||
|
if (onDeleteClick || canModify) {
|
||||||
|
const onDeleteClickDefault = () => {
|
||||||
|
// Show delete confirmation dialog
|
||||||
|
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
|
||||||
|
title: _t("Delete Widget"),
|
||||||
|
description: _t(
|
||||||
|
"Deleting a widget removes it for all users in this room." +
|
||||||
|
" Are you sure you want to delete this widget?"),
|
||||||
|
button: _t("Delete widget"),
|
||||||
|
onFinished: (confirmed) => {
|
||||||
|
if (!confirmed) return;
|
||||||
|
WidgetUtils.setRoomWidget(roomId, app.id);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
onFinished();
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteButton = <IconizedContextMenuOption
|
||||||
|
onClick={onDeleteClick || onDeleteClickDefault}
|
||||||
|
label={userWidget ? _t("Remove") : _t("Remove for everyone")}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isAllowedWidget = SettingsStore.getValue("allowedWidgets", roomId)[app.eventId];
|
||||||
|
if (isAllowedWidget === undefined) {
|
||||||
|
isAllowedWidget = app.creatorUserId === cli.getUserId();
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLocalWidget = WidgetType.JITSI.matches(app.type);
|
||||||
|
let revokeButton;
|
||||||
|
if (!userWidget && !isLocalWidget && isAllowedWidget) {
|
||||||
|
const onRevokeClick = () => {
|
||||||
|
console.info("Revoking permission for widget to load: " + app.eventId);
|
||||||
|
const current = SettingsStore.getValue("allowedWidgets", roomId);
|
||||||
|
current[app.eventId] = false;
|
||||||
|
SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
// We don't really need to do anything about this - the user will just hit the button again.
|
||||||
|
});
|
||||||
|
onFinished();
|
||||||
|
};
|
||||||
|
|
||||||
|
revokeButton = <IconizedContextMenuOption onClick={onRevokeClick} label={_t("Revoke permissions")} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pinnedWidgets = WidgetStore.instance.getPinnedApps(roomId);
|
||||||
|
const widgetIndex = pinnedWidgets.findIndex(widget => widget.id === app.id);
|
||||||
|
|
||||||
|
let moveLeftButton;
|
||||||
|
if (showUnpin && widgetIndex > 0) {
|
||||||
|
const onClick = () => {
|
||||||
|
WidgetStore.instance.movePinnedWidget(app.id, -1);
|
||||||
|
onFinished();
|
||||||
|
};
|
||||||
|
|
||||||
|
moveLeftButton = <IconizedContextMenuOption onClick={onClick} label={_t("Move left")} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
let moveRightButton;
|
||||||
|
if (showUnpin && widgetIndex < pinnedWidgets.length - 1) {
|
||||||
|
const onClick = () => {
|
||||||
|
WidgetStore.instance.movePinnedWidget(app.id, 1);
|
||||||
|
onFinished();
|
||||||
|
};
|
||||||
|
|
||||||
|
moveRightButton = <IconizedContextMenuOption onClick={onClick} label={_t("Move right")} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <IconizedContextMenu {...props} chevronFace={ChevronFace.None} onFinished={onFinished}>
|
||||||
|
<IconizedContextMenuOptionList>
|
||||||
|
{ editButton }
|
||||||
|
{ revokeButton }
|
||||||
|
{ deleteButton }
|
||||||
|
{ snapshotButton }
|
||||||
|
{ moveLeftButton }
|
||||||
|
{ moveRightButton }
|
||||||
|
{ unpinButton }
|
||||||
|
</IconizedContextMenuOptionList>
|
||||||
|
</IconizedContextMenu>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WidgetContextMenu;
|
||||||
|
|
|
@ -26,6 +26,7 @@ interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
|
||||||
tooltip?: React.ReactNode;
|
tooltip?: React.ReactNode;
|
||||||
tooltipClassName?: string;
|
tooltipClassName?: string;
|
||||||
forceHide?: boolean;
|
forceHide?: boolean;
|
||||||
|
yOffset?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -63,12 +64,13 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const {title, tooltip, children, tooltipClassName, forceHide, ...props} = this.props;
|
const {title, tooltip, children, tooltipClassName, forceHide, yOffset, ...props} = this.props;
|
||||||
|
|
||||||
const tip = this.state.hover ? <Tooltip
|
const tip = this.state.hover ? <Tooltip
|
||||||
className="mx_AccessibleTooltipButton_container"
|
className="mx_AccessibleTooltipButton_container"
|
||||||
tooltipClassName={classNames("mx_AccessibleTooltipButton_tooltip", tooltipClassName)}
|
tooltipClassName={classNames("mx_AccessibleTooltipButton_tooltip", tooltipClassName)}
|
||||||
label={tooltip || title}
|
label={tooltip || title}
|
||||||
|
yOffset={yOffset}
|
||||||
/> : <div />;
|
/> : <div />;
|
||||||
return (
|
return (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
|
|
|
@ -22,56 +22,54 @@ import React, {createRef} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||||
import AccessibleButton from './AccessibleButton';
|
import AccessibleButton from './AccessibleButton';
|
||||||
import Modal from '../../../Modal';
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import AppPermission from './AppPermission';
|
import AppPermission from './AppPermission';
|
||||||
import AppWarning from './AppWarning';
|
import AppWarning from './AppWarning';
|
||||||
import Spinner from './Spinner';
|
import Spinner from './Spinner';
|
||||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import {aboveLeftOf, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
|
import {aboveLeftOf, ContextMenuButton} from "../../structures/ContextMenu";
|
||||||
import PersistedElement from "./PersistedElement";
|
import PersistedElement, {getPersistKey} from "./PersistedElement";
|
||||||
import {WidgetType} from "../../../widgets/WidgetType";
|
import {WidgetType} from "../../../widgets/WidgetType";
|
||||||
import {SettingLevel} from "../../../settings/SettingLevel";
|
import {SettingLevel} from "../../../settings/SettingLevel";
|
||||||
import WidgetStore from "../../../stores/WidgetStore";
|
|
||||||
import {Action} from "../../../dispatcher/actions";
|
|
||||||
import {StopGapWidget} from "../../../stores/widgets/StopGapWidget";
|
import {StopGapWidget} from "../../../stores/widgets/StopGapWidget";
|
||||||
import {ElementWidgetActions} from "../../../stores/widgets/ElementWidgetActions";
|
import {ElementWidgetActions} from "../../../stores/widgets/ElementWidgetActions";
|
||||||
import {MatrixCapabilities} from "matrix-widget-api";
|
import {MatrixCapabilities} from "matrix-widget-api";
|
||||||
|
import RoomWidgetContextMenu from "../context_menus/WidgetContextMenu";
|
||||||
|
import WidgetAvatar from "../avatars/WidgetAvatar";
|
||||||
|
|
||||||
export default class AppTile extends React.Component {
|
export default class AppTile extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
// The key used for PersistedElement
|
// The key used for PersistedElement
|
||||||
this._persistKey = 'widget_' + this.props.app.id;
|
this._persistKey = getPersistKey(this.props.app.id);
|
||||||
this._sgWidget = new StopGapWidget(this.props);
|
this._sgWidget = new StopGapWidget(this.props);
|
||||||
this._sgWidget.on("preparing", this._onWidgetPrepared);
|
this._sgWidget.on("preparing", this._onWidgetPrepared);
|
||||||
this._sgWidget.on("ready", this._onWidgetReady);
|
this._sgWidget.on("ready", this._onWidgetReady);
|
||||||
this.iframe = null; // ref to the iframe (callback style)
|
this.iframe = null; // ref to the iframe (callback style)
|
||||||
|
|
||||||
this.state = this._getNewState(props);
|
this.state = this._getNewState(props);
|
||||||
|
|
||||||
this._onAction = this._onAction.bind(this);
|
|
||||||
this._onEditClick = this._onEditClick.bind(this);
|
|
||||||
this._onDeleteClick = this._onDeleteClick.bind(this);
|
|
||||||
this._onRevokeClicked = this._onRevokeClicked.bind(this);
|
|
||||||
this._onSnapshotClick = this._onSnapshotClick.bind(this);
|
|
||||||
this.onClickMenuBar = this.onClickMenuBar.bind(this);
|
|
||||||
this._onMinimiseClick = this._onMinimiseClick.bind(this);
|
|
||||||
this._grantWidgetPermission = this._grantWidgetPermission.bind(this);
|
|
||||||
this._revokeWidgetPermission = this._revokeWidgetPermission.bind(this);
|
|
||||||
this._onPopoutWidgetClick = this._onPopoutWidgetClick.bind(this);
|
|
||||||
this._onReloadWidgetClick = this._onReloadWidgetClick.bind(this);
|
|
||||||
|
|
||||||
this._contextMenuButton = createRef();
|
this._contextMenuButton = createRef();
|
||||||
this._menu_bar = createRef();
|
|
||||||
|
this._allowedWidgetsWatchRef = SettingsStore.watchSetting("allowedWidgets", null, this.onAllowedWidgetsChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is a function to make the impact of calling SettingsStore slightly less
|
||||||
|
hasPermissionToLoad = (props) => {
|
||||||
|
if (this._usingLocalWidget()) return true;
|
||||||
|
if (!props.room) return true; // user widgets always have permissions
|
||||||
|
|
||||||
|
const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", props.room.roomId);
|
||||||
|
if (currentlyAllowedWidgets[props.app.eventId] === undefined) {
|
||||||
|
return props.userId === props.creatorUserId;
|
||||||
|
}
|
||||||
|
return !!currentlyAllowedWidgets[props.app.eventId];
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set initial component state when the App wUrl (widget URL) is being updated.
|
* Set initial component state when the App wUrl (widget URL) is being updated.
|
||||||
* Component props *must* be passed (rather than relying on this.props).
|
* Component props *must* be passed (rather than relying on this.props).
|
||||||
|
@ -79,29 +77,32 @@ export default class AppTile extends React.Component {
|
||||||
* @return {Object} Updated component state to be set with setState
|
* @return {Object} Updated component state to be set with setState
|
||||||
*/
|
*/
|
||||||
_getNewState(newProps) {
|
_getNewState(newProps) {
|
||||||
// This is a function to make the impact of calling SettingsStore slightly less
|
|
||||||
const hasPermissionToLoad = () => {
|
|
||||||
if (this._usingLocalWidget()) return true;
|
|
||||||
|
|
||||||
if (!newProps.room) return true; // user widgets always have permissions
|
|
||||||
const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", newProps.room.roomId);
|
|
||||||
return !!currentlyAllowedWidgets[newProps.app.eventId];
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
initialising: true, // True while we are mangling the widget URL
|
initialising: true, // True while we are mangling the widget URL
|
||||||
// True while the iframe content is loading
|
// True while the iframe content is loading
|
||||||
loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey),
|
loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey),
|
||||||
// Assume that widget has permission to load if we are the user who
|
// Assume that widget has permission to load if we are the user who
|
||||||
// added it to the room, or if explicitly granted by the user
|
// added it to the room, or if explicitly granted by the user
|
||||||
hasPermissionToLoad: newProps.userId === newProps.creatorUserId || hasPermissionToLoad(),
|
hasPermissionToLoad: this.hasPermissionToLoad(newProps),
|
||||||
error: null,
|
error: null,
|
||||||
deleting: false,
|
|
||||||
widgetPageTitle: newProps.widgetPageTitle,
|
widgetPageTitle: newProps.widgetPageTitle,
|
||||||
menuDisplayed: false,
|
menuDisplayed: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onAllowedWidgetsChange = () => {
|
||||||
|
const hasPermissionToLoad = this.hasPermissionToLoad(this.props);
|
||||||
|
|
||||||
|
if (this.state.hasPermissionToLoad && !hasPermissionToLoad) {
|
||||||
|
// Force the widget to be non-persistent (able to be deleted/forgotten)
|
||||||
|
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
||||||
|
PersistedElement.destroyElement(this._persistKey);
|
||||||
|
this._sgWidget.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ hasPermissionToLoad });
|
||||||
|
};
|
||||||
|
|
||||||
isMixedContent() {
|
isMixedContent() {
|
||||||
const parentContentProtocol = window.location.protocol;
|
const parentContentProtocol = window.location.protocol;
|
||||||
const u = url.parse(this.props.app.url);
|
const u = url.parse(this.props.app.url);
|
||||||
|
@ -116,7 +117,7 @@ export default class AppTile extends React.Component {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
// Only fetch IM token on mount if we're showing and have permission to load
|
// Only fetch IM token on mount if we're showing and have permission to load
|
||||||
if (this.props.show && this.state.hasPermissionToLoad) {
|
if (this.state.hasPermissionToLoad) {
|
||||||
this._startWidget();
|
this._startWidget();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,6 +138,8 @@ export default class AppTile extends React.Component {
|
||||||
if (this._sgWidget) {
|
if (this._sgWidget) {
|
||||||
this._sgWidget.stop();
|
this._sgWidget.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsStore.unwatchSetting(this._allowedWidgetsWatchRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
_resetWidget(newProps) {
|
_resetWidget(newProps) {
|
||||||
|
@ -168,21 +171,8 @@ export default class AppTile extends React.Component {
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
|
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
|
||||||
if (nextProps.app.url !== this.props.app.url) {
|
if (nextProps.app.url !== this.props.app.url) {
|
||||||
this._getNewState(nextProps);
|
this._getNewState(nextProps);
|
||||||
if (this.props.show && this.state.hasPermissionToLoad) {
|
|
||||||
this._resetWidget(nextProps);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextProps.show && !this.props.show) {
|
|
||||||
// We assume that persisted widgets are loaded and don't need a spinner.
|
|
||||||
if (this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey)) {
|
|
||||||
this.setState({
|
|
||||||
loading: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Start the widget now that we're showing if we already have permission to load
|
|
||||||
if (this.state.hasPermissionToLoad) {
|
if (this.state.hasPermissionToLoad) {
|
||||||
this._startWidget();
|
this._resetWidget(nextProps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,35 +183,6 @@ export default class AppTile extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_canUserModify() {
|
|
||||||
// User widgets should always be modifiable by their creator
|
|
||||||
if (this.props.userWidget && MatrixClientPeg.get().credentials.userId === this.props.creatorUserId) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Check if the current user can modify widgets in the current room
|
|
||||||
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onEditClick() {
|
|
||||||
console.log("Edit widget ID ", this.props.app.id);
|
|
||||||
if (this.props.onEditClick) {
|
|
||||||
this.props.onEditClick();
|
|
||||||
} else {
|
|
||||||
WidgetUtils.editWidget(this.props.room, this.props.app);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_onSnapshotClick() {
|
|
||||||
this._sgWidget.widgetApi.takeScreenshot().then(data => {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'picture_snapshot',
|
|
||||||
file: data.screenshot,
|
|
||||||
});
|
|
||||||
}).catch(err => {
|
|
||||||
console.error("Failed to take screenshot: ", err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ends all widget interaction, such as cancelling calls and disabling webcams.
|
* Ends all widget interaction, such as cancelling calls and disabling webcams.
|
||||||
* @private
|
* @private
|
||||||
|
@ -251,57 +212,6 @@ export default class AppTile extends React.Component {
|
||||||
this._sgWidget.stop({forceDestroy: true});
|
this._sgWidget.stop({forceDestroy: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* If user has permission to modify widgets, delete the widget,
|
|
||||||
* otherwise revoke access for the widget to load in the user's browser
|
|
||||||
*/
|
|
||||||
_onDeleteClick() {
|
|
||||||
if (this.props.onDeleteClick) {
|
|
||||||
this.props.onDeleteClick();
|
|
||||||
} else if (this._canUserModify()) {
|
|
||||||
// Show delete confirmation dialog
|
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
|
||||||
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
|
|
||||||
title: _t("Delete Widget"),
|
|
||||||
description: _t(
|
|
||||||
"Deleting a widget removes it for all users in this room." +
|
|
||||||
" Are you sure you want to delete this widget?"),
|
|
||||||
button: _t("Delete widget"),
|
|
||||||
onFinished: (confirmed) => {
|
|
||||||
if (!confirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.setState({deleting: true});
|
|
||||||
|
|
||||||
this._endWidgetActions().then(() => {
|
|
||||||
return WidgetUtils.setRoomWidget(
|
|
||||||
this.props.room.roomId,
|
|
||||||
this.props.app.id,
|
|
||||||
);
|
|
||||||
}).catch((e) => {
|
|
||||||
console.error('Failed to delete widget', e);
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
|
|
||||||
Modal.createTrackedDialog('Failed to remove widget', '', ErrorDialog, {
|
|
||||||
title: _t('Failed to remove widget'),
|
|
||||||
description: _t('An error ocurred whilst trying to remove the widget from the room'),
|
|
||||||
});
|
|
||||||
}).finally(() => {
|
|
||||||
this.setState({deleting: false});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_onUnpinClicked = () => {
|
|
||||||
WidgetStore.instance.unpinWidget(this.props.app.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onRevokeClicked() {
|
|
||||||
console.info("Revoke widget permissions - %s", this.props.app.id);
|
|
||||||
this._revokeWidgetPermission();
|
|
||||||
}
|
|
||||||
|
|
||||||
_onWidgetPrepared = () => {
|
_onWidgetPrepared = () => {
|
||||||
this.setState({loading: false});
|
this.setState({loading: false});
|
||||||
};
|
};
|
||||||
|
@ -312,7 +222,7 @@ export default class AppTile extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onAction(payload) {
|
_onAction = payload => {
|
||||||
if (payload.widgetId === this.props.app.id) {
|
if (payload.widgetId === this.props.app.id) {
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'm.sticker':
|
case 'm.sticker':
|
||||||
|
@ -322,19 +232,11 @@ export default class AppTile extends React.Component {
|
||||||
console.warn('Ignoring sticker message. Invalid capability');
|
console.warn('Ignoring sticker message. Invalid capability');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Action.AppTileDelete:
|
|
||||||
this._onDeleteClick();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Action.AppTileRevoke:
|
|
||||||
this._onRevokeClicked();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
_grantWidgetPermission() {
|
_grantWidgetPermission = () => {
|
||||||
const roomId = this.props.room.roomId;
|
const roomId = this.props.room.roomId;
|
||||||
console.info("Granting permission for widget to load: " + this.props.app.eventId);
|
console.info("Granting permission for widget to load: " + this.props.app.eventId);
|
||||||
const current = SettingsStore.getValue("allowedWidgets", roomId);
|
const current = SettingsStore.getValue("allowedWidgets", roomId);
|
||||||
|
@ -348,26 +250,7 @@ export default class AppTile extends React.Component {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
// We don't really need to do anything about this - the user will just hit the button again.
|
// We don't really need to do anything about this - the user will just hit the button again.
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
_revokeWidgetPermission() {
|
|
||||||
const roomId = this.props.room.roomId;
|
|
||||||
console.info("Revoking permission for widget to load: " + this.props.app.eventId);
|
|
||||||
const current = SettingsStore.getValue("allowedWidgets", roomId);
|
|
||||||
current[this.props.app.eventId] = false;
|
|
||||||
SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).then(() => {
|
|
||||||
this.setState({hasPermissionToLoad: false});
|
|
||||||
|
|
||||||
// Force the widget to be non-persistent (able to be deleted/forgotten)
|
|
||||||
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
|
||||||
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
|
||||||
PersistedElement.destroyElement(this._persistKey);
|
|
||||||
this._sgWidget.stop();
|
|
||||||
}).catch(err => {
|
|
||||||
console.error(err);
|
|
||||||
// We don't really need to do anything about this - the user will just hit the button again.
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
formatAppTileName() {
|
formatAppTileName() {
|
||||||
let appTileName = "No name";
|
let appTileName = "No name";
|
||||||
|
@ -377,32 +260,6 @@ export default class AppTile extends React.Component {
|
||||||
return appTileName;
|
return appTileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickMenuBar(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
|
|
||||||
// Ignore clicks on menu bar children
|
|
||||||
if (ev.target !== this._menu_bar.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggle the view state of the apps drawer
|
|
||||||
if (this.props.userWidget) {
|
|
||||||
this._onMinimiseClick();
|
|
||||||
} else {
|
|
||||||
if (this.props.show) {
|
|
||||||
// if we were being shown, end the widget as we're about to be minimized.
|
|
||||||
this._endWidgetActions();
|
|
||||||
} else {
|
|
||||||
// restart the widget actions
|
|
||||||
this._resetWidget(this.props);
|
|
||||||
}
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'appsDrawer',
|
|
||||||
show: !this.props.show,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether we're using a local version of the widget rather than loading the
|
* Whether we're using a local version of the widget rather than loading the
|
||||||
* actual widget URL
|
* actual widget URL
|
||||||
|
@ -422,22 +279,18 @@ export default class AppTile extends React.Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
|
<WidgetAvatar app={this.props.app} />
|
||||||
<b>{ name }</b>
|
<b>{ name }</b>
|
||||||
<span>{ title ? titleSpacer : '' }{ title }</span>
|
<span>{ title ? titleSpacer : '' }{ title }</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onMinimiseClick(e) {
|
// TODO replace with full screen interactions
|
||||||
if (this.props.onMinimiseClick) {
|
_onPopoutWidgetClick = () => {
|
||||||
this.props.onMinimiseClick();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_onPopoutWidgetClick() {
|
|
||||||
// Ensure Jitsi conferences are closed on pop-out, to not confuse the user to join them
|
// Ensure Jitsi conferences are closed on pop-out, to not confuse the user to join them
|
||||||
// twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop).
|
// twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop).
|
||||||
if (WidgetType.JITSI.matches(this.props.app.type) && this.props.show) {
|
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
||||||
this._endWidgetActions().then(() => {
|
this._endWidgetActions().then(() => {
|
||||||
if (this.iframe) {
|
if (this.iframe) {
|
||||||
// Reload iframe
|
// Reload iframe
|
||||||
|
@ -450,13 +303,7 @@ export default class AppTile extends React.Component {
|
||||||
// window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
|
// window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
|
||||||
Object.assign(document.createElement('a'),
|
Object.assign(document.createElement('a'),
|
||||||
{ target: '_blank', href: this._sgWidget.popoutUrl, rel: 'noreferrer noopener'}).click();
|
{ target: '_blank', href: this._sgWidget.popoutUrl, rel: 'noreferrer noopener'}).click();
|
||||||
}
|
};
|
||||||
|
|
||||||
_onReloadWidgetClick() {
|
|
||||||
// Reload iframe in this way to avoid cross-origin restrictions
|
|
||||||
// eslint-disable-next-line no-self-assign
|
|
||||||
this.iframe.src = this.iframe.src;
|
|
||||||
}
|
|
||||||
|
|
||||||
_onContextMenuClick = () => {
|
_onContextMenuClick = () => {
|
||||||
this.setState({ menuDisplayed: true });
|
this.setState({ menuDisplayed: true });
|
||||||
|
@ -469,11 +316,6 @@ export default class AppTile extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
let appTileBody;
|
let appTileBody;
|
||||||
|
|
||||||
// Don't render widget if it is in the process of being deleted
|
|
||||||
if (this.state.deleting) {
|
|
||||||
return <div />;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin
|
// Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin
|
||||||
// because that would allow the iframe to programmatically remove the sandbox attribute, but
|
// because that would allow the iframe to programmatically remove the sandbox attribute, but
|
||||||
// this would only be for content hosted on the same origin as the element client: anything
|
// this would only be for content hosted on the same origin as the element client: anything
|
||||||
|
@ -488,72 +330,67 @@ export default class AppTile extends React.Component {
|
||||||
|
|
||||||
const appTileBodyClass = 'mx_AppTileBody' + (this.props.miniMode ? '_mini ' : ' ');
|
const appTileBodyClass = 'mx_AppTileBody' + (this.props.miniMode ? '_mini ' : ' ');
|
||||||
|
|
||||||
if (this.props.show) {
|
const loadingElement = (
|
||||||
const loadingElement = (
|
<div className="mx_AppLoading_spinner_fadeIn">
|
||||||
<div className="mx_AppLoading_spinner_fadeIn">
|
<Spinner message={_t("Loading...")} />
|
||||||
<Spinner message={_t("Loading...")} />
|
</div>
|
||||||
|
);
|
||||||
|
if (!this.state.hasPermissionToLoad) {
|
||||||
|
// only possible for room widgets, can assert this.props.room here
|
||||||
|
const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
|
||||||
|
appTileBody = (
|
||||||
|
<div className={appTileBodyClass}>
|
||||||
|
<AppPermission
|
||||||
|
roomId={this.props.room.roomId}
|
||||||
|
creatorUserId={this.props.creatorUserId}
|
||||||
|
url={this._sgWidget.embedUrl}
|
||||||
|
isRoomEncrypted={isEncrypted}
|
||||||
|
onPermissionGranted={this._grantWidgetPermission}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
if (!this.state.hasPermissionToLoad) {
|
} else if (this.state.initialising) {
|
||||||
// only possible for room widgets, can assert this.props.room here
|
appTileBody = (
|
||||||
const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
|
<div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')}>
|
||||||
|
{ loadingElement }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (this.isMixedContent()) {
|
||||||
appTileBody = (
|
appTileBody = (
|
||||||
<div className={appTileBodyClass}>
|
<div className={appTileBodyClass}>
|
||||||
<AppPermission
|
<AppWarning errorMsg="Error - Mixed content" />
|
||||||
roomId={this.props.room.roomId}
|
|
||||||
creatorUserId={this.props.creatorUserId}
|
|
||||||
url={this._sgWidget.embedUrl}
|
|
||||||
isRoomEncrypted={isEncrypted}
|
|
||||||
onPermissionGranted={this._grantWidgetPermission}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (this.state.initialising) {
|
|
||||||
appTileBody = (
|
|
||||||
<div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')}>
|
|
||||||
{ loadingElement }
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
if (this.isMixedContent()) {
|
appTileBody = (
|
||||||
appTileBody = (
|
<div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')}>
|
||||||
<div className={appTileBodyClass}>
|
{ this.state.loading && loadingElement }
|
||||||
<AppWarning errorMsg="Error - Mixed content" />
|
<iframe
|
||||||
</div>
|
allow={iframeFeatures}
|
||||||
);
|
ref={this._iframeRefChange}
|
||||||
} else {
|
src={this._sgWidget.embedUrl}
|
||||||
appTileBody = (
|
allowFullScreen={true}
|
||||||
<div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')}>
|
sandbox={sandboxFlags}
|
||||||
{ this.state.loading && loadingElement }
|
/>
|
||||||
<iframe
|
</div>
|
||||||
allow={iframeFeatures}
|
);
|
||||||
ref={this._iframeRefChange}
|
// if the widget would be allowed to remain on screen, we must put it in
|
||||||
src={this._sgWidget.embedUrl}
|
// a PersistedElement from the get-go, otherwise the iframe will be
|
||||||
allowFullScreen={true}
|
// re-mounted later when we do.
|
||||||
sandbox={sandboxFlags}
|
if (this.props.whitelistCapabilities.includes('m.always_on_screen')) {
|
||||||
/>
|
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
||||||
</div>
|
// Also wrap the PersistedElement in a div to fix the height, otherwise
|
||||||
);
|
// AppTile's border is in the wrong place
|
||||||
// if the widget would be allowed to remain on screen, we must put it in
|
appTileBody = <div className="mx_AppTile_persistedWrapper">
|
||||||
// a PersistedElement from the get-go, otherwise the iframe will be
|
<PersistedElement persistKey={this._persistKey}>
|
||||||
// re-mounted later when we do.
|
{appTileBody}
|
||||||
if (this.props.whitelistCapabilities.includes('m.always_on_screen')) {
|
</PersistedElement>
|
||||||
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
</div>;
|
||||||
// Also wrap the PersistedElement in a div to fix the height, otherwise
|
|
||||||
// AppTile's border is in the wrong place
|
|
||||||
appTileBody = <div className="mx_AppTile_persistedWrapper">
|
|
||||||
<PersistedElement persistKey={this._persistKey}>
|
|
||||||
{appTileBody}
|
|
||||||
</PersistedElement>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const showMinimiseButton = this.props.showMinimise && this.props.show;
|
|
||||||
const showMaximiseButton = this.props.showMinimise && !this.props.show;
|
|
||||||
|
|
||||||
let appTileClasses;
|
let appTileClasses;
|
||||||
if (this.props.miniMode) {
|
if (this.props.miniMode) {
|
||||||
appTileClasses = {mx_AppTile_mini: true};
|
appTileClasses = {mx_AppTile_mini: true};
|
||||||
|
@ -562,73 +399,37 @@ export default class AppTile extends React.Component {
|
||||||
} else {
|
} else {
|
||||||
appTileClasses = {mx_AppTile: true};
|
appTileClasses = {mx_AppTile: true};
|
||||||
}
|
}
|
||||||
appTileClasses.mx_AppTile_minimised = !this.props.show;
|
|
||||||
appTileClasses = classNames(appTileClasses);
|
appTileClasses = classNames(appTileClasses);
|
||||||
|
|
||||||
const menuBarClasses = classNames({
|
|
||||||
mx_AppTileMenuBar: true,
|
|
||||||
mx_AppTileMenuBar_expanded: this.props.show,
|
|
||||||
});
|
|
||||||
|
|
||||||
let contextMenu;
|
let contextMenu;
|
||||||
if (this.state.menuDisplayed) {
|
if (this.state.menuDisplayed) {
|
||||||
const elementRect = this._contextMenuButton.current.getBoundingClientRect();
|
|
||||||
|
|
||||||
const canUserModify = this._canUserModify();
|
|
||||||
const showEditButton = Boolean(this._sgWidget.isManagedByManager && canUserModify);
|
|
||||||
const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify;
|
|
||||||
const showPictureSnapshotButton = this.props.show && this._sgWidget.widgetApi &&
|
|
||||||
this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.Screenshots);
|
|
||||||
|
|
||||||
const WidgetContextMenu = sdk.getComponent('views.context_menus.WidgetContextMenu');
|
|
||||||
contextMenu = (
|
contextMenu = (
|
||||||
<ContextMenu {...aboveLeftOf(elementRect, null)} onFinished={this._closeContextMenu}>
|
<RoomWidgetContextMenu
|
||||||
<WidgetContextMenu
|
{...aboveLeftOf(this._contextMenuButton.current.getBoundingClientRect(), null)}
|
||||||
onUnpinClicked={
|
app={this.props.app}
|
||||||
ActiveWidgetStore.getWidgetPersistence(this.props.app.id) ? null : this._onUnpinClicked
|
onFinished={this._closeContextMenu}
|
||||||
}
|
showUnpin={!this.props.userWidget}
|
||||||
onRevokeClicked={this._onRevokeClicked}
|
userWidget={this.props.userWidget}
|
||||||
onEditClicked={showEditButton ? this._onEditClick : undefined}
|
/>
|
||||||
onDeleteClicked={showDeleteButton ? this._onDeleteClick : undefined}
|
|
||||||
onSnapshotClicked={showPictureSnapshotButton ? this._onSnapshotClick : undefined}
|
|
||||||
onReloadClicked={this.props.showReload ? this._onReloadWidgetClick : undefined}
|
|
||||||
onFinished={this._closeContextMenu}
|
|
||||||
/>
|
|
||||||
</ContextMenu>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <React.Fragment>
|
return <React.Fragment>
|
||||||
<div className={appTileClasses} id={this.props.app.id}>
|
<div className={appTileClasses} id={this.props.app.id}>
|
||||||
{ this.props.showMenubar &&
|
{ this.props.showMenubar &&
|
||||||
<div ref={this._menu_bar} className={menuBarClasses} onClick={this.onClickMenuBar}>
|
<div className="mx_AppTileMenuBar">
|
||||||
<span className="mx_AppTileMenuBarTitle" style={{pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false)}}>
|
<span className="mx_AppTileMenuBarTitle" style={{pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false)}}>
|
||||||
{ /* Minimise widget */ }
|
|
||||||
{ showMinimiseButton && <AccessibleButton
|
|
||||||
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_minimise"
|
|
||||||
title={_t('Minimize widget')}
|
|
||||||
onClick={this._onMinimiseClick}
|
|
||||||
/> }
|
|
||||||
{ /* Maximise widget */ }
|
|
||||||
{ showMaximiseButton && <AccessibleButton
|
|
||||||
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_maximise"
|
|
||||||
title={_t('Maximize widget')}
|
|
||||||
onClick={this._onMinimiseClick}
|
|
||||||
/> }
|
|
||||||
{ /* Title */ }
|
|
||||||
{ this.props.showTitle && this._getTileTitle() }
|
{ this.props.showTitle && this._getTileTitle() }
|
||||||
</span>
|
</span>
|
||||||
<span className="mx_AppTileMenuBarWidgets">
|
<span className="mx_AppTileMenuBarWidgets">
|
||||||
{ /* Popout widget */ }
|
|
||||||
{ this.props.showPopout && <AccessibleButton
|
{ this.props.showPopout && <AccessibleButton
|
||||||
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_popout"
|
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_popout"
|
||||||
title={_t('Popout widget')}
|
title={_t('Popout widget')}
|
||||||
onClick={this._onPopoutWidgetClick}
|
onClick={this._onPopoutWidgetClick}
|
||||||
/> }
|
/> }
|
||||||
{ /* Context menu */ }
|
|
||||||
{ <ContextMenuButton
|
{ <ContextMenuButton
|
||||||
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_menu"
|
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_menu"
|
||||||
label={_t('More options')}
|
label={_t("Options")}
|
||||||
isExpanded={this.state.menuDisplayed}
|
isExpanded={this.state.menuDisplayed}
|
||||||
inputRef={this._contextMenuButton}
|
inputRef={this._contextMenuButton}
|
||||||
onClick={this._onContextMenuClick}
|
onClick={this._onContextMenuClick}
|
||||||
|
@ -661,8 +462,6 @@ AppTile.propTypes = {
|
||||||
creatorUserId: PropTypes.string,
|
creatorUserId: PropTypes.string,
|
||||||
waitForIframeLoad: PropTypes.bool,
|
waitForIframeLoad: PropTypes.bool,
|
||||||
showMenubar: PropTypes.bool,
|
showMenubar: PropTypes.bool,
|
||||||
// Should the AppTile render itself
|
|
||||||
show: PropTypes.bool,
|
|
||||||
// Optional onEditClickHandler (overrides default behaviour)
|
// Optional onEditClickHandler (overrides default behaviour)
|
||||||
onEditClick: PropTypes.func,
|
onEditClick: PropTypes.func,
|
||||||
// Optional onDeleteClickHandler (overrides default behaviour)
|
// Optional onDeleteClickHandler (overrides default behaviour)
|
||||||
|
@ -671,19 +470,10 @@ AppTile.propTypes = {
|
||||||
onMinimiseClick: PropTypes.func,
|
onMinimiseClick: PropTypes.func,
|
||||||
// Optionally hide the tile title
|
// Optionally hide the tile title
|
||||||
showTitle: PropTypes.bool,
|
showTitle: PropTypes.bool,
|
||||||
// Optionally hide the tile minimise icon
|
|
||||||
showMinimise: PropTypes.bool,
|
|
||||||
// Optionally handle minimise button pointer events (default false)
|
// Optionally handle minimise button pointer events (default false)
|
||||||
handleMinimisePointerEvents: PropTypes.bool,
|
handleMinimisePointerEvents: PropTypes.bool,
|
||||||
// Optionally hide the delete icon
|
|
||||||
showDelete: PropTypes.bool,
|
|
||||||
// Optionally hide the popout widget icon
|
// Optionally hide the popout widget icon
|
||||||
showPopout: PropTypes.bool,
|
showPopout: PropTypes.bool,
|
||||||
// Optionally show the reload widget icon
|
|
||||||
// This is not currently intended for use with production widgets. However
|
|
||||||
// it can be useful when developing persistent widgets in order to avoid
|
|
||||||
// having to reload all of Element to get new widget content.
|
|
||||||
showReload: PropTypes.bool,
|
|
||||||
// Widget capabilities to allow by default (without user confirmation)
|
// Widget capabilities to allow by default (without user confirmation)
|
||||||
// NOTE -- Use with caution. This is intended to aid better integration / UX
|
// NOTE -- Use with caution. This is intended to aid better integration / UX
|
||||||
// basic widget capabilities, e.g. injecting sticker message events.
|
// basic widget capabilities, e.g. injecting sticker message events.
|
||||||
|
@ -696,10 +486,7 @@ AppTile.defaultProps = {
|
||||||
waitForIframeLoad: true,
|
waitForIframeLoad: true,
|
||||||
showMenubar: true,
|
showMenubar: true,
|
||||||
showTitle: true,
|
showTitle: true,
|
||||||
showMinimise: true,
|
|
||||||
showDelete: true,
|
|
||||||
showPopout: true,
|
showPopout: true,
|
||||||
showReload: false,
|
|
||||||
handleMinimisePointerEvents: false,
|
handleMinimisePointerEvents: false,
|
||||||
whitelistCapabilities: [],
|
whitelistCapabilities: [],
|
||||||
userWidget: false,
|
userWidget: false,
|
||||||
|
|
|
@ -173,3 +173,5 @@ export default class PersistedElement extends React.Component {
|
||||||
return <div ref={this.collectChildContainer} />;
|
return <div ref={this.collectChildContainer} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getPersistKey = (appId: string) => 'widget_' + appId;
|
||||||
|
|
|
@ -79,13 +79,10 @@ export default class PersistentApp extends React.Component {
|
||||||
fullWidth={true}
|
fullWidth={true}
|
||||||
room={persistentWidgetInRoom}
|
room={persistentWidgetInRoom}
|
||||||
userId={MatrixClientPeg.get().credentials.userId}
|
userId={MatrixClientPeg.get().credentials.userId}
|
||||||
show={true}
|
|
||||||
creatorUserId={app.creatorUserId}
|
creatorUserId={app.creatorUserId}
|
||||||
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
|
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
|
||||||
waitForIframeLoad={app.waitForIframeLoad}
|
waitForIframeLoad={app.waitForIframeLoad}
|
||||||
whitelistCapabilities={capWhitelist}
|
whitelistCapabilities={capWhitelist}
|
||||||
showDelete={false}
|
|
||||||
showMinimise={false}
|
|
||||||
miniMode={true}
|
miniMode={true}
|
||||||
showMenubar={false}
|
showMenubar={false}
|
||||||
/>;
|
/>;
|
||||||
|
|
|
@ -36,6 +36,7 @@ interface IProps {
|
||||||
// the react element to put into the tooltip
|
// the react element to put into the tooltip
|
||||||
label: React.ReactNode;
|
label: React.ReactNode;
|
||||||
forceOnRight?: boolean;
|
forceOnRight?: boolean;
|
||||||
|
yOffset?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Tooltip extends React.Component<IProps> {
|
export default class Tooltip extends React.Component<IProps> {
|
||||||
|
@ -46,6 +47,7 @@ export default class Tooltip extends React.Component<IProps> {
|
||||||
|
|
||||||
public static readonly defaultProps = {
|
public static readonly defaultProps = {
|
||||||
visible: true,
|
visible: true,
|
||||||
|
yOffset: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create a wrapper for the tooltip outside the parent and attach it to the body element
|
// Create a wrapper for the tooltip outside the parent and attach it to the body element
|
||||||
|
@ -82,9 +84,9 @@ export default class Tooltip extends React.Component<IProps> {
|
||||||
offset = Math.floor(parentBox.height - MIN_TOOLTIP_HEIGHT);
|
offset = Math.floor(parentBox.height - MIN_TOOLTIP_HEIGHT);
|
||||||
}
|
}
|
||||||
|
|
||||||
style.top = (parentBox.top - 2) + window.pageYOffset + offset;
|
style.top = (parentBox.top - 2 + this.props.yOffset) + window.pageYOffset + offset;
|
||||||
if (!this.props.forceOnRight && parentBox.right > window.innerWidth / 2) {
|
if (!this.props.forceOnRight && parentBox.right > window.innerWidth / 2) {
|
||||||
style.right = window.innerWidth - parentBox.right - window.pageXOffset - 8;
|
style.right = window.innerWidth - parentBox.right - window.pageXOffset - 16;
|
||||||
} else {
|
} else {
|
||||||
style.left = parentBox.right + window.pageXOffset + 6;
|
style.left = parentBox.right + window.pageXOffset + 6;
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
||||||
import React, {useCallback, useState, useEffect, useContext} from "react";
|
import React, {useCallback, useState, useEffect, useContext} from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
import {Room} from "matrix-js-sdk/src/models/room";
|
||||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
|
||||||
|
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import { useIsEncrypted } from '../../../hooks/useIsEncrypted';
|
import { useIsEncrypted } from '../../../hooks/useIsEncrypted';
|
||||||
|
@ -37,12 +36,14 @@ import WidgetUtils from "../../../utils/WidgetUtils";
|
||||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import TextWithTooltip from "../elements/TextWithTooltip";
|
import TextWithTooltip from "../elements/TextWithTooltip";
|
||||||
import BaseAvatar from "../avatars/BaseAvatar";
|
import WidgetAvatar from "../avatars/WidgetAvatar";
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import WidgetStore, {IApp} from "../../../stores/WidgetStore";
|
import WidgetStore, {IApp, MAX_PINNED} from "../../../stores/WidgetStore";
|
||||||
import { E2EStatus } from "../../../utils/ShieldUtils";
|
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||||
import RoomContext from "../../../contexts/RoomContext";
|
import RoomContext from "../../../contexts/RoomContext";
|
||||||
import {UIFeature} from "../../../settings/UIFeature";
|
import {UIFeature} from "../../../settings/UIFeature";
|
||||||
|
import {ChevronFace, ContextMenuTooltipButton, useContextMenu} from "../../structures/ContextMenu";
|
||||||
|
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -68,11 +69,11 @@ const Button: React.FC<IButtonProps> = ({ children, className, onClick }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useWidgets = (room: Room) => {
|
export const useWidgets = (room: Room) => {
|
||||||
const [apps, setApps] = useState<IApp[]>(WidgetStore.instance.getApps(room));
|
const [apps, setApps] = useState<IApp[]>(WidgetStore.instance.getApps(room.roomId));
|
||||||
|
|
||||||
const updateApps = useCallback(() => {
|
const updateApps = useCallback(() => {
|
||||||
// Copy the array so that we always trigger a re-render, as some updates mutate the array of apps/settings
|
// 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)]);
|
setApps([...WidgetStore.instance.getApps(room.roomId)]);
|
||||||
}, [room]);
|
}, [room]);
|
||||||
|
|
||||||
useEffect(updateApps, [room]);
|
useEffect(updateApps, [room]);
|
||||||
|
@ -82,8 +83,92 @@ export const useWidgets = (room: Room) => {
|
||||||
return apps;
|
return apps;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface IAppRowProps {
|
||||||
|
app: IApp;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppRow: React.FC<IAppRowProps> = ({ app }) => {
|
||||||
|
const name = WidgetUtils.getWidgetName(app);
|
||||||
|
const dataTitle = WidgetUtils.getWidgetDataTitle(app);
|
||||||
|
const subtitle = dataTitle && " - " + dataTitle;
|
||||||
|
|
||||||
|
const onOpenWidgetClick = () => {
|
||||||
|
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
||||||
|
action: Action.SetRightPanelPhase,
|
||||||
|
phase: RightPanelPhases.Widget,
|
||||||
|
refireParams: {
|
||||||
|
widgetId: app.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPinned = WidgetStore.instance.isPinned(app.id);
|
||||||
|
const togglePin = isPinned
|
||||||
|
? () => { WidgetStore.instance.unpinWidget(app.id); }
|
||||||
|
: () => { WidgetStore.instance.pinWidget(app.id); };
|
||||||
|
|
||||||
|
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
|
||||||
|
let contextMenu;
|
||||||
|
if (menuDisplayed) {
|
||||||
|
const rect = handle.current.getBoundingClientRect();
|
||||||
|
contextMenu = <WidgetContextMenu
|
||||||
|
chevronFace={ChevronFace.None}
|
||||||
|
right={window.innerWidth - rect.right}
|
||||||
|
bottom={window.innerHeight - rect.top}
|
||||||
|
onFinished={closeMenu}
|
||||||
|
app={app}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cannotPin = !isPinned && !WidgetStore.instance.canPin(app.id);
|
||||||
|
|
||||||
|
let pinTitle: string;
|
||||||
|
if (cannotPin) {
|
||||||
|
pinTitle = _t("You can only pin up to %(count)s widgets", { count: MAX_PINNED });
|
||||||
|
} else {
|
||||||
|
pinTitle = isPinned ? _t("Unpin") : _t("Pin");
|
||||||
|
}
|
||||||
|
|
||||||
|
const classes = classNames("mx_BaseCard_Button mx_RoomSummaryCard_Button", {
|
||||||
|
mx_RoomSummaryCard_Button_pinned: isPinned,
|
||||||
|
});
|
||||||
|
|
||||||
|
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}
|
||||||
|
yOffset={-48}
|
||||||
|
>
|
||||||
|
<WidgetAvatar app={app} />
|
||||||
|
<span>{name}</span>
|
||||||
|
{ subtitle }
|
||||||
|
</AccessibleTooltipButton>
|
||||||
|
|
||||||
|
<ContextMenuTooltipButton
|
||||||
|
className="mx_RoomSummaryCard_app_options"
|
||||||
|
isExpanded={menuDisplayed}
|
||||||
|
onClick={openMenu}
|
||||||
|
title={_t("Options")}
|
||||||
|
yOffset={-24}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className="mx_RoomSummaryCard_app_pinToggle"
|
||||||
|
onClick={togglePin}
|
||||||
|
title={pinTitle}
|
||||||
|
disabled={cannotPin}
|
||||||
|
yOffset={-24}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{ contextMenu }
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
const AppsSection: React.FC<IAppsSectionProps> = ({ room }) => {
|
const AppsSection: React.FC<IAppsSectionProps> = ({ room }) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
|
||||||
const apps = useWidgets(room);
|
const apps = useWidgets(room);
|
||||||
|
|
||||||
const onManageIntegrations = () => {
|
const onManageIntegrations = () => {
|
||||||
|
@ -100,65 +185,7 @@ const AppsSection: React.FC<IAppsSectionProps> = ({ room }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return <Group className="mx_RoomSummaryCard_appsGroup" title={_t("Widgets")}>
|
return <Group className="mx_RoomSummaryCard_appsGroup" title={_t("Widgets")}>
|
||||||
{ apps.map(app => {
|
{ apps.map(app => <AppRow key={app.id} app={app} />) }
|
||||||
const name = WidgetUtils.getWidgetName(app);
|
|
||||||
const dataTitle = WidgetUtils.getWidgetDataTitle(app);
|
|
||||||
const subtitle = dataTitle && " - " + dataTitle;
|
|
||||||
|
|
||||||
let iconUrls = [require("../../../../res/img/element-icons/room/default_app.svg")];
|
|
||||||
// heuristics for some better icons until Widgets support their own icons
|
|
||||||
if (app.type.includes("meeting") || app.type.includes("calendar")) {
|
|
||||||
iconUrls = [require("../../../../res/img/element-icons/room/default_cal.svg")];
|
|
||||||
} else if (app.type.includes("pad") || app.type.includes("doc") || app.type.includes("calc")) {
|
|
||||||
iconUrls = [require("../../../../res/img/element-icons/room/default_doc.svg")];
|
|
||||||
} else if (app.type.includes("clock")) {
|
|
||||||
iconUrls = [require("../../../../res/img/element-icons/room/default_clock.svg")];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (app.avatar_url) { // MSC2765
|
|
||||||
iconUrls.unshift(getHttpUriForMxc(cli.getHomeserverUrl(), app.avatar_url, 20, 20, "crop"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const isPinned = WidgetStore.instance.isPinned(app.id);
|
|
||||||
const classes = classNames("mx_RoomSummaryCard_icon_app", {
|
|
||||||
mx_RoomSummaryCard_icon_app_pinned: isPinned,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isPinned) {
|
|
||||||
const onClick = () => {
|
|
||||||
WidgetStore.instance.unpinWidget(app.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
return <AccessibleTooltipButton
|
|
||||||
key={app.id}
|
|
||||||
className={classNames("mx_BaseCard_Button mx_RoomSummaryCard_Button", classes)}
|
|
||||||
onClick={onClick}
|
|
||||||
title={_t("Unpin app")}
|
|
||||||
>
|
|
||||||
<BaseAvatar name={app.id} urls={iconUrls} width={20} height={20} />
|
|
||||||
<span>{name}</span>
|
|
||||||
{ subtitle }
|
|
||||||
</AccessibleTooltipButton>
|
|
||||||
}
|
|
||||||
|
|
||||||
const onOpenWidgetClick = () => {
|
|
||||||
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
|
||||||
action: Action.SetRightPanelPhase,
|
|
||||||
phase: RightPanelPhases.Widget,
|
|
||||||
refireParams: {
|
|
||||||
widgetId: app.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button key={app.id} className={classes} onClick={onOpenWidgetClick}>
|
|
||||||
<BaseAvatar name={app.id} urls={iconUrls} width={20} height={20} />
|
|
||||||
<span>{name}</span>
|
|
||||||
{ subtitle }
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}) }
|
|
||||||
|
|
||||||
<AccessibleButton kind="link" onClick={onManageIntegrations}>
|
<AccessibleButton kind="link" onClick={onManageIntegrations}>
|
||||||
{ apps.length > 0 ? _t("Edit widgets, bridges & bots") : _t("Add widgets, bridges & bots") }
|
{ apps.length > 0 ? _t("Edit widgets, bridges & bots") : _t("Add widgets, bridges & bots") }
|
||||||
|
|
|
@ -20,7 +20,6 @@ import {Room} from "matrix-js-sdk/src/models/room";
|
||||||
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 from "../../../utils/WidgetUtils";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
|
||||||
import AppTile from "../elements/AppTile";
|
import AppTile from "../elements/AppTile";
|
||||||
import {_t} from "../../../languageHandler";
|
import {_t} from "../../../languageHandler";
|
||||||
import {useWidgets} from "./RoomSummaryCard";
|
import {useWidgets} from "./RoomSummaryCard";
|
||||||
|
@ -30,16 +29,7 @@ import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPa
|
||||||
import {Action} from "../../../dispatcher/actions";
|
import {Action} from "../../../dispatcher/actions";
|
||||||
import WidgetStore from "../../../stores/WidgetStore";
|
import WidgetStore from "../../../stores/WidgetStore";
|
||||||
import {ChevronFace, ContextMenuButton, useContextMenu} from "../../structures/ContextMenu";
|
import {ChevronFace, ContextMenuButton, useContextMenu} from "../../structures/ContextMenu";
|
||||||
import IconizedContextMenu, {
|
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
|
||||||
IconizedContextMenuOption,
|
|
||||||
IconizedContextMenuOptionList,
|
|
||||||
} from "../context_menus/IconizedContextMenu";
|
|
||||||
import {AppTileActionPayload} from "../../../dispatcher/payloads/AppTileActionPayload";
|
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
|
||||||
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
|
|
||||||
import { MatrixCapabilities } from "matrix-widget-api";
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -69,111 +59,22 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
|
||||||
// Don't render anything as we are about to transition
|
// Don't render anything as we are about to transition
|
||||||
if (!app || isPinned) return null;
|
if (!app || isPinned) return null;
|
||||||
|
|
||||||
const header = <React.Fragment>
|
|
||||||
<h2>{ WidgetUtils.getWidgetName(app) }</h2>
|
|
||||||
</React.Fragment>;
|
|
||||||
|
|
||||||
const canModify = WidgetUtils.canUserModifyWidgets(room.roomId);
|
|
||||||
|
|
||||||
let contextMenu;
|
let contextMenu;
|
||||||
if (menuDisplayed) {
|
if (menuDisplayed) {
|
||||||
let snapshotButton;
|
|
||||||
const widgetMessaging = WidgetMessagingStore.instance.getMessagingForId(app.id);
|
|
||||||
if (widgetMessaging?.hasCapability(MatrixCapabilities.Screenshots)) {
|
|
||||||
const onSnapshotClick = () => {
|
|
||||||
widgetMessaging.takeScreenshot().then(data => {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'picture_snapshot',
|
|
||||||
file: data.screenshot,
|
|
||||||
});
|
|
||||||
}).catch(err => {
|
|
||||||
console.error("Failed to take screenshot: ", err);
|
|
||||||
});
|
|
||||||
closeMenu();
|
|
||||||
};
|
|
||||||
|
|
||||||
snapshotButton = <IconizedContextMenuOption onClick={onSnapshotClick} label={_t("Take a picture")} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
let deleteButton;
|
|
||||||
if (canModify) {
|
|
||||||
const onDeleteClick = () => {
|
|
||||||
defaultDispatcher.dispatch<AppTileActionPayload>({
|
|
||||||
action: Action.AppTileDelete,
|
|
||||||
widgetId: app.id,
|
|
||||||
});
|
|
||||||
closeMenu();
|
|
||||||
};
|
|
||||||
|
|
||||||
deleteButton = <IconizedContextMenuOption onClick={onDeleteClick} label={_t("Remove for everyone")} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const onRevokeClick = () => {
|
|
||||||
defaultDispatcher.dispatch<AppTileActionPayload>({
|
|
||||||
action: Action.AppTileRevoke,
|
|
||||||
widgetId: app.id,
|
|
||||||
});
|
|
||||||
closeMenu();
|
|
||||||
};
|
|
||||||
|
|
||||||
const rect = handle.current.getBoundingClientRect();
|
const rect = handle.current.getBoundingClientRect();
|
||||||
contextMenu = (
|
contextMenu = (
|
||||||
<IconizedContextMenu
|
<WidgetContextMenu
|
||||||
chevronFace={ChevronFace.None}
|
chevronFace={ChevronFace.None}
|
||||||
right={window.innerWidth - rect.right}
|
right={window.innerWidth - rect.right - 12}
|
||||||
bottom={window.innerHeight - rect.top}
|
top={rect.bottom + 12}
|
||||||
onFinished={closeMenu}
|
onFinished={closeMenu}
|
||||||
>
|
app={app}
|
||||||
<IconizedContextMenuOptionList>
|
/>
|
||||||
{ snapshotButton }
|
|
||||||
{ deleteButton }
|
|
||||||
<IconizedContextMenuOption onClick={onRevokeClick} label={_t("Remove for me")} />
|
|
||||||
</IconizedContextMenuOptionList>
|
|
||||||
</IconizedContextMenu>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const onPinClick = () => {
|
const header = <React.Fragment>
|
||||||
WidgetStore.instance.pinWidget(app.id);
|
<h2>{ WidgetUtils.getWidgetName(app) }</h2>
|
||||||
};
|
|
||||||
|
|
||||||
const onEditClick = () => {
|
|
||||||
WidgetUtils.editWidget(room, app);
|
|
||||||
};
|
|
||||||
|
|
||||||
let editButton;
|
|
||||||
if (canModify) {
|
|
||||||
editButton = <AccessibleButton kind="secondary" onClick={onEditClick}>
|
|
||||||
{ _t("Edit") }
|
|
||||||
</AccessibleButton>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pinButtonClasses = canModify ? "" : "mx_WidgetCard_widePinButton";
|
|
||||||
|
|
||||||
let pinButton;
|
|
||||||
if (WidgetStore.instance.canPin(app.id)) {
|
|
||||||
pinButton = <AccessibleButton
|
|
||||||
kind="secondary"
|
|
||||||
onClick={onPinClick}
|
|
||||||
className={pinButtonClasses}
|
|
||||||
>
|
|
||||||
{ _t("Pin to room") }
|
|
||||||
</AccessibleButton>;
|
|
||||||
} else {
|
|
||||||
pinButton = <AccessibleTooltipButton
|
|
||||||
title={_t("You can only pin 2 widgets at a time")}
|
|
||||||
tooltipClassName="mx_WidgetCard_maxPinnedTooltip"
|
|
||||||
kind="secondary"
|
|
||||||
className={pinButtonClasses}
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
{ _t("Pin to room") }
|
|
||||||
</AccessibleTooltipButton>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const footer = <React.Fragment>
|
|
||||||
{ editButton }
|
|
||||||
{ pinButton }
|
|
||||||
<ContextMenuButton
|
<ContextMenuButton
|
||||||
kind="secondary"
|
kind="secondary"
|
||||||
className="mx_WidgetCard_optionsButton"
|
className="mx_WidgetCard_optionsButton"
|
||||||
|
@ -182,16 +83,12 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
|
||||||
isExpanded={menuDisplayed}
|
isExpanded={menuDisplayed}
|
||||||
label={_t("Options")}
|
label={_t("Options")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{ contextMenu }
|
{ contextMenu }
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
|
|
||||||
return <BaseCard
|
return <BaseCard
|
||||||
header={header}
|
header={header}
|
||||||
footer={footer}
|
className="mx_WidgetCard"
|
||||||
className={classNames("mx_WidgetCard", {
|
|
||||||
mx_WidgetCard_noEdit: !canModify,
|
|
||||||
})}
|
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
previousPhase={RightPanelPhases.RoomSummary}
|
previousPhase={RightPanelPhases.RoomSummary}
|
||||||
withoutScrollContainer
|
withoutScrollContainer
|
||||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useState} from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {Resizable} from "re-resizable";
|
import {Resizable} from "re-resizable";
|
||||||
|
@ -24,15 +24,16 @@ import AppTile from '../elements/AppTile';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import * as ScalarMessaging from '../../../ScalarMessaging';
|
import * as ScalarMessaging from '../../../ScalarMessaging';
|
||||||
import { _t } from '../../../languageHandler';
|
|
||||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||||
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
|
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
|
||||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import {useLocalStorageState} from "../../../hooks/useLocalStorageState";
|
import {useLocalStorageState} from "../../../hooks/useLocalStorageState";
|
||||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||||
import WidgetStore from "../../../stores/WidgetStore";
|
import WidgetStore from "../../../stores/WidgetStore";
|
||||||
|
import ResizeHandle from "../elements/ResizeHandle";
|
||||||
|
import Resizer from "../../../resizer/resizer";
|
||||||
|
import PercentageDistributor from "../../../resizer/distributors/percentage";
|
||||||
|
|
||||||
export default class AppsDrawer extends React.Component {
|
export default class AppsDrawer extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -52,6 +53,11 @@ export default class AppsDrawer extends React.Component {
|
||||||
this.state = {
|
this.state = {
|
||||||
apps: this._getApps(),
|
apps: this._getApps(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this._resizeContainer = null;
|
||||||
|
this.resizer = this._createResizer();
|
||||||
|
|
||||||
|
this.props.resizeNotifier.on("isResizing", this.onIsResizing);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -64,6 +70,10 @@ export default class AppsDrawer extends React.Component {
|
||||||
ScalarMessaging.stopListening();
|
ScalarMessaging.stopListening();
|
||||||
WidgetStore.instance.off(this.props.room.roomId, this._updateApps);
|
WidgetStore.instance.off(this.props.room.roomId, this._updateApps);
|
||||||
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
|
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
|
||||||
|
if (this._resizeContainer) {
|
||||||
|
this.resizer.detach();
|
||||||
|
}
|
||||||
|
this.props.resizeNotifier.off("isResizing", this.onIsResizing);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
|
@ -73,6 +83,95 @@ export default class AppsDrawer extends React.Component {
|
||||||
this._updateApps();
|
this._updateApps();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onIsResizing = (resizing) => {
|
||||||
|
this.setState({ resizing });
|
||||||
|
if (!resizing) {
|
||||||
|
this._relaxResizer();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_createResizer() {
|
||||||
|
const classNames = {
|
||||||
|
handle: "mx_ResizeHandle",
|
||||||
|
vertical: "mx_ResizeHandle_vertical",
|
||||||
|
reverse: "mx_ResizeHandle_reverse",
|
||||||
|
};
|
||||||
|
const collapseConfig = {
|
||||||
|
onResizeStart: () => {
|
||||||
|
this._resizeContainer.classList.add("mx_AppsDrawer_resizing");
|
||||||
|
},
|
||||||
|
onResizeStop: () => {
|
||||||
|
this._resizeContainer.classList.remove("mx_AppsDrawer_resizing");
|
||||||
|
// persist to localStorage
|
||||||
|
localStorage.setItem(this._getStorageKey(), JSON.stringify([
|
||||||
|
this.state.apps.map(app => app.id),
|
||||||
|
...this.state.apps.slice(1).map((_, i) => this.resizer.forHandleAt(i).size),
|
||||||
|
]));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// pass a truthy container for now, we won't call attach until we update it
|
||||||
|
const resizer = new Resizer({}, PercentageDistributor, collapseConfig);
|
||||||
|
resizer.setClassNames(classNames);
|
||||||
|
return resizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
_collectResizer = (ref) => {
|
||||||
|
if (this._resizeContainer) {
|
||||||
|
this.resizer.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ref) {
|
||||||
|
this.resizer.container = ref;
|
||||||
|
this.resizer.attach();
|
||||||
|
}
|
||||||
|
this._resizeContainer = ref;
|
||||||
|
this._loadResizerPreferences();
|
||||||
|
};
|
||||||
|
|
||||||
|
_getStorageKey = () => `mx_apps_drawer-${this.props.room.roomId}`;
|
||||||
|
|
||||||
|
_getAppsHash = (apps) => apps.map(app => app.id).join("~");
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
if (this._getAppsHash(this.state.apps) !== this._getAppsHash(prevState.apps)) {
|
||||||
|
this._loadResizerPreferences();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_relaxResizer = () => {
|
||||||
|
const distributors = this.resizer.getDistributors();
|
||||||
|
|
||||||
|
// relax all items if they had any overconstrained flexboxes
|
||||||
|
distributors.forEach(d => d.start());
|
||||||
|
distributors.forEach(d => d.finish());
|
||||||
|
};
|
||||||
|
|
||||||
|
_loadResizerPreferences = () => {
|
||||||
|
try {
|
||||||
|
const [[...lastIds], ...sizes] = JSON.parse(localStorage.getItem(this._getStorageKey()));
|
||||||
|
// Every app was included in the last split, reuse the last sizes
|
||||||
|
if (this.state.apps.length <= lastIds.length && this.state.apps.every((app, i) => lastIds[i] === app.id)) {
|
||||||
|
sizes.forEach((size, i) => {
|
||||||
|
const distributor = this.resizer.forHandleAt(i);
|
||||||
|
if (distributor) {
|
||||||
|
distributor.size = size;
|
||||||
|
distributor.finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// this is expected
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.apps) {
|
||||||
|
const distributors = this.resizer.getDistributors();
|
||||||
|
distributors.forEach(d => d.item.clearSize());
|
||||||
|
distributors.forEach(d => d.start());
|
||||||
|
distributors.forEach(d => d.finish());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onAction = (action) => {
|
onAction = (action) => {
|
||||||
const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer';
|
const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer';
|
||||||
switch (action.action) {
|
switch (action.action) {
|
||||||
|
@ -91,7 +190,7 @@ export default class AppsDrawer extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_getApps = () => WidgetStore.instance.getApps(this.props.room, true);
|
_getApps = () => WidgetStore.instance.getPinnedApps(this.props.room.roomId);
|
||||||
|
|
||||||
_updateApps = () => {
|
_updateApps = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -99,15 +198,6 @@ export default class AppsDrawer extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_canUserModify() {
|
|
||||||
try {
|
|
||||||
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_launchManageIntegrations() {
|
_launchManageIntegrations() {
|
||||||
if (SettingsStore.getValue("feature_many_integration_managers")) {
|
if (SettingsStore.getValue("feature_many_integration_managers")) {
|
||||||
IntegrationManagers.sharedInstance().openAll();
|
IntegrationManagers.sharedInstance().openAll();
|
||||||
|
@ -116,12 +206,9 @@ export default class AppsDrawer extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickAddWidget = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
this._launchManageIntegrations();
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
if (!this.props.showApps) return <div />;
|
||||||
|
|
||||||
const apps = this.state.apps.map((app, index, arr) => {
|
const apps = this.state.apps.map((app, index, arr) => {
|
||||||
const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, this.props.room.roomId);
|
const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, this.props.room.roomId);
|
||||||
|
|
||||||
|
@ -131,7 +218,6 @@ export default class AppsDrawer extends React.Component {
|
||||||
fullWidth={arr.length < 2}
|
fullWidth={arr.length < 2}
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
userId={this.props.userId}
|
userId={this.props.userId}
|
||||||
show={this.props.showApps}
|
|
||||||
creatorUserId={app.creatorUserId}
|
creatorUserId={app.creatorUserId}
|
||||||
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
|
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
|
||||||
waitForIframeLoad={app.waitForIframeLoad}
|
waitForIframeLoad={app.waitForIframeLoad}
|
||||||
|
@ -143,21 +229,6 @@ export default class AppsDrawer extends React.Component {
|
||||||
return <div />;
|
return <div />;
|
||||||
}
|
}
|
||||||
|
|
||||||
let addWidget;
|
|
||||||
if (this.props.showApps &&
|
|
||||||
this._canUserModify()
|
|
||||||
) {
|
|
||||||
addWidget = <AccessibleButton
|
|
||||||
onClick={this.onClickAddWidget}
|
|
||||||
className={this.state.apps.length<2 ?
|
|
||||||
'mx_AddWidget_button mx_AddWidget_button_full_width' :
|
|
||||||
'mx_AddWidget_button'
|
|
||||||
}
|
|
||||||
title={_t('Add a widget')}>
|
|
||||||
[+] { _t('Add a widget') }
|
|
||||||
</AccessibleButton>;
|
|
||||||
}
|
|
||||||
|
|
||||||
let spinner;
|
let spinner;
|
||||||
if (
|
if (
|
||||||
apps.length === 0 && WidgetEchoStore.roomHasPendingWidgets(
|
apps.length === 0 && WidgetEchoStore.roomHasPendingWidgets(
|
||||||
|
@ -170,9 +241,11 @@ export default class AppsDrawer extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
"mx_AppsDrawer": true,
|
mx_AppsDrawer: true,
|
||||||
"mx_AppsDrawer_fullWidth": apps.length < 2,
|
mx_AppsDrawer_fullWidth: apps.length < 2,
|
||||||
"mx_AppsDrawer_minimised": !this.props.showApps,
|
mx_AppsDrawer_resizing: this.state.resizing,
|
||||||
|
mx_AppsDrawer_2apps: apps.length === 2,
|
||||||
|
mx_AppsDrawer_3apps: apps.length === 3,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -182,13 +255,20 @@ export default class AppsDrawer extends React.Component {
|
||||||
minHeight={100}
|
minHeight={100}
|
||||||
maxHeight={this.props.maxHeight ? this.props.maxHeight - 50 : undefined}
|
maxHeight={this.props.maxHeight ? this.props.maxHeight - 50 : undefined}
|
||||||
handleClass="mx_AppsContainer_resizerHandle"
|
handleClass="mx_AppsContainer_resizerHandle"
|
||||||
className="mx_AppsContainer"
|
className="mx_AppsContainer_resizer"
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
>
|
>
|
||||||
{ apps }
|
<div className="mx_AppsContainer" ref={this._collectResizer}>
|
||||||
{ spinner }
|
{ 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>
|
</PersistentVResizer>
|
||||||
{ this._canUserModify() && addWidget }
|
{ spinner }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -205,14 +285,12 @@ const PersistentVResizer = ({
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const [height, setHeight] = useLocalStorageState("pvr_" + id, 280); // old fixed height was 273px
|
const [height, setHeight] = useLocalStorageState("pvr_" + id, 280); // old fixed height was 273px
|
||||||
const [resizing, setResizing] = useState(false);
|
|
||||||
|
|
||||||
return <Resizable
|
return <Resizable
|
||||||
size={{height: Math.min(height, maxHeight)}}
|
size={{height: Math.min(height, maxHeight)}}
|
||||||
minHeight={minHeight}
|
minHeight={minHeight}
|
||||||
maxHeight={maxHeight}
|
maxHeight={maxHeight}
|
||||||
onResizeStart={() => {
|
onResizeStart={() => {
|
||||||
if (!resizing) setResizing(true);
|
|
||||||
resizeNotifier.startResizing();
|
resizeNotifier.startResizing();
|
||||||
}}
|
}}
|
||||||
onResize={() => {
|
onResize={() => {
|
||||||
|
@ -220,14 +298,11 @@ const PersistentVResizer = ({
|
||||||
}}
|
}}
|
||||||
onResizeStop={(e, dir, ref, d) => {
|
onResizeStop={(e, dir, ref, d) => {
|
||||||
setHeight(height + d.height);
|
setHeight(height + d.height);
|
||||||
if (resizing) setResizing(false);
|
|
||||||
resizeNotifier.stopResizing();
|
resizeNotifier.stopResizing();
|
||||||
}}
|
}}
|
||||||
handleWrapperClass={handleWrapperClass}
|
handleWrapperClass={handleWrapperClass}
|
||||||
handleClasses={{bottom: handleClass}}
|
handleClasses={{bottom: handleClass}}
|
||||||
className={classNames(className, {
|
className={className}
|
||||||
mx_AppsDrawer_resizing: resizing,
|
|
||||||
})}
|
|
||||||
enable={{bottom: true}}
|
enable={{bottom: true}}
|
||||||
>
|
>
|
||||||
{ children }
|
{ children }
|
||||||
|
|
|
@ -42,6 +42,8 @@ export default class RoomHeader extends React.Component {
|
||||||
onLeaveClick: PropTypes.func,
|
onLeaveClick: PropTypes.func,
|
||||||
onCancelClick: PropTypes.func,
|
onCancelClick: PropTypes.func,
|
||||||
e2eStatus: PropTypes.string,
|
e2eStatus: PropTypes.string,
|
||||||
|
onAppsClick: PropTypes.func,
|
||||||
|
appsShown: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -230,6 +232,17 @@ export default class RoomHeader extends React.Component {
|
||||||
title={_t("Forget room")} />;
|
title={_t("Forget room")} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let appsButton;
|
||||||
|
if (this.props.onAppsClick) {
|
||||||
|
appsButton =
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className={classNames("mx_RoomHeader_button mx_RoomHeader_appsButton", {
|
||||||
|
mx_RoomHeader_appsButton_highlight: this.props.appsShown,
|
||||||
|
})}
|
||||||
|
onClick={this.props.onAppsClick}
|
||||||
|
title={this.props.appsShown ? _t("Hide Widgets") : _t("Show Widgets")} />;
|
||||||
|
}
|
||||||
|
|
||||||
let searchButton;
|
let searchButton;
|
||||||
if (this.props.onSearchClick && this.props.inRoom) {
|
if (this.props.onSearchClick && this.props.inRoom) {
|
||||||
searchButton =
|
searchButton =
|
||||||
|
@ -243,6 +256,7 @@ export default class RoomHeader extends React.Component {
|
||||||
<div className="mx_RoomHeader_buttons">
|
<div className="mx_RoomHeader_buttons">
|
||||||
{ pinnedEventsButton }
|
{ pinnedEventsButton }
|
||||||
{ forgetButton }
|
{ forgetButton }
|
||||||
|
{ appsButton }
|
||||||
{ searchButton }
|
{ searchButton }
|
||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,6 @@ interface IProps {
|
||||||
onBlur: (ev: React.FocusEvent) => void;
|
onBlur: (ev: React.FocusEvent) => void;
|
||||||
onResize: () => void;
|
onResize: () => void;
|
||||||
resizeNotifier: ResizeNotifier;
|
resizeNotifier: ResizeNotifier;
|
||||||
collapsed: boolean;
|
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -366,7 +365,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
let explorePrompt: JSX.Element;
|
let explorePrompt: JSX.Element;
|
||||||
if (RoomListStore.instance.getFirstNameFilterCondition()) {
|
if (!this.props.isMinimized && RoomListStore.instance.getFirstNameFilterCondition()) {
|
||||||
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
||||||
<div>{_t("Can't see what you’re looking for?")}</div>
|
<div>{_t("Can't see what you’re looking for?")}</div>
|
||||||
<AccessibleButton kind="link" onClick={this.onExplore}>
|
<AccessibleButton kind="link" onClick={this.onExplore}>
|
||||||
|
|
|
@ -272,13 +272,10 @@ export default class Stickerpicker extends React.Component {
|
||||||
userId={MatrixClientPeg.get().credentials.userId}
|
userId={MatrixClientPeg.get().credentials.userId}
|
||||||
creatorUserId={stickerpickerWidget.sender || MatrixClientPeg.get().credentials.userId}
|
creatorUserId={stickerpickerWidget.sender || MatrixClientPeg.get().credentials.userId}
|
||||||
waitForIframeLoad={true}
|
waitForIframeLoad={true}
|
||||||
show={true}
|
|
||||||
showMenubar={true}
|
showMenubar={true}
|
||||||
onEditClick={this._launchManageIntegrations}
|
onEditClick={this._launchManageIntegrations}
|
||||||
onDeleteClick={this._removeStickerpickerWidgets}
|
onDeleteClick={this._removeStickerpickerWidgets}
|
||||||
showTitle={false}
|
showTitle={false}
|
||||||
showMinimise={true}
|
|
||||||
showDelete={false}
|
|
||||||
showCancel={false}
|
showCancel={false}
|
||||||
showPopout={false}
|
showPopout={false}
|
||||||
onMinimiseClick={this._onHideStickersClick}
|
onMinimiseClick={this._onHideStickersClick}
|
||||||
|
|
|
@ -94,14 +94,4 @@ export enum Action {
|
||||||
* Trigged after the phase of the right panel is set. Should be used with AfterRightPanelPhaseChangePayload.
|
* Trigged after the phase of the right panel is set. Should be used with AfterRightPanelPhaseChangePayload.
|
||||||
*/
|
*/
|
||||||
AfterRightPanelPhaseChange = "after_right_panel_phase_change",
|
AfterRightPanelPhaseChange = "after_right_panel_phase_change",
|
||||||
|
|
||||||
/**
|
|
||||||
* Requests that the AppTile deletes the widget. Should be used with the AppTileActionPayload.
|
|
||||||
*/
|
|
||||||
AppTileDelete = "appTile_delete",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Requests that the AppTile revokes the widget. Should be used with the AppTileActionPayload.
|
|
||||||
*/
|
|
||||||
AppTileRevoke = "appTile_revoke",
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1031,7 +1031,6 @@
|
||||||
"Remove %(phone)s?": "Remove %(phone)s?",
|
"Remove %(phone)s?": "Remove %(phone)s?",
|
||||||
"A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.",
|
"A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.",
|
||||||
"Phone Number": "Phone Number",
|
"Phone Number": "Phone Number",
|
||||||
"Add a widget": "Add a widget",
|
|
||||||
"Drop File Here": "Drop File Here",
|
"Drop File Here": "Drop File Here",
|
||||||
"Drop file here to upload": "Drop file here to upload",
|
"Drop file here to upload": "Drop file here to upload",
|
||||||
"This user has not verified all of their sessions.": "This user has not verified all of their sessions.",
|
"This user has not verified all of their sessions.": "This user has not verified all of their sessions.",
|
||||||
|
@ -1113,6 +1112,8 @@
|
||||||
"(~%(count)s results)|one": "(~%(count)s result)",
|
"(~%(count)s results)|one": "(~%(count)s result)",
|
||||||
"Join Room": "Join Room",
|
"Join Room": "Join Room",
|
||||||
"Forget room": "Forget room",
|
"Forget room": "Forget room",
|
||||||
|
"Hide Widgets": "Hide Widgets",
|
||||||
|
"Show Widgets": "Show Widgets",
|
||||||
"Search": "Search",
|
"Search": "Search",
|
||||||
"Invites": "Invites",
|
"Invites": "Invites",
|
||||||
"Favourites": "Favourites",
|
"Favourites": "Favourites",
|
||||||
|
@ -1278,8 +1279,11 @@
|
||||||
"Yours, or the other users’ session": "Yours, or the other users’ session",
|
"Yours, or the other users’ session": "Yours, or the other users’ session",
|
||||||
"Members": "Members",
|
"Members": "Members",
|
||||||
"Room Info": "Room Info",
|
"Room Info": "Room Info",
|
||||||
|
"You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets",
|
||||||
|
"Unpin": "Unpin",
|
||||||
|
"Unpin a widget to view it in this panel": "Unpin a widget to view it in this panel",
|
||||||
|
"Options": "Options",
|
||||||
"Widgets": "Widgets",
|
"Widgets": "Widgets",
|
||||||
"Unpin app": "Unpin app",
|
|
||||||
"Edit widgets, bridges & bots": "Edit widgets, bridges & bots",
|
"Edit widgets, bridges & bots": "Edit widgets, bridges & bots",
|
||||||
"Add widgets, bridges & bots": "Add widgets, bridges & bots",
|
"Add widgets, bridges & bots": "Add widgets, bridges & bots",
|
||||||
"Not encrypted": "Not encrypted",
|
"Not encrypted": "Not encrypted",
|
||||||
|
@ -1302,7 +1306,6 @@
|
||||||
"Invite": "Invite",
|
"Invite": "Invite",
|
||||||
"Share Link to User": "Share Link to User",
|
"Share Link to User": "Share Link to User",
|
||||||
"Direct message": "Direct message",
|
"Direct message": "Direct message",
|
||||||
"Options": "Options",
|
|
||||||
"Demote yourself?": "Demote yourself?",
|
"Demote yourself?": "Demote yourself?",
|
||||||
"You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.",
|
"You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.",
|
||||||
"Demote": "Demote",
|
"Demote": "Demote",
|
||||||
|
@ -1366,12 +1369,6 @@
|
||||||
"You cancelled verification.": "You cancelled verification.",
|
"You cancelled verification.": "You cancelled verification.",
|
||||||
"Verification cancelled": "Verification cancelled",
|
"Verification cancelled": "Verification cancelled",
|
||||||
"Compare emoji": "Compare emoji",
|
"Compare emoji": "Compare emoji",
|
||||||
"Take a picture": "Take a picture",
|
|
||||||
"Remove for everyone": "Remove for everyone",
|
|
||||||
"Remove for me": "Remove for me",
|
|
||||||
"Edit": "Edit",
|
|
||||||
"Pin to room": "Pin to room",
|
|
||||||
"You can only pin 2 widgets at a time": "You can only pin 2 widgets at a time",
|
|
||||||
"Sunday": "Sunday",
|
"Sunday": "Sunday",
|
||||||
"Monday": "Monday",
|
"Monday": "Monday",
|
||||||
"Tuesday": "Tuesday",
|
"Tuesday": "Tuesday",
|
||||||
|
@ -1390,6 +1387,7 @@
|
||||||
"Error decrypting audio": "Error decrypting audio",
|
"Error decrypting audio": "Error decrypting audio",
|
||||||
"React": "React",
|
"React": "React",
|
||||||
"Reply": "Reply",
|
"Reply": "Reply",
|
||||||
|
"Edit": "Edit",
|
||||||
"Message Actions": "Message Actions",
|
"Message Actions": "Message Actions",
|
||||||
"Attachment": "Attachment",
|
"Attachment": "Attachment",
|
||||||
"Error decrypting attachment": "Error decrypting attachment",
|
"Error decrypting attachment": "Error decrypting attachment",
|
||||||
|
@ -1482,15 +1480,7 @@
|
||||||
"Widgets do not use message encryption.": "Widgets do not use message encryption.",
|
"Widgets do not use message encryption.": "Widgets do not use message encryption.",
|
||||||
"Widget added by": "Widget added by",
|
"Widget added by": "Widget added by",
|
||||||
"This widget may use cookies.": "This widget may use cookies.",
|
"This widget may use cookies.": "This widget may use cookies.",
|
||||||
"Delete Widget": "Delete Widget",
|
|
||||||
"Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?",
|
|
||||||
"Delete widget": "Delete widget",
|
|
||||||
"Failed to remove widget": "Failed to remove widget",
|
|
||||||
"An error ocurred whilst trying to remove the widget from the room": "An error ocurred whilst trying to remove the widget from the room",
|
|
||||||
"Minimize widget": "Minimize widget",
|
|
||||||
"Maximize widget": "Maximize widget",
|
|
||||||
"Popout widget": "Popout widget",
|
"Popout widget": "Popout widget",
|
||||||
"More options": "More options",
|
|
||||||
"Use the <a>Desktop app</a> to see all encrypted files": "Use the <a>Desktop app</a> to see all encrypted files",
|
"Use the <a>Desktop app</a> to see all encrypted files": "Use the <a>Desktop app</a> to see all encrypted files",
|
||||||
"Use the <a>Desktop app</a> to search encrypted messages": "Use the <a>Desktop app</a> to search encrypted messages",
|
"Use the <a>Desktop app</a> to search encrypted messages": "Use the <a>Desktop app</a> to search encrypted messages",
|
||||||
"This version of %(brand)s does not support viewing some encrypted files": "This version of %(brand)s does not support viewing some encrypted files",
|
"This version of %(brand)s does not support viewing some encrypted files": "This version of %(brand)s does not support viewing some encrypted files",
|
||||||
|
@ -1923,9 +1913,14 @@
|
||||||
"Set status": "Set status",
|
"Set status": "Set status",
|
||||||
"Set a new status...": "Set a new status...",
|
"Set a new status...": "Set a new status...",
|
||||||
"View Community": "View Community",
|
"View Community": "View Community",
|
||||||
"Unpin": "Unpin",
|
"Take a picture": "Take a picture",
|
||||||
"Reload": "Reload",
|
"Delete Widget": "Delete Widget",
|
||||||
"Take picture": "Take picture",
|
"Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?",
|
||||||
|
"Delete widget": "Delete widget",
|
||||||
|
"Remove for everyone": "Remove for everyone",
|
||||||
|
"Revoke permissions": "Revoke permissions",
|
||||||
|
"Move left": "Move left",
|
||||||
|
"Move right": "Move right",
|
||||||
"This room is public": "This room is public",
|
"This room is public": "This room is public",
|
||||||
"Away": "Away",
|
"Away": "Away",
|
||||||
"User Status": "User Status",
|
"User Status": "User Status",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 New Vector Ltd
|
Copyright 2019 - 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -16,9 +16,16 @@ limitations under the License.
|
||||||
|
|
||||||
import FixedDistributor from "./fixed";
|
import FixedDistributor from "./fixed";
|
||||||
import ResizeItem from "../item";
|
import ResizeItem from "../item";
|
||||||
|
import Resizer, {IConfig} from "../resizer";
|
||||||
|
import Sizer from "../sizer";
|
||||||
|
|
||||||
class CollapseItem extends ResizeItem {
|
export interface ICollapseConfig extends IConfig {
|
||||||
notifyCollapsed(collapsed) {
|
toggleSize: number;
|
||||||
|
onCollapsed?(collapsed: boolean, id: string, element: HTMLElement): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CollapseItem extends ResizeItem<ICollapseConfig> {
|
||||||
|
notifyCollapsed(collapsed: boolean) {
|
||||||
const callback = this.resizer.config.onCollapsed;
|
const callback = this.resizer.config.onCollapsed;
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback(collapsed, this.id, this.domNode);
|
callback(collapsed, this.id, this.domNode);
|
||||||
|
@ -26,18 +33,20 @@ class CollapseItem extends ResizeItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class CollapseDistributor extends FixedDistributor {
|
export default class CollapseDistributor extends FixedDistributor<ICollapseConfig, CollapseItem> {
|
||||||
static createItem(resizeHandle, resizer, sizer) {
|
static createItem(resizeHandle: HTMLDivElement, resizer: Resizer<ICollapseConfig>, sizer: Sizer) {
|
||||||
return new CollapseItem(resizeHandle, resizer, sizer);
|
return new CollapseItem(resizeHandle, resizer, sizer);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(item, config) {
|
private readonly toggleSize: number;
|
||||||
|
private isCollapsed = false;
|
||||||
|
|
||||||
|
constructor(item: CollapseItem) {
|
||||||
super(item);
|
super(item);
|
||||||
this.toggleSize = config && config.toggleSize;
|
this.toggleSize = item.resizer?.config?.toggleSize;
|
||||||
this.isCollapsed = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resize(newSize) {
|
public resize(newSize: number) {
|
||||||
const isCollapsedSize = newSize < this.toggleSize;
|
const isCollapsedSize = newSize < this.toggleSize;
|
||||||
if (isCollapsedSize && !this.isCollapsed) {
|
if (isCollapsedSize && !this.isCollapsed) {
|
||||||
this.isCollapsed = true;
|
this.isCollapsed = true;
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 New Vector Ltd
|
Copyright 2019 - 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import ResizeItem from "../item";
|
import ResizeItem from "../item";
|
||||||
import Sizer from "../sizer";
|
import Sizer from "../sizer";
|
||||||
|
import Resizer, {IConfig} from "../resizer";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
distributors translate a moving cursor into
|
distributors translate a moving cursor into
|
||||||
|
@ -27,29 +28,42 @@ they have two methods:
|
||||||
within the container bounding box. For internal use.
|
within the container bounding box. For internal use.
|
||||||
This method usually ends up calling `resize` once the start offset is subtracted.
|
This method usually ends up calling `resize` once the start offset is subtracted.
|
||||||
*/
|
*/
|
||||||
export default class FixedDistributor {
|
export default class FixedDistributor<C extends IConfig, I extends ResizeItem<any> = ResizeItem<C>> {
|
||||||
static createItem(resizeHandle, resizer, sizer) {
|
static createItem(resizeHandle: HTMLDivElement, resizer: Resizer, sizer: Sizer): ResizeItem {
|
||||||
return new ResizeItem(resizeHandle, resizer, sizer);
|
return new ResizeItem(resizeHandle, resizer, sizer);
|
||||||
}
|
}
|
||||||
|
|
||||||
static createSizer(containerElement, vertical, reverse) {
|
static createSizer(containerElement: HTMLElement, vertical: boolean, reverse: boolean): Sizer {
|
||||||
return new Sizer(containerElement, vertical, reverse);
|
return new Sizer(containerElement, vertical, reverse);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(item) {
|
private readonly beforeOffset: number;
|
||||||
this.item = item;
|
|
||||||
|
constructor(public readonly item: I) {
|
||||||
this.beforeOffset = item.offset();
|
this.beforeOffset = item.offset();
|
||||||
}
|
}
|
||||||
|
|
||||||
resize(size) {
|
public get size() {
|
||||||
|
return this.item.getSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public set size(size: string) {
|
||||||
|
this.item.setRawSize(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
public resize(size: number) {
|
||||||
this.item.setSize(size);
|
this.item.setSize(size);
|
||||||
}
|
}
|
||||||
|
|
||||||
resizeFromContainerOffset(offset) {
|
public resizeFromContainerOffset(offset: number) {
|
||||||
this.resize(offset - this.beforeOffset);
|
this.resize(offset - this.beforeOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {}
|
public start() {
|
||||||
|
this.item.start();
|
||||||
|
}
|
||||||
|
|
||||||
finish() {}
|
public finish() {
|
||||||
|
this.item.finish();
|
||||||
|
}
|
||||||
}
|
}
|
49
src/resizer/distributors/percentage.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 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 Sizer from "../sizer";
|
||||||
|
import FixedDistributor from "./fixed";
|
||||||
|
import {IConfig} from "../resizer";
|
||||||
|
|
||||||
|
class PercentageSizer extends Sizer {
|
||||||
|
public start(item: HTMLElement) {
|
||||||
|
if (this.vertical) {
|
||||||
|
item.style.minHeight = null;
|
||||||
|
} else {
|
||||||
|
item.style.minWidth = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public finish(item: HTMLElement) {
|
||||||
|
const parent = item.offsetParent as HTMLElement;
|
||||||
|
if (!parent) return;
|
||||||
|
if (this.vertical) {
|
||||||
|
const p = ((item.offsetHeight / parent.offsetHeight) * 100).toFixed(2) + "%";
|
||||||
|
item.style.minHeight = p;
|
||||||
|
item.style.height = p;
|
||||||
|
} else {
|
||||||
|
const p = ((item.offsetWidth / parent.offsetWidth) * 100).toFixed(2) + "%";
|
||||||
|
item.style.minWidth = p;
|
||||||
|
item.style.width = p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class PercentageDistributor extends FixedDistributor<IConfig> {
|
||||||
|
static createSizer(containerElement: HTMLElement, vertical: boolean, reverse: boolean) {
|
||||||
|
return new PercentageSizer(containerElement, vertical, reverse);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@ -15,6 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export FixedDistributor from "./distributors/fixed";
|
export {default as FixedDistributor} from "./distributors/fixed";
|
||||||
export CollapseDistributor from "./distributors/collapse";
|
export {default as PercentageDistributor} from "./distributors/percentage";
|
||||||
export Resizer from "./resizer";
|
export {default as CollapseDistributor} from "./distributors/collapse";
|
||||||
|
export {default as Resizer} from "./resizer";
|
|
@ -1,107 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2019 New Vector Ltd
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default class ResizeItem {
|
|
||||||
constructor(handle, resizer, sizer) {
|
|
||||||
const id = handle.getAttribute("data-id");
|
|
||||||
const reverse = resizer.isReverseResizeHandle(handle);
|
|
||||||
const domNode = reverse ? handle.nextElementSibling : handle.previousElementSibling;
|
|
||||||
|
|
||||||
this.domNode = domNode;
|
|
||||||
this.id = id;
|
|
||||||
this.reverse = reverse;
|
|
||||||
this.resizer = resizer;
|
|
||||||
this.sizer = sizer;
|
|
||||||
}
|
|
||||||
|
|
||||||
_copyWith(handle, resizer, sizer) {
|
|
||||||
const Ctor = this.constructor;
|
|
||||||
return new Ctor(handle, resizer, sizer);
|
|
||||||
}
|
|
||||||
|
|
||||||
_advance(forwards) {
|
|
||||||
// opposite direction from fromResizeHandle to get back to handle
|
|
||||||
let handle = this.reverse ?
|
|
||||||
this.domNode.previousElementSibling :
|
|
||||||
this.domNode.nextElementSibling;
|
|
||||||
const moveNext = forwards !== this.reverse; // xor
|
|
||||||
// iterate at least once to avoid infinite loop
|
|
||||||
do {
|
|
||||||
if (moveNext) {
|
|
||||||
handle = handle.nextElementSibling;
|
|
||||||
} else {
|
|
||||||
handle = handle.previousElementSibling;
|
|
||||||
}
|
|
||||||
} while (handle && !this.resizer.isResizeHandle(handle));
|
|
||||||
|
|
||||||
if (handle) {
|
|
||||||
const nextHandle = this._copyWith(handle, this.resizer, this.sizer);
|
|
||||||
nextHandle.reverse = this.reverse;
|
|
||||||
return nextHandle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
next() {
|
|
||||||
return this._advance(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
previous() {
|
|
||||||
return this._advance(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
size() {
|
|
||||||
return this.sizer.getItemSize(this.domNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
offset() {
|
|
||||||
return this.sizer.getItemOffset(this.domNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
setSize(size) {
|
|
||||||
this.sizer.setItemSize(this.domNode, size);
|
|
||||||
const callback = this.resizer.config.onResized;
|
|
||||||
if (callback) {
|
|
||||||
callback(size, this.id, this.domNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clearSize() {
|
|
||||||
this.sizer.clearItemSize(this.domNode);
|
|
||||||
const callback = this.resizer.config.onResized;
|
|
||||||
if (callback) {
|
|
||||||
callback(null, this.id, this.domNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
first() {
|
|
||||||
const firstHandle = Array.from(this.domNode.parentElement.children).find(el => {
|
|
||||||
return this.resizer.isResizeHandle(el);
|
|
||||||
});
|
|
||||||
if (firstHandle) {
|
|
||||||
return this._copyWith(firstHandle, this.resizer, this.sizer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
last() {
|
|
||||||
const lastHandle = Array.from(this.domNode.parentElement.children).reverse().find(el => {
|
|
||||||
return this.resizer.isResizeHandle(el);
|
|
||||||
});
|
|
||||||
if (lastHandle) {
|
|
||||||
return this._copyWith(lastHandle, this.resizer, this.sizer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
125
src/resizer/item.ts
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 - 2020 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 Resizer, {IConfig} from "./resizer";
|
||||||
|
import Sizer from "./sizer";
|
||||||
|
|
||||||
|
export default class ResizeItem<C extends IConfig = IConfig> {
|
||||||
|
public readonly domNode: HTMLElement;
|
||||||
|
protected readonly id: string;
|
||||||
|
protected reverse: boolean;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
handle: HTMLElement,
|
||||||
|
public readonly resizer: Resizer<C>,
|
||||||
|
public readonly sizer: Sizer,
|
||||||
|
) {
|
||||||
|
this.reverse = resizer.isReverseResizeHandle(handle);
|
||||||
|
this.domNode = <HTMLElement>(this.reverse ? handle.nextElementSibling : handle.previousElementSibling);
|
||||||
|
this.id = handle.getAttribute("data-id");
|
||||||
|
}
|
||||||
|
|
||||||
|
private copyWith(handle: HTMLElement, resizer: Resizer, sizer: Sizer) {
|
||||||
|
const Ctor = this.constructor as typeof ResizeItem;
|
||||||
|
return new Ctor(handle, resizer, sizer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private advance(forwards: boolean) {
|
||||||
|
// opposite direction from fromResizeHandle to get back to handle
|
||||||
|
let handle = this.reverse ? this.domNode.previousElementSibling : this.domNode.nextElementSibling;
|
||||||
|
const moveNext = forwards !== this.reverse; // xor
|
||||||
|
// iterate at least once to avoid infinite loop
|
||||||
|
do {
|
||||||
|
if (moveNext) {
|
||||||
|
handle = handle.nextElementSibling;
|
||||||
|
} else {
|
||||||
|
handle = handle.previousElementSibling;
|
||||||
|
}
|
||||||
|
} while (handle && !this.resizer.isResizeHandle(<HTMLElement>handle));
|
||||||
|
|
||||||
|
if (handle) {
|
||||||
|
const nextHandle = this.copyWith(<HTMLElement>handle, this.resizer, this.sizer);
|
||||||
|
nextHandle.reverse = this.reverse;
|
||||||
|
return nextHandle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public next() {
|
||||||
|
return this.advance(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public previous() {
|
||||||
|
return this.advance(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public size() {
|
||||||
|
return this.sizer.getItemSize(this.domNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public offset() {
|
||||||
|
return this.sizer.getItemOffset(this.domNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public start() {
|
||||||
|
this.sizer.start(this.domNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public finish() {
|
||||||
|
this.sizer.finish(this.domNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSize() {
|
||||||
|
return this.sizer.getDesiredItemSize(this.domNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setRawSize(size: string) {
|
||||||
|
this.sizer.setItemSize(this.domNode, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setSize(size: number) {
|
||||||
|
this.setRawSize(`${Math.round(size)}px`);
|
||||||
|
const callback = this.resizer.config.onResized;
|
||||||
|
if (callback) {
|
||||||
|
callback(size, this.id, this.domNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public clearSize() {
|
||||||
|
this.sizer.clearItemSize(this.domNode);
|
||||||
|
const callback = this.resizer.config.onResized;
|
||||||
|
if (callback) {
|
||||||
|
callback(null, this.id, this.domNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public first() {
|
||||||
|
const firstHandle = Array.from(this.domNode.parentElement.children).find(el => {
|
||||||
|
return this.resizer.isResizeHandle(<HTMLElement>el);
|
||||||
|
});
|
||||||
|
if (firstHandle) {
|
||||||
|
return this.copyWith(<HTMLElement>firstHandle, this.resizer, this.sizer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public last() {
|
||||||
|
const lastHandle = Array.from(this.domNode.parentElement.children).reverse().find(el => {
|
||||||
|
return this.resizer.isResizeHandle(<HTMLElement>el);
|
||||||
|
});
|
||||||
|
if (lastHandle) {
|
||||||
|
return this.copyWith(<HTMLElement>lastHandle, this.resizer, this.sizer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018 New Vector Ltd
|
Copyright 2018 - 2020 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,86 +14,105 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
import {throttle} from "lodash";
|
||||||
classNames:
|
|
||||||
|
import FixedDistributor from "./distributors/fixed";
|
||||||
|
import ResizeItem from "./item";
|
||||||
|
import Sizer from "./sizer";
|
||||||
|
|
||||||
|
interface IClassNames {
|
||||||
// class on resize-handle
|
// class on resize-handle
|
||||||
handle: string
|
handle?: string;
|
||||||
// class on resize-handle
|
// class on resize-handle
|
||||||
reverse: string
|
reverse?: string;
|
||||||
// class on resize-handle
|
// class on resize-handle
|
||||||
vertical: string
|
vertical?: string;
|
||||||
// class on container
|
// class on container
|
||||||
resizing: string
|
resizing?: string;
|
||||||
*/
|
}
|
||||||
|
|
||||||
|
export interface IConfig {
|
||||||
|
onResizeStart?(): void;
|
||||||
|
onResizeStop?(): void;
|
||||||
|
onResized?(size: number, id: string, element: HTMLElement): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Resizer<C extends IConfig = IConfig> {
|
||||||
|
private classNames: IClassNames;
|
||||||
|
|
||||||
export default class Resizer {
|
|
||||||
// TODO move vertical/horizontal to config option/container class
|
// TODO move vertical/horizontal to config option/container class
|
||||||
// as it doesn't make sense to mix them within one container/Resizer
|
// as it doesn't make sense to mix them within one container/Resizer
|
||||||
constructor(container, distributorCtor, config) {
|
constructor(
|
||||||
|
public container: HTMLElement,
|
||||||
|
private readonly distributorCtor: {
|
||||||
|
new(item: ResizeItem): FixedDistributor<C, any>;
|
||||||
|
createItem(resizeHandle: HTMLDivElement, resizer: Resizer, sizer: Sizer): ResizeItem;
|
||||||
|
createSizer(containerElement: HTMLElement, vertical: boolean, reverse: boolean): Sizer;
|
||||||
|
},
|
||||||
|
public readonly config?: C,
|
||||||
|
) {
|
||||||
if (!container) {
|
if (!container) {
|
||||||
throw new Error("Resizer requires a non-null `container` arg");
|
throw new Error("Resizer requires a non-null `container` arg");
|
||||||
}
|
}
|
||||||
this.container = container;
|
|
||||||
this.distributorCtor = distributorCtor;
|
|
||||||
this.config = config;
|
|
||||||
this.classNames = {
|
this.classNames = {
|
||||||
handle: "resizer-handle",
|
handle: "resizer-handle",
|
||||||
reverse: "resizer-reverse",
|
reverse: "resizer-reverse",
|
||||||
vertical: "resizer-vertical",
|
vertical: "resizer-vertical",
|
||||||
resizing: "resizer-resizing",
|
resizing: "resizer-resizing",
|
||||||
};
|
};
|
||||||
this._onMouseDown = this._onMouseDown.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setClassNames(classNames) {
|
public setClassNames(classNames: IClassNames) {
|
||||||
this.classNames = classNames;
|
this.classNames = classNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
attach() {
|
public attach() {
|
||||||
this.container.addEventListener("mousedown", this._onMouseDown, false);
|
this.container.addEventListener("mousedown", this.onMouseDown, false);
|
||||||
|
window.addEventListener("resize", this.onResize);
|
||||||
}
|
}
|
||||||
|
|
||||||
detach() {
|
public detach() {
|
||||||
this.container.removeEventListener("mousedown", this._onMouseDown, false);
|
this.container.removeEventListener("mousedown", this.onMouseDown, false);
|
||||||
|
window.removeEventListener("resize", this.onResize);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Gives the distributor for a specific resize handle, as if you would have started
|
Gives the distributor for a specific resize handle, as if you would have started
|
||||||
to drag that handle. Can be used to manipulate the size of an item programmatically.
|
to drag that handle. Can be used to manipulate the size of an item programmatically.
|
||||||
@param {number} handleIndex the index of the resize handle in the container
|
@param {number} handleIndex the index of the resize handle in the container
|
||||||
@return {Distributor} a new distributor for the given handle
|
@return {FixedDistributor} a new distributor for the given handle
|
||||||
*/
|
*/
|
||||||
forHandleAt(handleIndex) {
|
public forHandleAt(handleIndex: number): FixedDistributor<C> {
|
||||||
const handles = this._getResizeHandles();
|
const handles = this.getResizeHandles();
|
||||||
const handle = handles[handleIndex];
|
const handle = handles[handleIndex];
|
||||||
if (handle) {
|
if (handle) {
|
||||||
const {distributor} = this._createSizerAndDistributor(handle);
|
const {distributor} = this.createSizerAndDistributor(<HTMLDivElement>handle);
|
||||||
return distributor;
|
return distributor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
forHandleWithId(id) {
|
public forHandleWithId(id: string): FixedDistributor<C> {
|
||||||
const handles = this._getResizeHandles();
|
const handles = this.getResizeHandles();
|
||||||
const handle = handles.find((h) => h.getAttribute("data-id") === id);
|
const handle = handles.find((h) => h.getAttribute("data-id") === id);
|
||||||
if (handle) {
|
if (handle) {
|
||||||
const {distributor} = this._createSizerAndDistributor(handle);
|
const {distributor} = this.createSizerAndDistributor(<HTMLDivElement>handle);
|
||||||
return distributor;
|
return distributor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isReverseResizeHandle(el) {
|
public isReverseResizeHandle(el: HTMLElement): boolean {
|
||||||
return el && el.classList.contains(this.classNames.reverse);
|
return el && el.classList.contains(this.classNames.reverse);
|
||||||
}
|
}
|
||||||
|
|
||||||
isResizeHandle(el) {
|
public isResizeHandle(el: HTMLElement): boolean {
|
||||||
return el && el.classList.contains(this.classNames.handle);
|
return el && el.classList.contains(this.classNames.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onMouseDown(event) {
|
private onMouseDown = (event: MouseEvent) => {
|
||||||
// use closest in case the resize handle contains
|
// use closest in case the resize handle contains
|
||||||
// child dom nodes that can be the target
|
// child dom nodes that can be the target
|
||||||
const resizeHandle = event.target && event.target.closest(`.${this.classNames.handle}`);
|
const resizeHandle = event.target && (<HTMLDivElement>event.target).closest(`.${this.classNames.handle}`);
|
||||||
if (!resizeHandle || resizeHandle.parentElement !== this.container) {
|
if (!resizeHandle || resizeHandle.parentElement !== this.container) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -109,7 +127,7 @@ export default class Resizer {
|
||||||
this.config.onResizeStart();
|
this.config.onResizeStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
const {sizer, distributor} = this._createSizerAndDistributor(resizeHandle);
|
const {sizer, distributor} = this.createSizerAndDistributor(<HTMLDivElement>resizeHandle);
|
||||||
distributor.start();
|
distributor.start();
|
||||||
|
|
||||||
const onMouseMove = (event) => {
|
const onMouseMove = (event) => {
|
||||||
|
@ -122,10 +140,10 @@ export default class Resizer {
|
||||||
if (this.classNames.resizing) {
|
if (this.classNames.resizing) {
|
||||||
this.container.classList.remove(this.classNames.resizing);
|
this.container.classList.remove(this.classNames.resizing);
|
||||||
}
|
}
|
||||||
|
distributor.finish();
|
||||||
if (this.config.onResizeStop) {
|
if (this.config.onResizeStop) {
|
||||||
this.config.onResizeStop();
|
this.config.onResizeStop();
|
||||||
}
|
}
|
||||||
distributor.finish();
|
|
||||||
body.removeEventListener("mouseup", finishResize, false);
|
body.removeEventListener("mouseup", finishResize, false);
|
||||||
document.removeEventListener("mouseleave", finishResize, false);
|
document.removeEventListener("mouseleave", finishResize, false);
|
||||||
body.removeEventListener("mousemove", onMouseMove, false);
|
body.removeEventListener("mousemove", onMouseMove, false);
|
||||||
|
@ -133,21 +151,39 @@ export default class Resizer {
|
||||||
body.addEventListener("mouseup", finishResize, false);
|
body.addEventListener("mouseup", finishResize, false);
|
||||||
document.addEventListener("mouseleave", finishResize, false);
|
document.addEventListener("mouseleave", finishResize, false);
|
||||||
body.addEventListener("mousemove", onMouseMove, false);
|
body.addEventListener("mousemove", onMouseMove, false);
|
||||||
}
|
};
|
||||||
|
|
||||||
_createSizerAndDistributor(resizeHandle) {
|
private onResize = throttle(() => {
|
||||||
|
const distributors = this.getDistributors();
|
||||||
|
|
||||||
|
// relax all items if they had any overconstrained flexboxes
|
||||||
|
distributors.forEach(d => d.start());
|
||||||
|
distributors.forEach(d => d.finish());
|
||||||
|
}, 100, {trailing: true, leading: true});
|
||||||
|
|
||||||
|
public getDistributors = () => {
|
||||||
|
return this.getResizeHandles().map(handle => {
|
||||||
|
const {distributor} = this.createSizerAndDistributor(<HTMLDivElement>handle);
|
||||||
|
return distributor;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private createSizerAndDistributor(
|
||||||
|
resizeHandle: HTMLDivElement,
|
||||||
|
): {sizer: Sizer, distributor: FixedDistributor<any>} {
|
||||||
const vertical = resizeHandle.classList.contains(this.classNames.vertical);
|
const vertical = resizeHandle.classList.contains(this.classNames.vertical);
|
||||||
const reverse = this.isReverseResizeHandle(resizeHandle);
|
const reverse = this.isReverseResizeHandle(resizeHandle);
|
||||||
const Distributor = this.distributorCtor;
|
const Distributor = this.distributorCtor;
|
||||||
const sizer = Distributor.createSizer(this.container, vertical, reverse);
|
const sizer = Distributor.createSizer(this.container, vertical, reverse);
|
||||||
const item = Distributor.createItem(resizeHandle, this, sizer);
|
const item = Distributor.createItem(resizeHandle, this, sizer);
|
||||||
const distributor = new Distributor(item, this.config);
|
const distributor = new Distributor(item);
|
||||||
return {sizer, distributor};
|
return {sizer, distributor};
|
||||||
}
|
}
|
||||||
|
|
||||||
_getResizeHandles() {
|
private getResizeHandles() {
|
||||||
|
if (!this.container.children) return [];
|
||||||
return Array.from(this.container.children).filter(el => {
|
return Array.from(this.container.children).filter(el => {
|
||||||
return this.isResizeHandle(el);
|
return this.isResizeHandle(<HTMLElement>el);
|
||||||
});
|
}) as HTMLElement[];
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018 New Vector Ltd
|
Copyright 2018 - 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -19,18 +19,18 @@ implements DOM/CSS operations for resizing.
|
||||||
The sizer determines what CSS mechanism is used for sizing items, like flexbox, ...
|
The sizer determines what CSS mechanism is used for sizing items, like flexbox, ...
|
||||||
*/
|
*/
|
||||||
export default class Sizer {
|
export default class Sizer {
|
||||||
constructor(container, vertical, reverse) {
|
constructor(
|
||||||
this.container = container;
|
protected readonly container: HTMLElement,
|
||||||
this.reverse = reverse;
|
protected readonly vertical: boolean,
|
||||||
this.vertical = vertical;
|
protected readonly reverse: boolean,
|
||||||
}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@param {Element} item the dom element being resized
|
@param {Element} item the dom element being resized
|
||||||
@return {number} how far the edge of the item is from the edge of the container
|
@return {number} how far the edge of the item is from the edge of the container
|
||||||
*/
|
*/
|
||||||
getItemOffset(item) {
|
public getItemOffset(item: HTMLElement): number {
|
||||||
const offset = (this.vertical ? item.offsetTop : item.offsetLeft) - this._getOffset();
|
const offset = (this.vertical ? item.offsetTop : item.offsetLeft) - this.getOffset();
|
||||||
if (this.reverse) {
|
if (this.reverse) {
|
||||||
return this.getTotalSize() - (offset + this.getItemSize(item));
|
return this.getTotalSize() - (offset + this.getItemSize(item));
|
||||||
} else {
|
} else {
|
||||||
|
@ -42,41 +42,49 @@ export default class Sizer {
|
||||||
@param {Element} item the dom element being resized
|
@param {Element} item the dom element being resized
|
||||||
@return {number} the width/height of an item in the container
|
@return {number} the width/height of an item in the container
|
||||||
*/
|
*/
|
||||||
getItemSize(item) {
|
public getItemSize(item: HTMLElement): number {
|
||||||
return this.vertical ? item.offsetHeight : item.offsetWidth;
|
return this.vertical ? item.offsetHeight : item.offsetWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return {number} the width/height of the container */
|
/** @return {number} the width/height of the container */
|
||||||
getTotalSize() {
|
public getTotalSize(): number {
|
||||||
return this.vertical ? this.container.offsetHeight : this.container.offsetWidth;
|
return this.vertical ? this.container.offsetHeight : this.container.offsetWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return {number} container offset to offsetParent */
|
/** @return {number} container offset to offsetParent */
|
||||||
_getOffset() {
|
private getOffset(): number {
|
||||||
return this.vertical ? this.container.offsetTop : this.container.offsetLeft;
|
return this.vertical ? this.container.offsetTop : this.container.offsetLeft;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return {number} container offset to document */
|
/** @return {number} container offset to document */
|
||||||
_getPageOffset() {
|
private getPageOffset(): number {
|
||||||
let element = this.container;
|
let element = this.container;
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
while (element) {
|
while (element) {
|
||||||
const pos = this.vertical ? element.offsetTop : element.offsetLeft;
|
const pos = this.vertical ? element.offsetTop : element.offsetLeft;
|
||||||
offset = offset + pos;
|
offset = offset + pos;
|
||||||
element = element.offsetParent;
|
element = <HTMLElement>element.offsetParent;
|
||||||
}
|
}
|
||||||
return offset;
|
return offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
setItemSize(item, size) {
|
public getDesiredItemSize(item: HTMLElement) {
|
||||||
if (this.vertical) {
|
if (this.vertical) {
|
||||||
item.style.height = `${Math.round(size)}px`;
|
return item.style.height;
|
||||||
} else {
|
} else {
|
||||||
item.style.width = `${Math.round(size)}px`;
|
return item.style.width;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clearItemSize(item) {
|
public setItemSize(item: HTMLElement, size: string) {
|
||||||
|
if (this.vertical) {
|
||||||
|
item.style.height = size;
|
||||||
|
} else {
|
||||||
|
item.style.width = size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public clearItemSize(item: HTMLElement) {
|
||||||
if (this.vertical) {
|
if (this.vertical) {
|
||||||
item.style.height = null;
|
item.style.height = null;
|
||||||
} else {
|
} else {
|
||||||
|
@ -84,17 +92,21 @@ export default class Sizer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public start(item: HTMLElement) {}
|
||||||
|
|
||||||
|
public finish(item: HTMLElement) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@param {MouseEvent} event the mouse event
|
@param {MouseEvent} event the mouse event
|
||||||
@return {number} the distance between the cursor and the edge of the container,
|
@return {number} the distance between the cursor and the edge of the container,
|
||||||
along the applicable axis (vertical or horizontal)
|
along the applicable axis (vertical or horizontal)
|
||||||
*/
|
*/
|
||||||
offsetFromEvent(event) {
|
public offsetFromEvent(event: MouseEvent) {
|
||||||
const pos = this.vertical ? event.pageY : event.pageX;
|
const pos = this.vertical ? event.pageY : event.pageX;
|
||||||
if (this.reverse) {
|
if (this.reverse) {
|
||||||
return (this._getPageOffset() + this.getTotalSize()) - pos;
|
return (this.getPageOffset() + this.getTotalSize()) - pos;
|
||||||
} else {
|
} else {
|
||||||
return pos - this._getPageOffset();
|
return pos - this.getPageOffset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -23,6 +23,7 @@ import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
|
||||||
import defaultDispatcher from "../dispatcher/dispatcher";
|
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||||
import SettingsStore from "../settings/SettingsStore";
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
import WidgetEchoStore from "../stores/WidgetEchoStore";
|
import WidgetEchoStore from "../stores/WidgetEchoStore";
|
||||||
|
import RoomViewStore from "../stores/RoomViewStore";
|
||||||
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
|
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
|
||||||
import WidgetUtils from "../utils/WidgetUtils";
|
import WidgetUtils from "../utils/WidgetUtils";
|
||||||
import {SettingLevel} from "../settings/SettingLevel";
|
import {SettingLevel} from "../settings/SettingLevel";
|
||||||
|
@ -43,6 +44,8 @@ interface IRoomWidgets {
|
||||||
pinned: Record<string, boolean>;
|
pinned: Record<string, boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const MAX_PINNED = 3;
|
||||||
|
|
||||||
// TODO consolidate WidgetEchoStore into this
|
// TODO consolidate WidgetEchoStore into this
|
||||||
// TODO consolidate ActiveWidgetStore into this
|
// TODO consolidate ActiveWidgetStore into this
|
||||||
export default class WidgetStore extends AsyncStoreWithClient<IState> {
|
export default class WidgetStore extends AsyncStoreWithClient<IState> {
|
||||||
|
@ -65,7 +68,7 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
|
||||||
private initRoom(roomId: string) {
|
private initRoom(roomId: string) {
|
||||||
if (!this.roomMap.has(roomId)) {
|
if (!this.roomMap.has(roomId)) {
|
||||||
this.roomMap.set(roomId, {
|
this.roomMap.set(roomId, {
|
||||||
pinned: {},
|
pinned: {}, // ordered
|
||||||
widgets: [],
|
widgets: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -153,27 +156,34 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
|
||||||
|
|
||||||
public isPinned(widgetId: string) {
|
public isPinned(widgetId: string) {
|
||||||
const roomId = this.getRoomId(widgetId);
|
const roomId = this.getRoomId(widgetId);
|
||||||
const roomInfo = this.getRoom(roomId);
|
return !!this.getPinnedApps(roomId).find(w => w.id === widgetId);
|
||||||
|
|
||||||
let pinned = roomInfo && roomInfo.pinned[widgetId];
|
|
||||||
// Jitsi widgets should be pinned by default
|
|
||||||
const widget = this.widgetMap.get(widgetId);
|
|
||||||
if (pinned === undefined && WidgetType.JITSI.matches(widget?.type)) pinned = true;
|
|
||||||
return pinned;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public canPin(widgetId: string) {
|
public canPin(widgetId: string) {
|
||||||
// only allow pinning up to a max of two as we do not yet have grid splits
|
|
||||||
// the only case it will go to three is if you have two and then a Jitsi gets added
|
|
||||||
const roomId = this.getRoomId(widgetId);
|
const roomId = this.getRoomId(widgetId);
|
||||||
const roomInfo = this.getRoom(roomId);
|
return this.getPinnedApps(roomId).length < MAX_PINNED;
|
||||||
return roomInfo && Object.keys(roomInfo.pinned).filter(k => {
|
|
||||||
return roomInfo.pinned[k] && roomInfo.widgets.some(app => app.id === k);
|
|
||||||
}).length < 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public pinWidget(widgetId: string) {
|
public pinWidget(widgetId: string) {
|
||||||
|
const roomId = this.getRoomId(widgetId);
|
||||||
|
const roomInfo = this.getRoom(roomId);
|
||||||
|
if (!roomInfo) return;
|
||||||
|
|
||||||
|
// When pinning, first confirm all the widgets (Jitsi) which were autopinned so that the order is correct
|
||||||
|
const autoPinned = this.getPinnedApps(roomId).filter(app => !roomInfo.pinned[app.id]);
|
||||||
|
autoPinned.forEach(app => {
|
||||||
|
this.setPinned(app.id, true);
|
||||||
|
});
|
||||||
|
|
||||||
this.setPinned(widgetId, true);
|
this.setPinned(widgetId, true);
|
||||||
|
|
||||||
|
// Show the apps drawer upon the user pinning a widget
|
||||||
|
if (RoomViewStore.getRoomId() === this.getRoomId(widgetId)) {
|
||||||
|
defaultDispatcher.dispatch({
|
||||||
|
action: "appsDrawer",
|
||||||
|
show: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public unpinWidget(widgetId: string) {
|
public unpinWidget(widgetId: string) {
|
||||||
|
@ -184,6 +194,10 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
|
||||||
const roomId = this.getRoomId(widgetId);
|
const roomId = this.getRoomId(widgetId);
|
||||||
const roomInfo = this.getRoom(roomId);
|
const roomInfo = this.getRoom(roomId);
|
||||||
if (!roomInfo) return;
|
if (!roomInfo) return;
|
||||||
|
if (roomInfo.pinned[widgetId] === false && value) {
|
||||||
|
// delete this before write to maintain the correct object insertion order
|
||||||
|
delete roomInfo.pinned[widgetId];
|
||||||
|
}
|
||||||
roomInfo.pinned[widgetId] = value;
|
roomInfo.pinned[widgetId] = value;
|
||||||
|
|
||||||
// Clean up the pinned record
|
// Clean up the pinned record
|
||||||
|
@ -198,13 +212,61 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
|
||||||
this.emit(UPDATE_EVENT);
|
this.emit(UPDATE_EVENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getApps(room: Room, pinned?: boolean): IApp[] {
|
public movePinnedWidget(widgetId: string, delta: 1 | -1) {
|
||||||
const roomInfo = this.getRoom(room.roomId);
|
// TODO simplify this by changing the storage medium of pinned to an array once the Jitsi default-on goes away
|
||||||
if (!roomInfo) return [];
|
const roomId = this.getRoomId(widgetId);
|
||||||
if (pinned) {
|
const roomInfo = this.getRoom(roomId);
|
||||||
return roomInfo.widgets.filter(app => this.isPinned(app.id));
|
if (!roomInfo || roomInfo.pinned[widgetId] === false) return;
|
||||||
|
|
||||||
|
const pinnedApps = this.getPinnedApps(roomId).map(app => app.id);
|
||||||
|
const i = pinnedApps.findIndex(id => id === widgetId);
|
||||||
|
|
||||||
|
if (delta > 0) {
|
||||||
|
pinnedApps.splice(i, 2, pinnedApps[i + 1], pinnedApps[i]);
|
||||||
|
} else {
|
||||||
|
pinnedApps.splice(i - 1, 2, pinnedApps[i], pinnedApps[i - 1]);
|
||||||
}
|
}
|
||||||
return roomInfo.widgets;
|
|
||||||
|
const reorderedPinned: IRoomWidgets["pinned"] = {};
|
||||||
|
pinnedApps.forEach(id => {
|
||||||
|
reorderedPinned[id] = true;
|
||||||
|
});
|
||||||
|
Object.keys(roomInfo.pinned).forEach(id => {
|
||||||
|
if (reorderedPinned[id] === undefined) {
|
||||||
|
reorderedPinned[id] = roomInfo.pinned[id];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
roomInfo.pinned = reorderedPinned;
|
||||||
|
|
||||||
|
SettingsStore.setValue("Widgets.pinned", roomId, SettingLevel.ROOM_ACCOUNT, roomInfo.pinned);
|
||||||
|
this.emit(roomId);
|
||||||
|
this.emit(UPDATE_EVENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPinnedApps(roomId: string): IApp[] {
|
||||||
|
// returns the apps in the order they were pinned with, up to the maximum
|
||||||
|
const roomInfo = this.getRoom(roomId);
|
||||||
|
if (!roomInfo) return [];
|
||||||
|
|
||||||
|
// Show Jitsi widgets even if the user already had the maximum pinned, instead of their latest pinned,
|
||||||
|
// except if the user already explicitly unpinned the Jitsi widget
|
||||||
|
const priorityWidget = roomInfo.widgets.find(widget => {
|
||||||
|
return roomInfo.pinned[widget.id] === undefined && WidgetType.JITSI.matches(widget.type);
|
||||||
|
});
|
||||||
|
|
||||||
|
const order = Object.keys(roomInfo.pinned).filter(k => roomInfo.pinned[k]);
|
||||||
|
let apps = order.map(wId => this.widgetMap.get(wId)).filter(Boolean);
|
||||||
|
apps = apps.slice(0, priorityWidget ? MAX_PINNED - 1 : MAX_PINNED);
|
||||||
|
if (priorityWidget) {
|
||||||
|
apps.push(priorityWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
return apps;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getApps(roomId: string): IApp[] {
|
||||||
|
const roomInfo = this.getRoom(roomId);
|
||||||
|
return roomInfo?.widgets || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public doesRoomHaveConference(room: Room): boolean {
|
public doesRoomHaveConference(room: Room): boolean {
|
||||||
|
|
|
@ -40,10 +40,12 @@ export default class ResizeNotifier extends EventEmitter {
|
||||||
|
|
||||||
startResizing() {
|
startResizing() {
|
||||||
this._isResizing = true;
|
this._isResizing = true;
|
||||||
|
this.emit("isResizing", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
stopResizing() {
|
stopResizing() {
|
||||||
this._isResizing = false;
|
this._isResizing = false;
|
||||||
|
this.emit("isResizing", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
_noisyMiddlePanel() {
|
_noisyMiddlePanel() {
|
||||||
|
|
|
@ -488,6 +488,7 @@ export default class WidgetUtils {
|
||||||
'avatarUrl=$matrix_avatar_url',
|
'avatarUrl=$matrix_avatar_url',
|
||||||
'userId=$matrix_user_id',
|
'userId=$matrix_user_id',
|
||||||
'roomId=$matrix_room_id',
|
'roomId=$matrix_room_id',
|
||||||
|
'theme=$theme',
|
||||||
];
|
];
|
||||||
if (opts.auth) {
|
if (opts.auth) {
|
||||||
queryStringParts.push(`auth=${opts.auth}`);
|
queryStringParts.push(`auth=${opts.auth}`);
|
||||||
|
@ -523,4 +524,16 @@ export default class WidgetUtils {
|
||||||
IntegrationManagers.sharedInstance().getPrimaryManager().open(room, 'type_' + app.type, app.id);
|
IntegrationManagers.sharedInstance().getPrimaryManager().open(room, 'type_' + app.type, app.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static isManagedByManager(app) {
|
||||||
|
if (WidgetUtils.isScalarUrl(app.url)) {
|
||||||
|
const managers = IntegrationManagers.sharedInstance();
|
||||||
|
if (managers.hasManager()) {
|
||||||
|
// TODO: Pick the right manager for the widget
|
||||||
|
const defaultManager = managers.getPrimaryManager();
|
||||||
|
return WidgetUtils.isScalarUrl(defaultManager.apiUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6507,7 +6507,7 @@ mathml-tag-names@^2.0.1:
|
||||||
|
|
||||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
|
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
|
||||||
version "8.5.0"
|
version "8.5.0"
|
||||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/d8c4101fdd521e189f4755c6f02a8971b991ef5f"
|
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/9f713781cdfea2349115ffaac2d665e8b07fd5dc"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.11.2"
|
"@babel/runtime" "^7.11.2"
|
||||||
another-json "^0.2.0"
|
another-json "^0.2.0"
|
||||||
|
|