Merge pull request #4858 from matrix-org/t3chguy/room-list/2

Room Tile context menu, notifications, indicator and placement
This commit is contained in:
Michael Telatynski 2020-07-01 19:20:09 +01:00 committed by GitHub
commit 28e430060c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 307 additions and 182 deletions

View file

@ -588,27 +588,16 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
// A context menu that largely fits the | [icon] [label] | format.
.mx_IconizedContextMenu {
// Put 20px of padding around the whole menu. We do this instead of a
// simple `padding: 20px` rule so the horizontal rules added by the
// optionLists is rendered correctly (full width).
> * {
padding-left: 20px;
padding-right: 20px;
&:first-child {
padding-top: 20px;
}
&:last-child {
padding-bottom: 16px;
}
}
min-width: 146px;
.mx_IconizedContextMenu_optionList {
& > * {
padding-left: 20px;
padding-right: 20px;
}
// the notFirst class is for cases where the optionList might be under a header of sorts.
&:nth-child(n + 2), .mx_IconizedContextMenu_optionList_notFirst {
margin-top: 12px;
// This is a bit of a hack when we could just use a simple border-top property,
// however we have a (kinda) good reason for doing it this way: we need opacity.
// To get the right color, we need an opacity modifier which means we have to work
@ -631,72 +620,55 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
}
}
ul {
list-style: none;
margin: 0;
padding: 0;
// round the top corners of the top button for the hover effect to be bounded
&:first-child .mx_AccessibleButton:first-child {
border-radius: 4px 4px 0 0; // radius matches .mx_ContextualMenu
}
li {
margin: 0;
padding: 12px 0 0;
// round the bottom corners of the bottom button for the hover effect to be bounded
&:last-child .mx_AccessibleButton:last-child {
border-radius: 0 0 4px 4px; // radius matches .mx_ContextualMenu
}
.mx_AccessibleButton {
text-decoration: none;
color: $primary-fg-color;
font-size: $font-15px;
line-height: $font-24px;
.mx_AccessibleButton {
// pad the inside of the button so that the hover background is padded too
padding-top: 12px;
padding-bottom: 12px;
text-decoration: none;
color: $primary-fg-color;
font-size: $font-15px;
line-height: $font-24px;
// Create a flexbox to more easily define the list items
display: flex;
align-items: center;
// Create a flexbox to more easily define the list items
display: flex;
align-items: center;
img, .mx_IconizedContextMenu_icon { // icons
width: 16px;
min-width: 16px;
max-width: 16px;
}
&:hover {
background-color: $menu-selected-color;
}
span:last-child { // labels
padding-left: 14px;
width: 100%;
flex: 1;
img, .mx_IconizedContextMenu_icon { // icons
width: 16px;
min-width: 16px;
max-width: 16px;
}
// Ellipsize any text overflow
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
span.mx_IconizedContextMenu_label { // labels
padding-left: 14px;
width: 100%;
flex: 1;
// Ellipsize any text overflow
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
}
&.mx_IconizedContextMenu_compact {
> * {
padding-left: 11px;
padding-right: 16px;
&:first-child {
padding-top: 13px;
}
&:last-child {
padding-bottom: 13px;
}
}
.mx_IconizedContextMenu_optionList {
&:nth-child(n + 2), .mx_IconizedContextMenu_optionList_notFirst {
margin-top: 10px;
li:first-child {
padding-top: 10px;
}
}
li:first-child {
padding-top: 0;
}
.mx_IconizedContextMenu_optionList > * {
padding: 8px 16px 8px 11px;
}
}
}

View file

@ -86,6 +86,8 @@ limitations under the License.
.mx_UserMenu_contextMenu_redRow {
.mx_AccessibleButton {
padding-top: 16px;
padding-bottom: 16px;
color: $warning-color !important; // !important to override styles from context menu
}
@ -95,6 +97,8 @@ limitations under the License.
}
.mx_UserMenu_contextMenu_header {
padding: 20px;
// Create a flexbox to organize the header a bit easier
display: flex;
align-items: center;

View file

@ -92,20 +92,18 @@ limitations under the License.
justify-content: center;
}
// The menu button is hidden by default
// TODO: [Notifications] Use mx_RoomTile2_notificationsButton, similar to the following approach:
// https://github.com/matrix-org/matrix-react-sdk/blob/2180a56074f3698fc0241c309a72ba6cad802d1c/res/css/views/rooms/_RoomSublist2.scss#L48-L76
// You'll need to do the same down below on the &:hover selector for the tile.
// See https://github.com/vector-im/riot-web/issues/13961.
// ... also remove this 5 line TODO comment.
// The context menu buttons are hidden by default
.mx_RoomTile2_menuButton,
.mx_RoomTile2_notificationsButton {
width: 0;
height: 0;
visibility: hidden;
width: 20px;
height: 20px;
margin: auto 0 auto 8px;
position: relative;
display: none;
&::before {
top: 2px;
left: 2px;
content: '';
width: 16px;
height: 16px;
@ -117,9 +115,12 @@ limitations under the License.
}
}
// If the room has an overriden notification setting then we always show the notifications menu button
.mx_RoomTile2_notificationsButton.mx_RoomTile2_notificationsButton_show {
display: block;
}
.mx_RoomTile2_menuButton::before {
top: 8px;
left: -1px; // this is off-center to align it with the badges
mask-image: url('$(res)/img/feather-customised/more-horizontal.svg');
}
@ -132,10 +133,9 @@ limitations under the License.
visibility: hidden;
}
.mx_RoomTile2_notificationsButton,
.mx_RoomTile2_menuButton {
width: 18px;
height: 32px;
visibility: visible;
display: block;
}
}
}
@ -158,6 +158,23 @@ limitations under the License.
}
}
// We use these both in context menus and the room tiles
.mx_RoomTile2_iconBell::before {
mask-image: url('$(res)/img/feather-customised/bell.svg');
}
.mx_RoomTile2_iconBellDot::before {
mask-image: url('$(res)/img/feather-customised/bell-notification.custom.svg');
}
.mx_RoomTile2_iconBellCrossed::before {
mask-image: url('$(res)/img/feather-customised/bell-crossed.svg');
}
.mx_RoomTile2_iconBellMentions::before {
mask-image: url('$(res)/img/feather-customised/bell-mentions.custom.svg');
}
.mx_RoomTile2_iconCheck::before {
mask-image: url('$(res)/img/feather-customised/check.svg');
}
.mx_RoomTile2_contextMenu {
.mx_RoomTile2_contextMenu_redRow {
.mx_AccessibleButton {
@ -169,6 +186,16 @@ limitations under the License.
}
}
.mx_RoomTile2_contextMenu_activeRow {
&.mx_AccessibleButton, .mx_AccessibleButton {
color: $accent-color !important; // !important to override styles from context menu
}
.mx_IconizedContextMenu_icon::before {
background-color: $accent-color;
}
}
.mx_IconizedContextMenu_icon {
position: relative;
width: 16px;

View file

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.31422 2.4647C8.07372 2.6004 7.98877 2.90537 8.12448 3.14587C8.26018 3.38637 8.56515 3.47132 8.80565 3.33562L8.31422 2.4647ZM18.9999 9.00016L18.4999 8.9999V9.00016H18.9999ZM18.4999 13.0002C18.4999 13.2763 18.7238 13.5002 18.9999 13.5002C19.2761 13.5002 19.4999 13.2763 19.4999 13.0002H18.4999ZM17 17.5004C17.2761 17.5004 17.5 17.2765 17.5 17.0004C17.5 16.7242 17.2761 16.5004 17 16.5004V17.5004ZM2 16.5004C1.72386 16.5004 1.5 16.7242 1.5 17.0004C1.5 17.2765 1.72386 17.5004 2 17.5004V16.5004ZM5 9.00036H5.5L5.5 8.99973L5 9.00036ZM6.22429 6.00974C6.35096 5.76436 6.25474 5.46276 6.00937 5.33608C5.764 5.2094 5.46239 5.30562 5.33571 5.551L6.22429 6.00974ZM14.1625 21.2509C14.301 21.012 14.2197 20.7061 13.9808 20.5675C13.742 20.4289 13.436 20.5103 13.2975 20.7491L14.1625 21.2509ZM10.7025 20.7491C10.5639 20.5103 10.2579 20.4289 10.0191 20.5675C9.78021 20.7061 9.6989 21.012 9.83746 21.2509L10.7025 20.7491ZM8.80565 3.33562C10.8187 2.19975 13.2834 2.21831 15.2791 3.38436L15.7836 2.52094C13.4809 1.17549 10.6369 1.15408 8.31422 2.4647L8.80565 3.33562ZM15.2791 3.38436C17.2748 4.55042 18.5011 6.68854 18.4999 8.9999L19.4999 9.00041C19.5013 6.33346 18.0863 3.86639 15.7836 2.52094L15.2791 3.38436ZM18.4999 9.00016V13.0002H19.4999V9.00016H18.4999ZM17 16.5004H2V17.5004H17V16.5004ZM2 17.5004C3.933 17.5004 5.5 15.9334 5.5 14.0004H4.5C4.5 15.3811 3.38071 16.5004 2 16.5004V17.5004ZM5.5 14.0004V9.00036H4.5V14.0004H5.5ZM5.5 8.99973C5.49869 7.95947 5.74707 6.93408 6.22429 6.00974L5.33571 5.551C4.78509 6.61755 4.49849 7.80069 4.5 9.00099L5.5 8.99973ZM13.2975 20.7491C13.0291 21.2117 12.5348 21.4965 12 21.4965V22.4965C12.8913 22.4965 13.7152 22.0219 14.1625 21.2509L13.2975 20.7491ZM12 21.4965C11.4652 21.4965 10.9708 21.2117 10.7025 20.7491L9.83746 21.2509C10.2847 22.0219 11.1086 22.4965 12 22.4965V21.4965Z" fill="#2E2F32"/>
<path d="M1 1L23 23" stroke="#2E2F32" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.62998 3.57476C7.55241 1.6636 9.6476 0.644608 11.692 1.13263C13.7342 1.62012 15.1666 3.47754 15.1667 5.60305V5.60308V6.01222C15.1667 6.95553 14.4163 7.73965 13.4668 7.73965C12.9349 7.73965 12.4655 7.49363 12.1555 7.11141C11.7768 7.49925 11.2519 7.74098 10.6668 7.74098C9.49647 7.74098 8.56689 6.77368 8.56689 5.60441C8.56689 4.43514 9.49647 3.46784 10.6668 3.46784C11.8348 3.46784 12.7629 4.43111 12.7668 5.59709L12.7668 5.60308V6.01222C12.7668 6.4247 13.0908 6.73965 13.4668 6.73965C13.8428 6.73965 14.1667 6.4247 14.1667 6.01222V5.60311V5.60308C14.1666 3.92595 13.0379 2.48201 11.4598 2.1053C9.8839 1.72911 8.25387 2.51086 7.53057 4.00944C6.80579 5.5111 7.19017 7.3233 8.44894 8.38151C9.70415 9.43672 11.5011 9.46808 12.7905 8.45807C13.0079 8.28778 13.3221 8.32596 13.4924 8.54335C13.6627 8.76074 13.6245 9.07501 13.4071 9.24529C11.745 10.5473 9.42229 10.5062 7.80545 9.14696C6.19216 7.79072 5.70903 5.48285 6.62998 3.57476ZM10.6668 4.46784C10.07 4.46784 9.56689 4.96597 9.56689 5.60441C9.56689 6.24285 10.07 6.74098 10.6668 6.74098C11.2637 6.74098 11.7668 6.24285 11.7668 5.60441C11.7668 4.96597 11.2637 4.46784 10.6668 4.46784ZM5.48951 2.14C5.61741 2.38474 5.5227 2.68682 5.27796 2.81472C3.92878 3.51981 3 4.95881 3 6.62506V10.0347C3 10.6137 2.8091 11.1505 2.48631 11.5805H13.8333C14.1095 11.5805 14.3333 11.8043 14.3333 12.0805C14.3333 12.3566 14.1095 12.5805 13.8333 12.5805H0.5C0.223858 12.5805 0 12.3566 0 12.0805C0 11.8043 0.223858 11.5805 0.5 11.5805C1.31782 11.5805 2 10.8991 2 10.0347V6.62506C2 4.58053 3.14094 2.80322 4.81479 1.92845C5.05953 1.80055 5.36161 1.89527 5.48951 2.14ZM5.76678 14.3741C6.00698 14.2379 6.31214 14.3222 6.44836 14.5624C6.59999 14.8298 6.8752 14.9886 7.16676 14.9886C7.45832 14.9886 7.73354 14.8298 7.88516 14.5624C8.02139 14.3222 8.32654 14.2379 8.56674 14.3741C8.80695 14.5104 8.89124 14.8155 8.75502 15.0557C8.42959 15.6296 7.82596 15.9886 7.16676 15.9886C6.50756 15.9886 5.90393 15.6296 5.5785 15.0557C5.44228 14.8155 5.52657 14.5104 5.76678 14.3741Z" fill="#2E2F32"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.73 21C13.3722 21.6168 12.7131 21.9965 12 21.9965C11.287 21.9965 10.6278 21.6168 10.27 21" stroke="#2E2F32" stroke-linecap="round"/>
<path d="M11.9999 2.00024C8.13388 2.00024 4.99988 5.13425 4.99988 9.00024V14.0002C4.99988 15.6571 3.65673 17.0002 1.99988 17.0002H21.9999C20.343 17.0002 18.9999 15.6571 18.9999 14.0002V12.75" stroke="#2E2F32" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="18.75" cy="5.25" r="4.75" stroke="#2E2F32"/>
</svg>

After

Width:  |  Height:  |  Size: 563 B

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 17.5C22.2761 17.5 22.5 17.2761 22.5 17C22.5 16.7239 22.2761 16.5 22 16.5V17.5ZM2 16.5C1.72386 16.5 1.5 16.7239 1.5 17C1.5 17.2761 1.72386 17.5 2 17.5V16.5ZM5 9H4.5H5ZM19 9H19.5H19ZM14.1625 21.2509C14.3011 21.012 14.2197 20.7061 13.9809 20.5675C13.742 20.4289 13.4361 20.5103 13.2975 20.7491L14.1625 21.2509ZM10.7025 20.7491C10.5639 20.5103 10.258 20.4289 10.0191 20.5675C9.78025 20.7061 9.69894 21.012 9.8375 21.2509L10.7025 20.7491ZM22 16.5H2V17.5H22V16.5ZM2 17.5C3.933 17.5 5.5 15.933 5.5 14H4.5C4.5 15.3807 3.38071 16.5 2 16.5V17.5ZM5.5 14V9H4.5V14H5.5ZM5.5 9C5.5 5.41015 8.41015 2.5 12 2.5V1.5C7.85786 1.5 4.5 4.85786 4.5 9H5.5ZM12 2.5C15.5899 2.5 18.5 5.41015 18.5 9H19.5C19.5 4.85786 16.1421 1.5 12 1.5V2.5ZM18.5 9V14H19.5V9H18.5ZM18.5 14C18.5 15.933 20.067 17.5 22 17.5V16.5C20.6193 16.5 19.5 15.3807 19.5 14H18.5ZM13.2975 20.7491C13.0292 21.2117 12.5348 21.4965 12 21.4965V22.4965C12.8913 22.4965 13.7153 22.0219 14.1625 21.2509L13.2975 20.7491ZM12 21.4965C11.4652 21.4965 10.9708 21.2117 10.7025 20.7491L9.8375 21.2509C10.2847 22.0219 11.1087 22.4965 12 22.4965V21.4965Z" fill="#2E2F32"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -191,12 +191,10 @@ export default class UserMenu extends React.Component<IProps, IState> {
let homeButton = null;
if (this.hasHomePage) {
homeButton = (
<li>
<AccessibleButton onClick={this.onHomeClick}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconHome" />
<span>{_t("Home")}</span>
</AccessibleButton>
</li>
<AccessibleButton onClick={this.onHomeClick}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconHome" />
<span>{_t("Home")}</span>
</AccessibleButton>
);
}
@ -204,7 +202,8 @@ export default class UserMenu extends React.Component<IProps, IState> {
return (
<ContextMenu
chevronFace="none"
left={elementRect.width + elementRect.left}
// -20 to overlap the context menu by just over the width of the `...` icon and make it look connected
left={elementRect.width + elementRect.left - 20}
top={elementRect.top + elementRect.height}
onFinished={this.onCloseMenu}
>
@ -232,49 +231,33 @@ export default class UserMenu extends React.Component<IProps, IState> {
</div>
{hostingLink}
<div className="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst">
<ul>
{homeButton}
<li>
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconBell" />
<span>{_t("Notification settings")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, USER_SECURITY_TAB)}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconLock" />
<span>{_t("Security & privacy")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, null)}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconSettings" />
<span>{_t("All settings")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={this.onShowArchived}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconArchive" />
<span>{_t("Archived rooms")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={this.onProvideFeedback}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconMessage" />
<span>{_t("Feedback")}</span>
</AccessibleButton>
</li>
</ul>
{homeButton}
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconBell" />
<span className="mx_IconizedContextMenu_label">{_t("Notification settings")}</span>
</AccessibleButton>
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, USER_SECURITY_TAB)}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconLock" />
<span className="mx_IconizedContextMenu_label">{_t("Security & privacy")}</span>
</AccessibleButton>
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, null)}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconSettings" />
<span className="mx_IconizedContextMenu_label">{_t("All settings")}</span>
</AccessibleButton>
<AccessibleButton onClick={this.onShowArchived}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconArchive" />
<span className="mx_IconizedContextMenu_label">{_t("Archived rooms")}</span>
</AccessibleButton>
<AccessibleButton onClick={this.onProvideFeedback}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconMessage" />
<span className="mx_IconizedContextMenu_label">{_t("Feedback")}</span>
</AccessibleButton>
</div>
<div className="mx_IconizedContextMenu_optionList">
<ul>
<li className="mx_UserMenu_contextMenu_redRow">
<AccessibleButton onClick={this.onSignOutClick}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconSignOut" />
<span>{_t("Sign out")}</span>
</AccessibleButton>
</li>
</ul>
<div className="mx_IconizedContextMenu_optionList mx_UserMenu_contextMenu_redRow">
<AccessibleButton onClick={this.onSignOutClick}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconSignOut" />
<span className="mx_IconizedContextMenu_label">{_t("Sign out")}</span>
</AccessibleButton>
</div>
</div>
</ContextMenu>

View file

@ -32,10 +32,13 @@ import NotificationBadge, {
TagSpecificNotificationState
} from "./NotificationBadge";
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 RoomTileIcon from "./RoomTileIcon";
import { getRoomNotifsState, ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE } from "../../../RoomNotifs";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { setRoomNotifsState } from "../../../RoomNotifs";
// 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
@ -61,11 +64,46 @@ interface IState {
hover: boolean;
notificationState: INotificationState;
selected: boolean;
notificationsMenuDisplayed: boolean;
generalMenuDisplayed: boolean;
}
const contextMenuBelow = (elementRect) => {
// align the context menu's icons with the icon which opened the context menu
const left = elementRect.left + window.pageXOffset - 9;
let 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<INotifOptionProps> = ({active, onClick, iconClassName, label}) => {
const classes = classNames({
mx_RoomTile2_contextMenu_activeRow: active,
});
let activeIcon;
if (active) {
activeIcon = <span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconCheck" />;
}
return (
<MenuItemRadio className={classes} onClick={onClick} active={active} label={label}>
<span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} />
<span className="mx_IconizedContextMenu_label">{ label }</span>
{ activeIcon }
</MenuItemRadio>
);
};
export default class RoomTile2 extends React.Component<IProps, IState> {
private roomTileRef: React.RefObject<HTMLDivElement> = createRef();
private notificationsMenuButtonRef: React.RefObject<HTMLButtonElement> = createRef();
private generalMenuButtonRef: React.RefObject<HTMLButtonElement> = createRef();
// TODO: a11y: https://github.com/vector-im/riot-web/issues/14180
@ -77,6 +115,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
hover: false,
notificationState: new TagSpecificNotificationState(this.props.room, this.props.tag),
selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId,
notificationsMenuDisplayed: false,
generalMenuDisplayed: false,
};
@ -111,6 +150,18 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
this.setState({selected: isActive});
};
private onNotificationsMenuOpenClick = (ev: InputEvent) => {
ev.preventDefault();
ev.stopPropagation();
this.setState({notificationsMenuDisplayed: true});
};
private onCloseNotificationsMenu = (ev: InputEvent) => {
ev.preventDefault();
ev.stopPropagation();
this.setState({notificationsMenuDisplayed: false});
};
private onGeneralMenuOpenClick = (ev: InputEvent) => {
ev.preventDefault();
ev.stopPropagation();
@ -153,6 +204,100 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
this.setState({generalMenuDisplayed: false}); // 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);
}
// Close the context menu
this.setState({
notificationsMenuDisplayed: false,
});
}
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.notificationsMenuDisplayed) {
const elementRect = this.notificationsMenuButtonRef.current.getBoundingClientRect();
contextMenu = (
<ContextMenu {...contextMenuBelow(elementRect)} onFinished={this.onCloseNotificationsMenu}>
<div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile2_contextMenu">
<div className="mx_IconizedContextMenu_optionList">
<NotifOption
label={_t("Use default")}
active={state === ALL_MESSAGES}
iconClassName="mx_RoomTile2_iconBell"
onClick={this.onClickAllNotifs}
/>
<NotifOption
label={_t("All messages")}
active={state === ALL_MESSAGES_LOUD}
iconClassName="mx_RoomTile2_iconBellDot"
onClick={this.onClickAlertMe}
/>
<NotifOption
label={_t("Mentions & Keywords")}
active={state === MENTIONS_ONLY}
iconClassName="mx_RoomTile2_iconBellMentions"
onClick={this.onClickMentions}
/>
<NotifOption
label={_t("None")}
active={state === MUTE}
iconClassName="mx_RoomTile2_iconBellCrossed"
onClick={this.onClickMute}
/>
</div>
</div>
</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 (
<React.Fragment>
<ContextMenuButton
className={classes}
onClick={this.onNotificationsMenuOpenClick}
inputRef={this.notificationsMenuButtonRef}
label={_t("Notification options")}
isExpanded={this.state.notificationsMenuDisplayed}
/>
{contextMenu}
</React.Fragment>
);
}
private renderGeneralMenu(): React.ReactElement {
if (this.props.isMinimized) return null; // no menu when minimized
@ -163,50 +308,25 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
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();
const elementRect = this.generalMenuButtonRef.current.getBoundingClientRect();
contextMenu = (
<ContextMenu
chevronFace="none"
left={elementRect.left}
top={elementRect.top + elementRect.height + 8}
onFinished={this.onCloseGeneralMenu}
>
<div
className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile2_contextMenu"
style={{width: elementRect.width}}
>
<ContextMenu {...contextMenuBelow(elementRect)} onFinished={this.onCloseGeneralMenu}>
<div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile2_contextMenu">
<div className="mx_IconizedContextMenu_optionList">
<ul>
<li>
<AccessibleButton onClick={(e) => this.onTagRoom(e, DefaultTagID.Favourite)}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconStar" />
<span>{_t("Favourite")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={(e) => this.onTagRoom(e, DefaultTagID.LowPriority)}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconArrowDown" />
<span>{_t("Low Priority")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={this.onOpenRoomSettings}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSettings" />
<span>{_t("Settings")}</span>
</AccessibleButton>
</li>
</ul>
<AccessibleButton onClick={(e) => this.onTagRoom(e, DefaultTagID.Favourite)}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconStar" />
<span className="mx_IconizedContextMenu_label">{_t("Favourite")}</span>
</AccessibleButton>
<AccessibleButton onClick={this.onOpenRoomSettings}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSettings" />
<span className="mx_IconizedContextMenu_label">{_t("Settings")}</span>
</AccessibleButton>
</div>
<div className="mx_IconizedContextMenu_optionList">
<ul>
<li className="mx_RoomTile2_contextMenu_redRow">
<AccessibleButton onClick={this.onLeaveRoomClick}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSignOut" />
<span>{_t("Leave Room")}</span>
</AccessibleButton>
</li>
</ul>
<div className="mx_IconizedContextMenu_optionList mx_RoomTile2_contextMenu_redRow">
<AccessibleButton onClick={this.onLeaveRoomClick}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSignOut" />
<span className="mx_IconizedContextMenu_label">{_t("Leave Room")}</span>
</AccessibleButton>
</div>
</div>
</ContextMenu>
@ -234,7 +354,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
const classes = classNames({
'mx_RoomTile2': true,
'mx_RoomTile2_selected': this.state.selected,
'mx_RoomTile2_hasMenuOpen': this.state.generalMenuDisplayed,
'mx_RoomTile2_hasMenuOpen': this.state.generalMenuDisplayed || this.state.notificationsMenuDisplayed,
'mx_RoomTile2_minimized': this.props.isMinimized,
});
@ -285,7 +405,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
const avatarSize = 32;
return (
<React.Fragment>
<RovingTabIndexWrapper inputRef={this.roomTileRef}>
<RovingTabIndexWrapper>
{({onFocus, isActive, ref}) =>
<AccessibleButton
onFocus={onFocus}
@ -305,6 +425,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
<div className="mx_RoomTile2_badgeContainer">
{badge}
</div>
{this.renderNotificationsMenu()}
{this.renderGeneralMenu()}
</AccessibleButton>
}

View file

@ -1218,8 +1218,11 @@
"%(count)s unread messages.|one": "1 unread message.",
"Unread mentions.": "Unread mentions.",
"Unread messages.": "Unread messages.",
"Use default": "Use default",
"All messages": "All messages",
"Mentions & Keywords": "Mentions & Keywords",
"Notification options": "Notification options",
"Favourite": "Favourite",
"Low Priority": "Low Priority",
"Leave Room": "Leave Room",
"Room options": "Room options",
"Add a topic": "Add a topic",
@ -1897,10 +1900,10 @@
"Failed to forget room %(errCode)s": "Failed to forget room %(errCode)s",
"Notification settings": "Notification settings",
"All messages (noisy)": "All messages (noisy)",
"All messages": "All messages",
"Mentions only": "Mentions only",
"Leave": "Leave",
"Forget": "Forget",
"Low Priority": "Low Priority",
"Direct Chat": "Direct Chat",
"Clear status": "Clear status",
"Update status": "Update status",