Merge branch 'develop' into gsouquet-scroll-to-live-reset-hash

This commit is contained in:
Germain Souquet 2021-06-02 13:54:05 +01:00
commit bc50028f70
96 changed files with 4936 additions and 1483 deletions

View file

@ -1,51 +0,0 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
export default class AutoHideScrollbar extends React.Component {
constructor(props) {
super(props);
this._collectContainerRef = this._collectContainerRef.bind(this);
}
_collectContainerRef(ref) {
if (ref && !this.containerRef) {
this.containerRef = ref;
}
if (this.props.wrappedRef) {
this.props.wrappedRef(ref);
}
}
getScrollTop() {
return this.containerRef.scrollTop;
}
render() {
return (<div
ref={this._collectContainerRef}
style={this.props.style}
className={["mx_AutoHideScrollbar", this.props.className].join(" ")}
onScroll={this.props.onScroll}
onWheel={this.props.onWheel}
tabIndex={this.props.tabIndex}
>
{ this.props.children }
</div>);
}
}

View file

@ -0,0 +1,65 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
interface IProps {
className?: string;
onScroll?: () => void;
onWheel?: () => void;
style?: React.CSSProperties
tabIndex?: number,
wrappedRef?: (ref: HTMLDivElement) => void;
}
export default class AutoHideScrollbar extends React.Component<IProps> {
private containerRef: React.RefObject<HTMLDivElement> = React.createRef();
public componentDidMount() {
if (this.containerRef.current && this.props.onScroll) {
// 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
this.containerRef.current.addEventListener("scroll", this.props.onScroll, { passive: true });
}
if (this.props.wrappedRef) {
this.props.wrappedRef(this.containerRef.current);
}
}
public componentWillUnmount() {
if (this.containerRef.current && this.props.onScroll) {
this.containerRef.current.removeEventListener("scroll", this.props.onScroll);
}
}
public getScrollTop(): number {
return this.containerRef.current.scrollTop;
}
public render() {
return (<div
ref={this.containerRef}
style={this.props.style}
className={["mx_AutoHideScrollbar", this.props.className].join(" ")}
onWheel={this.props.onWheel}
tabIndex={this.props.tabIndex}
>
{ this.props.children }
</div>);
}
}

View file

@ -23,6 +23,7 @@ import classNames from "classnames";
import {Key} from "../../Keyboard";
import {Writeable} from "../../@types/common";
import {replaceableComponent} from "../../utils/replaceableComponent";
import UIStore from "../../stores/UIStore";
// Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and
@ -410,12 +411,12 @@ export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None
const buttonBottom = elementRect.bottom + window.pageYOffset;
const buttonTop = elementRect.top + window.pageYOffset;
// Align the right edge of the menu to the right edge of the button
menuOptions.right = window.innerWidth - buttonRight;
menuOptions.right = UIStore.instance.windowWidth - buttonRight;
// Align the menu vertically on whichever side of the button has more space available.
if (buttonBottom < window.innerHeight / 2) {
if (buttonBottom < UIStore.instance.windowHeight / 2) {
menuOptions.top = buttonBottom + vPadding;
} else {
menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding;
menuOptions.bottom = (UIStore.instance.windowHeight - buttonTop) + vPadding;
}
return menuOptions;
@ -430,12 +431,12 @@ export const alwaysAboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFac
const buttonBottom = elementRect.bottom + window.pageYOffset;
const buttonTop = elementRect.top + window.pageYOffset;
// Align the right edge of the menu to the right edge of the button
menuOptions.right = window.innerWidth - buttonRight;
menuOptions.right = UIStore.instance.windowWidth - buttonRight;
// Align the menu vertically on whichever side of the button has more space available.
if (buttonBottom < window.innerHeight / 2) {
if (buttonBottom < UIStore.instance.windowHeight / 2) {
menuOptions.top = buttonBottom + vPadding;
} else {
menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding;
menuOptions.bottom = (UIStore.instance.windowHeight - buttonTop) + vPadding;
}
return menuOptions;
@ -451,7 +452,7 @@ export const alwaysAboveRightOf = (elementRect: DOMRect, chevronFace = ChevronFa
// Align the left edge of the menu to the left edge of the button
menuOptions.left = buttonLeft;
// Align the menu vertically above the menu
menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding;
menuOptions.bottom = (UIStore.instance.windowHeight - buttonTop) + vPadding;
return menuOptions;
};

View file

@ -36,7 +36,7 @@ import FlairStore from '../../stores/FlairStore';
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks";
import {Group} from "matrix-js-sdk/src/models/group";
import {allSettled, sleep} from "../../utils/promise";
import {sleep} from "../../utils/promise";
import RightPanelStore from "../../stores/RightPanelStore";
import AutoHideScrollbar from "./AutoHideScrollbar";
import {mediaFromMxc} from "../../customisations/Media";
@ -99,7 +99,7 @@ class CategoryRoomList extends React.Component {
onFinished: (success, addrs) => {
if (!success) return;
const errorList = [];
allSettled(addrs.map((addr) => {
Promise.allSettled(addrs.map((addr) => {
return GroupStore
.addRoomToGroupSummary(this.props.groupId, addr.address)
.catch(() => { errorList.push(addr.address); });
@ -274,7 +274,7 @@ class RoleUserList extends React.Component {
onFinished: (success, addrs) => {
if (!success) return;
const errorList = [];
allSettled(addrs.map((addr) => {
Promise.allSettled(addrs.map((addr) => {
return GroupStore
.addUserToGroupSummary(addr.address)
.catch(() => { errorList.push(addr.address); });

View file

@ -59,7 +59,9 @@ export default class IndicatorScrollbar extends React.Component {
_collectScroller(scroller) {
if (scroller && !this._scrollElement) {
this._scrollElement = scroller;
this._scrollElement.addEventListener("scroll", this.checkOverflow);
// 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
this._scrollElement.addEventListener("scroll", this.checkOverflow, { passive: true });
this.checkOverflow();
}
}

View file

@ -43,6 +43,7 @@ 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";
interface IProps {
isMinimized: boolean;
@ -66,6 +67,7 @@ const cssClasses = [
@replaceableComponent("structures.LeftPanel")
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 bgImageWatcherRef: string;
@ -90,10 +92,14 @@ export default class LeftPanel extends React.Component<IProps, IState> {
this.groupFilterPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => {
this.setState({showGroupFilterPanel: SettingsStore.getValue("TagPanel.enableTagPanel")});
});
}
// 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 componentDidMount() {
UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.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
this.listContainerRef.current?.addEventListener("scroll", this.onScroll, { passive: true });
}
public componentWillUnmount() {
@ -103,7 +109,15 @@ export default class LeftPanel extends React.Component<IProps, IState> {
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
OwnProfileStore.instance.off(UPDATE_EVENT, this.onBackgroundImageUpdate);
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize);
UIStore.instance.stopTrackingElementDimensions("ListContainer");
UIStore.instance.removeListener("ListContainer", this.refreshStickyHeaders);
this.listContainerRef.current?.removeEventListener("scroll", this.onScroll);
}
public componentDidUpdate(prevProps: IProps, prevState: IState): void {
if (prevState.activeSpace !== this.state.activeSpace) {
this.refreshStickyHeaders();
}
}
private updateActiveSpace = (activeSpace: Room) => {
@ -114,6 +128,11 @@ export default class LeftPanel extends React.Component<IProps, IState> {
dis.fire(Action.ViewRoomDirectory);
};
private refreshStickyHeaders = () => {
if (!this.listContainerRef.current) return; // ignore: no headers to sticky
this.handleStickyHeaders(this.listContainerRef.current);
}
private onBreadcrumbsUpdate = () => {
const newVal = BreadcrumbsStore.instance.visible;
if (newVal !== this.state.showBreadcrumbs) {
@ -156,9 +175,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
const bottomEdge = list.offsetHeight + list.scrollTop;
const sublists = list.querySelectorAll<HTMLDivElement>(".mx_RoomSublist:not(.mx_RoomSublist_hidden)");
const headerRightMargin = 15; // calculated from margins and widths to align with non-sticky tiles
const headerStickyWidth = list.clientWidth - headerRightMargin;
// We track which styles we want on a target before making the changes to avoid
// excessive layout updates.
const targetStyles = new Map<HTMLDivElement, {
@ -228,7 +244,8 @@ export default class LeftPanel extends React.Component<IProps, IState> {
header.classList.add("mx_RoomSublist_headerContainer_stickyBottom");
}
const offset = window.innerHeight - (list.parentElement.offsetTop + list.parentElement.offsetHeight);
const offset = UIStore.instance.windowHeight -
(list.parentElement.offsetTop + list.parentElement.offsetHeight);
const newBottom = `${offset}px`;
if (header.style.bottom !== newBottom) {
header.style.bottom = newBottom;
@ -247,14 +264,20 @@ export default class LeftPanel extends React.Component<IProps, IState> {
header.classList.add("mx_RoomSublist_headerContainer_sticky");
}
const newWidth = `${headerStickyWidth}px`;
if (header.style.width !== newWidth) {
header.style.width = newWidth;
const listDimensions = UIStore.instance.getElementDimensions("ListContainer");
if (listDimensions) {
const headerRightMargin = 15; // calculated from margins and widths to align with non-sticky tiles
const headerStickyWidth = listDimensions.width - headerRightMargin;
const newWidth = `${headerStickyWidth}px`;
if (header.style.width !== newWidth) {
header.style.width = newWidth;
}
}
} else if (!style.stickyTop && !style.stickyBottom) {
if (header.classList.contains("mx_RoomSublist_headerContainer_sticky")) {
header.classList.remove("mx_RoomSublist_headerContainer_sticky");
}
if (header.style.width) {
header.style.removeProperty('width');
}
@ -276,16 +299,11 @@ export default class LeftPanel extends React.Component<IProps, IState> {
}
}
private onScroll = (ev: React.MouseEvent<HTMLDivElement>) => {
private onScroll = (ev: Event) => {
const list = ev.target as HTMLDivElement;
this.handleStickyHeaders(list);
};
private onResize = () => {
if (!this.listContainerRef.current) return; // ignore: no headers to sticky
this.handleStickyHeaders(this.listContainerRef.current);
};
private onFocus = (ev: React.FocusEvent) => {
this.focusedElement = ev.target;
};
@ -420,8 +438,8 @@ export default class LeftPanel extends React.Component<IProps, IState> {
onFocus={this.onFocus}
onBlur={this.onBlur}
isMinimized={this.props.isMinimized}
onResize={this.onResize}
activeSpace={this.state.activeSpace}
onListCollapse={this.refreshStickyHeaders}
/>;
const containerClasses = classNames({
@ -435,17 +453,16 @@ export default class LeftPanel extends React.Component<IProps, IState> {
);
return (
<div className={containerClasses}>
<div className={containerClasses} ref={this.ref}>
{leftLeftPanel}
<aside className="mx_LeftPanel_roomListContainer">
{this.renderHeader()}
{this.renderSearchExplore()}
{this.renderBreadcrumbs()}
<RoomListNumResults />
<RoomListNumResults onVisibilityChange={this.refreshStickyHeaders} />
<div className="mx_LeftPanel_roomListWrapper">
<div
className={roomListClasses}
onScroll={this.onScroll}
ref={this.listContainerRef}
// Firefox sometimes makes this element focusable due to
// overflow:scroll;, so force it out of tab order.
@ -454,7 +471,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
{roomList}
</div>
</div>
{ !this.props.isMinimized && <LeftPanelWidget onResize={this.onResize} /> }
{ !this.props.isMinimized && <LeftPanelWidget /> }
</aside>
</div>
);

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useContext, useEffect, useMemo} from "react";
import React, {useContext, useMemo} from "react";
import {Resizable} from "re-resizable";
import classNames from "classnames";
@ -27,16 +27,13 @@ import WidgetUtils, {IWidgetEvent} from "../../utils/WidgetUtils";
import {useAccountData} from "../../hooks/useAccountData";
import AppTile from "../views/elements/AppTile";
import {useSettingValue} from "../../hooks/useSettings";
interface IProps {
onResize(): void;
}
import UIStore from "../../stores/UIStore";
const MIN_HEIGHT = 100;
const MAX_HEIGHT = 500; // or 50% of the window height
const INITIAL_HEIGHT = 280;
const LeftPanelWidget: React.FC<IProps> = ({ onResize }) => {
const LeftPanelWidget: React.FC = () => {
const cli = useContext(MatrixClientContext);
const mWidgetsEvent = useAccountData<Record<string, IWidgetEvent>>(cli, "m.widgets");
@ -56,7 +53,6 @@ const LeftPanelWidget: React.FC<IProps> = ({ onResize }) => {
const [height, setHeight] = useLocalStorageState("left-panel-widget-height", INITIAL_HEIGHT);
const [expanded, setExpanded] = useLocalStorageState("left-panel-widget-expanded", true);
useEffect(onResize, [expanded, onResize]);
const [onFocus, isActive, ref] = useRovingTabIndex();
const tabIndex = isActive ? 0 : -1;
@ -68,8 +64,7 @@ const LeftPanelWidget: React.FC<IProps> = ({ onResize }) => {
content = <Resizable
size={{height} as any}
minHeight={MIN_HEIGHT}
maxHeight={Math.min(window.innerHeight / 2, MAX_HEIGHT)}
onResize={onResize}
maxHeight={Math.min(UIStore.instance.windowHeight / 2, MAX_HEIGHT)}
onResizeStop={(e, dir, ref, d) => {
setHeight(height + d.height);
}}

View file

@ -87,6 +87,7 @@ import defaultDispatcher from "../../dispatcher/dispatcher";
import SecurityCustomisations from "../../customisations/Security";
import PerformanceMonitor, { PerformanceEntryNames } from "../../performance";
import UIStore, { UI_EVENTS } from "../../stores/UIStore";
/** constants for MatrixChat.state.view */
export enum Views {
@ -225,13 +226,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
firstSyncPromise: IDeferred<void>;
private screenAfterLogin?: IScreen;
private windowWidth: number;
private pageChanging: boolean;
private tokenLogin?: boolean;
private accountPassword?: string;
private accountPasswordTimer?: NodeJS.Timeout;
private focusComposer: boolean;
private subTitleStatus: string;
private prevWindowWidth: number;
private readonly loggedInView: React.RefObject<LoggedInViewType>;
private readonly dispatcherRef: any;
@ -277,9 +278,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
}
this.windowWidth = 10000;
this.handleResize();
window.addEventListener('resize', this.handleResize);
this.prevWindowWidth = UIStore.instance.windowWidth || 1000;
UIStore.instance.on(UI_EVENTS.Resize, this.handleResize);
this.pageChanging = false;
@ -436,7 +436,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
dis.unregister(this.dispatcherRef);
this.themeWatcher.stop();
this.fontWatcher.stop();
window.removeEventListener('resize', this.handleResize);
UIStore.destroy();
this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize);
if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer);
@ -1820,18 +1820,19 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
handleResize = () => {
const hideLhsThreshold = 1000;
const showLhsThreshold = 1000;
const LHS_THRESHOLD = 1000;
const width = UIStore.instance.windowWidth;
if (this.windowWidth > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) {
dis.dispatch({ action: 'hide_left_panel' });
}
if (this.windowWidth <= showLhsThreshold && window.innerWidth > showLhsThreshold) {
if (this.prevWindowWidth < LHS_THRESHOLD && width >= LHS_THRESHOLD) {
dis.dispatch({ action: 'show_left_panel' });
}
if (this.prevWindowWidth >= LHS_THRESHOLD && width < LHS_THRESHOLD) {
dis.dispatch({ action: 'hide_left_panel' });
}
this.prevWindowWidth = width;
this.state.resizeNotifier.notifyWindowResized();
this.windowWidth = window.innerWidth;
};
private dispatchTimelineResize() {
@ -2090,6 +2091,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
onForgotPasswordClick={showPasswordReset ? this.onForgotPasswordClick : undefined}
onServerConfigChange={this.onServerConfigChange}
fragmentAfterLogin={fragmentAfterLogin}
defaultUsername={this.props.startingFragmentQueryParams.defaultUsername}
{...this.getServerProperties()}
/>
);

View file

@ -83,6 +83,7 @@ import { objectHasDiff } from "../../utils/objects";
import SpaceRoomView from "./SpaceRoomView";
import { IOpts } from "../../createRoom";
import {replaceableComponent} from "../../utils/replaceableComponent";
import UIStore from "../../stores/UIStore";
const DEBUG = false;
let debuglog = function(msg: string) {};
@ -1125,7 +1126,8 @@ export default class RoomView extends React.Component<IProps, IState> {
Promise.resolve().then(() => {
const signUrl = this.props.threepidInvite?.signUrl;
dis.dispatch({
action: 'join_room',
action: Action.JoinRoom,
roomId: this.getRoomId(),
opts: { inviteSignUrl: signUrl },
_type: "unknown", // TODO: instrumentation
});
@ -1598,7 +1600,7 @@ export default class RoomView extends React.Component<IProps, IState> {
// a maxHeight on the underlying remote video tag.
// header + footer + status + give us at least 120px of scrollback at all times.
let auxPanelMaxHeight = window.innerHeight -
let auxPanelMaxHeight = UIStore.instance.windowHeight -
(54 + // height of RoomHeader
36 + // height of the status area
51 + // minimum height of the message compmoser

View file

@ -902,13 +902,13 @@ export default class ScrollPanel extends React.Component {
onScroll={this.onScroll}
onWheel={this.props.onUserScroll}
className={`mx_ScrollPanel ${this.props.className}`} style={this.props.style}>
{ this.props.fixedChildren }
<div className="mx_RoomView_messageListWrapper">
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite" role="list">
{ this.props.children }
</ol>
</div>
</AutoHideScrollbar>
);
{ this.props.fixedChildren }
<div className="mx_RoomView_messageListWrapper">
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite" role="list">
{ this.props.children }
</ol>
</div>
</AutoHideScrollbar>
);
}
}

View file

@ -101,15 +101,13 @@ const Tile: React.FC<ITileProps> = ({
numChildRooms,
children,
}) => {
const name = room.name || room.canonical_alias || room.aliases?.[0]
const cli = MatrixClientPeg.get();
const joinedRoom = cli.getRoom(room.room_id)?.getMyMembership() === "join" ? cli.getRoom(room.room_id) : null;
const name = joinedRoom?.name || room.name || room.canonical_alias || room.aliases?.[0]
|| (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room"));
const [showChildren, toggleShowChildren] = useStateToggle(true);
const cli = MatrixClientPeg.get();
const cliRoom = cli.getRoom(room.room_id);
const myMembership = cliRoom?.getMyMembership();
const onPreviewClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
@ -122,7 +120,7 @@ const Tile: React.FC<ITileProps> = ({
}
let button;
if (myMembership === "join") {
if (joinedRoom) {
button = <AccessibleButton onClick={onPreviewClick} kind="primary_outline">
{ _t("View") }
</AccessibleButton>;
@ -146,17 +144,27 @@ const Tile: React.FC<ITileProps> = ({
}
}
let url: string;
if (room.avatar_url) {
url = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(20);
let avatar;
if (joinedRoom) {
avatar = <RoomAvatar room={joinedRoom} width={20} height={20} />;
} else {
avatar = <BaseAvatar
name={name}
idName={room.room_id}
url={room.avatar_url ? mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(20) : null}
width={20}
height={20}
/>;
}
let description = _t("%(count)s members", { count: room.num_joined_members });
if (numChildRooms !== undefined) {
description += " · " + _t("%(count)s rooms", { count: numChildRooms });
}
if (room.topic) {
description += " · " + room.topic;
const topic = joinedRoom?.currentState?.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic || room.topic;
if (topic) {
description += " · " + topic;
}
let suggestedSection;
@ -167,7 +175,7 @@ const Tile: React.FC<ITileProps> = ({
}
const content = <React.Fragment>
<BaseAvatar name={name} idName={room.room_id} url={url} width={20} height={20} />
{ avatar }
<div className="mx_SpaceRoomDirectory_roomTile_name">
{ name }
{ suggestedSection }
@ -311,7 +319,7 @@ export const HierarchyLevel = ({
key={roomId}
room={rooms.get(roomId)}
numChildRooms={Array.from(relations.get(roomId)?.values() || [])
.filter(ev => rooms.get(ev.state_key)?.room_type !== RoomType.Space).length}
.filter(ev => rooms.has(ev.state_key) && !rooms.get(ev.state_key).room_type).length}
suggested={relations.get(spaceId)?.get(roomId)?.content.suggested}
selected={selectedMap?.get(spaceId)?.has(roomId)}
onViewRoomClick={(autoJoin) => {
@ -429,7 +437,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
let content;
if (roomsMap) {
const numRooms = Array.from(roomsMap.values()).filter(r => r.room_type !== RoomType.Space).length;
const numRooms = Array.from(roomsMap.values()).filter(r => !r.room_type).length;
const numSpaces = roomsMap.size - numRooms - 1; // -1 at the end to exclude the space we are looking at
let countsStr;

View file

@ -417,9 +417,13 @@ const SpaceLanding = ({ space }) => {
{ inviteButton }
{ settingsButton }
</div>
<div className="mx_SpaceRoomView_landing_topic">
<RoomTopic room={space} />
</div>
<RoomTopic room={space}>
{(topic, ref) => (
<div className="mx_SpaceRoomView_landing_topic" ref={ref}>
{ topic }
</div>
)}
</RoomTopic>
<SpaceFeedbackPrompt />
<hr />
@ -437,7 +441,6 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
const [error, setError] = useState("");
const numFields = 3;
const placeholders = [_t("General"), _t("Random"), _t("Support")];
// TODO vary default prefills for "Just Me" spaces
const [roomNames, setRoomName] = useStateArray(numFields, [_t("General"), _t("Random"), ""]);
const fields = new Array(numFields).fill(0).map((_, i) => {
const name = "roomName" + i;

View file

@ -57,7 +57,8 @@ import { IHostSignupConfig } from "../views/dialogs/HostSignupDialogTypes";
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore";
import RoomName from "../views/elements/RoomName";
import {replaceableComponent} from "../../utils/replaceableComponent";
import InlineSpinner from "../views/elements/InlineSpinner";
import TooltipButton from "../views/elements/TooltipButton";
interface IProps {
isMinimized: boolean;
}
@ -68,6 +69,7 @@ interface IState {
contextMenuPosition: PartialDOMRect;
isDarkTheme: boolean;
selectedSpace?: Room;
pendingRoomJoin: Set<string>;
}
@replaceableComponent("structures.UserMenu")
@ -84,6 +86,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
this.state = {
contextMenuPosition: null,
isDarkTheme: this.isUserOnDarkTheme(),
pendingRoomJoin: new Set<string>(),
};
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
@ -103,6 +106,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
this.dispatcherRef = defaultDispatcher.register(this.onAction);
this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged);
this.tagStoreRef = GroupFilterOrderStore.addListener(this.onTagStoreUpdate);
MatrixClientPeg.get().on("Room", this.onRoom);
}
public componentWillUnmount() {
@ -114,6 +118,11 @@ export default class UserMenu extends React.Component<IProps, IState> {
if (SettingsStore.getValue("feature_spaces")) {
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
}
MatrixClientPeg.get().removeListener("Room", this.onRoom);
}
private onRoom = (room: Room): void => {
this.removePendingJoinRoom(room.roomId);
}
private onTagStoreUpdate = () => {
@ -147,15 +156,39 @@ export default class UserMenu extends React.Component<IProps, IState> {
};
private onAction = (ev: ActionPayload) => {
if (ev.action !== Action.ToggleUserMenu) return; // not interested
if (this.state.contextMenuPosition) {
this.setState({contextMenuPosition: null});
} else {
if (this.buttonRef.current) this.buttonRef.current.click();
switch (ev.action) {
case Action.ToggleUserMenu:
if (this.state.contextMenuPosition) {
this.setState({contextMenuPosition: null});
} else {
if (this.buttonRef.current) this.buttonRef.current.click();
}
break;
case Action.JoinRoom:
this.addPendingJoinRoom(ev.roomId);
break;
case Action.JoinRoomReady:
case Action.JoinRoomError:
this.removePendingJoinRoom(ev.roomId);
break;
}
};
private addPendingJoinRoom(roomId: string): void {
this.setState({
pendingRoomJoin: new Set<string>(this.state.pendingRoomJoin)
.add(roomId),
});
}
private removePendingJoinRoom(roomId: string): void {
if (this.state.pendingRoomJoin.delete(roomId)) {
this.setState({
pendingRoomJoin: new Set<string>(this.state.pendingRoomJoin),
})
}
}
private onOpenMenuClick = (ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
@ -617,6 +650,14 @@ export default class UserMenu extends React.Component<IProps, IState> {
/>
</span>
{name}
{this.state.pendingRoomJoin.size > 0 && (
<InlineSpinner>
<TooltipButton helpText={_t(
"Currently joining %(count)s rooms",
{ count: this.state.pendingRoomJoin.size },
)} />
</InlineSpinner>
)}
{dnd}
{buttons}
</div>

View file

@ -59,6 +59,7 @@ interface IProps {
fallbackHsUrl?: string;
defaultDeviceDisplayName?: string;
fragmentAfterLogin?: string;
defaultUsername?: string;
// Called when the user has logged in. Params:
// - The object returned by the login API
@ -119,7 +120,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
flows: null,
username: "",
username: props.defaultUsername? props.defaultUsername: '',
phoneCountry: null,
phoneNumber: "",

View file

@ -223,7 +223,8 @@ export default class Registration extends React.Component<IProps, IState> {
this.setState({
flows: e.data.flows,
});
} else if (e.httpStatus === 403 && e.errcode === "M_UNKNOWN") {
} else if (e.httpStatus === 403 || e.errcode === "M_FORBIDDEN") {
// Check for 403 or M_FORBIDDEN, Synapse used to send 403 M_UNKNOWN but now sends 403 M_FORBIDDEN.
// At this point registration is pretty much disabled, but before we do that let's
// quickly check to see if the server supports SSO instead. If it does, we'll send
// the user off to the login page to figure their account out.
@ -467,7 +468,7 @@ export default class Registration extends React.Component<IProps, IState> {
let ssoSection;
if (this.state.ssoFlow) {
let continueWithSection;
const providers = this.state.ssoFlow["org.matrix.msc2858.identity_providers"] || [];
const providers = this.state.ssoFlow.identity_providers || [];
// when there is only a single (or 0) providers we show a wide button with `Continue with X` text
if (providers.length > 1) {
// i18n: ssoButtons is a placeholder to help translators understand context