Fix handles
This commit is contained in:
commit
173ccbcec9
70 changed files with 713 additions and 180 deletions
|
@ -233,6 +233,9 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
|||
switch (ev.key) {
|
||||
case Key.TAB:
|
||||
case Key.ESCAPE:
|
||||
// close on left and right arrows too for when it is a context menu on a <Toolbar />
|
||||
case Key.ARROW_LEFT:
|
||||
case Key.ARROW_RIGHT:
|
||||
this.props.onFinished();
|
||||
break;
|
||||
case Key.ARROW_UP:
|
||||
|
|
|
@ -38,7 +38,7 @@ const HomePage = () => {
|
|||
}
|
||||
|
||||
const brandingConfig = config.branding;
|
||||
let logoUrl = "themes/riot/img/logos/riot-logo.svg";
|
||||
let logoUrl = "themes/element/img/logos/element-logo.svg";
|
||||
if (brandingConfig && brandingConfig.authHeaderLogoUrl) {
|
||||
logoUrl = brandingConfig.authHeaderLogoUrl;
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ const HomePage = () => {
|
|||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
return <AutoHideScrollbar className="mx_HomePage mx_HomePage_default">
|
||||
<div className="mx_HomePage_default_wrapper">
|
||||
<img src={logoUrl} alt="Riot" />
|
||||
<img src={logoUrl} alt={config.brand || "Riot"} />
|
||||
<h1>{ _t("Welcome to %(appName)s", { appName: config.brand || "Riot" }) }</h1>
|
||||
<h4>{ _t("Liberate your communication") }</h4>
|
||||
<div className="mx_HomePage_default_buttons">
|
||||
|
|
|
@ -269,7 +269,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
|||
const firstRoom = this.listContainerRef.current.querySelector<HTMLDivElement>(".mx_RoomTile2");
|
||||
if (firstRoom) {
|
||||
firstRoom.click();
|
||||
this.onSearch(""); // clear the search field
|
||||
return true; // to get the field to clear
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -675,12 +675,16 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
case 'hide_left_panel':
|
||||
this.setState({
|
||||
collapseLhs: true,
|
||||
}, () => {
|
||||
this.state.resizeNotifier.notifyLeftHandleResized();
|
||||
});
|
||||
break;
|
||||
case 'focus_room_filter': // for CtrlOrCmd+K to work by expanding the left panel first
|
||||
case 'show_left_panel':
|
||||
this.setState({
|
||||
collapseLhs: false,
|
||||
}, () => {
|
||||
this.state.resizeNotifier.notifyLeftHandleResized();
|
||||
});
|
||||
break;
|
||||
case 'panel_disable': {
|
||||
|
|
|
@ -28,8 +28,8 @@ import { Action } from "../../dispatcher/actions";
|
|||
interface IProps {
|
||||
onQueryUpdate: (newQuery: string) => void;
|
||||
isMinimized: boolean;
|
||||
onVerticalArrow(ev: React.KeyboardEvent);
|
||||
onEnter(ev: React.KeyboardEvent);
|
||||
onVerticalArrow(ev: React.KeyboardEvent): void;
|
||||
onEnter(ev: React.KeyboardEvent): boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -107,7 +107,13 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
} else if (ev.key === Key.ARROW_UP || ev.key === Key.ARROW_DOWN) {
|
||||
this.props.onVerticalArrow(ev);
|
||||
} else if (ev.key === Key.ENTER) {
|
||||
this.props.onEnter(ev);
|
||||
const shouldClear = this.props.onEnter(ev);
|
||||
if (shouldClear) {
|
||||
// wrap in set immediate to delay it so that we don't clear the filter & then change room
|
||||
setImmediate(() => {
|
||||
this.clearInput();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -66,7 +66,7 @@ export default class DecoratedRoomAvatar extends React.PureComponent<IProps, ISt
|
|||
oobData={this.props.oobData}
|
||||
viewAvatarOnClick={this.props.viewAvatarOnClick}
|
||||
/>
|
||||
<RoomTileIcon room={this.props.room} tag={this.props.tag} />
|
||||
<RoomTileIcon room={this.props.room} />
|
||||
{badge}
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@ import PropTypes from 'prop-types';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import TagOrderActions from '../../../actions/TagOrderActions';
|
||||
import * as sdk from '../../../index';
|
||||
import {MenuItem} from "../../structures/ContextMenu";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
|
|
|
@ -23,26 +23,26 @@ import DialogButtons from '../elements/DialogButtons';
|
|||
export enum RebrandDialogKind {
|
||||
NAG,
|
||||
ONE_TIME,
|
||||
};
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
onFinished: (bool) => void;
|
||||
kind: RebrandDialogKind,
|
||||
targetUrl?: string,
|
||||
kind: RebrandDialogKind;
|
||||
targetUrl?: string;
|
||||
}
|
||||
|
||||
export default class RebrandDialog extends React.PureComponent<IProps> {
|
||||
private onDoneClick = () => {
|
||||
this.props.onFinished(true);
|
||||
}
|
||||
};
|
||||
|
||||
private onGoToElementClick = () => {
|
||||
this.props.onFinished(true);
|
||||
}
|
||||
};
|
||||
|
||||
private onRemindMeLaterClick = () => {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
};
|
||||
|
||||
private getPrettyTargetUrl() {
|
||||
const u = new URL(this.props.targetUrl);
|
||||
|
@ -79,14 +79,14 @@ export default class RebrandDialog extends React.PureComponent<IProps> {
|
|||
cancelButton={"Remind me later"}
|
||||
onCancel={this.onRemindMeLaterClick}
|
||||
focus={true}
|
||||
/>
|
||||
/>;
|
||||
} else {
|
||||
return <DialogButtons primaryButton={_t("Done")}
|
||||
primaryButtonClass='primary'
|
||||
hasCancel={false}
|
||||
onPrimaryButtonClick={this.onDoneClick}
|
||||
focus={true}
|
||||
/>
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import {IProps} from "./AccessibleButton";
|
||||
|
@ -24,6 +24,7 @@ import Tooltip from './Tooltip';
|
|||
|
||||
interface ITooltipProps extends IProps {
|
||||
title: string;
|
||||
tooltip?: React.ReactNode;
|
||||
tooltipClassName?: string;
|
||||
}
|
||||
|
||||
|
@ -52,16 +53,12 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
|
|||
};
|
||||
|
||||
render() {
|
||||
const {title, children, ...props} = this.props;
|
||||
const tooltipClassName = classnames(
|
||||
"mx_AccessibleTooltipButton_tooltip",
|
||||
this.props.tooltipClassName,
|
||||
);
|
||||
const {title, tooltip, children, tooltipClassName, ...props} = this.props;
|
||||
|
||||
const tip = this.state.hover ? <Tooltip
|
||||
className="mx_AccessibleTooltipButton_container"
|
||||
tooltipClassName={tooltipClassName}
|
||||
label={title}
|
||||
tooltipClassName={classNames("mx_AccessibleTooltipButton_tooltip", tooltipClassName)}
|
||||
label={tooltip || title}
|
||||
/> : <div />;
|
||||
return (
|
||||
<AccessibleButton {...props} onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} aria-label={title}>
|
||||
|
|
|
@ -25,9 +25,12 @@ import dis from '../../../dispatcher/dispatcher';
|
|||
import {aboveLeftOf, ContextMenu, ContextMenuButton, useContextMenu} from '../../structures/ContextMenu';
|
||||
import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import Toolbar from "../../../accessibility/Toolbar";
|
||||
import {RovingAccessibleButton, useRovingTabIndex} from "../../../accessibility/RovingTabIndex";
|
||||
|
||||
const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => {
|
||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
||||
const [onFocus, isActive, ref] = useRovingTabIndex(button);
|
||||
useEffect(() => {
|
||||
onFocusChange(menuDisplayed);
|
||||
}, [onFocusChange, menuDisplayed]);
|
||||
|
@ -57,7 +60,9 @@ const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFo
|
|||
label={_t("Options")}
|
||||
onClick={openMenu}
|
||||
isExpanded={menuDisplayed}
|
||||
inputRef={button}
|
||||
inputRef={ref}
|
||||
onFocus={onFocus}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
/>
|
||||
|
||||
{ contextMenu }
|
||||
|
@ -66,6 +71,7 @@ const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFo
|
|||
|
||||
const ReactButton = ({mxEvent, reactions, onFocusChange}) => {
|
||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
||||
const [onFocus, isActive, ref] = useRovingTabIndex(button);
|
||||
useEffect(() => {
|
||||
onFocusChange(menuDisplayed);
|
||||
}, [onFocusChange, menuDisplayed]);
|
||||
|
@ -85,7 +91,9 @@ const ReactButton = ({mxEvent, reactions, onFocusChange}) => {
|
|||
label={_t("React")}
|
||||
onClick={openMenu}
|
||||
isExpanded={menuDisplayed}
|
||||
inputRef={button}
|
||||
inputRef={ref}
|
||||
onFocus={onFocus}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
/>
|
||||
|
||||
{ contextMenu }
|
||||
|
@ -148,8 +156,6 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
};
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let reactButton;
|
||||
let replyButton;
|
||||
let editButton;
|
||||
|
@ -161,7 +167,7 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
if (this.context.canReply) {
|
||||
replyButton = <AccessibleButton
|
||||
replyButton = <RovingAccessibleButton
|
||||
className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"
|
||||
title={_t("Reply")}
|
||||
onClick={this.onReplyClick}
|
||||
|
@ -169,7 +175,7 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
if (canEditContent(this.props.mxEvent)) {
|
||||
editButton = <AccessibleButton
|
||||
editButton = <RovingAccessibleButton
|
||||
className="mx_MessageActionBar_maskButton mx_MessageActionBar_editButton"
|
||||
title={_t("Edit")}
|
||||
onClick={this.onEditClick}
|
||||
|
@ -177,7 +183,7 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
}
|
||||
|
||||
// aria-live=off to not have this read out automatically as navigating around timeline, gets repetitive.
|
||||
return <div className="mx_MessageActionBar" role="toolbar" aria-label={_t("Message Actions")} aria-live="off">
|
||||
return <Toolbar className="mx_MessageActionBar" aria-label={_t("Message Actions")} aria-live="off">
|
||||
{reactButton}
|
||||
{replyButton}
|
||||
{editButton}
|
||||
|
@ -188,6 +194,6 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
permalinkCreator={this.props.permalinkCreator}
|
||||
onFocusChange={this.onFocusChange}
|
||||
/>
|
||||
</div>;
|
||||
</Toolbar>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent {
|
|||
},
|
||||
{
|
||||
reactors: () => {
|
||||
return <div className="mx_ReactionsRowButtonTooltip_senders">
|
||||
return <div className="mx_Tooltip_title">
|
||||
{formatCommaSeparatedList(senders, 6)}
|
||||
</div>;
|
||||
},
|
||||
|
@ -63,7 +63,7 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent {
|
|||
if (!shortName) {
|
||||
return null;
|
||||
}
|
||||
return <div className="mx_ReactionsRowButtonTooltip_reactedWith">
|
||||
return <div className="mx_Tooltip_sub">
|
||||
{sub}
|
||||
</div>;
|
||||
},
|
||||
|
|
|
@ -376,11 +376,21 @@ export default createReactClass({
|
|||
const date = this.props.mxEvent.replacingEventDate();
|
||||
const dateString = date && formatDate(date);
|
||||
|
||||
const tooltip = <div>
|
||||
<div className="mx_Tooltip_title">
|
||||
{_t("Edited at %(date)s", {date: dateString})}
|
||||
</div>
|
||||
<div className="mx_Tooltip_sub">
|
||||
{_t("Click to view edits")}
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
return (
|
||||
<AccessibleTooltipButton
|
||||
className="mx_EventTile_edited"
|
||||
onClick={this._openHistoryDialog}
|
||||
title={_t("Edited at %(date)s. Click to view edits.", {date: dateString})}
|
||||
tooltip={tooltip}
|
||||
tooltipClassName="mx_Tooltip_timeline"
|
||||
>
|
||||
<span>{`(${_t("edited")})`}</span>
|
||||
|
|
|
@ -333,7 +333,7 @@ export default createReactClass({
|
|||
return;
|
||||
}
|
||||
|
||||
const eventSenderTrust = this.context.checkDeviceTrust(
|
||||
const eventSenderTrust = encryptionInfo.sender && this.context.checkDeviceTrust(
|
||||
senderId, encryptionInfo.sender.deviceId,
|
||||
);
|
||||
if (!eventSenderTrust) {
|
||||
|
|
|
@ -25,7 +25,8 @@ import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
|||
import { CSSTransition } from "react-transition-group";
|
||||
import RoomListStore from "../../../stores/room-list/RoomListStore2";
|
||||
import { DefaultTagID } from "../../../stores/room-list/models";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex";
|
||||
import Toolbar from "../../../accessibility/Toolbar";
|
||||
|
||||
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
|
||||
|
||||
|
@ -86,7 +87,7 @@ export default class RoomBreadcrumbs2 extends React.PureComponent<IProps, IState
|
|||
const roomTags = RoomListStore.instance.getTagsForRoom(r);
|
||||
const roomTag = roomTags.includes(DefaultTagID.DM) ? DefaultTagID.DM : roomTags[0];
|
||||
return (
|
||||
<AccessibleTooltipButton
|
||||
<RovingAccessibleTooltipButton
|
||||
className="mx_RoomBreadcrumbs2_crumb"
|
||||
key={r.roomId}
|
||||
onClick={() => this.viewRoom(r, i)}
|
||||
|
@ -101,7 +102,7 @@ export default class RoomBreadcrumbs2 extends React.PureComponent<IProps, IState
|
|||
displayBadge={true}
|
||||
forceCount={true}
|
||||
/>
|
||||
</AccessibleTooltipButton>
|
||||
</RovingAccessibleTooltipButton>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -112,9 +113,9 @@ export default class RoomBreadcrumbs2 extends React.PureComponent<IProps, IState
|
|||
appear={true} in={this.state.doAnimation} timeout={640}
|
||||
classNames='mx_RoomBreadcrumbs2'
|
||||
>
|
||||
<div className='mx_RoomBreadcrumbs2'>
|
||||
<Toolbar className='mx_RoomBreadcrumbs2'>
|
||||
{tiles.slice(this.state.skipFirst ? 1 : 0)}
|
||||
</div>
|
||||
</Toolbar>
|
||||
</CSSTransition>
|
||||
);
|
||||
} else {
|
||||
|
|
|
@ -31,7 +31,6 @@ import ManageIntegsButton from '../elements/ManageIntegsButton';
|
|||
import {CancelButton} from './SimpleRoomHeader';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import E2EIcon from './E2EIcon';
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import {DefaultTagID} from "../../../stores/room-list/models";
|
||||
|
@ -158,10 +157,6 @@ export default createReactClass({
|
|||
let settingsButton = null;
|
||||
let pinnedEventsButton = null;
|
||||
|
||||
const dmUserId = DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId);
|
||||
const joinRules = this.props.room && this.props.room.currentState.getStateEvents("m.room.join_rules", "");
|
||||
const joinRule = joinRules && joinRules.getContent().join_rule;
|
||||
|
||||
if (this.props.onCancelClick) {
|
||||
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
|
||||
}
|
||||
|
|
|
@ -444,24 +444,20 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
<React.Fragment>
|
||||
<hr />
|
||||
<div>
|
||||
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Unread rooms")}</div>
|
||||
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Appearance")}</div>
|
||||
<StyledMenuItemCheckbox
|
||||
onClose={this.onCloseMenu}
|
||||
onChange={this.onUnreadFirstChanged}
|
||||
checked={isUnreadFirst}
|
||||
>
|
||||
{_t("Always show first")}
|
||||
{_t("Show rooms with unread messages first")}
|
||||
</StyledMenuItemCheckbox>
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Show")}</div>
|
||||
<StyledMenuItemCheckbox
|
||||
onClose={this.onCloseMenu}
|
||||
onChange={this.onMessagePreviewChanged}
|
||||
checked={this.layout.showPreviews}
|
||||
>
|
||||
{_t("Message preview")}
|
||||
{_t("Show previews of messages")}
|
||||
</StyledMenuItemCheckbox>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
|
|
|
@ -22,6 +22,8 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { isPresenceEnabled } from "../../../utils/presence";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import TextWithTooltip from "../elements/TextWithTooltip";
|
||||
|
||||
enum Icon {
|
||||
// Note: the names here are used in CSS class names
|
||||
|
@ -32,9 +34,21 @@ enum Icon {
|
|||
PresenceOffline = "OFFLINE",
|
||||
}
|
||||
|
||||
function tooltipText(variant: Icon) {
|
||||
switch (variant) {
|
||||
case Icon.Globe:
|
||||
return _t("This room is public");
|
||||
case Icon.PresenceOnline:
|
||||
return _t("Online");
|
||||
case Icon.PresenceAway:
|
||||
return _t("Away");
|
||||
case Icon.PresenceOffline:
|
||||
return _t("Offline");
|
||||
}
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
tag: TagID;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -122,10 +136,11 @@ export default class RoomTileIcon extends React.Component<IProps, IState> {
|
|||
private calculateIcon(): Icon {
|
||||
let icon = Icon.None;
|
||||
|
||||
if (this.props.tag === DefaultTagID.DM && this.props.room.getJoinedMemberCount() === 2) {
|
||||
// We look at the DMRoomMap and not the tag here so that we don't exclude DMs in Favourites
|
||||
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId);
|
||||
if (otherUserId && this.props.room.getJoinedMemberCount() === 2) {
|
||||
// Track presence, if available
|
||||
if (isPresenceEnabled()) {
|
||||
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId);
|
||||
if (otherUserId) {
|
||||
this.dmUser = MatrixClientPeg.get().getUser(otherUserId);
|
||||
icon = this.getPresenceIcon();
|
||||
|
@ -145,6 +160,10 @@ export default class RoomTileIcon extends React.Component<IProps, IState> {
|
|||
public render(): React.ReactElement {
|
||||
if (this.state.icon === Icon.None) return null;
|
||||
|
||||
return <span className={`mx_RoomTileIcon mx_RoomTileIcon_${this.state.icon.toLowerCase()}`} />;
|
||||
return <TextWithTooltip
|
||||
tooltip={tooltipText(this.state.icon)}
|
||||
tooltipClass="mx_Tooltip_timeline"
|
||||
class={`mx_RoomTileIcon mx_RoomTileIcon_${this.state.icon.toLowerCase()}`}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -120,7 +120,7 @@ export default class HelpUserSettingsTab extends React.Component {
|
|||
<span className='mx_SettingsTab_subheading'>{_t("Credits")}</span>
|
||||
<ul>
|
||||
<li>
|
||||
The <a href="themes/riot/img/backgrounds/lake.jpg" rel="noreferrer noopener" target="_blank">
|
||||
The <a href="themes/element/img/backgrounds/lake.jpg" rel="noreferrer noopener" target="_blank">
|
||||
default cover photo</a> is ©
|
||||
<a href="https://www.flickr.com/golan" rel="noreferrer noopener" target="_blank">Jesús Roncero</a>{' '}
|
||||
used under the terms of
|
||||
|
@ -158,7 +158,7 @@ export default class HelpUserSettingsTab extends React.Component {
|
|||
},
|
||||
{
|
||||
'a': (sub) => <a
|
||||
href="https://about.riot.im/need-help/"
|
||||
href="https://element.io/help"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
|
@ -177,7 +177,7 @@ export default class HelpUserSettingsTab extends React.Component {
|
|||
},
|
||||
{
|
||||
'a': (sub) => <a
|
||||
href="https://about.riot.im/need-help/"
|
||||
href="https://element.io/help"
|
||||
rel='noreferrer noopener'
|
||||
target='_blank'
|
||||
>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue