diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index cfb9bc3b6d..04a718524c 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -45,6 +45,36 @@ limitations under the License. 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 { text-transform: uppercase; opacity: 0.5; @@ -77,4 +107,76 @@ limitations under the License. align-items: center; } } + + &:hover, &.mx_RoomSublist2_hasMenuOpen { + .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; + } } diff --git a/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss index 41c9469bc1..d5ae053f6f 100644 --- a/res/css/views/rooms/_RoomTile2.scss +++ b/res/css/views/rooms/_RoomTile2.scss @@ -51,7 +51,7 @@ limitations under the License. .mx_RoomTile2_name { font-size: $font-14px; - line-height: $font-19px; + line-height: $font-18px; } .mx_RoomTile2_name.mx_RoomTile2_nameHasUnreadEvents { @@ -63,6 +63,10 @@ limitations under the License. line-height: $font-18px; color: $roomtile2-preview-color; } + + .mx_RoomTile2_nameWithPreview { + margin-top: -4px; // shift the name up a bit more + } } .mx_RoomTile2_badgeContainer { diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index a50c34cf03..3c6f6aa5a4 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -178,6 +178,7 @@ $roomtile2-preview-color: #9e9e9e; $roomtile2-badge-color: #61708b; $roomtile2-selected-bg-color: #FFF; $theme-button-bg-color: #e3e8f0; +$roomsublist2-divider-color: #e9eaeb; $roomtile-name-color: #61708b; $roomtile-badge-fg-color: $accent-fg-color; diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index cd27156cbd..c1653aba15 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -27,6 +27,8 @@ import RoomTile2 from "./RoomTile2"; import { ResizableBox, ResizeCallbackData } from "react-resizable"; import { ListLayout } from "../../../stores/room-list/ListLayout"; import NotificationBadge, { ListNotificationState } from "./NotificationBadge"; +import {ContextMenu, ContextMenuButton} from "../../structures/ContextMenu"; +import StyledCheckbox from "../elements/StyledCheckbox"; /******************************************************************* * CAUTION * @@ -41,7 +43,6 @@ interface IProps { rooms?: Room[]; startAsHidden: boolean; label: string; - showMessagePreviews: boolean; onAddRoom?: () => void; addRoomLabel: string; isInvite: boolean; @@ -57,16 +58,19 @@ interface IProps { interface IState { notificationState: ListNotificationState; + menuDisplayed: boolean; } export default class RoomSublist2 extends React.Component { private headerButton = createRef(); + private menuButtonRef: React.RefObject = createRef(); constructor(props: IProps) { super(props); this.state = { notificationState: new ListNotificationState(this.props.isInvite), + menuDisplayed: false, }; this.state.notificationState.setRooms(this.props.rooms); } @@ -97,6 +101,26 @@ export default class RoomSublist2 extends React.Component { 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[] { const tiles: React.ReactElement[] = []; @@ -106,7 +130,7 @@ export default class RoomSublist2 extends React.Component { ); } @@ -115,6 +139,61 @@ export default class RoomSublist2 extends React.Component { return tiles; } + private renderMenu(): React.ReactElement { + let contextMenu = null; + if (this.state.menuDisplayed) { + const elementRect = this.menuButtonRef.current.getBoundingClientRect(); + contextMenu = ( + +
+
+
{_t("Sort by")}
+ TODO: Radios are blocked by https://github.com/matrix-org/matrix-react-sdk/pull/4731 +
+
+
+
{_t("Unread rooms")}
+ + {_t("Always show first")} + +
+
+
+
{_t("Show")}
+ + {_t("Message preview")} + +
+
+
+ ); + } + + return ( + + + {contextMenu} + + ); + } + private renderHeader(): React.ReactElement { // TODO: Title on collapsed // TODO: Incoming call box @@ -129,22 +208,26 @@ export default class RoomSublist2 extends React.Component { const badge = ; - // TODO: Aux button - // let addRoomButton = null; - // if (!!this.props.onAddRoom) { - // addRoomButton = ( - // - // ); - // } + let addRoomButton = null; + if (!!this.props.onAddRoom) { + addRoomButton = ( + + ); + } + + const classes = classNames({ + 'mx_RoomSublist2_headerContainer': true, + 'mx_RoomSublist2_headerContainer_withAux': !!addRoomButton, + }); // TODO: a11y (see old component) return ( -
+
{
{badge}
+ {this.renderMenu()} + {addRoomButton}
); }} @@ -174,6 +259,7 @@ export default class RoomSublist2 extends React.Component { // TODO: Proper collapse support 'mx_RoomSublist2': true, 'mx_RoomSublist2_collapsed': false, // len && isCollapsed + 'mx_RoomSublist2_hasMenuOpen': this.state.menuDisplayed, }); let content = null; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8a1d112e5d..c0a1f9f26b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1134,6 +1134,13 @@ "Securely back up your keys to avoid losing them. Learn more.": "Securely back up your keys to avoid losing them. Learn more.", "Not now": "Not now", "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 %(n)s more": "Show %(n)s more", "Options": "Options", "%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.", @@ -2017,7 +2024,6 @@ "There's no one else here! Would you like to invite others or stop warning about the empty room?": "There's no one else here! Would you like to invite others or stop warning about the empty room?", "Jump to first unread room.": "Jump to first unread room.", "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 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", diff --git a/src/stores/room-list/ListLayout.ts b/src/stores/room-list/ListLayout.ts index a6abb7d37a..7fceb84123 100644 --- a/src/stores/room-list/ListLayout.ts +++ b/src/stores/room-list/ListLayout.ts @@ -20,10 +20,12 @@ const TILE_HEIGHT_PX = 44; interface ISerializedListLayout { numTiles: number; + showPreviews: boolean; } export class ListLayout { private _n = 0; + private _previews = false; constructor(public readonly tagId: TagID) { const serialized = localStorage.getItem(this.key); @@ -31,9 +33,19 @@ export class ListLayout { // We don't use the setters as they cause writes. const parsed = JSON.parse(serialized); 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 { return TILE_HEIGHT_PX; } @@ -48,7 +60,7 @@ export class ListLayout { public set visibleTiles(v: number) { this._n = v; - localStorage.setItem(this.key, JSON.stringify(this.serialize())); + this.save(); } public get minVisibleTiles(): number { @@ -63,9 +75,14 @@ export class ListLayout { return px / this.tileHeight; } + private save() { + localStorage.setItem(this.key, JSON.stringify(this.serialize())); + } + private serialize(): ISerializedListLayout { return { numTiles: this.visibleTiles, + showPreviews: this.showPreviews, }; } }