diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx
new file mode 100644
index 0000000000..1156c80313
--- /dev/null
+++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx
@@ -0,0 +1,63 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from 'react';
+import { Room } from "matrix-js-sdk/src/models/room";
+
+import { TagID } from '../../../stores/room-list/models';
+import RoomAvatar from "./RoomAvatar";
+import RoomTileIcon from "../rooms/RoomTileIcon";
+import NotificationBadge, { INotificationState, TagSpecificNotificationState } from '../rooms/NotificationBadge';
+
+interface IProps {
+ room: Room;
+ avatarSize: number;
+ tag: TagID;
+ displayBadge?: boolean;
+ forceCount?: boolean;
+}
+
+interface IState {
+ notificationState?: INotificationState;
+}
+
+export default class DecoratedRoomAvatar extends React.PureComponent {
+
+ constructor(props: IProps) {
+ super(props);
+
+ this.state = {
+ notificationState: new TagSpecificNotificationState(this.props.room, this.props.tag),
+ };
+ }
+
+ public render(): React.ReactNode {
+ let badge: React.ReactNode;
+ if (this.props.displayBadge) {
+ badge = ;
+ }
+
+ return
+
+
+ {badge}
+
;
+ }
+}
\ No newline at end of file
diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx
index dfc02176c6..829b05fbfc 100644
--- a/src/components/views/rooms/NotificationBadge.tsx
+++ b/src/components/views/rooms/NotificationBadge.tsx
@@ -18,6 +18,10 @@ import React from "react";
import classNames from "classnames";
import { formatMinimalBadgeCount } from "../../../utils/FormattingUtils";
import SettingsStore from "../../../settings/SettingsStore";
+import { DefaultTagID, TagID } from "../../../stores/room-list/models";
+import { readReceiptChangeIsFor } from "../../../utils/read-receipts";
+import AccessibleButton from "../elements/AccessibleButton";
+import { XOR } from "../../../@types/common";
import { INotificationState, NOTIFICATION_STATE_UPDATE } from "../../../stores/notifications/INotificationState";
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
@@ -36,11 +40,18 @@ interface IProps {
roomId?: string;
}
+interface IClickableProps extends IProps, React.InputHTMLAttributes {
+ /**
+ * If specified will return an AccessibleButton instead of a div.
+ */
+ onClick?(ev: React.MouseEvent);
+}
+
interface IState {
showCounts: boolean; // whether or not to show counts. Independent of props.forceCount
}
-export default class NotificationBadge extends React.PureComponent {
+export default class NotificationBadge extends React.PureComponent, IState> {
private countWatcherRef: string;
constructor(props: IProps) {
@@ -83,23 +94,25 @@ export default class NotificationBadge extends React.PureComponent= NotificationColor.Red;
- const hasCount = this.props.notification.color >= NotificationColor.Grey;
- const hasAnySymbol = this.props.notification.symbol || this.props.notification.count > 0;
+ const hasNotif = notification.color >= NotificationColor.Red;
+ const hasCount = notification.color >= NotificationColor.Grey;
+ const hasAnySymbol = notification.symbol || notification.count > 0;
let isEmptyBadge = !hasAnySymbol || !hasCount;
- if (this.props.forceCount) {
+ if (forceCount) {
isEmptyBadge = false;
if (!hasCount) return null; // Can't render a badge
}
- let symbol = this.props.notification.symbol || formatMinimalBadgeCount(this.props.notification.count);
+ let symbol = notification.symbol || formatMinimalBadgeCount(notification.count);
if (isEmptyBadge) symbol = "";
const classes = classNames({
@@ -111,6 +124,14 @@ export default class NotificationBadge extends React.PureComponent 2,
});
+ if (onClick) {
+ return (
+
+ {symbol}
+
+ );
+ }
+
return (
{symbol}
diff --git a/src/components/views/rooms/RoomBreadcrumbs2.tsx b/src/components/views/rooms/RoomBreadcrumbs2.tsx
index bd12ced6ee..c0417fc592 100644
--- a/src/components/views/rooms/RoomBreadcrumbs2.tsx
+++ b/src/components/views/rooms/RoomBreadcrumbs2.tsx
@@ -17,13 +17,15 @@ limitations under the License.
import React from "react";
import { BreadcrumbsStore } from "../../../stores/BreadcrumbsStore";
import AccessibleButton from "../elements/AccessibleButton";
-import RoomAvatar from "../avatars/RoomAvatar";
+import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import { _t } from "../../../languageHandler";
import { Room } from "matrix-js-sdk/src/models/room";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import Analytics from "../../../Analytics";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { CSSTransition } from "react-transition-group";
+import RoomListStore from "../../../stores/room-list/RoomListStore2";
+import { DefaultTagID } from "../../../stores/room-list/models";
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
@@ -93,6 +95,8 @@ export default class RoomBreadcrumbs2 extends React.PureComponent {
+ const roomTags = RoomListStore.instance.getTagsForRoom(r);
+ const roomTag = roomTags.includes(DefaultTagID.DM) ? DefaultTagID.DM : roomTags[0];
return (
this.viewRoom(r, i)}
aria-label={_t("Room %(name)s", {name: r.name})}
>
-
+
);
});
diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx
index a1298e107b..5d7b8ad2c6 100644
--- a/src/components/views/rooms/RoomList2.tsx
+++ b/src/components/views/rooms/RoomList2.tsx
@@ -25,10 +25,15 @@ import { ITagMap } from "../../../stores/room-list/algorithms/models";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import { Dispatcher } from "flux";
import dis from "../../../dispatcher/dispatcher";
+import defaultDispatcher from "../../../dispatcher/dispatcher";
import RoomSublist2 from "./RoomSublist2";
import { ActionPayload } from "../../../dispatcher/payloads";
import { NameFilterCondition } from "../../../stores/room-list/filters/NameFilterCondition";
import { ListLayout } from "../../../stores/room-list/ListLayout";
+import { MatrixClientPeg } from "../../../MatrixClientPeg";
+import GroupAvatar from "../avatars/GroupAvatar";
+import TemporaryTile from "./TemporaryTile";
+import { NotificationColor, StaticNotificationState } from "./NotificationBadge";
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
@@ -173,6 +178,40 @@ export default class RoomList2 extends React.Component {
});
}
+ private renderCommunityInvites(): React.ReactElement[] {
+ // TODO: Put community invites in a more sensible place (not in the room list)
+ return MatrixClientPeg.get().getGroups().filter(g => {
+ if (g.myMembership !== 'invite') return false;
+ return !this.searchFilter || this.searchFilter.matches(g.name);
+ }).map(g => {
+ const avatar = (
+
+ );
+ const openGroup = () => {
+ defaultDispatcher.dispatch({
+ action: 'view_group',
+ group_id: g.groupId,
+ });
+ };
+ return (
+
+ );
+ });
+ }
+
private renderSublists(): React.ReactElement[] {
const components: React.ReactElement[] = [];
@@ -195,6 +234,7 @@ export default class RoomList2 extends React.Component {
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null;
+ const extraTiles = orderedTagId === DefaultTagID.Invite ? this.renderCommunityInvites() : null;
components.push(
{
isInvite={aesthetics.isInvite}
layout={this.state.layouts.get(orderedTagId)}
isMinimized={this.props.isMinimized}
+ extraBadTilesThatShouldntExist={extraTiles}
/>
);
}
diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx
index 47c877f2d0..547aa5e67e 100644
--- a/src/components/views/rooms/RoomSublist2.tsx
+++ b/src/components/views/rooms/RoomSublist2.tsx
@@ -32,8 +32,9 @@ import StyledRadioButton from "../elements/StyledRadioButton";
import RoomListStore from "../../../stores/room-list/RoomListStore2";
import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorithms/models";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
-import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
+import dis from "../../../dispatcher/dispatcher";
import NotificationBadge from "./NotificationBadge";
+import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
@@ -63,33 +64,35 @@ interface IProps {
isMinimized: boolean;
tagId: TagID;
+ // TODO: Don't use this. It's for community invites, and community invites shouldn't be here.
+ // You should feel bad if you use this.
+ extraBadTilesThatShouldntExist?: React.ReactElement[];
+
// TODO: Account for https://github.com/vector-im/riot-web/issues/14179
}
+type PartialDOMRect = Pick;
+
interface IState {
notificationState: ListNotificationState;
- menuDisplayed: boolean;
+ contextMenuPosition: PartialDOMRect;
isResizing: 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, this.props.tagId),
- menuDisplayed: false,
+ contextMenuPosition: null,
isResizing: false,
};
this.state.notificationState.setRooms(this.props.rooms);
}
private get numTiles(): number {
- // TODO: Account for group invites: https://github.com/vector-im/riot-web/issues/14179
- return (this.props.rooms || []).length;
+ return (this.props.rooms || []).length + (this.props.extraBadTilesThatShouldntExist || []).length;
}
private get numVisibleTiles(): number {
@@ -139,11 +142,24 @@ export default class RoomSublist2 extends React.Component {
private onOpenMenuClick = (ev: InputEvent) => {
ev.preventDefault();
ev.stopPropagation();
- this.setState({menuDisplayed: true});
+ const target = ev.target as HTMLButtonElement;
+ this.setState({contextMenuPosition: target.getBoundingClientRect()});
+ };
+
+ private onContextMenu = (ev: React.MouseEvent) => {
+ ev.preventDefault();
+ ev.stopPropagation();
+ this.setState({
+ contextMenuPosition: {
+ left: ev.clientX,
+ top: ev.clientY,
+ height: 0,
+ },
+ });
};
private onCloseMenu = () => {
- this.setState({menuDisplayed: false});
+ this.setState({contextMenuPosition: null});
};
private onUnreadFirstChanged = async () => {
@@ -161,6 +177,30 @@ export default class RoomSublist2 extends React.Component {
this.forceUpdate(); // because the layout doesn't trigger a re-render
};
+ private onBadgeClick = (ev: React.MouseEvent) => {
+ ev.preventDefault();
+ ev.stopPropagation();
+
+ let room;
+ if (this.props.tagId === DefaultTagID.Invite) {
+ // switch to first room as that'll be the top of the list for the user
+ room = this.props.rooms && this.props.rooms[0];
+ } else {
+ // find the first room with a count of the same colour as the badge count
+ room = this.props.rooms.find((r: Room) => {
+ const notifState = this.state.notificationState.getForRoom(r);
+ return notifState.count > 0 && notifState.color === this.state.notificationState.color;
+ });
+ }
+
+ if (room) {
+ dis.dispatch({
+ action: 'view_room',
+ room_id: room.roomId,
+ });
+ }
+ };
+
private onHeaderClick = (ev: React.MouseEvent) => {
let target = ev.target as HTMLDivElement;
if (!target.classList.contains('mx_RoomSublist2_headerText')) {
@@ -188,6 +228,10 @@ export default class RoomSublist2 extends React.Component {
const tiles: React.ReactElement[] = [];
+ if (this.props.extraBadTilesThatShouldntExist) {
+ tiles.push(...this.props.extraBadTilesThatShouldntExist);
+ }
+
if (this.props.rooms) {
const visibleRooms = this.props.rooms.slice(0, this.numVisibleTiles);
for (const room of visibleRooms) {
@@ -203,6 +247,14 @@ export default class RoomSublist2 extends React.Component {
}
}
+ // We only have to do this because of the extra tiles. We do it conditionally
+ // to avoid spending cycles on slicing. It's generally fine to do this though
+ // as users are unlikely to have more than a handful of tiles when the extra
+ // tiles are used.
+ if (tiles.length > this.numVisibleTiles) {
+ return tiles.slice(0, this.numVisibleTiles);
+ }
+
return tiles;
}
@@ -213,15 +265,14 @@ export default class RoomSublist2 extends React.Component {
}
let contextMenu = null;
- if (this.state.menuDisplayed) {
- const elementRect = this.menuButtonRef.current.getBoundingClientRect();
+ if (this.state.contextMenuPosition) {
const isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic;
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
contextMenu = (
{this.props.label}
@@ -358,7 +417,7 @@ export default class RoomSublist2 extends React.Component {
const classes = classNames({
'mx_RoomSublist2': true,
- 'mx_RoomSublist2_hasMenuOpen': this.state.menuDisplayed,
+ 'mx_RoomSublist2_hasMenuOpen': !!this.state.contextMenuPosition,
'mx_RoomSublist2_minimized': this.props.isMinimized,
});
diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx
index c331b7f7b1..2647a24412 100644
--- a/src/components/views/rooms/RoomTile2.tsx
+++ b/src/components/views/rooms/RoomTile2.tsx
@@ -22,15 +22,18 @@ import { Room } from "matrix-js-sdk/src/models/room";
import classNames from "classnames";
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
-import RoomAvatar from "../../views/avatars/RoomAvatar";
import dis from '../../../dispatcher/dispatcher';
import { Key } from "../../../Keyboard";
import ActiveRoomObserver from "../../../ActiveRoomObserver";
import { _t } from "../../../languageHandler";
-import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
+import { ContextMenu, ContextMenuButton, MenuItemRadio } from "../../structures/ContextMenu";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
+import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import RoomTileIcon from "./RoomTileIcon";
+import { getRoomNotifsState, ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE } from "../../../RoomNotifs";
+import { MatrixClientPeg } from "../../../MatrixClientPeg";
+import { setRoomNotifsState } from "../../../RoomNotifs";
import { TagSpecificNotificationState } from "../../../stores/notifications/TagSpecificNotificationState";
import { INotificationState } from "../../../stores/notifications/INotificationState";
import NotificationBadge from "./NotificationBadge";
@@ -56,17 +59,51 @@ interface IProps {
// TODO: Incoming call boxes: https://github.com/vector-im/riot-web/issues/14177
}
+type PartialDOMRect = Pick;
+
interface IState {
hover: boolean;
notificationState: INotificationState;
selected: boolean;
- generalMenuDisplayed: boolean;
+ notificationsMenuPosition: PartialDOMRect;
+ generalMenuPosition: PartialDOMRect;
}
-export default class RoomTile2 extends React.Component {
- private roomTileRef: React.RefObject = createRef();
- private generalMenuButtonRef: React.RefObject = createRef();
+const contextMenuBelow = (elementRect: PartialDOMRect) => {
+ // align the context menu's icons with the icon which opened the context menu
+ const left = elementRect.left + window.pageXOffset - 9;
+ const top = elementRect.bottom + window.pageYOffset + 17;
+ const chevronFace = "none";
+ return {left, top, chevronFace};
+};
+interface INotifOptionProps {
+ active: boolean;
+ iconClassName: string;
+ label: string;
+ onClick(ev: ButtonEvent);
+}
+
+const NotifOption: React.FC = ({active, onClick, iconClassName, label}) => {
+ const classes = classNames({
+ mx_RoomTile2_contextMenu_activeRow: active,
+ });
+
+ let activeIcon;
+ if (active) {
+ activeIcon = ;
+ }
+
+ return (
+
+
+ { label }
+ { activeIcon }
+
+ );
+};
+
+export default class RoomTile2 extends React.Component {
// TODO: a11y: https://github.com/vector-im/riot-web/issues/14180
constructor(props: IProps) {
@@ -76,7 +113,8 @@ export default class RoomTile2 extends React.Component {
hover: false,
notificationState: new TagSpecificNotificationState(this.props.room, this.props.tag),
selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId,
- generalMenuDisplayed: false,
+ notificationsMenuPosition: null,
+ generalMenuPosition: null,
};
ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate);
@@ -97,6 +135,8 @@ export default class RoomTile2 extends React.Component {
};
private onTileClick = (ev: React.KeyboardEvent) => {
+ ev.preventDefault();
+ ev.stopPropagation();
dis.dispatch({
action: 'view_room',
// TODO: Support show_room_tile in new room list: https://github.com/vector-im/riot-web/issues/14233
@@ -110,16 +150,37 @@ export default class RoomTile2 extends React.Component {
this.setState({selected: isActive});
};
+ private onNotificationsMenuOpenClick = (ev: InputEvent) => {
+ ev.preventDefault();
+ ev.stopPropagation();
+ const target = ev.target as HTMLButtonElement;
+ this.setState({notificationsMenuPosition: target.getBoundingClientRect()});
+ };
+
+ private onCloseNotificationsMenu = () => {
+ this.setState({notificationsMenuPosition: null});
+ };
+
private onGeneralMenuOpenClick = (ev: InputEvent) => {
ev.preventDefault();
ev.stopPropagation();
- this.setState({generalMenuDisplayed: true});
+ const target = ev.target as HTMLButtonElement;
+ this.setState({generalMenuPosition: target.getBoundingClientRect()});
};
- private onCloseGeneralMenu = (ev: InputEvent) => {
+ private onContextMenu = (ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
- this.setState({generalMenuDisplayed: false});
+ this.setState({
+ generalMenuPosition: {
+ left: ev.clientX,
+ bottom: ev.clientY,
+ },
+ });
+ };
+
+ private onCloseGeneralMenu = () => {
+ this.setState({generalMenuPosition: null});
};
private onTagRoom = (ev: ButtonEvent, tagId: TagID) => {
@@ -138,7 +199,7 @@ export default class RoomTile2 extends React.Component {
action: 'leave_room',
room_id: this.props.room.roomId,
});
- this.setState({generalMenuDisplayed: false}); // hide the menu
+ this.setState({generalMenuPosition: null}); // hide the menu
};
private onOpenRoomSettings = (ev: ButtonEvent) => {
@@ -149,9 +210,98 @@ export default class RoomTile2 extends React.Component {
action: 'open_room_settings',
room_id: this.props.room.roomId,
});
- this.setState({generalMenuDisplayed: false}); // hide the menu
+ this.setState({generalMenuPosition: null}); // hide the menu
};
+ private async saveNotifState(ev: ButtonEvent, newState: ALL_MESSAGES_LOUD | ALL_MESSAGES | MENTIONS_ONLY | MUTE) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ if (MatrixClientPeg.get().isGuest()) return;
+
+ try {
+ // TODO add local echo - https://github.com/vector-im/riot-web/issues/14280
+ await setRoomNotifsState(this.props.room.roomId, newState);
+ } catch (error) {
+ // TODO: some form of error notification to the user to inform them that their state change failed.
+ // https://github.com/vector-im/riot-web/issues/14281
+ console.error(error);
+ }
+
+ this.setState({notificationsMenuPosition: null}); // Close the context menu
+ }
+
+ private onClickAllNotifs = ev => this.saveNotifState(ev, ALL_MESSAGES);
+ private onClickAlertMe = ev => this.saveNotifState(ev, ALL_MESSAGES_LOUD);
+ private onClickMentions = ev => this.saveNotifState(ev, MENTIONS_ONLY);
+ private onClickMute = ev => this.saveNotifState(ev, MUTE);
+
+ private renderNotificationsMenu(): React.ReactElement {
+ if (this.props.isMinimized || MatrixClientPeg.get().isGuest() || this.props.tag === DefaultTagID.Invite) {
+ // the menu makes no sense in these cases so do not show one
+ return null;
+ }
+
+ const state = getRoomNotifsState(this.props.room.roomId);
+
+ let contextMenu = null;
+ if (this.state.notificationsMenuPosition) {
+ contextMenu = (
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ const classes = classNames("mx_RoomTile2_notificationsButton", {
+ // Show bell icon for the default case too.
+ mx_RoomTile2_iconBell: state === ALL_MESSAGES_LOUD || state === ALL_MESSAGES,
+ mx_RoomTile2_iconBellDot: state === MENTIONS_ONLY,
+ mx_RoomTile2_iconBellCrossed: state === MUTE,
+ // XXX: RoomNotifs assumes ALL_MESSAGES is default, this is wrong,
+ // but cannot be fixed until FTUE Notifications lands.
+ mx_RoomTile2_notificationsButton_show: state !== ALL_MESSAGES,
+ });
+
+ return (
+
+
+ {contextMenu}
+
+ );
+ }
+
private renderGeneralMenu(): React.ReactElement {
if (this.props.isMinimized) return null; // no menu when minimized
@@ -161,51 +311,25 @@ export default class RoomTile2 extends React.Component {
}
let contextMenu = null;
- if (this.state.generalMenuDisplayed) {
- // The context menu appears within the list, so use the room tile as a reference point
- const elementRect = this.roomTileRef.current.getBoundingClientRect();
+ if (this.state.generalMenuPosition) {
contextMenu = (
-
-
@@ -217,9 +341,8 @@ export default class RoomTile2 extends React.Component {
{contextMenu}
@@ -233,17 +356,25 @@ export default class RoomTile2 extends React.Component {
const classes = classNames({
'mx_RoomTile2': true,
'mx_RoomTile2_selected': this.state.selected,
- 'mx_RoomTile2_hasMenuOpen': this.state.generalMenuDisplayed,
+ 'mx_RoomTile2_hasMenuOpen': !!(this.state.generalMenuPosition || this.state.notificationsMenuPosition),
'mx_RoomTile2_minimized': this.props.isMinimized,
});
- const badge = (
- ;
+
+ let badge: React.ReactNode;
+ if (!this.props.isMinimized) {
+ badge =
- );
+ />;
+ }
// TODO: the original RoomTile uses state for the room name. Do we need to?
let name = this.props.room.name;
@@ -281,10 +412,9 @@ export default class RoomTile2 extends React.Component {
);
if (this.props.isMinimized) nameContainer = null;
- const avatarSize = 32;
return (
-
+
{({onFocus, isActive, ref}) =>
{
onMouseLeave={this.onTileMouseLeave}
onClick={this.onTileClick}
role="treeitem"
+ onContextMenu={this.onContextMenu}
>
-
-
-
-
+ {roomAvatar}
{nameContainer}
{badge}
+ {this.renderNotificationsMenu()}
{this.renderGeneralMenu()}
}
diff --git a/src/components/views/rooms/TemporaryTile.tsx b/src/components/views/rooms/TemporaryTile.tsx
new file mode 100644
index 0000000000..676969cade
--- /dev/null
+++ b/src/components/views/rooms/TemporaryTile.tsx
@@ -0,0 +1,114 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from "react";
+import classNames from "classnames";
+import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
+import AccessibleButton from "../../views/elements/AccessibleButton";
+import NotificationBadge, { INotificationState, NotificationColor } from "./NotificationBadge";
+
+interface IProps {
+ isMinimized: boolean;
+ isSelected: boolean;
+ displayName: string;
+ avatar: React.ReactElement;
+ notificationState: INotificationState;
+ onClick: () => void;
+}
+
+interface IState {
+ hover: boolean;
+}
+
+export default class TemporaryTile extends React.Component {
+ constructor(props: IProps) {
+ super(props);
+
+ this.state = {
+ hover: false,
+ };
+ }
+
+ private onTileMouseEnter = () => {
+ this.setState({hover: true});
+ };
+
+ private onTileMouseLeave = () => {
+ this.setState({hover: false});
+ };
+
+ public render(): React.ReactElement {
+ // XXX: We copy classes because it's easier
+ const classes = classNames({
+ 'mx_RoomTile2': true,
+ 'mx_RoomTile2_selected': this.props.isSelected,
+ 'mx_RoomTile2_minimized': this.props.isMinimized,
+ });
+
+ const badge = (
+
+ );
+
+ let name = this.props.displayName;
+ if (typeof name !== 'string') name = '';
+ name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
+
+ const nameClasses = classNames({
+ "mx_RoomTile2_name": true,
+ "mx_RoomTile2_nameHasUnreadEvents": this.props.notificationState.color >= NotificationColor.Bold,
+ });
+
+ let nameContainer = (
+