diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx
index ab5b93794c..ec5afd13f0 100644
--- a/src/components/structures/LoggedInView.tsx
+++ b/src/components/structures/LoggedInView.tsx
@@ -21,7 +21,7 @@ import * as PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk/src/client';
import { DragDropContext } from 'react-beautiful-dnd';
-import {Key, isOnlyCtrlOrCmdKeyEvent, isOnlyCtrlOrCmdIgnoreShiftKeyEvent} from '../../Keyboard';
+import {Key, isOnlyCtrlOrCmdKeyEvent, isOnlyCtrlOrCmdIgnoreShiftKeyEvent, isMac} from '../../Keyboard';
import PageTypes from '../../PageTypes';
import CallMediaHandler from '../../CallMediaHandler';
import { fixupColorFonts } from '../../utils/FontManager';
@@ -52,6 +52,7 @@ import RoomListStore from "../../stores/room-list/RoomListStore";
import NonUrgentToastContainer from "./NonUrgentToastContainer";
import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload";
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
+import Modal from "../../Modal";
import { ICollapseConfig } from "../../resizer/distributors/collapse";
// We need to fetch each pinned message individually (if we don't already have it)
@@ -392,6 +393,7 @@ class LoggedInView extends React.Component {
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
const hasModifier = ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey;
const isModifier = ev.key === Key.ALT || ev.key === Key.CONTROL || ev.key === Key.META || ev.key === Key.SHIFT;
+ const modKey = isMac ? ev.metaKey : ev.ctrlKey;
switch (ev.key) {
case Key.PAGE_UP:
@@ -436,6 +438,16 @@ class LoggedInView extends React.Component {
}
break;
+ case Key.H:
+ if (ev.altKey && modKey) {
+ dis.dispatch({
+ action: 'view_home_page',
+ });
+ Modal.closeCurrentModal("homeKeyboardShortcut");
+ handled = true;
+ }
+ break;
+
case Key.ARROW_UP:
case Key.ARROW_DOWN:
if (ev.altKey && !ev.ctrlKey && !ev.metaKey) {
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index 22cd73eff7..114347196a 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -87,38 +87,37 @@ import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
export enum Views {
// a special initial state which is only used at startup, while we are
// trying to re-animate a matrix client or register as a guest.
- LOADING = 0,
+ LOADING,
// we are showing the welcome view
- WELCOME = 1,
+ WELCOME,
// we are showing the login view
- LOGIN = 2,
+ LOGIN,
// we are showing the registration view
- REGISTER = 3,
-
- // completing the registration flow
- POST_REGISTRATION = 4,
+ REGISTER,
// showing the 'forgot password' view
- FORGOT_PASSWORD = 5,
+ FORGOT_PASSWORD,
// showing flow to trust this new device with cross-signing
- COMPLETE_SECURITY = 6,
+ COMPLETE_SECURITY,
// flow to setup SSSS / cross-signing on this account
- E2E_SETUP = 7,
+ E2E_SETUP,
// we are logged in with an active matrix client. The logged_in state also
// includes guests users as they too are logged in at the client level.
- LOGGED_IN = 8,
+ LOGGED_IN,
// We are logged out (invalid token) but have our local state again. The user
// should log back in to rehydrate the client.
- SOFT_LOGOUT = 9,
+ SOFT_LOGOUT,
}
+const AUTH_SCREENS = ["register", "login", "forgot_password", "start_sso", "start_cas"];
+
// Actions that are redirected through the onboarding process prior to being
// re-dispatched. NOTE: some actions are non-trivial and would require
// re-factoring to be included in this list in future.
@@ -562,11 +561,6 @@ export default class MatrixChat extends React.PureComponent {
ThemeController.isLogin = true;
this.themeWatcher.recheck();
break;
- case 'start_post_registration':
- this.setState({
- view: Views.POST_REGISTRATION,
- });
- break;
case 'start_password_recovery':
this.setStateForNewView({
view: Views.FORGOT_PASSWORD,
@@ -653,8 +647,9 @@ export default class MatrixChat extends React.PureComponent {
}
case Action.ViewRoomDirectory: {
const RoomDirectory = sdk.getComponent("structures.RoomDirectory");
- Modal.createTrackedDialog('Room directory', '', RoomDirectory, {},
- 'mx_RoomDirectory_dialogWrapper', false, true);
+ Modal.createTrackedDialog('Room directory', '', RoomDirectory, {
+ initialText: payload.initialText,
+ }, 'mx_RoomDirectory_dialogWrapper', false, true);
// View the welcome or home page if we need something to look at
this.viewSomethingBehindModal();
@@ -677,7 +672,7 @@ export default class MatrixChat extends React.PureComponent {
this.chatCreateOrReuse(payload.user_id);
break;
case 'view_create_chat':
- showStartChatInviteDialog();
+ showStartChatInviteDialog(payload.initialText || "");
break;
case 'view_invite':
showRoomInviteDialog(payload.roomId);
@@ -1554,6 +1549,14 @@ export default class MatrixChat extends React.PureComponent {
}
showScreen(screen: string, params?: {[key: string]: any}) {
+ const cli = MatrixClientPeg.get();
+ const isLoggedOutOrGuest = !cli || cli.isGuest();
+ if (!isLoggedOutOrGuest && AUTH_SCREENS.includes(screen)) {
+ // user is logged in and landing on an auth page which will uproot their session, redirect them home instead
+ dis.dispatch({ action: "view_home_page" });
+ return;
+ }
+
if (screen === 'register') {
dis.dispatch({
action: 'start_registration',
@@ -1570,7 +1573,7 @@ export default class MatrixChat extends React.PureComponent {
params: params,
});
} else if (screen === 'soft_logout') {
- if (MatrixClientPeg.get() && MatrixClientPeg.get().getUserId() && !Lifecycle.isSoftLogout()) {
+ if (cli.getUserId() && !Lifecycle.isSoftLogout()) {
// Logged in - visit a room
this.viewLastRoom();
} else {
@@ -1621,14 +1624,6 @@ export default class MatrixChat extends React.PureComponent {
dis.dispatch({
action: 'view_my_groups',
});
- } else if (screen === 'complete_security') {
- dis.dispatch({
- action: 'start_complete_security',
- });
- } else if (screen === 'post_registration') {
- dis.dispatch({
- action: 'start_post_registration',
- });
} else if (screen.indexOf('room/') === 0) {
// Rooms can have the following formats:
// #room_alias:domain or !opaque_id:domain
@@ -1799,14 +1794,6 @@ export default class MatrixChat extends React.PureComponent {
return Lifecycle.setLoggedIn(credentials);
}
- onFinishPostRegistration = () => {
- // Don't confuse this with "PageType" which is the middle window to show
- this.setState({
- view: Views.LOGGED_IN,
- });
- this.showScreen("settings");
- };
-
onSendEvent(roomId: string, event: MatrixEvent) {
const cli = MatrixClientPeg.get();
if (!cli) {
@@ -1971,13 +1958,6 @@ export default class MatrixChat extends React.PureComponent {
accountPassword={this.accountPassword}
/>
);
- } else if (this.state.view === Views.POST_REGISTRATION) {
- // needs to be before normal PageTypes as you are logged in technically
- const PostRegistration = sdk.getComponent('structures.auth.PostRegistration');
- view = (
-
- );
} else if (this.state.view === Views.LOGGED_IN) {
// store errors stop the client syncing and require user intervention, so we'll
// be showing a dialog. Don't show anything else.
diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index e2e3592536..375545f819 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -30,6 +30,8 @@ import {_t} from "../../languageHandler";
import {haveTileForEvent} from "../views/rooms/EventTile";
import {textForEvent} from "../../TextForEvent";
import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
+import DMRoomMap from "../../utils/DMRoomMap";
+import NewRoomIntro from "../views/rooms/NewRoomIntro";
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
const continuedTypes = ['m.sticker', 'm.room.message'];
@@ -952,15 +954,25 @@ class CreationGrouper {
}).reduce((a, b) => a.concat(b), []);
// Get sender profile from the latest event in the summary as the m.room.create doesn't contain one
const ev = this.events[this.events.length - 1];
+
+ let summaryText;
+ const roomId = ev.getRoomId();
+ const creator = ev.sender ? ev.sender.name : ev.getSender();
+ if (DMRoomMap.shared().getUserIdForRoomId(roomId)) {
+ summaryText = _t("%(creator)s created this DM.", { creator });
+ } else {
+ summaryText = _t("%(creator)s created and configured the room.", { creator });
+ }
+
+ ret.push();
+
ret.push(
{ eventTiles }
,
diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js
index ece70e3a8f..e3323b05fa 100644
--- a/src/components/structures/RoomDirectory.js
+++ b/src/components/structures/RoomDirectory.js
@@ -44,6 +44,7 @@ function track(action) {
export default class RoomDirectory extends React.Component {
static propTypes = {
+ initialText: PropTypes.string,
onFinished: PropTypes.func.isRequired,
};
@@ -61,7 +62,7 @@ export default class RoomDirectory extends React.Component {
error: null,
instanceId: undefined,
roomServer: MatrixClientPeg.getHomeserverName(),
- filterString: null,
+ filterString: this.props.initialText || "",
selectedCommunityId: SettingsStore.getValue("feature_communities_v2_prototypes")
? selectedCommunityId
: null,
@@ -686,6 +687,7 @@ export default class RoomDirectory extends React.Component {
onJoinClick={this.onJoinFromSearchClick}
placeholder={placeholder}
showJoinButton={showJoinButton}
+ initialText={this.props.initialText}
/>
{dropdown}
;
diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx
index 526aecddd7..a64e40bc65 100644
--- a/src/components/structures/RoomSearch.tsx
+++ b/src/components/structures/RoomSearch.tsx
@@ -148,7 +148,7 @@ export default class RoomSearch extends React.PureComponent {
onBlur={this.onBlur}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
- placeholder={_t("Search")}
+ placeholder={_t("Filter")}
autoComplete="off"
/>
);
@@ -164,7 +164,7 @@ export default class RoomSearch extends React.PureComponent {
if (this.props.isMinimized) {
icon = (
diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js
index e390be6979..e6d2985073 100644
--- a/src/components/structures/RoomStatusBar.js
+++ b/src/components/structures/RoomStatusBar.js
@@ -41,9 +41,6 @@ export default class RoomStatusBar extends React.Component {
static propTypes = {
// the room this statusbar is representing.
room: PropTypes.object.isRequired,
- // This is true when the user is alone in the room, but has also sent a message.
- // Used to suggest to the user to invite someone
- sentMessageAndIsAlone: PropTypes.bool,
// The active call in the room, if any (means we show the call bar
// along with the status of the call)
@@ -68,10 +65,6 @@ export default class RoomStatusBar extends React.Component {
// 'you are alone' bar
onInviteClick: PropTypes.func,
- // callback for when the user clicks on the 'stop warning me' button in the
- // 'you are alone' bar
- onStopWarningClick: PropTypes.func,
-
// callback for when we do something that changes the size of the
// status bar. This is used to trigger a re-layout in the parent
// component.
@@ -159,10 +152,7 @@ export default class RoomStatusBar extends React.Component {
// changed - so we use '0' to indicate normal size, and other values to
// indicate other sizes.
_getSize() {
- if (this._shouldShowConnectionError() ||
- this._showCallBar() ||
- this.props.sentMessageAndIsAlone
- ) {
+ if (this._shouldShowConnectionError() || this._showCallBar()) {
return STATUS_BAR_EXPANDED;
} else if (this.state.unsentMessages.length > 0) {
return STATUS_BAR_EXPANDED_LARGE;
@@ -325,24 +315,6 @@ export default class RoomStatusBar extends React.Component {
);
}
- // If you're alone in the room, and have sent a message, suggest to invite someone
- if (this.props.sentMessageAndIsAlone && !this.props.isPeeking) {
- return (
-
- { _t("There's no one else here! Would you like to invite others " +
- "or stop warning about the empty room?",
- {},
- {
- 'inviteText': (sub) =>
- { sub },
- 'nowarnText': (sub) =>
- { sub },
- },
- ) }
-
- );
- }
-
return null;
}
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 0cb4a5d305..de7ae347dd 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -71,7 +71,7 @@ import RoomHeader from "../views/rooms/RoomHeader";
import TintableSvg from "../views/elements/TintableSvg";
import {XOR} from "../../@types/common";
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
-import { CallState, CallType, MatrixCall } from "matrix-js-sdk/lib/webrtc/call";
+import { CallState, CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import WidgetStore from "../../stores/WidgetStore";
import {UPDATE_EVENT} from "../../stores/AsyncStore";
import Notifier from "../../Notifier";
@@ -150,7 +150,6 @@ export interface IState {
guestsCanJoin: boolean;
canPeek: boolean;
showApps: boolean;
- isAlone: boolean;
isPeeking: boolean;
showingPinned: boolean;
showReadReceipts: boolean;
@@ -223,7 +222,6 @@ export default class RoomView extends React.Component {
guestsCanJoin: false,
canPeek: false,
showApps: false,
- isAlone: false,
isPeeking: false,
showingPinned: false,
showReadReceipts: true,
@@ -705,9 +703,8 @@ export default class RoomView extends React.Component {
private onAction = payload => {
switch (payload.action) {
- case 'message_send_failed':
case 'message_sent':
- this.checkIfAlone(this.state.room);
+ this.checkDesktopNotifications();
break;
case 'post_sticker_message':
this.injectSticker(
@@ -1025,36 +1022,15 @@ export default class RoomView extends React.Component {
}
// rate limited because a power level change will emit an event for every member in the room.
- private updateRoomMembers = rateLimitedFunc((dueToMember) => {
+ private updateRoomMembers = rateLimitedFunc(() => {
this.updateDMState();
-
- let memberCountInfluence = 0;
- if (dueToMember && dueToMember.membership === "invite" && this.state.room.getInvitedMemberCount() === 0) {
- // A member got invited, but the room hasn't detected that change yet. Influence the member
- // count by 1 to counteract this.
- memberCountInfluence = 1;
- }
- this.checkIfAlone(this.state.room, memberCountInfluence);
-
this.updateE2EStatus(this.state.room);
}, 500);
- private checkIfAlone(room: Room, countInfluence?: number) {
- let warnedAboutLonelyRoom = false;
- if (localStorage) {
- warnedAboutLonelyRoom = Boolean(localStorage.getItem('mx_user_alone_warned_' + this.state.room.roomId));
- }
- if (warnedAboutLonelyRoom) {
- if (this.state.isAlone) this.setState({isAlone: false});
- return;
- }
-
- let joinedOrInvitedMemberCount = room.getJoinedMemberCount() + room.getInvitedMemberCount();
- if (countInfluence) joinedOrInvitedMemberCount += countInfluence;
- this.setState({isAlone: joinedOrInvitedMemberCount === 1});
-
- // if they are not alone additionally prompt the user about notifications so they don't miss replies
- if (joinedOrInvitedMemberCount > 1 && Notifier.shouldShowPrompt()) {
+ private checkDesktopNotifications() {
+ const memberCount = this.state.room.getJoinedMemberCount() + this.state.room.getInvitedMemberCount();
+ // if they are not alone prompt the user about notifications so they don't miss replies
+ if (memberCount > 1 && Notifier.shouldShowPrompt()) {
showNotificationsToast(true);
}
}
@@ -1091,14 +1067,6 @@ export default class RoomView extends React.Component {
action: 'view_invite',
roomId: this.state.room.roomId,
});
- this.setState({isAlone: false}); // there's a good chance they'll invite someone
- };
-
- private onStopAloneWarningClick = () => {
- if (localStorage) {
- localStorage.setItem('mx_user_alone_warned_' + this.state.room.roomId, String(true));
- }
- this.setState({isAlone: false});
};
private onJoinButtonClicked = () => {
@@ -1147,16 +1115,9 @@ export default class RoomView extends React.Component {
ev.dataTransfer.dropEffect = 'none';
- const items = [...ev.dataTransfer.items];
- if (items.length >= 1) {
- const isDraggingFiles = items.every(function(item) {
- return item.kind == 'file';
- });
-
- if (isDraggingFiles) {
- this.setState({ draggingFile: true });
- ev.dataTransfer.dropEffect = 'copy';
- }
+ if (ev.dataTransfer.types.includes("Files") || ev.dataTransfer.types.includes("application/x-moz-file")) {
+ this.setState({ draggingFile: true });
+ ev.dataTransfer.dropEffect = 'copy';
}
};
@@ -1797,12 +1758,10 @@ export default class RoomView extends React.Component {
isStatusAreaExpanded = this.state.statusBarVisible;
statusBar = ;
diff --git a/src/components/structures/ToastContainer.tsx b/src/components/structures/ToastContainer.tsx
index 84473031fa..513cca82c3 100644
--- a/src/components/structures/ToastContainer.tsx
+++ b/src/components/structures/ToastContainer.tsx
@@ -55,11 +55,11 @@ export default class ToastContainer extends React.Component<{}, IState> {
let toast;
if (totalCount !== 0) {
const topToast = this.state.toasts[0];
- const {title, icon, key, component, props} = topToast;
+ const {title, icon, key, component, className, props} = topToast;
const toastClasses = classNames("mx_Toast_toast", {
"mx_Toast_hasIcon": icon,
[`mx_Toast_icon_${icon}`]: icon,
- });
+ }, className);
let countIndicator;
if (isStacked || this.state.countSeen > 0) {
diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx
index 4847d41fa8..75208b8cfe 100644
--- a/src/components/structures/UserMenu.tsx
+++ b/src/components/structures/UserMenu.tsx
@@ -190,11 +190,18 @@ export default class UserMenu extends React.Component {
this.setState({contextMenuPosition: null}); // also close the menu
};
- private onSignOutClick = (ev: ButtonEvent) => {
+ private onSignOutClick = async (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
- Modal.createTrackedDialog('Logout from LeftPanel', '', LogoutDialog);
+ const cli = MatrixClientPeg.get();
+ if (!cli || !cli.isCryptoEnabled() || !(await cli.exportRoomKeys())?.length) {
+ // log out without user prompt if they have no local megolm sessions
+ dis.dispatch({action: 'logout'});
+ } else {
+ Modal.createTrackedDialog('Logout from LeftPanel', '', LogoutDialog);
+ }
+
this.setState({contextMenuPosition: null}); // also close the menu
};
@@ -203,6 +210,7 @@ export default class UserMenu extends React.Component {
ev.stopPropagation();
defaultDispatcher.dispatch({action: 'view_home_page'});
+ this.setState({contextMenuPosition: null}); // also close the menu
};
private onCommunitySettingsClick = (ev: ButtonEvent) => {
diff --git a/src/components/structures/auth/PostRegistration.js b/src/components/structures/auth/PostRegistration.js
deleted file mode 100644
index aa36de6596..0000000000
--- a/src/components/structures/auth/PostRegistration.js
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
-Copyright 2015, 2016 OpenMarket Ltd
-
-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 React from 'react';
-import PropTypes from 'prop-types';
-import * as sdk from '../../../index';
-import {MatrixClientPeg} from '../../../MatrixClientPeg';
-import { _t } from '../../../languageHandler';
-import AuthPage from "../../views/auth/AuthPage";
-
-export default class PostRegistration extends React.Component {
- static propTypes = {
- onComplete: PropTypes.func.isRequired,
- };
-
- state = {
- avatarUrl: null,
- errorString: null,
- busy: false,
- };
-
- componentDidMount() {
- // There is some assymetry between ChangeDisplayName and ChangeAvatar,
- // as ChangeDisplayName will auto-get the name but ChangeAvatar expects
- // the URL to be passed to you (because it's also used for room avatars).
- const cli = MatrixClientPeg.get();
- this.setState({busy: true});
- const self = this;
- cli.getProfileInfo(cli.credentials.userId).then(function(result) {
- self.setState({
- avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(result.avatar_url),
- busy: false,
- });
- }, function(error) {
- self.setState({
- errorString: _t("Failed to fetch avatar URL"),
- busy: false,
- });
- });
- }
-
- render() {
- const ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName');
- const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
- const AuthHeader = sdk.getComponent('auth.AuthHeader');
- const AuthBody = sdk.getComponent("auth.AuthBody");
- return (
-
-
-
-
-
-
- );
- }
-}
diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js
index 630e04da9c..80bf3b72cd 100644
--- a/src/components/structures/auth/Registration.js
+++ b/src/components/structures/auth/Registration.js
@@ -502,6 +502,11 @@ export default class Registration extends React.Component {
return null;
}
+ // Hide the server picker once the user is doing UI Auth unless encountered a fatal server error
+ if (this.state.phase !== PHASE_SERVER_DETAILS && this.state.doingUIAuth && !this.state.serverErrorIsFatal) {
+ return null;
+ }
+
// If we're on a different phase, we only show the server type selector,
// which is always shown if we allow custom URLs at all.
// (if there's a fatal server error, we need to show the full server
@@ -582,17 +587,6 @@ export default class Registration extends React.Component {
;
} else if (this.state.flows.length) {
- let onEditServerDetailsClick = null;
- // If custom URLs are allowed and we haven't selected the Free server type, wire
- // up the server details edit link.
- if (
- PHASES_ENABLED &&
- !SdkConfig.get()['disable_custom_urls'] &&
- this.state.serverType !== ServerType.FREE
- ) {
- onEditServerDetailsClick = this.onEditServerDetailsClick;
- }
-
return ;
} else {
+ let yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
+ serverName: this.props.serverConfig.hsName,
+ });
+ if (this.props.serverConfig.hsNameIsDifferent) {
+ const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
+
+ yourMatrixAccountText = _t('Create your Matrix account on ', {}, {
+ 'underlinedServerName': () => {
+ return
+ {this.props.serverConfig.hsName}
+ ;
+ },
+ });
+ }
+
+ // If custom URLs are allowed, user is not doing UIA flows and they haven't selected the Free server type,
+ // wire up the server details edit link.
+ let editLink = null;
+ if (PHASES_ENABLED &&
+ !SdkConfig.get()['disable_custom_urls'] &&
+ this.state.serverType !== ServerType.FREE &&
+ !this.state.doingUIAuth
+ ) {
+ editLink = (
+
+ {_t('Change')}
+
+ );
+ }
+
body =