Merge branches 'develop' and 't3chguy/confusing_copy' of github.com:matrix-org/matrix-react-sdk into t3chguy/confusing_copy

This commit is contained in:
Michael Telatynski 2020-05-26 13:35:18 +01:00
commit 0713139dc5
53 changed files with 1899 additions and 575 deletions

View file

@ -92,7 +92,7 @@ const CategoryRoomList = createReactClass({
Modal.createTrackedDialog('Add Rooms to Group Summary', '', AddressPickerDialog, {
title: _t('Add rooms to the community summary'),
description: _t("Which rooms would you like to add to this summary?"),
placeholder: _t("Room name or alias"),
placeholder: _t("Room name or address"),
button: _t("Add to summary"),
pickerType: 'room',
validAddressTypes: ['mx-room-id'],

View file

@ -29,7 +29,7 @@ import { fixupColorFonts } from '../../utils/FontManager';
import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher';
import sessionStore from '../../stores/SessionStore';
import {MatrixClientPeg, MatrixClientCreds} from '../../MatrixClientPeg';
import {MatrixClientPeg, IMatrixClientCreds} from '../../MatrixClientPeg';
import SettingsStore from "../../settings/SettingsStore";
import TagOrderActions from '../../actions/TagOrderActions';
@ -57,7 +57,7 @@ function canElementReceiveInput(el) {
interface IProps {
matrixClient: MatrixClient;
onRegistered: (credentials: MatrixClientCreds) => Promise<MatrixClient>;
onRegistered: (credentials: IMatrixClientCreds) => Promise<MatrixClient>;
viaServers?: string[];
hideToSRUsers: boolean;
resizeNotifier: ResizeNotifier;

View file

@ -932,9 +932,20 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
});
}
private viewGroup(payload) {
private async viewGroup(payload) {
const groupId = payload.group_id;
// Wait for the first sync to complete
if (!this.firstSyncComplete) {
if (!this.firstSyncPromise) {
console.warn('Cannot view a group before first sync. group_id:', groupId);
return;
}
await this.firstSyncPromise.promise;
}
this.setState({
view: Views.LOGGED_IN,
currentGroupId: groupId,
currentGroupIsNew: payload.group_is_new,
});

View file

@ -199,7 +199,7 @@ export default createReactClass({
let desc;
if (alias) {
desc = _t('Delete the room alias %(alias)s and remove %(name)s from the directory?', {alias: alias, name: name});
desc = _t('Delete the room address %(alias)s and remove %(name)s from the directory?', {alias, name});
} else {
desc = _t('Remove %(name)s from the directory?', {name: name});
}
@ -216,7 +216,7 @@ export default createReactClass({
MatrixClientPeg.get().setRoomDirectoryVisibility(room.room_id, 'private').then(() => {
if (!alias) return;
step = _t('delete the alias.');
step = _t('delete the address.');
return MatrixClientPeg.get().deleteAlias(alias);
}).then(() => {
modal.close();

View file

@ -15,7 +15,6 @@ limitations under the License.
*/
import * as React from "react";
import { _t } from '../../languageHandler';
import ToastStore from "../../stores/ToastStore";
import classNames from "classnames";
@ -50,14 +49,21 @@ export default class ToastContainer extends React.Component {
"mx_Toast_hasIcon": icon,
[`mx_Toast_icon_${icon}`]: icon,
});
const countIndicator = isStacked ? _t(" (1/%(totalCount)s)", {totalCount}) : null;
let countIndicator;
if (isStacked) {
countIndicator = `(1/${totalCount})`;
}
const toastProps = Object.assign({}, props, {
key,
toastKey: key,
});
toast = (<div className={toastClasses}>
<h2>{title}{countIndicator}</h2>
<div className="mx_Toast_title">
<h2>{title}</h2>
<span>{countIndicator}</span>
</div>
<div className="mx_Toast_body">{React.createElement(component, toastProps)}</div>
</div>);
}

View file

@ -26,58 +26,48 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {useEventEmitter} from "../../../hooks/useEventEmitter";
import {toPx} from "../../../utils/units";
const useImageUrl = ({url, urls, idName, name, defaultToInitialLetter}) => {
const useImageUrl = ({url, urls}) => {
const [imageUrls, setUrls] = useState([]);
const [urlsIndex, setIndex] = useState();
const onError = () => {
const nextIndex = urlsIndex + 1;
if (nextIndex < imageUrls.length) {
// try the next one
setIndex(nextIndex);
}
};
const defaultImageUrl = useMemo(() => AvatarLogic.defaultAvatarUrlForString(idName || name), [idName, name]);
const onError = useCallback(() => {
setIndex(i => i + 1); // try the next one
}, []);
const memoizedUrls = useMemo(() => urls, [JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
// work out the full set of urls to try to load. This is formed like so:
// imageUrls: [ props.url, ...props.urls, default image ]
// imageUrls: [ props.url, ...props.urls ]
let _urls = [];
if (!SettingsStore.getValue("lowBandwidth")) {
_urls = urls || [];
_urls = memoizedUrls || [];
if (url) {
_urls.unshift(url); // put in urls[0]
}
}
if (defaultToInitialLetter) {
_urls.push(defaultImageUrl); // lowest priority
}
// deduplicate URLs
_urls = Array.from(new Set(_urls));
setIndex(0);
setUrls(_urls);
}, [url, ...(urls || [])]); // eslint-disable-line react-hooks/exhaustive-deps
}, [url, memoizedUrls]); // eslint-disable-line react-hooks/exhaustive-deps
const cli = useContext(MatrixClientContext);
const onClientSync = useCallback((syncState, prevState) => {
// Consider the client reconnected if there is no error with syncing.
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
const reconnected = syncState !== "ERROR" && prevState !== syncState;
if (reconnected && urlsIndex > 0 ) { // Did we fall back?
// Start from the highest priority URL again
if (reconnected) {
setIndex(0);
}
}, [urlsIndex]);
}, []);
useEventEmitter(cli, "sync", onClientSync);
const imageUrl = imageUrls[urlsIndex];
return [imageUrl, imageUrl === defaultImageUrl, onError];
return [imageUrl, onError];
};
const BaseAvatar = (props) => {
@ -96,9 +86,9 @@ const BaseAvatar = (props) => {
...otherProps
} = props;
const [imageUrl, isDefault, onError] = useImageUrl({url, urls, idName, name, defaultToInitialLetter});
const [imageUrl, onError] = useImageUrl({url, urls});
if (isDefault) {
if (!imageUrl && defaultToInitialLetter) {
const initialLetter = AvatarLogic.getInitialLetter(name);
const textNode = (
<span
@ -116,7 +106,7 @@ const BaseAvatar = (props) => {
const imgNode = (
<img
className="mx_BaseAvatar_image"
src={imageUrl}
src={AvatarLogic.defaultAvatarUrlForString(idName || name)}
alt=""
title={title}
onError={onError}

View file

@ -98,7 +98,7 @@ export default createReactClass({
render: function() {
return (
<input type="text" className="mx_RoomAlias" placeholder={_t("Alias (optional)")}
<input type="text" className="mx_RoomAlias" placeholder={_t("Address (optional)")}
onChange={this.onValueChanged} onFocus={this.onFocus} onBlur={this.onBlur}
value={this.props.alias} />
);

View file

@ -181,7 +181,7 @@ export default createReactClass({
let publicPrivateLabel;
let aliasField;
if (this.state.isPublic) {
publicPrivateLabel = (<p>{_t("Set a room alias to easily share your room with other people.")}</p>);
publicPrivateLabel = (<p>{_t("Set a room address to easily share your room with other people.")}</p>);
const domain = MatrixClientPeg.get().getDomain();
aliasField = (
<div className="mx_CreateRoomDialog_aliasContainer">

View file

@ -45,7 +45,7 @@ export default class RoomAliasField extends React.PureComponent {
const maxlength = 255 - this.props.domain.length - 2; // 2 for # and :
return (
<Field
label={_t("Room alias")}
label={_t("Room address")}
className="mx_RoomAliasField"
prefix={poundSign}
postfix={domain}
@ -87,7 +87,7 @@ export default class RoomAliasField extends React.PureComponent {
}, {
key: "required",
test: async ({ value, allowEmpty }) => allowEmpty || !!value,
invalid: () => _t("Please provide a room alias"),
invalid: () => _t("Please provide a room address"),
}, {
key: "taken",
final: true,
@ -107,8 +107,8 @@ export default class RoomAliasField extends React.PureComponent {
return !!err.errcode;
}
},
valid: () => _t("This alias is available to use"),
invalid: () => _t("This alias is already in use"),
valid: () => _t("This address is available to use"),
invalid: () => _t("This address is already in use"),
},
],
});

View file

@ -67,7 +67,13 @@ class Category extends React.PureComponent {
const localScrollTop = Math.max(0, scrollTop - listTop);
return (
<section className="mx_EmojiPicker_category" data-category-id={this.props.id}>
<section
id={`mx_EmojiPicker_category_${this.props.id}`}
className="mx_EmojiPicker_category"
data-category-id={this.props.id}
role="tabpanel"
aria-label={name}
>
<h2 className="mx_EmojiPicker_category_label">
{name}
</h2>

View file

@ -16,6 +16,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {MenuItem} from "../../structures/ContextMenu";
class Emoji extends React.PureComponent {
static propTypes = {
@ -30,14 +31,18 @@ class Emoji extends React.PureComponent {
const { onClick, onMouseEnter, onMouseLeave, emoji, selectedEmojis } = this.props;
const isSelected = selectedEmojis && selectedEmojis.has(emoji.unicode);
return (
<li onClick={() => onClick(emoji)}
<MenuItem
element="li"
onClick={() => onClick(emoji)}
onMouseEnter={() => onMouseEnter(emoji)}
onMouseLeave={() => onMouseLeave(emoji)}
className="mx_EmojiPicker_item_wrapper">
className="mx_EmojiPicker_item_wrapper"
label={emoji.unicode}
>
<div className={`mx_EmojiPicker_item ${isSelected ? 'mx_EmojiPicker_item_selected' : ''}`}>
{emoji.unicode}
</div>
</li>
</MenuItem>
);
}
}

View file

@ -147,8 +147,12 @@ class EmojiPicker extends React.Component {
// We update this here instead of through React to avoid re-render on scroll.
if (cat.visible) {
cat.ref.current.classList.add("mx_EmojiPicker_anchor_visible");
cat.ref.current.setAttribute("aria-selected", true);
cat.ref.current.setAttribute("tabindex", 0);
} else {
cat.ref.current.classList.remove("mx_EmojiPicker_anchor_visible");
cat.ref.current.setAttribute("aria-selected", false);
cat.ref.current.setAttribute("tabindex", -1);
}
}
}

View file

@ -16,23 +16,89 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import classNames from "classnames";
import {_t} from "../../../languageHandler";
import {Key} from "../../../Keyboard";
class Header extends React.PureComponent {
static propTypes = {
categories: PropTypes.arrayOf(PropTypes.object).isRequired,
onAnchorClick: PropTypes.func.isRequired,
refs: PropTypes.object,
};
findNearestEnabled(index, delta) {
index += this.props.categories.length;
const cats = [...this.props.categories, ...this.props.categories, ...this.props.categories];
while (index < cats.length && index >= 0) {
if (cats[index].enabled) return index % this.props.categories.length;
index += delta > 0 ? 1 : -1;
}
}
changeCategoryRelative(delta) {
const current = this.props.categories.findIndex(c => c.visible);
this.changeCategoryAbsolute(current + delta, delta);
}
changeCategoryAbsolute(index, delta=1) {
const category = this.props.categories[this.findNearestEnabled(index, delta)];
if (category) {
this.props.onAnchorClick(category.id);
category.ref.current.focus();
}
}
// Implements ARIA Tabs with Automatic Activation pattern
// https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-1/tabs.html
onKeyDown = (ev) => {
let handled = true;
switch (ev.key) {
case Key.ARROW_LEFT:
this.changeCategoryRelative(-1);
break;
case Key.ARROW_RIGHT:
this.changeCategoryRelative(1);
break;
case Key.HOME:
this.changeCategoryAbsolute(0);
break;
case Key.END:
this.changeCategoryAbsolute(this.props.categories.length - 1, -1);
break;
default:
handled = false;
}
if (handled) {
ev.preventDefault();
ev.stopPropagation();
}
};
render() {
return (
<nav className="mx_EmojiPicker_header">
{this.props.categories.map(category => (
<button disabled={!category.enabled} key={category.id} ref={category.ref}
className={`mx_EmojiPicker_anchor ${category.visible ? 'mx_EmojiPicker_anchor_visible' : ''}
mx_EmojiPicker_anchor_${category.id}`}
onClick={() => this.props.onAnchorClick(category.id)} title={category.name} />
))}
<nav className="mx_EmojiPicker_header" role="tablist" aria-label={_t("Categories")} onKeyDown={this.onKeyDown}>
{this.props.categories.map(category => {
const classes = classNames(`mx_EmojiPicker_anchor mx_EmojiPicker_anchor_${category.id}`, {
mx_EmojiPicker_anchor_visible: category.visible,
});
// Properties of this button are also modified by EmojiPicker's updateVisibility in DOM.
return <button
disabled={!category.enabled}
key={category.id}
ref={category.ref}
className={classes}
onClick={() => this.props.onAnchorClick(category.id)}
title={category.name}
role="tab"
tabIndex={category.visible ? 0 : -1} // roving
aria-selected={category.visible}
aria-controls={`mx_EmojiPicker_category_${category.id}`}
/>;
})}
</nav>
);
}

View file

@ -72,7 +72,7 @@ class QuickReactions extends React.Component {
</React.Fragment>
}
</h2>
<ul className="mx_EmojiPicker_list">
<ul className="mx_EmojiPicker_list" aria-label={_t("Quick Reactions")}>
{QUICK_REACTIONS.map(emoji => <Emoji
key={emoji.hexcode} emoji={emoji} onClick={this.props.onClick}
onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}

View file

@ -18,6 +18,7 @@ import React from 'react';
import PropTypes from "prop-types";
import EmojiPicker from "./EmojiPicker";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import dis from "../../../dispatcher/dispatcher";
class ReactionPicker extends React.Component {
static propTypes = {
@ -105,6 +106,7 @@ class ReactionPicker extends React.Component {
"key": reaction,
},
});
dis.dispatch({action: "message_sent"});
return true;
}
}

View file

@ -22,6 +22,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
import dis from "../../../dispatcher/dispatcher";
export default class ReactionsRowButton extends React.PureComponent {
static propTypes = {
@ -60,6 +61,7 @@ export default class ReactionsRowButton extends React.PureComponent {
"key": content,
},
});
dis.dispatch({action: "message_sent"});
}
};

View file

@ -220,10 +220,10 @@ export default class AliasSettings extends React.Component {
}
}).catch((err) => {
console.error(err);
Modal.createTrackedDialog('Error creating alias', '', ErrorDialog, {
title: _t("Error creating alias"),
Modal.createTrackedDialog('Error creating address', '', ErrorDialog, {
title: _t("Error creating address"),
description: _t(
"There was an error creating that alias. It may not be allowed by the server " +
"There was an error creating that address. It may not be allowed by the server " +
"or a temporary failure occurred.",
),
});
@ -245,15 +245,15 @@ export default class AliasSettings extends React.Component {
console.error(err);
let description;
if (err.errcode === "M_FORBIDDEN") {
description = _t("You don't have permission to delete the alias.");
description = _t("You don't have permission to delete the address.");
} else {
description = _t(
"There was an error removing that alias. It may no longer exist or a temporary " +
"There was an error removing that address. It may no longer exist or a temporary " +
"error occurred.",
);
}
Modal.createTrackedDialog('Error removing alias', '', ErrorDialog, {
title: _t("Error removing alias"),
Modal.createTrackedDialog('Error removing address', '', ErrorDialog, {
title: _t("Error removing address"),
description,
});
});

View file

@ -190,6 +190,7 @@ export default class EditMessageComposer extends React.Component {
const roomId = editedEvent.getRoomId();
this._cancelPreviousPendingEdit();
this.context.sendMessage(roomId, editContent);
dis.dispatch({action: "message_sent"});
}
// close the event editing and focus composer

View file

@ -403,7 +403,7 @@ export default createReactClass({
},
shouldHighlight: function() {
const actions = this.context.getPushActionsForEvent(this.props.mxEvent);
const actions = this.context.getPushActionsForEvent(this.props.mxEvent.replacingEvent() || this.props.mxEvent);
if (!actions || !actions.tweaks) { return false; }
// don't show self-highlights from another of our clients

View file

@ -39,6 +39,14 @@ import * as FormattingUtils from "../../../utils/FormattingUtils";
* warning disappears. *
*******************************************************************/
enum NotificationColor {
// Inverted (None -> Red) because we do integer comparisons on this
None, // nothing special
Bold, // no badge, show as unread
Grey, // unread notified messages
Red, // unread pings
}
interface IProps {
room: Room;
@ -47,16 +55,14 @@ interface IProps {
// TODO: Incoming call boxes?
}
interface IBadgeState {
showBadge: boolean; // if numUnread > 0 && !showBadge -> bold room
numUnread: number; // used only if showBadge or showBadgeHighlight is true
hasUnread: number; // used to make the room bold
showBadgeHighlight: boolean; // make the badge red
isInvite: boolean; // show a `!` instead of a number
interface INotificationState {
symbol: string;
color: NotificationColor;
}
interface IState extends IBadgeState {
interface IState {
hover: boolean;
notificationState: INotificationState;
}
export default class RoomTile2 extends React.Component<IProps, IState> {
@ -78,8 +84,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
this.state = {
hover: false,
...this.getBadgeState(),
notificationState: this.getNotificationState(),
};
}
@ -87,28 +92,58 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
// TODO: Listen for changes to the badge count and update as needed
}
private updateBadgeCount() {
this.setState({...this.getBadgeState()});
// XXX: This is a bit of an awful-looking hack. We should probably be using state for
// this, but instead we're kinda forced to either duplicate the code or thread a variable
// through the code paths. This feels like the least evil option.
private get roomIsInvite(): boolean {
return getEffectiveMembership(this.props.room.getMyMembership()) === EffectiveMembership.Invite;
}
private getBadgeState(): IBadgeState {
// TODO: Make this code path faster
const highlightCount = RoomNotifs.getUnreadNotificationCount(this.props.room, 'highlight');
const numUnread = RoomNotifs.getUnreadNotificationCount(this.props.room);
const showBadge = Unread.doesRoomHaveUnreadMessages(this.props.room);
const myMembership = getEffectiveMembership(this.props.room.getMyMembership());
const isInvite = myMembership === EffectiveMembership.Invite;
const notifState = RoomNotifs.getRoomNotifsState(this.props.room.roomId);
const shouldShowNotifBadge = RoomNotifs.shouldShowNotifBadge(notifState);
const shouldShowHighlightBadge = RoomNotifs.shouldShowMentionBadge(notifState);
// TODO: Make use of this function when the notification state needs updating.
private updateNotificationState() {
this.setState({notificationState: this.getNotificationState()});
}
return {
showBadge: (showBadge && shouldShowNotifBadge) || isInvite,
numUnread,
hasUnread: showBadge,
showBadgeHighlight: (highlightCount > 0 && shouldShowHighlightBadge) || isInvite,
isInvite,
private getNotificationState(): INotificationState {
const state: INotificationState = {
color: NotificationColor.None,
symbol: null,
};
if (this.roomIsInvite) {
state.color = NotificationColor.Red;
state.symbol = "!";
} else {
const redNotifs = RoomNotifs.getUnreadNotificationCount(this.props.room, 'highlight');
const greyNotifs = RoomNotifs.getUnreadNotificationCount(this.props.room, 'total');
// For a 'true count' we pick the grey notifications first because they include the
// red notifications. If we don't have a grey count for some reason we use the red
// count. If that count is broken for some reason, assume zero. This avoids us showing
// a badge for 'NaN' (which formats as 'NaNB' for NaN Billion).
const trueCount = greyNotifs ? greyNotifs : (redNotifs ? redNotifs : 0);
// Note: we only set the symbol if we have an actual count. We don't want to show
// zero on badges.
if (redNotifs > 0) {
state.color = NotificationColor.Red;
state.symbol = FormattingUtils.formatCount(trueCount);
} else if (greyNotifs > 0) {
state.color = NotificationColor.Grey;
state.symbol = FormattingUtils.formatCount(trueCount);
} else {
// We don't have any notified messages, but we might have unread messages. Let's
// find out.
const hasUnread = Unread.doesRoomHaveUnreadMessages(this.props.room);
if (hasUnread) {
state.color = NotificationColor.Bold;
// no symbol for this state
}
}
}
return state;
}
private onTileMouseEnter = () => {
@ -135,15 +170,17 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
// TODO: a11y proper
// TODO: Render more than bare minimum
const hasBadge = this.state.notificationState.color > NotificationColor.Bold;
const isUnread = this.state.notificationState.color > NotificationColor.None;
const classes = classNames({
'mx_RoomTile': true,
// 'mx_RoomTile_selected': this.state.selected,
'mx_RoomTile_unread': this.state.numUnread > 0 || this.state.hasUnread,
'mx_RoomTile_unreadNotify': this.state.showBadge,
'mx_RoomTile_highlight': this.state.showBadgeHighlight,
'mx_RoomTile_invited': this.state.isInvite,
'mx_RoomTile_unread': isUnread,
'mx_RoomTile_unreadNotify': this.state.notificationState.color >= NotificationColor.Grey,
'mx_RoomTile_highlight': this.state.notificationState.color >= NotificationColor.Red,
'mx_RoomTile_invited': this.roomIsInvite,
// 'mx_RoomTile_menuDisplayed': isMenuDisplayed,
'mx_RoomTile_noBadges': !this.state.showBadge,
'mx_RoomTile_noBadges': !hasBadge,
// 'mx_RoomTile_transparent': this.props.transparent,
// 'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed,
});
@ -154,13 +191,12 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
let badge;
if (this.state.showBadge) {
if (hasBadge) {
const badgeClasses = classNames({
'mx_RoomTile_badge': true,
'mx_RoomTile_badgeButton': false, // this.state.badgeHover || isMenuDisplayed
});
const formattedCount = this.state.isInvite ? `!` : FormattingUtils.formatCount(this.state.numUnread);
badge = <div className={badgeClasses}>{formattedCount}</div>;
badge = <div className={badgeClasses}>{this.state.notificationState.symbol}</div>;
}
// TODO: the original RoomTile uses state for the room name. Do we need to?
@ -170,8 +206,8 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
const nameClasses = classNames({
'mx_RoomTile_name': true,
'mx_RoomTile_invite': this.state.isInvite,
'mx_RoomTile_badgeShown': this.state.showBadge,
'mx_RoomTile_invite': this.roomIsInvite,
'mx_RoomTile_badgeShown': hasBadge,
});
// TODO: Support collapsed state properly

View file

@ -312,6 +312,7 @@ export default class SendMessageComposer extends React.Component {
event: null,
});
}
dis.dispatch({action: "message_sent"});
}
this.sendHistoryManager.save(this.model);

View file

@ -247,7 +247,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
<div className='mx_SecurityRoomSettingsTab_warning'>
<img src={require("../../../../../../res/img/warning.svg")} width={15} height={15} />
<span>
{_t("To link to this room, please add an alias.")}
{_t("To link to this room, please add an address.")}
</span>
</div>
);

View file

@ -84,7 +84,7 @@ export default class MjolnirUserSettingsTab extends React.Component {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to subscribe to Mjolnir list', '', ErrorDialog, {
title: _t('Error subscribing to list'),
description: _t('Please verify the room ID or alias and try again.'),
description: _t('Please verify the room ID or address and try again.'),
});
} finally {
this.setState({busy: false});
@ -305,7 +305,7 @@ export default class MjolnirUserSettingsTab extends React.Component {
<form onSubmit={this._onSubscribeList} autoComplete="off">
<Field
type="text"
label={_t("Room ID or alias of ban list")}
label={_t("Room ID or address of ban list")}
value={this.state.newList}
onChange={this._onNewListChanged}
/>