mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-16 13:14:58 +08:00
Merge pull request #6262 from matrix-org/gsouquet/fix-backdrop-filter
Move backdrop filter to a canvas based solution
This commit is contained in:
commit
939874167a
@ -64,6 +64,7 @@
|
|||||||
"cheerio": "^1.0.0-rc.9",
|
"cheerio": "^1.0.0-rc.9",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"commonmark": "^0.29.3",
|
"commonmark": "^0.29.3",
|
||||||
|
"context-filter-polyfill": "^0.2.4",
|
||||||
"counterpart": "^0.18.6",
|
"counterpart": "^0.18.6",
|
||||||
"diff-dom": "^4.2.2",
|
"diff-dom": "^4.2.2",
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
@ -195,6 +196,7 @@
|
|||||||
"decoderWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
|
"decoderWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
|
||||||
"decoderWorker\\.min\\.wasm": "<rootDir>/__mocks__/empty.js",
|
"decoderWorker\\.min\\.wasm": "<rootDir>/__mocks__/empty.js",
|
||||||
"waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
|
"waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
|
||||||
|
"context-filter-polyfill": "<rootDir>/__mocks__/empty.js",
|
||||||
"workers/(.+)\\.worker\\.ts": "<rootDir>/__mocks__/workerMock.js",
|
"workers/(.+)\\.worker\\.ts": "<rootDir>/__mocks__/workerMock.js",
|
||||||
"RecorderWorklet": "<rootDir>/__mocks__/empty.js"
|
"RecorderWorklet": "<rootDir>/__mocks__/empty.js"
|
||||||
},
|
},
|
||||||
|
@ -168,7 +168,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
|||||||
// it has the appearance of a text box so the controls
|
// it has the appearance of a text box so the controls
|
||||||
// appear to be part of the input
|
// appear to be part of the input
|
||||||
|
|
||||||
.mx_Dialog, .mx_MatrixChat {
|
.mx_Dialog, .mx_MatrixChat_wrapper {
|
||||||
.mx_textinput > input[type=text],
|
.mx_textinput > input[type=text],
|
||||||
.mx_textinput > input[type=search] {
|
.mx_textinput > input[type=search] {
|
||||||
border: none;
|
border: none;
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
@import "./structures/_LeftPanelWidget.scss";
|
@import "./structures/_LeftPanelWidget.scss";
|
||||||
@import "./structures/_MainSplit.scss";
|
@import "./structures/_MainSplit.scss";
|
||||||
@import "./structures/_MatrixChat.scss";
|
@import "./structures/_MatrixChat.scss";
|
||||||
|
@import "./structures/_BackdropPanel.scss";
|
||||||
@import "./structures/_MyGroups.scss";
|
@import "./structures/_MyGroups.scss";
|
||||||
@import "./structures/_NonUrgentToastContainer.scss";
|
@import "./structures/_NonUrgentToastContainer.scss";
|
||||||
@import "./structures/_NotificationPanel.scss";
|
@import "./structures/_NotificationPanel.scss";
|
||||||
|
51
res/css/structures/_BackdropPanel.scss
Normal file
51
res/css/structures/_BackdropPanel.scss
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_BackdropPanel {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: " ";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--lp-background-overlay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_BackdropPanel--canvas {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
min-height: 100%;
|
||||||
|
z-index: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:nth-of-type(2n-1) {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
&:nth-of-type(2n) {
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
}
|
@ -14,10 +14,17 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
.mx_MatrixChat--with-avatar {
|
||||||
|
.mx_GroupFilterPanel {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_GroupFilterPanel {
|
.mx_GroupFilterPanel {
|
||||||
flex: 1;
|
|
||||||
background-color: $groupFilterPanel-bg-color;
|
background-color: $groupFilterPanel-bg-color;
|
||||||
|
flex: 1;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -17,15 +17,22 @@ limitations under the License.
|
|||||||
$groupFilterPanelWidth: 56px; // only applies in this file, used for calculations
|
$groupFilterPanelWidth: 56px; // only applies in this file, used for calculations
|
||||||
$roomListCollapsedWidth: 68px;
|
$roomListCollapsedWidth: 68px;
|
||||||
|
|
||||||
|
.mx_MatrixChat--with-avatar {
|
||||||
|
.mx_LeftPanel,
|
||||||
|
.mx_LeftPanel .mx_LeftPanel_roomListContainer {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_LeftPanel {
|
.mx_LeftPanel {
|
||||||
background-color: $roomlist-bg-color;
|
background-color: $roomlist-bg-color;
|
||||||
// TODO decrease this once Spaces launches as it'll no longer need to include the 56px Community Panel
|
// TODO decrease this once Spaces launches as it'll no longer need to include the 56px Community Panel
|
||||||
min-width: 206px;
|
min-width: 206px;
|
||||||
max-width: 50%;
|
|
||||||
|
|
||||||
// Create a row-based flexbox for the GroupFilterPanel and the room list
|
// Create a row-based flexbox for the GroupFilterPanel and the room list
|
||||||
display: flex;
|
display: flex;
|
||||||
contain: content;
|
contain: content;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
.mx_LeftPanel_GroupFilterPanelContainer {
|
.mx_LeftPanel_GroupFilterPanelContainer {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
|
@ -29,8 +29,6 @@ limitations under the License.
|
|||||||
.mx_MatrixChat_wrapper {
|
.mx_MatrixChat_wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
@ -42,15 +40,16 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mx_MatrixChat {
|
.mx_MatrixChat {
|
||||||
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
order: 2;
|
|
||||||
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
flex-grow: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
max-width: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MatrixChat_syncError {
|
.mx_MatrixChat_syncError {
|
||||||
|
@ -18,6 +18,8 @@ limitations under the License.
|
|||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,11 +22,18 @@ $activeBorderTransparentGap: 1px;
|
|||||||
$activeBackgroundColor: $roomtile-selected-bg-color;
|
$activeBackgroundColor: $roomtile-selected-bg-color;
|
||||||
$activeBorderColor: $secondary-fg-color;
|
$activeBorderColor: $secondary-fg-color;
|
||||||
|
|
||||||
|
.mx_MatrixChat--with-avatar {
|
||||||
|
.mx_SpacePanel {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_SpacePanel {
|
.mx_SpacePanel {
|
||||||
flex: 0 0 auto;
|
|
||||||
background-color: $groupFilterPanel-bg-color;
|
background-color: $groupFilterPanel-bg-color;
|
||||||
|
flex: 0 0 auto;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
// Create another flexbox so the Panel fills the container
|
// Create another flexbox so the Panel fills the container
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -238,9 +238,12 @@ $voice-playback-button-fg-color: $message-body-panel-icon-fg-color;
|
|||||||
// Appearance tab colors
|
// Appearance tab colors
|
||||||
$appearance-tab-border-color: $room-highlight-color;
|
$appearance-tab-border-color: $room-highlight-color;
|
||||||
|
|
||||||
// blur amounts for left left panel (only for element theme, used in _mods.scss)
|
// blur amounts for left left panel (only for element theme)
|
||||||
$roomlist-background-blur-amount: 60px;
|
:root {
|
||||||
$groupFilterPanel-background-blur-amount: 30px;
|
--llp-background-blur: 160px;
|
||||||
|
--lp-background-blur: 90px;
|
||||||
|
--lp-background-overlay: rgba(255, 255, 255, 0.055);
|
||||||
|
}
|
||||||
|
|
||||||
$composer-shadow-color: rgba(0, 0, 0, 0.28);
|
$composer-shadow-color: rgba(0, 0, 0, 0.28);
|
||||||
|
|
||||||
|
@ -2,10 +2,6 @@
|
|||||||
@import "../../light/css/_paths.scss";
|
@import "../../light/css/_paths.scss";
|
||||||
@import "../../light/css/_fonts.scss";
|
@import "../../light/css/_fonts.scss";
|
||||||
@import "../../light/css/_light.scss";
|
@import "../../light/css/_light.scss";
|
||||||
// important this goes before _mods,
|
|
||||||
// as $groupFilterPanel-background-blur-amount and
|
|
||||||
// $roomlist-background-blur-amount
|
|
||||||
// are overridden in _dark.scss
|
|
||||||
@import "_dark.scss";
|
@import "_dark.scss";
|
||||||
@import "../../light/css/_mods.scss";
|
@import "../../light/css/_mods.scss";
|
||||||
@import "../../../../res/css/_components.scss";
|
@import "../../../../res/css/_components.scss";
|
||||||
|
@ -361,10 +361,12 @@ $voice-playback-button-fg-color: $message-body-panel-icon-fg-color;
|
|||||||
// FontSlider colors
|
// FontSlider colors
|
||||||
$appearance-tab-border-color: $input-darker-bg-color;
|
$appearance-tab-border-color: $input-darker-bg-color;
|
||||||
|
|
||||||
// blur amounts for left left panel (only for element theme, used in _mods.scss)
|
// blur amounts for left left panel (only for element theme)
|
||||||
$roomlist-background-blur-amount: 40px;
|
:root {
|
||||||
$groupFilterPanel-background-blur-amount: 20px;
|
--llp-background-blur: 120px;
|
||||||
|
--lp-background-blur: 60px;
|
||||||
|
--lp-background-overlay: rgba(0, 0, 0, 0.055);
|
||||||
|
}
|
||||||
$composer-shadow-color: rgba(0, 0, 0, 0.04);
|
$composer-shadow-color: rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
// Bubble tiles
|
// Bubble tiles
|
||||||
|
@ -4,27 +4,6 @@
|
|||||||
// set the user avatar (if any) as a background so
|
// set the user avatar (if any) as a background so
|
||||||
// it can be blurred by the tag panel and room list
|
// it can be blurred by the tag panel and room list
|
||||||
|
|
||||||
@supports (backdrop-filter: none) {
|
|
||||||
.mx_LeftPanel {
|
|
||||||
background-image: var(--avatar-url, unset);
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: cover;
|
|
||||||
background-position: left top;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_GroupFilterPanel {
|
|
||||||
backdrop-filter: blur($groupFilterPanel-background-blur-amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_SpacePanel {
|
|
||||||
backdrop-filter: blur($groupFilterPanel-background-blur-amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_LeftPanel .mx_LeftPanel_roomListContainer {
|
|
||||||
backdrop-filter: blur($roomlist-background-blur-amount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomSublist_showNButton {
|
.mx_RoomSublist_showNButton {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
|
165
src/components/structures/BackdropPanel.tsx
Normal file
165
src/components/structures/BackdropPanel.tsx
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { createRef } from "react";
|
||||||
|
import "context-filter-polyfill";
|
||||||
|
|
||||||
|
import UIStore from "../../stores/UIStore";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
backgroundImage?: CanvasImageSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
// Left Panel image
|
||||||
|
lpImage?: string;
|
||||||
|
// Left-left panel image
|
||||||
|
llpImage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class BackdropPanel extends React.PureComponent<IProps, IState> {
|
||||||
|
private leftLeftPanelRef = createRef<HTMLCanvasElement>();
|
||||||
|
private leftPanelRef = createRef<HTMLCanvasElement>();
|
||||||
|
|
||||||
|
private sizes = {
|
||||||
|
leftLeftPanelWidth: 0,
|
||||||
|
leftPanelWidth: 0,
|
||||||
|
height: 0,
|
||||||
|
};
|
||||||
|
private style = getComputedStyle(document.documentElement);
|
||||||
|
|
||||||
|
public state: IState = {};
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
UIStore.instance.on("SpacePanel", this.onResize);
|
||||||
|
UIStore.instance.on("GroupFilterPanelContainer", this.onResize);
|
||||||
|
this.onResize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
UIStore.instance.off("SpacePanel", this.onResize);
|
||||||
|
UIStore.instance.on("GroupFilterPanelContainer", this.onResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate(prevProps: IProps) {
|
||||||
|
if (prevProps.backgroundImage !== this.props.backgroundImage) {
|
||||||
|
this.setState({});
|
||||||
|
this.onResize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onResize = () => {
|
||||||
|
if (this.props.backgroundImage) {
|
||||||
|
const groupFilterPanelDimensions = UIStore.instance.getElementDimensions("GroupFilterPanelContainer");
|
||||||
|
const spacePanelDimensions = UIStore.instance.getElementDimensions("SpacePanel");
|
||||||
|
const roomListDimensions = UIStore.instance.getElementDimensions("LeftPanel");
|
||||||
|
this.sizes = {
|
||||||
|
leftLeftPanelWidth: spacePanelDimensions?.width ?? groupFilterPanelDimensions?.width ?? 0,
|
||||||
|
leftPanelWidth: roomListDimensions?.width ?? 0,
|
||||||
|
height: UIStore.instance.windowHeight,
|
||||||
|
};
|
||||||
|
this.refreshBackdropImage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private refreshBackdropImage = (): void => {
|
||||||
|
const leftLeftPanelContext = this.leftLeftPanelRef.current.getContext("2d");
|
||||||
|
const leftPanelContext = this.leftPanelRef.current.getContext("2d");
|
||||||
|
const { leftLeftPanelWidth, leftPanelWidth, height } = this.sizes;
|
||||||
|
const width = leftLeftPanelWidth + leftPanelWidth;
|
||||||
|
const { backgroundImage } = this.props;
|
||||||
|
|
||||||
|
const imageWidth = (backgroundImage as ImageBitmap).width;
|
||||||
|
const imageHeight = (backgroundImage as ImageBitmap).height;
|
||||||
|
|
||||||
|
const contentRatio = imageWidth / imageHeight;
|
||||||
|
const containerRatio = width / height;
|
||||||
|
let resultHeight;
|
||||||
|
let resultWidth;
|
||||||
|
if (contentRatio > containerRatio) {
|
||||||
|
resultHeight = height;
|
||||||
|
resultWidth = height * contentRatio;
|
||||||
|
} else {
|
||||||
|
resultWidth = width;
|
||||||
|
resultHeight = width / contentRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This value has been chosen to be as close with rendering as the css-only
|
||||||
|
// backdrop-filter: blur effect was, mostly takes effect for vertical pictures.
|
||||||
|
const x = width * 0.1;
|
||||||
|
const y = (height - resultHeight) / 2;
|
||||||
|
|
||||||
|
this.leftLeftPanelRef.current.width = leftLeftPanelWidth;
|
||||||
|
this.leftLeftPanelRef.current.height = height;
|
||||||
|
this.leftPanelRef.current.width = (window.screen.width * 0.5);
|
||||||
|
this.leftPanelRef.current.height = height;
|
||||||
|
|
||||||
|
const spacesBlur = this.style.getPropertyValue('--llp-background-blur');
|
||||||
|
const roomListBlur = this.style.getPropertyValue('--lp-background-blur');
|
||||||
|
|
||||||
|
leftLeftPanelContext.filter = `blur(${spacesBlur})`;
|
||||||
|
leftPanelContext.filter = `blur(${roomListBlur})`;
|
||||||
|
leftLeftPanelContext.drawImage(
|
||||||
|
backgroundImage,
|
||||||
|
0, 0,
|
||||||
|
imageWidth, imageHeight,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
resultWidth,
|
||||||
|
resultHeight,
|
||||||
|
);
|
||||||
|
leftPanelContext.drawImage(
|
||||||
|
backgroundImage,
|
||||||
|
0, 0,
|
||||||
|
imageWidth, imageHeight,
|
||||||
|
x - leftLeftPanelWidth,
|
||||||
|
y,
|
||||||
|
resultWidth,
|
||||||
|
resultHeight,
|
||||||
|
);
|
||||||
|
this.setState({
|
||||||
|
lpImage: this.leftPanelRef.current.toDataURL('image/jpeg', 1),
|
||||||
|
llpImage: this.leftLeftPanelRef.current.toDataURL('image/jpeg', 1),
|
||||||
|
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
if (!this.props.backgroundImage) return null;
|
||||||
|
return <div className="mx_BackdropPanel">
|
||||||
|
<img
|
||||||
|
className="mx_BackdropPanel--canvas"
|
||||||
|
src={this.state.llpImage} />
|
||||||
|
<img
|
||||||
|
className="mx_BackdropPanel--canvas"
|
||||||
|
src={this.state.lpImage} />
|
||||||
|
<canvas
|
||||||
|
ref={this.leftLeftPanelRef}
|
||||||
|
className="mx_BackdropPanel--canvas"
|
||||||
|
style={{
|
||||||
|
display: this.state.lpImage ? 'none' : 'block',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<canvas
|
||||||
|
style={{
|
||||||
|
display: this.state.lpImage ? 'none' : 'block',
|
||||||
|
}}
|
||||||
|
ref={this.leftPanelRef}
|
||||||
|
className="mx_BackdropPanel--canvas"
|
||||||
|
/>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { EventSubscription } from "fbemitter";
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import GroupFilterOrderStore from '../../stores/GroupFilterOrderStore';
|
import GroupFilterOrderStore from '../../stores/GroupFilterOrderStore';
|
||||||
|
|
||||||
@ -30,22 +31,43 @@ import AutoHideScrollbar from "./AutoHideScrollbar";
|
|||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import UserTagTile from "../views/elements/UserTagTile";
|
import UserTagTile from "../views/elements/UserTagTile";
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
|
import UIStore from "../../stores/UIStore";
|
||||||
|
|
||||||
|
interface IGroupFilterPanelProps {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Properly type this after migrating GroupFilterOrderStore.js to Typescript
|
||||||
|
type OrderedTagsTemporaryType = Array<{}>;
|
||||||
|
// FIXME: Properly type this after migrating GroupFilterOrderStore.js to Typescript
|
||||||
|
type SelectedTagsTemporaryType = Array<{}>;
|
||||||
|
|
||||||
|
interface IGroupFilterPanelState {
|
||||||
|
// FIXME: Properly type this after migrating GroupFilterOrderStore.js to Typescript
|
||||||
|
orderedTags: OrderedTagsTemporaryType;
|
||||||
|
// FIXME: Properly type this after migrating GroupFilterOrderStore.js to Typescript
|
||||||
|
selectedTags: SelectedTagsTemporaryType;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.GroupFilterPanel")
|
@replaceableComponent("structures.GroupFilterPanel")
|
||||||
class GroupFilterPanel extends React.Component {
|
class GroupFilterPanel extends React.Component<IGroupFilterPanelProps, IGroupFilterPanelState> {
|
||||||
static contextType = MatrixClientContext;
|
public static contextType = MatrixClientContext;
|
||||||
|
|
||||||
state = {
|
public state = {
|
||||||
orderedTags: [],
|
orderedTags: [],
|
||||||
selectedTags: [],
|
selectedTags: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
private ref = React.createRef<HTMLDivElement>();
|
||||||
this.unmounted = false;
|
private unmounted = false;
|
||||||
this.context.on("Group.myMembership", this._onGroupMyMembership);
|
private groupFilterOrderStoreToken?: EventSubscription;
|
||||||
this.context.on("sync", this._onClientSync);
|
|
||||||
|
|
||||||
this._groupFilterOrderStoreToken = GroupFilterOrderStore.addListener(() => {
|
public componentDidMount() {
|
||||||
|
this.unmounted = false;
|
||||||
|
this.context.on("Group.myMembership", this.onGroupMyMembership);
|
||||||
|
this.context.on("sync", this.onClientSync);
|
||||||
|
|
||||||
|
this.groupFilterOrderStoreToken = GroupFilterOrderStore.addListener(() => {
|
||||||
if (this.unmounted) {
|
if (this.unmounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -56,23 +78,25 @@ class GroupFilterPanel extends React.Component {
|
|||||||
});
|
});
|
||||||
// This could be done by anything with a matrix client
|
// This could be done by anything with a matrix client
|
||||||
dis.dispatch(GroupActions.fetchJoinedGroups(this.context));
|
dis.dispatch(GroupActions.fetchJoinedGroups(this.context));
|
||||||
|
UIStore.instance.trackElementDimensions("GroupPanel", this.ref.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
this.unmounted = true;
|
this.unmounted = true;
|
||||||
this.context.removeListener("Group.myMembership", this._onGroupMyMembership);
|
this.context.removeListener("Group.myMembership", this.onGroupMyMembership);
|
||||||
this.context.removeListener("sync", this._onClientSync);
|
this.context.removeListener("sync", this.onClientSync);
|
||||||
if (this._groupFilterOrderStoreToken) {
|
if (this.groupFilterOrderStoreToken) {
|
||||||
this._groupFilterOrderStoreToken.remove();
|
this.groupFilterOrderStoreToken.remove();
|
||||||
}
|
}
|
||||||
|
UIStore.instance.stopTrackingElementDimensions("GroupPanel");
|
||||||
}
|
}
|
||||||
|
|
||||||
_onGroupMyMembership = () => {
|
private onGroupMyMembership = () => {
|
||||||
if (this.unmounted) return;
|
if (this.unmounted) return;
|
||||||
dis.dispatch(GroupActions.fetchJoinedGroups(this.context));
|
dis.dispatch(GroupActions.fetchJoinedGroups(this.context));
|
||||||
};
|
};
|
||||||
|
|
||||||
_onClientSync = (syncState, prevState) => {
|
private onClientSync = (syncState, prevState) => {
|
||||||
// Consider the client reconnected if there is no error with syncing.
|
// Consider the client reconnected if there is no error with syncing.
|
||||||
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
|
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
|
||||||
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
||||||
@ -82,18 +106,18 @@ class GroupFilterPanel extends React.Component {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onClick = e => {
|
private onClick = e => {
|
||||||
// only dispatch if its not a no-op
|
// only dispatch if its not a no-op
|
||||||
if (this.state.selectedTags.length > 0) {
|
if (this.state.selectedTags.length > 0) {
|
||||||
dis.dispatch({ action: 'deselect_tags' });
|
dis.dispatch({ action: 'deselect_tags' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onClearFilterClick = ev => {
|
private onClearFilterClick = ev => {
|
||||||
dis.dispatch({ action: 'deselect_tags' });
|
dis.dispatch({ action: 'deselect_tags' });
|
||||||
};
|
};
|
||||||
|
|
||||||
renderGlobalIcon() {
|
private renderGlobalIcon() {
|
||||||
if (!SettingsStore.getValue("feature_communities_v2_prototypes")) return null;
|
if (!SettingsStore.getValue("feature_communities_v2_prototypes")) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -104,7 +128,7 @@ class GroupFilterPanel extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render() {
|
||||||
const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
|
const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
|
||||||
const ActionButton = sdk.getComponent('elements.ActionButton');
|
const ActionButton = sdk.getComponent('elements.ActionButton');
|
||||||
|
|
||||||
@ -147,7 +171,7 @@ class GroupFilterPanel extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className={classes} onClick={this.onClearFilterClick}>
|
return <div className={classes} onClick={this.onClearFilterClick} ref={this.ref}>
|
||||||
<AutoHideScrollbar
|
<AutoHideScrollbar
|
||||||
className="mx_GroupFilterPanel_scroller"
|
className="mx_GroupFilterPanel_scroller"
|
||||||
onClick={this.onClick}
|
onClick={this.onClick}
|
@ -37,11 +37,9 @@ import SettingsStore from "../../settings/SettingsStore";
|
|||||||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore";
|
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore";
|
||||||
import IndicatorScrollbar from "../structures/IndicatorScrollbar";
|
import IndicatorScrollbar from "../structures/IndicatorScrollbar";
|
||||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||||
import { OwnProfileStore } from "../../stores/OwnProfileStore";
|
|
||||||
import RoomListNumResults from "../views/rooms/RoomListNumResults";
|
import RoomListNumResults from "../views/rooms/RoomListNumResults";
|
||||||
import LeftPanelWidget from "./LeftPanelWidget";
|
import LeftPanelWidget from "./LeftPanelWidget";
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
import { mediaFromMxc } from "../../customisations/Media";
|
|
||||||
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore";
|
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore";
|
||||||
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
|
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
|
||||||
import UIStore from "../../stores/UIStore";
|
import UIStore from "../../stores/UIStore";
|
||||||
@ -71,6 +69,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||||||
private ref: React.RefObject<HTMLDivElement> = createRef();
|
private ref: React.RefObject<HTMLDivElement> = createRef();
|
||||||
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
|
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
|
||||||
private groupFilterPanelWatcherRef: string;
|
private groupFilterPanelWatcherRef: string;
|
||||||
|
private groupFilterPanelContainer = createRef<HTMLDivElement>();
|
||||||
private bgImageWatcherRef: string;
|
private bgImageWatcherRef: string;
|
||||||
private focusedElement = null;
|
private focusedElement = null;
|
||||||
private isDoingStickyHeaders = false;
|
private isDoingStickyHeaders = false;
|
||||||
@ -86,17 +85,19 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||||
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||||
OwnProfileStore.instance.on(UPDATE_EVENT, this.onBackgroundImageUpdate);
|
|
||||||
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
|
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
|
||||||
this.bgImageWatcherRef = SettingsStore.watchSetting(
|
|
||||||
"RoomList.backgroundImage", null, this.onBackgroundImageUpdate);
|
|
||||||
this.groupFilterPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => {
|
this.groupFilterPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => {
|
||||||
this.setState({ showGroupFilterPanel: SettingsStore.getValue("TagPanel.enableTagPanel") });
|
this.setState({ showGroupFilterPanel: SettingsStore.getValue("TagPanel.enableTagPanel") });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
|
UIStore.instance.trackElementDimensions("LeftPanel", this.ref.current);
|
||||||
UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current);
|
UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current);
|
||||||
|
if (this.groupFilterPanelContainer.current) {
|
||||||
|
const componentName = "GroupFilterPanelContainer";
|
||||||
|
UIStore.instance.trackElementDimensions(componentName, this.groupFilterPanelContainer.current);
|
||||||
|
}
|
||||||
UIStore.instance.on("ListContainer", this.refreshStickyHeaders);
|
UIStore.instance.on("ListContainer", this.refreshStickyHeaders);
|
||||||
// Using the passive option to not block the main thread
|
// Using the passive option to not block the main thread
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
|
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
|
||||||
@ -105,10 +106,8 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
SettingsStore.unwatchSetting(this.groupFilterPanelWatcherRef);
|
SettingsStore.unwatchSetting(this.groupFilterPanelWatcherRef);
|
||||||
SettingsStore.unwatchSetting(this.bgImageWatcherRef);
|
|
||||||
BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||||
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||||
OwnProfileStore.instance.off(UPDATE_EVENT, this.onBackgroundImageUpdate);
|
|
||||||
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
|
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
|
||||||
UIStore.instance.stopTrackingElementDimensions("ListContainer");
|
UIStore.instance.stopTrackingElementDimensions("ListContainer");
|
||||||
UIStore.instance.removeListener("ListContainer", this.refreshStickyHeaders);
|
UIStore.instance.removeListener("ListContainer", this.refreshStickyHeaders);
|
||||||
@ -149,23 +148,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onBackgroundImageUpdate = () => {
|
|
||||||
// Note: we do this in the LeftPanel as it uses this variable most prominently.
|
|
||||||
const avatarSize = 32; // arbitrary
|
|
||||||
let avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize);
|
|
||||||
const settingBgMxc = SettingsStore.getValue("RoomList.backgroundImage");
|
|
||||||
if (settingBgMxc) {
|
|
||||||
avatarUrl = mediaFromMxc(settingBgMxc).getSquareThumbnailHttp(avatarSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
const avatarUrlProp = `url(${avatarUrl})`;
|
|
||||||
if (!avatarUrl) {
|
|
||||||
document.body.style.removeProperty("--avatar-url");
|
|
||||||
} else if (document.body.style.getPropertyValue("--avatar-url") !== avatarUrlProp) {
|
|
||||||
document.body.style.setProperty("--avatar-url", avatarUrlProp);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private handleStickyHeaders(list: HTMLDivElement) {
|
private handleStickyHeaders(list: HTMLDivElement) {
|
||||||
if (this.isDoingStickyHeaders) return;
|
if (this.isDoingStickyHeaders) return;
|
||||||
this.isDoingStickyHeaders = true;
|
this.isDoingStickyHeaders = true;
|
||||||
@ -443,7 +425,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||||||
let leftLeftPanel;
|
let leftLeftPanel;
|
||||||
if (this.state.showGroupFilterPanel) {
|
if (this.state.showGroupFilterPanel) {
|
||||||
leftLeftPanel = (
|
leftLeftPanel = (
|
||||||
<div className="mx_LeftPanel_GroupFilterPanelContainer">
|
<div className="mx_LeftPanel_GroupFilterPanelContainer" ref={this.groupFilterPanelContainer}>
|
||||||
<GroupFilterPanel />
|
<GroupFilterPanel />
|
||||||
{ SettingsStore.getValue("feature_custom_tags") ? <CustomRoomTagPanel /> : null }
|
{ SettingsStore.getValue("feature_custom_tags") ? <CustomRoomTagPanel /> : null }
|
||||||
</div>
|
</div>
|
||||||
|
@ -58,12 +58,16 @@ import { replaceableComponent } from "../../utils/replaceableComponent";
|
|||||||
import CallHandler from '../../CallHandler';
|
import CallHandler from '../../CallHandler';
|
||||||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
import AudioFeedArrayForCall from '../views/voip/AudioFeedArrayForCall';
|
import AudioFeedArrayForCall from '../views/voip/AudioFeedArrayForCall';
|
||||||
|
import { OwnProfileStore } from '../../stores/OwnProfileStore';
|
||||||
|
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||||
import RoomView from './RoomView';
|
import RoomView from './RoomView';
|
||||||
import ToastContainer from './ToastContainer';
|
import ToastContainer from './ToastContainer';
|
||||||
import MyGroups from "./MyGroups";
|
import MyGroups from "./MyGroups";
|
||||||
import UserView from "./UserView";
|
import UserView from "./UserView";
|
||||||
import GroupView from "./GroupView";
|
import GroupView from "./GroupView";
|
||||||
|
import BackdropPanel from "./BackdropPanel";
|
||||||
import SpaceStore from "../../stores/SpaceStore";
|
import SpaceStore from "../../stores/SpaceStore";
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
// 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.
|
||||||
@ -127,6 +131,7 @@ interface IState {
|
|||||||
usageLimitEventTs?: number;
|
usageLimitEventTs?: number;
|
||||||
useCompactLayout: boolean;
|
useCompactLayout: boolean;
|
||||||
activeCalls: Array<MatrixCall>;
|
activeCalls: Array<MatrixCall>;
|
||||||
|
backgroundImage?: CanvasImageSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -193,7 +198,10 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
this.resizer = this.createResizer();
|
this.resizer = this.createResizer();
|
||||||
this.resizer.attach();
|
this.resizer.attach();
|
||||||
|
|
||||||
|
OwnProfileStore.instance.on(UPDATE_EVENT, this.refreshBackgroundImage);
|
||||||
this.loadResizerPreferences();
|
this.loadResizerPreferences();
|
||||||
|
this.refreshBackgroundImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
@ -202,10 +210,17 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||||||
this._matrixClient.removeListener("accountData", this.onAccountData);
|
this._matrixClient.removeListener("accountData", this.onAccountData);
|
||||||
this._matrixClient.removeListener("sync", this.onSync);
|
this._matrixClient.removeListener("sync", this.onSync);
|
||||||
this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents);
|
this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents);
|
||||||
|
OwnProfileStore.instance.off(UPDATE_EVENT, this.refreshBackgroundImage);
|
||||||
SettingsStore.unwatchSetting(this.compactLayoutWatcherRef);
|
SettingsStore.unwatchSetting(this.compactLayoutWatcherRef);
|
||||||
this.resizer.detach();
|
this.resizer.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private refreshBackgroundImage = async (): Promise<void> => {
|
||||||
|
this.setState({
|
||||||
|
backgroundImage: await OwnProfileStore.instance.getAvatarBitmap(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
private onAction = (payload): void => {
|
private onAction = (payload): void => {
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'call_state': {
|
case 'call_state': {
|
||||||
@ -608,10 +623,11 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let bodyClasses = 'mx_MatrixChat';
|
const bodyClasses = classNames({
|
||||||
if (this.state.useCompactLayout) {
|
'mx_MatrixChat': true,
|
||||||
bodyClasses += ' mx_MatrixChat_useCompactLayout';
|
'mx_MatrixChat_useCompactLayout': this.state.useCompactLayout,
|
||||||
}
|
'mx_MatrixChat--with-avatar': this.state.backgroundImage,
|
||||||
|
});
|
||||||
|
|
||||||
const audioFeedArraysForCalls = this.state.activeCalls.map((call) => {
|
const audioFeedArraysForCalls = this.state.activeCalls.map((call) => {
|
||||||
return (
|
return (
|
||||||
@ -629,14 +645,17 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||||||
>
|
>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<div ref={this._resizeContainer} className={bodyClasses}>
|
<div ref={this._resizeContainer} className={bodyClasses}>
|
||||||
|
<BackdropPanel
|
||||||
|
backgroundImage={this.state.backgroundImage}
|
||||||
|
/>
|
||||||
{ SpaceStore.spacesEnabled ? <SpacePanel /> : null }
|
{ SpaceStore.spacesEnabled ? <SpacePanel /> : null }
|
||||||
<LeftPanel
|
<LeftPanel
|
||||||
isMinimized={this.props.collapseLhs || false}
|
isMinimized={this.props.collapseLhs || false}
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
/>
|
/>
|
||||||
<ResizeHandle />
|
<ResizeHandle />
|
||||||
{ pageElement }
|
|
||||||
</div>
|
</div>
|
||||||
|
{ pageElement }
|
||||||
</div>
|
</div>
|
||||||
<CallContainer />
|
<CallContainer />
|
||||||
<NonUrgentToastContainer />
|
<NonUrgentToastContainer />
|
||||||
|
@ -14,7 +14,16 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { ComponentProps, Dispatch, ReactNode, SetStateAction, useEffect, useState } from "react";
|
import React, {
|
||||||
|
ComponentProps,
|
||||||
|
Dispatch,
|
||||||
|
ReactNode,
|
||||||
|
SetStateAction,
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import { DragDropContext, Draggable, Droppable } from "react-beautiful-dnd";
|
import { DragDropContext, Draggable, Droppable } from "react-beautiful-dnd";
|
||||||
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";
|
||||||
@ -43,6 +52,7 @@ import IconizedContextMenu, {
|
|||||||
} from "../context_menus/IconizedContextMenu";
|
} from "../context_menus/IconizedContextMenu";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||||
|
import UIStore from "../../../stores/UIStore";
|
||||||
|
|
||||||
const useSpaces = (): [Room[], Room[], Room | null] => {
|
const useSpaces = (): [Room[], Room[], Room | null] => {
|
||||||
const invites = useEventEmitterState<Room[]>(SpaceStore.instance, UPDATE_INVITED_SPACES, () => {
|
const invites = useEventEmitterState<Room[]>(SpaceStore.instance, UPDATE_INVITED_SPACES, () => {
|
||||||
@ -206,6 +216,11 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(({ children, isPanelCo
|
|||||||
|
|
||||||
const SpacePanel = () => {
|
const SpacePanel = () => {
|
||||||
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
|
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
|
||||||
|
const ref = useRef<HTMLUListElement>();
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
UIStore.instance.trackElementDimensions("SpacePanel", ref.current);
|
||||||
|
return () => UIStore.instance.stopTrackingElementDimensions("SpacePanel");
|
||||||
|
}, []);
|
||||||
|
|
||||||
const onKeyDown = (ev: React.KeyboardEvent) => {
|
const onKeyDown = (ev: React.KeyboardEvent) => {
|
||||||
let handled = true;
|
let handled = true;
|
||||||
@ -280,6 +295,7 @@ const SpacePanel = () => {
|
|||||||
onKeyDown={onKeyDownHandler}
|
onKeyDown={onKeyDownHandler}
|
||||||
role="tree"
|
role="tree"
|
||||||
aria-label={_t("Spaces")}
|
aria-label={_t("Spaces")}
|
||||||
|
ref={ref}
|
||||||
>
|
>
|
||||||
<Droppable droppableId="top-level-spaces">
|
<Droppable droppableId="top-level-spaces">
|
||||||
{ (provided, snapshot) => (
|
{ (provided, snapshot) => (
|
||||||
|
@ -19,10 +19,12 @@ import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
|
|||||||
import defaultDispatcher from "../dispatcher/dispatcher";
|
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { User } from "matrix-js-sdk/src/models/user";
|
import { User } from "matrix-js-sdk/src/models/user";
|
||||||
import { throttle } from "lodash";
|
import { memoize, throttle } from "lodash";
|
||||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||||
import { _t } from "../languageHandler";
|
import { _t } from "../languageHandler";
|
||||||
import { mediaFromMxc } from "../customisations/Media";
|
import { mediaFromMxc } from "../customisations/Media";
|
||||||
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
|
import { getDrawable } from "../utils/drawable";
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
@ -137,6 +139,22 @@ export class OwnProfileStore extends AsyncStoreWithClient<IState> {
|
|||||||
await this.updateState({ displayName: profileInfo.displayname, avatarUrl: profileInfo.avatar_url });
|
await this.updateState({ displayName: profileInfo.displayname, avatarUrl: profileInfo.avatar_url });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public async getAvatarBitmap(avatarSize = 32): Promise<CanvasImageSource> {
|
||||||
|
let avatarUrl = this.getHttpAvatarUrl(avatarSize);
|
||||||
|
const settingBgMxc = SettingsStore.getValue("RoomList.backgroundImage");
|
||||||
|
if (settingBgMxc) {
|
||||||
|
avatarUrl = mediaFromMxc(settingBgMxc).getSquareThumbnailHttp(avatarSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (avatarUrl) {
|
||||||
|
return await this.buildBitmap(avatarUrl);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildBitmap = memoize(getDrawable);
|
||||||
|
|
||||||
private onStateEvents = throttle(async (ev: MatrixEvent) => {
|
private onStateEvents = throttle(async (ev: MatrixEvent) => {
|
||||||
const myUserId = MatrixClientPeg.get().getUserId();
|
const myUserId = MatrixClientPeg.get().getUserId();
|
||||||
if (ev.getType() === 'm.room.member' && ev.getSender() === myUserId && ev.getStateKey() === myUserId) {
|
if (ev.getType() === 'm.room.member' && ev.getSender() === myUserId && ev.getStateKey() === myUserId) {
|
||||||
|
36
src/utils/drawable.ts
Normal file
36
src/utils/drawable.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch an image using the best available method based on browser compatibility
|
||||||
|
* @param url the URL of the image to fetch
|
||||||
|
* @returns a canvas drawable object
|
||||||
|
*/
|
||||||
|
export async function getDrawable(url: string): Promise<CanvasImageSource> {
|
||||||
|
if ('createImageBitmap' in window) {
|
||||||
|
const response = await fetch(url);
|
||||||
|
const blob = await response.blob();
|
||||||
|
return await createImageBitmap(blob);
|
||||||
|
} else {
|
||||||
|
return new Promise<HTMLImageElement>((resolve, reject) => {
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.crossOrigin = "anonymous";
|
||||||
|
img.onload = () => resolve(img);
|
||||||
|
img.onerror = (e) => reject(e);
|
||||||
|
img.src = url;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -2917,6 +2917,11 @@ content-type@^1.0.4:
|
|||||||
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
|
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
|
||||||
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
|
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
|
||||||
|
|
||||||
|
context-filter-polyfill@^0.2.4:
|
||||||
|
version "0.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/context-filter-polyfill/-/context-filter-polyfill-0.2.4.tgz#ecf88d3197e7c3a47e9a7ae2d5167b703945a5d4"
|
||||||
|
integrity sha512-LDZ3WiTzo6kIeJM7j8kPSgZf+gbD1cV1GaLyYO8RWvAg25cO3zUo3d2KizO0w9hAezNwz7tTbuWKpPdvLWzKqQ==
|
||||||
|
|
||||||
convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0:
|
convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0:
|
||||||
version "1.8.0"
|
version "1.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369"
|
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369"
|
||||||
|
Loading…
Reference in New Issue
Block a user