Improve spotlight accessibility by adding context menus (#8907)
* Extract room general context menu from roomtile * Create hook to access and change a room’s notification state * Extract room notification context menu from roomtile * Add room context menus to rooms in spotlight * Make arrow movement apply to the whole dialog, not just the input box
This commit is contained in:
parent
6a125d5a1d
commit
780a903e2f
12 changed files with 632 additions and 283 deletions
|
@ -89,6 +89,8 @@
|
||||||
@import "./views/context_menus/_DeviceContextMenu.scss";
|
@import "./views/context_menus/_DeviceContextMenu.scss";
|
||||||
@import "./views/context_menus/_IconizedContextMenu.scss";
|
@import "./views/context_menus/_IconizedContextMenu.scss";
|
||||||
@import "./views/context_menus/_MessageContextMenu.scss";
|
@import "./views/context_menus/_MessageContextMenu.scss";
|
||||||
|
@import "./views/context_menus/_RoomGeneralContextMenu.scss";
|
||||||
|
@import "./views/context_menus/_RoomNotificationContextMenu.scss";
|
||||||
@import "./views/dialogs/_AddExistingToSpaceDialog.scss";
|
@import "./views/dialogs/_AddExistingToSpaceDialog.scss";
|
||||||
@import "./views/dialogs/_AnalyticsLearnMoreDialog.scss";
|
@import "./views/dialogs/_AnalyticsLearnMoreDialog.scss";
|
||||||
@import "./views/dialogs/_BugReportDialog.scss";
|
@import "./views/dialogs/_BugReportDialog.scss";
|
||||||
|
|
63
res/css/views/context_menus/_RoomGeneralContextMenu.scss
Normal file
63
res/css/views/context_menus/_RoomGeneralContextMenu.scss
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
.mx_RoomGeneralContextMenu_iconStar::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/roomlist/favorite.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomGeneralContextMenu_iconArrowDown::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/roomlist/low-priority.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomGeneralContextMenu_iconNotificationsDefault::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/notifications.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomGeneralContextMenu_iconNotificationsAllMessages::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/roomlist/notifications-default.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomGeneralContextMenu_iconNotificationsMentionsKeywords::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/roomlist/notifications-dm.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomGeneralContextMenu_iconNotificationsNone::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/roomlist/notifications-off.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomGeneralContextMenu_iconPeople::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/members.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomGeneralContextMenu_iconFiles::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/files.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomGeneralContextMenu_iconPins::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/pin-upright.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomGeneralContextMenu_iconWidgets::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/apps.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomGeneralContextMenu_iconSettings::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/settings.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomGeneralContextMenu_iconExport::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/export.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomGeneralContextMenu_iconDeveloperTools::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/settings/flask.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomGeneralContextMenu_iconCopyLink::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/link.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomGeneralContextMenu_iconInvite::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/invite.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomGeneralContextMenu_iconSignOut::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/leave.svg');
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
.mx_RoomNotificationContextMenu_iconBell::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/notifications.svg');
|
||||||
|
}
|
||||||
|
.mx_RoomNotificationContextMenu_iconBellDot::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/roomlist/notifications-default.svg');
|
||||||
|
}
|
||||||
|
.mx_RoomNotificationContextMenu_iconBellMentions::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/roomlist/notifications-dm.svg');
|
||||||
|
}
|
||||||
|
.mx_RoomNotificationContextMenu_iconBellCrossed::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/roomlist/notifications-off.svg');
|
||||||
|
}
|
|
@ -239,6 +239,13 @@ limitations under the License.
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
.mx_SpotlightDialog_option--endAdornment {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-left: auto;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
&.mx_SpotlightDialog_result_multiline {
|
&.mx_SpotlightDialog_result_multiline {
|
||||||
align-items: start;
|
align-items: start;
|
||||||
|
|
||||||
|
@ -309,8 +316,45 @@ limitations under the License.
|
||||||
margin-left: $spacing-8;
|
margin-left: $spacing-8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_SpotlightDialog_option--menu,
|
||||||
|
.mx_SpotlightDialog_option--notifications {
|
||||||
|
width: 20px;
|
||||||
|
min-width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
position: relative;
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
content: '';
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
position: absolute;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
background: $tertiary-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover::before, &[aria-selected=true]::before {
|
||||||
|
background-color: $secondary-content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpotlightDialog_option--menu::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/context-menu.svg');
|
||||||
|
}
|
||||||
|
|
||||||
&:hover, &[aria-selected=true] {
|
&:hover, &[aria-selected=true] {
|
||||||
background-color: $system;
|
background-color: $system;
|
||||||
|
|
||||||
|
.mx_SpotlightDialog_option--menu,
|
||||||
|
.mx_SpotlightDialog_option--notifications {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&[aria-selected=true] .mx_SpotlightDialog_enterPrompt {
|
&[aria-selected=true] .mx_SpotlightDialog_enterPrompt {
|
||||||
|
@ -436,7 +480,7 @@ limitations under the License.
|
||||||
color: $tertiary-content;
|
color: $tertiary-content;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background-color: $quinary-content;
|
background-color: $quinary-content;
|
||||||
margin: 0 $spacing-4 0 auto;
|
margin-right: $spacing-4;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
186
src/components/views/context_menus/RoomGeneralContextMenu.tsx
Normal file
186
src/components/views/context_menus/RoomGeneralContextMenu.tsx
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
/*
|
||||||
|
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 { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import React, { useContext } from "react";
|
||||||
|
|
||||||
|
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||||
|
import RoomListActions from "../../../actions/RoomListActions";
|
||||||
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
|
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||||
|
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||||
|
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
|
||||||
|
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||||
|
import { IProps as IContextMenuProps } from "../../structures/ContextMenu";
|
||||||
|
import IconizedContextMenu, {
|
||||||
|
IconizedContextMenuCheckbox,
|
||||||
|
IconizedContextMenuOption,
|
||||||
|
IconizedContextMenuOptionList,
|
||||||
|
} from "../context_menus/IconizedContextMenu";
|
||||||
|
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||||
|
|
||||||
|
interface IProps extends IContextMenuProps {
|
||||||
|
room: Room;
|
||||||
|
onPostFavoriteClick?: (event: ButtonEvent) => void;
|
||||||
|
onPostLowPriorityClick?: (event: ButtonEvent) => void;
|
||||||
|
onPostInviteClick?: (event: ButtonEvent) => void;
|
||||||
|
onPostCopyLinkClick?: (event: ButtonEvent) => void;
|
||||||
|
onPostSettingsClick?: (event: ButtonEvent) => void;
|
||||||
|
onPostForgetClick?: (event: ButtonEvent) => void;
|
||||||
|
onPostLeaveClick?: (event: ButtonEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RoomGeneralContextMenu = ({
|
||||||
|
room, onFinished,
|
||||||
|
onPostFavoriteClick, onPostLowPriorityClick, onPostInviteClick, onPostCopyLinkClick, onPostSettingsClick,
|
||||||
|
onPostLeaveClick, onPostForgetClick, ...props
|
||||||
|
}: IProps) => {
|
||||||
|
const cli = useContext(MatrixClientContext);
|
||||||
|
const roomTags = useEventEmitterState(
|
||||||
|
RoomListStore.instance,
|
||||||
|
LISTS_UPDATE_EVENT,
|
||||||
|
() => RoomListStore.instance.getTagsForRoom(room),
|
||||||
|
);
|
||||||
|
const isDm = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
||||||
|
const wrapHandler = (
|
||||||
|
handler: (ev: ButtonEvent) => void,
|
||||||
|
postHandler?: (ev: ButtonEvent) => void,
|
||||||
|
persistent = false,
|
||||||
|
): (ev: ButtonEvent) => void => {
|
||||||
|
return (ev: ButtonEvent) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
handler(ev);
|
||||||
|
|
||||||
|
const action = getKeyBindingsManager().getAccessibilityAction(ev as React.KeyboardEvent);
|
||||||
|
if (!persistent || action === KeyBindingAction.Enter) {
|
||||||
|
onFinished();
|
||||||
|
}
|
||||||
|
postHandler?.(ev);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTagRoom = (ev: ButtonEvent, tagId: TagID) => {
|
||||||
|
if (tagId === DefaultTagID.Favourite || tagId === DefaultTagID.LowPriority) {
|
||||||
|
const inverseTag = tagId === DefaultTagID.Favourite ? DefaultTagID.LowPriority : DefaultTagID.Favourite;
|
||||||
|
const isApplied = RoomListStore.instance.getTagsForRoom(room).includes(tagId);
|
||||||
|
const removeTag = isApplied ? tagId : inverseTag;
|
||||||
|
const addTag = isApplied ? null : tagId;
|
||||||
|
dis.dispatch(RoomListActions.tagRoom(cli, room, removeTag, addTag, undefined, 0));
|
||||||
|
} else {
|
||||||
|
logger.warn(`Unexpected tag ${tagId} applied to ${room.roomId}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isFavorite = roomTags.includes(DefaultTagID.Favourite);
|
||||||
|
const favoriteOption: JSX.Element = <IconizedContextMenuCheckbox
|
||||||
|
onClick={wrapHandler((ev) =>
|
||||||
|
onTagRoom(ev, DefaultTagID.Favourite), onPostFavoriteClick, true)}
|
||||||
|
active={isFavorite}
|
||||||
|
label={isFavorite ? _t("Favourited") : _t("Favourite")}
|
||||||
|
iconClassName="mx_RoomGeneralContextMenu_iconStar"
|
||||||
|
/>;
|
||||||
|
|
||||||
|
const isLowPriority = roomTags.includes(DefaultTagID.LowPriority);
|
||||||
|
const lowPriorityOption: JSX.Element = <IconizedContextMenuCheckbox
|
||||||
|
onClick={wrapHandler((ev) =>
|
||||||
|
onTagRoom(ev, DefaultTagID.LowPriority), onPostLowPriorityClick, true)}
|
||||||
|
active={isLowPriority}
|
||||||
|
label={_t("Low Priority")}
|
||||||
|
iconClassName="mx_RoomGeneralContextMenu_iconArrowDown"
|
||||||
|
/>;
|
||||||
|
|
||||||
|
let inviteOption: JSX.Element;
|
||||||
|
if (room.canInvite(cli.getUserId()) && !isDm) {
|
||||||
|
inviteOption = <IconizedContextMenuOption
|
||||||
|
onClick={wrapHandler(() => dis.dispatch({
|
||||||
|
action: "view_invite",
|
||||||
|
roomId: room.roomId,
|
||||||
|
}), onPostInviteClick)}
|
||||||
|
label={_t("Invite")}
|
||||||
|
iconClassName="mx_RoomGeneralContextMenu_iconInvite"
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let copyLinkOption: JSX.Element;
|
||||||
|
if (!isDm) {
|
||||||
|
copyLinkOption = <IconizedContextMenuOption
|
||||||
|
onClick={wrapHandler(() => dis.dispatch({
|
||||||
|
action: "copy_room",
|
||||||
|
room_id: room.roomId,
|
||||||
|
}), onPostCopyLinkClick)}
|
||||||
|
label={_t("Copy room link")}
|
||||||
|
iconClassName="mx_RoomGeneralContextMenu_iconCopyLink"
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsOption: JSX.Element = <IconizedContextMenuOption
|
||||||
|
onClick={wrapHandler(() => dis.dispatch({
|
||||||
|
action: "open_room_settings",
|
||||||
|
room_id: room.roomId,
|
||||||
|
}), onPostSettingsClick)}
|
||||||
|
label={_t("Settings")}
|
||||||
|
iconClassName="mx_RoomGeneralContextMenu_iconSettings"
|
||||||
|
/>;
|
||||||
|
|
||||||
|
let leaveOption: JSX.Element;
|
||||||
|
if (roomTags.includes(DefaultTagID.Archived)) {
|
||||||
|
leaveOption = <IconizedContextMenuOption
|
||||||
|
iconClassName="mx_RoomGeneralContextMenu_iconSignOut"
|
||||||
|
label={_t("Forget Room")}
|
||||||
|
className="mx_IconizedContextMenu_option_red"
|
||||||
|
onClick={wrapHandler(() => dis.dispatch({
|
||||||
|
action: "forget_room",
|
||||||
|
room_id: room.roomId,
|
||||||
|
}), onPostForgetClick)}
|
||||||
|
/>;
|
||||||
|
} else {
|
||||||
|
leaveOption = <IconizedContextMenuOption
|
||||||
|
onClick={wrapHandler(() => dis.dispatch({
|
||||||
|
action: "leave_room",
|
||||||
|
room_id: room.roomId,
|
||||||
|
}), onPostLeaveClick)}
|
||||||
|
label={_t("Leave")}
|
||||||
|
className="mx_IconizedContextMenu_option_red"
|
||||||
|
iconClassName="mx_RoomGeneralContextMenu_iconSignOut"
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <IconizedContextMenu
|
||||||
|
{...props}
|
||||||
|
onFinished={onFinished}
|
||||||
|
className="mx_RoomGeneralContextMenu"
|
||||||
|
compact
|
||||||
|
>
|
||||||
|
{ !roomTags.includes(DefaultTagID.Archived) && (
|
||||||
|
<IconizedContextMenuOptionList>
|
||||||
|
{ favoriteOption }
|
||||||
|
{ lowPriorityOption }
|
||||||
|
{ inviteOption }
|
||||||
|
{ copyLinkOption }
|
||||||
|
{ settingsOption }
|
||||||
|
</IconizedContextMenuOptionList>
|
||||||
|
) }
|
||||||
|
<IconizedContextMenuOptionList red>
|
||||||
|
{ leaveOption }
|
||||||
|
</IconizedContextMenuOptionList>
|
||||||
|
</IconizedContextMenu>;
|
||||||
|
};
|
|
@ -0,0 +1,97 @@
|
||||||
|
/*
|
||||||
|
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 { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||||
|
import { useNotificationState } from "../../../hooks/useRoomNotificationState";
|
||||||
|
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
import { RoomNotifState } from "../../../RoomNotifs";
|
||||||
|
import { IProps as IContextMenuProps } from "../../structures/ContextMenu";
|
||||||
|
import IconizedContextMenu, {
|
||||||
|
IconizedContextMenuOptionList,
|
||||||
|
IconizedContextMenuRadio,
|
||||||
|
} from "../context_menus/IconizedContextMenu";
|
||||||
|
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||||
|
|
||||||
|
interface IProps extends IContextMenuProps {
|
||||||
|
room: Room;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RoomNotificationContextMenu = ({ room, onFinished, ...props }: IProps) => {
|
||||||
|
const [notificationState, setNotificationState] = useNotificationState(room);
|
||||||
|
|
||||||
|
const wrapHandler = (
|
||||||
|
handler: (ev: ButtonEvent) => void,
|
||||||
|
persistent = false,
|
||||||
|
): (ev: ButtonEvent) => void => {
|
||||||
|
return (ev: ButtonEvent) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
handler(ev);
|
||||||
|
|
||||||
|
const action = getKeyBindingsManager().getAccessibilityAction(ev as React.KeyboardEvent);
|
||||||
|
if (!persistent || action === KeyBindingAction.Enter) {
|
||||||
|
onFinished();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultOption: JSX.Element = <IconizedContextMenuRadio
|
||||||
|
label={_t("Use default")}
|
||||||
|
active={notificationState === RoomNotifState.AllMessages}
|
||||||
|
iconClassName="mx_RoomNotificationContextMenu_iconBell"
|
||||||
|
onClick={wrapHandler(() => setNotificationState(RoomNotifState.AllMessages))}
|
||||||
|
/>;
|
||||||
|
|
||||||
|
const allMessagesOption: JSX.Element = <IconizedContextMenuRadio
|
||||||
|
label={_t("All messages")}
|
||||||
|
active={notificationState === RoomNotifState.AllMessagesLoud}
|
||||||
|
iconClassName="mx_RoomNotificationContextMenu_iconBellDot"
|
||||||
|
onClick={wrapHandler(() => setNotificationState(RoomNotifState.AllMessagesLoud))}
|
||||||
|
/>;
|
||||||
|
|
||||||
|
const mentionsOption: JSX.Element = <IconizedContextMenuRadio
|
||||||
|
label={_t("Mentions & Keywords")}
|
||||||
|
active={notificationState === RoomNotifState.MentionsOnly}
|
||||||
|
iconClassName="mx_RoomNotificationContextMenu_iconBellMentions"
|
||||||
|
onClick={wrapHandler(() => setNotificationState(RoomNotifState.MentionsOnly))}
|
||||||
|
/>;
|
||||||
|
|
||||||
|
const muteOption: JSX.Element = <IconizedContextMenuRadio
|
||||||
|
label={_t("None")}
|
||||||
|
active={notificationState === RoomNotifState.Mute}
|
||||||
|
iconClassName="mx_RoomNotificationContextMenu_iconBellCrossed"
|
||||||
|
onClick={wrapHandler(() => setNotificationState(RoomNotifState.Mute))}
|
||||||
|
/>;
|
||||||
|
|
||||||
|
return <IconizedContextMenu
|
||||||
|
{...props}
|
||||||
|
onFinished={onFinished}
|
||||||
|
className="mx_RoomNotificationContextMenu"
|
||||||
|
compact
|
||||||
|
>
|
||||||
|
<IconizedContextMenuOptionList first>
|
||||||
|
{ defaultOption }
|
||||||
|
{ allMessagesOption }
|
||||||
|
{ mentionsOption }
|
||||||
|
{ muteOption }
|
||||||
|
</IconizedContextMenuOptionList>
|
||||||
|
</IconizedContextMenu>;
|
||||||
|
};
|
|
@ -37,7 +37,9 @@ export const Option: React.FC<OptionProps> = ({ inputRef, children, endAdornment
|
||||||
role="option"
|
role="option"
|
||||||
>
|
>
|
||||||
{ children }
|
{ children }
|
||||||
<kbd className="mx_SpotlightDialog_enterPrompt" aria-hidden>↵</kbd>
|
<div className="mx_SpotlightDialog_option--endAdornment">
|
||||||
{ endAdornment }
|
<kbd className="mx_SpotlightDialog_enterPrompt" aria-hidden>↵</kbd>
|
||||||
|
{ endAdornment }
|
||||||
|
</div>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021-2022 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 classNames from "classnames";
|
||||||
|
import { Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
import React, { Fragment, useState } from "react";
|
||||||
|
|
||||||
|
import { ContextMenuTooltipButton } from "../../../../accessibility/context_menu/ContextMenuTooltipButton";
|
||||||
|
import { useNotificationState } from "../../../../hooks/useRoomNotificationState";
|
||||||
|
import { _t } from "../../../../languageHandler";
|
||||||
|
import { RoomNotifState } from "../../../../RoomNotifs";
|
||||||
|
import { RoomGeneralContextMenu } from "../../context_menus/RoomGeneralContextMenu";
|
||||||
|
import { RoomNotificationContextMenu } from "../../context_menus/RoomNotificationContextMenu";
|
||||||
|
import SpaceContextMenu from "../../context_menus/SpaceContextMenu";
|
||||||
|
import { ButtonEvent } from "../../elements/AccessibleButton";
|
||||||
|
import { contextMenuBelow } from "../../rooms/RoomTile";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
room: Room;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RoomResultContextMenus({ room }: Props) {
|
||||||
|
const [notificationState] = useNotificationState(room);
|
||||||
|
|
||||||
|
const [generalMenuPosition, setGeneralMenuPosition] = useState<DOMRect | null>(null);
|
||||||
|
const [notificationMenuPosition, setNotificationMenuPosition] = useState<DOMRect | null>(null);
|
||||||
|
|
||||||
|
let generalMenu: JSX.Element;
|
||||||
|
if (generalMenuPosition !== null) {
|
||||||
|
if (room.isSpaceRoom()) {
|
||||||
|
generalMenu = <SpaceContextMenu
|
||||||
|
{...contextMenuBelow(generalMenuPosition)}
|
||||||
|
space={room}
|
||||||
|
onFinished={() => setGeneralMenuPosition(null)}
|
||||||
|
/>;
|
||||||
|
} else {
|
||||||
|
generalMenu = <RoomGeneralContextMenu
|
||||||
|
{...contextMenuBelow(generalMenuPosition)}
|
||||||
|
room={room}
|
||||||
|
onFinished={() => setGeneralMenuPosition(null)}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let notificationMenu: JSX.Element;
|
||||||
|
if (notificationMenuPosition !== null) {
|
||||||
|
notificationMenu = <RoomNotificationContextMenu
|
||||||
|
{...contextMenuBelow(notificationMenuPosition)}
|
||||||
|
room={room}
|
||||||
|
onFinished={() => setNotificationMenuPosition(null)}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const notificationMenuClasses = classNames("mx_SpotlightDialog_option--notifications", {
|
||||||
|
// Show bell icon for the default case too.
|
||||||
|
mx_RoomNotificationContextMenu_iconBell: notificationState === RoomNotifState.AllMessages,
|
||||||
|
mx_RoomNotificationContextMenu_iconBellDot: notificationState === RoomNotifState.AllMessagesLoud,
|
||||||
|
mx_RoomNotificationContextMenu_iconBellMentions: notificationState === RoomNotifState.MentionsOnly,
|
||||||
|
mx_RoomNotificationContextMenu_iconBellCrossed: notificationState === RoomNotifState.Mute,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<ContextMenuTooltipButton
|
||||||
|
className="mx_SpotlightDialog_option--menu"
|
||||||
|
onClick={(ev: ButtonEvent) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
const target = ev.target as HTMLElement;
|
||||||
|
setGeneralMenuPosition(target.getBoundingClientRect());
|
||||||
|
}}
|
||||||
|
title={room.isSpaceRoom() ? _t("Space options") : _t("Room options")}
|
||||||
|
isExpanded={generalMenuPosition !== null}
|
||||||
|
/>
|
||||||
|
{ !room.isSpaceRoom() && (
|
||||||
|
<ContextMenuTooltipButton
|
||||||
|
className={notificationMenuClasses}
|
||||||
|
onClick={(ev: ButtonEvent) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
const target = ev.target as HTMLElement;
|
||||||
|
setNotificationMenuPosition(target.getBoundingClientRect());
|
||||||
|
}}
|
||||||
|
title={_t("Notification options")}
|
||||||
|
isExpanded={notificationMenuPosition !== null}
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
{ generalMenu }
|
||||||
|
{ notificationMenu }
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
|
@ -88,6 +88,7 @@ import FeedbackDialog from "../FeedbackDialog";
|
||||||
import { IDialogProps } from "../IDialogProps";
|
import { IDialogProps } from "../IDialogProps";
|
||||||
import { Option } from "./Option";
|
import { Option } from "./Option";
|
||||||
import { PublicRoomResultDetails } from "./PublicRoomResultDetails";
|
import { PublicRoomResultDetails } from "./PublicRoomResultDetails";
|
||||||
|
import { RoomResultContextMenus } from "./RoomResultContextMenus";
|
||||||
import { RoomContextDetails } from "../../rooms/RoomContextDetails";
|
import { RoomContextDetails } from "../../rooms/RoomContextDetails";
|
||||||
import { TooltipOption } from "./TooltipOption";
|
import { TooltipOption } from "./TooltipOption";
|
||||||
|
|
||||||
|
@ -506,8 +507,11 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
||||||
let otherSearchesSection: JSX.Element;
|
let otherSearchesSection: JSX.Element;
|
||||||
if (trimmedQuery || filter !== Filter.PublicRooms) {
|
if (trimmedQuery || filter !== Filter.PublicRooms) {
|
||||||
otherSearchesSection = (
|
otherSearchesSection = (
|
||||||
<div className="mx_SpotlightDialog_section mx_SpotlightDialog_otherSearches" role="group">
|
<div
|
||||||
<h4>
|
className="mx_SpotlightDialog_section mx_SpotlightDialog_otherSearches"
|
||||||
|
role="group"
|
||||||
|
aria-labelledby="mx_SpotlightDialog_section_otherSearches">
|
||||||
|
<h4 id="mx_SpotlightDialog_section_otherSearches">
|
||||||
{ trimmedQuery
|
{ trimmedQuery
|
||||||
? _t('Use "%(query)s" to search', { query })
|
? _t('Use "%(query)s" to search', { query })
|
||||||
: _t("Search for") }
|
: _t("Search for") }
|
||||||
|
@ -544,7 +548,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
||||||
const unreadLabel = roomAriaUnreadLabel(result.room, notification);
|
const unreadLabel = roomAriaUnreadLabel(result.room, notification);
|
||||||
const ariaProperties = {
|
const ariaProperties = {
|
||||||
"aria-label": unreadLabel ? `${result.room.name} ${unreadLabel}` : result.room.name,
|
"aria-label": unreadLabel ? `${result.room.name} ${unreadLabel}` : result.room.name,
|
||||||
"aria-details": `mx_SpotlightDialog_button_result_${result.room.roomId}_details`,
|
"aria-describedby": `mx_SpotlightDialog_button_result_${result.room.roomId}_details`,
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Option
|
<Option
|
||||||
|
@ -553,6 +557,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
||||||
onClick={(ev) => {
|
onClick={(ev) => {
|
||||||
viewRoom(result.room.roomId, true, ev?.type !== "click");
|
viewRoom(result.room.roomId, true, ev?.type !== "click");
|
||||||
}}
|
}}
|
||||||
|
endAdornment={<RoomResultContextMenus room={result.room} />}
|
||||||
{...ariaProperties}
|
{...ariaProperties}
|
||||||
>
|
>
|
||||||
<DecoratedRoomAvatar
|
<DecoratedRoomAvatar
|
||||||
|
@ -948,8 +953,10 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
aria-labelledby="mx_SpotlightDialog_section_recentSearches"
|
aria-labelledby="mx_SpotlightDialog_section_recentSearches"
|
||||||
>
|
>
|
||||||
<h4 id="mx_SpotlightDialog_section_recentSearches">
|
<h4>
|
||||||
{ _t("Recent searches") }
|
<span id="mx_SpotlightDialog_section_recentSearches">
|
||||||
|
{ _t("Recent searches") }
|
||||||
|
</span>
|
||||||
<AccessibleButton kind="link" onClick={clearRecentSearches}>
|
<AccessibleButton kind="link" onClick={clearRecentSearches}>
|
||||||
{ _t("Clear") }
|
{ _t("Clear") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
@ -960,7 +967,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
||||||
const unreadLabel = roomAriaUnreadLabel(room, notification);
|
const unreadLabel = roomAriaUnreadLabel(room, notification);
|
||||||
const ariaProperties = {
|
const ariaProperties = {
|
||||||
"aria-label": unreadLabel ? `${room.name} ${unreadLabel}` : room.name,
|
"aria-label": unreadLabel ? `${room.name} ${unreadLabel}` : room.name,
|
||||||
"aria-details": `mx_SpotlightDialog_button_recentSearch_${room.roomId}_details`,
|
"aria-describedby": `mx_SpotlightDialog_button_recentSearch_${room.roomId}_details`,
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Option
|
<Option
|
||||||
|
@ -969,6 +976,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
||||||
onClick={(ev) => {
|
onClick={(ev) => {
|
||||||
viewRoom(room.roomId, true, ev?.type !== "click");
|
viewRoom(room.roomId, true, ev?.type !== "click");
|
||||||
}}
|
}}
|
||||||
|
endAdornment={<RoomResultContextMenus room={room} />}
|
||||||
{...ariaProperties}
|
{...ariaProperties}
|
||||||
>
|
>
|
||||||
<DecoratedRoomAvatar
|
<DecoratedRoomAvatar
|
||||||
|
@ -1034,6 +1042,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let ref: RefObject<HTMLElement>;
|
||||||
const accessibilityAction = getKeyBindingsManager().getAccessibilityAction(ev);
|
const accessibilityAction = getKeyBindingsManager().getAccessibilityAction(ev);
|
||||||
switch (accessibilityAction) {
|
switch (accessibilityAction) {
|
||||||
case KeyBindingAction.Escape:
|
case KeyBindingAction.Escape:
|
||||||
|
@ -1041,22 +1050,6 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
onFinished();
|
onFinished();
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onKeyDown = (ev: KeyboardEvent) => {
|
|
||||||
let ref: RefObject<HTMLElement>;
|
|
||||||
|
|
||||||
const action = getKeyBindingsManager().getAccessibilityAction(ev);
|
|
||||||
|
|
||||||
switch (action) {
|
|
||||||
case KeyBindingAction.Backspace:
|
|
||||||
if (!query && filter !== null) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
ev.preventDefault();
|
|
||||||
setFilter(null);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case KeyBindingAction.ArrowUp:
|
case KeyBindingAction.ArrowUp:
|
||||||
case KeyBindingAction.ArrowDown:
|
case KeyBindingAction.ArrowDown:
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
@ -1075,7 +1068,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
||||||
}
|
}
|
||||||
|
|
||||||
const idx = refs.indexOf(rovingContext.state.activeRef);
|
const idx = refs.indexOf(rovingContext.state.activeRef);
|
||||||
ref = findSiblingElement(refs, idx + (action === KeyBindingAction.ArrowUp ? -1 : 1));
|
ref = findSiblingElement(refs, idx + (accessibilityAction === KeyBindingAction.ArrowUp ? -1 : 1));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -1092,14 +1085,9 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
||||||
|
|
||||||
const refs = rovingContext.state.refs.filter(refIsForRecentlyViewed);
|
const refs = rovingContext.state.refs.filter(refIsForRecentlyViewed);
|
||||||
const idx = refs.indexOf(rovingContext.state.activeRef);
|
const idx = refs.indexOf(rovingContext.state.activeRef);
|
||||||
ref = findSiblingElement(refs, idx + (action === KeyBindingAction.ArrowLeft ? -1 : 1));
|
ref = findSiblingElement(refs, idx + (accessibilityAction === KeyBindingAction.ArrowLeft ? -1 : 1));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case KeyBindingAction.Enter:
|
|
||||||
ev.stopPropagation();
|
|
||||||
ev.preventDefault();
|
|
||||||
rovingContext.state.activeRef?.current?.click();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ref) {
|
if (ref) {
|
||||||
|
@ -1113,6 +1101,25 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onKeyDown = (ev: KeyboardEvent) => {
|
||||||
|
const action = getKeyBindingsManager().getAccessibilityAction(ev);
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case KeyBindingAction.Backspace:
|
||||||
|
if (!query && filter !== null) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
setFilter(null);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case KeyBindingAction.Enter:
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
rovingContext.state.activeRef?.current?.click();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const openFeedback = SdkConfig.get().bug_report_endpoint_url ? () => {
|
const openFeedback = SdkConfig.get().bug_report_endpoint_url ? () => {
|
||||||
Modal.createDialog(FeedbackDialog, {
|
Modal.createDialog(FeedbackDialog, {
|
||||||
feature: "spotlight",
|
feature: "spotlight",
|
||||||
|
|
|
@ -18,7 +18,6 @@ limitations under the License.
|
||||||
import React, { createRef } from "react";
|
import React, { createRef } from "react";
|
||||||
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
|
||||||
|
|
||||||
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
||||||
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
|
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
|
||||||
|
@ -32,9 +31,8 @@ import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewSto
|
||||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||||
import { RoomNotifState } from "../../../RoomNotifs";
|
import { RoomNotifState } from "../../../RoomNotifs";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
|
import { RoomNotificationContextMenu } from "../context_menus/RoomNotificationContextMenu";
|
||||||
import NotificationBadge from "./NotificationBadge";
|
import NotificationBadge from "./NotificationBadge";
|
||||||
import RoomListStore from "../../../stores/room-list/RoomListStore";
|
|
||||||
import RoomListActions from "../../../actions/RoomListActions";
|
|
||||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||||
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||||
import { NotificationState, NotificationStateEvents } from "../../../stores/notifications/NotificationState";
|
import { NotificationState, NotificationStateEvents } from "../../../stores/notifications/NotificationState";
|
||||||
|
@ -42,18 +40,13 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import { EchoChamber } from "../../../stores/local-echo/EchoChamber";
|
import { EchoChamber } from "../../../stores/local-echo/EchoChamber";
|
||||||
import { CachedRoomKey, RoomEchoChamber } from "../../../stores/local-echo/RoomEchoChamber";
|
import { CachedRoomKey, RoomEchoChamber } from "../../../stores/local-echo/RoomEchoChamber";
|
||||||
import { PROPERTY_UPDATED } from "../../../stores/local-echo/GenericEchoChamber";
|
import { PROPERTY_UPDATED } from "../../../stores/local-echo/GenericEchoChamber";
|
||||||
import IconizedContextMenu, {
|
|
||||||
IconizedContextMenuCheckbox,
|
|
||||||
IconizedContextMenuOption,
|
|
||||||
IconizedContextMenuOptionList,
|
|
||||||
IconizedContextMenuRadio,
|
|
||||||
} from "../context_menus/IconizedContextMenu";
|
|
||||||
import PosthogTrackers from "../../../PosthogTrackers";
|
import PosthogTrackers from "../../../PosthogTrackers";
|
||||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||||
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||||
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||||
import { RoomViewStore } from "../../../stores/RoomViewStore";
|
import { RoomViewStore } from "../../../stores/RoomViewStore";
|
||||||
import VideoRoomSummary from "./VideoRoomSummary";
|
import VideoRoomSummary from "./VideoRoomSummary";
|
||||||
|
import { RoomGeneralContextMenu } from "../context_menus/RoomGeneralContextMenu";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -267,118 +260,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
this.setState({ generalMenuPosition: null });
|
this.setState({ generalMenuPosition: null });
|
||||||
};
|
};
|
||||||
|
|
||||||
private onTagRoom = (ev: ButtonEvent, tagId: TagID) => {
|
|
||||||
ev.preventDefault();
|
|
||||||
ev.stopPropagation();
|
|
||||||
|
|
||||||
if (tagId === DefaultTagID.Favourite || tagId === DefaultTagID.LowPriority) {
|
|
||||||
const inverseTag = tagId === DefaultTagID.Favourite ? DefaultTagID.LowPriority : DefaultTagID.Favourite;
|
|
||||||
const isApplied = RoomListStore.instance.getTagsForRoom(this.props.room).includes(tagId);
|
|
||||||
const removeTag = isApplied ? tagId : inverseTag;
|
|
||||||
const addTag = isApplied ? null : tagId;
|
|
||||||
defaultDispatcher.dispatch(RoomListActions.tagRoom(
|
|
||||||
MatrixClientPeg.get(),
|
|
||||||
this.props.room,
|
|
||||||
removeTag,
|
|
||||||
addTag,
|
|
||||||
undefined,
|
|
||||||
0,
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
logger.warn(`Unexpected tag ${tagId} applied to ${this.props.room.roomId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const action = getKeyBindingsManager().getAccessibilityAction(ev as React.KeyboardEvent);
|
|
||||||
switch (action) {
|
|
||||||
case KeyBindingAction.Enter:
|
|
||||||
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
|
|
||||||
this.setState({ generalMenuPosition: null }); // hide the menu
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private onLeaveRoomClick = (ev: ButtonEvent) => {
|
|
||||||
ev.preventDefault();
|
|
||||||
ev.stopPropagation();
|
|
||||||
|
|
||||||
defaultDispatcher.dispatch({
|
|
||||||
action: 'leave_room',
|
|
||||||
room_id: this.props.room.roomId,
|
|
||||||
});
|
|
||||||
this.setState({ generalMenuPosition: null }); // hide the menu
|
|
||||||
|
|
||||||
PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuLeaveItem", ev);
|
|
||||||
};
|
|
||||||
|
|
||||||
private onForgetRoomClick = (ev: ButtonEvent) => {
|
|
||||||
ev.preventDefault();
|
|
||||||
ev.stopPropagation();
|
|
||||||
|
|
||||||
defaultDispatcher.dispatch({
|
|
||||||
action: 'forget_room',
|
|
||||||
room_id: this.props.room.roomId,
|
|
||||||
});
|
|
||||||
this.setState({ generalMenuPosition: null }); // hide the menu
|
|
||||||
};
|
|
||||||
|
|
||||||
private onOpenRoomSettings = (ev: ButtonEvent) => {
|
|
||||||
ev.preventDefault();
|
|
||||||
ev.stopPropagation();
|
|
||||||
|
|
||||||
defaultDispatcher.dispatch({
|
|
||||||
action: 'open_room_settings',
|
|
||||||
room_id: this.props.room.roomId,
|
|
||||||
});
|
|
||||||
this.setState({ generalMenuPosition: null }); // hide the menu
|
|
||||||
|
|
||||||
PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuSettingsItem", ev);
|
|
||||||
};
|
|
||||||
|
|
||||||
private onCopyRoomClick = (ev: ButtonEvent) => {
|
|
||||||
ev.preventDefault();
|
|
||||||
ev.stopPropagation();
|
|
||||||
|
|
||||||
defaultDispatcher.dispatch({
|
|
||||||
action: 'copy_room',
|
|
||||||
room_id: this.props.room.roomId,
|
|
||||||
});
|
|
||||||
this.setState({ generalMenuPosition: null }); // hide the menu
|
|
||||||
};
|
|
||||||
|
|
||||||
private onInviteClick = (ev: ButtonEvent) => {
|
|
||||||
ev.preventDefault();
|
|
||||||
ev.stopPropagation();
|
|
||||||
|
|
||||||
defaultDispatcher.dispatch({
|
|
||||||
action: 'view_invite',
|
|
||||||
roomId: this.props.room.roomId,
|
|
||||||
});
|
|
||||||
this.setState({ generalMenuPosition: null }); // hide the menu
|
|
||||||
|
|
||||||
PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuInviteItem", ev);
|
|
||||||
};
|
|
||||||
|
|
||||||
private async saveNotifState(ev: ButtonEvent, newState: RoomNotifState) {
|
|
||||||
ev.preventDefault();
|
|
||||||
ev.stopPropagation();
|
|
||||||
if (MatrixClientPeg.get().isGuest()) return;
|
|
||||||
|
|
||||||
this.roomProps.notificationVolume = newState;
|
|
||||||
|
|
||||||
const action = getKeyBindingsManager().getAccessibilityAction(ev as React.KeyboardEvent);
|
|
||||||
switch (action) {
|
|
||||||
case KeyBindingAction.Enter:
|
|
||||||
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
|
|
||||||
this.setState({ notificationsMenuPosition: null }); // hide the menu
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onClickAllNotifs = ev => this.saveNotifState(ev, RoomNotifState.AllMessages);
|
|
||||||
private onClickAlertMe = ev => this.saveNotifState(ev, RoomNotifState.AllMessagesLoud);
|
|
||||||
private onClickMentions = ev => this.saveNotifState(ev, RoomNotifState.MentionsOnly);
|
|
||||||
private onClickMute = ev => this.saveNotifState(ev, RoomNotifState.Mute);
|
|
||||||
|
|
||||||
private renderNotificationsMenu(isActive: boolean): React.ReactElement {
|
private renderNotificationsMenu(isActive: boolean): React.ReactElement {
|
||||||
if (MatrixClientPeg.get().isGuest() || this.props.tag === DefaultTagID.Archived ||
|
if (MatrixClientPeg.get().isGuest() || this.props.tag === DefaultTagID.Archived ||
|
||||||
!this.showContextMenu || this.props.isMinimized
|
!this.showContextMenu || this.props.isMinimized
|
||||||
|
@ -389,49 +270,12 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
|
|
||||||
const state = this.roomProps.notificationVolume;
|
const state = this.roomProps.notificationVolume;
|
||||||
|
|
||||||
let contextMenu = null;
|
|
||||||
if (this.state.notificationsMenuPosition) {
|
|
||||||
contextMenu = <IconizedContextMenu
|
|
||||||
{...contextMenuBelow(this.state.notificationsMenuPosition)}
|
|
||||||
onFinished={this.onCloseNotificationsMenu}
|
|
||||||
className="mx_RoomTile_contextMenu"
|
|
||||||
compact
|
|
||||||
>
|
|
||||||
<IconizedContextMenuOptionList first>
|
|
||||||
<IconizedContextMenuRadio
|
|
||||||
label={_t("Use default")}
|
|
||||||
active={state === RoomNotifState.AllMessages}
|
|
||||||
iconClassName="mx_RoomTile_iconBell"
|
|
||||||
onClick={this.onClickAllNotifs}
|
|
||||||
/>
|
|
||||||
<IconizedContextMenuRadio
|
|
||||||
label={_t("All messages")}
|
|
||||||
active={state === RoomNotifState.AllMessagesLoud}
|
|
||||||
iconClassName="mx_RoomTile_iconBellDot"
|
|
||||||
onClick={this.onClickAlertMe}
|
|
||||||
/>
|
|
||||||
<IconizedContextMenuRadio
|
|
||||||
label={_t("Mentions & Keywords")}
|
|
||||||
active={state === RoomNotifState.MentionsOnly}
|
|
||||||
iconClassName="mx_RoomTile_iconBellMentions"
|
|
||||||
onClick={this.onClickMentions}
|
|
||||||
/>
|
|
||||||
<IconizedContextMenuRadio
|
|
||||||
label={_t("None")}
|
|
||||||
active={state === RoomNotifState.Mute}
|
|
||||||
iconClassName="mx_RoomTile_iconBellCrossed"
|
|
||||||
onClick={this.onClickMute}
|
|
||||||
/>
|
|
||||||
</IconizedContextMenuOptionList>
|
|
||||||
</IconizedContextMenu>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const classes = classNames("mx_RoomTile_notificationsButton", {
|
const classes = classNames("mx_RoomTile_notificationsButton", {
|
||||||
// Show bell icon for the default case too.
|
// Show bell icon for the default case too.
|
||||||
mx_RoomTile_iconBell: state === RoomNotifState.AllMessages,
|
mx_RoomNotificationContextMenu_iconBell: state === RoomNotifState.AllMessages,
|
||||||
mx_RoomTile_iconBellDot: state === RoomNotifState.AllMessagesLoud,
|
mx_RoomNotificationContextMenu_iconBellDot: state === RoomNotifState.AllMessagesLoud,
|
||||||
mx_RoomTile_iconBellMentions: state === RoomNotifState.MentionsOnly,
|
mx_RoomNotificationContextMenu_iconBellMentions: state === RoomNotifState.MentionsOnly,
|
||||||
mx_RoomTile_iconBellCrossed: state === RoomNotifState.Mute,
|
mx_RoomNotificationContextMenu_iconBellCrossed: state === RoomNotifState.Mute,
|
||||||
|
|
||||||
// Only show the icon by default if the room is overridden to muted.
|
// Only show the icon by default if the room is overridden to muted.
|
||||||
// TODO: [FTUE Notifications] Probably need to detect global mute state
|
// TODO: [FTUE Notifications] Probably need to detect global mute state
|
||||||
|
@ -447,93 +291,19 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
isExpanded={!!this.state.notificationsMenuPosition}
|
isExpanded={!!this.state.notificationsMenuPosition}
|
||||||
tabIndex={isActive ? 0 : -1}
|
tabIndex={isActive ? 0 : -1}
|
||||||
/>
|
/>
|
||||||
{ contextMenu }
|
{ this.state.notificationsMenuPosition && (
|
||||||
|
<RoomNotificationContextMenu
|
||||||
|
{...contextMenuBelow(this.state.notificationsMenuPosition)}
|
||||||
|
onFinished={this.onCloseNotificationsMenu}
|
||||||
|
room={this.props.room}
|
||||||
|
/>
|
||||||
|
) }
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderGeneralMenu(): React.ReactElement {
|
private renderGeneralMenu(): React.ReactElement {
|
||||||
if (!this.showContextMenu) return null; // no menu to show
|
if (!this.showContextMenu) return null; // no menu to show
|
||||||
|
|
||||||
let contextMenu = null;
|
|
||||||
if (this.state.generalMenuPosition && this.props.tag === DefaultTagID.Archived) {
|
|
||||||
contextMenu = <IconizedContextMenu
|
|
||||||
{...contextMenuBelow(this.state.generalMenuPosition)}
|
|
||||||
onFinished={this.onCloseGeneralMenu}
|
|
||||||
className="mx_RoomTile_contextMenu"
|
|
||||||
compact
|
|
||||||
>
|
|
||||||
<IconizedContextMenuOptionList red>
|
|
||||||
<IconizedContextMenuOption
|
|
||||||
iconClassName="mx_RoomTile_iconSignOut"
|
|
||||||
label={_t("Forget Room")}
|
|
||||||
onClick={this.onForgetRoomClick}
|
|
||||||
/>
|
|
||||||
</IconizedContextMenuOptionList>
|
|
||||||
</IconizedContextMenu>;
|
|
||||||
} else if (this.state.generalMenuPosition) {
|
|
||||||
const roomTags = RoomListStore.instance.getTagsForRoom(this.props.room);
|
|
||||||
|
|
||||||
const isFavorite = roomTags.includes(DefaultTagID.Favourite);
|
|
||||||
const favouriteLabel = isFavorite ? _t("Favourited") : _t("Favourite");
|
|
||||||
|
|
||||||
const isLowPriority = roomTags.includes(DefaultTagID.LowPriority);
|
|
||||||
const lowPriorityLabel = _t("Low Priority");
|
|
||||||
|
|
||||||
const isDm = roomTags.includes(DefaultTagID.DM);
|
|
||||||
|
|
||||||
const userId = MatrixClientPeg.get().getUserId();
|
|
||||||
const canInvite = this.props.room.canInvite(userId) && !isDm; // hide invite in DMs from this quick menu
|
|
||||||
contextMenu = <IconizedContextMenu
|
|
||||||
{...contextMenuBelow(this.state.generalMenuPosition)}
|
|
||||||
onFinished={this.onCloseGeneralMenu}
|
|
||||||
className="mx_RoomTile_contextMenu"
|
|
||||||
compact
|
|
||||||
>
|
|
||||||
<IconizedContextMenuOptionList>
|
|
||||||
<IconizedContextMenuCheckbox
|
|
||||||
onClick={(e) => {
|
|
||||||
this.onTagRoom(e, DefaultTagID.Favourite);
|
|
||||||
PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuFavouriteToggle", e);
|
|
||||||
}}
|
|
||||||
active={isFavorite}
|
|
||||||
label={favouriteLabel}
|
|
||||||
iconClassName="mx_RoomTile_iconStar"
|
|
||||||
/>
|
|
||||||
<IconizedContextMenuCheckbox
|
|
||||||
onClick={(e) => this.onTagRoom(e, DefaultTagID.LowPriority)}
|
|
||||||
active={isLowPriority}
|
|
||||||
label={lowPriorityLabel}
|
|
||||||
iconClassName="mx_RoomTile_iconArrowDown"
|
|
||||||
/>
|
|
||||||
{ canInvite ? (
|
|
||||||
<IconizedContextMenuOption
|
|
||||||
onClick={this.onInviteClick}
|
|
||||||
label={_t("Invite")}
|
|
||||||
iconClassName="mx_RoomTile_iconInvite"
|
|
||||||
/>
|
|
||||||
) : null }
|
|
||||||
{ !isDm ? <IconizedContextMenuOption
|
|
||||||
onClick={this.onCopyRoomClick}
|
|
||||||
label={_t("Copy room link")}
|
|
||||||
iconClassName="mx_RoomTile_iconCopyLink"
|
|
||||||
/> : null }
|
|
||||||
<IconizedContextMenuOption
|
|
||||||
onClick={this.onOpenRoomSettings}
|
|
||||||
label={_t("Settings")}
|
|
||||||
iconClassName="mx_RoomTile_iconSettings"
|
|
||||||
/>
|
|
||||||
</IconizedContextMenuOptionList>
|
|
||||||
<IconizedContextMenuOptionList red>
|
|
||||||
<IconizedContextMenuOption
|
|
||||||
onClick={this.onLeaveRoomClick}
|
|
||||||
label={_t("Leave")}
|
|
||||||
iconClassName="mx_RoomTile_iconSignOut"
|
|
||||||
/>
|
|
||||||
</IconizedContextMenuOptionList>
|
|
||||||
</IconizedContextMenu>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<ContextMenuTooltipButton
|
<ContextMenuTooltipButton
|
||||||
|
@ -542,7 +312,25 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
title={_t("Room options")}
|
title={_t("Room options")}
|
||||||
isExpanded={!!this.state.generalMenuPosition}
|
isExpanded={!!this.state.generalMenuPosition}
|
||||||
/>
|
/>
|
||||||
{ contextMenu }
|
{ this.state.generalMenuPosition && (
|
||||||
|
<RoomGeneralContextMenu
|
||||||
|
{...contextMenuBelow(this.state.generalMenuPosition)}
|
||||||
|
onFinished={this.onCloseGeneralMenu}
|
||||||
|
room={this.props.room}
|
||||||
|
onPostFavoriteClick={(ev: ButtonEvent) => PosthogTrackers.trackInteraction(
|
||||||
|
"WebRoomListRoomTileContextMenuFavouriteToggle", ev,
|
||||||
|
)}
|
||||||
|
onPostInviteClick={(ev: ButtonEvent) => PosthogTrackers.trackInteraction(
|
||||||
|
"WebRoomListRoomTileContextMenuInviteItem", ev,
|
||||||
|
)}
|
||||||
|
onPostSettingsClick={(ev: ButtonEvent) => PosthogTrackers.trackInteraction(
|
||||||
|
"WebRoomListRoomTileContextMenuSettingsItem", ev,
|
||||||
|
)}
|
||||||
|
onPostLeaveClick={(ev: ButtonEvent) => PosthogTrackers.trackInteraction(
|
||||||
|
"WebRoomListRoomTileContextMenuLeaveItem", ev,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) }
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
41
src/hooks/useRoomNotificationState.ts
Normal file
41
src/hooks/useRoomNotificationState.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
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 { useCallback, useMemo, useState } from "react";
|
||||||
|
import { Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { RoomNotifState } from "../RoomNotifs";
|
||||||
|
import { EchoChamber } from "../stores/local-echo/EchoChamber";
|
||||||
|
import { PROPERTY_UPDATED } from "../stores/local-echo/GenericEchoChamber";
|
||||||
|
import { CachedRoomKey } from "../stores/local-echo/RoomEchoChamber";
|
||||||
|
import { useEventEmitter } from "./useEventEmitter";
|
||||||
|
|
||||||
|
export const useNotificationState = (room: Room): [RoomNotifState, (state: RoomNotifState) => void] => {
|
||||||
|
const echoChamber = useMemo(() => EchoChamber.forRoom(room), [room]);
|
||||||
|
const [notificationState, setNotificationState] = useState<RoomNotifState>(
|
||||||
|
echoChamber.notificationVolume,
|
||||||
|
);
|
||||||
|
useEventEmitter(echoChamber, PROPERTY_UPDATED, (key: CachedRoomKey) => {
|
||||||
|
if (key === CachedRoomKey.NotificationVolume) {
|
||||||
|
setNotificationState(echoChamber.notificationVolume);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const setter = useCallback(
|
||||||
|
(state: RoomNotifState) => echoChamber.notificationVolume = state,
|
||||||
|
[echoChamber],
|
||||||
|
);
|
||||||
|
return [notificationState, setter];
|
||||||
|
};
|
|
@ -1880,14 +1880,7 @@
|
||||||
"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",
|
||||||
"Show less": "Show less",
|
"Show less": "Show less",
|
||||||
"Use default": "Use default",
|
|
||||||
"Mentions & Keywords": "Mentions & Keywords",
|
|
||||||
"Notification options": "Notification options",
|
"Notification options": "Notification options",
|
||||||
"Forget Room": "Forget Room",
|
|
||||||
"Favourited": "Favourited",
|
|
||||||
"Favourite": "Favourite",
|
|
||||||
"Low Priority": "Low Priority",
|
|
||||||
"Copy room link": "Copy room link",
|
|
||||||
"%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.",
|
"%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.",
|
||||||
"%(count)s unread messages including mentions.|one": "1 unread mention.",
|
"%(count)s unread messages including mentions.|one": "1 unread mention.",
|
||||||
"%(count)s unread messages.|other": "%(count)s unread messages.",
|
"%(count)s unread messages.|other": "%(count)s unread messages.",
|
||||||
|
@ -2938,7 +2931,14 @@
|
||||||
"Report": "Report",
|
"Report": "Report",
|
||||||
"Copy link": "Copy link",
|
"Copy link": "Copy link",
|
||||||
"Forget": "Forget",
|
"Forget": "Forget",
|
||||||
|
"Favourited": "Favourited",
|
||||||
|
"Favourite": "Favourite",
|
||||||
"Mentions only": "Mentions only",
|
"Mentions only": "Mentions only",
|
||||||
|
"Copy room link": "Copy room link",
|
||||||
|
"Low Priority": "Low Priority",
|
||||||
|
"Forget Room": "Forget Room",
|
||||||
|
"Use default": "Use default",
|
||||||
|
"Mentions & Keywords": "Mentions & Keywords",
|
||||||
"See room timeline (devtools)": "See room timeline (devtools)",
|
"See room timeline (devtools)": "See room timeline (devtools)",
|
||||||
"Space": "Space",
|
"Space": "Space",
|
||||||
"Space home": "Space home",
|
"Space home": "Space home",
|
||||||
|
|
Loading…
Reference in a new issue