Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into travis/blurhash
Conflicts: package.json src/ContentMessages.tsx yarn.lock
This commit is contained in:
commit
660a849cdd
235 changed files with 10534 additions and 4471 deletions
|
@ -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>);
|
||||
}
|
||||
}
|
65
src/components/structures/AutoHideScrollbar.tsx
Normal file
65
src/components/structures/AutoHideScrollbar.tsx
Normal 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>);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -50,6 +50,9 @@ class FilePanel extends React.Component {
|
|||
if (room?.roomId !== this.props?.roomId) return;
|
||||
if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return;
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
client.decryptEventIfNeeded(ev);
|
||||
|
||||
if (ev.isBeingDecrypted()) {
|
||||
this.decryptingEvents.add(ev.getId());
|
||||
} else {
|
||||
|
|
|
@ -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); });
|
||||
|
|
|
@ -24,13 +24,16 @@ import { HostSignupStore } from "../../stores/HostSignupStore";
|
|||
import SdkConfig from "../../SdkConfig";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {}
|
||||
interface IProps {
|
||||
onClick?(): void;
|
||||
}
|
||||
|
||||
interface IState {}
|
||||
|
||||
@replaceableComponent("structures.HostSignupAction")
|
||||
export default class HostSignupAction extends React.PureComponent<IProps, IState> {
|
||||
private openDialog = async () => {
|
||||
this.props.onClick?.();
|
||||
await HostSignupStore.instance.setHostSignupActive(true);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,9 @@ 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}
|
||||
onResize={this.refreshStickyHeaders}
|
||||
onListCollapse={this.refreshStickyHeaders}
|
||||
/>;
|
||||
|
||||
const containerClasses = classNames({
|
||||
|
@ -435,17 +454,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 +472,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>
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
}}
|
||||
|
|
|
@ -358,7 +358,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
|
||||
const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM);
|
||||
for (const eventId of pinnedEventIds) {
|
||||
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0);
|
||||
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId);
|
||||
const event = timeline.getEvents().find(ev => ev.getId() === eventId);
|
||||
if (event) events.push(event);
|
||||
}
|
||||
|
|
|
@ -86,6 +86,9 @@ import {RoomUpdateCause} from "../../stores/room-list/models";
|
|||
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 {
|
||||
// a special initial state which is only used at startup, while we are
|
||||
|
@ -223,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;
|
||||
|
@ -275,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;
|
||||
|
||||
|
@ -376,7 +378,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
this.onLoggedIn();
|
||||
}
|
||||
|
||||
const promisesList = [this.firstSyncPromise.promise];
|
||||
const promisesList: Promise<any>[] = [this.firstSyncPromise.promise];
|
||||
if (cryptoEnabled) {
|
||||
// wait for the client to finish downloading cross-signing keys for us so we
|
||||
// know whether or not we have keys set up on this account
|
||||
|
@ -434,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);
|
||||
|
@ -484,42 +486,22 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
startPageChangeTimer() {
|
||||
// Tor doesn't support performance
|
||||
if (!performance || !performance.mark) return null;
|
||||
|
||||
// This shouldn't happen because UNSAFE_componentWillUpdate and componentDidUpdate
|
||||
// are used.
|
||||
if (this.pageChanging) {
|
||||
console.warn('MatrixChat.startPageChangeTimer: timer already started');
|
||||
return;
|
||||
}
|
||||
this.pageChanging = true;
|
||||
performance.mark('element_MatrixChat_page_change_start');
|
||||
PerformanceMonitor.instance.start(PerformanceEntryNames.PAGE_CHANGE);
|
||||
}
|
||||
|
||||
stopPageChangeTimer() {
|
||||
// Tor doesn't support performance
|
||||
if (!performance || !performance.mark) return null;
|
||||
const perfMonitor = PerformanceMonitor.instance;
|
||||
|
||||
if (!this.pageChanging) {
|
||||
console.warn('MatrixChat.stopPageChangeTimer: timer not started');
|
||||
return;
|
||||
}
|
||||
this.pageChanging = false;
|
||||
performance.mark('element_MatrixChat_page_change_stop');
|
||||
performance.measure(
|
||||
'element_MatrixChat_page_change_delta',
|
||||
'element_MatrixChat_page_change_start',
|
||||
'element_MatrixChat_page_change_stop',
|
||||
);
|
||||
performance.clearMarks('element_MatrixChat_page_change_start');
|
||||
performance.clearMarks('element_MatrixChat_page_change_stop');
|
||||
const measurement = performance.getEntriesByName('element_MatrixChat_page_change_delta').pop();
|
||||
perfMonitor.stop(PerformanceEntryNames.PAGE_CHANGE);
|
||||
|
||||
// In practice, sometimes the entries list is empty, so we get no measurement
|
||||
if (!measurement) return null;
|
||||
const entries = perfMonitor.getEntries({
|
||||
name: PerformanceEntryNames.PAGE_CHANGE,
|
||||
});
|
||||
const measurement = entries.pop();
|
||||
|
||||
return measurement.duration;
|
||||
return measurement
|
||||
? measurement.duration
|
||||
: null;
|
||||
}
|
||||
|
||||
shouldTrackPageChange(prevState: IState, state: IState) {
|
||||
|
@ -683,7 +665,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
break;
|
||||
}
|
||||
case 'view_create_room':
|
||||
this.createRoom(payload.public);
|
||||
this.createRoom(payload.public, payload.defaultName);
|
||||
break;
|
||||
case 'view_create_group': {
|
||||
let CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog")
|
||||
|
@ -1029,7 +1011,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
}
|
||||
|
||||
private async createRoom(defaultPublic = false) {
|
||||
private async createRoom(defaultPublic = false, defaultName?: string) {
|
||||
const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
|
||||
if (communityId) {
|
||||
// double check the user will have permission to associate this room with the community
|
||||
|
@ -1043,7 +1025,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog');
|
||||
const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, { defaultPublic });
|
||||
const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, {
|
||||
defaultPublic,
|
||||
defaultName,
|
||||
});
|
||||
|
||||
const [shouldCreate, opts] = await modal.finished;
|
||||
if (shouldCreate) {
|
||||
|
@ -1632,11 +1617,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
action: 'start_registration',
|
||||
params: params,
|
||||
});
|
||||
PerformanceMonitor.instance.start(PerformanceEntryNames.REGISTER);
|
||||
} else if (screen === 'login') {
|
||||
dis.dispatch({
|
||||
action: 'start_login',
|
||||
params: params,
|
||||
});
|
||||
PerformanceMonitor.instance.start(PerformanceEntryNames.LOGIN);
|
||||
} else if (screen === 'forgot_password') {
|
||||
dis.dispatch({
|
||||
action: 'start_password_recovery',
|
||||
|
@ -1833,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() {
|
||||
|
@ -1965,6 +1953,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
// Create and start the client
|
||||
await Lifecycle.setLoggedIn(credentials);
|
||||
await this.postLoginSetup();
|
||||
PerformanceMonitor.instance.stop(PerformanceEntryNames.LOGIN);
|
||||
PerformanceMonitor.instance.stop(PerformanceEntryNames.REGISTER);
|
||||
};
|
||||
|
||||
// complete security / e2e setup has finished
|
||||
|
@ -2101,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()}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -19,17 +19,17 @@ limitations under the License.
|
|||
import React, {createRef} from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import shouldHideEvent from '../../shouldHideEvent';
|
||||
import {wantsDateSeparator} from '../../DateUtils';
|
||||
import * as sdk from '../../index';
|
||||
|
||||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import SettingsStore from '../../settings/SettingsStore';
|
||||
import RoomContext from "../../contexts/RoomContext";
|
||||
import {Layout, LayoutPropType} from "../../settings/Layout";
|
||||
import {_t} from "../../languageHandler";
|
||||
import {haveTileForEvent} from "../views/rooms/EventTile";
|
||||
import {textForEvent} from "../../TextForEvent";
|
||||
import {hasText} from "../../TextForEvent";
|
||||
import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
|
||||
import DMRoomMap from "../../utils/DMRoomMap";
|
||||
import NewRoomIntro from "../views/rooms/NewRoomIntro";
|
||||
|
@ -121,6 +121,9 @@ export default class MessagePanel extends React.Component {
|
|||
// callback which is called when the panel is scrolled.
|
||||
onScroll: PropTypes.func,
|
||||
|
||||
// callback which is called when the user interacts with the room timeline
|
||||
onUserScroll: PropTypes.func,
|
||||
|
||||
// callback which is called when more content is needed.
|
||||
onFillRequest: PropTypes.func,
|
||||
|
||||
|
@ -149,6 +152,8 @@ export default class MessagePanel extends React.Component {
|
|||
enableFlair: PropTypes.bool,
|
||||
};
|
||||
|
||||
static contextType = RoomContext;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
@ -378,7 +383,7 @@ export default class MessagePanel extends React.Component {
|
|||
// Always show highlighted event
|
||||
if (this.props.highlightedEventId === mxEv.getId()) return true;
|
||||
|
||||
return !shouldHideEvent(mxEv);
|
||||
return !shouldHideEvent(mxEv, this.context);
|
||||
}
|
||||
|
||||
_readMarkerForEvent(eventId, isLastEvent) {
|
||||
|
@ -613,10 +618,6 @@ export default class MessagePanel extends React.Component {
|
|||
const eventId = mxEv.getId();
|
||||
const highlight = (eventId === this.props.highlightedEventId);
|
||||
|
||||
// we can't use local echoes as scroll tokens, because their event IDs change.
|
||||
// Local echos have a send "status".
|
||||
const scrollToken = mxEv.status ? undefined : eventId;
|
||||
|
||||
const readReceipts = this._readReceiptsByEvent[eventId];
|
||||
|
||||
let isLastSuccessful = false;
|
||||
|
@ -645,39 +646,36 @@ export default class MessagePanel extends React.Component {
|
|||
|
||||
// use txnId as key if available so that we don't remount during sending
|
||||
ret.push(
|
||||
<li
|
||||
key={mxEv.getTxnId() || eventId}
|
||||
ref={this._collectEventNode.bind(this, eventId)}
|
||||
data-scroll-tokens={scrollToken}
|
||||
>
|
||||
<TileErrorBoundary mxEvent={mxEv}>
|
||||
<EventTile
|
||||
mxEvent={mxEv}
|
||||
continuation={continuation}
|
||||
isRedacted={mxEv.isRedacted()}
|
||||
replacingEventId={mxEv.replacingEventId()}
|
||||
editState={isEditing && this.props.editState}
|
||||
onHeightChanged={this._onHeightChanged}
|
||||
readReceipts={readReceipts}
|
||||
readReceiptMap={this._readReceiptMap}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
checkUnmounting={this._isUnmounting}
|
||||
eventSendStatus={mxEv.getAssociatedStatus()}
|
||||
tileShape={this.props.tileShape}
|
||||
isTwelveHour={this.props.isTwelveHour}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
last={last}
|
||||
lastInSection={willWantDateSeparator}
|
||||
lastSuccessful={isLastSuccessful}
|
||||
isSelectedEvent={highlight}
|
||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||
showReactions={this.props.showReactions}
|
||||
layout={this.props.layout}
|
||||
enableFlair={this.props.enableFlair}
|
||||
showReadReceipts={this.props.showReadReceipts}
|
||||
/>
|
||||
</TileErrorBoundary>
|
||||
</li>,
|
||||
<TileErrorBoundary key={mxEv.getTxnId() || eventId} mxEvent={mxEv}>
|
||||
<EventTile
|
||||
as="li"
|
||||
ref={this._collectEventNode.bind(this, eventId)}
|
||||
alwaysShowTimestamps={this.props.alwaysShowTimestamps}
|
||||
mxEvent={mxEv}
|
||||
continuation={continuation}
|
||||
isRedacted={mxEv.isRedacted()}
|
||||
replacingEventId={mxEv.replacingEventId()}
|
||||
editState={isEditing && this.props.editState}
|
||||
onHeightChanged={this._onHeightChanged}
|
||||
readReceipts={readReceipts}
|
||||
readReceiptMap={this._readReceiptMap}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
checkUnmounting={this._isUnmounting}
|
||||
eventSendStatus={mxEv.getAssociatedStatus()}
|
||||
tileShape={this.props.tileShape}
|
||||
isTwelveHour={this.props.isTwelveHour}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
last={last}
|
||||
lastInSection={willWantDateSeparator}
|
||||
lastSuccessful={isLastSuccessful}
|
||||
isSelectedEvent={highlight}
|
||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||
showReactions={this.props.showReactions}
|
||||
layout={this.props.layout}
|
||||
enableFlair={this.props.enableFlair}
|
||||
showReadReceipts={this.props.showReadReceipts}
|
||||
/>
|
||||
</TileErrorBoundary>,
|
||||
);
|
||||
|
||||
return ret;
|
||||
|
@ -779,7 +777,7 @@ export default class MessagePanel extends React.Component {
|
|||
}
|
||||
|
||||
_collectEventNode = (eventId, node) => {
|
||||
this.eventNodes[eventId] = node;
|
||||
this.eventNodes[eventId] = node?.ref?.current;
|
||||
}
|
||||
|
||||
// once dynamic content in the events load, make the scrollPanel check the
|
||||
|
@ -853,13 +851,6 @@ export default class MessagePanel extends React.Component {
|
|||
|
||||
const style = this.props.hidden ? { display: 'none' } : {};
|
||||
|
||||
const className = classNames(
|
||||
this.props.className,
|
||||
{
|
||||
"mx_MessagePanel_alwaysShowTimestamps": this.props.alwaysShowTimestamps,
|
||||
},
|
||||
);
|
||||
|
||||
let whoIsTyping;
|
||||
if (this.props.room && !this.props.tileShape && this.state.showTypingNotifications) {
|
||||
whoIsTyping = (<WhoIsTypingTile
|
||||
|
@ -883,8 +874,9 @@ export default class MessagePanel extends React.Component {
|
|||
<ErrorBoundary>
|
||||
<ScrollPanel
|
||||
ref={this._scrollPanel}
|
||||
className={className}
|
||||
className={this.props.className}
|
||||
onScroll={this.props.onScroll}
|
||||
onUserScroll={this.props.onUserScroll}
|
||||
onResize={this.onResize}
|
||||
onFillRequest={this.props.onFillRequest}
|
||||
onUnfillRequest={this.props.onUnfillRequest}
|
||||
|
@ -1175,11 +1167,8 @@ class MemberGrouper {
|
|||
|
||||
add(ev) {
|
||||
if (ev.getType() === 'm.room.member') {
|
||||
// We'll just double check that it's worth our time to do so, through an
|
||||
// ugly hack. If textForEvent returns something, we should group it for
|
||||
// rendering but if it doesn't then we'll exclude it.
|
||||
const renderText = textForEvent(ev);
|
||||
if (!renderText || renderText.trim().length === 0) return; // quietly ignore
|
||||
// We can ignore any events that don't actually have a message to display
|
||||
if (!hasText(ev)) return;
|
||||
}
|
||||
this.readMarker = this.readMarker || this.panel._readMarkerForEvent(
|
||||
ev.getId(),
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2016, 2019, 2021 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.
|
||||
|
@ -16,29 +14,25 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from "prop-types";
|
||||
import React from "react";
|
||||
|
||||
import { _t } from '../../languageHandler';
|
||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
||||
import * as sdk from "../../index";
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import BaseCard from "../views/right_panel/BaseCard";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import TimelinePanel from "./TimelinePanel";
|
||||
import Spinner from "../views/elements/Spinner";
|
||||
|
||||
interface IProps {
|
||||
onClose(): void;
|
||||
}
|
||||
|
||||
/*
|
||||
* Component which shows the global notification list using a TimelinePanel
|
||||
*/
|
||||
@replaceableComponent("structures.NotificationPanel")
|
||||
class NotificationPanel extends React.Component {
|
||||
static propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default class NotificationPanel extends React.PureComponent<IProps> {
|
||||
render() {
|
||||
// wrap a TimelinePanel with the jump-to-event bits turned off.
|
||||
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
|
||||
const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
|
||||
<h2>{_t('You’re all caught up')}</h2>
|
||||
<p>{_t('You have no visible notifications.')}</p>
|
||||
|
@ -47,6 +41,7 @@ class NotificationPanel extends React.Component {
|
|||
let content;
|
||||
const timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
|
||||
if (timelineSet) {
|
||||
// wrap a TimelinePanel with the jump-to-event bits turned off.
|
||||
content = (
|
||||
<TimelinePanel
|
||||
manageReadReceipts={false}
|
||||
|
@ -55,11 +50,12 @@ class NotificationPanel extends React.Component {
|
|||
showUrlPreview={false}
|
||||
tileShape="notif"
|
||||
empty={emptyState}
|
||||
alwaysShowTimestamps={true}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
console.error("No notifTimelineSet available!");
|
||||
content = <Loader />;
|
||||
content = <Spinner />;
|
||||
}
|
||||
|
||||
return <BaseCard className="mx_NotificationPanel" onClose={this.props.onClose} withoutScrollContainer>
|
||||
|
@ -67,5 +63,3 @@ class NotificationPanel extends React.Component {
|
|||
</BaseCard>;
|
||||
}
|
||||
}
|
||||
|
||||
export default NotificationPanel;
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2015 - 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2015 - 2021 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.
|
||||
|
@ -16,70 +16,92 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
|
||||
import * as sdk from '../../index';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import RateLimitedFunc from '../../ratelimitedfunc';
|
||||
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
||||
import GroupStore from '../../stores/GroupStore';
|
||||
import {
|
||||
RightPanelPhases,
|
||||
RIGHT_PANEL_PHASES_NO_ARGS,
|
||||
RIGHT_PANEL_SPACE_PHASES,
|
||||
RightPanelPhases,
|
||||
} from "../../stores/RightPanelStorePhases";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import {Action} from "../../dispatcher/actions";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
|
||||
import WidgetCard from "../views/right_panel/WidgetCard";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
import MemberList from "../views/rooms/MemberList";
|
||||
import GroupMemberList from "../views/groups/GroupMemberList";
|
||||
import GroupRoomList from "../views/groups/GroupRoomList";
|
||||
import GroupRoomInfo from "../views/groups/GroupRoomInfo";
|
||||
import UserInfo from "../views/right_panel/UserInfo";
|
||||
import ThirdPartyMemberInfo from "../views/rooms/ThirdPartyMemberInfo";
|
||||
import FilePanel from "./FilePanel";
|
||||
import NotificationPanel from "./NotificationPanel";
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard";
|
||||
|
||||
interface IProps {
|
||||
room?: Room; // if showing panels for a given room, this is set
|
||||
groupId?: string; // if showing panels for a given group, this is set
|
||||
user?: User; // used if we know the user ahead of opening the panel
|
||||
resizeNotifier: ResizeNotifier;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
phase: RightPanelPhases;
|
||||
isUserPrivilegedInGroup?: boolean;
|
||||
member?: RoomMember;
|
||||
verificationRequest?: VerificationRequest;
|
||||
verificationRequestPromise?: Promise<VerificationRequest>;
|
||||
space?: Room;
|
||||
widgetId?: string;
|
||||
groupRoomId?: string;
|
||||
groupId?: string;
|
||||
event: MatrixEvent;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.RightPanel")
|
||||
export default class RightPanel extends React.Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
room: PropTypes.instanceOf(Room), // if showing panels for a given room, this is set
|
||||
groupId: PropTypes.string, // if showing panels for a given group, this is set
|
||||
user: PropTypes.object, // used if we know the user ahead of opening the panel
|
||||
};
|
||||
}
|
||||
|
||||
export default class RightPanel extends React.Component<IProps, IState> {
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
private readonly delayedUpdate: RateLimitedFunc;
|
||||
private dispatcherRef: string;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
...RightPanelStore.getSharedInstance().roomPanelPhaseParams,
|
||||
phase: this._getPhaseFromProps(),
|
||||
phase: this.getPhaseFromProps(),
|
||||
isUserPrivilegedInGroup: null,
|
||||
member: this._getUserForPanel(),
|
||||
member: this.getUserForPanel(),
|
||||
};
|
||||
this.onAction = this.onAction.bind(this);
|
||||
this.onRoomStateMember = this.onRoomStateMember.bind(this);
|
||||
this.onGroupStoreUpdated = this.onGroupStoreUpdated.bind(this);
|
||||
this.onInviteToGroupButtonClick = this.onInviteToGroupButtonClick.bind(this);
|
||||
this.onAddRoomToGroupButtonClick = this.onAddRoomToGroupButtonClick.bind(this);
|
||||
|
||||
this._delayedUpdate = new RateLimitedFunc(() => {
|
||||
this.delayedUpdate = new RateLimitedFunc(() => {
|
||||
this.forceUpdate();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Helper function to split out the logic for _getPhaseFromProps() and the constructor
|
||||
// Helper function to split out the logic for getPhaseFromProps() and the constructor
|
||||
// as both are called at the same time in the constructor.
|
||||
_getUserForPanel() {
|
||||
private getUserForPanel() {
|
||||
if (this.state && this.state.member) return this.state.member;
|
||||
const lastParams = RightPanelStore.getSharedInstance().roomPanelPhaseParams;
|
||||
return this.props.user || lastParams['member'];
|
||||
}
|
||||
|
||||
// gets the current phase from the props and also maybe the store
|
||||
_getPhaseFromProps() {
|
||||
private getPhaseFromProps() {
|
||||
const rps = RightPanelStore.getSharedInstance();
|
||||
const userForPanel = this._getUserForPanel();
|
||||
const userForPanel = this.getUserForPanel();
|
||||
if (this.props.groupId) {
|
||||
if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.groupPanelPhase)) {
|
||||
dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.GroupMemberList});
|
||||
|
@ -118,7 +140,7 @@ export default class RightPanel extends React.Component {
|
|||
this.dispatcherRef = dis.register(this.onAction);
|
||||
const cli = this.context;
|
||||
cli.on("RoomState.members", this.onRoomStateMember);
|
||||
this._initGroupStore(this.props.groupId);
|
||||
this.initGroupStore(this.props.groupId);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -126,61 +148,47 @@ export default class RightPanel extends React.Component {
|
|||
if (this.context) {
|
||||
this.context.removeListener("RoomState.members", this.onRoomStateMember);
|
||||
}
|
||||
this._unregisterGroupStore(this.props.groupId);
|
||||
this.unregisterGroupStore();
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
|
||||
if (newProps.groupId !== this.props.groupId) {
|
||||
this._unregisterGroupStore(this.props.groupId);
|
||||
this._initGroupStore(newProps.groupId);
|
||||
this.unregisterGroupStore();
|
||||
this.initGroupStore(newProps.groupId);
|
||||
}
|
||||
}
|
||||
|
||||
_initGroupStore(groupId) {
|
||||
private initGroupStore(groupId: string) {
|
||||
if (!groupId) return;
|
||||
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
|
||||
}
|
||||
|
||||
_unregisterGroupStore() {
|
||||
private unregisterGroupStore() {
|
||||
GroupStore.unregisterListener(this.onGroupStoreUpdated);
|
||||
}
|
||||
|
||||
onGroupStoreUpdated() {
|
||||
private onGroupStoreUpdated = () => {
|
||||
this.setState({
|
||||
isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onInviteToGroupButtonClick() {
|
||||
showGroupInviteDialog(this.props.groupId).then(() => {
|
||||
this.setState({
|
||||
phase: RightPanelPhases.GroupMemberList,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onAddRoomToGroupButtonClick() {
|
||||
showGroupAddRoomDialog(this.props.groupId).then(() => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
onRoomStateMember(ev, state, member) {
|
||||
private onRoomStateMember = (ev: MatrixEvent, _, member: RoomMember) => {
|
||||
if (!this.props.room || member.roomId !== this.props.room.roomId) {
|
||||
return;
|
||||
}
|
||||
// redraw the badge on the membership list
|
||||
if (this.state.phase === RightPanelPhases.RoomMemberList && member.roomId === this.props.room.roomId) {
|
||||
this._delayedUpdate();
|
||||
this.delayedUpdate();
|
||||
} else if (this.state.phase === RightPanelPhases.RoomMemberInfo && member.roomId === this.props.room.roomId &&
|
||||
member.userId === this.state.member.userId) {
|
||||
// refresh the member info (e.g. new power level)
|
||||
this._delayedUpdate();
|
||||
this.delayedUpdate();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onAction(payload) {
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
if (payload.action === Action.AfterRightPanelPhaseChange) {
|
||||
this.setState({
|
||||
phase: payload.phase,
|
||||
|
@ -194,9 +202,9 @@ export default class RightPanel extends React.Component {
|
|||
space: payload.space,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onClose = () => {
|
||||
private onClose = () => {
|
||||
// XXX: There are three different ways of 'closing' this panel depending on what state
|
||||
// things are in... this knows far more than it should do about the state of the rest
|
||||
// of the app and is generally a bit silly.
|
||||
|
@ -224,16 +232,6 @@ export default class RightPanel extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
const MemberList = sdk.getComponent('rooms.MemberList');
|
||||
const UserInfo = sdk.getComponent('right_panel.UserInfo');
|
||||
const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo');
|
||||
const NotificationPanel = sdk.getComponent('structures.NotificationPanel');
|
||||
const FilePanel = sdk.getComponent('structures.FilePanel');
|
||||
|
||||
const GroupMemberList = sdk.getComponent('groups.GroupMemberList');
|
||||
const GroupRoomList = sdk.getComponent('groups.GroupRoomList');
|
||||
const GroupRoomInfo = sdk.getComponent('groups.GroupRoomInfo');
|
||||
|
||||
let panel = <div />;
|
||||
const roomId = this.props.room ? this.props.room.roomId : undefined;
|
||||
|
||||
|
@ -285,6 +283,7 @@ export default class RightPanel extends React.Component {
|
|||
user={this.state.member}
|
||||
groupId={this.props.groupId}
|
||||
key={this.state.member.userId}
|
||||
phase={this.state.phase}
|
||||
onClose={this.onClose} />;
|
||||
break;
|
||||
|
||||
|
@ -299,6 +298,12 @@ export default class RightPanel extends React.Component {
|
|||
panel = <NotificationPanel onClose={this.onClose} />;
|
||||
break;
|
||||
|
||||
case RightPanelPhases.PinnedMessages:
|
||||
if (SettingsStore.getValue("feature_pinning")) {
|
||||
panel = <PinnedMessagesCard room={this.props.room} onClose={this.onClose} />;
|
||||
}
|
||||
break;
|
||||
|
||||
case RightPanelPhases.FilePanel:
|
||||
panel = <FilePanel roomId={roomId} resizeNotifier={this.props.resizeNotifier} onClose={this.onClose} />;
|
||||
break;
|
|
@ -1,7 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2015, 2016, 2019, 2020, 2021 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.
|
||||
|
@ -16,39 +15,90 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
||||
import * as sdk from "../../index";
|
||||
import React from "react";
|
||||
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import Modal from "../../Modal";
|
||||
import { linkifyAndSanitizeHtml } from '../../HtmlUtils';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../languageHandler';
|
||||
import SdkConfig from '../../SdkConfig';
|
||||
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
|
||||
import Analytics from '../../Analytics';
|
||||
import {ALL_ROOMS} from "../views/directory/NetworkDropdown";
|
||||
import {ALL_ROOMS, IFieldType, IInstance, IProtocol, Protocols} from "../views/directory/NetworkDropdown";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
|
||||
import GroupStore from "../../stores/GroupStore";
|
||||
import FlairStore from "../../stores/FlairStore";
|
||||
import CountlyAnalytics from "../../CountlyAnalytics";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../customisations/Media";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../customisations/Media";
|
||||
import { IDialogProps } from "../views/dialogs/IDialogProps";
|
||||
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
|
||||
import BaseAvatar from "../views/avatars/BaseAvatar";
|
||||
import ErrorDialog from "../views/dialogs/ErrorDialog";
|
||||
import QuestionDialog from "../views/dialogs/QuestionDialog";
|
||||
import BaseDialog from "../views/dialogs/BaseDialog";
|
||||
import DirectorySearchBox from "../views/elements/DirectorySearchBox";
|
||||
import NetworkDropdown from "../views/directory/NetworkDropdown";
|
||||
import ScrollPanel from "./ScrollPanel";
|
||||
import Spinner from "../views/elements/Spinner";
|
||||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
|
||||
|
||||
const MAX_NAME_LENGTH = 80;
|
||||
const MAX_TOPIC_LENGTH = 800;
|
||||
|
||||
function track(action) {
|
||||
function track(action: string) {
|
||||
Analytics.trackEvent('RoomDirectory', action);
|
||||
}
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
initialText?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
publicRooms: IRoom[];
|
||||
loading: boolean;
|
||||
protocolsLoading: boolean;
|
||||
error?: string;
|
||||
instanceId: string | symbol;
|
||||
roomServer: string;
|
||||
filterString: string;
|
||||
selectedCommunityId?: string;
|
||||
communityName?: string;
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
interface IRoom {
|
||||
room_id: string;
|
||||
name?: string;
|
||||
avatar_url?: string;
|
||||
topic?: string;
|
||||
canonical_alias?: string;
|
||||
aliases?: string[];
|
||||
world_readable: boolean;
|
||||
guest_can_join: boolean;
|
||||
num_joined_members: number;
|
||||
}
|
||||
|
||||
interface IPublicRoomsRequest {
|
||||
limit?: number;
|
||||
since?: string;
|
||||
server?: string;
|
||||
filter?: object;
|
||||
include_all_networks?: boolean;
|
||||
third_party_instance_id?: string;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
@replaceableComponent("structures.RoomDirectory")
|
||||
export default class RoomDirectory extends React.Component {
|
||||
static propTypes = {
|
||||
initialText: PropTypes.string,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||
private readonly startTime: number;
|
||||
private unmounted = false
|
||||
private nextBatch: string = null;
|
||||
private filterTimeout: NodeJS.Timeout;
|
||||
private protocols: Protocols;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -56,41 +106,21 @@ export default class RoomDirectory extends React.Component {
|
|||
CountlyAnalytics.instance.trackRoomDirectoryBegin();
|
||||
this.startTime = CountlyAnalytics.getTimestamp();
|
||||
|
||||
const selectedCommunityId = GroupFilterOrderStore.getSelectedTags()[0];
|
||||
this.state = {
|
||||
publicRooms: [],
|
||||
loading: true,
|
||||
protocolsLoading: true,
|
||||
error: null,
|
||||
instanceId: undefined,
|
||||
roomServer: MatrixClientPeg.getHomeserverName(),
|
||||
filterString: this.props.initialText || "",
|
||||
selectedCommunityId: SettingsStore.getValue("feature_communities_v2_prototypes")
|
||||
? selectedCommunityId
|
||||
: null,
|
||||
communityName: null,
|
||||
};
|
||||
const selectedCommunityId = SettingsStore.getValue("feature_communities_v2_prototypes")
|
||||
? GroupFilterOrderStore.getSelectedTags()[0]
|
||||
: null;
|
||||
|
||||
this._unmounted = false;
|
||||
this.nextBatch = null;
|
||||
this.filterTimeout = null;
|
||||
this.scrollPanel = null;
|
||||
this.protocols = null;
|
||||
|
||||
this.state.protocolsLoading = true;
|
||||
let protocolsLoading = true;
|
||||
if (!MatrixClientPeg.get()) {
|
||||
// We may not have a client yet when invoked from welcome page
|
||||
this.state.protocolsLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.state.selectedCommunityId) {
|
||||
protocolsLoading = false;
|
||||
} else if (!selectedCommunityId) {
|
||||
MatrixClientPeg.get().getThirdpartyProtocols().then((response) => {
|
||||
this.protocols = response;
|
||||
this.setState({protocolsLoading: false});
|
||||
this.setState({ protocolsLoading: false });
|
||||
}, (err) => {
|
||||
console.warn(`error loading third party protocols: ${err}`);
|
||||
this.setState({protocolsLoading: false});
|
||||
this.setState({ protocolsLoading: false });
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
// Guests currently aren't allowed to use this API, so
|
||||
// ignore this as otherwise this error is literally the
|
||||
|
@ -103,19 +133,31 @@ export default class RoomDirectory extends React.Component {
|
|||
error: _t(
|
||||
'%(brand)s failed to get the protocol list from the homeserver. ' +
|
||||
'The homeserver may be too old to support third party networks.',
|
||||
{brand},
|
||||
{ brand },
|
||||
),
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// We don't use the protocols in the communities v2 prototype experience
|
||||
this.state.protocolsLoading = false;
|
||||
protocolsLoading = false;
|
||||
|
||||
// Grab the profile info async
|
||||
FlairStore.getGroupProfileCached(MatrixClientPeg.get(), this.state.selectedCommunityId).then(profile => {
|
||||
this.setState({communityName: profile.name});
|
||||
this.setState({ communityName: profile.name });
|
||||
});
|
||||
}
|
||||
|
||||
this.state = {
|
||||
publicRooms: [],
|
||||
loading: true,
|
||||
error: null,
|
||||
instanceId: undefined,
|
||||
roomServer: MatrixClientPeg.getHomeserverName(),
|
||||
filterString: this.props.initialText || "",
|
||||
selectedCommunityId,
|
||||
communityName: null,
|
||||
protocolsLoading,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -126,10 +168,10 @@ export default class RoomDirectory extends React.Component {
|
|||
if (this.filterTimeout) {
|
||||
clearTimeout(this.filterTimeout);
|
||||
}
|
||||
this._unmounted = true;
|
||||
this.unmounted = true;
|
||||
}
|
||||
|
||||
refreshRoomList = () => {
|
||||
private refreshRoomList = () => {
|
||||
if (this.state.selectedCommunityId) {
|
||||
this.setState({
|
||||
publicRooms: GroupStore.getGroupRooms(this.state.selectedCommunityId).map(r => {
|
||||
|
@ -165,7 +207,7 @@ export default class RoomDirectory extends React.Component {
|
|||
this.getMoreRooms();
|
||||
};
|
||||
|
||||
getMoreRooms() {
|
||||
private getMoreRooms() {
|
||||
if (this.state.selectedCommunityId) return Promise.resolve(); // no more rooms
|
||||
if (!MatrixClientPeg.get()) return Promise.resolve();
|
||||
|
||||
|
@ -173,34 +215,34 @@ export default class RoomDirectory extends React.Component {
|
|||
loading: true,
|
||||
});
|
||||
|
||||
const my_filter_string = this.state.filterString;
|
||||
const my_server = this.state.roomServer;
|
||||
const filterString = this.state.filterString;
|
||||
const roomServer = this.state.roomServer;
|
||||
// remember the next batch token when we sent the request
|
||||
// too. If it's changed, appending to the list will corrupt it.
|
||||
const my_next_batch = this.nextBatch;
|
||||
const opts = {limit: 20};
|
||||
if (my_server != MatrixClientPeg.getHomeserverName()) {
|
||||
opts.server = my_server;
|
||||
const nextBatch = this.nextBatch;
|
||||
const opts: IPublicRoomsRequest = { limit: 20 };
|
||||
if (roomServer != MatrixClientPeg.getHomeserverName()) {
|
||||
opts.server = roomServer;
|
||||
}
|
||||
if (this.state.instanceId === ALL_ROOMS) {
|
||||
opts.include_all_networks = true;
|
||||
} else if (this.state.instanceId) {
|
||||
opts.third_party_instance_id = this.state.instanceId;
|
||||
opts.third_party_instance_id = this.state.instanceId as string;
|
||||
}
|
||||
if (this.nextBatch) opts.since = this.nextBatch;
|
||||
if (my_filter_string) opts.filter = { generic_search_term: my_filter_string };
|
||||
if (filterString) opts.filter = { generic_search_term: filterString };
|
||||
return MatrixClientPeg.get().publicRooms(opts).then((data) => {
|
||||
if (
|
||||
my_filter_string != this.state.filterString ||
|
||||
my_server != this.state.roomServer ||
|
||||
my_next_batch != this.nextBatch) {
|
||||
filterString != this.state.filterString ||
|
||||
roomServer != this.state.roomServer ||
|
||||
nextBatch != this.nextBatch) {
|
||||
// if the filter or server has changed since this request was sent,
|
||||
// throw away the result (don't even clear the busy flag
|
||||
// since we must still have a request in flight)
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._unmounted) {
|
||||
if (this.unmounted) {
|
||||
// if we've been unmounted, we don't care either.
|
||||
return;
|
||||
}
|
||||
|
@ -211,23 +253,23 @@ export default class RoomDirectory extends React.Component {
|
|||
}
|
||||
|
||||
this.nextBatch = data.next_batch;
|
||||
this.setState((s) => {
|
||||
s.publicRooms.push(...(data.chunk || []));
|
||||
s.loading = false;
|
||||
return s;
|
||||
});
|
||||
this.setState((s) => ({
|
||||
...s,
|
||||
publicRooms: [...s.publicRooms, ...(data.chunk || [])],
|
||||
loading: false,
|
||||
}));
|
||||
return Boolean(data.next_batch);
|
||||
}, (err) => {
|
||||
if (
|
||||
my_filter_string != this.state.filterString ||
|
||||
my_server != this.state.roomServer ||
|
||||
my_next_batch != this.nextBatch) {
|
||||
filterString != this.state.filterString ||
|
||||
roomServer != this.state.roomServer ||
|
||||
nextBatch != this.nextBatch) {
|
||||
// as above: we don't care about errors for old
|
||||
// requests either
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._unmounted) {
|
||||
if (this.unmounted) {
|
||||
// if we've been unmounted, we don't care either.
|
||||
return;
|
||||
}
|
||||
|
@ -252,13 +294,10 @@ export default class RoomDirectory extends React.Component {
|
|||
* HS admins to do this through the RoomSettings interface, but
|
||||
* this needs SPEC-417.
|
||||
*/
|
||||
removeFromDirectory(room) {
|
||||
const alias = get_display_alias_for_room(room);
|
||||
private removeFromDirectory(room: IRoom) {
|
||||
const alias = getDisplayAliasForRoom(room);
|
||||
const name = room.name || alias || _t('Unnamed room');
|
||||
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
||||
let desc;
|
||||
if (alias) {
|
||||
desc = _t('Delete the room address %(alias)s and remove %(name)s from the directory?', {alias, name});
|
||||
|
@ -269,11 +308,10 @@ export default class RoomDirectory extends React.Component {
|
|||
Modal.createTrackedDialog('Remove from Directory', '', QuestionDialog, {
|
||||
title: _t('Remove from Directory'),
|
||||
description: desc,
|
||||
onFinished: (should_delete) => {
|
||||
if (!should_delete) return;
|
||||
onFinished: (shouldDelete: boolean) => {
|
||||
if (!shouldDelete) return;
|
||||
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const modal = Modal.createDialog(Loader);
|
||||
const modal = Modal.createDialog(Spinner);
|
||||
let step = _t('remove %(name)s from the directory.', {name: name});
|
||||
|
||||
MatrixClientPeg.get().setRoomDirectoryVisibility(room.room_id, 'private').then(() => {
|
||||
|
@ -289,14 +327,16 @@ export default class RoomDirectory extends React.Component {
|
|||
console.error("Failed to " + step + ": " + err);
|
||||
Modal.createTrackedDialog('Remove from Directory Error', '', ErrorDialog, {
|
||||
title: _t('Error'),
|
||||
description: ((err && err.message) ? err.message : _t('The server may be unavailable or overloaded')),
|
||||
description: (err && err.message)
|
||||
? err.message
|
||||
: _t('The server may be unavailable or overloaded'),
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onRoomClicked = (room, ev) => {
|
||||
private onRoomClicked = (room: IRoom, ev: ButtonEvent) => {
|
||||
if (ev.shiftKey && !this.state.selectedCommunityId) {
|
||||
ev.preventDefault();
|
||||
this.removeFromDirectory(room);
|
||||
|
@ -305,7 +345,7 @@ export default class RoomDirectory extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
onOptionChange = (server, instanceId) => {
|
||||
private onOptionChange = (server: string, instanceId?: string | symbol) => {
|
||||
// clear next batch so we don't try to load more rooms
|
||||
this.nextBatch = null;
|
||||
this.setState({
|
||||
|
@ -325,13 +365,13 @@ export default class RoomDirectory extends React.Component {
|
|||
// Easiest to just blow away the state & re-fetch.
|
||||
};
|
||||
|
||||
onFillRequest = (backwards) => {
|
||||
private onFillRequest = (backwards: boolean) => {
|
||||
if (backwards || !this.nextBatch) return Promise.resolve(false);
|
||||
|
||||
return this.getMoreRooms();
|
||||
};
|
||||
|
||||
onFilterChange = (alias) => {
|
||||
private onFilterChange = (alias: string) => {
|
||||
this.setState({
|
||||
filterString: alias || null,
|
||||
});
|
||||
|
@ -349,7 +389,7 @@ export default class RoomDirectory extends React.Component {
|
|||
}, 700);
|
||||
};
|
||||
|
||||
onFilterClear = () => {
|
||||
private onFilterClear = () => {
|
||||
// update immediately
|
||||
this.setState({
|
||||
filterString: null,
|
||||
|
@ -360,7 +400,7 @@ export default class RoomDirectory extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
onJoinFromSearchClick = (alias) => {
|
||||
private onJoinFromSearchClick = (alias: string) => {
|
||||
// If we don't have a particular instance id selected, just show that rooms alias
|
||||
if (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) {
|
||||
// If the user specified an alias without a domain, add on whichever server is selected
|
||||
|
@ -373,9 +413,10 @@ export default class RoomDirectory extends React.Component {
|
|||
// This is a 3rd party protocol. Let's see if we can join it
|
||||
const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId);
|
||||
const instance = instanceForInstanceId(this.protocols, this.state.instanceId);
|
||||
const fields = protocolName ? this._getFieldsForThirdPartyLocation(alias, this.protocols[protocolName], instance) : null;
|
||||
const fields = protocolName
|
||||
? this.getFieldsForThirdPartyLocation(alias, this.protocols[protocolName], instance)
|
||||
: null;
|
||||
if (!fields) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const brand = SdkConfig.get().brand;
|
||||
Modal.createTrackedDialog('Unable to join network', '', ErrorDialog, {
|
||||
title: _t('Unable to join network'),
|
||||
|
@ -387,14 +428,12 @@ export default class RoomDirectory extends React.Component {
|
|||
if (resp.length > 0 && resp[0].alias) {
|
||||
this.showRoomAlias(resp[0].alias, true);
|
||||
} else {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Room not found', '', ErrorDialog, {
|
||||
title: _t('Room not found'),
|
||||
description: _t('Couldn\'t find a matching Matrix room'),
|
||||
});
|
||||
}
|
||||
}, (e) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Fetching third party location failed', '', ErrorDialog, {
|
||||
title: _t('Fetching third party location failed'),
|
||||
description: _t('Unable to look up room ID from server'),
|
||||
|
@ -403,36 +442,37 @@ export default class RoomDirectory extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
onPreviewClick = (ev, room) => {
|
||||
private onPreviewClick = (ev: ButtonEvent, room: IRoom) => {
|
||||
this.showRoom(room, null, false, true);
|
||||
ev.stopPropagation();
|
||||
};
|
||||
|
||||
onViewClick = (ev, room) => {
|
||||
private onViewClick = (ev: ButtonEvent, room: IRoom) => {
|
||||
this.showRoom(room);
|
||||
ev.stopPropagation();
|
||||
};
|
||||
|
||||
onJoinClick = (ev, room) => {
|
||||
private onJoinClick = (ev: ButtonEvent, room: IRoom) => {
|
||||
this.showRoom(room, null, true);
|
||||
ev.stopPropagation();
|
||||
};
|
||||
|
||||
onCreateRoomClick = room => {
|
||||
private onCreateRoomClick = () => {
|
||||
this.onFinished();
|
||||
dis.dispatch({
|
||||
action: 'view_create_room',
|
||||
public: true,
|
||||
defaultName: this.state.filterString.trim(),
|
||||
});
|
||||
};
|
||||
|
||||
showRoomAlias(alias, autoJoin=false) {
|
||||
private showRoomAlias(alias: string, autoJoin = false) {
|
||||
this.showRoom(null, alias, autoJoin);
|
||||
}
|
||||
|
||||
showRoom(room, room_alias, autoJoin = false, shouldPeek = false) {
|
||||
private showRoom(room: IRoom, roomAlias?: string, autoJoin = false, shouldPeek = false) {
|
||||
this.onFinished();
|
||||
const payload = {
|
||||
const payload: ActionPayload = {
|
||||
action: 'view_room',
|
||||
auto_join: autoJoin,
|
||||
should_peek: shouldPeek,
|
||||
|
@ -449,15 +489,15 @@ export default class RoomDirectory extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
if (!room_alias) {
|
||||
room_alias = get_display_alias_for_room(room);
|
||||
if (!roomAlias) {
|
||||
roomAlias = getDisplayAliasForRoom(room);
|
||||
}
|
||||
|
||||
payload.oob_data = {
|
||||
avatarUrl: room.avatar_url,
|
||||
// XXX: This logic is duplicated from the JS SDK which
|
||||
// would normally decide what the name is.
|
||||
name: room.name || room_alias || _t('Unnamed room'),
|
||||
name: room.name || roomAlias || _t('Unnamed room'),
|
||||
};
|
||||
|
||||
if (this.state.roomServer) {
|
||||
|
@ -471,21 +511,19 @@ export default class RoomDirectory extends React.Component {
|
|||
// which servers to start querying. However, there's no other way to join rooms in
|
||||
// this list without aliases at present, so if roomAlias isn't set here we have no
|
||||
// choice but to supply the ID.
|
||||
if (room_alias) {
|
||||
payload.room_alias = room_alias;
|
||||
if (roomAlias) {
|
||||
payload.room_alias = roomAlias;
|
||||
} else {
|
||||
payload.room_id = room.room_id;
|
||||
}
|
||||
dis.dispatch(payload);
|
||||
}
|
||||
|
||||
createRoomCells(room) {
|
||||
private createRoomCells(room: IRoom) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const clientRoom = client.getRoom(room.room_id);
|
||||
const hasJoinedRoom = clientRoom && clientRoom.getMyMembership() === "join";
|
||||
const isGuest = client.isGuest();
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
let previewButton;
|
||||
let joinOrViewButton;
|
||||
|
||||
|
@ -495,20 +533,26 @@ export default class RoomDirectory extends React.Component {
|
|||
// it is readable, the preview appears as normal.
|
||||
if (!hasJoinedRoom && (room.world_readable || isGuest)) {
|
||||
previewButton = (
|
||||
<AccessibleButton kind="secondary" onClick={(ev) => this.onPreviewClick(ev, room)}>{_t("Preview")}</AccessibleButton>
|
||||
<AccessibleButton kind="secondary" onClick={(ev) => this.onPreviewClick(ev, room)}>
|
||||
{ _t("Preview") }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
if (hasJoinedRoom) {
|
||||
joinOrViewButton = (
|
||||
<AccessibleButton kind="secondary" onClick={(ev) => this.onViewClick(ev, room)}>{_t("View")}</AccessibleButton>
|
||||
<AccessibleButton kind="secondary" onClick={(ev) => this.onViewClick(ev, room)}>
|
||||
{ _t("View") }
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else if (!isGuest) {
|
||||
joinOrViewButton = (
|
||||
<AccessibleButton kind="primary" onClick={(ev) => this.onJoinClick(ev, room)}>{_t("Join")}</AccessibleButton>
|
||||
<AccessibleButton kind="primary" onClick={(ev) => this.onJoinClick(ev, room)}>
|
||||
{ _t("Join") }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
let name = room.name || get_display_alias_for_room(room) || _t('Unnamed room');
|
||||
let name = room.name || getDisplayAliasForRoom(room) || _t('Unnamed room');
|
||||
if (name.length > MAX_NAME_LENGTH) {
|
||||
name = `${name.substring(0, MAX_NAME_LENGTH)}...`;
|
||||
}
|
||||
|
@ -531,9 +575,13 @@ export default class RoomDirectory extends React.Component {
|
|||
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||
className="mx_RoomDirectory_roomAvatar"
|
||||
>
|
||||
<BaseAvatar width={32} height={32} resizeMethod='crop'
|
||||
name={ name } idName={ name }
|
||||
url={ avatarUrl }
|
||||
<BaseAvatar
|
||||
width={32}
|
||||
height={32}
|
||||
resizeMethod='crop'
|
||||
name={name}
|
||||
idName={name}
|
||||
url={avatarUrl}
|
||||
/>
|
||||
</div>,
|
||||
<div key={ `${room.room_id}_description` }
|
||||
|
@ -547,7 +595,7 @@ export default class RoomDirectory extends React.Component {
|
|||
onClick={ (ev) => { ev.stopPropagation(); } }
|
||||
dangerouslySetInnerHTML={{ __html: topic }}
|
||||
/>
|
||||
<div className="mx_RoomDirectory_alias">{ get_display_alias_for_room(room) }</div>
|
||||
<div className="mx_RoomDirectory_alias">{ getDisplayAliasForRoom(room) }</div>
|
||||
</div>,
|
||||
<div key={ `${room.room_id}_memberCount` }
|
||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||
|
@ -576,20 +624,16 @@ export default class RoomDirectory extends React.Component {
|
|||
];
|
||||
}
|
||||
|
||||
collectScrollPanel = (element) => {
|
||||
this.scrollPanel = element;
|
||||
};
|
||||
|
||||
_stringLooksLikeId(s, field_type) {
|
||||
private stringLooksLikeId(s: string, fieldType: IFieldType) {
|
||||
let pat = /^#[^\s]+:[^\s]/;
|
||||
if (field_type && field_type.regexp) {
|
||||
pat = new RegExp(field_type.regexp);
|
||||
if (fieldType && fieldType.regexp) {
|
||||
pat = new RegExp(fieldType.regexp);
|
||||
}
|
||||
|
||||
return pat.test(s);
|
||||
}
|
||||
|
||||
_getFieldsForThirdPartyLocation(userInput, protocol, instance) {
|
||||
private getFieldsForThirdPartyLocation(userInput: string, protocol: IProtocol, instance: IInstance) {
|
||||
// make an object with the fields specified by that protocol. We
|
||||
// require that the values of all but the last field come from the
|
||||
// instance. The last is the user input.
|
||||
|
@ -605,71 +649,73 @@ export default class RoomDirectory extends React.Component {
|
|||
return fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* called by the parent component when PageUp/Down/etc is pressed.
|
||||
*
|
||||
* We pass it down to the scroll panel.
|
||||
*/
|
||||
handleScrollKey = ev => {
|
||||
if (this.scrollPanel) {
|
||||
this.scrollPanel.handleScrollKey(ev);
|
||||
}
|
||||
};
|
||||
|
||||
onFinished = () => {
|
||||
private onFinished = () => {
|
||||
CountlyAnalytics.instance.trackRoomDirectory(this.startTime);
|
||||
this.props.onFinished();
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
render() {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let content;
|
||||
if (this.state.error) {
|
||||
content = this.state.error;
|
||||
} else if (this.state.protocolsLoading) {
|
||||
content = <Loader />;
|
||||
content = <Spinner />;
|
||||
} else {
|
||||
const cells = (this.state.publicRooms || [])
|
||||
.reduce((cells, room) => cells.concat(this.createRoomCells(room)), [],);
|
||||
.reduce((cells, room) => cells.concat(this.createRoomCells(room)), []);
|
||||
// we still show the scrollpanel, at least for now, because
|
||||
// otherwise we don't fetch more because we don't get a fill
|
||||
// request from the scrollpanel because there isn't one
|
||||
|
||||
let spinner;
|
||||
if (this.state.loading) {
|
||||
spinner = <Loader />;
|
||||
spinner = <Spinner />;
|
||||
}
|
||||
|
||||
let scrollpanel_content;
|
||||
const createNewButton = <>
|
||||
<hr />
|
||||
<AccessibleButton kind="primary" onClick={this.onCreateRoomClick} className="mx_RoomDirectory_newRoom">
|
||||
{ _t("Create new room") }
|
||||
</AccessibleButton>
|
||||
</>;
|
||||
|
||||
let scrollPanelContent;
|
||||
let footer;
|
||||
if (cells.length === 0 && !this.state.loading) {
|
||||
scrollpanel_content = <i>{ _t('No rooms to show') }</i>;
|
||||
footer = <>
|
||||
<h5>{ _t('No results for "%(query)s"', { query: this.state.filterString.trim() }) }</h5>
|
||||
<p>
|
||||
{ _t("Try different words or check for typos. " +
|
||||
"Some results may not be visible as they're private and you need an invite to join them.") }
|
||||
</p>
|
||||
{ createNewButton }
|
||||
</>;
|
||||
} else {
|
||||
scrollpanel_content = <div className="mx_RoomDirectory_table">
|
||||
scrollPanelContent = <div className="mx_RoomDirectory_table">
|
||||
{ cells }
|
||||
</div>;
|
||||
if (!this.state.loading && !this.nextBatch) {
|
||||
footer = createNewButton;
|
||||
}
|
||||
}
|
||||
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||
content = <ScrollPanel ref={this.collectScrollPanel}
|
||||
content = <ScrollPanel
|
||||
className="mx_RoomDirectory_tableWrapper"
|
||||
onFillRequest={ this.onFillRequest }
|
||||
onFillRequest={this.onFillRequest}
|
||||
stickyBottom={false}
|
||||
startAtBottom={false}
|
||||
>
|
||||
{ scrollpanel_content }
|
||||
{ scrollPanelContent }
|
||||
{ spinner }
|
||||
{ footer && <div className="mx_RoomDirectory_footer">
|
||||
{ footer }
|
||||
</div> }
|
||||
</ScrollPanel>;
|
||||
}
|
||||
|
||||
let listHeader;
|
||||
if (!this.state.protocolsLoading) {
|
||||
const NetworkDropdown = sdk.getComponent('directory.NetworkDropdown');
|
||||
const DirectorySearchBox = sdk.getComponent('elements.DirectorySearchBox');
|
||||
|
||||
const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId);
|
||||
let instance_expected_field_type;
|
||||
let instanceExpectedFieldType;
|
||||
if (
|
||||
protocolName &&
|
||||
this.protocols &&
|
||||
|
@ -677,21 +723,27 @@ export default class RoomDirectory extends React.Component {
|
|||
this.protocols[protocolName].location_fields.length > 0 &&
|
||||
this.protocols[protocolName].field_types
|
||||
) {
|
||||
const last_field = this.protocols[protocolName].location_fields.slice(-1)[0];
|
||||
instance_expected_field_type = this.protocols[protocolName].field_types[last_field];
|
||||
const lastField = this.protocols[protocolName].location_fields.slice(-1)[0];
|
||||
instanceExpectedFieldType = this.protocols[protocolName].field_types[lastField];
|
||||
}
|
||||
|
||||
let placeholder = _t('Find a room…');
|
||||
if (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) {
|
||||
placeholder = _t("Find a room… (e.g. %(exampleRoom)s)", {exampleRoom: "#example:" + this.state.roomServer});
|
||||
} else if (instance_expected_field_type) {
|
||||
placeholder = instance_expected_field_type.placeholder;
|
||||
placeholder = _t("Find a room… (e.g. %(exampleRoom)s)", {
|
||||
exampleRoom: "#example:" + this.state.roomServer,
|
||||
});
|
||||
} else if (instanceExpectedFieldType) {
|
||||
placeholder = instanceExpectedFieldType.placeholder;
|
||||
}
|
||||
|
||||
let showJoinButton = this._stringLooksLikeId(this.state.filterString, instance_expected_field_type);
|
||||
let showJoinButton = this.stringLooksLikeId(this.state.filterString, instanceExpectedFieldType);
|
||||
if (protocolName) {
|
||||
const instance = instanceForInstanceId(this.protocols, this.state.instanceId);
|
||||
if (this._getFieldsForThirdPartyLocation(this.state.filterString, this.protocols[protocolName], instance) === null) {
|
||||
if (this.getFieldsForThirdPartyLocation(
|
||||
this.state.filterString,
|
||||
this.protocols[protocolName],
|
||||
instance,
|
||||
) === null) {
|
||||
showJoinButton = false;
|
||||
}
|
||||
}
|
||||
|
@ -723,12 +775,11 @@ export default class RoomDirectory extends React.Component {
|
|||
}
|
||||
const explanation =
|
||||
_t("If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.", null,
|
||||
{a: sub => {
|
||||
return (<AccessibleButton
|
||||
kind="secondary"
|
||||
onClick={this.onCreateRoomClick}
|
||||
>{sub}</AccessibleButton>);
|
||||
}},
|
||||
{a: sub => (
|
||||
<AccessibleButton kind="secondary" onClick={this.onCreateRoomClick}>
|
||||
{ sub }
|
||||
</AccessibleButton>
|
||||
)},
|
||||
);
|
||||
|
||||
const title = this.state.selectedCommunityId
|
||||
|
@ -756,6 +807,6 @@ export default class RoomDirectory extends React.Component {
|
|||
|
||||
// Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom
|
||||
// but works with the objects we get from the public room list
|
||||
function get_display_alias_for_room(room) {
|
||||
return room.canonical_alias || (room.aliases ? room.aliases[0] : "");
|
||||
function getDisplayAliasForRoom(room: IRoom) {
|
||||
return room.canonical_alias || room.aliases?.[0] || "";
|
||||
}
|
|
@ -46,7 +46,7 @@ import RoomViewStore from '../../stores/RoomViewStore';
|
|||
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
||||
import WidgetEchoStore from '../../stores/WidgetEchoStore';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import {Layout} from "../../settings/Layout";
|
||||
import { Layout } from "../../settings/Layout";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import { haveTileForEvent } from "../views/rooms/EventTile";
|
||||
|
@ -54,16 +54,13 @@ import RoomContext from "../../contexts/RoomContext";
|
|||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils';
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { SettingLevel } from "../../settings/SettingLevel";
|
||||
import { IMatrixClientCreds } from "../../MatrixClientPeg";
|
||||
import ScrollPanel from "./ScrollPanel";
|
||||
import TimelinePanel from "./TimelinePanel";
|
||||
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
||||
import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
|
||||
import ForwardMessage from "../views/rooms/ForwardMessage";
|
||||
import SearchBar from "../views/rooms/SearchBar";
|
||||
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
||||
import PinnedEventsPanel from "../views/rooms/PinnedEventsPanel";
|
||||
import AuxPanel from "../views/rooms/AuxPanel";
|
||||
import RoomHeader from "../views/rooms/RoomHeader";
|
||||
import { XOR } from "../../@types/common";
|
||||
|
@ -82,7 +79,9 @@ import { getKeyBindingsManager, RoomAction } from '../../KeyBindingsManager';
|
|||
import { objectHasDiff } from "../../utils/objects";
|
||||
import SpaceRoomView from "./SpaceRoomView";
|
||||
import { IOpts } from "../../createRoom";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import { omit } from 'lodash';
|
||||
import UIStore from "../../stores/UIStore";
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function(msg: string) {};
|
||||
|
@ -136,7 +135,6 @@ export interface IState {
|
|||
// Whether to highlight the event scrolled to
|
||||
isInitialEventHighlighted?: boolean;
|
||||
replyToEvent?: MatrixEvent;
|
||||
forwardingEvent?: MatrixEvent;
|
||||
numUnreadMessages: number;
|
||||
draggingFile: boolean;
|
||||
searching: boolean;
|
||||
|
@ -155,8 +153,6 @@ export interface IState {
|
|||
canPeek: boolean;
|
||||
showApps: boolean;
|
||||
isPeeking: boolean;
|
||||
showingPinned: boolean;
|
||||
showReadReceipts: boolean;
|
||||
showRightPanel: boolean;
|
||||
// error object, as from the matrix client/server API
|
||||
// If we failed to load information about the room,
|
||||
|
@ -175,6 +171,7 @@ export interface IState {
|
|||
statusBarVisible: boolean;
|
||||
// We load this later by asking the js-sdk to suggest a version for us.
|
||||
// This object is the result of Room#getRecommendedVersion()
|
||||
|
||||
upgradeRecommendation?: {
|
||||
version: string;
|
||||
needsUpgrade: boolean;
|
||||
|
@ -183,6 +180,12 @@ export interface IState {
|
|||
canReact: boolean;
|
||||
canReply: boolean;
|
||||
layout: Layout;
|
||||
lowBandwidth: boolean;
|
||||
showReadReceipts: boolean;
|
||||
showRedactions: boolean;
|
||||
showJoinLeaves: boolean;
|
||||
showAvatarChanges: boolean;
|
||||
showDisplaynameChanges: boolean;
|
||||
matrixClientIsReady: boolean;
|
||||
showUrlPreview?: boolean;
|
||||
e2eStatus?: E2EStatus;
|
||||
|
@ -200,8 +203,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
private readonly dispatcherRef: string;
|
||||
private readonly roomStoreToken: EventSubscription;
|
||||
private readonly rightPanelStoreToken: EventSubscription;
|
||||
private readonly showReadReceiptsWatchRef: string;
|
||||
private readonly layoutWatcherRef: string;
|
||||
private settingWatchers: string[];
|
||||
|
||||
private unmounted = false;
|
||||
private permalinkCreators: Record<string, RoomPermalinkCreator> = {};
|
||||
|
@ -232,8 +234,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
canPeek: false,
|
||||
showApps: false,
|
||||
isPeeking: false,
|
||||
showingPinned: false,
|
||||
showReadReceipts: true,
|
||||
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
|
||||
joining: false,
|
||||
atEndOfLiveTimeline: true,
|
||||
|
@ -243,6 +243,12 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
canReact: false,
|
||||
canReply: false,
|
||||
layout: SettingsStore.getValue("layout"),
|
||||
lowBandwidth: SettingsStore.getValue("lowBandwidth"),
|
||||
showReadReceipts: true,
|
||||
showRedactions: true,
|
||||
showJoinLeaves: true,
|
||||
showAvatarChanges: true,
|
||||
showDisplaynameChanges: true,
|
||||
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
|
||||
dragCounter: 0,
|
||||
};
|
||||
|
@ -269,9 +275,14 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
|
||||
WidgetStore.instance.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
|
||||
|
||||
this.showReadReceiptsWatchRef = SettingsStore.watchSetting("showReadReceipts", null,
|
||||
this.onReadReceiptsChange);
|
||||
this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, this.onLayoutChange);
|
||||
this.settingWatchers = [
|
||||
SettingsStore.watchSetting("layout", null, () =>
|
||||
this.setState({ layout: SettingsStore.getValue("layout") }),
|
||||
),
|
||||
SettingsStore.watchSetting("lowBandwidth", null, () =>
|
||||
this.setState({ lowBandwidth: SettingsStore.getValue("lowBandwidth") }),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private onWidgetStoreUpdate = () => {
|
||||
|
@ -324,14 +335,45 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
initialEventId: RoomViewStore.getInitialEventId(),
|
||||
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
|
||||
replyToEvent: RoomViewStore.getQuotingEvent(),
|
||||
forwardingEvent: RoomViewStore.getForwardingEvent(),
|
||||
// we should only peek once we have a ready client
|
||||
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
|
||||
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId),
|
||||
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
|
||||
showRedactions: SettingsStore.getValue("showRedactions", roomId),
|
||||
showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId),
|
||||
showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId),
|
||||
showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId),
|
||||
wasContextSwitch: RoomViewStore.getWasContextSwitch(),
|
||||
};
|
||||
|
||||
// Add watchers for each of the settings we just looked up
|
||||
this.settingWatchers = this.settingWatchers.concat([
|
||||
SettingsStore.watchSetting("showReadReceipts", null, () =>
|
||||
this.setState({
|
||||
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
|
||||
}),
|
||||
),
|
||||
SettingsStore.watchSetting("showRedactions", null, () =>
|
||||
this.setState({
|
||||
showRedactions: SettingsStore.getValue("showRedactions", roomId),
|
||||
}),
|
||||
),
|
||||
SettingsStore.watchSetting("showJoinLeaves", null, () =>
|
||||
this.setState({
|
||||
showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId),
|
||||
}),
|
||||
),
|
||||
SettingsStore.watchSetting("showAvatarChanges", null, () =>
|
||||
this.setState({
|
||||
showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId),
|
||||
}),
|
||||
),
|
||||
SettingsStore.watchSetting("showDisplaynameChanges", null, () =>
|
||||
this.setState({
|
||||
showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId),
|
||||
}),
|
||||
),
|
||||
]);
|
||||
|
||||
if (!initial && this.state.shouldPeek && !newState.shouldPeek) {
|
||||
// Stop peeking because we have joined this room now
|
||||
this.context.stopPeeking();
|
||||
|
@ -528,7 +570,20 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return (objectHasDiff(this.props, nextProps) || objectHasDiff(this.state, nextState));
|
||||
const hasPropsDiff = objectHasDiff(this.props, nextProps);
|
||||
|
||||
// React only shallow comparison and we only want to trigger
|
||||
// a component re-render if a room requires an upgrade
|
||||
const newUpgradeRecommendation = nextState.upgradeRecommendation || {}
|
||||
|
||||
const state = omit(this.state, ['upgradeRecommendation']);
|
||||
const newState = omit(nextState, ['upgradeRecommendation'])
|
||||
|
||||
const hasStateDiff =
|
||||
objectHasDiff(state, newState) ||
|
||||
(newUpgradeRecommendation.needsUpgrade === true)
|
||||
|
||||
return hasPropsDiff || hasStateDiff;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
|
@ -627,10 +682,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
if (this.showReadReceiptsWatchRef) {
|
||||
SettingsStore.unwatchSetting(this.showReadReceiptsWatchRef);
|
||||
}
|
||||
|
||||
// cancel any pending calls to the rate_limited_funcs
|
||||
this.updateRoomMembers.cancelPendingCall();
|
||||
|
||||
|
@ -638,7 +689,20 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
// console.log("Tinter.tint from RoomView.unmount");
|
||||
// Tinter.tint(); // reset colourscheme
|
||||
|
||||
SettingsStore.unwatchSetting(this.layoutWatcherRef);
|
||||
for (const watcher of this.settingWatchers) {
|
||||
SettingsStore.unwatchSetting(watcher);
|
||||
}
|
||||
}
|
||||
|
||||
private onUserScroll = () => {
|
||||
if (this.state.initialEventId && this.state.isInitialEventHighlighted) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.state.room.roomId,
|
||||
event_id: this.state.initialEventId,
|
||||
highlighted: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private onLayoutChange = () => {
|
||||
|
@ -797,7 +861,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
// update unread count when scrolled up
|
||||
if (!this.state.searchResults && this.state.atEndOfLiveTimeline) {
|
||||
// no change
|
||||
} else if (!shouldHideEvent(ev)) {
|
||||
} else if (!shouldHideEvent(ev, this.state)) {
|
||||
this.setState((state, props) => {
|
||||
return {numUnreadMessages: state.numUnreadMessages + 1};
|
||||
});
|
||||
|
@ -1114,7 +1178,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
|
||||
});
|
||||
|
@ -1375,13 +1440,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
return ret;
|
||||
}
|
||||
|
||||
private onPinnedClick = () => {
|
||||
const nowShowingPinned = !this.state.showingPinned;
|
||||
const roomId = this.state.room.roomId;
|
||||
this.setState({showingPinned: nowShowingPinned, searching: false});
|
||||
SettingsStore.setValue("PinnedEvents.isOpen", roomId, SettingLevel.ROOM_DEVICE, nowShowingPinned);
|
||||
};
|
||||
|
||||
private onCallPlaced = (type: PlaceCallType) => {
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
|
@ -1394,18 +1452,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
dis.dispatch({ action: "open_room_settings" });
|
||||
};
|
||||
|
||||
private onCancelClick = () => {
|
||||
console.log("updateTint from onCancelClick");
|
||||
this.updateTint();
|
||||
if (this.state.forwardingEvent) {
|
||||
dis.dispatch({
|
||||
action: 'forward_event',
|
||||
event: null,
|
||||
});
|
||||
}
|
||||
dis.fire(Action.FocusComposer);
|
||||
};
|
||||
|
||||
private onAppsClick = () => {
|
||||
dis.dispatch({
|
||||
action: "appsDrawer",
|
||||
|
@ -1498,7 +1544,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
private onSearchClick = () => {
|
||||
this.setState({
|
||||
searching: !this.state.searching,
|
||||
showingPinned: false,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -1511,8 +1556,19 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
|
||||
// jump down to the bottom of this room, where new events are arriving
|
||||
private jumpToLiveTimeline = () => {
|
||||
this.messagePanel.jumpToLiveTimeline();
|
||||
dis.fire(Action.FocusComposer);
|
||||
if (this.state.initialEventId && this.state.isInitialEventHighlighted) {
|
||||
// If we were viewing a highlighted event, firing view_room without
|
||||
// an event will take care of both clearing the URL fragment and
|
||||
// jumping to the bottom
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.state.room.roomId,
|
||||
});
|
||||
} else {
|
||||
// Otherwise we have to jump manually
|
||||
this.messagePanel.jumpToLiveTimeline();
|
||||
dis.fire(Action.FocusComposer);
|
||||
}
|
||||
};
|
||||
|
||||
// jump up to wherever our read marker is
|
||||
|
@ -1585,7 +1641,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
|
||||
|
@ -1598,33 +1654,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
this.setState({auxPanelMaxHeight: auxPanelMaxHeight});
|
||||
};
|
||||
|
||||
private onFullscreenClick = () => {
|
||||
dis.dispatch({
|
||||
action: 'video_fullscreen',
|
||||
fullscreen: true,
|
||||
}, true);
|
||||
};
|
||||
|
||||
private onMuteAudioClick = () => {
|
||||
const call = this.getCallForRoom();
|
||||
if (!call) {
|
||||
return;
|
||||
}
|
||||
const newState = !call.isMicrophoneMuted();
|
||||
call.setMicrophoneMuted(newState);
|
||||
this.forceUpdate(); // TODO: just update the voip buttons
|
||||
};
|
||||
|
||||
private onMuteVideoClick = () => {
|
||||
const call = this.getCallForRoom();
|
||||
if (!call) {
|
||||
return;
|
||||
}
|
||||
const newState = !call.isLocalVideoMuted();
|
||||
call.setLocalVideoMuted(newState);
|
||||
this.forceUpdate(); // TODO: just update the voip buttons
|
||||
};
|
||||
|
||||
private onStatusBarVisible = () => {
|
||||
if (this.unmounted) return;
|
||||
this.setState({
|
||||
|
@ -1856,11 +1885,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
|
||||
let aux = null;
|
||||
let previewBar;
|
||||
let hideCancel = false;
|
||||
if (this.state.forwardingEvent) {
|
||||
aux = <ForwardMessage onCancelClick={this.onCancelClick} />;
|
||||
} else if (this.state.searching) {
|
||||
hideCancel = true; // has own cancel
|
||||
if (this.state.searching) {
|
||||
aux = <SearchBar
|
||||
searchInProgress={this.state.searchInProgress}
|
||||
onCancelClick={this.onCancelSearchClick}
|
||||
|
@ -1869,10 +1894,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
/>;
|
||||
} else if (showRoomUpgradeBar) {
|
||||
aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />;
|
||||
hideCancel = true;
|
||||
} else if (this.state.showingPinned) {
|
||||
hideCancel = true; // has own cancel
|
||||
aux = <PinnedEventsPanel room={this.state.room} onCancelClick={this.onPinnedClick} />;
|
||||
} else if (myMembership !== "join") {
|
||||
// We do have a room object for this room, but we're not currently in it.
|
||||
// We may have a 3rd party invite to it.
|
||||
|
@ -1881,7 +1902,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
inviterName = this.props.oobData.inviterName;
|
||||
}
|
||||
const invitedEmail = this.props.threepidInvite?.toEmail;
|
||||
hideCancel = true;
|
||||
previewBar = (
|
||||
<RoomPreviewBar
|
||||
onJoinClick={this.onJoinButtonClicked}
|
||||
|
@ -1999,11 +2019,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
hideMessagePanel = true;
|
||||
}
|
||||
|
||||
const shouldHighlight = this.state.isInitialEventHighlighted;
|
||||
let highlightedEventId = null;
|
||||
if (this.state.forwardingEvent) {
|
||||
highlightedEventId = this.state.forwardingEvent.getId();
|
||||
} else if (shouldHighlight) {
|
||||
if (this.state.isInitialEventHighlighted) {
|
||||
highlightedEventId = this.state.initialEventId;
|
||||
}
|
||||
|
||||
|
@ -2028,6 +2045,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
eventId={this.state.initialEventId}
|
||||
eventPixelOffset={this.state.initialEventPixelOffset}
|
||||
onScroll={this.onMessageListScroll}
|
||||
onUserScroll={this.onUserScroll}
|
||||
onReadMarkerUpdated={this.updateTopUnreadMessagesBar}
|
||||
showUrlPreview = {this.state.showUrlPreview}
|
||||
className={messagePanelClassNames}
|
||||
|
@ -2054,6 +2072,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
highlight={this.state.room.getUnreadNotificationCount('highlight') > 0}
|
||||
numUnreadMessages={this.state.numUnreadMessages}
|
||||
onScrollToBottomClick={this.jumpToLiveTimeline}
|
||||
roomId={this.state.roomId}
|
||||
/>);
|
||||
}
|
||||
|
||||
|
@ -2090,8 +2109,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
inRoom={myMembership === 'join'}
|
||||
onSearchClick={this.onSearchClick}
|
||||
onSettingsClick={this.onSettingsClick}
|
||||
onPinnedClick={this.onPinnedClick}
|
||||
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
|
||||
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
||||
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
|
|
|
@ -133,6 +133,10 @@ export default class ScrollPanel extends React.Component {
|
|||
*/
|
||||
onScroll: PropTypes.func,
|
||||
|
||||
/* onUserScroll: callback which is called when the user interacts with the room timeline
|
||||
*/
|
||||
onUserScroll: PropTypes.func,
|
||||
|
||||
/* className: classnames to add to the top-level div
|
||||
*/
|
||||
className: PropTypes.string,
|
||||
|
@ -535,21 +539,29 @@ export default class ScrollPanel extends React.Component {
|
|||
* @param {object} ev the keyboard event
|
||||
*/
|
||||
handleScrollKey = ev => {
|
||||
let isScrolling = false;
|
||||
const roomAction = getKeyBindingsManager().getRoomAction(ev);
|
||||
switch (roomAction) {
|
||||
case RoomAction.ScrollUp:
|
||||
this.scrollRelative(-1);
|
||||
isScrolling = true;
|
||||
break;
|
||||
case RoomAction.RoomScrollDown:
|
||||
this.scrollRelative(1);
|
||||
isScrolling = true;
|
||||
break;
|
||||
case RoomAction.JumpToFirstMessage:
|
||||
this.scrollToTop();
|
||||
isScrolling = true;
|
||||
break;
|
||||
case RoomAction.JumpToLatestMessage:
|
||||
this.scrollToBottom();
|
||||
isScrolling = true;
|
||||
break;
|
||||
}
|
||||
if (isScrolling && this.props.onUserScroll) {
|
||||
this.props.onUserScroll(ev);
|
||||
}
|
||||
};
|
||||
|
||||
/* Scroll the panel to bring the DOM node with the scroll token
|
||||
|
@ -888,9 +900,8 @@ export default class ScrollPanel extends React.Component {
|
|||
<AutoHideScrollbar
|
||||
wrappedRef={this._collectScroll}
|
||||
onScroll={this.onScroll}
|
||||
className={`mx_ScrollPanel ${this.props.className}`}
|
||||
style={this.props.style}
|
||||
>
|
||||
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">
|
||||
|
|
|
@ -76,7 +76,7 @@ export interface ISpaceSummaryEvent {
|
|||
order?: string;
|
||||
suggested?: boolean;
|
||||
auto_join?: boolean;
|
||||
via?: string;
|
||||
via?: string[];
|
||||
};
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
@ -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) {
|
||||
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) => {
|
||||
|
@ -356,9 +364,9 @@ export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: a
|
|||
parentChildRelations.getOrCreate(ev.room_id, new Map()).set(ev.state_key, ev);
|
||||
childParentRelations.getOrCreate(ev.state_key, new Set()).add(ev.room_id);
|
||||
}
|
||||
if (Array.isArray(ev.content["via"])) {
|
||||
if (Array.isArray(ev.content.via)) {
|
||||
const set = viaMap.getOrCreate(ev.state_key, new Set());
|
||||
ev.content["via"].forEach(via => set.add(via));
|
||||
ev.content.via.forEach(via => set.add(via));
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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;
|
||||
|
@ -471,8 +479,12 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
|||
try {
|
||||
for (const [parentId, childId] of selectedRelations) {
|
||||
await cli.sendStateEvent(parentId, EventType.SpaceChild, {}, childId);
|
||||
parentChildMap.get(parentId).get(childId).content = {};
|
||||
parentChildMap.set(parentId, new Map(parentChildMap.get(parentId)));
|
||||
parentChildMap.get(parentId).delete(childId);
|
||||
if (parentChildMap.get(parentId).size > 0) {
|
||||
parentChildMap.set(parentId, new Map(parentChildMap.get(parentId)));
|
||||
} else {
|
||||
parentChildMap.delete(parentId);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
setError(_t("Failed to remove some rooms. Try again later"));
|
||||
|
@ -508,6 +520,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
|||
setError("Failed to update some suggestions. Try again later");
|
||||
}
|
||||
setSaving(false);
|
||||
setSelected(new Map());
|
||||
}}
|
||||
kind="primary_outline"
|
||||
disabled={disabled}
|
||||
|
|
|
@ -28,7 +28,7 @@ import RoomTopic from "../views/elements/RoomTopic";
|
|||
import InlineSpinner from "../views/elements/InlineSpinner";
|
||||
import {inviteMultipleToRoom, showRoomInviteDialog} from "../../RoomInvite";
|
||||
import {useRoomMembers} from "../../hooks/useRoomMembers";
|
||||
import createRoom, {IOpts, Preset} from "../../createRoom";
|
||||
import createRoom, {IOpts} from "../../createRoom";
|
||||
import Field from "../views/elements/Field";
|
||||
import {useEventEmitter} from "../../hooks/useEventEmitter";
|
||||
import withValidation from "../views/elements/Validation";
|
||||
|
@ -65,6 +65,7 @@ import dis from "../../dispatcher/dispatcher";
|
|||
import Modal from "../../Modal";
|
||||
import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
import { Preset } from "matrix-js-sdk/src/@types/partials";
|
||||
|
||||
interface IProps {
|
||||
space: Room;
|
||||
|
@ -417,9 +418,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 +442,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;
|
||||
|
@ -584,6 +588,10 @@ const SpaceSetupPrivateScope = ({ space, justCreatedOpts, onFinished }) => {
|
|||
<h3>{ _t("Me and my teammates") }</h3>
|
||||
<div>{ _t("A private space for you and your teammates") }</div>
|
||||
</AccessibleButton>
|
||||
<div className="mx_SpaceRoomView_betaWarning">
|
||||
<h3>{ _t("Teammates might not be able to view or join any private rooms you make.") }</h3>
|
||||
<p>{ _t("We're working on this as part of the beta, but just want to let you know.") }</p>
|
||||
</div>
|
||||
<SpaceFeedbackPrompt />
|
||||
</div>;
|
||||
};
|
||||
|
@ -806,7 +814,7 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
|
|||
let suggestedRooms = SpaceStore.instance.suggestedRooms;
|
||||
if (SpaceStore.instance.activeSpace !== this.props.space) {
|
||||
// the space store has the suggested rooms loaded for a different space, fetch the right ones
|
||||
suggestedRooms = (await SpaceStore.instance.fetchSuggestedRooms(this.props.space, 1)).rooms;
|
||||
suggestedRooms = (await SpaceStore.instance.fetchSuggestedRooms(this.props.space, 1));
|
||||
}
|
||||
|
||||
if (suggestedRooms.length) {
|
||||
|
@ -814,9 +822,11 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
|
|||
defaultDispatcher.dispatch({
|
||||
action: "view_room",
|
||||
room_id: room.room_id,
|
||||
room_alias: room.canonical_alias || room.aliases?.[0],
|
||||
via_servers: room.viaServers,
|
||||
oobData: {
|
||||
avatarUrl: room.avatar_url,
|
||||
name: room.name || room.canonical_alias || room.aliases.pop() || _t("Empty room"),
|
||||
name: room.name || room.canonical_alias || room.aliases?.[0] || _t("Empty room"),
|
||||
},
|
||||
});
|
||||
return;
|
||||
|
|
|
@ -26,6 +26,7 @@ import {EventTimeline} from "matrix-js-sdk/src/models/event-timeline";
|
|||
import {TimelineWindow} from "matrix-js-sdk/src/timeline-window";
|
||||
import { _t } from '../../languageHandler';
|
||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
||||
import RoomContext from "../../contexts/RoomContext";
|
||||
import UserActivity from "../../UserActivity";
|
||||
import Modal from "../../Modal";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
|
@ -36,7 +37,6 @@ import shouldHideEvent from '../../shouldHideEvent';
|
|||
import EditorStateTransfer from '../../utils/EditorStateTransfer';
|
||||
import {haveTileForEvent} from "../views/rooms/EventTile";
|
||||
import {UIFeature} from "../../settings/UIFeature";
|
||||
import {objectHasDiff} from "../../utils/objects";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
import { arrayFastClone } from "../../utils/arrays";
|
||||
|
||||
|
@ -94,6 +94,9 @@ class TimelinePanel extends React.Component {
|
|||
// callback which is called when the panel is scrolled.
|
||||
onScroll: PropTypes.func,
|
||||
|
||||
// callback which is called when the user interacts with the room timeline
|
||||
onUserScroll: PropTypes.func,
|
||||
|
||||
// callback which is called when the read-up-to mark is updated.
|
||||
onReadMarkerUpdated: PropTypes.func,
|
||||
|
||||
|
@ -118,8 +121,13 @@ class TimelinePanel extends React.Component {
|
|||
|
||||
// which layout to use
|
||||
layout: LayoutPropType,
|
||||
|
||||
// whether to always show timestamps for an event
|
||||
alwaysShowTimestamps: PropTypes.bool,
|
||||
}
|
||||
|
||||
static contextType = RoomContext;
|
||||
|
||||
// a map from room id to read marker event timestamp
|
||||
static roomReadMarkerTsMap = {};
|
||||
|
||||
|
@ -258,37 +266,15 @@ class TimelinePanel extends React.Component {
|
|||
console.warn("Replacing timelineSet on a TimelinePanel - confusion may ensue");
|
||||
}
|
||||
|
||||
if (newProps.eventId != this.props.eventId) {
|
||||
const differentEventId = newProps.eventId != this.props.eventId;
|
||||
const differentHighlightedEventId = newProps.highlightedEventId != this.props.highlightedEventId;
|
||||
if (differentEventId || differentHighlightedEventId) {
|
||||
console.log("TimelinePanel switching to eventId " + newProps.eventId +
|
||||
" (was " + this.props.eventId + ")");
|
||||
return this._initTimeline(newProps);
|
||||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if (objectHasDiff(this.props, nextProps)) {
|
||||
if (DEBUG) {
|
||||
console.group("Timeline.shouldComponentUpdate: props change");
|
||||
console.log("props before:", this.props);
|
||||
console.log("props after:", nextProps);
|
||||
console.groupEnd();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (objectHasDiff(this.state, nextState)) {
|
||||
if (DEBUG) {
|
||||
console.group("Timeline.shouldComponentUpdate: state change");
|
||||
console.log("state before:", this.state);
|
||||
console.log("state after:", nextState);
|
||||
console.groupEnd();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// set a boolean to say we've been unmounted, which any pending
|
||||
// promises can use to throw away their results.
|
||||
|
@ -1149,9 +1135,8 @@ class TimelinePanel extends React.Component {
|
|||
arrayFastClone(events)
|
||||
.reverse()
|
||||
.forEach(event => {
|
||||
if (event.shouldAttemptDecryption()) {
|
||||
event.attemptDecryption(MatrixClientPeg.get()._crypto);
|
||||
}
|
||||
const client = MatrixClientPeg.get();
|
||||
client.decryptEventIfNeeded(event);
|
||||
});
|
||||
|
||||
const firstVisibleEventIndex = this._checkForPreJoinUISI(events);
|
||||
|
@ -1306,7 +1291,7 @@ class TimelinePanel extends React.Component {
|
|||
|
||||
const shouldIgnore = !!ev.status || // local echo
|
||||
(ignoreOwn && ev.sender && ev.sender.userId == myUserId); // own message
|
||||
const isWithoutTile = !haveTileForEvent(ev) || shouldHideEvent(ev);
|
||||
const isWithoutTile = !haveTileForEvent(ev) || shouldHideEvent(ev, this.context);
|
||||
|
||||
if (isWithoutTile || !node) {
|
||||
// don't start counting if the event should be ignored,
|
||||
|
@ -1457,10 +1442,11 @@ class TimelinePanel extends React.Component {
|
|||
ourUserId={MatrixClientPeg.get().credentials.userId}
|
||||
stickyBottom={stickyBottom}
|
||||
onScroll={this.onMessageListScroll}
|
||||
onUserScroll={this.props.onUserScroll}
|
||||
onFillRequest={this.onMessageListFillRequest}
|
||||
onUnfillRequest={this.onMessageListUnfillRequest}
|
||||
isTwelveHour={this.state.isTwelveHour}
|
||||
alwaysShowTimestamps={this.state.alwaysShowTimestamps}
|
||||
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.alwaysShowTimestamps}
|
||||
className={this.props.className}
|
||||
tileShape={this.props.tileShape}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
|
|
|
@ -55,6 +55,7 @@ export default class ToastContainer extends React.Component<{}, IState> {
|
|||
const totalCount = this.state.toasts.length;
|
||||
const isStacked = totalCount > 1;
|
||||
let toast;
|
||||
let containerClasses;
|
||||
if (totalCount !== 0) {
|
||||
const topToast = this.state.toasts[0];
|
||||
const {title, icon, key, component, className, props} = topToast;
|
||||
|
@ -79,16 +80,17 @@ export default class ToastContainer extends React.Component<{}, IState> {
|
|||
</div>
|
||||
<div className="mx_Toast_body">{React.createElement(component, toastProps)}</div>
|
||||
</div>);
|
||||
|
||||
containerClasses = classNames("mx_ToastContainer", {
|
||||
"mx_ToastContainer_stacked": isStacked,
|
||||
});
|
||||
}
|
||||
|
||||
const containerClasses = classNames("mx_ToastContainer", {
|
||||
"mx_ToastContainer_stacked": isStacked,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={containerClasses} role="alert">
|
||||
{toast}
|
||||
</div>
|
||||
);
|
||||
return toast
|
||||
? (
|
||||
<div className={containerClasses} role="alert">
|
||||
{toast}
|
||||
</div>
|
||||
)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
@ -333,9 +366,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
const mxDomain = MatrixClientPeg.get().getDomain();
|
||||
const validDomains = hostSignupDomains.filter(d => (d === mxDomain || mxDomain.endsWith(`.${d}`)));
|
||||
if (!hostSignupConfig.domains || validDomains.length > 0) {
|
||||
topSection = <div onClick={this.onCloseMenu}>
|
||||
<HostSignupAction />
|
||||
</div>;
|
||||
topSection = <HostSignupAction onClick={this.onCloseMenu} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -617,6 +648,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>
|
||||
|
|
|
@ -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: "",
|
||||
|
||||
|
|
|
@ -61,7 +61,7 @@ interface IProps {
|
|||
is_url?: string;
|
||||
session_id: string;
|
||||
/* eslint-enable camelcase */
|
||||
}): void;
|
||||
}): string;
|
||||
// registration shouldn't know or care how login is done.
|
||||
onLoginClick(): void;
|
||||
onServerConfigChange(config: ValidatedServerConfig): void;
|
||||
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue