You've already forked matrix-react-sdk
							
							
				mirror of
				https://github.com/matrix-org/matrix-react-sdk.git
				synced 2025-11-04 11:51:45 +03:00 
			
		
		
		
	Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/ts/12
This commit is contained in:
		@@ -27,9 +27,15 @@ export enum CallEventGrouperEvent {
 | 
			
		||||
    SilencedChanged = "silenced_changed",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const CONNECTING_STATES = [
 | 
			
		||||
    CallState.Connecting,
 | 
			
		||||
    CallState.WaitLocalMedia,
 | 
			
		||||
    CallState.CreateOffer,
 | 
			
		||||
    CallState.CreateAnswer,
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const SUPPORTED_STATES = [
 | 
			
		||||
    CallState.Connected,
 | 
			
		||||
    CallState.Connecting,
 | 
			
		||||
    CallState.Ringing,
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@@ -61,6 +67,10 @@ export default class CallEventGrouper extends EventEmitter {
 | 
			
		||||
        return [...this.events].find((event) => event.getType() === EventType.CallReject);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private get selectAnswer(): MatrixEvent {
 | 
			
		||||
        return [...this.events].find((event) => event.getType() === EventType.CallSelectAnswer);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public get isVoice(): boolean {
 | 
			
		||||
        const invite = this.invite;
 | 
			
		||||
        if (!invite) return;
 | 
			
		||||
@@ -74,6 +84,19 @@ export default class CallEventGrouper extends EventEmitter {
 | 
			
		||||
        return this.hangup?.getContent()?.reason;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public get rejectParty(): string {
 | 
			
		||||
        return this.reject?.getSender();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public get gotRejected(): boolean {
 | 
			
		||||
        return Boolean(this.reject);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public get duration(): Date {
 | 
			
		||||
        if (!this.hangup || !this.selectAnswer) return;
 | 
			
		||||
        return new Date(this.hangup.getDate().getTime() - this.selectAnswer.getDate().getTime());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns true if there are only events from the other side - we missed the call
 | 
			
		||||
     */
 | 
			
		||||
@@ -119,7 +142,9 @@ export default class CallEventGrouper extends EventEmitter {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private setState = () => {
 | 
			
		||||
        if (SUPPORTED_STATES.includes(this.call?.state)) {
 | 
			
		||||
        if (CONNECTING_STATES.includes(this.call?.state)) {
 | 
			
		||||
            this.state = CallState.Connecting;
 | 
			
		||||
        } else if (SUPPORTED_STATES.includes(this.call?.state)) {
 | 
			
		||||
            this.state = this.call.state;
 | 
			
		||||
        } else {
 | 
			
		||||
            if (this.callWasMissed) this.state = CustomCallState.Missed;
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
 | 
			
		||||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
import React, { CSSProperties, RefObject, useRef, useState } from "react";
 | 
			
		||||
import React, { CSSProperties, RefObject, SyntheticEvent, useRef, useState } from "react";
 | 
			
		||||
import ReactDOM from "react-dom";
 | 
			
		||||
import classNames from "classnames";
 | 
			
		||||
 | 
			
		||||
@@ -80,6 +80,10 @@ export interface IProps extends IPosition {
 | 
			
		||||
    managed?: boolean;
 | 
			
		||||
    wrapperClassName?: string;
 | 
			
		||||
 | 
			
		||||
    // If true, this context menu will be mounted as a child to the parent container. Otherwise
 | 
			
		||||
    // it will be mounted to a container at the root of the DOM.
 | 
			
		||||
    mountAsChild?: boolean;
 | 
			
		||||
 | 
			
		||||
    // Function to be called on menu close
 | 
			
		||||
    onFinished();
 | 
			
		||||
    // on resize callback
 | 
			
		||||
@@ -390,7 +394,13 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(): React.ReactChild {
 | 
			
		||||
        return ReactDOM.createPortal(this.renderMenu(), getOrCreateContainer());
 | 
			
		||||
        if (this.props.mountAsChild) {
 | 
			
		||||
            // Render as a child of the current parent
 | 
			
		||||
            return this.renderMenu();
 | 
			
		||||
        } else {
 | 
			
		||||
            // Render as a child of a container at the root of the DOM
 | 
			
		||||
            return ReactDOM.createPortal(this.renderMenu(), getOrCreateContainer());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -461,10 +471,14 @@ type ContextMenuTuple<T> = [boolean, RefObject<T>, () => void, () => void, (val:
 | 
			
		||||
export const useContextMenu = <T extends any = HTMLElement>(): ContextMenuTuple<T> => {
 | 
			
		||||
    const button = useRef<T>(null);
 | 
			
		||||
    const [isOpen, setIsOpen] = useState(false);
 | 
			
		||||
    const open = () => {
 | 
			
		||||
    const open = (ev?: SyntheticEvent) => {
 | 
			
		||||
        ev?.preventDefault();
 | 
			
		||||
        ev?.stopPropagation();
 | 
			
		||||
        setIsOpen(true);
 | 
			
		||||
    };
 | 
			
		||||
    const close = () => {
 | 
			
		||||
    const close = (ev?: SyntheticEvent) => {
 | 
			
		||||
        ev?.preventDefault();
 | 
			
		||||
        ev?.stopPropagation();
 | 
			
		||||
        setIsOpen(false);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -107,6 +107,7 @@ import UIStore, { UI_EVENTS } from "../../stores/UIStore";
 | 
			
		||||
import SoftLogout from './auth/SoftLogout';
 | 
			
		||||
import { makeRoomPermalink } from "../../utils/permalinks/Permalinks";
 | 
			
		||||
import { copyPlaintext } from "../../utils/strings";
 | 
			
		||||
import { PosthogAnalytics } from '../../PosthogAnalytics';
 | 
			
		||||
 | 
			
		||||
/** constants for MatrixChat.state.view */
 | 
			
		||||
export enum Views {
 | 
			
		||||
@@ -387,6 +388,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
 | 
			
		||||
        if (SettingsStore.getValue("analyticsOptIn")) {
 | 
			
		||||
            Analytics.enable();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        PosthogAnalytics.instance.updateAnonymityFromSettings();
 | 
			
		||||
        PosthogAnalytics.instance.updatePlatformSuperProperties();
 | 
			
		||||
 | 
			
		||||
        CountlyAnalytics.instance.enable(/* anonymous = */ true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -443,6 +448,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
 | 
			
		||||
            const durationMs = this.stopPageChangeTimer();
 | 
			
		||||
            Analytics.trackPageChange(durationMs);
 | 
			
		||||
            CountlyAnalytics.instance.trackPageChange(durationMs);
 | 
			
		||||
            PosthogAnalytics.instance.trackPageView(durationMs);
 | 
			
		||||
        }
 | 
			
		||||
        if (this.focusComposer) {
 | 
			
		||||
            dis.fire(Action.FocusSendMessageComposer);
 | 
			
		||||
 
 | 
			
		||||
@@ -51,7 +51,12 @@ import EditorStateTransfer from "../../utils/EditorStateTransfer";
 | 
			
		||||
 | 
			
		||||
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
 | 
			
		||||
const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
 | 
			
		||||
const membershipTypes = [EventType.RoomMember, EventType.RoomThirdPartyInvite, EventType.RoomServerAcl];
 | 
			
		||||
const groupedEvents = [
 | 
			
		||||
    EventType.RoomMember,
 | 
			
		||||
    EventType.RoomThirdPartyInvite,
 | 
			
		||||
    EventType.RoomServerAcl,
 | 
			
		||||
    EventType.RoomPinnedEvents,
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
// check if there is a previous event and it has the same sender as this event
 | 
			
		||||
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
 | 
			
		||||
@@ -618,7 +623,15 @@ export default class MessagePanel extends React.Component<IProps, IState> {
 | 
			
		||||
 | 
			
		||||
            for (const Grouper of groupers) {
 | 
			
		||||
                if (Grouper.canStartGroup(this, mxEv)) {
 | 
			
		||||
                    grouper = new Grouper(this, mxEv, prevEvent, lastShownEvent, nextEvent, nextTile);
 | 
			
		||||
                    grouper = new Grouper(
 | 
			
		||||
                        this,
 | 
			
		||||
                        mxEv,
 | 
			
		||||
                        prevEvent,
 | 
			
		||||
                        lastShownEvent,
 | 
			
		||||
                        this.props.layout,
 | 
			
		||||
                        nextEvent,
 | 
			
		||||
                        nextTile,
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            if (!grouper) {
 | 
			
		||||
@@ -981,6 +994,7 @@ abstract class BaseGrouper {
 | 
			
		||||
        public readonly event: MatrixEvent,
 | 
			
		||||
        public readonly prevEvent: MatrixEvent,
 | 
			
		||||
        public readonly lastShownEvent: MatrixEvent,
 | 
			
		||||
        protected readonly layout: Layout,
 | 
			
		||||
        public readonly nextEvent?: MatrixEvent,
 | 
			
		||||
        public readonly nextEventTile?: MatrixEvent,
 | 
			
		||||
    ) {
 | 
			
		||||
@@ -1107,6 +1121,7 @@ class CreationGrouper extends BaseGrouper {
 | 
			
		||||
                onToggle={panel.onHeightChanged} // Update scroll state
 | 
			
		||||
                summaryMembers={[ev.sender]}
 | 
			
		||||
                summaryText={summaryText}
 | 
			
		||||
                layout={this.layout}
 | 
			
		||||
            >
 | 
			
		||||
                { eventTiles }
 | 
			
		||||
            </EventListSummary>,
 | 
			
		||||
@@ -1134,10 +1149,11 @@ class RedactionGrouper extends BaseGrouper {
 | 
			
		||||
        ev: MatrixEvent,
 | 
			
		||||
        prevEvent: MatrixEvent,
 | 
			
		||||
        lastShownEvent: MatrixEvent,
 | 
			
		||||
        layout: Layout,
 | 
			
		||||
        nextEvent: MatrixEvent,
 | 
			
		||||
        nextEventTile: MatrixEvent,
 | 
			
		||||
    ) {
 | 
			
		||||
        super(panel, ev, prevEvent, lastShownEvent, nextEvent, nextEventTile);
 | 
			
		||||
        super(panel, ev, prevEvent, lastShownEvent, layout, nextEvent, nextEventTile);
 | 
			
		||||
        this.events = [ev];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -1202,6 +1218,7 @@ class RedactionGrouper extends BaseGrouper {
 | 
			
		||||
                onToggle={panel.onHeightChanged} // Update scroll state
 | 
			
		||||
                summaryMembers={Array.from(senders)}
 | 
			
		||||
                summaryText={_t("%(count)s messages deleted.", { count: eventTiles.length })}
 | 
			
		||||
                layout={this.layout}
 | 
			
		||||
            >
 | 
			
		||||
                { eventTiles }
 | 
			
		||||
            </EventListSummary>,
 | 
			
		||||
@@ -1222,7 +1239,7 @@ class RedactionGrouper extends BaseGrouper {
 | 
			
		||||
// Wrap consecutive member events in a ListSummary, ignore if redacted
 | 
			
		||||
class MemberGrouper extends BaseGrouper {
 | 
			
		||||
    static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean {
 | 
			
		||||
        return panel.shouldShowEvent(ev) && membershipTypes.includes(ev.getType() as EventType);
 | 
			
		||||
        return panel.shouldShowEvent(ev) && groupedEvents.includes(ev.getType() as EventType);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
@@ -1230,8 +1247,9 @@ class MemberGrouper extends BaseGrouper {
 | 
			
		||||
        public readonly event: MatrixEvent,
 | 
			
		||||
        public readonly prevEvent: MatrixEvent,
 | 
			
		||||
        public readonly lastShownEvent: MatrixEvent,
 | 
			
		||||
        protected readonly layout: Layout,
 | 
			
		||||
    ) {
 | 
			
		||||
        super(panel, event, prevEvent, lastShownEvent);
 | 
			
		||||
        super(panel, event, prevEvent, lastShownEvent, layout);
 | 
			
		||||
        this.events = [event];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -1239,7 +1257,7 @@ class MemberGrouper extends BaseGrouper {
 | 
			
		||||
        if (this.panel.wantsDateSeparator(this.events[0], ev.getDate())) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        return membershipTypes.includes(ev.getType() as EventType);
 | 
			
		||||
        return groupedEvents.includes(ev.getType() as EventType);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public add(ev: MatrixEvent, showHiddenEvents?: boolean): void {
 | 
			
		||||
@@ -1306,6 +1324,7 @@ class MemberGrouper extends BaseGrouper {
 | 
			
		||||
                events={this.events}
 | 
			
		||||
                onToggle={panel.onHeightChanged} // Update scroll state
 | 
			
		||||
                startExpanded={highlightInMels}
 | 
			
		||||
                layout={this.layout}
 | 
			
		||||
            >
 | 
			
		||||
                { eventTiles }
 | 
			
		||||
            </MemberEventListSummary>,
 | 
			
		||||
 
 | 
			
		||||
@@ -183,8 +183,14 @@ export default class ScrollPanel extends React.Component<IProps> {
 | 
			
		||||
    private readonly itemlist = createRef<HTMLOListElement>();
 | 
			
		||||
    private unmounted = false;
 | 
			
		||||
    private scrollTimeout: Timer;
 | 
			
		||||
    // Are we currently trying to backfill?
 | 
			
		||||
    private isFilling: boolean;
 | 
			
		||||
    // Is the current fill request caused by a props update?
 | 
			
		||||
    private isFillingDueToPropsUpdate = false;
 | 
			
		||||
    // Did another request to check the fill state arrive while we were trying to backfill?
 | 
			
		||||
    private fillRequestWhileRunning: boolean;
 | 
			
		||||
    // Is that next fill request scheduled because of a props update?
 | 
			
		||||
    private pendingFillDueToPropsUpdate: boolean;
 | 
			
		||||
    private scrollState: IScrollState;
 | 
			
		||||
    private preventShrinkingState: IPreventShrinkingState;
 | 
			
		||||
    private unfillDebouncer: number;
 | 
			
		||||
@@ -213,7 +219,7 @@ export default class ScrollPanel extends React.Component<IProps> {
 | 
			
		||||
        // adding events to the top).
 | 
			
		||||
        //
 | 
			
		||||
        // This will also re-check the fill state, in case the paginate was inadequate
 | 
			
		||||
        this.checkScroll();
 | 
			
		||||
        this.checkScroll(true);
 | 
			
		||||
        this.updatePreventShrinking();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -251,12 +257,12 @@ export default class ScrollPanel extends React.Component<IProps> {
 | 
			
		||||
 | 
			
		||||
    // after an update to the contents of the panel, check that the scroll is
 | 
			
		||||
    // where it ought to be, and set off pagination requests if necessary.
 | 
			
		||||
    public checkScroll = () => {
 | 
			
		||||
    public checkScroll = (isFromPropsUpdate = false) => {
 | 
			
		||||
        if (this.unmounted) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        this.restoreSavedScrollState();
 | 
			
		||||
        this.checkFillState();
 | 
			
		||||
        this.checkFillState(0, isFromPropsUpdate);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // return true if the content is fully scrolled down right now; else false.
 | 
			
		||||
@@ -319,7 +325,7 @@ export default class ScrollPanel extends React.Component<IProps> {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // check the scroll state and send out backfill requests if necessary.
 | 
			
		||||
    public checkFillState = async (depth = 0): Promise<void> => {
 | 
			
		||||
    public checkFillState = async (depth = 0, isFromPropsUpdate = false): Promise<void> => {
 | 
			
		||||
        if (this.unmounted) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
@@ -355,14 +361,20 @@ export default class ScrollPanel extends React.Component<IProps> {
 | 
			
		||||
        // don't allow more than 1 chain of calls concurrently
 | 
			
		||||
        // do make a note when a new request comes in while already running one,
 | 
			
		||||
        // so we can trigger a new chain of calls once done.
 | 
			
		||||
        // However, we make an exception for when we're already filling due to a
 | 
			
		||||
        // props (or children) update, because very often the children include
 | 
			
		||||
        // spinners to say whether we're paginating or not, so this would cause
 | 
			
		||||
        // infinite paginating.
 | 
			
		||||
        if (isFirstCall) {
 | 
			
		||||
            if (this.isFilling) {
 | 
			
		||||
            if (this.isFilling && !this.isFillingDueToPropsUpdate) {
 | 
			
		||||
                debuglog("isFilling: not entering while request is ongoing, marking for a subsequent request");
 | 
			
		||||
                this.fillRequestWhileRunning = true;
 | 
			
		||||
                this.pendingFillDueToPropsUpdate = isFromPropsUpdate;
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            debuglog("isFilling: setting");
 | 
			
		||||
            this.isFilling = true;
 | 
			
		||||
            this.isFillingDueToPropsUpdate = isFromPropsUpdate;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const itemlist = this.itemlist.current;
 | 
			
		||||
@@ -393,11 +405,14 @@ export default class ScrollPanel extends React.Component<IProps> {
 | 
			
		||||
        if (isFirstCall) {
 | 
			
		||||
            debuglog("isFilling: clearing");
 | 
			
		||||
            this.isFilling = false;
 | 
			
		||||
            this.isFillingDueToPropsUpdate = false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.fillRequestWhileRunning) {
 | 
			
		||||
            const refillDueToPropsUpdate = this.pendingFillDueToPropsUpdate;
 | 
			
		||||
            this.fillRequestWhileRunning = false;
 | 
			
		||||
            this.checkFillState();
 | 
			
		||||
            this.pendingFillDueToPropsUpdate = false;
 | 
			
		||||
            this.checkFillState(0, refillDueToPropsUpdate);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,6 @@ limitations under the License.
 | 
			
		||||
 | 
			
		||||
import React, { ReactNode, useMemo, useState } from "react";
 | 
			
		||||
import { Room } from "matrix-js-sdk/src/models/room";
 | 
			
		||||
import { MatrixClient } from "matrix-js-sdk/src/client";
 | 
			
		||||
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
 | 
			
		||||
import { ISpaceSummaryRoom, ISpaceSummaryEvent } from "matrix-js-sdk/src/@types/spaces";
 | 
			
		||||
import classNames from "classnames";
 | 
			
		||||
@@ -44,11 +43,13 @@ import { getChildOrder } from "../../stores/SpaceStore";
 | 
			
		||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
 | 
			
		||||
import { linkifyElement } from "../../HtmlUtils";
 | 
			
		||||
import { getDisplayAliasForAliasSet } from "../../Rooms";
 | 
			
		||||
import { useDispatcher } from "../../hooks/useDispatcher";
 | 
			
		||||
import defaultDispatcher from "../../dispatcher/dispatcher";
 | 
			
		||||
import { Action } from "../../dispatcher/actions";
 | 
			
		||||
 | 
			
		||||
interface IHierarchyProps {
 | 
			
		||||
    space: Room;
 | 
			
		||||
    initialText?: string;
 | 
			
		||||
    refreshToken?: any;
 | 
			
		||||
    additionalButtons?: ReactNode;
 | 
			
		||||
    showRoom(room: ISpaceSummaryRoom, viaServers?: string[], autoJoin?: boolean): void;
 | 
			
		||||
}
 | 
			
		||||
@@ -315,18 +316,25 @@ export const HierarchyLevel = ({
 | 
			
		||||
    </React.Fragment>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// mutate argument refreshToken to force a reload
 | 
			
		||||
export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: any): [
 | 
			
		||||
export const useSpaceSummary = (space: Room): [
 | 
			
		||||
    null,
 | 
			
		||||
    ISpaceSummaryRoom[],
 | 
			
		||||
    Map<string, Map<string, ISpaceSummaryEvent>>?,
 | 
			
		||||
    Map<string, Set<string>>?,
 | 
			
		||||
    Map<string, Set<string>>?,
 | 
			
		||||
] | [Error] => {
 | 
			
		||||
    // crude temporary refresh token approach until we have pagination and rework the data flow here
 | 
			
		||||
    const [refreshToken, setRefreshToken] = useState(0);
 | 
			
		||||
    useDispatcher(defaultDispatcher, (payload => {
 | 
			
		||||
        if (payload.action === Action.UpdateSpaceHierarchy) {
 | 
			
		||||
            setRefreshToken(t => t + 1);
 | 
			
		||||
        }
 | 
			
		||||
    }));
 | 
			
		||||
 | 
			
		||||
    // TODO pagination
 | 
			
		||||
    return useAsyncMemo(async () => {
 | 
			
		||||
        try {
 | 
			
		||||
            const data = await cli.getSpaceSummary(space.roomId);
 | 
			
		||||
            const data = await space.client.getSpaceSummary(space.roomId);
 | 
			
		||||
 | 
			
		||||
            const parentChildRelations = new EnhancedMap<string, Map<string, ISpaceSummaryEvent>>();
 | 
			
		||||
            const childParentRelations = new EnhancedMap<string, Set<string>>();
 | 
			
		||||
@@ -354,7 +362,6 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
 | 
			
		||||
    space,
 | 
			
		||||
    initialText = "",
 | 
			
		||||
    showRoom,
 | 
			
		||||
    refreshToken,
 | 
			
		||||
    additionalButtons,
 | 
			
		||||
    children,
 | 
			
		||||
}) => {
 | 
			
		||||
@@ -364,7 +371,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
 | 
			
		||||
 | 
			
		||||
    const [selected, setSelected] = useState(new Map<string, Set<string>>()); // Map<parentId, Set<childId>>
 | 
			
		||||
 | 
			
		||||
    const [summaryError, rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(cli, space, refreshToken);
 | 
			
		||||
    const [summaryError, rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(space);
 | 
			
		||||
 | 
			
		||||
    const roomsMap = useMemo(() => {
 | 
			
		||||
        if (!rooms) return null;
 | 
			
		||||
 
 | 
			
		||||
@@ -47,13 +47,23 @@ import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
 | 
			
		||||
import { SetRightPanelPhasePayload } from "../../dispatcher/payloads/SetRightPanelPhasePayload";
 | 
			
		||||
import { useStateArray } from "../../hooks/useStateArray";
 | 
			
		||||
import SpacePublicShare from "../views/spaces/SpacePublicShare";
 | 
			
		||||
import { shouldShowSpaceSettings, showAddExistingRooms, showCreateNewRoom, showSpaceSettings } from "../../utils/space";
 | 
			
		||||
import {
 | 
			
		||||
    shouldShowSpaceSettings,
 | 
			
		||||
    showAddExistingRooms,
 | 
			
		||||
    showCreateNewRoom,
 | 
			
		||||
    showCreateNewSubspace,
 | 
			
		||||
    showSpaceSettings,
 | 
			
		||||
} from "../../utils/space";
 | 
			
		||||
import { showRoom, SpaceHierarchy } from "./SpaceRoomDirectory";
 | 
			
		||||
import MemberAvatar from "../views/avatars/MemberAvatar";
 | 
			
		||||
import { useStateToggle } from "../../hooks/useStateToggle";
 | 
			
		||||
import SpaceStore from "../../stores/SpaceStore";
 | 
			
		||||
import FacePile from "../views/elements/FacePile";
 | 
			
		||||
import { AddExistingToSpace } from "../views/dialogs/AddExistingToSpaceDialog";
 | 
			
		||||
import {
 | 
			
		||||
    AddExistingToSpace,
 | 
			
		||||
    defaultDmsRenderer,
 | 
			
		||||
    defaultRoomsRenderer,
 | 
			
		||||
    defaultSpacesRenderer,
 | 
			
		||||
} from "../views/dialogs/AddExistingToSpaceDialog";
 | 
			
		||||
import { ChevronFace, ContextMenuButton, useContextMenu } from "./ContextMenu";
 | 
			
		||||
import IconizedContextMenu, {
 | 
			
		||||
    IconizedContextMenuOption,
 | 
			
		||||
@@ -62,10 +72,8 @@ import IconizedContextMenu, {
 | 
			
		||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
 | 
			
		||||
import { BetaPill } from "../views/beta/BetaCard";
 | 
			
		||||
import { UserTab } from "../views/dialogs/UserSettingsDialog";
 | 
			
		||||
import Modal from "../../Modal";
 | 
			
		||||
import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog";
 | 
			
		||||
import SdkConfig from "../../SdkConfig";
 | 
			
		||||
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
 | 
			
		||||
import { SpaceFeedbackPrompt } from "../views/spaces/SpaceCreateMenu";
 | 
			
		||||
 | 
			
		||||
interface IProps {
 | 
			
		||||
    space: Room;
 | 
			
		||||
@@ -92,28 +100,6 @@ enum Phase {
 | 
			
		||||
    PrivateExistingRooms,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// XXX: Temporary for the Spaces Beta only
 | 
			
		||||
export const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => {
 | 
			
		||||
    if (!SdkConfig.get().bug_report_endpoint_url) return null;
 | 
			
		||||
 | 
			
		||||
    return <div className="mx_SpaceFeedbackPrompt">
 | 
			
		||||
        <hr />
 | 
			
		||||
        <div>
 | 
			
		||||
            <span className="mx_SpaceFeedbackPrompt_text">{ _t("Spaces are a beta feature.") }</span>
 | 
			
		||||
            <AccessibleButton
 | 
			
		||||
                kind="link"
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                    if (onClick) onClick();
 | 
			
		||||
                    Modal.createTrackedDialog("Beta Feedback", "feature_spaces", BetaFeedbackDialog, {
 | 
			
		||||
                    featureId: "feature_spaces",
 | 
			
		||||
                    });
 | 
			
		||||
                }}>
 | 
			
		||||
                { _t("Feedback") }
 | 
			
		||||
            </AccessibleButton>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const RoomMemberCount = ({ room, children }) => {
 | 
			
		||||
    const members = useRoomMembers(room);
 | 
			
		||||
    const count = members.length;
 | 
			
		||||
@@ -206,11 +192,11 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
 | 
			
		||||
 | 
			
		||||
        if (inviteSender) {
 | 
			
		||||
            inviterSection = <div className="mx_SpaceRoomView_preview_inviter">
 | 
			
		||||
                <MemberAvatar member={inviter} width={32} height={32} />
 | 
			
		||||
                <MemberAvatar member={inviter} fallbackUserId={inviteSender} width={32} height={32} />
 | 
			
		||||
                <div>
 | 
			
		||||
                    <div className="mx_SpaceRoomView_preview_inviter_name">
 | 
			
		||||
                        { _t("<inviter/> invites you", {}, {
 | 
			
		||||
                            inviter: () => <b>{ inviter.name || inviteSender }</b>,
 | 
			
		||||
                            inviter: () => <b>{ inviter?.name || inviteSender }</b>,
 | 
			
		||||
                        }) }
 | 
			
		||||
                    </div>
 | 
			
		||||
                    { inviter ? <div className="mx_SpaceRoomView_preview_inviter_mxid">
 | 
			
		||||
@@ -307,7 +293,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
 | 
			
		||||
    </div>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
 | 
			
		||||
const SpaceLandingAddButton = ({ space }) => {
 | 
			
		||||
    const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
 | 
			
		||||
 | 
			
		||||
    let contextMenu;
 | 
			
		||||
@@ -331,24 +317,32 @@ const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
 | 
			
		||||
                        closeMenu();
 | 
			
		||||
 | 
			
		||||
                        if (await showCreateNewRoom(space)) {
 | 
			
		||||
                            onNewRoomAdded();
 | 
			
		||||
                            defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
 | 
			
		||||
                        }
 | 
			
		||||
                    }}
 | 
			
		||||
                />
 | 
			
		||||
                <IconizedContextMenuOption
 | 
			
		||||
                    label={_t("Add existing room")}
 | 
			
		||||
                    iconClassName="mx_RoomList_iconHash"
 | 
			
		||||
                    onClick={async (e) => {
 | 
			
		||||
                    onClick={(e) => {
 | 
			
		||||
                        e.preventDefault();
 | 
			
		||||
                        e.stopPropagation();
 | 
			
		||||
                        closeMenu();
 | 
			
		||||
 | 
			
		||||
                        const [added] = await showAddExistingRooms(space);
 | 
			
		||||
                        if (added) {
 | 
			
		||||
                            onNewRoomAdded();
 | 
			
		||||
                        }
 | 
			
		||||
                        showAddExistingRooms(space);
 | 
			
		||||
                    }}
 | 
			
		||||
                />
 | 
			
		||||
                <IconizedContextMenuOption
 | 
			
		||||
                    label={_t("Add space")}
 | 
			
		||||
                    iconClassName="mx_RoomList_iconPlus"
 | 
			
		||||
                    onClick={(e) => {
 | 
			
		||||
                        e.preventDefault();
 | 
			
		||||
                        e.stopPropagation();
 | 
			
		||||
                        closeMenu();
 | 
			
		||||
                        showCreateNewSubspace(space);
 | 
			
		||||
                    }}
 | 
			
		||||
                >
 | 
			
		||||
                    <BetaPill />
 | 
			
		||||
                </IconizedContextMenuOption>
 | 
			
		||||
            </IconizedContextMenuOptionList>
 | 
			
		||||
        </IconizedContextMenu>;
 | 
			
		||||
    }
 | 
			
		||||
@@ -389,11 +383,9 @@ const SpaceLanding = ({ space }) => {
 | 
			
		||||
 | 
			
		||||
    const canAddRooms = myMembership === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
 | 
			
		||||
 | 
			
		||||
    const [refreshToken, forceUpdate] = useStateToggle(false);
 | 
			
		||||
 | 
			
		||||
    let addRoomButton;
 | 
			
		||||
    if (canAddRooms) {
 | 
			
		||||
        addRoomButton = <SpaceLandingAddButton space={space} onNewRoomAdded={forceUpdate} />;
 | 
			
		||||
        addRoomButton = <SpaceLandingAddButton space={space} />;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let settingsButton;
 | 
			
		||||
@@ -416,6 +408,7 @@ const SpaceLanding = ({ space }) => {
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return <div className="mx_SpaceRoomView_landing">
 | 
			
		||||
        <SpaceFeedbackPrompt />
 | 
			
		||||
        <RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
 | 
			
		||||
        <div className="mx_SpaceRoomView_landing_name">
 | 
			
		||||
            <RoomName room={space}>
 | 
			
		||||
@@ -440,15 +433,8 @@ const SpaceLanding = ({ space }) => {
 | 
			
		||||
                </div>
 | 
			
		||||
            ) }
 | 
			
		||||
        </RoomTopic>
 | 
			
		||||
        <SpaceFeedbackPrompt />
 | 
			
		||||
        <hr />
 | 
			
		||||
 | 
			
		||||
        <SpaceHierarchy
 | 
			
		||||
            space={space}
 | 
			
		||||
            showRoom={showRoom}
 | 
			
		||||
            refreshToken={refreshToken}
 | 
			
		||||
            additionalButtons={addRoomButton}
 | 
			
		||||
        />
 | 
			
		||||
        <SpaceHierarchy space={space} showRoom={showRoom} additionalButtons={addRoomButton} />
 | 
			
		||||
    </div>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -531,7 +517,6 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
 | 
			
		||||
                value={buttonLabel}
 | 
			
		||||
            />
 | 
			
		||||
        </div>
 | 
			
		||||
        <SpaceFeedbackPrompt />
 | 
			
		||||
    </div>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -550,11 +535,12 @@ const SpaceAddExistingRooms = ({ space, onFinished }) => {
 | 
			
		||||
                    { _t("Skip for now") }
 | 
			
		||||
                </AccessibleButton>
 | 
			
		||||
            }
 | 
			
		||||
            filterPlaceholder={_t("Search for rooms or spaces")}
 | 
			
		||||
            onFinished={onFinished}
 | 
			
		||||
            roomsRenderer={defaultRoomsRenderer}
 | 
			
		||||
            spacesRenderer={defaultSpacesRenderer}
 | 
			
		||||
            dmsRenderer={defaultDmsRenderer}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <div className="mx_SpaceRoomView_buttons" />
 | 
			
		||||
        <SpaceFeedbackPrompt />
 | 
			
		||||
    </div>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -574,7 +560,6 @@ const SpaceSetupPublicShare = ({ justCreatedOpts, space, onFinished, createdRoom
 | 
			
		||||
                { createdRooms ? _t("Go to my first room") : _t("Go to my space") }
 | 
			
		||||
            </AccessibleButton>
 | 
			
		||||
        </div>
 | 
			
		||||
        <SpaceFeedbackPrompt />
 | 
			
		||||
    </div>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -603,9 +588,8 @@ const SpaceSetupPrivateScope = ({ space, justCreatedOpts, onFinished }) => {
 | 
			
		||||
        </AccessibleButton>
 | 
			
		||||
        <div className="mx_SpaceRoomView_betaWarning">
 | 
			
		||||
            <h3>{ _t("Teammates might not be able to view or join any private rooms you make.") }</h3>
 | 
			
		||||
            <p>{ _t("We're working on this as part of the beta, but just want to let you know.") }</p>
 | 
			
		||||
            <p>{ _t("We're working on this, but just want to let you know.") }</p>
 | 
			
		||||
        </div>
 | 
			
		||||
        <SpaceFeedbackPrompt />
 | 
			
		||||
    </div>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -728,7 +712,6 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
 | 
			
		||||
                value={buttonLabel}
 | 
			
		||||
            />
 | 
			
		||||
        </div>
 | 
			
		||||
        <SpaceFeedbackPrompt />
 | 
			
		||||
    </div>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -757,16 +757,20 @@ class TimelinePanel extends React.Component<IProps, IState> {
 | 
			
		||||
            }
 | 
			
		||||
            this.lastRMSentEventId = this.state.readMarkerEventId;
 | 
			
		||||
 | 
			
		||||
            const roomId = this.props.timelineSet.room.roomId;
 | 
			
		||||
            const hiddenRR = SettingsStore.getValue("feature_hidden_read_receipts", roomId);
 | 
			
		||||
 | 
			
		||||
            debuglog('TimelinePanel: Sending Read Markers for ',
 | 
			
		||||
                this.props.timelineSet.room.roomId,
 | 
			
		||||
                'rm', this.state.readMarkerEventId,
 | 
			
		||||
                lastReadEvent ? 'rr ' + lastReadEvent.getId() : '',
 | 
			
		||||
                ' hidden:' + hiddenRR,
 | 
			
		||||
            );
 | 
			
		||||
            MatrixClientPeg.get().setRoomReadMarkers(
 | 
			
		||||
                this.props.timelineSet.room.roomId,
 | 
			
		||||
                roomId,
 | 
			
		||||
                this.state.readMarkerEventId,
 | 
			
		||||
                lastReadEvent, // Could be null, in which case no RR is sent
 | 
			
		||||
                {},
 | 
			
		||||
                { hidden: hiddenRR },
 | 
			
		||||
            ).catch((e) => {
 | 
			
		||||
                // /read_markers API is not implemented on this HS, fallback to just RR
 | 
			
		||||
                if (e.errcode === 'M_UNRECOGNIZED' && lastReadEvent) {
 | 
			
		||||
 
 | 
			
		||||
@@ -58,28 +58,39 @@ export default class ToastContainer extends React.Component<{}, IState> {
 | 
			
		||||
        let containerClasses;
 | 
			
		||||
        if (totalCount !== 0) {
 | 
			
		||||
            const topToast = this.state.toasts[0];
 | 
			
		||||
            const { title, icon, key, component, className, props } = topToast;
 | 
			
		||||
            const toastClasses = classNames("mx_Toast_toast", {
 | 
			
		||||
            const { title, icon, key, component, className, bodyClassName, props } = topToast;
 | 
			
		||||
            const bodyClasses = classNames("mx_Toast_body", bodyClassName);
 | 
			
		||||
            const toastClasses = classNames("mx_Toast_toast", className, {
 | 
			
		||||
                "mx_Toast_hasIcon": icon,
 | 
			
		||||
                [`mx_Toast_icon_${icon}`]: icon,
 | 
			
		||||
            }, className);
 | 
			
		||||
 | 
			
		||||
            let countIndicator;
 | 
			
		||||
            if (isStacked || this.state.countSeen > 0) {
 | 
			
		||||
                countIndicator = ` (${this.state.countSeen + 1}/${this.state.countSeen + totalCount})`;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            });
 | 
			
		||||
            const toastProps = Object.assign({}, props, {
 | 
			
		||||
                key,
 | 
			
		||||
                toastKey: key,
 | 
			
		||||
            });
 | 
			
		||||
            toast = (<div className={toastClasses}>
 | 
			
		||||
                <div className="mx_Toast_title">
 | 
			
		||||
                    <h2>{ title }</h2>
 | 
			
		||||
                    <span>{ countIndicator }</span>
 | 
			
		||||
            const content = React.createElement(component, toastProps);
 | 
			
		||||
 | 
			
		||||
            let countIndicator;
 | 
			
		||||
            if (title && isStacked || this.state.countSeen > 0) {
 | 
			
		||||
                countIndicator = ` (${this.state.countSeen + 1}/${this.state.countSeen + totalCount})`;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let titleElement;
 | 
			
		||||
            if (title) {
 | 
			
		||||
                titleElement = (
 | 
			
		||||
                    <div className="mx_Toast_title">
 | 
			
		||||
                        <h2>{ title }</h2>
 | 
			
		||||
                        <span>{ countIndicator }</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            toast = (
 | 
			
		||||
                <div className={toastClasses}>
 | 
			
		||||
                    { titleElement }
 | 
			
		||||
                    <div className={bodyClasses}>{ content }</div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className="mx_Toast_body">{ React.createElement(component, toastProps) }</div>
 | 
			
		||||
            </div>);
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            containerClasses = classNames("mx_ToastContainer", {
 | 
			
		||||
                "mx_ToastContainer_stacked": isStacked,
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user