mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-16 21:24:59 +08:00
Merge pull request #4742 from matrix-org/travis/room-list/hover-state
Add hover states and basic context menu to new room list
This commit is contained in:
commit
64a8767c5a
@ -41,6 +41,36 @@ limitations under the License.
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Both of these buttons are hidden by default until the list is hovered
|
||||||
|
.mx_RoomSublist2_auxButton,
|
||||||
|
.mx_RoomSublist2_menuButton {
|
||||||
|
width: 0;
|
||||||
|
margin: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
left: 4px;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
background: $muted-fg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomSublist2_auxButton::before {
|
||||||
|
mask-image: url('$(res)/img/feather-customised/plus.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomSublist2_menuButton::before {
|
||||||
|
mask-image: url('$(res)/img/feather-customised/more-horizontal.svg');
|
||||||
|
}
|
||||||
|
|
||||||
.mx_RoomSublist2_headerText {
|
.mx_RoomSublist2_headerText {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
@ -132,10 +162,82 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The aforementioned selector for the hover state.
|
// The aforementioned selector for the hover state.
|
||||||
&:hover .react-resizable-handle {
|
&:hover, &.mx_RoomSublist2_hasMenuOpen {
|
||||||
opacity: 0.2;
|
.react-resizable-handle {
|
||||||
|
opacity: 0.2;
|
||||||
|
|
||||||
// Update the render() function for RoomSublist2 if this changes
|
// Update the render() function for RoomSublist2 if this changes
|
||||||
border: 2px solid $primary-fg-color;
|
border: 2px solid $primary-fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomSublist2_headerContainer {
|
||||||
|
// If the header doesn't have an aux button we still need to hide the badge for
|
||||||
|
// the menu button.
|
||||||
|
.mx_RoomSublist2_badgeContainer {
|
||||||
|
// Completely hide the badge
|
||||||
|
width: 0;
|
||||||
|
margin: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.mx_RoomSublist2_headerContainer_withAux) {
|
||||||
|
// The menu button will be the rightmost button, so make it correctly aligned.
|
||||||
|
.mx_RoomSublist2_menuButton {
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both of these buttons have circled backgrounds and are visible at this point,
|
||||||
|
// so make them so.
|
||||||
|
.mx_RoomSublist2_auxButton,
|
||||||
|
.mx_RoomSublist2_menuButton {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 32px;
|
||||||
|
margin-left: 16px;
|
||||||
|
background-color: #fff; // TODO: Variable and theme
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have a hover style on the room list with no specific list hovered, so account for that
|
||||||
|
.mx_RoomList2:hover .mx_RoomSublist2,
|
||||||
|
.mx_RoomSublist2_hasMenuOpen {
|
||||||
|
.mx_RoomSublist2_headerContainer_withAux {
|
||||||
|
.mx_RoomSublist2_badgeContainer {
|
||||||
|
// Completely hide the badge
|
||||||
|
width: 0;
|
||||||
|
margin: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomSublist2_auxButton {
|
||||||
|
// Show the aux button, but not the list button
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin-right: 16px;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomSublist2_contextMenu {
|
||||||
|
padding: 20px 16px;
|
||||||
|
width: 250px;
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin-top: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
margin-right: 16px; // additional 16px
|
||||||
|
border: 1px solid $roomsublist2-divider-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomSublist2_contextMenu_title {
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-20px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ limitations under the License.
|
|||||||
|
|
||||||
.mx_RoomTile2_name {
|
.mx_RoomTile2_name {
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
line-height: $font-19px;
|
line-height: $font-18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomTile2_name.mx_RoomTile2_nameHasUnreadEvents {
|
.mx_RoomTile2_name.mx_RoomTile2_nameHasUnreadEvents {
|
||||||
@ -63,6 +63,10 @@ limitations under the License.
|
|||||||
line-height: $font-18px;
|
line-height: $font-18px;
|
||||||
color: $roomtile2-preview-color;
|
color: $roomtile2-preview-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_RoomTile2_nameWithPreview {
|
||||||
|
margin-top: -4px; // shift the name up a bit more
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomTile2_badgeContainer {
|
.mx_RoomTile2_badgeContainer {
|
||||||
|
@ -178,6 +178,7 @@ $roomtile2-preview-color: #9e9e9e;
|
|||||||
$roomtile2-badge-color: #61708b;
|
$roomtile2-badge-color: #61708b;
|
||||||
$roomtile2-selected-bg-color: #FFF;
|
$roomtile2-selected-bg-color: #FFF;
|
||||||
$theme-button-bg-color: #e3e8f0;
|
$theme-button-bg-color: #e3e8f0;
|
||||||
|
$roomsublist2-divider-color: #e9eaeb;
|
||||||
|
|
||||||
$roomtile-name-color: #61708b;
|
$roomtile-name-color: #61708b;
|
||||||
$roomtile-badge-fg-color: $accent-fg-color;
|
$roomtile-badge-fg-color: $accent-fg-color;
|
||||||
|
@ -200,7 +200,6 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
|||||||
addRoomLabel={aesthetics.addRoomLabel}
|
addRoomLabel={aesthetics.addRoomLabel}
|
||||||
isInvite={aesthetics.isInvite}
|
isInvite={aesthetics.isInvite}
|
||||||
layout={this.state.layouts.get(orderedTagId)}
|
layout={this.state.layouts.get(orderedTagId)}
|
||||||
showMessagePreviews={orderedTagId === DefaultTagID.DM}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,8 @@ import RoomTile2 from "./RoomTile2";
|
|||||||
import { ResizableBox, ResizeCallbackData } from "react-resizable";
|
import { ResizableBox, ResizeCallbackData } from "react-resizable";
|
||||||
import { ListLayout } from "../../../stores/room-list/ListLayout";
|
import { ListLayout } from "../../../stores/room-list/ListLayout";
|
||||||
import NotificationBadge, { ListNotificationState } from "./NotificationBadge";
|
import NotificationBadge, { ListNotificationState } from "./NotificationBadge";
|
||||||
|
import {ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
|
||||||
|
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||||
|
|
||||||
/*******************************************************************
|
/*******************************************************************
|
||||||
* CAUTION *
|
* CAUTION *
|
||||||
@ -41,7 +43,6 @@ interface IProps {
|
|||||||
rooms?: Room[];
|
rooms?: Room[];
|
||||||
startAsHidden: boolean;
|
startAsHidden: boolean;
|
||||||
label: string;
|
label: string;
|
||||||
showMessagePreviews: boolean;
|
|
||||||
onAddRoom?: () => void;
|
onAddRoom?: () => void;
|
||||||
addRoomLabel: string;
|
addRoomLabel: string;
|
||||||
isInvite: boolean;
|
isInvite: boolean;
|
||||||
@ -57,16 +58,19 @@ interface IProps {
|
|||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
notificationState: ListNotificationState;
|
notificationState: ListNotificationState;
|
||||||
|
menuDisplayed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class RoomSublist2 extends React.Component<IProps, IState> {
|
export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
private headerButton = createRef();
|
private headerButton = createRef();
|
||||||
|
private menuButtonRef: React.RefObject<HTMLButtonElement> = createRef();
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
notificationState: new ListNotificationState(this.props.isInvite),
|
notificationState: new ListNotificationState(this.props.isInvite),
|
||||||
|
menuDisplayed: false,
|
||||||
};
|
};
|
||||||
this.state.notificationState.setRooms(this.props.rooms);
|
this.state.notificationState.setRooms(this.props.rooms);
|
||||||
}
|
}
|
||||||
@ -97,6 +101,26 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||||||
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onOpenMenuClick = (ev: InputEvent) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
this.setState({menuDisplayed: true});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onCloseMenu = () => {
|
||||||
|
this.setState({menuDisplayed: false});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onUnreadFirstChanged = () => {
|
||||||
|
// TODO: Support per-list algorithm changes
|
||||||
|
console.log("Unread first changed");
|
||||||
|
};
|
||||||
|
|
||||||
|
private onMessagePreviewChanged = () => {
|
||||||
|
this.props.layout.showPreviews = !this.props.layout.showPreviews;
|
||||||
|
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
||||||
|
};
|
||||||
|
|
||||||
private renderTiles(): React.ReactElement[] {
|
private renderTiles(): React.ReactElement[] {
|
||||||
const tiles: React.ReactElement[] = [];
|
const tiles: React.ReactElement[] = [];
|
||||||
|
|
||||||
@ -106,7 +130,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||||||
<RoomTile2
|
<RoomTile2
|
||||||
room={room}
|
room={room}
|
||||||
key={`room-${room.roomId}`}
|
key={`room-${room.roomId}`}
|
||||||
showMessagePreview={this.props.showMessagePreviews}
|
showMessagePreview={this.props.layout.showPreviews}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -115,6 +139,61 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||||||
return tiles;
|
return tiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderMenu(): React.ReactElement {
|
||||||
|
let contextMenu = null;
|
||||||
|
if (this.state.menuDisplayed) {
|
||||||
|
const elementRect = this.menuButtonRef.current.getBoundingClientRect();
|
||||||
|
contextMenu = (
|
||||||
|
<ContextMenu
|
||||||
|
chevronFace="none"
|
||||||
|
left={elementRect.left}
|
||||||
|
top={elementRect.top + elementRect.height}
|
||||||
|
onFinished={this.onCloseMenu}
|
||||||
|
>
|
||||||
|
<div className="mx_RoomSublist2_contextMenu">
|
||||||
|
<div>
|
||||||
|
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Sort by")}</div>
|
||||||
|
TODO: Radios are blocked by https://github.com/matrix-org/matrix-react-sdk/pull/4731
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<div>
|
||||||
|
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Unread rooms")}</div>
|
||||||
|
<StyledCheckbox
|
||||||
|
onChange={this.onUnreadFirstChanged}
|
||||||
|
checked={false/*TODO*/}
|
||||||
|
>
|
||||||
|
{_t("Always show first")}
|
||||||
|
</StyledCheckbox>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<div>
|
||||||
|
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Show")}</div>
|
||||||
|
<StyledCheckbox
|
||||||
|
onChange={this.onMessagePreviewChanged}
|
||||||
|
checked={this.props.layout.showPreviews}
|
||||||
|
>
|
||||||
|
{_t("Message preview")}
|
||||||
|
</StyledCheckbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ContextMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<ContextMenuButton
|
||||||
|
className="mx_RoomSublist2_menuButton"
|
||||||
|
onClick={this.onOpenMenuClick}
|
||||||
|
inputRef={this.menuButtonRef}
|
||||||
|
label={_t("List options")}
|
||||||
|
isExpanded={this.state.menuDisplayed}
|
||||||
|
/>
|
||||||
|
{contextMenu}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private renderHeader(): React.ReactElement {
|
private renderHeader(): React.ReactElement {
|
||||||
// TODO: Title on collapsed
|
// TODO: Title on collapsed
|
||||||
// TODO: Incoming call box
|
// TODO: Incoming call box
|
||||||
@ -129,22 +208,26 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
const badge = <NotificationBadge allowNoCount={false} notification={this.state.notificationState}/>;
|
const badge = <NotificationBadge allowNoCount={false} notification={this.state.notificationState}/>;
|
||||||
|
|
||||||
// TODO: Aux button
|
let addRoomButton = null;
|
||||||
// let addRoomButton = null;
|
if (!!this.props.onAddRoom) {
|
||||||
// if (!!this.props.onAddRoom) {
|
addRoomButton = (
|
||||||
// addRoomButton = (
|
<AccessibleButton
|
||||||
// <AccessibleTooltipButton
|
tabIndex={tabIndex}
|
||||||
// tabIndex={tabIndex}
|
onClick={this.onAddRoom}
|
||||||
// onClick={this.onAddRoom}
|
className="mx_RoomSublist2_auxButton"
|
||||||
// className="mx_RoomSublist2_addButton"
|
aria-label={this.props.addRoomLabel || _t("Add room")}
|
||||||
// title={this.props.addRoomLabel || _t("Add room")}
|
/>
|
||||||
// />
|
);
|
||||||
// );
|
}
|
||||||
// }
|
|
||||||
|
const classes = classNames({
|
||||||
|
'mx_RoomSublist2_headerContainer': true,
|
||||||
|
'mx_RoomSublist2_headerContainer_withAux': !!addRoomButton,
|
||||||
|
});
|
||||||
|
|
||||||
// TODO: a11y (see old component)
|
// TODO: a11y (see old component)
|
||||||
return (
|
return (
|
||||||
<div className={"mx_RoomSublist2_headerContainer"}>
|
<div className={classes}>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
inputRef={ref}
|
inputRef={ref}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
@ -157,6 +240,8 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||||||
<div className="mx_RoomSublist2_badgeContainer">
|
<div className="mx_RoomSublist2_badgeContainer">
|
||||||
{badge}
|
{badge}
|
||||||
</div>
|
</div>
|
||||||
|
{this.renderMenu()}
|
||||||
|
{addRoomButton}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@ -174,6 +259,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||||||
// TODO: Proper collapse support
|
// TODO: Proper collapse support
|
||||||
'mx_RoomSublist2': true,
|
'mx_RoomSublist2': true,
|
||||||
'mx_RoomSublist2_collapsed': false, // len && isCollapsed
|
'mx_RoomSublist2_collapsed': false, // len && isCollapsed
|
||||||
|
'mx_RoomSublist2_hasMenuOpen': this.state.menuDisplayed,
|
||||||
});
|
});
|
||||||
|
|
||||||
let content = null;
|
let content = null;
|
||||||
|
@ -1135,6 +1135,13 @@
|
|||||||
"Securely back up your keys to avoid losing them. <a>Learn more.</a>": "Securely back up your keys to avoid losing them. <a>Learn more.</a>",
|
"Securely back up your keys to avoid losing them. <a>Learn more.</a>": "Securely back up your keys to avoid losing them. <a>Learn more.</a>",
|
||||||
"Not now": "Not now",
|
"Not now": "Not now",
|
||||||
"Don't ask me again": "Don't ask me again",
|
"Don't ask me again": "Don't ask me again",
|
||||||
|
"Sort by": "Sort by",
|
||||||
|
"Unread rooms": "Unread rooms",
|
||||||
|
"Always show first": "Always show first",
|
||||||
|
"Show": "Show",
|
||||||
|
"Message preview": "Message preview",
|
||||||
|
"List options": "List options",
|
||||||
|
"Add room": "Add room",
|
||||||
"Show %(count)s more|other": "Show %(count)s more",
|
"Show %(count)s more|other": "Show %(count)s more",
|
||||||
"Show %(count)s more|one": "Show %(count)s more",
|
"Show %(count)s more|one": "Show %(count)s more",
|
||||||
"Options": "Options",
|
"Options": "Options",
|
||||||
@ -2019,7 +2026,6 @@
|
|||||||
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?",
|
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?",
|
||||||
"Jump to first unread room.": "Jump to first unread room.",
|
"Jump to first unread room.": "Jump to first unread room.",
|
||||||
"Jump to first invite.": "Jump to first invite.",
|
"Jump to first invite.": "Jump to first invite.",
|
||||||
"Add room": "Add room",
|
|
||||||
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
|
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
|
||||||
"You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",
|
"You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",
|
||||||
"Search failed": "Search failed",
|
"Search failed": "Search failed",
|
||||||
|
@ -20,10 +20,12 @@ const TILE_HEIGHT_PX = 44;
|
|||||||
|
|
||||||
interface ISerializedListLayout {
|
interface ISerializedListLayout {
|
||||||
numTiles: number;
|
numTiles: number;
|
||||||
|
showPreviews: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ListLayout {
|
export class ListLayout {
|
||||||
private _n = 0;
|
private _n = 0;
|
||||||
|
private _previews = false;
|
||||||
|
|
||||||
constructor(public readonly tagId: TagID) {
|
constructor(public readonly tagId: TagID) {
|
||||||
const serialized = localStorage.getItem(this.key);
|
const serialized = localStorage.getItem(this.key);
|
||||||
@ -31,9 +33,19 @@ export class ListLayout {
|
|||||||
// We don't use the setters as they cause writes.
|
// We don't use the setters as they cause writes.
|
||||||
const parsed = <ISerializedListLayout>JSON.parse(serialized);
|
const parsed = <ISerializedListLayout>JSON.parse(serialized);
|
||||||
this._n = parsed.numTiles;
|
this._n = parsed.numTiles;
|
||||||
|
this._previews = parsed.showPreviews;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get showPreviews(): boolean {
|
||||||
|
return this._previews;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set showPreviews(v: boolean) {
|
||||||
|
this._previews = v;
|
||||||
|
this.save();
|
||||||
|
}
|
||||||
|
|
||||||
public get tileHeight(): number {
|
public get tileHeight(): number {
|
||||||
return TILE_HEIGHT_PX;
|
return TILE_HEIGHT_PX;
|
||||||
}
|
}
|
||||||
@ -48,7 +60,7 @@ export class ListLayout {
|
|||||||
|
|
||||||
public set visibleTiles(v: number) {
|
public set visibleTiles(v: number) {
|
||||||
this._n = v;
|
this._n = v;
|
||||||
localStorage.setItem(this.key, JSON.stringify(this.serialize()));
|
this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
public get minVisibleTiles(): number {
|
public get minVisibleTiles(): number {
|
||||||
@ -80,9 +92,14 @@ export class ListLayout {
|
|||||||
return px / this.tileHeight;
|
return px / this.tileHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private save() {
|
||||||
|
localStorage.setItem(this.key, JSON.stringify(this.serialize()));
|
||||||
|
}
|
||||||
|
|
||||||
private serialize(): ISerializedListLayout {
|
private serialize(): ISerializedListLayout {
|
||||||
return {
|
return {
|
||||||
numTiles: this.visibleTiles,
|
numTiles: this.visibleTiles,
|
||||||
|
showPreviews: this.showPreviews,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user