Merge pull request #4896 from matrix-org/t3chguy/room-list/123
New Room List accessibilitypull/21833/head
commit
502a0d930d
|
@ -86,11 +86,15 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations
|
||||||
|
|
||||||
.mx_RoomSearch_expanded + .mx_LeftPanel2_exploreButton {
|
.mx_RoomSearch_expanded + .mx_LeftPanel2_exploreButton {
|
||||||
// Cheaty way to return the occupied space to the filter input
|
// Cheaty way to return the occupied space to the filter input
|
||||||
|
flex-basis: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
width: 0;
|
width: 0;
|
||||||
|
|
||||||
// Don't forget to hide the masked ::before icon
|
// Don't forget to hide the masked ::before icon,
|
||||||
visibility: hidden;
|
// using display:none or visibility:hidden would break accessibility
|
||||||
|
&::before {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_LeftPanel2_exploreButton {
|
.mx_LeftPanel2_exploreButton {
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
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 {
|
||||||
|
ALL_MESSAGES,
|
||||||
|
ALL_MESSAGES_LOUD,
|
||||||
|
MENTIONS_ONLY,
|
||||||
|
MUTE,
|
||||||
|
} from "./RoomNotifs";
|
||||||
|
|
||||||
|
export type Volume = ALL_MESSAGES_LOUD | ALL_MESSAGES | MENTIONS_ONLY | MUTE;
|
|
@ -23,6 +23,8 @@ import classNames from 'classnames';
|
||||||
import {Key} from "../../Keyboard";
|
import {Key} from "../../Keyboard";
|
||||||
import * as sdk from "../../index";
|
import * as sdk from "../../index";
|
||||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||||
|
import StyledCheckbox from "../views/elements/StyledCheckbox";
|
||||||
|
import StyledRadioButton from "../views/elements/StyledRadioButton";
|
||||||
|
|
||||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||||
|
@ -421,6 +423,54 @@ MenuItemCheckbox.propTypes = {
|
||||||
onClick: PropTypes.func.isRequired,
|
onClick: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Semantic component for representing a styled role=menuitemcheckbox
|
||||||
|
export const StyledMenuItemCheckbox = ({children, label, onChange, onClose, checked, disabled=false, ...props}) => {
|
||||||
|
const onKeyDown = (e) => {
|
||||||
|
if (e.key === Key.ENTER || e.key === Key.SPACE) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
onChange();
|
||||||
|
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
|
||||||
|
if (e.key === Key.ENTER) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onKeyUp = (e) => {
|
||||||
|
// prevent the input default handler as we handle it on keydown to match
|
||||||
|
// https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-2/menubar-2.html
|
||||||
|
if (e.key === Key.SPACE || e.key === Key.ENTER) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<StyledCheckbox
|
||||||
|
{...props}
|
||||||
|
role="menuitemcheckbox"
|
||||||
|
aria-checked={checked}
|
||||||
|
checked={checked}
|
||||||
|
aria-disabled={disabled}
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-label={label}
|
||||||
|
onChange={onChange}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onKeyUp={onKeyUp}
|
||||||
|
>
|
||||||
|
{ children }
|
||||||
|
</StyledCheckbox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
StyledMenuItemCheckbox.propTypes = {
|
||||||
|
...StyledCheckbox.propTypes,
|
||||||
|
label: PropTypes.string, // optional
|
||||||
|
checked: PropTypes.bool.isRequired,
|
||||||
|
disabled: PropTypes.bool, // optional
|
||||||
|
className: PropTypes.string, // optional
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
onClose: PropTypes.func.isRequired, // gets called after onChange on Key.ENTER
|
||||||
|
};
|
||||||
|
|
||||||
// Semantic component for representing a role=menuitemradio
|
// Semantic component for representing a role=menuitemradio
|
||||||
export const MenuItemRadio = ({children, label, active=false, disabled=false, ...props}) => {
|
export const MenuItemRadio = ({children, label, active=false, disabled=false, ...props}) => {
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
|
@ -439,6 +489,54 @@ MenuItemRadio.propTypes = {
|
||||||
onClick: PropTypes.func.isRequired,
|
onClick: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Semantic component for representing a styled role=menuitemradio
|
||||||
|
export const StyledMenuItemRadio = ({children, label, onChange, onClose, checked=false, disabled=false, ...props}) => {
|
||||||
|
const onKeyDown = (e) => {
|
||||||
|
if (e.key === Key.ENTER || e.key === Key.SPACE) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
onChange();
|
||||||
|
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
|
||||||
|
if (e.key === Key.ENTER) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onKeyUp = (e) => {
|
||||||
|
// prevent the input default handler as we handle it on keydown to match
|
||||||
|
// https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-2/menubar-2.html
|
||||||
|
if (e.key === Key.SPACE || e.key === Key.ENTER) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<StyledRadioButton
|
||||||
|
{...props}
|
||||||
|
role="menuitemradio"
|
||||||
|
aria-checked={checked}
|
||||||
|
checked={checked}
|
||||||
|
aria-disabled={disabled}
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-label={label}
|
||||||
|
onChange={onChange}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onKeyUp={onKeyUp}
|
||||||
|
>
|
||||||
|
{ children }
|
||||||
|
</StyledRadioButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
StyledMenuItemRadio.propTypes = {
|
||||||
|
...StyledMenuItemRadio.propTypes,
|
||||||
|
label: PropTypes.string, // optional
|
||||||
|
checked: PropTypes.bool.isRequired,
|
||||||
|
disabled: PropTypes.bool, // optional
|
||||||
|
className: PropTypes.string, // optional
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
onClose: PropTypes.func.isRequired, // gets called after onChange on Key.ENTER
|
||||||
|
};
|
||||||
|
|
||||||
// Placement method for <ContextMenu /> to position context menu to right of elementRect with chevronOffset
|
// Placement method for <ContextMenu /> to position context menu to right of elementRect with chevronOffset
|
||||||
export const toRightOf = (elementRect, chevronOffset=12) => {
|
export const toRightOf = (elementRect, chevronOffset=12) => {
|
||||||
const left = elementRect.right + window.pageXOffset + 3;
|
const left = elementRect.right + window.pageXOffset + 3;
|
||||||
|
|
|
@ -56,6 +56,15 @@ interface IState {
|
||||||
showTagPanel: boolean;
|
showTagPanel: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List of CSS classes which should be included in keyboard navigation within the room list
|
||||||
|
const cssClasses = [
|
||||||
|
"mx_RoomSearch_input",
|
||||||
|
"mx_RoomSearch_icon", // minimized <RoomSearch />
|
||||||
|
"mx_RoomSublist2_headerText",
|
||||||
|
"mx_RoomTile2",
|
||||||
|
"mx_RoomSublist2_showNButton",
|
||||||
|
];
|
||||||
|
|
||||||
export default class LeftPanel2 extends React.Component<IProps, IState> {
|
export default class LeftPanel2 extends React.Component<IProps, IState> {
|
||||||
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
|
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
|
||||||
private tagPanelWatcherRef: string;
|
private tagPanelWatcherRef: string;
|
||||||
|
@ -212,10 +221,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
||||||
if (element) {
|
if (element) {
|
||||||
classes = element.classList;
|
classes = element.classList;
|
||||||
}
|
}
|
||||||
} while (element && !(
|
} while (element && !cssClasses.some(c => classes.contains(c)));
|
||||||
classes.contains("mx_RoomTile2") ||
|
|
||||||
classes.contains("mx_RoomSublist2_headerText") ||
|
|
||||||
classes.contains("mx_RoomSearch_input")));
|
|
||||||
|
|
||||||
if (element) {
|
if (element) {
|
||||||
element.focus();
|
element.focus();
|
||||||
|
@ -246,7 +252,12 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
private renderSearchExplore(): React.ReactNode {
|
private renderSearchExplore(): React.ReactNode {
|
||||||
return (
|
return (
|
||||||
<div className="mx_LeftPanel2_filterContainer" onFocus={this.onFocus} onBlur={this.onBlur}>
|
<div
|
||||||
|
className="mx_LeftPanel2_filterContainer"
|
||||||
|
onFocus={this.onFocus}
|
||||||
|
onBlur={this.onBlur}
|
||||||
|
onKeyDown={this.onKeyDown}
|
||||||
|
>
|
||||||
<RoomSearch
|
<RoomSearch
|
||||||
onQueryUpdate={this.onSearch}
|
onQueryUpdate={this.onSearch}
|
||||||
isMinimized={this.props.isMinimized}
|
isMinimized={this.props.isMinimized}
|
||||||
|
@ -256,7 +267,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
||||||
// TODO fix the accessibility of this: https://github.com/vector-im/riot-web/issues/14180
|
// TODO fix the accessibility of this: https://github.com/vector-im/riot-web/issues/14180
|
||||||
className="mx_LeftPanel2_exploreButton"
|
className="mx_LeftPanel2_exploreButton"
|
||||||
onClick={this.onExplore}
|
onClick={this.onExplore}
|
||||||
alt={_t("Explore rooms")}
|
title={_t("Explore rooms")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -149,7 +149,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||||
let clearButton = (
|
let clearButton = (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className='mx_RoomSearch_clearButton'
|
title={_t("Clear filter")}
|
||||||
|
className="mx_RoomSearch_clearButton"
|
||||||
onClick={this.clearInput}
|
onClick={this.clearInput}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -157,8 +158,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||||
if (this.props.isMinimized) {
|
if (this.props.isMinimized) {
|
||||||
icon = (
|
icon = (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
tabIndex={-1}
|
title={_t("Search rooms")}
|
||||||
className='mx_RoomSearch_icon'
|
className="mx_RoomSearch_icon"
|
||||||
onClick={this.openSearch}
|
onClick={this.openSearch}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -329,7 +329,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
className={classes}
|
className={classes}
|
||||||
onClick={this.onOpenMenuClick}
|
onClick={this.onOpenMenuClick}
|
||||||
inputRef={this.buttonRef}
|
inputRef={this.buttonRef}
|
||||||
label={_t("Account settings")}
|
label={_t("User menu")}
|
||||||
isExpanded={!!this.state.contextMenuPosition}
|
isExpanded={!!this.state.contextMenuPosition}
|
||||||
onContextMenu={this.onContextMenu}
|
onContextMenu={this.onContextMenu}
|
||||||
>
|
>
|
||||||
|
|
|
@ -132,7 +132,7 @@ const BaseAvatar = (props) => {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<span className="mx_BaseAvatar" ref={inputRef} {...otherProps}>
|
<span className="mx_BaseAvatar" ref={inputRef} {...otherProps} role="presentation">
|
||||||
{ textNode }
|
{ textNode }
|
||||||
{ imgNode }
|
{ imgNode }
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -274,9 +274,6 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
||||||
className="mx_RoomList2"
|
className="mx_RoomList2"
|
||||||
role="tree"
|
role="tree"
|
||||||
aria-label={_t("Rooms")}
|
aria-label={_t("Rooms")}
|
||||||
// Firefox sometimes makes this element focusable due to
|
|
||||||
// overflow:scroll;, so force it out of tab order.
|
|
||||||
tabIndex={-1}
|
|
||||||
>{sublists}</div>
|
>{sublists}</div>
|
||||||
)}
|
)}
|
||||||
</RovingTabIndexProvider>
|
</RovingTabIndexProvider>
|
||||||
|
|
|
@ -26,8 +26,12 @@ import AccessibleButton from "../../views/elements/AccessibleButton";
|
||||||
import RoomTile2 from "./RoomTile2";
|
import RoomTile2 from "./RoomTile2";
|
||||||
import { ResizableBox, ResizeCallbackData } from "react-resizable";
|
import { ResizableBox, ResizeCallbackData } from "react-resizable";
|
||||||
import { ListLayout } from "../../../stores/room-list/ListLayout";
|
import { ListLayout } from "../../../stores/room-list/ListLayout";
|
||||||
import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
|
import {
|
||||||
import StyledRadioButton from "../elements/StyledRadioButton";
|
ContextMenu,
|
||||||
|
ContextMenuButton,
|
||||||
|
StyledMenuItemCheckbox,
|
||||||
|
StyledMenuItemRadio,
|
||||||
|
} from "../../structures/ContextMenu";
|
||||||
import RoomListStore from "../../../stores/room-list/RoomListStore2";
|
import RoomListStore from "../../../stores/room-list/RoomListStore2";
|
||||||
import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorithms/models";
|
import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorithms/models";
|
||||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||||
|
@ -62,7 +66,7 @@ interface IProps {
|
||||||
onAddRoom?: () => void;
|
onAddRoom?: () => void;
|
||||||
addRoomLabel: string;
|
addRoomLabel: string;
|
||||||
isInvite: boolean;
|
isInvite: boolean;
|
||||||
layout: ListLayout;
|
layout?: ListLayout;
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
tagId: TagID;
|
tagId: TagID;
|
||||||
onResize: () => void;
|
onResize: () => void;
|
||||||
|
@ -136,11 +140,13 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private onShowAllClick = () => {
|
private onShowAllClick = () => {
|
||||||
|
// TODO a11y keep focus somewhere useful: https://github.com/vector-im/riot-web/issues/14180
|
||||||
this.props.layout.visibleTiles = this.props.layout.tilesWithPadding(this.numTiles, MAX_PADDING_HEIGHT);
|
this.props.layout.visibleTiles = this.props.layout.tilesWithPadding(this.numTiles, MAX_PADDING_HEIGHT);
|
||||||
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
||||||
};
|
};
|
||||||
|
|
||||||
private onShowLessClick = () => {
|
private onShowLessClick = () => {
|
||||||
|
// TODO a11y keep focus somewhere useful: https://github.com/vector-im/riot-web/issues/14180
|
||||||
this.props.layout.visibleTiles = this.props.layout.defaultVisibleTiles;
|
this.props.layout.visibleTiles = this.props.layout.defaultVisibleTiles;
|
||||||
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
||||||
};
|
};
|
||||||
|
@ -203,6 +209,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
room_id: room.roomId,
|
room_id: room.roomId,
|
||||||
|
show_room_tile: true, // to make sure the room gets scrolled into view
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -323,22 +330,24 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
<hr />
|
<hr />
|
||||||
<div>
|
<div>
|
||||||
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Unread rooms")}</div>
|
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Unread rooms")}</div>
|
||||||
<StyledCheckbox
|
<StyledMenuItemCheckbox
|
||||||
|
onClose={this.onCloseMenu}
|
||||||
onChange={this.onUnreadFirstChanged}
|
onChange={this.onUnreadFirstChanged}
|
||||||
checked={isUnreadFirst}
|
checked={isUnreadFirst}
|
||||||
>
|
>
|
||||||
{_t("Always show first")}
|
{_t("Always show first")}
|
||||||
</StyledCheckbox>
|
</StyledMenuItemCheckbox>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div>
|
<div>
|
||||||
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Show")}</div>
|
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Show")}</div>
|
||||||
<StyledCheckbox
|
<StyledMenuItemCheckbox
|
||||||
|
onClose={this.onCloseMenu}
|
||||||
onChange={this.onMessagePreviewChanged}
|
onChange={this.onMessagePreviewChanged}
|
||||||
checked={this.props.layout.showPreviews}
|
checked={this.props.layout.showPreviews}
|
||||||
>
|
>
|
||||||
{_t("Message preview")}
|
{_t("Message preview")}
|
||||||
</StyledCheckbox>
|
</StyledMenuItemCheckbox>
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
@ -354,20 +363,22 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
<div className="mx_RoomSublist2_contextMenu">
|
<div className="mx_RoomSublist2_contextMenu">
|
||||||
<div>
|
<div>
|
||||||
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Sort by")}</div>
|
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Sort by")}</div>
|
||||||
<StyledRadioButton
|
<StyledMenuItemRadio
|
||||||
|
onClose={this.onCloseMenu}
|
||||||
onChange={() => this.onTagSortChanged(SortAlgorithm.Recent)}
|
onChange={() => this.onTagSortChanged(SortAlgorithm.Recent)}
|
||||||
checked={!isAlphabetical}
|
checked={!isAlphabetical}
|
||||||
name={`mx_${this.props.tagId}_sortBy`}
|
name={`mx_${this.props.tagId}_sortBy`}
|
||||||
>
|
>
|
||||||
{_t("Activity")}
|
{_t("Activity")}
|
||||||
</StyledRadioButton>
|
</StyledMenuItemRadio>
|
||||||
<StyledRadioButton
|
<StyledMenuItemRadio
|
||||||
|
onClose={this.onCloseMenu}
|
||||||
onChange={() => this.onTagSortChanged(SortAlgorithm.Alphabetic)}
|
onChange={() => this.onTagSortChanged(SortAlgorithm.Alphabetic)}
|
||||||
checked={isAlphabetical}
|
checked={isAlphabetical}
|
||||||
name={`mx_${this.props.tagId}_sortBy`}
|
name={`mx_${this.props.tagId}_sortBy`}
|
||||||
>
|
>
|
||||||
{_t("A-Z")}
|
{_t("A-Z")}
|
||||||
</StyledRadioButton>
|
</StyledMenuItemRadio>
|
||||||
</div>
|
</div>
|
||||||
{otherSections}
|
{otherSections}
|
||||||
</div>
|
</div>
|
||||||
|
@ -390,16 +401,22 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
private renderHeader(): React.ReactElement {
|
private renderHeader(): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<RovingTabIndexWrapper>
|
<RovingTabIndexWrapper inputRef={this.headerButton}>
|
||||||
{({onFocus, isActive, ref}) => {
|
{({onFocus, isActive, ref}) => {
|
||||||
const tabIndex = isActive ? 0 : -1;
|
const tabIndex = isActive ? 0 : -1;
|
||||||
|
|
||||||
|
let ariaLabel = _t("Jump to first unread room.");
|
||||||
|
if (this.props.tagId === DefaultTagID.Invite) {
|
||||||
|
ariaLabel = _t("Jump to first invite.");
|
||||||
|
}
|
||||||
|
|
||||||
const badge = (
|
const badge = (
|
||||||
<NotificationBadge
|
<NotificationBadge
|
||||||
forceCount={true}
|
forceCount={true}
|
||||||
notification={this.state.notificationState}
|
notification={this.state.notificationState}
|
||||||
onClick={this.onBadgeClick}
|
onClick={this.onBadgeClick}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
|
aria-label={ariaLabel}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -440,7 +457,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
// doesn't become sticky.
|
// doesn't become sticky.
|
||||||
// The same applies to the notification badge.
|
// The same applies to the notification badge.
|
||||||
return (
|
return (
|
||||||
<div className={classes} onKeyDown={this.onHeaderKeyDown} onFocus={onFocus}>
|
<div className={classes} onKeyDown={this.onHeaderKeyDown} onFocus={onFocus} aria-label={this.props.label}>
|
||||||
<div className="mx_RoomSublist2_stickable">
|
<div className="mx_RoomSublist2_stickable">
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
|
@ -448,6 +465,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
className="mx_RoomSublist2_headerText"
|
className="mx_RoomSublist2_headerText"
|
||||||
role="treeitem"
|
role="treeitem"
|
||||||
|
aria-expanded={!this.props.layout || !this.props.layout.isCollapsed}
|
||||||
aria-level={1}
|
aria-level={1}
|
||||||
onClick={this.onHeaderClick}
|
onClick={this.onHeaderClick}
|
||||||
onContextMenu={this.onContextMenu}
|
onContextMenu={this.onContextMenu}
|
||||||
|
@ -503,12 +521,12 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
if (this.props.isMinimized) showMoreText = null;
|
if (this.props.isMinimized) showMoreText = null;
|
||||||
showNButton = (
|
showNButton = (
|
||||||
<div onClick={this.onShowAllClick} className={showMoreBtnClasses}>
|
<AccessibleButton onClick={this.onShowAllClick} className={showMoreBtnClasses} tabIndex={-1}>
|
||||||
<span className='mx_RoomSublist2_showMoreButtonChevron mx_RoomSublist2_showNButtonChevron'>
|
<span className='mx_RoomSublist2_showMoreButtonChevron mx_RoomSublist2_showNButtonChevron'>
|
||||||
{/* set by CSS masking */}
|
{/* set by CSS masking */}
|
||||||
</span>
|
</span>
|
||||||
{showMoreText}
|
{showMoreText}
|
||||||
</div>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
} else if (this.numTiles <= visibleTiles.length && this.numTiles > this.props.layout.defaultVisibleTiles) {
|
} else if (this.numTiles <= visibleTiles.length && this.numTiles > this.props.layout.defaultVisibleTiles) {
|
||||||
// we have all tiles visible - add a button to show less
|
// we have all tiles visible - add a button to show less
|
||||||
|
@ -518,13 +536,14 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
if (this.props.isMinimized) showLessText = null;
|
if (this.props.isMinimized) showLessText = null;
|
||||||
|
// TODO Roving tab index / treeitem?: https://github.com/vector-im/riot-web/issues/14180
|
||||||
showNButton = (
|
showNButton = (
|
||||||
<div onClick={this.onShowLessClick} className={showMoreBtnClasses}>
|
<AccessibleButton onClick={this.onShowLessClick} className={showMoreBtnClasses} tabIndex={-1}>
|
||||||
<span className='mx_RoomSublist2_showLessButtonChevron mx_RoomSublist2_showNButtonChevron'>
|
<span className='mx_RoomSublist2_showLessButtonChevron mx_RoomSublist2_showNButtonChevron'>
|
||||||
{/* set by CSS masking */}
|
{/* set by CSS masking */}
|
||||||
</span>
|
</span>
|
||||||
{showLessText}
|
{showLessText}
|
||||||
</div>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,17 +26,30 @@ import dis from '../../../dispatcher/dispatcher';
|
||||||
import { Key } from "../../../Keyboard";
|
import { Key } from "../../../Keyboard";
|
||||||
import ActiveRoomObserver from "../../../ActiveRoomObserver";
|
import ActiveRoomObserver from "../../../ActiveRoomObserver";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { ContextMenu, ContextMenuButton, MenuItemRadio } from "../../structures/ContextMenu";
|
import {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuButton,
|
||||||
|
MenuItemRadio,
|
||||||
|
MenuItemCheckbox,
|
||||||
|
MenuItem,
|
||||||
|
} from "../../structures/ContextMenu";
|
||||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||||
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
|
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
|
||||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||||
import { getRoomNotifsState, ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE } from "../../../RoomNotifs";
|
import {
|
||||||
|
getRoomNotifsState,
|
||||||
|
setRoomNotifsState,
|
||||||
|
ALL_MESSAGES,
|
||||||
|
ALL_MESSAGES_LOUD,
|
||||||
|
MENTIONS_ONLY,
|
||||||
|
MUTE,
|
||||||
|
} from "../../../RoomNotifs";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import { setRoomNotifsState } from "../../../RoomNotifs";
|
|
||||||
import { TagSpecificNotificationState } from "../../../stores/notifications/TagSpecificNotificationState";
|
import { TagSpecificNotificationState } from "../../../stores/notifications/TagSpecificNotificationState";
|
||||||
import { INotificationState } from "../../../stores/notifications/INotificationState";
|
import { INotificationState } from "../../../stores/notifications/INotificationState";
|
||||||
import NotificationBadge from "./NotificationBadge";
|
import NotificationBadge from "./NotificationBadge";
|
||||||
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
||||||
|
import { Volume } from "../../../RoomNotifsTypes";
|
||||||
|
|
||||||
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
|
// 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
|
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
|
@ -68,6 +81,8 @@ interface IState {
|
||||||
generalMenuPosition: PartialDOMRect;
|
generalMenuPosition: PartialDOMRect;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const messagePreviewId = (roomId: string) => `mx_RoomTile2_messagePreview_${roomId}`;
|
||||||
|
|
||||||
const contextMenuBelow = (elementRect: PartialDOMRect) => {
|
const contextMenuBelow = (elementRect: PartialDOMRect) => {
|
||||||
// align the context menu's icons with the icon which opened the context menu
|
// align the context menu's icons with the icon which opened the context menu
|
||||||
const left = elementRect.left + window.pageXOffset - 9;
|
const left = elementRect.left + window.pageXOffset - 9;
|
||||||
|
@ -123,6 +138,10 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
return !this.props.isMinimized && this.props.tag !== DefaultTagID.Invite;
|
return !this.props.isMinimized && this.props.tag !== DefaultTagID.Invite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get showMessagePreview(): boolean {
|
||||||
|
return !this.props.isMinimized && this.props.showMessagePreview;
|
||||||
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
if (this.props.room) {
|
if (this.props.room) {
|
||||||
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
||||||
|
@ -195,6 +214,11 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
// TODO: Support tagging: https://github.com/vector-im/riot-web/issues/14211
|
// TODO: Support tagging: https://github.com/vector-im/riot-web/issues/14211
|
||||||
// TODO: XOR favourites and low priority: https://github.com/vector-im/riot-web/issues/14210
|
// TODO: XOR favourites and low priority: https://github.com/vector-im/riot-web/issues/14210
|
||||||
|
|
||||||
|
if ((ev as React.KeyboardEvent).key === Key.ENTER) {
|
||||||
|
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
|
||||||
|
this.setState({generalMenuPosition: null}); // hide the menu
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onLeaveRoomClick = (ev: ButtonEvent) => {
|
private onLeaveRoomClick = (ev: ButtonEvent) => {
|
||||||
|
@ -219,11 +243,13 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
this.setState({generalMenuPosition: null}); // hide the menu
|
this.setState({generalMenuPosition: null}); // hide the menu
|
||||||
};
|
};
|
||||||
|
|
||||||
private async saveNotifState(ev: ButtonEvent, newState: ALL_MESSAGES_LOUD | ALL_MESSAGES | MENTIONS_ONLY | MUTE) {
|
private async saveNotifState(ev: ButtonEvent, newState: Volume) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
if (MatrixClientPeg.get().isGuest()) return;
|
if (MatrixClientPeg.get().isGuest()) return;
|
||||||
|
|
||||||
|
// get key before we go async and React discards the nativeEvent
|
||||||
|
const key = (ev as React.KeyboardEvent).key;
|
||||||
try {
|
try {
|
||||||
// TODO add local echo - https://github.com/vector-im/riot-web/issues/14280
|
// TODO add local echo - https://github.com/vector-im/riot-web/issues/14280
|
||||||
await setRoomNotifsState(this.props.room.roomId, newState);
|
await setRoomNotifsState(this.props.room.roomId, newState);
|
||||||
|
@ -233,7 +259,10 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({notificationsMenuPosition: null}); // Close the context menu
|
if (key === Key.ENTER) {
|
||||||
|
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
|
||||||
|
this.setState({notificationsMenuPosition: null}); // hide the menu
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onClickAllNotifs = ev => this.saveNotifState(ev, ALL_MESSAGES);
|
private onClickAllNotifs = ev => this.saveNotifState(ev, ALL_MESSAGES);
|
||||||
|
@ -322,20 +351,24 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
<ContextMenu {...contextMenuBelow(this.state.generalMenuPosition)} onFinished={this.onCloseGeneralMenu}>
|
<ContextMenu {...contextMenuBelow(this.state.generalMenuPosition)} onFinished={this.onCloseGeneralMenu}>
|
||||||
<div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile2_contextMenu">
|
<div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile2_contextMenu">
|
||||||
<div className="mx_IconizedContextMenu_optionList">
|
<div className="mx_IconizedContextMenu_optionList">
|
||||||
<AccessibleButton onClick={(e) => this.onTagRoom(e, DefaultTagID.Favourite)}>
|
<MenuItemCheckbox
|
||||||
|
onClick={(e) => this.onTagRoom(e, DefaultTagID.Favourite)}
|
||||||
|
active={false} // TODO: https://github.com/vector-im/riot-web/issues/14283
|
||||||
|
label={_t("Favourite")}
|
||||||
|
>
|
||||||
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconStar" />
|
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconStar" />
|
||||||
<span className="mx_IconizedContextMenu_label">{_t("Favourite")}</span>
|
<span className="mx_IconizedContextMenu_label">{_t("Favourite")}</span>
|
||||||
</AccessibleButton>
|
</MenuItemCheckbox>
|
||||||
<AccessibleButton onClick={this.onOpenRoomSettings}>
|
<MenuItem onClick={this.onOpenRoomSettings} label={_t("Settings")}>
|
||||||
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSettings" />
|
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSettings" />
|
||||||
<span className="mx_IconizedContextMenu_label">{_t("Settings")}</span>
|
<span className="mx_IconizedContextMenu_label">{_t("Settings")}</span>
|
||||||
</AccessibleButton>
|
</MenuItem>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_IconizedContextMenu_optionList mx_RoomTile2_contextMenu_redRow">
|
<div className="mx_IconizedContextMenu_optionList mx_RoomTile2_contextMenu_redRow">
|
||||||
<AccessibleButton onClick={this.onLeaveRoomClick}>
|
<MenuItem onClick={this.onLeaveRoomClick} label={_t("Leave Room")}>
|
||||||
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSignOut" />
|
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSignOut" />
|
||||||
<span className="mx_IconizedContextMenu_label">{_t("Leave Room")}</span>
|
<span className="mx_IconizedContextMenu_label">{_t("Leave Room")}</span>
|
||||||
</AccessibleButton>
|
</MenuItem>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
|
@ -375,8 +408,9 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
let badge: React.ReactNode;
|
let badge: React.ReactNode;
|
||||||
if (!this.props.isMinimized) {
|
if (!this.props.isMinimized) {
|
||||||
|
// aria-hidden because we summarise the unread count/highlight status in a manual aria-label below
|
||||||
badge = (
|
badge = (
|
||||||
<div className="mx_RoomTile2_badgeContainer">
|
<div className="mx_RoomTile2_badgeContainer" aria-hidden="true">
|
||||||
<NotificationBadge
|
<NotificationBadge
|
||||||
notification={this.state.notificationState}
|
notification={this.state.notificationState}
|
||||||
forceCount={false}
|
forceCount={false}
|
||||||
|
@ -392,24 +426,25 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
|
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
|
||||||
|
|
||||||
let messagePreview = null;
|
let messagePreview = null;
|
||||||
if (this.props.showMessagePreview && !this.props.isMinimized) {
|
if (this.showMessagePreview) {
|
||||||
// The preview store heavily caches this info, so should be safe to hammer.
|
// The preview store heavily caches this info, so should be safe to hammer.
|
||||||
const text = MessagePreviewStore.instance.getPreviewForRoom(this.props.room, this.props.tag);
|
const text = MessagePreviewStore.instance.getPreviewForRoom(this.props.room, this.props.tag);
|
||||||
|
|
||||||
// Only show the preview if there is one to show.
|
// Only show the preview if there is one to show.
|
||||||
if (text) {
|
if (text) {
|
||||||
messagePreview = (
|
messagePreview = (
|
||||||
<div className="mx_RoomTile2_messagePreview">
|
<div className="mx_RoomTile2_messagePreview" id={messagePreviewId(this.props.room.roomId)}>
|
||||||
{text}
|
{text}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const notificationColor = this.state.notificationState.color;
|
||||||
const nameClasses = classNames({
|
const nameClasses = classNames({
|
||||||
"mx_RoomTile2_name": true,
|
"mx_RoomTile2_name": true,
|
||||||
"mx_RoomTile2_nameWithPreview": !!messagePreview,
|
"mx_RoomTile2_nameWithPreview": !!messagePreview,
|
||||||
"mx_RoomTile2_nameHasUnreadEvents": this.state.notificationState.color >= NotificationColor.Bold,
|
"mx_RoomTile2_nameHasUnreadEvents": notificationColor >= NotificationColor.Bold,
|
||||||
});
|
});
|
||||||
|
|
||||||
let nameContainer = (
|
let nameContainer = (
|
||||||
|
@ -422,6 +457,27 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
if (this.props.isMinimized) nameContainer = null;
|
if (this.props.isMinimized) nameContainer = null;
|
||||||
|
|
||||||
|
let ariaLabel = name;
|
||||||
|
// The following labels are written in such a fashion to increase screen reader efficiency (speed).
|
||||||
|
if (this.props.tag === DefaultTagID.Invite) {
|
||||||
|
// append nothing
|
||||||
|
} else if (notificationColor >= NotificationColor.Red) {
|
||||||
|
ariaLabel += " " + _t("%(count)s unread messages including mentions.", {
|
||||||
|
count: this.state.notificationState.count,
|
||||||
|
});
|
||||||
|
} else if (notificationColor >= NotificationColor.Grey) {
|
||||||
|
ariaLabel += " " + _t("%(count)s unread messages.", {
|
||||||
|
count: this.state.notificationState.count,
|
||||||
|
});
|
||||||
|
} else if (notificationColor >= NotificationColor.Bold) {
|
||||||
|
ariaLabel += " " + _t("Unread messages.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let ariaDescribedBy: string;
|
||||||
|
if (this.showMessagePreview) {
|
||||||
|
ariaDescribedBy = messagePreviewId(this.props.room.roomId);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<RovingTabIndexWrapper>
|
<RovingTabIndexWrapper>
|
||||||
|
@ -434,8 +490,11 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
onMouseEnter={this.onTileMouseEnter}
|
onMouseEnter={this.onTileMouseEnter}
|
||||||
onMouseLeave={this.onTileMouseLeave}
|
onMouseLeave={this.onTileMouseLeave}
|
||||||
onClick={this.onTileClick}
|
onClick={this.onTileClick}
|
||||||
role="treeitem"
|
|
||||||
onContextMenu={this.onContextMenu}
|
onContextMenu={this.onContextMenu}
|
||||||
|
role="treeitem"
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
aria-selected={this.state.selected}
|
||||||
|
aria-describedby={ariaDescribedBy}
|
||||||
>
|
>
|
||||||
{roomAvatar}
|
{roomAvatar}
|
||||||
{nameContainer}
|
{nameContainer}
|
||||||
|
|
|
@ -1208,6 +1208,8 @@
|
||||||
"Activity": "Activity",
|
"Activity": "Activity",
|
||||||
"A-Z": "A-Z",
|
"A-Z": "A-Z",
|
||||||
"List options": "List options",
|
"List options": "List options",
|
||||||
|
"Jump to first unread room.": "Jump to first unread room.",
|
||||||
|
"Jump to first invite.": "Jump to first invite.",
|
||||||
"Add room": "Add room",
|
"Add room": "Add room",
|
||||||
"Show %(count)s more|other": "Show %(count)s more",
|
"Show %(count)s more|other": "Show %(count)s more",
|
||||||
"Show %(count)s more|one": "Show %(count)s more",
|
"Show %(count)s more|one": "Show %(count)s more",
|
||||||
|
@ -2089,6 +2091,8 @@
|
||||||
"Find a room…": "Find a room…",
|
"Find a room…": "Find a room…",
|
||||||
"Find a room… (e.g. %(exampleRoom)s)": "Find a room… (e.g. %(exampleRoom)s)",
|
"Find a room… (e.g. %(exampleRoom)s)": "Find a room… (e.g. %(exampleRoom)s)",
|
||||||
"If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.",
|
"If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.",
|
||||||
|
"Clear filter": "Clear filter",
|
||||||
|
"Search rooms": "Search rooms",
|
||||||
"You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.": "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.",
|
"You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.": "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.",
|
||||||
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.",
|
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.",
|
||||||
"Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.",
|
"Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.",
|
||||||
|
@ -2100,8 +2104,6 @@
|
||||||
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
|
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
|
||||||
"Active call": "Active call",
|
"Active call": "Active call",
|
||||||
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?",
|
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?",
|
||||||
"Jump to first unread room.": "Jump to first unread room.",
|
|
||||||
"Jump to first invite.": "Jump to first invite.",
|
|
||||||
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
|
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
|
||||||
"You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",
|
"You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",
|
||||||
"Search failed": "Search failed",
|
"Search failed": "Search failed",
|
||||||
|
@ -2116,7 +2118,6 @@
|
||||||
"Click to mute video": "Click to mute video",
|
"Click to mute video": "Click to mute video",
|
||||||
"Click to unmute audio": "Click to unmute audio",
|
"Click to unmute audio": "Click to unmute audio",
|
||||||
"Click to mute audio": "Click to mute audio",
|
"Click to mute audio": "Click to mute audio",
|
||||||
"Clear filter": "Clear filter",
|
|
||||||
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.",
|
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.",
|
||||||
"Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.",
|
"Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.",
|
||||||
"Failed to load timeline position": "Failed to load timeline position",
|
"Failed to load timeline position": "Failed to load timeline position",
|
||||||
|
@ -2131,7 +2132,7 @@
|
||||||
"All settings": "All settings",
|
"All settings": "All settings",
|
||||||
"Archived rooms": "Archived rooms",
|
"Archived rooms": "Archived rooms",
|
||||||
"Feedback": "Feedback",
|
"Feedback": "Feedback",
|
||||||
"Account settings": "Account settings",
|
"User menu": "User menu",
|
||||||
"Could not load user profile": "Could not load user profile",
|
"Could not load user profile": "Could not load user profile",
|
||||||
"Verify this login": "Verify this login",
|
"Verify this login": "Verify this login",
|
||||||
"Session verified": "Session verified",
|
"Session verified": "Session verified",
|
||||||
|
|
Loading…
Reference in New Issue