Replace breadcrumbs with recently viewed menu (#7073)
This commit is contained in:
parent
757d473971
commit
4a6d46b76a
15 changed files with 992 additions and 34 deletions
|
@ -143,6 +143,7 @@
|
||||||
@import "./views/elements/_ImageView.scss";
|
@import "./views/elements/_ImageView.scss";
|
||||||
@import "./views/elements/_InfoTooltip.scss";
|
@import "./views/elements/_InfoTooltip.scss";
|
||||||
@import "./views/elements/_InlineSpinner.scss";
|
@import "./views/elements/_InlineSpinner.scss";
|
||||||
|
@import "./views/elements/_InteractiveTooltip.scss";
|
||||||
@import "./views/elements/_InviteReason.scss";
|
@import "./views/elements/_InviteReason.scss";
|
||||||
@import "./views/elements/_ManageIntegsButton.scss";
|
@import "./views/elements/_ManageIntegsButton.scss";
|
||||||
@import "./views/elements/_MiniAvatarUploader.scss";
|
@import "./views/elements/_MiniAvatarUploader.scss";
|
||||||
|
@ -230,6 +231,7 @@
|
||||||
@import "./views/rooms/_NotificationBadge.scss";
|
@import "./views/rooms/_NotificationBadge.scss";
|
||||||
@import "./views/rooms/_PinnedEventTile.scss";
|
@import "./views/rooms/_PinnedEventTile.scss";
|
||||||
@import "./views/rooms/_PresenceLabel.scss";
|
@import "./views/rooms/_PresenceLabel.scss";
|
||||||
|
@import "./views/rooms/_RecentlyViewedButton.scss";
|
||||||
@import "./views/rooms/_ReplyPreview.scss";
|
@import "./views/rooms/_ReplyPreview.scss";
|
||||||
@import "./views/rooms/_ReplyTile.scss";
|
@import "./views/rooms/_ReplyTile.scss";
|
||||||
@import "./views/rooms/_RoomBreadcrumbs.scss";
|
@import "./views/rooms/_RoomBreadcrumbs.scss";
|
||||||
|
|
|
@ -37,6 +37,7 @@ limitations under the License.
|
||||||
position: absolute;
|
position: absolute;
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
z-index: 5001;
|
z-index: 5001;
|
||||||
|
width: max-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ContextualMenu_right {
|
.mx_ContextualMenu_right {
|
||||||
|
|
|
@ -133,7 +133,8 @@ $roomListCollapsedWidth: 68px;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
& + .mx_LeftPanel_exploreButton {
|
& + .mx_LeftPanel_exploreButton,
|
||||||
|
& + .mx_LeftPanel_recentsButton {
|
||||||
// Cheaty way to return the occupied space to the filter input
|
// Cheaty way to return the occupied space to the filter input
|
||||||
flex-basis: 0;
|
flex-basis: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -166,11 +167,12 @@ $roomListCollapsedWidth: 68px;
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
mask-size: contain;
|
mask-size: contain;
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
background: $secondary-content;
|
background-color: $secondary-content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_LeftPanel_exploreButton {
|
.mx_LeftPanel_exploreButton,
|
||||||
|
.mx_LeftPanel_recentsButton {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
@ -185,11 +187,10 @@ $roomListCollapsedWidth: 68px;
|
||||||
left: 8px;
|
left: 8px;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
|
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
mask-size: contain;
|
mask-size: contain;
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
background: $secondary-content;
|
background-color: $secondary-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -200,6 +201,14 @@ $roomListCollapsedWidth: 68px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_LeftPanel_exploreButton::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LeftPanel_recentsButton::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/clock.svg');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_LeftPanel_roomListFilterCount {
|
.mx_LeftPanel_roomListFilterCount {
|
||||||
|
@ -257,7 +266,8 @@ $roomListCollapsedWidth: 68px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_LeftPanel_exploreButton {
|
.mx_LeftPanel_exploreButton,
|
||||||
|
.mx_LeftPanel_recentsButton {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,11 +27,17 @@ limitations under the License.
|
||||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=255139
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=255139
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
|
&.mx_RoomAvatar_isSpaceRoom {
|
||||||
|
&.mx_BaseAvatar_image, .mx_BaseAvatar_image {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_BaseAvatar_initial {
|
.mx_BaseAvatar_initial {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0px;
|
left: 0;
|
||||||
color: $avatar-initial-color;
|
color: $avatar-initial-color;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
speak: none;
|
speak: none;
|
||||||
|
|
97
res/css/views/elements/_InteractiveTooltip.scss
Normal file
97
res/css/views/elements/_InteractiveTooltip.scss
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 - 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.
|
||||||
|
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_InteractiveTooltip_wrapper {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 5000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InteractiveTooltip {
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: $background;
|
||||||
|
color: $primary-content;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 5001;
|
||||||
|
box-shadow: 0 24px 8px rgb(17 17 26 / 4%), 0 8px 32px rgb(17 17 26 / 4%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_top {
|
||||||
|
top: 10px; // 8px chevron + 2px spacing
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_left {
|
||||||
|
left: 10px; // 8px chevron + 2px spacing
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_right {
|
||||||
|
right: 10px; // 8px chevron + 2px spacing
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_bottom {
|
||||||
|
bottom: 10px; // 8px chevron + 2px spacing
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InteractiveTooltip_chevron_top {
|
||||||
|
position: absolute;
|
||||||
|
left: calc(50% - 8px);
|
||||||
|
top: -8px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 8px solid transparent;
|
||||||
|
border-bottom: 8px solid $background;
|
||||||
|
border-right: 8px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adapted from https://codyhouse.co/blog/post/css-rounded-triangles-with-clip-path
|
||||||
|
// by Sebastiano Guerriero (@guerriero_se)
|
||||||
|
@supports (clip-path: polygon(0% 0%, 100% 100%, 0% 100%)) {
|
||||||
|
.mx_InteractiveTooltip_chevron_top {
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
background-color: inherit;
|
||||||
|
border: none;
|
||||||
|
clip-path: polygon(0% 0%, 100% 100%, 0% 100%);
|
||||||
|
transform: rotate(135deg);
|
||||||
|
border-radius: 0 0 0 3px;
|
||||||
|
top: calc(-8px / 1.414); // sqrt(2) because of rotation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InteractiveTooltip_chevron_bottom {
|
||||||
|
position: absolute;
|
||||||
|
left: calc(50% - 8px);
|
||||||
|
bottom: -8px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 8px solid transparent;
|
||||||
|
border-top: 8px solid $background;
|
||||||
|
border-right: 8px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adapted from https://codyhouse.co/blog/post/css-rounded-triangles-with-clip-path
|
||||||
|
// by Sebastiano Guerriero (@guerriero_se)
|
||||||
|
@supports (clip-path: polygon(0% 0%, 100% 100%, 0% 100%)) {
|
||||||
|
.mx_InteractiveTooltip_chevron_bottom {
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
background-color: inherit;
|
||||||
|
border: none;
|
||||||
|
clip-path: polygon(0% 0%, 100% 100%, 0% 100%);
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
border-radius: 0 0 0 3px;
|
||||||
|
bottom: calc(-8px / 1.414); // sqrt(2) because of rotation
|
||||||
|
}
|
||||||
|
}
|
73
res/css/views/rooms/_RecentlyViewedButton.scss
Normal file
73
res/css/views/rooms/_RecentlyViewedButton.scss
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
Copyright 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.
|
||||||
|
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_RecentlyViewedButton_ContextMenu {
|
||||||
|
padding: 16px 8px 16px 16px;
|
||||||
|
width: max-content;
|
||||||
|
max-width: 240px;
|
||||||
|
max-height: 400px;
|
||||||
|
border: 1px solid rgba($primary-content, .1);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 8px 4px rgba(0, 0, 0, 0.08);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
> h4 {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton {
|
||||||
|
margin-top: 2px;
|
||||||
|
padding: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
min-height: 34px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $panel-actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_BaseAvatar {
|
||||||
|
margin-right: 8px;
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RecentlyViewedButton_entry_label {
|
||||||
|
display: grid;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RecentlyViewedButton_entry_spaces {
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
color: $secondary-content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4
res/img/element-icons/clock.svg
Normal file
4
res/img/element-icons/clock.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="8.625" cy="8.625" r="6.375" stroke="#C1C6CD" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M8.25 5.625V9.375H11.625" stroke="#C1C6CD" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 320 B |
|
@ -25,12 +25,7 @@ import CallHandler from "../../CallHandler";
|
||||||
import { HEADER_HEIGHT } from "../views/rooms/RoomSublist";
|
import { HEADER_HEIGHT } from "../views/rooms/RoomSublist";
|
||||||
import { Action } from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
import RoomSearch from "./RoomSearch";
|
import RoomSearch from "./RoomSearch";
|
||||||
import RoomBreadcrumbs from "../views/rooms/RoomBreadcrumbs";
|
|
||||||
import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
|
|
||||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
|
||||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore";
|
|
||||||
import IndicatorScrollbar from "../structures/IndicatorScrollbar";
|
|
||||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||||
import LeftPanelWidget from "./LeftPanelWidget";
|
import LeftPanelWidget from "./LeftPanelWidget";
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
|
@ -41,14 +36,27 @@ import UIStore from "../../stores/UIStore";
|
||||||
import { findSiblingElement, IState as IRovingTabIndexState } from "../../accessibility/RovingTabIndex";
|
import { findSiblingElement, IState as IRovingTabIndexState } from "../../accessibility/RovingTabIndex";
|
||||||
import RoomListHeader from "../views/rooms/RoomListHeader";
|
import RoomListHeader from "../views/rooms/RoomListHeader";
|
||||||
import { Key } from "../../Keyboard";
|
import { Key } from "../../Keyboard";
|
||||||
|
import RecentlyViewedButton from "../views/rooms/RecentlyViewedButton";
|
||||||
|
import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
|
||||||
|
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore";
|
||||||
|
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||||
|
import IndicatorScrollbar from "./IndicatorScrollbar";
|
||||||
|
import RoomBreadcrumbs from "../views/rooms/RoomBreadcrumbs";
|
||||||
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
resizeNotifier: ResizeNotifier;
|
resizeNotifier: ResizeNotifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum BreadcrumbsMode {
|
||||||
|
Disabled,
|
||||||
|
Legacy,
|
||||||
|
Labs,
|
||||||
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
showBreadcrumbs: boolean;
|
showBreadcrumbs: BreadcrumbsMode;
|
||||||
activeSpace: SpaceKey;
|
activeSpace: SpaceKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,8 +73,8 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
showBreadcrumbs: BreadcrumbsStore.instance.visible,
|
|
||||||
activeSpace: SpaceStore.instance.activeSpace,
|
activeSpace: SpaceStore.instance.activeSpace,
|
||||||
|
showBreadcrumbs: LeftPanel.breadcrumbsMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||||
|
@ -74,6 +82,11 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
|
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static get breadcrumbsMode(): BreadcrumbsMode {
|
||||||
|
if (!SettingsStore.getValue("breadcrumbs")) return BreadcrumbsMode.Disabled;
|
||||||
|
return SettingsStore.getValue("feature_breadcrumbs_v2") ? BreadcrumbsMode.Labs : BreadcrumbsMode.Legacy;
|
||||||
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
UIStore.instance.trackElementDimensions("LeftPanel", this.ref.current);
|
UIStore.instance.trackElementDimensions("LeftPanel", this.ref.current);
|
||||||
UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current);
|
UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current);
|
||||||
|
@ -116,7 +129,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private onBreadcrumbsUpdate = () => {
|
private onBreadcrumbsUpdate = () => {
|
||||||
const newVal = BreadcrumbsStore.instance.visible;
|
const newVal = LeftPanel.breadcrumbsMode;
|
||||||
if (newVal !== this.state.showBreadcrumbs) {
|
if (newVal !== this.state.showBreadcrumbs) {
|
||||||
this.setState({ showBreadcrumbs: newVal });
|
this.setState({ showBreadcrumbs: newVal });
|
||||||
|
|
||||||
|
@ -323,7 +336,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderBreadcrumbs(): React.ReactNode {
|
private renderBreadcrumbs(): React.ReactNode {
|
||||||
if (this.state.showBreadcrumbs && !this.props.isMinimized) {
|
if (this.state.showBreadcrumbs === BreadcrumbsMode.Legacy && !this.props.isMinimized) {
|
||||||
return (
|
return (
|
||||||
<IndicatorScrollbar
|
<IndicatorScrollbar
|
||||||
className="mx_LeftPanel_breadcrumbsContainer mx_AutoHideScrollbar"
|
className="mx_LeftPanel_breadcrumbsContainer mx_AutoHideScrollbar"
|
||||||
|
@ -349,6 +362,17 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let rightButton: JSX.Element;
|
||||||
|
if (this.state.showBreadcrumbs === BreadcrumbsMode.Labs) {
|
||||||
|
rightButton = <RecentlyViewedButton />;
|
||||||
|
} else if (this.state.activeSpace === MetaSpace.Home) {
|
||||||
|
rightButton = <AccessibleTooltipButton
|
||||||
|
className="mx_LeftPanel_exploreButton"
|
||||||
|
onClick={this.onExplore}
|
||||||
|
title={_t("Explore rooms")}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="mx_LeftPanel_filterContainer"
|
className="mx_LeftPanel_filterContainer"
|
||||||
|
@ -363,12 +387,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{ dialPadButton }
|
{ dialPadButton }
|
||||||
|
{ rightButton }
|
||||||
{ this.state.activeSpace === MetaSpace.Home && <AccessibleTooltipButton
|
|
||||||
className="mx_LeftPanel_exploreButton"
|
|
||||||
onClick={this.onExplore}
|
|
||||||
title={_t("Explore rooms")}
|
|
||||||
/> }
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
496
src/components/views/elements/InteractiveTooltip.tsx
Normal file
496
src/components/views/elements/InteractiveTooltip.tsx
Normal file
|
@ -0,0 +1,496 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 - 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.
|
||||||
|
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, { CSSProperties, MouseEventHandler, ReactNode, RefCallback } from "react";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
import UIStore from "../../../stores/UIStore";
|
||||||
|
import { ChevronFace } from "../../structures/ContextMenu";
|
||||||
|
|
||||||
|
const InteractiveTooltipContainerId = "mx_InteractiveTooltip_Container";
|
||||||
|
|
||||||
|
// If the distance from tooltip to window edge is below this value, the tooltip
|
||||||
|
// will flip around to the other side of the target.
|
||||||
|
const MIN_SAFE_DISTANCE_TO_WINDOW_EDGE = 20;
|
||||||
|
|
||||||
|
function getOrCreateContainer(): HTMLElement {
|
||||||
|
let container = document.getElementById(InteractiveTooltipContainerId);
|
||||||
|
|
||||||
|
if (!container) {
|
||||||
|
container = document.createElement("div");
|
||||||
|
container.id = InteractiveTooltipContainerId;
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IRect {
|
||||||
|
top: number;
|
||||||
|
right: number;
|
||||||
|
bottom: number;
|
||||||
|
left: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInRect(x: number, y: number, rect: IRect): boolean {
|
||||||
|
const { top, right, bottom, left } = rect;
|
||||||
|
return x >= left && x <= right && y >= top && y <= bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the positive slope of the diagonal of the rect.
|
||||||
|
*
|
||||||
|
* @param {DOMRect} rect
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
function getDiagonalSlope(rect: IRect): number {
|
||||||
|
const { top, right, bottom, left } = rect;
|
||||||
|
return (bottom - top) / (right - left);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInUpperLeftHalf(x: number, y: number, rect: IRect): boolean {
|
||||||
|
const { bottom, left } = rect;
|
||||||
|
// Negative slope because Y values grow downwards and for this case, the
|
||||||
|
// diagonal goes from larger to smaller Y values.
|
||||||
|
const diagonalSlope = getDiagonalSlope(rect) * -1;
|
||||||
|
return isInRect(x, y, rect) && (y <= bottom + diagonalSlope * (x - left));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInLowerRightHalf(x: number, y: number, rect: IRect): boolean {
|
||||||
|
const { bottom, left } = rect;
|
||||||
|
// Negative slope because Y values grow downwards and for this case, the
|
||||||
|
// diagonal goes from larger to smaller Y values.
|
||||||
|
const diagonalSlope = getDiagonalSlope(rect) * -1;
|
||||||
|
return isInRect(x, y, rect) && (y >= bottom + diagonalSlope * (x - left));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInUpperRightHalf(x: number, y: number, rect: IRect): boolean {
|
||||||
|
const { top, left } = rect;
|
||||||
|
// Positive slope because Y values grow downwards and for this case, the
|
||||||
|
// diagonal goes from smaller to larger Y values.
|
||||||
|
const diagonalSlope = getDiagonalSlope(rect) * 1;
|
||||||
|
return isInRect(x, y, rect) && (y <= top + diagonalSlope * (x - left));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInLowerLeftHalf(x: number, y: number, rect: IRect): boolean {
|
||||||
|
const { top, left } = rect;
|
||||||
|
// Positive slope because Y values grow downwards and for this case, the
|
||||||
|
// diagonal goes from smaller to larger Y values.
|
||||||
|
const diagonalSlope = getDiagonalSlope(rect) * 1;
|
||||||
|
return isInRect(x, y, rect) && (y >= top + diagonalSlope * (x - left));
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Direction {
|
||||||
|
Top,
|
||||||
|
Left,
|
||||||
|
Bottom,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
// exported for tests
|
||||||
|
export function mouseWithinRegion(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
direction: Direction,
|
||||||
|
targetRect: DOMRect,
|
||||||
|
contentRect: DOMRect,
|
||||||
|
): boolean {
|
||||||
|
// When moving the mouse from the target to the tooltip, we create a safe area
|
||||||
|
// that includes the tooltip, the target, and the trapezoid ABCD between them:
|
||||||
|
// ┌───────────┐
|
||||||
|
// │ │
|
||||||
|
// │ │
|
||||||
|
// A └───E───F───┘ B
|
||||||
|
// V
|
||||||
|
// ┌─┐
|
||||||
|
// │ │
|
||||||
|
// C└─┘D
|
||||||
|
//
|
||||||
|
// As long as the mouse remains inside the safe area, the tooltip will stay open.
|
||||||
|
const buffer = 50;
|
||||||
|
if (isInRect(x, y, targetRect)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (direction) {
|
||||||
|
case Direction.Left: {
|
||||||
|
const contentRectWithBuffer = {
|
||||||
|
top: contentRect.top - buffer,
|
||||||
|
right: contentRect.right,
|
||||||
|
bottom: contentRect.bottom + buffer,
|
||||||
|
left: contentRect.left - buffer,
|
||||||
|
};
|
||||||
|
const trapezoidTop = {
|
||||||
|
top: contentRect.top - buffer,
|
||||||
|
right: targetRect.right,
|
||||||
|
bottom: targetRect.top,
|
||||||
|
left: contentRect.right,
|
||||||
|
};
|
||||||
|
const trapezoidCenter = {
|
||||||
|
top: targetRect.top,
|
||||||
|
right: targetRect.left,
|
||||||
|
bottom: targetRect.bottom,
|
||||||
|
left: contentRect.right,
|
||||||
|
};
|
||||||
|
const trapezoidBottom = {
|
||||||
|
top: targetRect.bottom,
|
||||||
|
right: targetRect.right,
|
||||||
|
bottom: contentRect.bottom + buffer,
|
||||||
|
left: contentRect.right,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
isInRect(x, y, contentRectWithBuffer) ||
|
||||||
|
isInLowerLeftHalf(x, y, trapezoidTop) ||
|
||||||
|
isInRect(x, y, trapezoidCenter) ||
|
||||||
|
isInUpperLeftHalf(x, y, trapezoidBottom)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case Direction.Right: {
|
||||||
|
const contentRectWithBuffer = {
|
||||||
|
top: contentRect.top - buffer,
|
||||||
|
right: contentRect.right + buffer,
|
||||||
|
bottom: contentRect.bottom + buffer,
|
||||||
|
left: contentRect.left,
|
||||||
|
};
|
||||||
|
const trapezoidTop = {
|
||||||
|
top: contentRect.top - buffer,
|
||||||
|
right: contentRect.left,
|
||||||
|
bottom: targetRect.top,
|
||||||
|
left: targetRect.left,
|
||||||
|
};
|
||||||
|
const trapezoidCenter = {
|
||||||
|
top: targetRect.top,
|
||||||
|
right: contentRect.left,
|
||||||
|
bottom: targetRect.bottom,
|
||||||
|
left: targetRect.right,
|
||||||
|
};
|
||||||
|
const trapezoidBottom = {
|
||||||
|
top: targetRect.bottom,
|
||||||
|
right: contentRect.left,
|
||||||
|
bottom: contentRect.bottom + buffer,
|
||||||
|
left: targetRect.left,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
isInRect(x, y, contentRectWithBuffer) ||
|
||||||
|
isInLowerRightHalf(x, y, trapezoidTop) ||
|
||||||
|
isInRect(x, y, trapezoidCenter) ||
|
||||||
|
isInUpperRightHalf(x, y, trapezoidBottom)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case Direction.Top: {
|
||||||
|
const contentRectWithBuffer = {
|
||||||
|
top: contentRect.top - buffer,
|
||||||
|
right: contentRect.right + buffer,
|
||||||
|
bottom: contentRect.bottom,
|
||||||
|
left: contentRect.left - buffer,
|
||||||
|
};
|
||||||
|
const trapezoidLeft = {
|
||||||
|
top: contentRect.bottom,
|
||||||
|
right: targetRect.left,
|
||||||
|
bottom: targetRect.bottom,
|
||||||
|
left: contentRect.left - buffer,
|
||||||
|
};
|
||||||
|
const trapezoidCenter = {
|
||||||
|
top: contentRect.bottom,
|
||||||
|
right: targetRect.right,
|
||||||
|
bottom: targetRect.top,
|
||||||
|
left: targetRect.left,
|
||||||
|
};
|
||||||
|
const trapezoidRight = {
|
||||||
|
top: contentRect.bottom,
|
||||||
|
right: contentRect.right + buffer,
|
||||||
|
bottom: targetRect.bottom,
|
||||||
|
left: targetRect.right,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
isInRect(x, y, contentRectWithBuffer) ||
|
||||||
|
isInUpperRightHalf(x, y, trapezoidLeft) ||
|
||||||
|
isInRect(x, y, trapezoidCenter) ||
|
||||||
|
isInUpperLeftHalf(x, y, trapezoidRight)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case Direction.Bottom: {
|
||||||
|
const contentRectWithBuffer = {
|
||||||
|
top: contentRect.top,
|
||||||
|
right: contentRect.right + buffer,
|
||||||
|
bottom: contentRect.bottom + buffer,
|
||||||
|
left: contentRect.left - buffer,
|
||||||
|
};
|
||||||
|
const trapezoidLeft = {
|
||||||
|
top: targetRect.top,
|
||||||
|
right: targetRect.left,
|
||||||
|
bottom: contentRect.top,
|
||||||
|
left: contentRect.left - buffer,
|
||||||
|
};
|
||||||
|
const trapezoidCenter = {
|
||||||
|
top: targetRect.bottom,
|
||||||
|
right: targetRect.right,
|
||||||
|
bottom: contentRect.top,
|
||||||
|
left: targetRect.left,
|
||||||
|
};
|
||||||
|
const trapezoidRight = {
|
||||||
|
top: targetRect.top,
|
||||||
|
right: contentRect.right + buffer,
|
||||||
|
bottom: contentRect.top,
|
||||||
|
left: targetRect.right,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
isInRect(x, y, contentRectWithBuffer) ||
|
||||||
|
isInLowerRightHalf(x, y, trapezoidLeft) ||
|
||||||
|
isInRect(x, y, trapezoidCenter) ||
|
||||||
|
isInLowerLeftHalf(x, y, trapezoidRight)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
children(props: {
|
||||||
|
ref: RefCallback<HTMLElement>;
|
||||||
|
onMouseOver: MouseEventHandler;
|
||||||
|
}): ReactNode;
|
||||||
|
// Content to show in the tooltip
|
||||||
|
content: ReactNode;
|
||||||
|
direction?: Direction;
|
||||||
|
// Function to call when visibility of the tooltip changes
|
||||||
|
onVisibilityChange?(visible: boolean): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
contentRect: DOMRect;
|
||||||
|
visible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This style of tooltip takes a "target" element as its child and centers the
|
||||||
|
* tooltip along one edge of the target.
|
||||||
|
*/
|
||||||
|
export default class InteractiveTooltip extends React.Component<IProps, IState> {
|
||||||
|
private target: HTMLElement;
|
||||||
|
|
||||||
|
public static defaultProps = {
|
||||||
|
side: Direction.Top,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
contentRect: null,
|
||||||
|
visible: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
// Whenever this passthrough component updates, also render the tooltip
|
||||||
|
// in a separate DOM tree. This allows the tooltip content to participate
|
||||||
|
// the normal React rendering cycle: when this component re-renders, the
|
||||||
|
// tooltip content re-renders.
|
||||||
|
// Once we upgrade to React 16, this could be done a bit more naturally
|
||||||
|
// using the portals feature instead.
|
||||||
|
this.renderTooltip();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
document.removeEventListener("mousemove", this.onMouseMove);
|
||||||
|
}
|
||||||
|
|
||||||
|
private collectContentRect = (element: HTMLElement): void => {
|
||||||
|
// We don't need to clean up when unmounting, so ignore
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
contentRect: element.getBoundingClientRect(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private collectTarget = (element: HTMLElement) => {
|
||||||
|
this.target = element;
|
||||||
|
};
|
||||||
|
|
||||||
|
private onLeftOfTarget(): boolean {
|
||||||
|
const { contentRect } = this.state;
|
||||||
|
const targetRect = this.target.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (this.props.direction === Direction.Left) {
|
||||||
|
const targetLeft = targetRect.left + window.pageXOffset;
|
||||||
|
return !contentRect || (targetLeft - contentRect.width > MIN_SAFE_DISTANCE_TO_WINDOW_EDGE);
|
||||||
|
} else {
|
||||||
|
const targetRight = targetRect.right + window.pageXOffset;
|
||||||
|
const spaceOnRight = UIStore.instance.windowWidth - targetRight;
|
||||||
|
return !contentRect || (spaceOnRight - contentRect.width < MIN_SAFE_DISTANCE_TO_WINDOW_EDGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private aboveTarget(): boolean {
|
||||||
|
const { contentRect } = this.state;
|
||||||
|
const targetRect = this.target.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (this.props.direction === Direction.Top) {
|
||||||
|
const targetTop = targetRect.top + window.pageYOffset;
|
||||||
|
return !contentRect || (targetTop - contentRect.height > MIN_SAFE_DISTANCE_TO_WINDOW_EDGE);
|
||||||
|
} else {
|
||||||
|
const targetBottom = targetRect.bottom + window.pageYOffset;
|
||||||
|
const spaceBelow = UIStore.instance.windowHeight - targetBottom;
|
||||||
|
return !contentRect || (spaceBelow - contentRect.height < MIN_SAFE_DISTANCE_TO_WINDOW_EDGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private get isOnTheSide(): boolean {
|
||||||
|
return this.props.direction === Direction.Left || this.props.direction === Direction.Right;
|
||||||
|
}
|
||||||
|
|
||||||
|
private onMouseMove = (ev: MouseEvent) => {
|
||||||
|
const { clientX: x, clientY: y } = ev;
|
||||||
|
const { contentRect } = this.state;
|
||||||
|
const targetRect = this.target.getBoundingClientRect();
|
||||||
|
|
||||||
|
let direction: Direction;
|
||||||
|
if (this.isOnTheSide) {
|
||||||
|
direction = this.onLeftOfTarget() ? Direction.Left : Direction.Right;
|
||||||
|
} else {
|
||||||
|
direction = this.aboveTarget() ? Direction.Top : Direction.Bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mouseWithinRegion(x, y, direction, targetRect, contentRect)) {
|
||||||
|
this.hideTooltip();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private onTargetMouseOver = (): void => {
|
||||||
|
this.showTooltip();
|
||||||
|
};
|
||||||
|
|
||||||
|
private showTooltip(): void {
|
||||||
|
// Don't enter visible state if we haven't collected the target yet
|
||||||
|
if (!this.target) return;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
visible: true,
|
||||||
|
});
|
||||||
|
this.props.onVisibilityChange?.(true);
|
||||||
|
document.addEventListener("mousemove", this.onMouseMove);
|
||||||
|
}
|
||||||
|
|
||||||
|
public hideTooltip() {
|
||||||
|
this.setState({
|
||||||
|
visible: false,
|
||||||
|
});
|
||||||
|
this.props.onVisibilityChange?.(false);
|
||||||
|
document.removeEventListener("mousemove", this.onMouseMove);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderTooltip() {
|
||||||
|
const { contentRect, visible } = this.state;
|
||||||
|
if (!visible) {
|
||||||
|
ReactDOM.unmountComponentAtNode(getOrCreateContainer());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetRect = this.target.getBoundingClientRect();
|
||||||
|
|
||||||
|
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||||
|
const targetLeft = targetRect.left + window.pageXOffset;
|
||||||
|
const targetRight = targetRect.right + window.pageXOffset;
|
||||||
|
const targetBottom = targetRect.bottom + window.pageYOffset;
|
||||||
|
const targetTop = targetRect.top + window.pageYOffset;
|
||||||
|
|
||||||
|
// Place the tooltip above the target by default. If we find that the
|
||||||
|
// tooltip content would extend past the safe area towards the window
|
||||||
|
// edge, flip around to below the target.
|
||||||
|
const position: Partial<IRect> = {};
|
||||||
|
let chevronFace: ChevronFace = null;
|
||||||
|
if (this.isOnTheSide) {
|
||||||
|
if (this.onLeftOfTarget()) {
|
||||||
|
position.left = targetLeft;
|
||||||
|
chevronFace = ChevronFace.Right;
|
||||||
|
} else {
|
||||||
|
position.left = targetRight;
|
||||||
|
chevronFace = ChevronFace.Left;
|
||||||
|
}
|
||||||
|
|
||||||
|
position.top = targetTop;
|
||||||
|
} else {
|
||||||
|
if (this.aboveTarget()) {
|
||||||
|
position.bottom = UIStore.instance.windowHeight - targetTop;
|
||||||
|
chevronFace = ChevronFace.Bottom;
|
||||||
|
} else {
|
||||||
|
position.top = targetBottom;
|
||||||
|
chevronFace = ChevronFace.Top;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center the tooltip horizontally with the target's center.
|
||||||
|
position.left = targetLeft + targetRect.width / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chevron = <div className={"mx_InteractiveTooltip_chevron_" + chevronFace} />;
|
||||||
|
|
||||||
|
const menuClasses = classNames({
|
||||||
|
'mx_InteractiveTooltip': true,
|
||||||
|
'mx_InteractiveTooltip_withChevron_top': chevronFace === ChevronFace.Top,
|
||||||
|
'mx_InteractiveTooltip_withChevron_left': chevronFace === ChevronFace.Left,
|
||||||
|
'mx_InteractiveTooltip_withChevron_right': chevronFace === ChevronFace.Right,
|
||||||
|
'mx_InteractiveTooltip_withChevron_bottom': chevronFace === ChevronFace.Bottom,
|
||||||
|
});
|
||||||
|
|
||||||
|
const menuStyle: CSSProperties = {};
|
||||||
|
if (contentRect && !this.isOnTheSide) {
|
||||||
|
menuStyle.left = `-${contentRect.width / 2}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tooltip = <div className="mx_InteractiveTooltip_wrapper" style={{ ...position }}>
|
||||||
|
<div className={menuClasses} style={menuStyle} ref={this.collectContentRect}>
|
||||||
|
{ chevron }
|
||||||
|
{ this.props.content }
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
|
||||||
|
ReactDOM.render(tooltip, getOrCreateContainer());
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return this.props.children({
|
||||||
|
ref: this.collectTarget,
|
||||||
|
onMouseOver: this.onTargetMouseOver,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
73
src/components/views/rooms/RecentlyViewedButton.tsx
Normal file
73
src/components/views/rooms/RecentlyViewedButton.tsx
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
Copyright 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.
|
||||||
|
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, { useRef } from "react";
|
||||||
|
|
||||||
|
import { BreadcrumbsStore } from "../../../stores/BreadcrumbsStore";
|
||||||
|
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||||
|
import { MenuItem } from "../../structures/ContextMenu";
|
||||||
|
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
|
import InteractiveTooltip, { Direction } from "../elements/InteractiveTooltip";
|
||||||
|
import { roomContextDetailsText } from "../../../Rooms";
|
||||||
|
|
||||||
|
const RecentlyViewedButton = () => {
|
||||||
|
const tooltipRef = useRef<InteractiveTooltip>();
|
||||||
|
const crumbs = useEventEmitterState(BreadcrumbsStore.instance, UPDATE_EVENT, () => BreadcrumbsStore.instance.rooms);
|
||||||
|
|
||||||
|
const content = <div className="mx_RecentlyViewedButton_ContextMenu">
|
||||||
|
<h4>{ _t("Recently viewed") }</h4>
|
||||||
|
<div>
|
||||||
|
{ crumbs.map(crumb => {
|
||||||
|
const contextDetails = roomContextDetailsText(crumb);
|
||||||
|
|
||||||
|
return <MenuItem
|
||||||
|
key={crumb.roomId}
|
||||||
|
onClick={() => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: "view_room",
|
||||||
|
room_id: crumb.roomId,
|
||||||
|
});
|
||||||
|
tooltipRef.current?.hideTooltip();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RoomAvatar room={crumb} width={24} height={24} />
|
||||||
|
<span className="mx_RecentlyViewedButton_entry_label">
|
||||||
|
<div>{ crumb.name }</div>
|
||||||
|
{ contextDetails && <div className="mx_RecentlyViewedButton_entry_spaces">
|
||||||
|
{ contextDetails }
|
||||||
|
</div> }
|
||||||
|
</span>
|
||||||
|
</MenuItem>;
|
||||||
|
}) }
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
|
||||||
|
return <InteractiveTooltip content={content} direction={Direction.Right} ref={tooltipRef}>
|
||||||
|
{ ({ ref, onMouseOver }) => (
|
||||||
|
<span
|
||||||
|
className="mx_LeftPanel_recentsButton"
|
||||||
|
title={_t("Recently viewed")}
|
||||||
|
ref={ref}
|
||||||
|
onMouseOver={onMouseOver}
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
</InteractiveTooltip>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RecentlyViewedButton;
|
|
@ -332,10 +332,12 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
||||||
<div className="mx_SettingsTab mx_PreferencesUserSettingsTab">
|
<div className="mx_SettingsTab mx_PreferencesUserSettingsTab">
|
||||||
<div className="mx_SettingsTab_heading">{ _t("Preferences") }</div>
|
<div className="mx_SettingsTab_heading">{ _t("Preferences") }</div>
|
||||||
|
|
||||||
<div className="mx_SettingsTab_section">
|
{ !SettingsStore.getValue("feature_breadcrumbs_v2") &&
|
||||||
<span className="mx_SettingsTab_subheading">{ _t("Room list") }</span>
|
<div className="mx_SettingsTab_section">
|
||||||
{ this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS) }
|
<span className="mx_SettingsTab_subheading">{ _t("Room list") }</span>
|
||||||
</div>
|
{ this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS) }
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<div className="mx_SettingsTab_section">
|
<div className="mx_SettingsTab_section">
|
||||||
<span className="mx_SettingsTab_subheading">{ _t("Spaces") }</span>
|
<span className="mx_SettingsTab_subheading">{ _t("Spaces") }</span>
|
||||||
|
|
|
@ -850,6 +850,7 @@
|
||||||
"Show info about bridges in room settings": "Show info about bridges in room settings",
|
"Show info about bridges in room settings": "Show info about bridges in room settings",
|
||||||
"New layout switcher (with message bubbles)": "New layout switcher (with message bubbles)",
|
"New layout switcher (with message bubbles)": "New layout switcher (with message bubbles)",
|
||||||
"Meta Spaces": "Meta Spaces",
|
"Meta Spaces": "Meta Spaces",
|
||||||
|
"Use new room breadcrumbs": "Use new room breadcrumbs",
|
||||||
"Don't send read receipts": "Don't send read receipts",
|
"Don't send read receipts": "Don't send read receipts",
|
||||||
"Font size": "Font size",
|
"Font size": "Font size",
|
||||||
"Use custom size": "Use custom size",
|
"Use custom size": "Use custom size",
|
||||||
|
@ -1693,6 +1694,7 @@
|
||||||
"Unknown": "Unknown",
|
"Unknown": "Unknown",
|
||||||
"Seen by %(userName)s at %(dateTime)s": "Seen by %(userName)s at %(dateTime)s",
|
"Seen by %(userName)s at %(dateTime)s": "Seen by %(userName)s at %(dateTime)s",
|
||||||
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Seen by %(displayName)s (%(userName)s) at %(dateTime)s",
|
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Seen by %(displayName)s (%(userName)s) at %(dateTime)s",
|
||||||
|
"Recently viewed": "Recently viewed",
|
||||||
"Replying": "Replying",
|
"Replying": "Replying",
|
||||||
"Room %(name)s": "Room %(name)s",
|
"Room %(name)s": "Room %(name)s",
|
||||||
"Recently visited rooms": "Recently visited rooms",
|
"Recently visited rooms": "Recently visited rooms",
|
||||||
|
|
|
@ -355,6 +355,13 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
new ReloadOnChangeController(),
|
new ReloadOnChangeController(),
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
|
"feature_breadcrumbs_v2": {
|
||||||
|
isFeature: true,
|
||||||
|
labsGroup: LabGroup.Rooms,
|
||||||
|
supportedLevels: LEVELS_FEATURE,
|
||||||
|
displayName: _td("Use new room breadcrumbs"),
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
"RoomList.backgroundImage": {
|
"RoomList.backgroundImage": {
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
default: null,
|
default: null,
|
||||||
|
@ -711,6 +718,7 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
displayName: _td("Show shortcuts to recently viewed rooms above the room list"),
|
displayName: _td("Show shortcuts to recently viewed rooms above the room list"),
|
||||||
default: true,
|
default: true,
|
||||||
|
controller: new IncompatibleController("feature_breadcrumbs_v2", true),
|
||||||
},
|
},
|
||||||
"showHiddenEventsInTimeline": {
|
"showHiddenEventsInTimeline": {
|
||||||
displayName: _td("Show hidden events in timeline"),
|
displayName: _td("Show hidden events in timeline"),
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import SettingsStore from "../settings/SettingsStore";
|
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
||||||
|
|
||||||
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
import { ActionPayload } from "../dispatcher/payloads";
|
import { ActionPayload } from "../dispatcher/payloads";
|
||||||
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
|
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
|
||||||
import defaultDispatcher from "../dispatcher/dispatcher";
|
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||||
import { arrayHasDiff } from "../utils/arrays";
|
import { arrayHasDiff } from "../utils/arrays";
|
||||||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
|
||||||
import { SettingLevel } from "../settings/SettingLevel";
|
import { SettingLevel } from "../settings/SettingLevel";
|
||||||
import SpaceStore from "./spaces/SpaceStore";
|
|
||||||
import { Action } from "../dispatcher/actions";
|
import { Action } from "../dispatcher/actions";
|
||||||
import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayload";
|
import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayload";
|
||||||
|
|
||||||
|
@ -44,6 +44,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
|
||||||
|
|
||||||
SettingsStore.monitorSetting("breadcrumb_rooms", null);
|
SettingsStore.monitorSetting("breadcrumb_rooms", null);
|
||||||
SettingsStore.monitorSetting("breadcrumbs", null);
|
SettingsStore.monitorSetting("breadcrumbs", null);
|
||||||
|
SettingsStore.monitorSetting("feature_breadcrumbs_v2", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static get instance(): BreadcrumbsStore {
|
public static get instance(): BreadcrumbsStore {
|
||||||
|
@ -58,8 +59,9 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
|
||||||
return this.state.enabled && this.meetsRoomRequirement;
|
return this.state.enabled && this.meetsRoomRequirement;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get meetsRoomRequirement(): boolean {
|
public get meetsRoomRequirement(): boolean {
|
||||||
return this.matrixClient && this.matrixClient.getVisibleRooms().length >= 20;
|
if (SettingsStore.getValue("feature_breadcrumbs_v2")) return true;
|
||||||
|
return this.matrixClient?.getVisibleRooms().length >= 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async onAction(payload: ActionPayload) {
|
protected async onAction(payload: ActionPayload) {
|
||||||
|
@ -69,7 +71,9 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
|
||||||
const settingUpdatedPayload = payload as SettingUpdatedPayload;
|
const settingUpdatedPayload = payload as SettingUpdatedPayload;
|
||||||
if (settingUpdatedPayload.settingName === 'breadcrumb_rooms') {
|
if (settingUpdatedPayload.settingName === 'breadcrumb_rooms') {
|
||||||
await this.updateRooms();
|
await this.updateRooms();
|
||||||
} else if (settingUpdatedPayload.settingName === 'breadcrumbs') {
|
} else if (settingUpdatedPayload.settingName === 'breadcrumbs' ||
|
||||||
|
settingUpdatedPayload.settingName === 'feature_breadcrumbs_v2'
|
||||||
|
) {
|
||||||
await this.updateState({ enabled: SettingsStore.getValue("breadcrumbs", null) });
|
await this.updateState({ enabled: SettingsStore.getValue("breadcrumbs", null) });
|
||||||
}
|
}
|
||||||
} else if (payload.action === Action.ViewRoom) {
|
} else if (payload.action === Action.ViewRoom) {
|
||||||
|
@ -126,7 +130,6 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async appendRoom(room: Room) {
|
private async appendRoom(room: Room) {
|
||||||
if (SpaceStore.spacesEnabled && room.isSpaceRoom()) return; // hide space rooms
|
|
||||||
let updated = false;
|
let updated = false;
|
||||||
const rooms = (this.state.rooms || []).slice(); // cheap clone
|
const rooms = (this.state.rooms || []).slice(); // cheap clone
|
||||||
|
|
||||||
|
|
162
test/components/views/elements/InteractiveTooltip-test.ts
Normal file
162
test/components/views/elements/InteractiveTooltip-test.ts
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
/*
|
||||||
|
Copyright 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.
|
||||||
|
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 '../../../skinned-sdk';
|
||||||
|
import { Direction, mouseWithinRegion } from "../../../../src/components/views/elements/InteractiveTooltip";
|
||||||
|
|
||||||
|
describe("InteractiveTooltip", () => {
|
||||||
|
describe("mouseWithinRegion", () => {
|
||||||
|
it("direction=left", () => {
|
||||||
|
const targetRect = {
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
top: 300,
|
||||||
|
right: 370,
|
||||||
|
bottom: 320,
|
||||||
|
left: 350,
|
||||||
|
} as DOMRect;
|
||||||
|
|
||||||
|
const contentRect = {
|
||||||
|
width: 100,
|
||||||
|
height: 400,
|
||||||
|
top: 100,
|
||||||
|
right: 200,
|
||||||
|
bottom: 500,
|
||||||
|
left: 100,
|
||||||
|
} as DOMRect;
|
||||||
|
|
||||||
|
// just within top left corner of contentRect
|
||||||
|
expect(mouseWithinRegion(101, 101, Direction.Left, targetRect, contentRect)).toBe(true);
|
||||||
|
// just outside top left corner of contentRect, within buffer
|
||||||
|
expect(mouseWithinRegion(101, 90, Direction.Left, targetRect, contentRect)).toBe(true);
|
||||||
|
// just within top right corner of targetRect
|
||||||
|
expect(mouseWithinRegion(369, 301, Direction.Left, targetRect, contentRect)).toBe(true);
|
||||||
|
// within the top triangular portion of the trapezoid
|
||||||
|
expect(mouseWithinRegion(300, 200, Direction.Left, targetRect, contentRect)).toBe(true);
|
||||||
|
// within the bottom triangular portion of the trapezoid
|
||||||
|
expect(mouseWithinRegion(300, 350, Direction.Left, targetRect, contentRect)).toBe(true);
|
||||||
|
// outside the top triangular portion of the trapezoid
|
||||||
|
expect(mouseWithinRegion(300, 140, Direction.Left, targetRect, contentRect)).toBe(false);
|
||||||
|
// outside the bottom triangular portion of the trapezoid
|
||||||
|
expect(mouseWithinRegion(300, 460, Direction.Left, targetRect, contentRect)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("direction=right", () => {
|
||||||
|
const targetRect = {
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
top: 300,
|
||||||
|
right: 370,
|
||||||
|
bottom: 320,
|
||||||
|
left: 350,
|
||||||
|
} as DOMRect;
|
||||||
|
|
||||||
|
const contentRect = {
|
||||||
|
width: 100,
|
||||||
|
height: 400,
|
||||||
|
top: 100,
|
||||||
|
right: 620,
|
||||||
|
bottom: 500,
|
||||||
|
left: 520,
|
||||||
|
} as DOMRect;
|
||||||
|
|
||||||
|
// just within top right corner of contentRect
|
||||||
|
expect(mouseWithinRegion(619, 101, Direction.Right, targetRect, contentRect)).toBe(true);
|
||||||
|
// just outside top right corner of contentRect, within buffer
|
||||||
|
expect(mouseWithinRegion(619, 90, Direction.Right, targetRect, contentRect)).toBe(true);
|
||||||
|
// just within top left corner of targetRect
|
||||||
|
expect(mouseWithinRegion(351, 301, Direction.Right, targetRect, contentRect)).toBe(true);
|
||||||
|
// within the top triangular portion of the trapezoid
|
||||||
|
expect(mouseWithinRegion(420, 200, Direction.Right, targetRect, contentRect)).toBe(true);
|
||||||
|
// within the bottom triangular portion of the trapezoid
|
||||||
|
expect(mouseWithinRegion(420, 350, Direction.Right, targetRect, contentRect)).toBe(true);
|
||||||
|
// outside the top triangular portion of the trapezoid
|
||||||
|
expect(mouseWithinRegion(420, 140, Direction.Right, targetRect, contentRect)).toBe(false);
|
||||||
|
// outside the bottom triangular portion of the trapezoid
|
||||||
|
expect(mouseWithinRegion(420, 460, Direction.Right, targetRect, contentRect)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("direction=top", () => {
|
||||||
|
const targetRect = {
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
top: 300,
|
||||||
|
right: 370,
|
||||||
|
bottom: 320,
|
||||||
|
left: 350,
|
||||||
|
} as DOMRect;
|
||||||
|
|
||||||
|
const contentRect = {
|
||||||
|
width: 400,
|
||||||
|
height: 100,
|
||||||
|
top: 100,
|
||||||
|
right: 550,
|
||||||
|
bottom: 200,
|
||||||
|
left: 150,
|
||||||
|
} as DOMRect;
|
||||||
|
|
||||||
|
// just within top right corner of contentRect
|
||||||
|
expect(mouseWithinRegion(549, 101, Direction.Top, targetRect, contentRect)).toBe(true);
|
||||||
|
// just outside top right corner of contentRect, within buffer
|
||||||
|
expect(mouseWithinRegion(549, 99, Direction.Top, targetRect, contentRect)).toBe(true);
|
||||||
|
// just within bottom left corner of targetRect
|
||||||
|
expect(mouseWithinRegion(351, 319, Direction.Top, targetRect, contentRect)).toBe(true);
|
||||||
|
// within the left triangular portion of the trapezoid
|
||||||
|
expect(mouseWithinRegion(240, 260, Direction.Top, targetRect, contentRect)).toBe(true);
|
||||||
|
// within the right triangular portion of the trapezoid
|
||||||
|
expect(mouseWithinRegion(480, 260, Direction.Top, targetRect, contentRect)).toBe(true);
|
||||||
|
// outside the left triangular portion of the trapezoid
|
||||||
|
expect(mouseWithinRegion(220, 260, Direction.Top, targetRect, contentRect)).toBe(false);
|
||||||
|
// outside the right triangular portion of the trapezoid
|
||||||
|
expect(mouseWithinRegion(500, 260, Direction.Top, targetRect, contentRect)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("direction=bottom", () => {
|
||||||
|
const targetRect = {
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
top: 300,
|
||||||
|
right: 370,
|
||||||
|
bottom: 320,
|
||||||
|
left: 350,
|
||||||
|
} as DOMRect;
|
||||||
|
|
||||||
|
const contentRect = {
|
||||||
|
width: 400,
|
||||||
|
height: 100,
|
||||||
|
top: 420,
|
||||||
|
right: 550,
|
||||||
|
bottom: 520,
|
||||||
|
left: 150,
|
||||||
|
} as DOMRect;
|
||||||
|
|
||||||
|
// just within bottom left corner of contentRect
|
||||||
|
expect(mouseWithinRegion(101, 519, Direction.Bottom, targetRect, contentRect)).toBe(true);
|
||||||
|
// just outside bottom left corner of contentRect, within buffer
|
||||||
|
expect(mouseWithinRegion(101, 521, Direction.Bottom, targetRect, contentRect)).toBe(true);
|
||||||
|
// just within top left corner of targetRect
|
||||||
|
expect(mouseWithinRegion(351, 301, Direction.Bottom, targetRect, contentRect)).toBe(true);
|
||||||
|
// within the left triangular portion of the trapezoid
|
||||||
|
expect(mouseWithinRegion(240, 360, Direction.Bottom, targetRect, contentRect)).toBe(true);
|
||||||
|
// within the right triangular portion of the trapezoid
|
||||||
|
expect(mouseWithinRegion(480, 360, Direction.Bottom, targetRect, contentRect)).toBe(true);
|
||||||
|
// outside the left triangular portion of the trapezoid
|
||||||
|
expect(mouseWithinRegion(220, 360, Direction.Bottom, targetRect, contentRect)).toBe(false);
|
||||||
|
// outside the right triangular portion of the trapezoid
|
||||||
|
expect(mouseWithinRegion(500, 360, Direction.Bottom, targetRect, contentRect)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Add table
Add a link
Reference in a new issue