You've already forked matrix-react-sdk
							
							
				mirror of
				https://github.com/matrix-org/matrix-react-sdk.git
				synced 2025-11-03 00:33:22 +03:00 
			
		
		
		
	Merge pull request #4735 from matrix-org/travis/room-list/breadcrumbs
Reimplement breadcrumbs for new room list
This commit is contained in:
		@@ -94,6 +94,7 @@
 | 
			
		||||
    "react-dom": "^16.9.0",
 | 
			
		||||
    "react-focus-lock": "^2.2.1",
 | 
			
		||||
    "react-resizable": "^1.10.1",
 | 
			
		||||
    "react-transition-group": "^4.4.1",
 | 
			
		||||
    "resize-observer-polyfill": "^1.5.0",
 | 
			
		||||
    "sanitize-html": "^1.18.4",
 | 
			
		||||
    "text-encoding-utf-8": "^1.0.1",
 | 
			
		||||
@@ -126,6 +127,7 @@
 | 
			
		||||
    "@types/qrcode": "^1.3.4",
 | 
			
		||||
    "@types/react": "^16.9",
 | 
			
		||||
    "@types/react-dom": "^16.9.8",
 | 
			
		||||
    "@types/react-transition-group": "^4.4.0",
 | 
			
		||||
    "@types/zxcvbn": "^4.4.0",
 | 
			
		||||
    "babel-eslint": "^10.0.3",
 | 
			
		||||
    "babel-jest": "^24.9.0",
 | 
			
		||||
 
 | 
			
		||||
@@ -179,6 +179,7 @@
 | 
			
		||||
@import "./views/rooms/_PresenceLabel.scss";
 | 
			
		||||
@import "./views/rooms/_ReplyPreview.scss";
 | 
			
		||||
@import "./views/rooms/_RoomBreadcrumbs.scss";
 | 
			
		||||
@import "./views/rooms/_RoomBreadcrumbs2.scss";
 | 
			
		||||
@import "./views/rooms/_RoomDropTarget.scss";
 | 
			
		||||
@import "./views/rooms/_RoomHeader.scss";
 | 
			
		||||
@import "./views/rooms/_RoomList.scss";
 | 
			
		||||
 
 | 
			
		||||
@@ -81,9 +81,9 @@ $roomListMinimizedWidth: 50px;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .mx_LeftPanel2_breadcrumbsContainer {
 | 
			
		||||
                // TODO: Improve CSS for breadcrumbs (currently shoved into the view rather than placed)
 | 
			
		||||
                width: 100%;
 | 
			
		||||
                overflow: hidden;
 | 
			
		||||
                margin-top: 8px;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										51
									
								
								res/css/views/rooms/_RoomBreadcrumbs2.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								res/css/views/rooms/_RoomBreadcrumbs2.scss
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
			
		||||
/*
 | 
			
		||||
Copyright 2020 The Matrix.org Foundation C.I.C.
 | 
			
		||||
 | 
			
		||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
you may not use this file except in compliance with the License.
 | 
			
		||||
You may obtain a copy of the License at
 | 
			
		||||
 | 
			
		||||
    http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 | 
			
		||||
Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
See the License for the specific language governing permissions and
 | 
			
		||||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
.mx_RoomBreadcrumbs2 {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
 | 
			
		||||
    // Create a flexbox for the crumbs
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
    align-items: flex-start;
 | 
			
		||||
 | 
			
		||||
    .mx_RoomBreadcrumbs2_crumb {
 | 
			
		||||
        margin-right: 8px;
 | 
			
		||||
        width: 32px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // These classes come from the CSSTransition component. There's many more classes we
 | 
			
		||||
    // could care about, but this is all we worried about for now. The animation works by
 | 
			
		||||
    // first triggering the enter state with the newest breadcrumb off screen (-40px) then
 | 
			
		||||
    // sliding it into view.
 | 
			
		||||
    &.mx_RoomBreadcrumbs2-enter {
 | 
			
		||||
        margin-left: -40px; // 32px for the avatar, 8px for the margin
 | 
			
		||||
    }
 | 
			
		||||
    &.mx_RoomBreadcrumbs2-enter-active {
 | 
			
		||||
        margin-left: 0;
 | 
			
		||||
 | 
			
		||||
        // Timing function is as-requested by design.
 | 
			
		||||
        // NOTE: The transition time MUST match the value passed to CSSTransition!
 | 
			
		||||
        transition: margin-left 640ms cubic-bezier(0.66, 0.02, 0.36, 1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .mx_RoomBreadcrumbs2_placeholder {
 | 
			
		||||
        font-weight: 600;
 | 
			
		||||
        font-size: $font-14px;
 | 
			
		||||
        line-height: 32px; // specifically to match the height this is not scaled
 | 
			
		||||
        height: 32px;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -24,10 +24,12 @@ import RoomList2 from "../views/rooms/RoomList2";
 | 
			
		||||
import { Action } from "../../dispatcher/actions";
 | 
			
		||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
 | 
			
		||||
import BaseAvatar from '../views/avatars/BaseAvatar';
 | 
			
		||||
import RoomBreadcrumbs from "../views/rooms/RoomBreadcrumbs";
 | 
			
		||||
import UserMenuButton from "./UserMenuButton";
 | 
			
		||||
import RoomSearch from "./RoomSearch";
 | 
			
		||||
import AccessibleButton from "../views/elements/AccessibleButton";
 | 
			
		||||
import RoomBreadcrumbs2 from "../views/rooms/RoomBreadcrumbs2";
 | 
			
		||||
import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
 | 
			
		||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
 | 
			
		||||
 | 
			
		||||
/*******************************************************************
 | 
			
		||||
 *   CAUTION                                                       *
 | 
			
		||||
@@ -43,6 +45,7 @@ interface IProps {
 | 
			
		||||
 | 
			
		||||
interface IState {
 | 
			
		||||
    searchFilter: string; // TODO: Move search into room list?
 | 
			
		||||
    showBreadcrumbs: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default class LeftPanel2 extends React.Component<IProps, IState> {
 | 
			
		||||
@@ -58,7 +61,14 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
 | 
			
		||||
 | 
			
		||||
        this.state = {
 | 
			
		||||
            searchFilter: "",
 | 
			
		||||
            showBreadcrumbs: BreadcrumbsStore.instance.visible,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public componentWillUnmount() {
 | 
			
		||||
        BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private onSearch = (term: string): void => {
 | 
			
		||||
@@ -69,6 +79,13 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
 | 
			
		||||
        dis.fire(Action.ViewRoomDirectory);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private onBreadcrumbsUpdate = () => {
 | 
			
		||||
        const newVal = BreadcrumbsStore.instance.visible;
 | 
			
		||||
        if (newVal !== this.state.showBreadcrumbs) {
 | 
			
		||||
            this.setState({showBreadcrumbs: newVal});
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private renderHeader(): React.ReactNode {
 | 
			
		||||
        // TODO: Update when profile info changes
 | 
			
		||||
        // TODO: Presence
 | 
			
		||||
@@ -84,6 +101,16 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
 | 
			
		||||
            displayName = myUser.rawDisplayName;
 | 
			
		||||
            avatarUrl = myUser.avatarUrl;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let breadcrumbs;
 | 
			
		||||
        if (this.state.showBreadcrumbs) {
 | 
			
		||||
            breadcrumbs = (
 | 
			
		||||
                <div className="mx_LeftPanel2_headerRow mx_LeftPanel2_breadcrumbsContainer">
 | 
			
		||||
                    <RoomBreadcrumbs2 />
 | 
			
		||||
                </div>
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            <div className="mx_LeftPanel2_userHeader">
 | 
			
		||||
                <div className="mx_LeftPanel2_headerRow">
 | 
			
		||||
@@ -103,9 +130,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
 | 
			
		||||
                        <UserMenuButton />
 | 
			
		||||
                    </span>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className="mx_LeftPanel2_headerRow mx_LeftPanel2_breadcrumbsContainer">
 | 
			
		||||
                    <RoomBreadcrumbs />
 | 
			
		||||
                </div>
 | 
			
		||||
                {breadcrumbs}
 | 
			
		||||
            </div>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
@@ -143,7 +168,6 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
 | 
			
		||||
            onBlur={() => {/*TODO*/}}
 | 
			
		||||
        />;
 | 
			
		||||
 | 
			
		||||
        // TODO: Breadcrumbs
 | 
			
		||||
        // TODO: Conference handling / calls
 | 
			
		||||
 | 
			
		||||
        const containerClasses = classNames({
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										125
									
								
								src/components/views/rooms/RoomBreadcrumbs2.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								src/components/views/rooms/RoomBreadcrumbs2.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,125 @@
 | 
			
		||||
/*
 | 
			
		||||
Copyright 2020 The Matrix.org Foundation C.I.C.
 | 
			
		||||
 | 
			
		||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
you may not use this file except in compliance with the License.
 | 
			
		||||
You may obtain a copy of the License at
 | 
			
		||||
 | 
			
		||||
    http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 | 
			
		||||
Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
See the License for the specific language governing permissions and
 | 
			
		||||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { BreadcrumbsStore } from "../../../stores/BreadcrumbsStore";
 | 
			
		||||
import AccessibleButton from "../elements/AccessibleButton";
 | 
			
		||||
import RoomAvatar from "../avatars/RoomAvatar";
 | 
			
		||||
import { _t } from "../../../languageHandler";
 | 
			
		||||
import { Room } from "matrix-js-sdk/src/models/room";
 | 
			
		||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
 | 
			
		||||
import Analytics from "../../../Analytics";
 | 
			
		||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
 | 
			
		||||
import { CSSTransition, TransitionGroup } from "react-transition-group";
 | 
			
		||||
 | 
			
		||||
/*******************************************************************
 | 
			
		||||
 *   CAUTION                                                       *
 | 
			
		||||
 *******************************************************************
 | 
			
		||||
 * This is a work in progress implementation and isn't complete or *
 | 
			
		||||
 * even useful as a component. Please avoid using it until this    *
 | 
			
		||||
 * warning disappears.                                             *
 | 
			
		||||
 *******************************************************************/
 | 
			
		||||
 | 
			
		||||
interface IProps {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IState {
 | 
			
		||||
    // Both of these control the animation for the breadcrumbs. For details on the
 | 
			
		||||
    // actual animation, see the CSS.
 | 
			
		||||
    //
 | 
			
		||||
    // doAnimation is to lie to the CSSTransition component (see onBreadcrumbsUpdate
 | 
			
		||||
    // for info). skipFirst is used to try and reduce jerky animation - also see the
 | 
			
		||||
    // breadcrumb update function for info on that.
 | 
			
		||||
    doAnimation: boolean;
 | 
			
		||||
    skipFirst: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default class RoomBreadcrumbs2 extends React.PureComponent<IProps, IState> {
 | 
			
		||||
    private isMounted = true;
 | 
			
		||||
 | 
			
		||||
    constructor(props: IProps) {
 | 
			
		||||
        super(props);
 | 
			
		||||
 | 
			
		||||
        this.state = {
 | 
			
		||||
            doAnimation: true, // technically we want animation on mount, but it won't be perfect
 | 
			
		||||
            skipFirst: false, // render the thing, as boring as it is
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public componentWillUnmount() {
 | 
			
		||||
        this.isMounted = false;
 | 
			
		||||
        BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private onBreadcrumbsUpdate = () => {
 | 
			
		||||
        if (!this.isMounted) return;
 | 
			
		||||
 | 
			
		||||
        // We need to trick the CSSTransition component into updating, which means we need to
 | 
			
		||||
        // tell it to not animate, then to animate a moment later. This causes two updates
 | 
			
		||||
        // which means two renders. The skipFirst change is so that our don't-animate state
 | 
			
		||||
        // doesn't show the breadcrumb we're about to reveal as it causes a visual jump/jerk.
 | 
			
		||||
        // The second update, on the next available tick, causes the "enter" animation to start
 | 
			
		||||
        // again and this time we want to show the newest breadcrumb because it'll be hidden
 | 
			
		||||
        // off screen for the animation.
 | 
			
		||||
        this.setState({doAnimation: false, skipFirst: true});
 | 
			
		||||
        setTimeout(() => this.setState({doAnimation: true, skipFirst: false}), 0);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private viewRoom = (room: Room, index: number) => {
 | 
			
		||||
        Analytics.trackEvent("Breadcrumbs", "click_node", index);
 | 
			
		||||
        defaultDispatcher.dispatch({action: "view_room", room_id: room.roomId});
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    public render(): React.ReactElement {
 | 
			
		||||
        // TODO: Decorate crumbs with icons
 | 
			
		||||
        const tiles = BreadcrumbsStore.instance.rooms.map((r, i) => {
 | 
			
		||||
            return (
 | 
			
		||||
                <AccessibleButton
 | 
			
		||||
                    className="mx_RoomBreadcrumbs2_crumb"
 | 
			
		||||
                    key={r.roomId}
 | 
			
		||||
                    onClick={() => this.viewRoom(r, i)}
 | 
			
		||||
                    aria-label={_t("Room %(name)s", {name: r.name})}
 | 
			
		||||
                >
 | 
			
		||||
                    <RoomAvatar room={r} width={32} height={32}/>
 | 
			
		||||
                </AccessibleButton>
 | 
			
		||||
            )
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (tiles.length > 0) {
 | 
			
		||||
            // NOTE: The CSSTransition timeout MUST match the timeout in our CSS!
 | 
			
		||||
            return (
 | 
			
		||||
                <CSSTransition
 | 
			
		||||
                    appear={true} in={this.state.doAnimation} timeout={640}
 | 
			
		||||
                    classNames='mx_RoomBreadcrumbs2'
 | 
			
		||||
                >
 | 
			
		||||
                    <div className='mx_RoomBreadcrumbs2'>
 | 
			
		||||
                        {tiles.slice(this.state.skipFirst ? 1 : 0)}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </CSSTransition>
 | 
			
		||||
            );
 | 
			
		||||
        } else {
 | 
			
		||||
            return (
 | 
			
		||||
                <div className='mx_RoomBreadcrumbs2'>
 | 
			
		||||
                    <div className="mx_RoomBreadcrumbs2_placeholder">
 | 
			
		||||
                        {_t("No recently visited rooms")}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1069,6 +1069,7 @@
 | 
			
		||||
    "Replying": "Replying",
 | 
			
		||||
    "Room %(name)s": "Room %(name)s",
 | 
			
		||||
    "Recent rooms": "Recent rooms",
 | 
			
		||||
    "No recently visited rooms": "No recently visited rooms",
 | 
			
		||||
    "No rooms to show": "No rooms to show",
 | 
			
		||||
    "Unnamed room": "Unnamed room",
 | 
			
		||||
    "World readable": "World readable",
 | 
			
		||||
 
 | 
			
		||||
@@ -181,6 +181,8 @@ export default class SettingsStore {
 | 
			
		||||
     * @param {String} roomId The room ID to monitor for changes in. Use null for all rooms.
 | 
			
		||||
     */
 | 
			
		||||
    static monitorSetting(settingName, roomId) {
 | 
			
		||||
        roomId = roomId || null; // the thing wants null specifically to work, so appease it.
 | 
			
		||||
 | 
			
		||||
        if (!this._monitors[settingName]) this._monitors[settingName] = {};
 | 
			
		||||
 | 
			
		||||
        const registerWatcher = () => {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										53
									
								
								src/stores/AsyncStoreWithClient.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/stores/AsyncStoreWithClient.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
			
		||||
/*
 | 
			
		||||
Copyright 2020 The Matrix.org Foundation C.I.C.
 | 
			
		||||
 | 
			
		||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
you may not use this file except in compliance with the License.
 | 
			
		||||
You may obtain a copy of the License at
 | 
			
		||||
 | 
			
		||||
    http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 | 
			
		||||
Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
See the License for the specific language governing permissions and
 | 
			
		||||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
import { MatrixClient } from "matrix-js-sdk/src/client";
 | 
			
		||||
import { AsyncStore } from "./AsyncStore";
 | 
			
		||||
import { ActionPayload } from "../dispatcher/payloads";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export abstract class AsyncStoreWithClient<T extends Object> extends AsyncStore<T> {
 | 
			
		||||
    protected matrixClient: MatrixClient;
 | 
			
		||||
 | 
			
		||||
    protected abstract async onAction(payload: ActionPayload);
 | 
			
		||||
 | 
			
		||||
    protected async onReady() {
 | 
			
		||||
        // Default implementation is to do nothing.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected async onNotReady() {
 | 
			
		||||
        // Default implementation is to do nothing.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected async onDispatch(payload: ActionPayload) {
 | 
			
		||||
        await this.onAction(payload);
 | 
			
		||||
 | 
			
		||||
        if (payload.action === 'MatrixActions.sync') {
 | 
			
		||||
            // Filter out anything that isn't the first PREPARED sync.
 | 
			
		||||
            if (!(payload.prevState === 'PREPARED' && payload.state !== 'PREPARED')) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.matrixClient = payload.matrixClient;
 | 
			
		||||
            await this.onReady();
 | 
			
		||||
        } else if (payload.action === 'on_client_not_viable' || payload.action === 'on_logged_out') {
 | 
			
		||||
            if (this.matrixClient) {
 | 
			
		||||
                await this.onNotReady();
 | 
			
		||||
                this.matrixClient = null;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										166
									
								
								src/stores/BreadcrumbsStore.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								src/stores/BreadcrumbsStore.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,166 @@
 | 
			
		||||
/*
 | 
			
		||||
Copyright 2020 The Matrix.org Foundation C.I.C.
 | 
			
		||||
 | 
			
		||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
you may not use this file except in compliance with the License.
 | 
			
		||||
You may obtain a copy of the License at
 | 
			
		||||
 | 
			
		||||
    http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 | 
			
		||||
Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
See the License for the specific language governing permissions and
 | 
			
		||||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
import SettingsStore, { SettingLevel } from "../settings/SettingsStore";
 | 
			
		||||
import { Room } from "matrix-js-sdk/src/models/room";
 | 
			
		||||
import { ActionPayload } from "../dispatcher/payloads";
 | 
			
		||||
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
 | 
			
		||||
import defaultDispatcher from "../dispatcher/dispatcher";
 | 
			
		||||
import { arrayHasDiff } from "../utils/arrays";
 | 
			
		||||
import { RoomListStoreTempProxy } from "./room-list/RoomListStoreTempProxy";
 | 
			
		||||
 | 
			
		||||
const MAX_ROOMS = 20; // arbitrary
 | 
			
		||||
const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90s, the time we wait for an autojoined room to show up
 | 
			
		||||
 | 
			
		||||
interface IState {
 | 
			
		||||
    enabled?: boolean;
 | 
			
		||||
    rooms?: Room[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
 | 
			
		||||
    private static internalInstance = new BreadcrumbsStore();
 | 
			
		||||
 | 
			
		||||
    private waitingRooms: { roomId: string, addedTs: number }[] = [];
 | 
			
		||||
 | 
			
		||||
    private constructor() {
 | 
			
		||||
        super(defaultDispatcher);
 | 
			
		||||
 | 
			
		||||
        SettingsStore.monitorSetting("breadcrumb_rooms", null);
 | 
			
		||||
        SettingsStore.monitorSetting("breadcrumbs", null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static get instance(): BreadcrumbsStore {
 | 
			
		||||
        return BreadcrumbsStore.internalInstance;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public get rooms(): Room[] {
 | 
			
		||||
        return this.state.rooms || [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public get visible(): boolean {
 | 
			
		||||
        return this.state.enabled;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected async onAction(payload: ActionPayload) {
 | 
			
		||||
        if (!this.matrixClient) return;
 | 
			
		||||
 | 
			
		||||
        // TODO: Remove when new room list is made the default
 | 
			
		||||
        if (!RoomListStoreTempProxy.isUsingNewStore()) return;
 | 
			
		||||
 | 
			
		||||
        if (payload.action === 'setting_updated') {
 | 
			
		||||
            if (payload.settingName === 'breadcrumb_rooms') {
 | 
			
		||||
                await this.updateRooms();
 | 
			
		||||
            } else if (payload.settingName === 'breadcrumbs') {
 | 
			
		||||
                await this.updateState({enabled: SettingsStore.getValue("breadcrumbs", null)});
 | 
			
		||||
            }
 | 
			
		||||
        } else if (payload.action === 'view_room') {
 | 
			
		||||
            if (payload.auto_join && !this.matrixClient.getRoom(payload.room_id)) {
 | 
			
		||||
                // Queue the room instead of pushing it immediately. We're probably just
 | 
			
		||||
                // waiting for a room join to complete.
 | 
			
		||||
                this.waitingRooms.push({roomId: payload.room_id, addedTs: Date.now()});
 | 
			
		||||
            } else {
 | 
			
		||||
                // The tests might not result in a valid room object.
 | 
			
		||||
                const room = this.matrixClient.getRoom(payload.room_id);
 | 
			
		||||
                if (room) await this.appendRoom(room);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected async onReady() {
 | 
			
		||||
        // TODO: Remove when new room list is made the default
 | 
			
		||||
        if (!RoomListStoreTempProxy.isUsingNewStore()) return;
 | 
			
		||||
 | 
			
		||||
        await this.updateRooms();
 | 
			
		||||
        await this.updateState({enabled: SettingsStore.getValue("breadcrumbs", null)});
 | 
			
		||||
 | 
			
		||||
        this.matrixClient.on("Room.myMembership", this.onMyMembership);
 | 
			
		||||
        this.matrixClient.on("Room", this.onRoom);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected async onNotReady() {
 | 
			
		||||
        // TODO: Remove when new room list is made the default
 | 
			
		||||
        if (!RoomListStoreTempProxy.isUsingNewStore()) return;
 | 
			
		||||
 | 
			
		||||
        this.matrixClient.removeListener("Room.myMembership", this.onMyMembership);
 | 
			
		||||
        this.matrixClient.removeListener("Room", this.onRoom);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private onMyMembership = async (room: Room) => {
 | 
			
		||||
        // We turn on breadcrumbs by default once the user has at least 1 room to show.
 | 
			
		||||
        if (!this.state.enabled) {
 | 
			
		||||
            await SettingsStore.setValue("breadcrumbs", null, SettingLevel.ACCOUNT, true);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private onRoom = async (room: Room) => {
 | 
			
		||||
        const waitingRoom = this.waitingRooms.find(r => r.roomId === room.roomId);
 | 
			
		||||
        if (!waitingRoom) return;
 | 
			
		||||
        this.waitingRooms.splice(this.waitingRooms.indexOf(waitingRoom), 1);
 | 
			
		||||
 | 
			
		||||
        if ((Date.now() - waitingRoom.addedTs) > AUTOJOIN_WAIT_THRESHOLD_MS) return; // Too long ago.
 | 
			
		||||
        await this.appendRoom(room);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private async updateRooms() {
 | 
			
		||||
        let roomIds = SettingsStore.getValue("breadcrumb_rooms");
 | 
			
		||||
        if (!roomIds || roomIds.length === 0) roomIds = [];
 | 
			
		||||
 | 
			
		||||
        const rooms = roomIds.map(r => this.matrixClient.getRoom(r)).filter(r => !!r);
 | 
			
		||||
        const currentRooms = this.state.rooms || [];
 | 
			
		||||
        if (!arrayHasDiff(rooms, currentRooms)) return; // no change (probably echo)
 | 
			
		||||
        await this.updateState({rooms});
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async appendRoom(room: Room) {
 | 
			
		||||
        const rooms = (this.state.rooms || []).slice(); // cheap clone
 | 
			
		||||
 | 
			
		||||
        // If the room is upgraded, use that room instead. We'll also splice out
 | 
			
		||||
        // any children of the room.
 | 
			
		||||
        const history = this.matrixClient.getRoomUpgradeHistory(room.roomId);
 | 
			
		||||
        if (history.length > 1) {
 | 
			
		||||
            room = history[history.length - 1]; // Last room is most recent in history
 | 
			
		||||
 | 
			
		||||
            // Take out any room that isn't the most recent room
 | 
			
		||||
            for (let i = 0; i < history.length - 1; i++) {
 | 
			
		||||
                const idx = rooms.findIndex(r => r.roomId === history[i].roomId);
 | 
			
		||||
                if (idx !== -1) rooms.splice(idx, 1);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Remove the existing room, if it is present
 | 
			
		||||
        const existingIdx = rooms.findIndex(r => r.roomId === room.roomId);
 | 
			
		||||
        if (existingIdx !== -1) {
 | 
			
		||||
            rooms.splice(existingIdx, 1);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Splice the room to the start of the list
 | 
			
		||||
        rooms.splice(0, 0, room);
 | 
			
		||||
 | 
			
		||||
        if (rooms.length > MAX_ROOMS) {
 | 
			
		||||
            // This looks weird, but it's saying to start at the MAX_ROOMS point in the
 | 
			
		||||
            // list and delete everything after it.
 | 
			
		||||
            rooms.splice(MAX_ROOMS, rooms.length - MAX_ROOMS);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Update the breadcrumbs
 | 
			
		||||
        await this.updateState({rooms});
 | 
			
		||||
        const roomIds = rooms.map(r => r.roomId);
 | 
			
		||||
        if (roomIds.length > 0) {
 | 
			
		||||
            await SettingsStore.setValue("breadcrumb_rooms", null, SettingLevel.ACCOUNT, roomIds);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -15,7 +15,8 @@
 | 
			
		||||
    "types": [
 | 
			
		||||
      "node",
 | 
			
		||||
      "react",
 | 
			
		||||
      "flux"
 | 
			
		||||
      "flux",
 | 
			
		||||
      "react-transition-group"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "include": [
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										29
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								yarn.lock
									
									
									
									
									
								
							@@ -968,7 +968,7 @@
 | 
			
		||||
    core-js-pure "^3.0.0"
 | 
			
		||||
    regenerator-runtime "^0.13.4"
 | 
			
		||||
 | 
			
		||||
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4":
 | 
			
		||||
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
 | 
			
		||||
  version "7.10.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.2.tgz#d103f21f2602497d38348a32e008637d506db839"
 | 
			
		||||
  integrity sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg==
 | 
			
		||||
@@ -1352,6 +1352,13 @@
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@types/react" "*"
 | 
			
		||||
 | 
			
		||||
"@types/react-transition-group@^4.4.0":
 | 
			
		||||
  version "4.4.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.0.tgz#882839db465df1320e4753e6e9f70ca7e9b4d46d"
 | 
			
		||||
  integrity sha512-/QfLHGpu+2fQOqQaXh8MG9q03bFENooTb/it4jr5kKaZlDQfWvjqWZg48AwzPVMBHlRuTRAY7hRHCEOXz5kV6w==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@types/react" "*"
 | 
			
		||||
 | 
			
		||||
"@types/react@*", "@types/react@^16.9":
 | 
			
		||||
  version "16.9.35"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.35.tgz#a0830d172e8aadd9bd41709ba2281a3124bbd368"
 | 
			
		||||
@@ -2835,7 +2842,7 @@ cssstyle@^1.0.0:
 | 
			
		||||
  dependencies:
 | 
			
		||||
    cssom "0.3.x"
 | 
			
		||||
 | 
			
		||||
csstype@^2.2.0:
 | 
			
		||||
csstype@^2.2.0, csstype@^2.6.7:
 | 
			
		||||
  version "2.6.10"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.10.tgz#e63af50e66d7c266edb6b32909cfd0aabe03928b"
 | 
			
		||||
  integrity sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w==
 | 
			
		||||
@@ -3054,6 +3061,14 @@ doctrine@^3.0.0:
 | 
			
		||||
  dependencies:
 | 
			
		||||
    esutils "^2.0.2"
 | 
			
		||||
 | 
			
		||||
dom-helpers@^5.0.1:
 | 
			
		||||
  version "5.1.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.1.4.tgz#4609680ab5c79a45f2531441f1949b79d6587f4b"
 | 
			
		||||
  integrity sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/runtime" "^7.8.7"
 | 
			
		||||
    csstype "^2.6.7"
 | 
			
		||||
 | 
			
		||||
dom-serializer@0, dom-serializer@^0.2.1:
 | 
			
		||||
  version "0.2.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
 | 
			
		||||
@@ -7136,6 +7151,16 @@ react-test-renderer@^16.0.0-0, react-test-renderer@^16.9.0:
 | 
			
		||||
    react-is "^16.8.6"
 | 
			
		||||
    scheduler "^0.19.1"
 | 
			
		||||
 | 
			
		||||
react-transition-group@^4.4.1:
 | 
			
		||||
  version "4.4.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9"
 | 
			
		||||
  integrity sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/runtime" "^7.5.5"
 | 
			
		||||
    dom-helpers "^5.0.1"
 | 
			
		||||
    loose-envify "^1.4.0"
 | 
			
		||||
    prop-types "^15.6.2"
 | 
			
		||||
 | 
			
		||||
react@^16.9.0:
 | 
			
		||||
  version "16.13.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user