Merge branch 'develop' into Discovery
This commit is contained in:
commit
edcd7c4426
86 changed files with 1340 additions and 941 deletions
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 })}
|
||||
/>;
|
||||
}
|
||||
|
|
|
@ -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});
|
||||
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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>}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 />.",
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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")}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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 you’ll 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>
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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>;
|
||||
};
|
||||
|
|
|
@ -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>;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue