Merge branch 'develop' into gsouquet/threaded-messaging-2349

This commit is contained in:
Germain Souquet 2021-08-19 10:44:32 +01:00
commit ffc7326b0c
43 changed files with 926 additions and 229 deletions

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.
*/
import type { EventSubscription } from "fbemitter";
import React from 'react';
import GroupFilterOrderStore from '../../stores/GroupFilterOrderStore';
@ -30,22 +31,43 @@ import AutoHideScrollbar from "./AutoHideScrollbar";
import SettingsStore from "../../settings/SettingsStore";
import UserTagTile from "../views/elements/UserTagTile";
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")
class GroupFilterPanel extends React.Component {
static contextType = MatrixClientContext;
class GroupFilterPanel extends React.Component<IGroupFilterPanelProps, IGroupFilterPanelState> {
public static contextType = MatrixClientContext;
state = {
public state = {
orderedTags: [],
selectedTags: [],
};
componentDidMount() {
this.unmounted = false;
this.context.on("Group.myMembership", this._onGroupMyMembership);
this.context.on("sync", this._onClientSync);
private ref = React.createRef<HTMLDivElement>();
private unmounted = false;
private groupFilterOrderStoreToken?: EventSubscription;
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) {
return;
}
@ -56,23 +78,25 @@ class GroupFilterPanel extends React.Component {
});
// This could be done by anything with a matrix client
dis.dispatch(GroupActions.fetchJoinedGroups(this.context));
UIStore.instance.trackElementDimensions("GroupPanel", this.ref.current);
}
componentWillUnmount() {
public componentWillUnmount() {
this.unmounted = true;
this.context.removeListener("Group.myMembership", this._onGroupMyMembership);
this.context.removeListener("sync", this._onClientSync);
if (this._groupFilterOrderStoreToken) {
this._groupFilterOrderStoreToken.remove();
this.context.removeListener("Group.myMembership", this.onGroupMyMembership);
this.context.removeListener("sync", this.onClientSync);
if (this.groupFilterOrderStoreToken) {
this.groupFilterOrderStoreToken.remove();
}
UIStore.instance.stopTrackingElementDimensions("GroupPanel");
}
_onGroupMyMembership = () => {
private onGroupMyMembership = () => {
if (this.unmounted) return;
dis.dispatch(GroupActions.fetchJoinedGroups(this.context));
};
_onClientSync = (syncState, prevState) => {
private onClientSync = (syncState, prevState) => {
// Consider the client reconnected if there is no error with syncing.
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
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
if (this.state.selectedTags.length > 0) {
dis.dispatch({ action: 'deselect_tags' });
}
};
onClearFilterClick = ev => {
private onClearFilterClick = ev => {
dis.dispatch({ action: 'deselect_tags' });
};
renderGlobalIcon() {
private renderGlobalIcon() {
if (!SettingsStore.getValue("feature_communities_v2_prototypes")) return null;
return (
@ -104,7 +128,7 @@ class GroupFilterPanel extends React.Component {
);
}
render() {
public render() {
const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
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
className="mx_GroupFilterPanel_scroller"
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 IndicatorScrollbar from "../structures/IndicatorScrollbar";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import { OwnProfileStore } from "../../stores/OwnProfileStore";
import RoomListNumResults from "../views/rooms/RoomListNumResults";
import LeftPanelWidget from "./LeftPanelWidget";
import { replaceableComponent } from "../../utils/replaceableComponent";
import { mediaFromMxc } from "../../customisations/Media";
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore";
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
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 listContainerRef: React.RefObject<HTMLDivElement> = createRef();
private groupFilterPanelWatcherRef: string;
private groupFilterPanelContainer = createRef<HTMLDivElement>();
private bgImageWatcherRef: string;
private focusedElement = null;
private isDoingStickyHeaders = false;
@ -86,17 +85,19 @@ export default class LeftPanel extends React.Component<IProps, IState> {
BreadcrumbsStore.instance.on(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);
this.bgImageWatcherRef = SettingsStore.watchSetting(
"RoomList.backgroundImage", null, this.onBackgroundImageUpdate);
this.groupFilterPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => {
this.setState({ showGroupFilterPanel: SettingsStore.getValue("TagPanel.enableTagPanel") });
});
}
public componentDidMount() {
UIStore.instance.trackElementDimensions("LeftPanel", this.ref.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);
// 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
@ -105,10 +106,8 @@ export default class LeftPanel extends React.Component<IProps, IState> {
public componentWillUnmount() {
SettingsStore.unwatchSetting(this.groupFilterPanelWatcherRef);
SettingsStore.unwatchSetting(this.bgImageWatcherRef);
BreadcrumbsStore.instance.off(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);
UIStore.instance.stopTrackingElementDimensions("ListContainer");
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) {
if (this.isDoingStickyHeaders) return;
this.isDoingStickyHeaders = true;
@ -443,7 +425,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
let leftLeftPanel;
if (this.state.showGroupFilterPanel) {
leftLeftPanel = (
<div className="mx_LeftPanel_GroupFilterPanelContainer">
<div className="mx_LeftPanel_GroupFilterPanelContainer" ref={this.groupFilterPanelContainer}>
<GroupFilterPanel />
{ SettingsStore.getValue("feature_custom_tags") ? <CustomRoomTagPanel /> : null }
</div>

View file

@ -55,15 +55,19 @@ import { getKeyBindingsManager, NavigationAction, RoomAction } from '../../KeyBi
import { IOpts } from "../../createRoom";
import SpacePanel from "../views/spaces/SpacePanel";
import { replaceableComponent } from "../../utils/replaceableComponent";
import CallHandler, { CallHandlerEvent } from '../../CallHandler';
import CallHandler from '../../CallHandler';
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import AudioFeedArrayForCall from '../views/voip/AudioFeedArrayForCall';
import { OwnProfileStore } from '../../stores/OwnProfileStore';
import { UPDATE_EVENT } from "../../stores/AsyncStore";
import RoomView from './RoomView';
import ToastContainer from './ToastContainer';
import MyGroups from "./MyGroups";
import UserView from "./UserView";
import GroupView from "./GroupView";
import BackdropPanel from "./BackdropPanel";
import SpaceStore from "../../stores/SpaceStore";
import classNames from 'classnames';
// 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.
@ -127,6 +131,7 @@ interface IState {
usageLimitEventTs?: number;
useCompactLayout: boolean;
activeCalls: Array<MatrixCall>;
backgroundImage?: CanvasImageSource;
}
/**
@ -142,6 +147,7 @@ interface IState {
class LoggedInView extends React.Component<IProps, IState> {
static displayName = 'LoggedInView';
private dispatcherRef: string;
protected readonly _matrixClient: MatrixClient;
protected readonly _roomView: React.RefObject<any>;
protected readonly _resizeContainer: React.RefObject<ResizeHandle>;
@ -156,7 +162,7 @@ class LoggedInView extends React.Component<IProps, IState> {
// use compact timeline view
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
usageLimitDismissed: false,
activeCalls: [],
activeCalls: CallHandler.sharedInstance().getAllActiveCalls(),
};
// stash the MatrixClient in case we log out before we are unmounted
@ -172,7 +178,7 @@ class LoggedInView extends React.Component<IProps, IState> {
componentDidMount() {
document.addEventListener('keydown', this.onNativeKeyDown, false);
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
this.dispatcherRef = dis.register(this.onAction);
this.updateServerNoticeEvents();
@ -192,25 +198,41 @@ class LoggedInView extends React.Component<IProps, IState> {
this.resizer = this.createResizer();
this.resizer.attach();
OwnProfileStore.instance.on(UPDATE_EVENT, this.refreshBackgroundImage);
this.loadResizerPreferences();
this.refreshBackgroundImage();
}
componentWillUnmount() {
document.removeEventListener('keydown', this.onNativeKeyDown, false);
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
dis.unregister(this.dispatcherRef);
this._matrixClient.removeListener("accountData", this.onAccountData);
this._matrixClient.removeListener("sync", this.onSync);
this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents);
OwnProfileStore.instance.off(UPDATE_EVENT, this.refreshBackgroundImage);
SettingsStore.unwatchSetting(this.compactLayoutWatcherRef);
this.resizer.detach();
}
private onCallsChanged = () => {
private refreshBackgroundImage = async (): Promise<void> => {
this.setState({
activeCalls: CallHandler.sharedInstance().getAllActiveCalls(),
backgroundImage: await OwnProfileStore.instance.getAvatarBitmap(),
});
};
private onAction = (payload): void => {
switch (payload.action) {
case 'call_state': {
const activeCalls = CallHandler.sharedInstance().getAllActiveCalls();
if (activeCalls !== this.state.activeCalls) {
this.setState({ activeCalls });
}
break;
}
}
};
public canResetTimelineInRoom = (roomId: string) => {
if (!this._roomView.current) {
return true;
@ -601,10 +623,11 @@ class LoggedInView extends React.Component<IProps, IState> {
break;
}
let bodyClasses = 'mx_MatrixChat';
if (this.state.useCompactLayout) {
bodyClasses += ' mx_MatrixChat_useCompactLayout';
}
const bodyClasses = classNames({
'mx_MatrixChat': true,
'mx_MatrixChat_useCompactLayout': this.state.useCompactLayout,
'mx_MatrixChat--with-avatar': this.state.backgroundImage,
});
const audioFeedArraysForCalls = this.state.activeCalls.map((call) => {
return (
@ -622,14 +645,17 @@ class LoggedInView extends React.Component<IProps, IState> {
>
<ToastContainer />
<div ref={this._resizeContainer} className={bodyClasses}>
<BackdropPanel
backgroundImage={this.state.backgroundImage}
/>
{ SpaceStore.spacesEnabled ? <SpacePanel /> : null }
<LeftPanel
isMinimized={this.props.collapseLhs || false}
resizeNotifier={this.props.resizeNotifier}
/>
<ResizeHandle />
{ pageElement }
</div>
{ pageElement }
</div>
<CallContainer />
<NonUrgentToastContainer />

View file

@ -108,6 +108,7 @@ import SoftLogout from './auth/SoftLogout';
import { makeRoomPermalink } from "../../utils/permalinks/Permalinks";
import { copyPlaintext } from "../../utils/strings";
import { PosthogAnalytics } from '../../PosthogAnalytics';
import { initSentry } from "../../sentry";
/** constants for MatrixChat.state.view */
export enum Views {
@ -393,6 +394,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
PosthogAnalytics.instance.updatePlatformSuperProperties();
CountlyAnalytics.instance.enable(/* anonymous = */ true);
initSentry(SdkConfig.get()["sentry"]);
}
private async postLoginSetup() {