Merge pull request #6262 from matrix-org/gsouquet/fix-backdrop-filter

Move backdrop filter to a canvas based solution
This commit is contained in:
Dariusz Niemczyk 2021-08-19 10:59:49 +02:00 committed by GitHub
commit 939874167a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 413 additions and 92 deletions

View File

@ -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"
}, },

View File

@ -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;

View File

@ -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";

View 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;
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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 {

View File

@ -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;
} }

View File

@ -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;

View File

@ -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);

View File

@ -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";

View File

@ -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

View File

@ -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;
} }

View 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>;
}
}

View File

@ -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}

View File

@ -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>

View File

@ -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 />

View File

@ -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) => (

View File

@ -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
View 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;
});
}
}

View File

@ -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"