Merge branch 'develop' into Discovery

This commit is contained in:
Travis Ralston 2021-03-22 23:04:41 -06:00
commit edcd7c4426
86 changed files with 1340 additions and 941 deletions

View file

@ -18,7 +18,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {Filter} from 'matrix-js-sdk';
import {Filter} from 'matrix-js-sdk/src/filter';
import * as sdk from '../../index';
import {MatrixClientPeg} from '../../MatrixClientPeg';
import EventIndexPeg from "../../indexing/EventIndexPeg";

View file

@ -35,7 +35,7 @@ import GroupStore from '../../stores/GroupStore';
import FlairStore from '../../stores/FlairStore';
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks";
import {Group} from "matrix-js-sdk";
import {Group} from "matrix-js-sdk/src/models/group";
import {allSettled, sleep} from "../../utils/promise";
import RightPanelStore from "../../stores/RightPanelStore";
import AutoHideScrollbar from "./AutoHideScrollbar";

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {InteractiveAuth} from "matrix-js-sdk";
import {InteractiveAuth} from "matrix-js-sdk/src/interactive-auth";
import React, {createRef} from 'react';
import PropTypes from 'prop-types';

View file

@ -38,7 +38,6 @@ import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import { OwnProfileStore } from "../../stores/OwnProfileStore";
import RoomListNumResults from "../views/rooms/RoomListNumResults";
import LeftPanelWidget from "./LeftPanelWidget";
import SpacePanel from "../views/spaces/SpacePanel";
import {replaceableComponent} from "../../utils/replaceableComponent";
import {mediaFromMxc} from "../../customisations/Media";
@ -392,11 +391,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
public render(): React.ReactNode {
let leftLeftPanel;
// Currently TagPanel.enableTagPanel is disabled when Legacy Communities are disabled so for now
// ignore it and force the rendering of SpacePanel if that Labs flag is enabled.
if (SettingsStore.getValue("feature_spaces")) {
leftLeftPanel = <SpacePanel />;
} else if (this.state.showGroupFilterPanel) {
if (this.state.showGroupFilterPanel) {
leftLeftPanel = (
<div className="mx_LeftPanel_GroupFilterPanelContainer">
<GroupFilterPanel />

View file

@ -56,6 +56,7 @@ import Modal from "../../Modal";
import { ICollapseConfig } from "../../resizer/distributors/collapse";
import HostSignupContainer from '../views/host_signup/HostSignupContainer';
import { IOpts } from "../../createRoom";
import SpacePanel from "../views/spaces/SpacePanel";
import {replaceableComponent} from "../../utils/replaceableComponent";
// We need to fetch each pinned message individually (if we don't already have it)
@ -229,21 +230,15 @@ class LoggedInView extends React.Component<IProps, IState> {
let size;
let collapsed;
const collapseConfig: ICollapseConfig = {
// TODO: the space panel currently does not have a fixed width,
// just the headers at each level have a max-width of 150px
// Taking 222px for the space panel for now,
// so this will look slightly off for now,
// depending on the depth of your space tree.
// To fix this, we'll need to turn toggleSize
// into a callback so it can be measured when starting the resize operation
toggleSize: 222 + 68,
// TODO decrease this once Spaces launches as it'll no longer need to include the 56px Community Panel
toggleSize: 206 - 50,
onCollapsed: (_collapsed) => {
collapsed = _collapsed;
if (_collapsed) {
dis.dispatch({action: "hide_left_panel"}, true);
dis.dispatch({action: "hide_left_panel"});
window.localStorage.setItem("mx_lhs_size", '0');
} else {
dis.dispatch({action: "show_left_panel"}, true);
dis.dispatch({action: "show_left_panel"});
}
},
onResized: (_size) => {
@ -670,13 +665,6 @@ class LoggedInView extends React.Component<IProps, IState> {
bodyClasses += ' mx_MatrixChat_useCompactLayout';
}
const leftPanel = (
<LeftPanel
isMinimized={this.props.collapseLhs || false}
resizeNotifier={this.props.resizeNotifier}
/>
);
return (
<MatrixClientContext.Provider value={this._matrixClient}>
<div
@ -688,7 +676,11 @@ class LoggedInView extends React.Component<IProps, IState> {
<ToastContainer />
<DragDropContext onDragEnd={this._onDragEnd}>
<div ref={this._resizeContainer} className={bodyClasses}>
{ leftPanel }
{ SettingsStore.getValue("feature_spaces") ? <SpacePanel /> : null }
<LeftPanel
isMinimized={this.props.collapseLhs || false}
resizeNotifier={this.props.resizeNotifier}
/>
<ResizeHandle />
{ pageElement }
</div>

View file

@ -1,8 +1,5 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017-2019 New Vector Ltd
Copyright 2019, 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.
@ -18,8 +15,7 @@ limitations under the License.
*/
import React, { createRef } from 'react';
// @ts-ignore - XXX: no idea why this import fails
import * as Matrix from "matrix-js-sdk";
import { createClient } from "matrix-js-sdk/src/matrix";
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
@ -82,9 +78,12 @@ import {UIFeature} from "../../settings/UIFeature";
import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
import DialPadModal from "../views/voip/DialPadModal";
import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast';
import { shouldUseLoginForWelcome } from "../../utils/pages";
import SpaceStore from "../../stores/SpaceStore";
import SpaceRoomDirectory from "./SpaceRoomDirectory";
import {replaceableComponent} from "../../utils/replaceableComponent";
import RoomListStore from "../../stores/room-list/RoomListStore";
import {RoomUpdateCause} from "../../stores/room-list/models";
/** constants for MatrixChat.state.view */
export enum Views {
@ -582,6 +581,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
break;
case 'logout':
dis.dispatch({action: "hangup_all"});
Lifecycle.logout();
break;
case 'require_registration':
@ -606,12 +606,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
if (payload.screenAfterLogin) {
this.screenAfterLogin = payload.screenAfterLogin;
}
this.setStateForNewView({
view: Views.LOGIN,
});
this.notifyNewScreen('login');
ThemeController.isLogin = true;
this.themeWatcher.recheck();
this.viewLogin();
break;
case 'start_password_recovery':
this.setStateForNewView({
@ -975,6 +970,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
private viewWelcome() {
if (shouldUseLoginForWelcome(SdkConfig.get())) {
return this.viewLogin();
}
this.setStateForNewView({
view: Views.WELCOME,
});
@ -983,6 +981,16 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.themeWatcher.recheck();
}
private viewLogin(otherState?: any) {
this.setStateForNewView({
view: Views.LOGIN,
...otherState,
});
this.notifyNewScreen('login');
ThemeController.isLogin = true;
this.themeWatcher.recheck();
}
private viewHome(justRegistered = false) {
// The home page requires the "logged in" view, so we'll set that.
this.setStateForNewView({
@ -1139,11 +1147,17 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
private forgetRoom(roomId: string) {
const room = MatrixClientPeg.get().getRoom(roomId);
MatrixClientPeg.get().forget(roomId).then(() => {
// Switch to home page if we're currently viewing the forgotten room
if (this.state.currentRoomId === roomId) {
dis.dispatch({ action: "view_home_page" });
}
// We have to manually update the room list because the forgotten room will not
// be notified to us, therefore the room list will have no other way of knowing
// the room is forgotten.
RoomListStore.instance.manualRoomUpdate(room, RoomUpdateCause.RoomRemoved);
}).catch((err) => {
const errCode = err.errcode || _td("unknown error code");
Modal.createTrackedDialog("Failed to forget room", '', ErrorDialog, {
@ -1298,17 +1312,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
* Called when the session is logged out
*/
private onLoggedOut() {
this.notifyNewScreen('login');
this.setStateForNewView({
view: Views.LOGIN,
this.viewLogin({
ready: false,
collapseLhs: false,
currentRoomId: null,
});
this.subTitleStatus = '';
this.setPageSubtitle();
ThemeController.isLogin = true;
this.themeWatcher.recheck();
}
/**
@ -1648,7 +1658,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
let cli = MatrixClientPeg.get();
if (!cli) {
const {hsUrl, isUrl} = this.props.serverConfig;
cli = Matrix.createClient({
cli = createClient({
baseUrl: hsUrl,
idBaseUrl: isUrl,
});

View file

@ -23,7 +23,6 @@ import classNames from 'classnames';
import shouldHideEvent from '../../shouldHideEvent';
import {wantsDateSeparator} from '../../DateUtils';
import * as sdk from '../../index';
import dis from "../../dispatcher/dispatcher";
import {MatrixClientPeg} from '../../MatrixClientPeg';
import SettingsStore from '../../settings/SettingsStore';
@ -210,13 +209,11 @@ export default class MessagePanel extends React.Component {
componentDidMount() {
this._isMounted = true;
this.dispatcherRef = dis.register(this.onAction);
}
componentWillUnmount() {
this._isMounted = false;
SettingsStore.unwatchSetting(this._showTypingNotificationsWatcherRef);
dis.unregister(this.dispatcherRef);
}
componentDidUpdate(prevProps, prevState) {
@ -229,14 +226,6 @@ export default class MessagePanel extends React.Component {
}
}
onAction = (payload) => {
switch (payload.action) {
case "scroll_to_bottom":
this.scrollToBottom();
break;
}
}
onShowTypingNotificationsChange = () => {
this.setState({
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
@ -1038,6 +1027,100 @@ class CreationGrouper {
}
}
class RedactionGrouper {
static canStartGroup = function(panel, ev) {
return panel._shouldShowEvent(ev) && ev.isRedacted();
}
constructor(panel, ev, prevEvent, lastShownEvent) {
this.panel = panel;
this.readMarker = panel._readMarkerForEvent(
ev.getId(),
ev === lastShownEvent,
);
this.events = [ev];
this.prevEvent = prevEvent;
this.lastShownEvent = lastShownEvent;
}
shouldGroup(ev) {
// absorb hidden events so that they do not break up streams of messages & redaction events being grouped
if (!this.panel._shouldShowEvent(ev)) {
return true;
}
if (this.panel._wantsDateSeparator(this.events[0], ev.getDate())) {
return false;
}
return ev.isRedacted();
}
add(ev) {
this.readMarker = this.readMarker || this.panel._readMarkerForEvent(
ev.getId(),
ev === this.lastShownEvent,
);
if (!this.panel._shouldShowEvent(ev)) {
return;
}
this.events.push(ev);
}
getTiles() {
if (!this.events || !this.events.length) return [];
const DateSeparator = sdk.getComponent('messages.DateSeparator');
const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
const panel = this.panel;
const ret = [];
const lastShownEvent = this.lastShownEvent;
if (panel._wantsDateSeparator(this.prevEvent, this.events[0].getDate())) {
const ts = this.events[0].getTs();
ret.push(
<li key={ts+'~'}><DateSeparator key={ts+'~'} ts={ts} /></li>,
);
}
const key = "redactioneventlistsummary-" + (
this.prevEvent ? this.events[0].getId() : "initial"
);
const senders = new Set();
let eventTiles = this.events.map((e, i) => {
senders.add(e.sender);
return panel._getTilesForEvent(i === 0 ? this.prevEvent : this.events[i - 1], e, e === lastShownEvent);
}).reduce((a, b) => a.concat(b), []);
if (eventTiles.length === 0) {
eventTiles = null;
}
ret.push(
<EventListSummary
key={key}
threshold={2}
events={this.events}
onToggle={panel._onHeightChanged} // Update scroll state
summaryMembers={Array.from(senders)}
summaryText={_t("%(count)s messages deleted.", { count: eventTiles.length })}
>
{ eventTiles }
</EventListSummary>,
);
if (this.readMarker) {
ret.push(this.readMarker);
}
return ret;
}
getNewPrevEvent() {
return this.events[0];
}
}
// Wrap consecutive member events in a ListSummary, ignore if redacted
class MemberGrouper {
static canStartGroup = function(panel, ev) {
@ -1148,4 +1231,4 @@ class MemberGrouper {
}
// all the grouper classes that we use
const groupers = [CreationGrouper, MemberGrouper];
const groupers = [CreationGrouper, MemberGrouper, RedactionGrouper];

View file

@ -16,7 +16,6 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import Matrix from 'matrix-js-sdk';
import { _t, _td } from '../../languageHandler';
import {MatrixClientPeg} from '../../MatrixClientPeg';
import Resend from '../../Resend';
@ -24,6 +23,7 @@ import dis from '../../dispatcher/dispatcher';
import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils';
import {Action} from "../../dispatcher/actions";
import {replaceableComponent} from "../../utils/replaceableComponent";
import {EventStatus} from "matrix-js-sdk/src/models/event";
const STATUS_BAR_HIDDEN = 0;
const STATUS_BAR_EXPANDED = 1;
@ -32,7 +32,7 @@ const STATUS_BAR_EXPANDED_LARGE = 2;
function getUnsentMessages(room) {
if (!room) { return []; }
return room.getPendingEvents().filter(function(ev) {
return ev.status === Matrix.EventStatus.NOT_SENT;
return ev.status === EventStatus.NOT_SENT;
});
}

View file

@ -14,27 +14,30 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useMemo, useRef, useState} from "react";
import React, {useMemo, useState} from "react";
import Room from "matrix-js-sdk/src/models/room";
import MatrixEvent from "matrix-js-sdk/src/models/event";
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
import classNames from "classnames";
import {sortBy} from "lodash";
import {MatrixClientPeg} from "../../MatrixClientPeg";
import dis from "../../dispatcher/dispatcher";
import {_t} from "../../languageHandler";
import AccessibleButton from "../views/elements/AccessibleButton";
import BaseDialog from "../views/dialogs/BaseDialog";
import FormButton from "../views/elements/FormButton";
import Spinner from "../views/elements/Spinner";
import SearchBox from "./SearchBox";
import RoomAvatar from "../views/avatars/RoomAvatar";
import RoomName from "../views/elements/RoomName";
import {useAsyncMemo} from "../../hooks/useAsyncMemo";
import {shouldShowSpaceSettings} from "../../utils/space";
import {EnhancedMap} from "../../utils/maps";
import StyledCheckbox from "../views/elements/StyledCheckbox";
import AutoHideScrollbar from "./AutoHideScrollbar";
import BaseAvatar from "../views/avatars/BaseAvatar";
import {mediaFromMxc} from "../../customisations/Media";
import InfoTooltip from "../views/elements/InfoTooltip";
import TextWithTooltip from "../views/elements/TextWithTooltip";
import {useStateToggle} from "../../hooks/useStateToggle";
interface IProps {
space: Room;
@ -72,215 +75,98 @@ export interface ISpaceSummaryEvent {
}
/* eslint-enable camelcase */
interface ISubspaceProps {
space: ISpaceSummaryRoom;
event?: MatrixEvent;
interface ITileProps {
room: ISpaceSummaryRoom;
editing?: boolean;
onPreviewClick?(): void;
queueAction?(action: IAction): void;
onJoinClick?(): void;
suggested?: boolean;
selected?: boolean;
numChildRooms?: number;
hasPermissions?: boolean;
onViewRoomClick(autoJoin: boolean): void;
onToggleClick?(): void;
}
const SubSpace: React.FC<ISubspaceProps> = ({
space,
const Tile: React.FC<ITileProps> = ({
room,
editing,
event,
queueAction,
onJoinClick,
onPreviewClick,
suggested,
selected,
hasPermissions,
onToggleClick,
onViewRoomClick,
numChildRooms,
children,
}) => {
const name = space.name || space.canonical_alias || space.aliases?.[0] || _t("Unnamed Space");
const name = room.name || room.canonical_alias || room.aliases?.[0]
|| (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room"));
const evContent = event?.getContent();
const [suggested, _setSuggested] = useState(evContent?.suggested);
const [removed, _setRemoved] = useState(!evContent?.via);
const cli = MatrixClientPeg.get();
const cliRoom = cli.getRoom(space.room_id);
const myMembership = cliRoom?.getMyMembership();
// TODO DRY code
let actions;
if (editing && queueAction) {
if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) {
const setSuggested = () => {
_setSuggested(v => {
queueAction({
event,
removed,
suggested: !v,
});
return !v;
});
};
const setRemoved = () => {
_setRemoved(v => {
queueAction({
event,
removed: !v,
suggested,
});
return !v;
});
};
if (removed) {
actions = <React.Fragment>
<FormButton kind="danger" onClick={setRemoved} label={_t("Undo")} />
</React.Fragment>;
} else {
actions = <React.Fragment>
<FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} />
<StyledCheckbox checked={suggested} onChange={setSuggested} />
</React.Fragment>;
}
} else {
actions = <span className="mx_SpaceRoomDirectory_actionsText">
{ _t("No permissions")}
</span>;
}
// TODO confirm remove from space click behaviour here
} else {
if (myMembership === "join") {
actions = <span className="mx_SpaceRoomDirectory_actionsText">
{ _t("You're in this space")}
</span>;
} else if (onJoinClick) {
actions = <React.Fragment>
<AccessibleButton onClick={onPreviewClick} kind="link">
{ _t("Preview") }
</AccessibleButton>
<FormButton onClick={onJoinClick} label={_t("Join")} />
</React.Fragment>
}
}
let url: string;
if (space.avatar_url) {
url = mediaFromMxc(space.avatar_url).getSquareThumbnailHttp(Math.floor(24 * window.devicePixelRatio));
}
return <div className="mx_SpaceRoomDirectory_subspace">
<div className="mx_SpaceRoomDirectory_subspace_info">
<BaseAvatar name={name} idName={space.room_id} url={url} width={24} height={24} />
{ name }
<div className="mx_SpaceRoomDirectory_actions">
{ actions }
</div>
</div>
<div className="mx_SpaceRoomDirectory_subspace_children">
{ children }
</div>
</div>
};
interface IAction {
event: MatrixEvent;
suggested: boolean;
removed: boolean;
}
interface IRoomTileProps {
room: ISpaceSummaryRoom;
event?: MatrixEvent;
editing?: boolean;
onPreviewClick(): void;
queueAction?(action: IAction): void;
onJoinClick?(): void;
}
const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinClick }: IRoomTileProps) => {
const name = room.name || room.canonical_alias || room.aliases?.[0] || _t("Unnamed Room");
const evContent = event?.getContent();
const [suggested, _setSuggested] = useState(evContent?.suggested);
const [removed, _setRemoved] = useState(!evContent?.via);
const [showChildren, toggleShowChildren] = useStateToggle(true);
const cli = MatrixClientPeg.get();
const cliRoom = cli.getRoom(room.room_id);
const myMembership = cliRoom?.getMyMembership();
let actions;
if (editing && queueAction) {
if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) {
const setSuggested = () => {
_setSuggested(v => {
queueAction({
event,
removed,
suggested: !v,
});
return !v;
});
};
const onPreviewClick = () => onViewRoomClick(false);
const onJoinClick = () => onViewRoomClick(true);
const setRemoved = () => {
_setRemoved(v => {
queueAction({
event,
removed: !v,
suggested,
});
return !v;
});
};
let button;
if (myMembership === "join") {
button = <AccessibleButton onClick={onPreviewClick} kind="primary_outline">
{ _t("Open") }
</AccessibleButton>;
} else if (onJoinClick) {
button = <AccessibleButton onClick={onJoinClick} kind="primary">
{ _t("Join") }
</AccessibleButton>;
}
if (removed) {
actions = <React.Fragment>
<FormButton kind="danger" onClick={setRemoved} label={_t("Undo")} />
</React.Fragment>;
} else {
actions = <React.Fragment>
<FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} />
<StyledCheckbox checked={suggested} onChange={setSuggested} />
</React.Fragment>;
}
let checkbox;
if (onToggleClick) {
if (hasPermissions) {
checkbox = <StyledCheckbox checked={!!selected} onChange={onToggleClick} />;
} else {
actions = <span className="mx_SpaceRoomDirectory_actionsText">
{ _t("No permissions")}
</span>;
}
// TODO confirm remove from space click behaviour here
} else {
if (myMembership === "join") {
actions = <span className="mx_SpaceRoomDirectory_actionsText">
{ _t("You're in this room")}
</span>;
} else if (onJoinClick) {
actions = <React.Fragment>
<AccessibleButton onClick={onPreviewClick} kind="link">
{ _t("Preview") }
</AccessibleButton>
<FormButton onClick={onJoinClick} label={_t("Join")} />
</React.Fragment>
checkbox = <TextWithTooltip
tooltip={_t("You don't have permission")}
onClick={ev => { ev.stopPropagation() }}
>
<StyledCheckbox disabled={true} />
</TextWithTooltip>;
}
}
let url: string;
if (room.avatar_url) {
url = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(Math.floor(32 * window.devicePixelRatio));
url = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(Math.floor(20 * window.devicePixelRatio));
}
let description = _t("%(count)s members", { count: room.num_joined_members });
if (numChildRooms) {
description += " · " + _t("%(count)s rooms", { count: numChildRooms });
}
if (room.topic) {
description += " · " + room.topic;
}
let suggestedSection;
if (suggested) {
suggestedSection = <InfoTooltip tooltip={_t("This room is suggested as a good one to join")}>
{ _t("Suggested") }
</InfoTooltip>;
}
const content = <React.Fragment>
<BaseAvatar name={name} idName={room.room_id} url={url} width={32} height={32} />
<BaseAvatar name={name} idName={room.room_id} url={url} width={20} height={20} />
<div className="mx_SpaceRoomDirectory_roomTile_name">
{ name }
{ suggestedSection }
</div>
<div className="mx_SpaceRoomDirectory_roomTile_info">
<div className="mx_SpaceRoomDirectory_roomTile_name">
{ name }
</div>
<div className="mx_SpaceRoomDirectory_roomTile_topic">
{ room.topic }
</div>
{ description }
</div>
<div className="mx_SpaceRoomDirectory_roomTile_memberCount">
{ room.num_joined_members }
</div>
<div className="mx_SpaceRoomDirectory_actions">
{ actions }
{ button }
{ checkbox }
</div>
</React.Fragment>;
@ -290,9 +176,38 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
</div>
}
return <AccessibleButton className="mx_SpaceRoomDirectory_roomTile" onClick={onPreviewClick}>
{ content }
</AccessibleButton>;
let childToggle;
let childSection;
if (children) {
// the chevron is purposefully a div rather than a button as it should be ignored for a11y
childToggle = <div
className={classNames("mx_SpaceRoomDirectory_subspace_toggle", {
mx_SpaceRoomDirectory_subspace_toggle_shown: showChildren,
})}
onClick={ev => {
ev.stopPropagation();
toggleShowChildren();
}}
/>;
if (showChildren) {
childSection = <div className="mx_SpaceRoomDirectory_subspace_children">
{ children }
</div>;
}
}
return <>
<AccessibleButton
className={classNames("mx_SpaceRoomDirectory_roomTile", {
mx_SpaceRoomDirectory_subspace: room.room_type === RoomType.Space,
})}
onClick={hasPermissions ? onToggleClick : onPreviewClick}
>
{ content }
{ childToggle }
</AccessibleButton>
{ childSection }
</>;
};
export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoin = false) => {
@ -325,88 +240,77 @@ export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoi
interface IHierarchyLevelProps {
spaceId: string;
rooms: Map<string, ISpaceSummaryRoom>;
editing?: boolean;
relations: EnhancedMap<string, string[]>;
relations: EnhancedMap<string, Map<string, ISpaceSummaryEvent>>;
parents: Set<string>;
queueAction?(action: IAction): void;
onPreviewClick(roomId: string): void;
onRemoveFromSpaceClick?(roomId: string): void;
onJoinClick?(roomId: string): void;
selectedMap?: Map<string, Set<string>>;
onViewRoomClick(roomId: string, autoJoin: boolean): void;
onToggleClick?(parentId: string, childId: string): void;
}
export const HierarchyLevel = ({
spaceId,
rooms,
editing,
relations,
parents,
onPreviewClick,
onJoinClick,
queueAction,
selectedMap,
onViewRoomClick,
onToggleClick,
}: IHierarchyLevelProps) => {
const cli = MatrixClientPeg.get();
const space = cli.getRoom(spaceId);
// TODO respect order
const [subspaces, childRooms] = relations.get(spaceId)?.reduce((result, roomId: string) => {
if (!rooms.has(roomId)) return result; // TODO wat
const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId())
const sortedChildren = sortBy([...relations.get(spaceId)?.values()], ev => ev.content.order || null);
const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => {
const roomId = ev.state_key;
if (!rooms.has(roomId)) return result;
result[rooms.get(roomId).room_type === RoomType.Space ? 0 : 1].push(roomId);
return result;
}, [[], []]) || [[], []];
// Don't render this subspace if it has no rooms we can show
// TODO this is broken - as a space may have subspaces we still need to show
// if (!childRooms.length) return null;
const userId = cli.getUserId();
const newParents = new Set(parents).add(spaceId);
return <React.Fragment>
{
childRooms.map(roomId => (
<RoomTile
<Tile
key={roomId}
room={rooms.get(roomId)}
event={space?.currentState.maySendStateEvent(EventType.SpaceChild, userId)
? space?.currentState.getStateEvents(EventType.SpaceChild, roomId)
: undefined}
editing={editing}
queueAction={queueAction}
onPreviewClick={() => {
onPreviewClick(roomId);
suggested={relations.get(spaceId)?.get(roomId)?.content.suggested}
selected={selectedMap?.get(spaceId)?.has(roomId)}
onViewRoomClick={(autoJoin) => {
onViewRoomClick(roomId, autoJoin);
}}
onJoinClick={onJoinClick ? () => {
onJoinClick(roomId);
} : undefined}
hasPermissions={hasPermissions}
onToggleClick={onToggleClick ? () => onToggleClick(spaceId, roomId) : undefined}
/>
))
}
{
subspaces.filter(roomId => !newParents.has(roomId)).map(roomId => (
<SubSpace
<Tile
key={roomId}
space={rooms.get(roomId)}
event={space?.currentState.getStateEvents(EventType.SpaceChild, roomId)}
editing={editing}
queueAction={queueAction}
onPreviewClick={() => {
onPreviewClick(roomId);
}}
onJoinClick={() => {
onJoinClick(roomId);
room={rooms.get(roomId)}
numChildRooms={Array.from(relations.get(roomId)?.values() || [])
.filter(ev => rooms.get(ev.state_key)?.room_type !== RoomType.Space).length}
suggested={relations.get(spaceId)?.get(roomId)?.content.suggested}
selected={selectedMap?.get(spaceId)?.has(roomId)}
onViewRoomClick={(autoJoin) => {
onViewRoomClick(roomId, autoJoin);
}}
hasPermissions={hasPermissions}
onToggleClick={onToggleClick ? () => onToggleClick(spaceId, roomId) : undefined}
>
<HierarchyLevel
spaceId={roomId}
rooms={rooms}
editing={editing}
relations={relations}
parents={newParents}
onPreviewClick={onPreviewClick}
onJoinClick={onJoinClick}
queueAction={queueAction}
selectedMap={selectedMap}
onViewRoomClick={onViewRoomClick}
onToggleClick={onToggleClick}
/>
</SubSpace>
</Tile>
))
}
</React.Fragment>
@ -415,8 +319,8 @@ export const HierarchyLevel = ({
const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinished }) => {
// TODO pagination
const cli = MatrixClientPeg.get();
const userId = cli.getUserId();
const [query, setQuery] = useState(initialText);
const [isEditing, setIsEditing] = useState(false);
const onCreateRoomClick = () => {
dis.dispatch({
@ -426,51 +330,19 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
onFinished();
};
// stored within a ref as we don't need to re-render when it changes
const pendingActions = useRef(new Map<string, IAction>());
const [selected, setSelected] = useState(new Map<string, Set<string>>()); // Map<parentId, Set<childId>>
let adminButton;
if (shouldShowSpaceSettings(cli, space)) { // TODO this is an imperfect test
const onManageButtonClicked = () => {
setIsEditing(true);
};
const onSaveButtonClicked = () => {
// TODO setBusy
pendingActions.current.forEach(({event, suggested, removed}) => {
const content = {
...event.getContent(),
suggested,
};
if (removed) {
delete content["via"];
}
cli.sendStateEvent(event.getRoomId(), event.getType(), content, event.getStateKey());
});
setIsEditing(false);
};
if (isEditing) {
adminButton = <React.Fragment>
<FormButton label={_t("Save changes")} onClick={onSaveButtonClicked} />
<span>{ _t("Promoted to users") }</span>
</React.Fragment>;
} else {
adminButton = <FormButton label={_t("Manage rooms")} onClick={onManageButtonClicked} />;
}
}
const [rooms, relations, viaMap] = useAsyncMemo(async () => {
const [rooms, parentChildMap, childParentMap, viaMap] = useAsyncMemo(async () => {
try {
const data = await cli.getSpaceSummary(space.roomId);
const parentChildRelations = new EnhancedMap<string, string[]>();
const parentChildRelations = new EnhancedMap<string, Map<string, ISpaceSummaryEvent>>();
const childParentRelations = new EnhancedMap<string, Set<string>>();
const viaMap = new EnhancedMap<string, Set<string>>();
data.events.map((ev: ISpaceSummaryEvent) => {
if (ev.type === EventType.SpaceChild) {
parentChildRelations.getOrCreate(ev.room_id, []).push(ev.state_key);
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"])) {
const set = viaMap.getOrCreate(ev.state_key, new Set());
@ -478,7 +350,7 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
}
});
return [data.rooms, parentChildRelations, viaMap];
return [data.rooms as ISpaceSummaryRoom[], parentChildRelations, childParentRelations, viaMap];
} catch (e) {
console.error(e); // TODO
}
@ -488,54 +360,204 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
const roomsMap = useMemo(() => {
if (!rooms) return null;
const lcQuery = query.toLowerCase();
const lcQuery = query.toLowerCase().trim();
const filteredRooms = rooms.filter(r => {
return r.room_type === RoomType.Space // always include spaces to allow filtering of sub-space rooms
|| r.name?.toLowerCase().includes(lcQuery)
|| r.topic?.toLowerCase().includes(lcQuery);
const roomsMap = new Map<string, ISpaceSummaryRoom>(rooms.map(r => [r.room_id, r]));
if (!lcQuery) return roomsMap;
const directMatches = rooms.filter(r => {
return r.name?.toLowerCase().includes(lcQuery) || r.topic?.toLowerCase().includes(lcQuery);
});
return new Map<string, ISpaceSummaryRoom>(filteredRooms.map(r => [r.room_id, r]));
// const root = rooms.get(space.roomId);
}, [rooms, query]);
// Walk back up the tree to find all parents of the direct matches to show their place in the hierarchy
const visited = new Set<string>();
const queue = [...directMatches.map(r => r.room_id)];
while (queue.length) {
const roomId = queue.pop();
visited.add(roomId);
childParentMap.get(roomId)?.forEach(parentId => {
if (!visited.has(parentId)) {
queue.push(parentId);
}
});
}
// Remove any mappings for rooms which were not visited in the walk
Array.from(roomsMap.keys()).forEach(roomId => {
if (!visited.has(roomId)) {
roomsMap.delete(roomId);
}
});
return roomsMap;
}, [rooms, childParentMap, query]);
const title = <React.Fragment>
<RoomAvatar room={space} height={40} width={40} />
<RoomAvatar room={space} height={32} width={32} />
<div>
<h1>{ _t("Explore rooms") }</h1>
<div><RoomName room={space} /></div>
</div>
</React.Fragment>;
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,
_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="link" onClick={onCreateRoomClick}>{sub}</AccessibleButton>;
}},
);
const [error, setError] = useState("");
const [removing, setRemoving] = useState(false);
const [saving, setSaving] = useState(false);
let content;
if (roomsMap) {
content = <AutoHideScrollbar className="mx_SpaceRoomDirectory_list">
<HierarchyLevel
spaceId={space.roomId}
rooms={roomsMap}
editing={isEditing}
relations={relations}
parents={new Set()}
queueAction={action => {
pendingActions.current.set(action.event.room_id, action);
}}
onPreviewClick={roomId => {
showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), false);
onFinished();
}}
onJoinClick={(roomId) => {
showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), true);
onFinished();
}}
/>
</AutoHideScrollbar>;
const numRooms = Array.from(roomsMap.values()).filter(r => r.room_type !== RoomType.Space).length;
const numSpaces = roomsMap.size - numRooms - 1; // -1 at the end to exclude the space we are looking at
let countsStr;
if (numSpaces > 1) {
countsStr = _t("%(count)s rooms and %(numSpaces)s spaces", { count: numRooms, numSpaces });
} else if (numSpaces > 0) {
countsStr = _t("%(count)s rooms and 1 space", { count: numRooms, numSpaces });
} else {
countsStr = _t("%(count)s rooms", { count: numRooms, numSpaces });
}
let editSection;
if (space.getMyMembership() === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
const selectedRelations = Array.from(selected.keys()).flatMap(parentId => {
return [...selected.get(parentId).values()].map(childId => [parentId, childId]) as [string, string][];
});
let buttons;
if (selectedRelations.length) {
const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => {
return parentChildMap.get(parentId)?.get(childId)?.content.suggested;
});
const disabled = removing || saving;
buttons = <>
<AccessibleButton
onClick={async () => {
setRemoving(true);
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)));
}
} catch (e) {
setError(_t("Failed to remove some rooms. Try again later"));
}
setRemoving(false);
}}
kind="danger_outline"
disabled={disabled}
>
{ removing ? _t("Removing...") : _t("Remove") }
</AccessibleButton>
<AccessibleButton
onClick={async () => {
setSaving(true);
try {
for (const [parentId, childId] of selectedRelations) {
const suggested = !selectionAllSuggested;
const existingContent = parentChildMap.get(parentId)?.get(childId)?.content;
if (!existingContent || existingContent.suggested === suggested) continue;
const content = {
...existingContent,
suggested: !selectionAllSuggested,
};
await cli.sendStateEvent(parentId, EventType.SpaceChild, content, childId);
parentChildMap.get(parentId).get(childId).content = content;
parentChildMap.set(parentId, new Map(parentChildMap.get(parentId)));
}
} catch (e) {
setError("Failed to update some suggestions. Try again later");
}
setSaving(false);
}}
kind="primary_outline"
disabled={disabled}
>
{ saving
? _t("Saving...")
: (selectionAllSuggested ? _t("Mark as not suggested") : _t("Mark as suggested"))
}
</AccessibleButton>
</>;
}
editSection = <span>
{ buttons }
</span>;
}
let results;
if (roomsMap.size) {
results = <>
<HierarchyLevel
spaceId={space.roomId}
rooms={roomsMap}
relations={parentChildMap}
parents={new Set()}
selectedMap={selected}
onToggleClick={(parentId, childId) => {
setError("");
if (!selected.has(parentId)) {
setSelected(new Map(selected.set(parentId, new Set([childId]))));
return;
}
const parentSet = selected.get(parentId);
if (!parentSet.has(childId)) {
setSelected(new Map(selected.set(parentId, new Set([...parentSet, childId]))));
return;
}
parentSet.delete(childId);
setSelected(new Map(selected.set(parentId, new Set(parentSet))));
}}
onViewRoomClick={(roomId, autoJoin) => {
showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), autoJoin);
onFinished();
}}
/>
<hr />
</>;
} else {
results = <div className="mx_SpaceRoomDirectory_noResults">
<h3>{ _t("No results found") }</h3>
<div>{ _t("You may want to try a different search or check for typos.") }</div>
</div>;
}
content = <>
<div className="mx_SpaceRoomDirectory_listHeader">
{ countsStr }
{ editSection }
</div>
{ error && <div className="mx_SpaceRoomDirectory_error">
{ error }
</div> }
<AutoHideScrollbar className="mx_SpaceRoomDirectory_list">
{ results }
<AccessibleButton
onClick={onCreateRoomClick}
kind="primary"
className="mx_SpaceRoomDirectory_createRoom"
>
{ _t("Create room") }
</AccessibleButton>
</AutoHideScrollbar>
</>;
} else {
content = <Spinner />;
}
// TODO loading state/error state
@ -546,13 +568,10 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
<SearchBox
className="mx_textinput_icon mx_textinput_search"
placeholder={ _t("Find a room...") }
placeholder={ _t("Search names and description") }
onSearch={setQuery}
/>
<div className="mx_SpaceRoomDirectory_listHeader">
{ adminButton }
</div>
{ content }
</div>
</BaseDialog>

View file

@ -17,6 +17,7 @@ limitations under the License.
import React, {RefObject, useContext, useRef, useState} from "react";
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
import {Room} from "matrix-js-sdk/src/models/room";
import {EventSubscription} from "fbemitter";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import RoomAvatar from "../views/avatars/RoomAvatar";
@ -31,7 +32,6 @@ import {useRoomMembers} from "../../hooks/useRoomMembers";
import createRoom, {IOpts, Preset} from "../../createRoom";
import Field from "../views/elements/Field";
import {useEventEmitter} from "../../hooks/useEventEmitter";
import StyledRadioGroup from "../views/elements/StyledRadioGroup";
import withValidation from "../views/elements/Validation";
import * as Email from "../../email";
import defaultDispatcher from "../../dispatcher/dispatcher";
@ -42,7 +42,6 @@ import ErrorBoundary from "../views/elements/ErrorBoundary";
import {ActionPayload} from "../../dispatcher/payloads";
import RightPanel from "./RightPanel";
import RightPanelStore from "../../stores/RightPanelStore";
import {EventSubscription} from "fbemitter";
import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
import {SetRightPanelPhasePayload} from "../../dispatcher/payloads/SetRightPanelPhasePayload";
import {useStateArray} from "../../hooks/useStateArray";
@ -54,6 +53,7 @@ import {EnhancedMap} from "../../utils/maps";
import AutoHideScrollbar from "./AutoHideScrollbar";
import MemberAvatar from "../views/avatars/MemberAvatar";
import {useStateToggle} from "../../hooks/useStateToggle";
import SpaceStore from "../../stores/SpaceStore";
interface IProps {
space: Room;
@ -66,6 +66,7 @@ interface IProps {
interface IState {
phase: Phase;
showRightPanel: boolean;
myMembership: string;
}
enum Phase {
@ -98,6 +99,8 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
const cli = useContext(MatrixClientContext);
const myMembership = useMyRoomMembership(space);
const [busy, setBusy] = useState(false);
let inviterSection;
let joinButtons;
if (myMembership === "invite") {
@ -121,11 +124,35 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
}
joinButtons = <>
<FormButton label={_t("Reject")} kind="secondary" onClick={onRejectButtonClicked} />
<FormButton label={_t("Accept")} onClick={onJoinButtonClicked} />
<FormButton
label={_t("Reject")}
kind="secondary"
onClick={() => {
setBusy(true);
onRejectButtonClicked();
}} />
<FormButton
label={_t("Accept")}
onClick={() => {
setBusy(true);
onJoinButtonClicked();
}}
/>
</>;
} else {
joinButtons = <FormButton label={_t("Join")} onClick={onJoinButtonClicked} />
joinButtons = (
<FormButton
label={_t("Join")}
onClick={() => {
setBusy(true);
onJoinButtonClicked();
}}
/>
)
}
if (busy) {
joinButtons = <InlineSpinner />;
}
let visibilitySection;
@ -230,10 +257,10 @@ const SpaceLanding = ({ space }) => {
try {
const data = await cli.getSpaceSummary(space.roomId, undefined, myMembership !== "join");
const parentChildRelations = new EnhancedMap<string, string[]>();
const parentChildRelations = new EnhancedMap<string, Map<string, ISpaceSummaryEvent>>();
data.events.map((ev: ISpaceSummaryEvent) => {
if (ev.type === EventType.SpaceChild) {
parentChildRelations.getOrCreate(ev.room_id, []).push(ev.state_key);
parentChildRelations.getOrCreate(ev.room_id, new Map()).set(ev.state_key, ev);
}
});
@ -257,11 +284,10 @@ const SpaceLanding = ({ space }) => {
<HierarchyLevel
spaceId={space.roomId}
rooms={roomsMap}
editing={false}
relations={relations}
parents={new Set()}
onPreviewClick={roomId => {
showRoom(roomsMap.get(roomId), [], false); // TODO
onViewRoomClick={(roomId, autoJoin) => {
showRoom(roomsMap.get(roomId), [], autoJoin);
}}
/>
</AutoHideScrollbar>;
@ -337,6 +363,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
placeholder={placeholders[i]}
value={roomNames[i]}
onChange={ev => setRoomName(i, ev.target.value)}
autoFocus={i === 2}
/>;
});
@ -369,7 +396,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
let buttonLabel = _t("Skip for now");
if (roomNames.some(name => name.trim())) {
onClick = onNextClick;
buttonLabel = busy ? _t("Creating rooms...") : _t("Next")
buttonLabel = busy ? _t("Creating rooms...") : _t("Continue")
}
return <div>
@ -391,50 +418,40 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
const SpaceSetupPublicShare = ({ space, onFinished }) => {
return <div className="mx_SpaceRoomView_publicShare">
<h1>{ _t("Share your public space") }</h1>
<div className="mx_SpacePublicShare_description">{ _t("At the moment only you can see it.") }</div>
<h1>{ _t("Share %(name)s", { name: space.name }) }</h1>
<div className="mx_SpacePublicShare_description">
{ _t("It's just you at the moment, it will be even better with others.") }
</div>
<SpacePublicShare space={space} onFinished={onFinished} />
<div className="mx_SpaceRoomView_buttons">
<FormButton label={_t("Finish")} onClick={onFinished} />
<FormButton label={_t("Go to my first room")} onClick={onFinished} />
</div>
</div>;
};
const SpaceSetupPrivateScope = ({ onFinished }) => {
const [option, setOption] = useState<string>(null);
const SpaceSetupPrivateScope = ({ space, onFinished }) => {
return <div className="mx_SpaceRoomView_privateScope">
<h1>{ _t("Who are you working with?") }</h1>
<div className="mx_SpaceRoomView_description">{ _t("Ensure the right people have access to the space.") }</div>
<StyledRadioGroup
name="privateSpaceScope"
value={option}
onChange={setOption}
definitions={[
{
value: "justMe",
className: "mx_SpaceRoomView_privateScope_justMeButton",
label: <React.Fragment>
<h3>{ _t("Just Me") }</h3>
<div>{ _t("A private space just for you") }</div>
</React.Fragment>,
}, {
value: "meAndMyTeammates",
className: "mx_SpaceRoomView_privateScope_meAndMyTeammatesButton",
label: <React.Fragment>
<h3>{ _t("Me and my teammates") }</h3>
<div>{ _t("A private space for you and your teammates") }</div>
</React.Fragment>,
},
]}
/>
<div className="mx_SpaceRoomView_buttons">
<FormButton label={_t("Next")} disabled={!option} onClick={() => onFinished(option !== "justMe")} />
<div className="mx_SpaceRoomView_description">
{ _t("Make sure the right people have access to %(name)s", { name: space.name }) }
</div>
<AccessibleButton
className="mx_SpaceRoomView_privateScope_justMeButton"
onClick={() => { onFinished(false) }}
>
<h3>{ _t("Just me") }</h3>
<div>{ _t("A private space to organise your rooms") }</div>
</AccessibleButton>
<AccessibleButton
className="mx_SpaceRoomView_privateScope_meAndMyTeammatesButton"
onClick={() => { onFinished(true) }}
>
<h3>{ _t("Me and my teammates") }</h3>
<div>{ _t("A private space for you and your teammates") }</div>
</AccessibleButton>
</div>;
};
@ -464,6 +481,7 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
onChange={ev => setEmailAddress(i, ev.target.value)}
ref={fieldRefs[i]}
onValidate={validateEmailRules}
autoFocus={i === 0}
/>;
});
@ -501,9 +519,18 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
setBusy(false);
};
let onClick = onFinished;
let buttonLabel = _t("Skip for now");
if (emailAddresses.some(name => name.trim())) {
onClick = onNextClick;
buttonLabel = busy ? _t("Inviting...") : _t("Continue")
}
return <div className="mx_SpaceRoomView_inviteTeammates">
<h1>{ _t("Invite your teammates") }</h1>
<div className="mx_SpaceRoomView_description">{ _t("Ensure the right people have access to the space.") }</div>
<div className="mx_SpaceRoomView_description">
{ _t("Make sure the right people have access. You can invite more later.") }
</div>
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
{ fields }
@ -518,8 +545,7 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
</div>
<div className="mx_SpaceRoomView_buttons">
<AccessibleButton onClick={onFinished} kind="link">{_t("Skip for now")}</AccessibleButton>
<FormButton label={busy ? _t("Inviting...") : _t("Next")} disabled={busy} onClick={onNextClick} />
<FormButton label={buttonLabel} disabled={busy} onClick={onClick} />
</div>
</div>;
};
@ -547,17 +573,26 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
this.state = {
phase,
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
myMembership: this.props.space.getMyMembership(),
};
this.dispatcherRef = defaultDispatcher.register(this.onAction);
this.rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelStoreUpdate);
this.context.on("Room.myMembership", this.onMyMembership);
}
componentWillUnmount() {
defaultDispatcher.unregister(this.dispatcherRef);
this.rightPanelStoreToken.remove();
this.context.off("Room.myMembership", this.onMyMembership);
}
private onMyMembership = (room: Room, myMembership: string) => {
if (room.roomId === this.props.space.roomId) {
this.setState({ myMembership });
}
};
private onRightPanelStoreUpdate = () => {
this.setState({
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
@ -594,10 +629,43 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
}
};
private goToFirstRoom = async () => {
const childRooms = SpaceStore.instance.getChildRooms(this.props.space.roomId);
if (childRooms.length) {
const room = childRooms[0];
defaultDispatcher.dispatch({
action: "view_room",
room_id: room.roomId,
});
return;
}
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;
}
if (suggestedRooms.length) {
const room = suggestedRooms[0];
defaultDispatcher.dispatch({
action: "view_room",
room_id: room.room_id,
oobData: {
avatarUrl: room.avatar_url,
name: room.name || room.canonical_alias || room.aliases.pop() || _t("Empty room"),
},
});
return;
}
this.setState({ phase: Phase.Landing });
};
private renderBody() {
switch (this.state.phase) {
case Phase.Landing:
if (this.props.space.getMyMembership() === "join") {
if (this.state.myMembership === "join") {
return <SpaceLanding space={this.props.space} />;
} else {
return <SpacePreview
@ -610,17 +678,16 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
return <SpaceSetupFirstRooms
space={this.props.space}
title={_t("What are some things you want to discuss?")}
description={_t("We'll create rooms for each topic.")}
description={_t("Let's create a room for each of them. " +
"You can add more later too, including already existing ones.")}
onFinished={() => this.setState({ phase: Phase.PublicShare })}
/>;
case Phase.PublicShare:
return <SpaceSetupPublicShare
space={this.props.space}
onFinished={() => this.setState({ phase: Phase.Landing })}
/>;
return <SpaceSetupPublicShare space={this.props.space} onFinished={this.goToFirstRoom} />;
case Phase.PrivateScope:
return <SpaceSetupPrivateScope
space={this.props.space}
onFinished={(invite: boolean) => {
this.setState({ phase: invite ? Phase.PrivateInvite : Phase.PrivateCreateRooms });
}}
@ -634,7 +701,8 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
return <SpaceSetupFirstRooms
space={this.props.space}
title={_t("What projects are you working on?")}
description={_t("We'll create rooms for each of them. You can add existing rooms after setup.")}
description={_t("We'll create rooms for each of them. " +
"You can add more later too, including already existing ones.")}
onFinished={() => this.setState({ phase: Phase.Landing })}
/>;
}

View file

@ -22,8 +22,8 @@ import {LayoutPropType} from "../../settings/Layout";
import React, {createRef} from 'react';
import ReactDOM from "react-dom";
import PropTypes from 'prop-types';
import {EventTimeline} from "matrix-js-sdk";
import * as Matrix from "matrix-js-sdk";
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 UserActivity from "../../UserActivity";
@ -463,6 +463,9 @@ class TimelinePanel extends React.Component {
}
});
}
if (payload.action === "scroll_to_bottom") {
this.jumpToLiveTimeline();
}
};
onRoomTimeline = (ev, room, toStartOfTimeline, removed, data) => {
@ -1007,7 +1010,7 @@ class TimelinePanel extends React.Component {
* returns a promise which will resolve when the load completes.
*/
_loadTimeline(eventId, pixelOffset, offsetBase) {
this._timelineWindow = new Matrix.TimelineWindow(
this._timelineWindow = new TimelineWindow(
MatrixClientPeg.get(), this.props.timelineSet,
{windowLimit: this.props.timelineCap});

View file

@ -17,13 +17,14 @@ limitations under the License.
import React from "react";
import PropTypes from "prop-types";
import Matrix from "matrix-js-sdk";
import {MatrixClientPeg} from "../../MatrixClientPeg";
import * as sdk from "../../index";
import Modal from '../../Modal';
import { _t } from '../../languageHandler';
import HomePage from "./HomePage";
import {replaceableComponent} from "../../utils/replaceableComponent";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
@replaceableComponent("structures.UserView")
export default class UserView extends React.Component {
@ -68,8 +69,8 @@ export default class UserView extends React.Component {
this.setState({loading: false});
return;
}
const fakeEvent = new Matrix.MatrixEvent({type: "m.room.member", content: profileInfo});
const member = new Matrix.RoomMember(null, this.props.userId);
const fakeEvent = new MatrixEvent({type: "m.room.member", content: profileInfo});
const member = new RoomMember(null, this.props.userId);
member.setMembershipEvent(fakeEvent);
this.setState({member, loading: false});
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import Matrix from 'matrix-js-sdk';
import {createClient} from 'matrix-js-sdk/src/matrix';
import React, {ReactNode} from 'react';
import {MatrixClient} from "matrix-js-sdk/src/client";
@ -181,7 +181,7 @@ export default class Registration extends React.Component<IProps, IState> {
}
const {hsUrl, isUrl} = serverConfig;
const cli = Matrix.createClient({
const cli = createClient({
baseUrl: hsUrl,
idBaseUrl: isUrl,
});

View file

@ -20,7 +20,7 @@ import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import {Group} from 'matrix-js-sdk';
import {Group} from 'matrix-js-sdk/src/models/group';
import GroupStore from "../../../stores/GroupStore";
import {MenuItem} from "../../structures/ContextMenu";
import {replaceableComponent} from "../../../utils/replaceableComponent";

View file

@ -19,7 +19,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {EventStatus} from 'matrix-js-sdk';
import {EventStatus} from 'matrix-js-sdk/src/models/event';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher';

View file

@ -69,6 +69,7 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
const existingRoomsSet = new Set(existingRooms);
const rooms = cli.getVisibleRooms().filter(room => {
return !existingRoomsSet.has(room) // not already in space
&& !room.isSpaceRoom() // not a space itself
&& room.name.toLowerCase().includes(lcQuery) // contains query
&& !DMRoomMap.shared().getUserIdForRoomId(room.roomId); // not a DM
});

View file

@ -16,7 +16,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
import { MatrixClient } from 'matrix-js-sdk/src/client';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import { GroupMemberType } from '../../../groups';

View file

@ -19,7 +19,6 @@ import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import SyntaxHighlight from '../elements/SyntaxHighlight';
import { _t } from '../../../languageHandler';
import { Room, MatrixEvent } from "matrix-js-sdk";
import Field from "../elements/Field";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {useEventEmitter} from "../../../hooks/useEventEmitter";
@ -39,6 +38,8 @@ import SettingsStore, {LEVEL_ORDER} from "../../../settings/SettingsStore";
import Modal from "../../../Modal";
import ErrorDialog from "./ErrorDialog";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {Room} from "matrix-js-sdk/src/models/room";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
class GenericEditor extends React.PureComponent {
// static propTypes = {onBack: PropTypes.func.isRequired};

View file

@ -100,6 +100,20 @@ export default (props) => {
);
}
let bugReports = null;
if (SdkConfig.get().bug_report_endpoint_url) {
bugReports = (
<p>{
_t("PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> " +
"to help us track down the problem.", {}, {
debugLogsLink: sub => (
<AccessibleButton kind="link" onClick={onDebugLogsLinkClick}>{sub}</AccessibleButton>
),
})
}</p>
);
}
return (<QuestionDialog
className="mx_FeedbackDialog"
hasCancelButton={!!hasFeedback}
@ -120,14 +134,7 @@ export default (props) => {
},
})
}</p>
<p>{
_t("PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> " +
"to help us track down the problem.", {}, {
debugLogsLink: sub => (
<AccessibleButton kind="link" onClick={onDebugLogsLinkClick}>{sub}</AccessibleButton>
),
})
}</p>
{bugReports}
</div>
{ countlyFeedbackSection }
</React.Fragment>}

View file

@ -1256,7 +1256,9 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
? _t("Invite to %(spaceName)s", {
spaceName: room.name || _t("Unnamed Space"),
})
: _t("Invite to this room");
: _t("Invite to %(roomName)s", {
roomName: room.name || _t("Unnamed Room"),
});
let helpTextUntranslated;
if (isSpace) {

View file

@ -18,7 +18,7 @@ import React, {PureComponent} from 'react';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import PropTypes from "prop-types";
import {MatrixEvent} from "matrix-js-sdk";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import SdkConfig from '../../../SdkConfig';
import Markdown from '../../../Markdown';

View file

@ -82,6 +82,33 @@ export default class RoomUpgradeWarningDialog extends React.Component {
const title = this.state.isPrivate ? _t("Upgrade private room") : _t("Upgrade public room");
let bugReports = (
<p>
{_t(
"This usually only affects how the room is processed on the server. If you're " +
"having problems with your %(brand)s, please report a bug.", {brand},
)}
</p>
);
if (SdkConfig.get().bug_report_endpoint_url) {
bugReports = (
<p>
{_t(
"This usually only affects how the room is processed on the server. If you're " +
"having problems with your %(brand)s, please <a>report a bug</a>.",
{
brand,
},
{
"a": (sub) => {
return <a href='#' onClick={this._openBugReportDialog}>{sub}</a>;
},
},
)}
</p>
);
}
return (
<BaseDialog
className='mx_RoomUpgradeWarningDialog'
@ -97,20 +124,7 @@ export default class RoomUpgradeWarningDialog extends React.Component {
"is unstable due to bugs, missing features or security vulnerabilities.",
)}
</p>
<p>
{_t(
"This usually only affects how the room is processed on the server. If you're " +
"having problems with your %(brand)s, please <a>report a bug</a>.",
{
brand,
},
{
"a": (sub) => {
return <a href='#' onClick={this._openBugReportDialog}>{sub}</a>;
},
},
)}
</p>
{bugReports}
<p>
{_t(
"You'll upgrade this room from <oldVersion /> to <newVersion />.",

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import {Room} from "matrix-js-sdk";
import {Room} from "matrix-js-sdk/src/models/room";
import * as sdk from '../../../index';
import {dialogTermsInteractionCallback, TermsNotSignedError} from "../../../Terms";
import classNames from 'classnames';

View file

@ -20,8 +20,8 @@ import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t, pickBestLanguage } from '../../../languageHandler';
import Matrix from 'matrix-js-sdk';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {SERVICE_TYPES} from "matrix-js-sdk/src/service-types";
class TermsCheckbox extends React.PureComponent {
static propTypes = {
@ -85,22 +85,22 @@ export default class TermsDialog extends React.PureComponent {
_nameForServiceType(serviceType, host) {
switch (serviceType) {
case Matrix.SERVICE_TYPES.IS:
case SERVICE_TYPES.IS:
return <div>{_t("Identity Server")}<br />({host})</div>;
case Matrix.SERVICE_TYPES.IM:
case SERVICE_TYPES.IM:
return <div>{_t("Integration Manager")}<br />({host})</div>;
}
}
_summaryForServiceType(serviceType) {
switch (serviceType) {
case Matrix.SERVICE_TYPES.IS:
case SERVICE_TYPES.IS:
return <div>
{_t("Find others by phone or email")}
<br />
{_t("Be found by phone or email")}
</div>;
case Matrix.SERVICE_TYPES.IM:
case SERVICE_TYPES.IM:
return <div>
{_t("Use bots, bridges, widgets and sticker packs")}
</div>;

View file

@ -19,7 +19,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../../index';
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
import { MatrixClient } from 'matrix-js-sdk';
import { MatrixClient } from 'matrix-js-sdk/src/client';
import { _t } from '../../../../languageHandler';
import { accessSecretStorage } from '../../../../SecurityManager';

View file

@ -17,7 +17,8 @@ import React from 'react';
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import classNames from 'classnames';
import { Room, RoomMember } from 'matrix-js-sdk';
import { Room } from 'matrix-js-sdk/src/models/room';
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import PropTypes from 'prop-types';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import FlairStore from "../../../stores/FlairStore";

View file

@ -21,7 +21,7 @@ import {_t} from '../../../languageHandler';
import PropTypes from 'prop-types';
import dis from '../../../dispatcher/dispatcher';
import {wantsDateSeparator} from '../../../DateUtils';
import {MatrixEvent} from 'matrix-js-sdk';
import {MatrixEvent} from 'matrix-js-sdk/src/models/event';
import {makeUserPermalink, RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
import SettingsStore from "../../../settings/SettingsStore";
import {LayoutPropType} from "../../../settings/Layout";

View file

@ -46,12 +46,14 @@ export default class TextWithTooltip extends React.Component {
render() {
const Tooltip = sdk.getComponent("elements.Tooltip");
const {class: className, children, tooltip, tooltipClass, ...props} = this.props;
return (
<span onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className={this.props.class}>
{this.props.children}
<span {...props} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className={className}>
{children}
{this.state.hover && <Tooltip
label={this.props.tooltip}
tooltipClassName={this.props.tooltipClass}
label={tooltip}
tooltipClassName={tooltipClass}
className={"mx_TextWithTooltip_tooltip"} /> }
</span>
);

View file

@ -19,7 +19,7 @@ import PropTypes from 'prop-types';
import * as HtmlUtils from '../../../HtmlUtils';
import { editBodyDiffToHtml } from '../../../utils/MessageDiffUtils';
import {formatTime} from '../../../DateUtils';
import {MatrixEvent} from 'matrix-js-sdk';
import {MatrixEvent} from 'matrix-js-sdk/src/models/event';
import {pillifyLinks, unmountPills} from '../../../utils/pillify';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';

View file

@ -18,7 +18,7 @@ limitations under the License.
import React, {useEffect} from 'react';
import PropTypes from 'prop-types';
import { EventStatus } from 'matrix-js-sdk';
import { EventStatus } from 'matrix-js-sdk/src/models/event';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';

View file

@ -16,7 +16,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {MatrixEvent} from 'matrix-js-sdk';
import {MatrixEvent} from 'matrix-js-sdk/src/models/event';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';

View file

@ -27,7 +27,7 @@ import {parseEvent} from '../../../editor/deserialize';
import {PartCreator} from '../../../editor/parts';
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
import classNames from 'classnames';
import {EventStatus} from 'matrix-js-sdk';
import {EventStatus} from 'matrix-js-sdk/src/models/event';
import BasicMessageComposer from "./BasicMessageComposer";
import {Key, isOnlyCtrlOrCmdKeyEvent} from "../../../Keyboard";
import MatrixClientContext from "../../../contexts/MatrixClientContext";

View file

@ -28,7 +28,7 @@ import * as sdk from "../../../index";
import dis from '../../../dispatcher/dispatcher';
import SettingsStore from "../../../settings/SettingsStore";
import {Layout, LayoutPropType} from "../../../settings/Layout";
import {EventStatus} from 'matrix-js-sdk';
import {EventStatus} from 'matrix-js-sdk/src/models/event';
import {formatTime} from "../../../DateUtils";
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {ALL_RULE_TYPES} from "../../../mjolnir/BanList";

View file

@ -85,8 +85,8 @@ class FormatButton extends React.PureComponent {
return (
<AccessibleTooltipButton
as="span"
role="button"
element="button"
type="button"
onClick={this.props.onClick}
title={this.props.label}
tooltip={tooltip}

View file

@ -333,6 +333,17 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
this.setState({generalMenuPosition: null}); // hide the menu
};
private onInviteClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
dis.dispatch({
action: 'view_invite',
roomId: this.props.room.roomId,
});
this.setState({generalMenuPosition: null}); // hide the menu
};
private async saveNotifState(ev: ButtonEvent, newState: Volume) {
ev.preventDefault();
ev.stopPropagation();
@ -453,6 +464,8 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
const isLowPriority = roomTags.includes(DefaultTagID.LowPriority);
const lowPriorityLabel = _t("Low Priority");
const userId = MatrixClientPeg.get().getUserId();
const canInvite = this.props.room.canInvite(userId);
contextMenu = <IconizedContextMenu
{...contextMenuBelow(this.state.generalMenuPosition)}
onFinished={this.onCloseGeneralMenu}
@ -472,7 +485,13 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
label={lowPriorityLabel}
iconClassName="mx_RoomTile_iconArrowDown"
/>
{canInvite ? (
<IconizedContextMenuOption
onClick={this.onInviteClick}
label={_t("Invite People")}
iconClassName="mx_RoomTile_iconInvite"
/>
) : null}
<IconizedContextMenuOption
onClick={this.onOpenRoomSettings}
label={_t("Settings")}

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {MatrixEvent} from "matrix-js-sdk";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import {_t} from "../../../languageHandler";
import dis from "../../../dispatcher/dispatcher";
import * as sdk from "../../../index";

View file

@ -32,7 +32,7 @@ import * as sdk from "../../../../..";
import Modal from "../../../../../Modal";
import dis from "../../../../../dispatcher/dispatcher";
import {Service, startTermsFlow} from "../../../../../Terms";
import {SERVICE_TYPES} from "matrix-js-sdk";
import {SERVICE_TYPES} from "matrix-js-sdk/src/service-types";
import IdentityAuthClient from "../../../../../IdentityAuthClient";
import {abbreviateUrl} from "../../../../../utils/UrlUtils";
import { getThreepidsWithBindStatus } from '../../../../../boundThreepids';

View file

@ -84,6 +84,7 @@ export default class VoiceUserSettingsTab extends React.Component {
}
}
if (error) {
console.log("Failed to list userMedia devices", error);
const brand = SdkConfig.get().brand;
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
Modal.createTrackedDialog('No media permissions', '', ErrorDialog, {

View file

@ -108,7 +108,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
body = <React.Fragment>
<h2>{ _t("Create a space") }</h2>
<p>{ _t("Spaces are new ways to group rooms and people. " +
"To join an existing space youll need an invite") }</p>
"To join an existing space you'll need an invite.") }</p>
<SpaceCreateMenuType
title={_t("Public")}
@ -140,9 +140,9 @@ const SpaceCreateMenu = ({ onFinished }) => {
</h2>
<p>
{
_t("Give it a photo, name and description to help you identify it.")
_t("Add some details to help people recognise it.")
} {
_t("You can change these at any point.")
_t("You can change these anytime.")
}
</p>

View file

@ -220,13 +220,19 @@ const SpacePanel = () => {
<SpaceButton
className={newClasses}
tooltip={menuDisplayed ? _t("Cancel") : _t("Create a space")}
onClick={menuDisplayed ? closeMenu : openMenu}
onClick={menuDisplayed ? closeMenu : () => {
openMenu();
if (!isPanelCollapsed) setPanelCollapsed(true);
}}
isNarrow={isPanelCollapsed}
/>
</AutoHideScrollbar>
<AccessibleTooltipButton
className={classNames("mx_SpacePanel_toggleCollapse", {expanded: !isPanelCollapsed})}
onClick={evt => setPanelCollapsed(!isPanelCollapsed)}
onClick={() => {
setPanelCollapsed(!isPanelCollapsed);
if (menuDisplayed) closeMenu();
}}
title={expandCollapseButtonTitle}
/>
{ contextMenu }

View file

@ -41,13 +41,13 @@ const SpacePublicShare = ({ space, onFinished }: IProps) => {
const success = await copyPlaintext(permalinkCreator.forRoom());
const text = success ? _t("Copied!") : _t("Failed to copy");
setCopiedText(text);
await sleep(10);
await sleep(5000);
if (copiedText === text) { // if the text hasn't changed by another click then clear it after some time
setCopiedText(_t("Click to copy"));
}
}}
>
{ _t("Share invite link") }
<h3>{ _t("Share invite link") }</h3>
<span>{ copiedText }</span>
</AccessibleButton>
<AccessibleButton
@ -57,7 +57,8 @@ const SpacePublicShare = ({ space, onFinished }: IProps) => {
onFinished();
}}
>
{ _t("Invite by email or username") }
<h3>{ _t("Invite people") }</h3>
<span>{ _t("Invite with email or username") }</span>
</AccessibleButton>
</div>;
};

View file

@ -51,6 +51,7 @@ interface IItemProps {
isNested?: boolean;
isPanelCollapsed?: boolean;
onExpand?: Function;
parents?: Set<string>;
}
interface IItemState {
@ -299,7 +300,8 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
const isNarrow = this.props.isPanelCollapsed;
const collapsed = this.state.collapsed || forceCollapsed;
const childSpaces = SpaceStore.instance.getChildSpaces(space.roomId);
const childSpaces = SpaceStore.instance.getChildSpaces(space.roomId)
.filter(s => !this.props.parents?.has(s.roomId));
const isActive = activeSpaces.includes(space);
const itemClasses = classNames({
"mx_SpaceItem": true,
@ -312,11 +314,17 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
mx_SpaceButton_narrow: isNarrow,
});
const notificationState = SpaceStore.instance.getNotificationState(space.roomId);
const childItems = childSpaces && !collapsed ? <SpaceTreeLevel
spaces={childSpaces}
activeSpaces={activeSpaces}
isNested={true}
/> : null;
let childItems;
if (childSpaces && !collapsed) {
childItems = <SpaceTreeLevel
spaces={childSpaces}
activeSpaces={activeSpaces}
isNested={true}
parents={new Set(this.props.parents).add(this.props.space.roomId)}
/>;
}
let notifBadge;
if (notificationState) {
notifBadge = <div className="mx_SpacePanel_badgeContainer">
@ -383,12 +391,14 @@ interface ITreeLevelProps {
spaces: Room[];
activeSpaces: Room[];
isNested?: boolean;
parents: Set<string>;
}
const SpaceTreeLevel: React.FC<ITreeLevelProps> = ({
spaces,
activeSpaces,
isNested,
parents,
}) => {
return <ul className="mx_SpaceTreeLevel">
{spaces.map(s => {
@ -397,6 +407,7 @@ const SpaceTreeLevel: React.FC<ITreeLevelProps> = ({
activeSpaces={activeSpaces}
space={s}
isNested={isNested}
parents={parents}
/>);
})}
</ul>;