Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 95b4abb

Browse files
authored
Merge pull request #4735 from matrix-org/travis/room-list/breadcrumbs
Reimplement breadcrumbs for new room list
2 parents 60b5f2d + b84af37 commit 95b4abb

File tree

12 files changed

+460
-9
lines changed

12 files changed

+460
-9
lines changed

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
"react-dom": "^16.9.0",
9595
"react-focus-lock": "^2.2.1",
9696
"react-resizable": "^1.10.1",
97+
"react-transition-group": "^4.4.1",
9798
"resize-observer-polyfill": "^1.5.0",
9899
"sanitize-html": "^1.18.4",
99100
"text-encoding-utf-8": "^1.0.1",
@@ -126,6 +127,7 @@
126127
"@types/qrcode": "^1.3.4",
127128
"@types/react": "^16.9",
128129
"@types/react-dom": "^16.9.8",
130+
"@types/react-transition-group": "^4.4.0",
129131
"@types/zxcvbn": "^4.4.0",
130132
"babel-eslint": "^10.0.3",
131133
"babel-jest": "^24.9.0",

res/css/_components.scss

+1
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@
179179
@import "./views/rooms/_PresenceLabel.scss";
180180
@import "./views/rooms/_ReplyPreview.scss";
181181
@import "./views/rooms/_RoomBreadcrumbs.scss";
182+
@import "./views/rooms/_RoomBreadcrumbs2.scss";
182183
@import "./views/rooms/_RoomDropTarget.scss";
183184
@import "./views/rooms/_RoomHeader.scss";
184185
@import "./views/rooms/_RoomList.scss";

res/css/structures/_LeftPanel2.scss

+1-1
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,9 @@ $roomListMinimizedWidth: 50px;
8181
}
8282

8383
.mx_LeftPanel2_breadcrumbsContainer {
84-
// TODO: Improve CSS for breadcrumbs (currently shoved into the view rather than placed)
8584
width: 100%;
8685
overflow: hidden;
86+
margin-top: 8px;
8787
}
8888
}
8989

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
Copyright 2020 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
.mx_RoomBreadcrumbs2 {
18+
width: 100%;
19+
20+
// Create a flexbox for the crumbs
21+
display: flex;
22+
flex-direction: row;
23+
align-items: flex-start;
24+
25+
.mx_RoomBreadcrumbs2_crumb {
26+
margin-right: 8px;
27+
width: 32px;
28+
}
29+
30+
// These classes come from the CSSTransition component. There's many more classes we
31+
// could care about, but this is all we worried about for now. The animation works by
32+
// first triggering the enter state with the newest breadcrumb off screen (-40px) then
33+
// sliding it into view.
34+
&.mx_RoomBreadcrumbs2-enter {
35+
margin-left: -40px; // 32px for the avatar, 8px for the margin
36+
}
37+
&.mx_RoomBreadcrumbs2-enter-active {
38+
margin-left: 0;
39+
40+
// Timing function is as-requested by design.
41+
// NOTE: The transition time MUST match the value passed to CSSTransition!
42+
transition: margin-left 640ms cubic-bezier(0.66, 0.02, 0.36, 1);
43+
}
44+
45+
.mx_RoomBreadcrumbs2_placeholder {
46+
font-weight: 600;
47+
font-size: $font-14px;
48+
line-height: 32px; // specifically to match the height this is not scaled
49+
height: 32px;
50+
}
51+
}

src/components/structures/LeftPanel2.tsx

+29-5
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ import RoomList2 from "../views/rooms/RoomList2";
2424
import { Action } from "../../dispatcher/actions";
2525
import { MatrixClientPeg } from "../../MatrixClientPeg";
2626
import BaseAvatar from '../views/avatars/BaseAvatar';
27-
import RoomBreadcrumbs from "../views/rooms/RoomBreadcrumbs";
2827
import UserMenuButton from "./UserMenuButton";
2928
import RoomSearch from "./RoomSearch";
3029
import AccessibleButton from "../views/elements/AccessibleButton";
30+
import RoomBreadcrumbs2 from "../views/rooms/RoomBreadcrumbs2";
31+
import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
32+
import { UPDATE_EVENT } from "../../stores/AsyncStore";
3133

3234
/*******************************************************************
3335
* CAUTION *
@@ -43,6 +45,7 @@ interface IProps {
4345

4446
interface IState {
4547
searchFilter: string; // TODO: Move search into room list?
48+
showBreadcrumbs: boolean;
4649
}
4750

4851
export default class LeftPanel2 extends React.Component<IProps, IState> {
@@ -58,7 +61,14 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
5861

5962
this.state = {
6063
searchFilter: "",
64+
showBreadcrumbs: BreadcrumbsStore.instance.visible,
6165
};
66+
67+
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
68+
}
69+
70+
public componentWillUnmount() {
71+
BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
6272
}
6373

6474
private onSearch = (term: string): void => {
@@ -69,6 +79,13 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
6979
dis.fire(Action.ViewRoomDirectory);
7080
};
7181

82+
private onBreadcrumbsUpdate = () => {
83+
const newVal = BreadcrumbsStore.instance.visible;
84+
if (newVal !== this.state.showBreadcrumbs) {
85+
this.setState({showBreadcrumbs: newVal});
86+
}
87+
};
88+
7289
private renderHeader(): React.ReactNode {
7390
// TODO: Update when profile info changes
7491
// TODO: Presence
@@ -84,6 +101,16 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
84101
displayName = myUser.rawDisplayName;
85102
avatarUrl = myUser.avatarUrl;
86103
}
104+
105+
let breadcrumbs;
106+
if (this.state.showBreadcrumbs) {
107+
breadcrumbs = (
108+
<div className="mx_LeftPanel2_headerRow mx_LeftPanel2_breadcrumbsContainer">
109+
<RoomBreadcrumbs2 />
110+
</div>
111+
);
112+
}
113+
87114
return (
88115
<div className="mx_LeftPanel2_userHeader">
89116
<div className="mx_LeftPanel2_headerRow">
@@ -103,9 +130,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
103130
<UserMenuButton />
104131
</span>
105132
</div>
106-
<div className="mx_LeftPanel2_headerRow mx_LeftPanel2_breadcrumbsContainer">
107-
<RoomBreadcrumbs />
108-
</div>
133+
{breadcrumbs}
109134
</div>
110135
);
111136
}
@@ -143,7 +168,6 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
143168
onBlur={() => {/*TODO*/}}
144169
/>;
145170

146-
// TODO: Breadcrumbs
147171
// TODO: Conference handling / calls
148172

149173
const containerClasses = classNames({
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
Copyright 2020 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React from "react";
18+
import { BreadcrumbsStore } from "../../../stores/BreadcrumbsStore";
19+
import AccessibleButton from "../elements/AccessibleButton";
20+
import RoomAvatar from "../avatars/RoomAvatar";
21+
import { _t } from "../../../languageHandler";
22+
import { Room } from "matrix-js-sdk/src/models/room";
23+
import defaultDispatcher from "../../../dispatcher/dispatcher";
24+
import Analytics from "../../../Analytics";
25+
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
26+
import { CSSTransition, TransitionGroup } from "react-transition-group";
27+
28+
/*******************************************************************
29+
* CAUTION *
30+
*******************************************************************
31+
* This is a work in progress implementation and isn't complete or *
32+
* even useful as a component. Please avoid using it until this *
33+
* warning disappears. *
34+
*******************************************************************/
35+
36+
interface IProps {
37+
}
38+
39+
interface IState {
40+
// Both of these control the animation for the breadcrumbs. For details on the
41+
// actual animation, see the CSS.
42+
//
43+
// doAnimation is to lie to the CSSTransition component (see onBreadcrumbsUpdate
44+
// for info). skipFirst is used to try and reduce jerky animation - also see the
45+
// breadcrumb update function for info on that.
46+
doAnimation: boolean;
47+
skipFirst: boolean;
48+
}
49+
50+
export default class RoomBreadcrumbs2 extends React.PureComponent<IProps, IState> {
51+
private isMounted = true;
52+
53+
constructor(props: IProps) {
54+
super(props);
55+
56+
this.state = {
57+
doAnimation: true, // technically we want animation on mount, but it won't be perfect
58+
skipFirst: false, // render the thing, as boring as it is
59+
};
60+
61+
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
62+
}
63+
64+
public componentWillUnmount() {
65+
this.isMounted = false;
66+
BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
67+
}
68+
69+
private onBreadcrumbsUpdate = () => {
70+
if (!this.isMounted) return;
71+
72+
// We need to trick the CSSTransition component into updating, which means we need to
73+
// tell it to not animate, then to animate a moment later. This causes two updates
74+
// which means two renders. The skipFirst change is so that our don't-animate state
75+
// doesn't show the breadcrumb we're about to reveal as it causes a visual jump/jerk.
76+
// The second update, on the next available tick, causes the "enter" animation to start
77+
// again and this time we want to show the newest breadcrumb because it'll be hidden
78+
// off screen for the animation.
79+
this.setState({doAnimation: false, skipFirst: true});
80+
setTimeout(() => this.setState({doAnimation: true, skipFirst: false}), 0);
81+
};
82+
83+
private viewRoom = (room: Room, index: number) => {
84+
Analytics.trackEvent("Breadcrumbs", "click_node", index);
85+
defaultDispatcher.dispatch({action: "view_room", room_id: room.roomId});
86+
};
87+
88+
public render(): React.ReactElement {
89+
// TODO: Decorate crumbs with icons
90+
const tiles = BreadcrumbsStore.instance.rooms.map((r, i) => {
91+
return (
92+
<AccessibleButton
93+
className="mx_RoomBreadcrumbs2_crumb"
94+
key={r.roomId}
95+
onClick={() => this.viewRoom(r, i)}
96+
aria-label={_t("Room %(name)s", {name: r.name})}
97+
>
98+
<RoomAvatar room={r} width={32} height={32}/>
99+
</AccessibleButton>
100+
)
101+
});
102+
103+
if (tiles.length > 0) {
104+
// NOTE: The CSSTransition timeout MUST match the timeout in our CSS!
105+
return (
106+
<CSSTransition
107+
appear={true} in={this.state.doAnimation} timeout={640}
108+
classNames='mx_RoomBreadcrumbs2'
109+
>
110+
<div className='mx_RoomBreadcrumbs2'>
111+
{tiles.slice(this.state.skipFirst ? 1 : 0)}
112+
</div>
113+
</CSSTransition>
114+
);
115+
} else {
116+
return (
117+
<div className='mx_RoomBreadcrumbs2'>
118+
<div className="mx_RoomBreadcrumbs2_placeholder">
119+
{_t("No recently visited rooms")}
120+
</div>
121+
</div>
122+
);
123+
}
124+
}
125+
}

src/i18n/strings/en_EN.json

+1
Original file line numberDiff line numberDiff line change
@@ -1069,6 +1069,7 @@
10691069
"Replying": "Replying",
10701070
"Room %(name)s": "Room %(name)s",
10711071
"Recent rooms": "Recent rooms",
1072+
"No recently visited rooms": "No recently visited rooms",
10721073
"No rooms to show": "No rooms to show",
10731074
"Unnamed room": "Unnamed room",
10741075
"World readable": "World readable",

src/settings/SettingsStore.js

+2
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,8 @@ export default class SettingsStore {
181181
* @param {String} roomId The room ID to monitor for changes in. Use null for all rooms.
182182
*/
183183
static monitorSetting(settingName, roomId) {
184+
roomId = roomId || null; // the thing wants null specifically to work, so appease it.
185+
184186
if (!this._monitors[settingName]) this._monitors[settingName] = {};
185187

186188
const registerWatcher = () => {

src/stores/AsyncStoreWithClient.ts

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
Copyright 2020 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { MatrixClient } from "matrix-js-sdk/src/client";
18+
import { AsyncStore } from "./AsyncStore";
19+
import { ActionPayload } from "../dispatcher/payloads";
20+
21+
22+
export abstract class AsyncStoreWithClient<T extends Object> extends AsyncStore<T> {
23+
protected matrixClient: MatrixClient;
24+
25+
protected abstract async onAction(payload: ActionPayload);
26+
27+
protected async onReady() {
28+
// Default implementation is to do nothing.
29+
}
30+
31+
protected async onNotReady() {
32+
// Default implementation is to do nothing.
33+
}
34+
35+
protected async onDispatch(payload: ActionPayload) {
36+
await this.onAction(payload);
37+
38+
if (payload.action === 'MatrixActions.sync') {
39+
// Filter out anything that isn't the first PREPARED sync.
40+
if (!(payload.prevState === 'PREPARED' && payload.state !== 'PREPARED')) {
41+
return;
42+
}
43+
44+
this.matrixClient = payload.matrixClient;
45+
await this.onReady();
46+
} else if (payload.action === 'on_client_not_viable' || payload.action === 'on_logged_out') {
47+
if (this.matrixClient) {
48+
await this.onNotReady();
49+
this.matrixClient = null;
50+
}
51+
}
52+
}
53+
}

0 commit comments

Comments
 (0)