Merge branch 'develop' into travis/room-list/unread-2

This commit is contained in:
Travis Ralston 2020-06-22 14:16:52 -06:00
commit 894357f7f6
16 changed files with 99 additions and 35 deletions

View file

@ -304,18 +304,18 @@ limitations under the License.
position: relative; position: relative;
.mx_RoomSublist2_badgeContainer { .mx_RoomSublist2_badgeContainer {
order: 1; order: 0;
align-self: flex-end; align-self: flex-end;
margin-right: 0; margin-right: 0;
} }
.mx_RoomSublist2_headerText { .mx_RoomSublist2_stickable {
order: 2; order: 1;
max-width: 100%; max-width: 100%;
} }
.mx_RoomSublist2_auxButton { .mx_RoomSublist2_auxButton {
order: 4; order: 2;
visibility: visible; visibility: visible;
width: 32px !important; // !important to override hover styles width: 32px !important; // !important to override hover styles
height: 32px !important; // !important to override hover styles height: 32px !important; // !important to override hover styles

View file

@ -25,6 +25,7 @@ import RoomViewStore from "./stores/RoomViewStore";
import {IntegrationManagers} from "./integrations/IntegrationManagers"; import {IntegrationManagers} from "./integrations/IntegrationManagers";
import SettingsStore from "./settings/SettingsStore"; import SettingsStore from "./settings/SettingsStore";
import {Capability} from "./widgets/WidgetApi"; import {Capability} from "./widgets/WidgetApi";
import {objectClone} from "./utils/objects";
const WIDGET_API_VERSION = '0.0.2'; // Current API version const WIDGET_API_VERSION = '0.0.2'; // Current API version
const SUPPORTED_WIDGET_API_VERSIONS = [ const SUPPORTED_WIDGET_API_VERSIONS = [
@ -247,7 +248,7 @@ export default class FromWidgetPostMessageApi {
* @param {Object} res Response data * @param {Object} res Response data
*/ */
sendResponse(event, res) { sendResponse(event, res) {
const data = JSON.parse(JSON.stringify(event.data)); const data = objectClone(event.data);
data.response = res; data.response = res;
event.source.postMessage(data, event.origin); event.source.postMessage(data, event.origin);
} }
@ -260,7 +261,7 @@ export default class FromWidgetPostMessageApi {
*/ */
sendError(event, msg, nestedError) { sendError(event, msg, nestedError) {
console.error('Action:' + event.data.action + ' failed with message: ' + msg); console.error('Action:' + event.data.action + ' failed with message: ' + msg);
const data = JSON.parse(JSON.stringify(event.data)); const data = objectClone(event.data);
data.response = { data.response = {
error: { error: {
message: msg, message: msg,

View file

@ -244,16 +244,17 @@ import RoomViewStore from './stores/RoomViewStore';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
import {IntegrationManagers} from "./integrations/IntegrationManagers"; import {IntegrationManagers} from "./integrations/IntegrationManagers";
import {WidgetType} from "./widgets/WidgetType"; import {WidgetType} from "./widgets/WidgetType";
import {objectClone} from "./utils/objects";
function sendResponse(event, res) { function sendResponse(event, res) {
const data = JSON.parse(JSON.stringify(event.data)); const data = objectClone(event.data);
data.response = res; data.response = res;
event.source.postMessage(data, event.origin); event.source.postMessage(data, event.origin);
} }
function sendError(event, msg, nestedError) { function sendError(event, msg, nestedError) {
console.error("Action:" + event.data.action + " failed with message: " + msg); console.error("Action:" + event.data.action + " failed with message: " + msg);
const data = JSON.parse(JSON.stringify(event.data)); const data = objectClone(event.data);
data.response = { data.response = {
error: { error: {
message: msg, message: msg,

View file

@ -19,7 +19,6 @@ import TagPanel from "./TagPanel";
import classNames from "classnames"; import classNames from "classnames";
import dis from "../../dispatcher/dispatcher"; import dis from "../../dispatcher/dispatcher";
import { _t } from "../../languageHandler"; import { _t } from "../../languageHandler";
import SearchBox from "./SearchBox";
import RoomList2 from "../views/rooms/RoomList2"; import RoomList2 from "../views/rooms/RoomList2";
import { Action } from "../../dispatcher/actions"; import { Action } from "../../dispatcher/actions";
import { MatrixClientPeg } from "../../MatrixClientPeg"; import { MatrixClientPeg } from "../../MatrixClientPeg";
@ -30,6 +29,8 @@ import AccessibleButton from "../views/elements/AccessibleButton";
import RoomBreadcrumbs2 from "../views/rooms/RoomBreadcrumbs2"; import RoomBreadcrumbs2 from "../views/rooms/RoomBreadcrumbs2";
import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore"; import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore"; import { UPDATE_EVENT } from "../../stores/AsyncStore";
import ResizeNotifier from "../../utils/ResizeNotifier";
import { createRef } from "react";
/******************************************************************* /*******************************************************************
* CAUTION * * CAUTION *
@ -41,6 +42,7 @@ import { UPDATE_EVENT } from "../../stores/AsyncStore";
interface IProps { interface IProps {
isMinimized: boolean; isMinimized: boolean;
resizeNotifier: ResizeNotifier;
} }
interface IState { interface IState {
@ -49,6 +51,8 @@ interface IState {
} }
export default class LeftPanel2 extends React.Component<IProps, IState> { export default class LeftPanel2 extends React.Component<IProps, IState> {
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
// TODO: Properly support TagPanel // TODO: Properly support TagPanel
// TODO: Properly support searching/filtering // TODO: Properly support searching/filtering
// TODO: Properly support breadcrumbs // TODO: Properly support breadcrumbs
@ -65,10 +69,15 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
}; };
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
// We watch the middle panel because we don't actually get resized, the middle panel does.
// We listen to the noisy channel to avoid choppy reaction times.
this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize);
} }
public componentWillUnmount() { public componentWillUnmount() {
BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize);
} }
private onSearch = (term: string): void => { private onSearch = (term: string): void => {
@ -86,9 +95,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
} }
}; };
// TODO: Apply this on resize, init, etc for reliability private handleStickyHeaders(list: HTMLDivElement) {
private onScroll = (ev: React.MouseEvent<HTMLDivElement>) => {
const list = ev.target as HTMLDivElement;
const rlRect = list.getBoundingClientRect(); const rlRect = list.getBoundingClientRect();
const bottom = rlRect.bottom; const bottom = rlRect.bottom;
const top = rlRect.top; const top = rlRect.top;
@ -123,6 +130,18 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
header.style.top = `unset`; header.style.top = `unset`;
} }
} }
}
// TODO: Apply this on resize, init, etc for reliability
private onScroll = (ev: React.MouseEvent<HTMLDivElement>) => {
const list = ev.target as HTMLDivElement;
this.handleStickyHeaders(list);
};
private onResize = () => {
console.log("Resize width");
if (!this.listContainerRef.current) return; // ignore: no headers to sticky
this.handleStickyHeaders(this.listContainerRef.current);
}; };
private renderHeader(): React.ReactNode { private renderHeader(): React.ReactNode {
@ -230,9 +249,11 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
<aside className="mx_LeftPanel2_roomListContainer"> <aside className="mx_LeftPanel2_roomListContainer">
{this.renderHeader()} {this.renderHeader()}
{this.renderSearchExplore()} {this.renderSearchExplore()}
<div className="mx_LeftPanel2_actualRoomListContainer" onScroll={this.onScroll}> <div
{roomList} className="mx_LeftPanel2_actualRoomListContainer"
</div> onScroll={this.onScroll}
ref={this.listContainerRef}
>{roomList}</div>
</aside> </aside>
</div> </div>
); );

View file

@ -677,7 +677,10 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
if (SettingsStore.isFeatureEnabled("feature_new_room_list")) { if (SettingsStore.isFeatureEnabled("feature_new_room_list")) {
// TODO: Supply props like collapsed and disabled to LeftPanel2 // TODO: Supply props like collapsed and disabled to LeftPanel2
leftPanel = ( leftPanel = (
<LeftPanel2 isMinimized={this.props.collapseLhs || false} /> <LeftPanel2
isMinimized={this.props.collapseLhs || false}
resizeNotifier={this.props.resizeNotifier}
/>
); );
} }

View file

@ -117,7 +117,7 @@ export default class UserMenuButton extends React.Component<IProps, IState> {
SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, false); SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, false);
const newTheme = this.state.isDarkTheme ? "light" : "dark"; const newTheme = this.state.isDarkTheme ? "light" : "dark";
SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme); SettingsStore.setValue("theme", null, SettingLevel.DEVICE, newTheme); // set at same level as Appearance tab
}; };
private onSettingsOpen = (ev: ButtonEvent, tabId: string) => { private onSettingsOpen = (ev: ButtonEvent, tabId: string) => {

View file

@ -291,7 +291,18 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
'mx_RoomSublist2_headerContainer_withAux': !!addRoomButton, 'mx_RoomSublist2_headerContainer_withAux': !!addRoomButton,
}); });
const badgeContainer = (
<div className="mx_RoomSublist2_badgeContainer">
{badge}
</div>
);
// TODO: a11y (see old component) // TODO: a11y (see old component)
// Note: the addRoomButton conditionally gets moved around
// the DOM depending on whether or not the list is minimized.
// If we're minimized, we want it below the header so it
// doesn't become sticky.
// The same applies to the notification badge.
return ( return (
<div className={classes}> <div className={classes}>
<div className='mx_RoomSublist2_stickable'> <div className='mx_RoomSublist2_stickable'>
@ -307,11 +318,11 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
<span>{this.props.label}</span> <span>{this.props.label}</span>
</AccessibleButton> </AccessibleButton>
{this.renderMenu()} {this.renderMenu()}
{addRoomButton} {this.props.isMinimized ? null : addRoomButton}
<div className="mx_RoomSublist2_badgeContainer"> {this.props.isMinimized ? null : badgeContainer}
{badge}
</div>
</div> </div>
{this.props.isMinimized ? badgeContainer : null}
{this.props.isMinimized ? addRoomButton : null}
</div> </div>
); );
}} }}

View file

@ -18,6 +18,7 @@ import React from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import {_t, pickBestLanguage} from "../../../languageHandler"; import {_t, pickBestLanguage} from "../../../languageHandler";
import * as sdk from "../../.."; import * as sdk from "../../..";
import {objectClone} from "../../../utils/objects";
export default class InlineTermsAgreement extends React.Component { export default class InlineTermsAgreement extends React.Component {
static propTypes = { static propTypes = {
@ -56,7 +57,7 @@ export default class InlineTermsAgreement extends React.Component {
} }
_togglePolicy = (index) => { _togglePolicy = (index) => {
const policies = JSON.parse(JSON.stringify(this.state.policies)); // deep & cheap clone const policies = objectClone(this.state.policies);
policies[index].checked = !policies[index].checked; policies[index].checked = !policies[index].checked;
this.setState({policies}); this.setState({policies});
}; };

View file

@ -18,7 +18,7 @@ limitations under the License.
import {MatrixClientPeg} from '../../MatrixClientPeg'; import {MatrixClientPeg} from '../../MatrixClientPeg';
import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler"; import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler";
import {SettingLevel} from "../SettingsStore"; import {SettingLevel} from "../SettingsStore";
import {objectKeyChanges} from "../../utils/objects"; import {objectClone, objectKeyChanges} from "../../utils/objects";
const BREADCRUMBS_LEGACY_EVENT_TYPE = "im.vector.riot.breadcrumb_rooms"; const BREADCRUMBS_LEGACY_EVENT_TYPE = "im.vector.riot.breadcrumb_rooms";
const BREADCRUMBS_EVENT_TYPE = "im.vector.setting.breadcrumbs"; const BREADCRUMBS_EVENT_TYPE = "im.vector.setting.breadcrumbs";
@ -162,7 +162,7 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa
const event = cli.getAccountData(eventType); const event = cli.getAccountData(eventType);
if (!event || !event.getContent()) return null; if (!event || !event.getContent()) return null;
return JSON.parse(JSON.stringify(event.getContent())); // clone to prevent mutation return objectClone(event.getContent()); // clone to prevent mutation
} }
_notifyBreadcrumbsUpdate(event) { _notifyBreadcrumbsUpdate(event) {

View file

@ -18,7 +18,7 @@ limitations under the License.
import {MatrixClientPeg} from '../../MatrixClientPeg'; import {MatrixClientPeg} from '../../MatrixClientPeg';
import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler"; import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler";
import {SettingLevel} from "../SettingsStore"; import {SettingLevel} from "../SettingsStore";
import {objectKeyChanges} from "../../utils/objects"; import {objectClone, objectKeyChanges} from "../../utils/objects";
const ALLOWED_WIDGETS_EVENT_TYPE = "im.vector.setting.allowed_widgets"; const ALLOWED_WIDGETS_EVENT_TYPE = "im.vector.setting.allowed_widgets";
@ -137,6 +137,6 @@ export default class RoomAccountSettingsHandler extends MatrixClientBackedSettin
const event = room.getAccountData(eventType); const event = room.getAccountData(eventType);
if (!event || !event.getContent()) return null; if (!event || !event.getContent()) return null;
return event.getContent(); return objectClone(event.getContent()); // clone to prevent mutation
} }
} }

View file

@ -18,7 +18,7 @@ limitations under the License.
import {MatrixClientPeg} from '../../MatrixClientPeg'; import {MatrixClientPeg} from '../../MatrixClientPeg';
import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler"; import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler";
import {SettingLevel} from "../SettingsStore"; import {SettingLevel} from "../SettingsStore";
import {objectKeyChanges} from "../../utils/objects"; import {objectClone, objectKeyChanges} from "../../utils/objects";
/** /**
* Gets and sets settings at the "room" level. * Gets and sets settings at the "room" level.
@ -117,6 +117,6 @@ export default class RoomSettingsHandler extends MatrixClientBackedSettingsHandl
const event = room.currentState.getStateEvents(eventType, ""); const event = room.currentState.getStateEvents(eventType, "");
if (!event || !event.getContent()) return null; if (!event || !event.getContent()) return null;
return event.getContent(); return objectClone(event.getContent()); // clone to prevent mutation
} }
} }

View file

@ -15,9 +15,13 @@ limitations under the License.
*/ */
/** /**
* Fires when the middle panel has been resized. * Fires when the middle panel has been resized (throttled).
* @event module:utils~ResizeNotifier#"middlePanelResized" * @event module:utils~ResizeNotifier#"middlePanelResized"
*/ */
/**
* Fires when the middle panel has been resized by a pixel.
* @event module:utils~ResizeNotifier#"middlePanelResizedNoisy"
*/
import { EventEmitter } from "events"; import { EventEmitter } from "events";
import { throttle } from "lodash"; import { throttle } from "lodash";
@ -29,15 +33,24 @@ export default class ResizeNotifier extends EventEmitter {
this._throttledMiddlePanel = throttle(() => this.emit("middlePanelResized"), 200); this._throttledMiddlePanel = throttle(() => this.emit("middlePanelResized"), 200);
} }
_noisyMiddlePanel() {
this.emit("middlePanelResizedNoisy");
}
_updateMiddlePanel() {
this._throttledMiddlePanel();
this._noisyMiddlePanel();
}
// can be called in quick succession // can be called in quick succession
notifyLeftHandleResized() { notifyLeftHandleResized() {
// don't emit event for own region // don't emit event for own region
this._throttledMiddlePanel(); this._updateMiddlePanel();
} }
// can be called in quick succession // can be called in quick succession
notifyRightHandleResized() { notifyRightHandleResized() {
this._throttledMiddlePanel(); this._updateMiddlePanel();
} }
// can be called in quick succession // can be called in quick succession
@ -48,7 +61,7 @@ export default class ResizeNotifier extends EventEmitter {
// taller than the available space // taller than the available space
this.emit("leftPanelResized"); this.emit("leftPanelResized");
this._throttledMiddlePanel(); this._updateMiddlePanel();
} }
} }

View file

@ -31,6 +31,7 @@ import {IntegrationManagers} from "../integrations/IntegrationManagers";
import {Capability} from "../widgets/WidgetApi"; import {Capability} from "../widgets/WidgetApi";
import {Room} from "matrix-js-sdk/src/models/room"; import {Room} from "matrix-js-sdk/src/models/room";
import {WidgetType} from "../widgets/WidgetType"; import {WidgetType} from "../widgets/WidgetType";
import {objectClone} from "./objects";
export default class WidgetUtils { export default class WidgetUtils {
/* Returns true if user is able to send state events to modify widgets in this room /* Returns true if user is able to send state events to modify widgets in this room
@ -222,7 +223,7 @@ export default class WidgetUtils {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
// Get the current widgets and clone them before we modify them, otherwise // Get the current widgets and clone them before we modify them, otherwise
// we'll modify the content of the old event. // we'll modify the content of the old event.
const userWidgets = JSON.parse(JSON.stringify(WidgetUtils.getUserWidgets())); const userWidgets = objectClone(WidgetUtils.getUserWidgets());
// Delete existing widget with ID // Delete existing widget with ID
try { try {

View file

@ -47,3 +47,14 @@ export function objectKeyChanges(a: any, b: any): string[] {
const diff = objectDiff(a, b); const diff = objectDiff(a, b);
return arrayMerge(diff.removed, diff.added, diff.changed); return arrayMerge(diff.removed, diff.added, diff.changed);
} }
/**
* Clones an object by running it through JSON parsing. Note that this
* will destroy any complicated object types which do not translate to
* JSON.
* @param obj The object to clone.
* @returns The cloned object
*/
export function objectClone(obj: any): any {
return JSON.parse(JSON.stringify(obj));
}

View file

@ -19,7 +19,7 @@ limitations under the License.
// converts a pixel value to rem. // converts a pixel value to rem.
export function toRem(pixelValue: number): string { export function toRem(pixelValue: number): string {
return pixelValue / 15 + "rem"; return pixelValue / 10 + "rem";
} }
export function toPx(pixelValue: number): string { export function toPx(pixelValue: number): string {

View file

@ -19,6 +19,7 @@ limitations under the License.
import { randomString } from "matrix-js-sdk/src/randomstring"; import { randomString } from "matrix-js-sdk/src/randomstring";
import { EventEmitter } from "events"; import { EventEmitter } from "events";
import { objectClone } from "../utils/objects";
export enum Capability { export enum Capability {
Screenshot = "m.capability.screenshot", Screenshot = "m.capability.screenshot",
@ -140,7 +141,7 @@ export class WidgetApi extends EventEmitter {
private replyToRequest(payload: ToWidgetRequest, reply: any) { private replyToRequest(payload: ToWidgetRequest, reply: any) {
if (!window.parent) return; if (!window.parent) return;
const request = JSON.parse(JSON.stringify(payload)); const request = objectClone(payload);
request.response = reply; request.response = reply;
window.parent.postMessage(request, this.origin); window.parent.postMessage(request, this.origin);