Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/15180

 Conflicts:
	src/settings/Settings.ts
	src/settings/UIFeature.ts
This commit is contained in:
Michael Telatynski 2020-09-17 13:42:27 +01:00
commit ae44a6d1fa
18 changed files with 219 additions and 103 deletions

View file

@ -18,6 +18,12 @@ limitations under the License.
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
&.mx_WelcomePage_registrationDisabled {
.mx_ButtonCreateAccount {
display: none;
}
}
} }
.mx_Welcome .mx_AuthBody_language { .mx_Welcome .mx_AuthBody_language {

View file

@ -170,15 +170,19 @@ class Analytics {
return !this.baseUrl; return !this.baseUrl;
} }
canEnable() {
const config = SdkConfig.get();
return navigator.doNotTrack !== "1" && config && config.piwik && config.piwik.url && config.piwik.siteId;
}
/** /**
* Enable Analytics if initialized but disabled * Enable Analytics if initialized but disabled
* otherwise try and initalize, no-op if piwik config missing * otherwise try and initalize, no-op if piwik config missing
*/ */
async enable() { async enable() {
if (!this.disabled) return; if (!this.disabled) return;
if (!this.canEnable()) return;
const config = SdkConfig.get(); const config = SdkConfig.get();
if (!config || !config.piwik || !config.piwik.url || !config.piwik.siteId) return;
this.baseUrl = new URL("piwik.php", config.piwik.url); this.baseUrl = new URL("piwik.php", config.piwik.url);
// set constants // set constants

View file

@ -79,6 +79,7 @@ import { SettingLevel } from "../../settings/SettingLevel";
import { leaveRoomBehaviour } from "../../utils/membership"; import { leaveRoomBehaviour } from "../../utils/membership";
import CreateCommunityPrototypeDialog from "../views/dialogs/CreateCommunityPrototypeDialog"; import CreateCommunityPrototypeDialog from "../views/dialogs/CreateCommunityPrototypeDialog";
import ThreepidInviteStore, { IThreepidInvite, IThreepidInviteWireFormat } from "../../stores/ThreepidInviteStore"; import ThreepidInviteStore, { IThreepidInvite, IThreepidInviteWireFormat } from "../../stores/ThreepidInviteStore";
import {UIFeature} from "../../settings/UIFeature";
/** constants for MatrixChat.state.view */ /** constants for MatrixChat.state.view */
export enum Views { export enum Views {
@ -1233,8 +1234,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
StorageManager.tryPersistStorage(); StorageManager.tryPersistStorage();
if (SettingsStore.getValue("showCookieBar") && this.props.config.piwik && navigator.doNotTrack !== "1") { if (SettingsStore.getValue("showCookieBar") && Analytics.canEnable()) {
showAnalyticsToast(this.props.config.piwik && this.props.config.piwik.policyUrl); showAnalyticsToast(this.props.config.piwik?.policyUrl);
} }
} }
@ -1372,6 +1373,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
ready: true, ready: true,
}); });
}); });
if (SettingsStore.getValue(UIFeature.Voip)) {
cli.on('Call.incoming', function(call) { cli.on('Call.incoming', function(call) {
// we dispatch this synchronously to make sure that the event // we dispatch this synchronously to make sure that the event
// handlers on the call are set up immediately (so that if // handlers on the call are set up immediately (so that if
@ -1381,6 +1384,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
call: call, call: call,
}, true); }, true);
}); });
}
cli.on('Session.logged_out', function(errObj) { cli.on('Session.logged_out', function(errObj) {
if (Lifecycle.isLoggingOut()) return; if (Lifecycle.isLoggingOut()) return;
@ -1942,7 +1947,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
render() { render() {
const fragmentAfterLogin = this.getFragmentAfterLogin(); const fragmentAfterLogin = this.getFragmentAfterLogin();
let view; let view = null;
if (this.state.view === Views.LOADING) { if (this.state.view === Views.LOADING) {
const Spinner = sdk.getComponent('elements.Spinner'); const Spinner = sdk.getComponent('elements.Spinner');
@ -2021,7 +2026,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} else if (this.state.view === Views.WELCOME) { } else if (this.state.view === Views.WELCOME) {
const Welcome = sdk.getComponent('auth.Welcome'); const Welcome = sdk.getComponent('auth.Welcome');
view = <Welcome />; view = <Welcome />;
} else if (this.state.view === Views.REGISTER) { } else if (this.state.view === Views.REGISTER && SettingsStore.getValue(UIFeature.Registration)) {
const Registration = sdk.getComponent('structures.auth.Registration'); const Registration = sdk.getComponent('structures.auth.Registration');
const email = ThreepidInviteStore.instance.pickBestInvite()?.toEmail; const email = ThreepidInviteStore.instance.pickBestInvite()?.toEmail;
view = ( view = (
@ -2039,7 +2044,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
{...this.getServerProperties()} {...this.getServerProperties()}
/> />
); );
} else if (this.state.view === Views.FORGOT_PASSWORD) { } else if (this.state.view === Views.FORGOT_PASSWORD && SettingsStore.getValue(UIFeature.PasswordReset)) {
const ForgotPassword = sdk.getComponent('structures.auth.ForgotPassword'); const ForgotPassword = sdk.getComponent('structures.auth.ForgotPassword');
view = ( view = (
<ForgotPassword <ForgotPassword
@ -2050,6 +2055,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
/> />
); );
} else if (this.state.view === Views.LOGIN) { } else if (this.state.view === Views.LOGIN) {
const showPasswordReset = SettingsStore.getValue(UIFeature.PasswordReset);
const Login = sdk.getComponent('structures.auth.Login'); const Login = sdk.getComponent('structures.auth.Login');
view = ( view = (
<Login <Login
@ -2058,7 +2064,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
onRegisterClick={this.onRegisterClick} onRegisterClick={this.onRegisterClick}
fallbackHsUrl={this.getFallbackHsUrl()} fallbackHsUrl={this.getFallbackHsUrl()}
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName} defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
onForgotPasswordClick={this.onForgotPasswordClick} onForgotPasswordClick={showPasswordReset ? this.onForgotPasswordClick : undefined}
onServerConfigChange={this.onServerConfigChange} onServerConfigChange={this.onServerConfigChange}
fragmentAfterLogin={fragmentAfterLogin} fragmentAfterLogin={fragmentAfterLogin}
{...this.getServerProperties()} {...this.getServerProperties()}

View file

@ -70,10 +70,10 @@ export default class RoomDirectory extends React.Component {
this.scrollPanel = null; this.scrollPanel = null;
this.protocols = null; this.protocols = null;
this.setState({protocolsLoading: true}); this.state.protocolsLoading = true;
if (!MatrixClientPeg.get()) { if (!MatrixClientPeg.get()) {
// We may not have a client yet when invoked from welcome page // We may not have a client yet when invoked from welcome page
this.setState({protocolsLoading: false}); this.state.protocolsLoading = false;
return; return;
} }
@ -102,14 +102,16 @@ export default class RoomDirectory extends React.Component {
}); });
} else { } else {
// We don't use the protocols in the communities v2 prototype experience // We don't use the protocols in the communities v2 prototype experience
this.setState({protocolsLoading: false}); this.state.protocolsLoading = false;
// Grab the profile info async // Grab the profile info async
FlairStore.getGroupProfileCached(MatrixClientPeg.get(), this.state.selectedCommunityId).then(profile => { FlairStore.getGroupProfileCached(MatrixClientPeg.get(), this.state.selectedCommunityId).then(profile => {
this.setState({communityName: profile.name}); this.setState({communityName: profile.name});
}); });
} }
}
componentDidMount() {
this.refreshRoomList(); this.refreshRoomList();
} }

View file

@ -50,6 +50,7 @@ import dis from "../../dispatcher/dispatcher";
import { RightPanelPhases } from "../../stores/RightPanelStorePhases"; import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
import ErrorDialog from "../views/dialogs/ErrorDialog"; import ErrorDialog from "../views/dialogs/ErrorDialog";
import EditCommunityPrototypeDialog from "../views/dialogs/EditCommunityPrototypeDialog"; import EditCommunityPrototypeDialog from "../views/dialogs/EditCommunityPrototypeDialog";
import {UIFeature} from "../../settings/UIFeature";
interface IProps { interface IProps {
isMinimized: boolean; isMinimized: boolean;
@ -285,6 +286,15 @@ export default class UserMenu extends React.Component<IProps, IState> {
); );
} }
let feedbackButton;
if (SettingsStore.getValue(UIFeature.Feedback)) {
feedbackButton = <IconizedContextMenuOption
iconClassName="mx_UserMenu_iconMessage"
label={_t("Feedback")}
onClick={this.onProvideFeedback}
/>;
}
let primaryHeader = ( let primaryHeader = (
<div className="mx_UserMenu_contextMenu_name"> <div className="mx_UserMenu_contextMenu_name">
<span className="mx_UserMenu_contextMenu_displayName"> <span className="mx_UserMenu_contextMenu_displayName">
@ -319,11 +329,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
label={_t("Archived rooms")} label={_t("Archived rooms")}
onClick={this.onShowArchived} onClick={this.onShowArchived}
/> */} /> */}
<IconizedContextMenuOption { feedbackButton }
iconClassName="mx_UserMenu_iconMessage"
label={_t("Feedback")}
onClick={this.onProvideFeedback}
/>
</IconizedContextMenuOptionList> </IconizedContextMenuOptionList>
<IconizedContextMenuOptionList red> <IconizedContextMenuOptionList red>
<IconizedContextMenuOption <IconizedContextMenuOption
@ -384,11 +390,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
aria-label={_t("User settings")} aria-label={_t("User settings")}
onClick={(e) => this.onSettingsOpen(e, null)} onClick={(e) => this.onSettingsOpen(e, null)}
/> />
<IconizedContextMenuOption { feedbackButton }
iconClassName="mx_UserMenu_iconMessage"
label={_t("Feedback")}
onClick={this.onProvideFeedback}
/>
</IconizedContextMenuOptionList> </IconizedContextMenuOptionList>
<IconizedContextMenuOptionList red> <IconizedContextMenuOptionList red>
<IconizedContextMenuOption <IconizedContextMenuOption

View file

@ -28,6 +28,8 @@ import classNames from "classnames";
import AuthPage from "../../views/auth/AuthPage"; import AuthPage from "../../views/auth/AuthPage";
import SSOButton from "../../views/elements/SSOButton"; import SSOButton from "../../views/elements/SSOButton";
import PlatformPeg from '../../../PlatformPeg'; import PlatformPeg from '../../../PlatformPeg';
import SettingsStore from "../../../settings/SettingsStore";
import {UIFeature} from "../../../settings/UIFeature";
// For validating phone numbers without country codes // For validating phone numbers without country codes
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/; const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
@ -679,7 +681,7 @@ export default class LoginComponent extends React.Component {
{_t("If you've joined lots of rooms, this might take a while")} {_t("If you've joined lots of rooms, this might take a while")}
</div> } </div> }
</div>; </div>;
} else { } else if (SettingsStore.getValue(UIFeature.Registration)) {
footer = ( footer = (
<a className="mx_AuthBody_changeFlow" onClick={this.onTryRegisterClick} href="#"> <a className="mx_AuthBody_changeFlow" onClick={this.onTryRegisterClick} href="#">
{ _t('Create account') } { _t('Create account') }

View file

@ -15,10 +15,14 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import classNames from "classnames";
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import AuthPage from "./AuthPage"; import AuthPage from "./AuthPage";
import {_td} from "../../../languageHandler"; import {_td} from "../../../languageHandler";
import SettingsStore from "../../../settings/SettingsStore";
import {UIFeature} from "../../../settings/UIFeature";
// translatable strings for Welcome pages // translatable strings for Welcome pages
_td("Sign in with SSO"); _td("Sign in with SSO");
@ -39,7 +43,9 @@ export default class Welcome extends React.PureComponent {
return ( return (
<AuthPage> <AuthPage>
<div className="mx_Welcome"> <div className={classNames("mx_Welcome", {
mx_WelcomePage_registrationDisabled: !SettingsStore.getValue(UIFeature.Registration),
})}>
<EmbeddedPage <EmbeddedPage
className="mx_WelcomePage" className="mx_WelcomePage"
url={pageUrl} url={pageUrl}

View file

@ -32,6 +32,7 @@ import FlairUserSettingsTab from "../settings/tabs/user/FlairUserSettingsTab";
import * as sdk from "../../../index"; import * as sdk from "../../../index";
import SdkConfig from "../../../SdkConfig"; import SdkConfig from "../../../SdkConfig";
import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab"; import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab";
import {UIFeature} from "../../../settings/UIFeature";
export const USER_GENERAL_TAB = "USER_GENERAL_TAB"; export const USER_GENERAL_TAB = "USER_GENERAL_TAB";
export const USER_APPEARANCE_TAB = "USER_APPEARANCE_TAB"; export const USER_APPEARANCE_TAB = "USER_APPEARANCE_TAB";
@ -104,12 +105,16 @@ export default class UserSettingsDialog extends React.Component {
"mx_UserSettingsDialog_preferencesIcon", "mx_UserSettingsDialog_preferencesIcon",
<PreferencesUserSettingsTab />, <PreferencesUserSettingsTab />,
)); ));
if (SettingsStore.getValue(UIFeature.Voip)) {
tabs.push(new Tab( tabs.push(new Tab(
USER_VOICE_TAB, USER_VOICE_TAB,
_td("Voice & Video"), _td("Voice & Video"),
"mx_UserSettingsDialog_voiceIcon", "mx_UserSettingsDialog_voiceIcon",
<VoiceUserSettingsTab />, <VoiceUserSettingsTab />,
)); ));
}
tabs.push(new Tab( tabs.push(new Tab(
USER_SECURITY_TAB, USER_SECURITY_TAB,
_td("Security & Privacy"), _td("Security & Privacy"),

View file

@ -20,6 +20,7 @@ import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../MatrixClientPeg';
import PlatformPeg from '../../../PlatformPeg'; import PlatformPeg from '../../../PlatformPeg';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import SdkConfig from "../../../SdkConfig";
/** /**
* This error boundary component can be used to wrap large content areas and * This error boundary component can be used to wrap large content areas and
@ -73,9 +74,10 @@ export default class ErrorBoundary extends React.PureComponent {
if (this.state.error) { if (this.state.error) {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const newIssueUrl = "https://github.com/vector-im/element-web/issues/new"; const newIssueUrl = "https://github.com/vector-im/element-web/issues/new";
return <div className="mx_ErrorBoundary">
<div className="mx_ErrorBoundary_body"> let bugReportSection;
<h1>{_t("Something went wrong!")}</h1> if (SdkConfig.get().bug_report_endpoint_url) {
bugReportSection = <React.Fragment>
<p>{_t( <p>{_t(
"Please <newIssueLink>create a new issue</newIssueLink> " + "Please <newIssueLink>create a new issue</newIssueLink> " +
"on GitHub so that we can investigate this bug.", {}, { "on GitHub so that we can investigate this bug.", {}, {
@ -94,6 +96,13 @@ export default class ErrorBoundary extends React.PureComponent {
<AccessibleButton onClick={this._onBugReport} kind='primary'> <AccessibleButton onClick={this._onBugReport} kind='primary'>
{_t("Submit debug logs")} {_t("Submit debug logs")}
</AccessibleButton> </AccessibleButton>
</React.Fragment>;
}
return <div className="mx_ErrorBoundary">
<div className="mx_ErrorBoundary_body">
<h1>{_t("Something went wrong!")}</h1>
{ bugReportSection }
<AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'> <AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'>
{_t("Clear cache and reload")} {_t("Clear cache and reload")}
</AccessibleButton> </AccessibleButton>

View file

@ -19,6 +19,7 @@ import classNames from 'classnames';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import SdkConfig from "../../../SdkConfig";
export default class TileErrorBoundary extends React.Component { export default class TileErrorBoundary extends React.Component {
constructor(props) { constructor(props) {
@ -54,14 +55,20 @@ export default class TileErrorBoundary extends React.Component {
mx_EventTile_content: true, mx_EventTile_content: true,
mx_EventTile_tileError: true, mx_EventTile_tileError: true,
}; };
let submitLogsButton;
if (SdkConfig.get().bug_report_endpoint_url) {
submitLogsButton = <a onClick={this._onBugReport} href="#">
{_t("Submit logs")}
</a>;
}
return (<div className={classNames(classes)}> return (<div className={classNames(classes)}>
<div className="mx_EventTile_line"> <div className="mx_EventTile_line">
<span> <span>
{_t("Can't load this message")} {_t("Can't load this message")}
{ mxEvent && ` (${mxEvent.getType()})` } { mxEvent && ` (${mxEvent.getType()})` }
<a onClick={this._onBugReport} href="#"> { submitLogsButton }
{_t("Submit logs")}
</a>
</span> </span>
</div> </div>
</div>); </div>);

View file

@ -952,7 +952,6 @@ function useRoomPermissions(cli, room, user) {
const PowerLevelSection = ({user, room, roomPermissions, powerLevels}) => { const PowerLevelSection = ({user, room, roomPermissions, powerLevels}) => {
const [isEditing, setEditing] = useState(false); const [isEditing, setEditing] = useState(false);
if (room && user.roomId) { // is in room
if (isEditing) { if (isEditing) {
return (<PowerLevelEditor return (<PowerLevelEditor
user={user} room={room} roomPermissions={roomPermissions} user={user} room={room} roomPermissions={roomPermissions}
@ -974,9 +973,6 @@ const PowerLevelSection = ({user, room, roomPermissions, powerLevels}) => {
</div> </div>
); );
} }
} else {
return null;
}
}; };
const PowerLevelEditor = ({user, room, roomPermissions, onFinished}) => { const PowerLevelEditor = ({user, room, roomPermissions, onFinished}) => {
@ -1268,14 +1264,15 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
spinner = <Loader imgClassName="mx_ContextualMenu_spinner" />; spinner = <Loader imgClassName="mx_ContextualMenu_spinner" />;
} }
const memberDetails = ( let memberDetails;
<PowerLevelSection if (room && member.roomId) {
memberDetails = <PowerLevelSection
powerLevels={powerLevels} powerLevels={powerLevels}
user={member} user={member}
room={room} room={room}
roomPermissions={roomPermissions} roomPermissions={roomPermissions}
/> />;
); }
// only display the devices list if our client supports E2E // only display the devices list if our client supports E2E
const cryptoEnabled = cli.isCryptoEnabled(); const cryptoEnabled = cli.isCryptoEnabled();

View file

@ -386,6 +386,14 @@ export default class GeneralUserSettingsTab extends React.Component {
width="18" height="18" alt={_t("Warning")} /> width="18" height="18" alt={_t("Warning")} />
: null; : null;
let accountManagementSection;
if (SettingsStore.getValue(UIFeature.Deactivate)) {
accountManagementSection = <>
<div className="mx_SettingsTab_heading">{_t("Deactivate account")}</div>
{this._renderManagementSection()}
</>;
}
return ( return (
<div className="mx_SettingsTab"> <div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{_t("General")}</div> <div className="mx_SettingsTab_heading">{_t("General")}</div>
@ -395,8 +403,7 @@ export default class GeneralUserSettingsTab extends React.Component {
<div className="mx_SettingsTab_heading">{discoWarning} {_t("Discovery")}</div> <div className="mx_SettingsTab_heading">{discoWarning} {_t("Discovery")}</div>
{this._renderDiscoverySection()} {this._renderDiscoverySection()}
{this._renderIntegrationManagerSection() /* Has its own title */} {this._renderIntegrationManagerSection() /* Has its own title */}
<div className="mx_SettingsTab_heading">{_t("Deactivate account")}</div> { accountManagementSection }
{this._renderManagementSection()}
</div> </div>
); );
} }

View file

@ -329,6 +329,29 @@ export default class SecurityUserSettingsTab extends React.Component {
</div>; </div>;
} }
let privacySection;
if (Analytics.canEnable()) {
privacySection = <React.Fragment>
<div className="mx_SettingsTab_heading">{_t("Privacy")}</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Analytics")}</span>
<div className="mx_SettingsTab_subsectionText">
{_t(
"%(brand)s collects anonymous analytics to allow us to improve the application.",
{ brand },
)}
&nbsp;
{_t("Privacy is important to us, so we don't collect any personal or " +
"identifiable data for our analytics.")}
<AccessibleButton className="mx_SettingsTab_linkBtn" onClick={Analytics.showDetailsModal}>
{_t("Learn more about how we use analytics.")}
</AccessibleButton>
</div>
<SettingsFlag name="analyticsOptIn" level={SettingLevel.DEVICE} onChange={this._updateAnalytics} />
</div>
</React.Fragment>;
}
return ( return (
<div className="mx_SettingsTab mx_SecurityUserSettingsTab"> <div className="mx_SettingsTab mx_SecurityUserSettingsTab">
{warning} {warning}
@ -357,24 +380,7 @@ export default class SecurityUserSettingsTab extends React.Component {
{crossSigning} {crossSigning}
{this._renderCurrentDeviceInfo()} {this._renderCurrentDeviceInfo()}
</div> </div>
<div className="mx_SettingsTab_heading">{_t("Privacy")}</div> { privacySection }
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Analytics")}</span>
<div className='mx_SettingsTab_subsectionText'>
{_t(
"%(brand)s collects anonymous analytics to allow us to improve the application.",
{ brand },
)}
&nbsp;
{_t("Privacy is important to us, so we don't collect any personal or " +
"identifiable data for our analytics.")}
<AccessibleButton className="mx_SettingsTab_linkBtn" onClick={Analytics.showDetailsModal}>
{_t("Learn more about how we use analytics.")}
</AccessibleButton>
</div>
<SettingsFlag name='analyticsOptIn' level={SettingLevel.DEVICE}
onChange={this._updateAnalytics} />
</div>
<div className="mx_SettingsTab_heading">{_t("Advanced")}</div> <div className="mx_SettingsTab_heading">{_t("Advanced")}</div>
<div className="mx_SettingsTab_section"> <div className="mx_SettingsTab_section">
{this._renderIgnoredUsers()} {this._renderIgnoredUsers()}

View file

@ -832,9 +832,9 @@
"Account management": "Account management", "Account management": "Account management",
"Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!", "Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!",
"Deactivate Account": "Deactivate Account", "Deactivate Account": "Deactivate Account",
"Deactivate account": "Deactivate account",
"General": "General", "General": "General",
"Discovery": "Discovery", "Discovery": "Discovery",
"Deactivate account": "Deactivate account",
"Legal": "Legal", "Legal": "Legal",
"Credits": "Credits", "Credits": "Credits",
"For help with using %(brand)s, click <a>here</a>.": "For help with using %(brand)s, click <a>here</a>.", "For help with using %(brand)s, click <a>here</a>.": "For help with using %(brand)s, click <a>here</a>.",
@ -912,13 +912,13 @@
"Message search": "Message search", "Message search": "Message search",
"Cross-signing": "Cross-signing", "Cross-signing": "Cross-signing",
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.", "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.",
"Where youre logged in": "Where youre logged in",
"Manage the names of and sign out of your sessions below or <a>verify them in your User Profile</a>.": "Manage the names of and sign out of your sessions below or <a>verify them in your User Profile</a>.",
"A session's public name is visible to people you communicate with": "A session's public name is visible to people you communicate with",
"Privacy": "Privacy", "Privacy": "Privacy",
"%(brand)s collects anonymous analytics to allow us to improve the application.": "%(brand)s collects anonymous analytics to allow us to improve the application.", "%(brand)s collects anonymous analytics to allow us to improve the application.": "%(brand)s collects anonymous analytics to allow us to improve the application.",
"Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.", "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.",
"Learn more about how we use analytics.": "Learn more about how we use analytics.", "Learn more about how we use analytics.": "Learn more about how we use analytics.",
"Where youre logged in": "Where youre logged in",
"Manage the names of and sign out of your sessions below or <a>verify them in your User Profile</a>.": "Manage the names of and sign out of your sessions below or <a>verify them in your User Profile</a>.",
"A session's public name is visible to people you communicate with": "A session's public name is visible to people you communicate with",
"No media permissions": "No media permissions", "No media permissions": "No media permissions",
"You may need to manually permit %(brand)s to access your microphone/webcam": "You may need to manually permit %(brand)s to access your microphone/webcam", "You may need to manually permit %(brand)s to access your microphone/webcam": "You may need to manually permit %(brand)s to access your microphone/webcam",
"Missing media permissions, click the button below to request.": "Missing media permissions, click the button below to request.", "Missing media permissions, click the button below to request.": "Missing media permissions, click the button below to request.",
@ -1440,8 +1440,8 @@
"Click to view edits": "Click to view edits", "Click to view edits": "Click to view edits",
"Edited at %(date)s. Click to view edits.": "Edited at %(date)s. Click to view edits.", "Edited at %(date)s. Click to view edits.": "Edited at %(date)s. Click to view edits.",
"edited": "edited", "edited": "edited",
"Can't load this message": "Can't load this message",
"Submit logs": "Submit logs", "Submit logs": "Submit logs",
"Can't load this message": "Can't load this message",
"Failed to load group members": "Failed to load group members", "Failed to load group members": "Failed to load group members",
"Filter community members": "Filter community members", "Filter community members": "Filter community members",
"Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Are you sure you want to remove '%(roomName)s' from %(groupId)s?", "Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Are you sure you want to remove '%(roomName)s' from %(groupId)s?",
@ -2132,10 +2132,10 @@
"Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s", "Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s",
"Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other", "Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other",
"Failed to find the general chat for this community": "Failed to find the general chat for this community", "Failed to find the general chat for this community": "Failed to find the general chat for this community",
"Feedback": "Feedback",
"Notification settings": "Notification settings", "Notification settings": "Notification settings",
"Security & privacy": "Security & privacy", "Security & privacy": "Security & privacy",
"All settings": "All settings", "All settings": "All settings",
"Feedback": "Feedback",
"Community settings": "Community settings", "Community settings": "Community settings",
"User settings": "User settings", "User settings": "User settings",
"Switch to light mode": "Switch to light mode", "Switch to light mode": "Switch to light mode",

View file

@ -588,6 +588,7 @@ export const SETTINGS: {[setting: string]: ISetting} = {
"showCallButtonsInComposer": { "showCallButtonsInComposer": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
default: true, default: true,
controller: new UIFeatureController(UIFeature.Voip),
}, },
"e2ee.manuallyVerifyAllSessions": { "e2ee.manuallyVerifyAllSessions": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
@ -622,6 +623,26 @@ export const SETTINGS: {[setting: string]: ISetting} = {
supportedLevels: LEVELS_UI_FEATURE, supportedLevels: LEVELS_UI_FEATURE,
default: true, default: true,
}, },
[UIFeature.Voip]: {
supportedLevels: LEVELS_UI_FEATURE,
default: true,
},
[UIFeature.Feedback]: {
supportedLevels: LEVELS_UI_FEATURE,
default: true,
},
[UIFeature.Registration]: {
supportedLevels: LEVELS_UI_FEATURE,
default: true,
},
[UIFeature.PasswordReset]: {
supportedLevels: LEVELS_UI_FEATURE,
default: true,
},
[UIFeature.Deactivate]: {
supportedLevels: LEVELS_UI_FEATURE,
default: true,
},
[UIFeature.ShareQRCode]: { [UIFeature.ShareQRCode]: {
supportedLevels: LEVELS_UI_FEATURE, supportedLevels: LEVELS_UI_FEATURE,
default: true, default: true,

View file

@ -18,6 +18,11 @@ limitations under the License.
export enum UIFeature { export enum UIFeature {
URLPreviews = "UIFeature.urlPreviews", URLPreviews = "UIFeature.urlPreviews",
Widgets = "UIFeature.widgets", Widgets = "UIFeature.widgets",
Voip = "UIFeature.voip",
Feedback = "UIFeature.feedback",
Registration = "UIFeature.registration",
PasswordReset = "UIFeature.passwordReset",
Deactivate = "UIFeature.deactivate",
ShareQRCode = "UIFeature.shareQrCode", ShareQRCode = "UIFeature.shareQrCode",
ShareSocial = "UIFeature.shareSocial", ShareSocial = "UIFeature.shareSocial",
} }

View file

@ -18,6 +18,7 @@ limitations under the License.
import React from "react"; import React from "react";
import {Store} from 'flux/utils'; import {Store} from 'flux/utils';
import {MatrixError} from "matrix-js-sdk/src/http-api";
import dis from '../dispatcher/dispatcher'; import dis from '../dispatcher/dispatcher';
import {MatrixClientPeg} from '../MatrixClientPeg'; import {MatrixClientPeg} from '../MatrixClientPeg';
@ -26,6 +27,9 @@ import Modal from '../Modal';
import { _t } from '../languageHandler'; import { _t } from '../languageHandler';
import { getCachedRoomIDForAlias, storeRoomAliasInCache } from '../RoomAliasCache'; import { getCachedRoomIDForAlias, storeRoomAliasInCache } from '../RoomAliasCache';
import {ActionPayload} from "../dispatcher/payloads"; import {ActionPayload} from "../dispatcher/payloads";
import {retry} from "../utils/promise";
const NUM_JOIN_RETRY = 5;
const INITIAL_STATE = { const INITIAL_STATE = {
// Whether we're joining the currently viewed room (see isJoining()) // Whether we're joining the currently viewed room (see isJoining())
@ -259,24 +263,32 @@ class RoomViewStore extends Store<ActionPayload> {
}); });
} }
private joinRoom(payload: ActionPayload) { private async joinRoom(payload: ActionPayload) {
this.setState({ this.setState({
joining: true, joining: true,
}); });
MatrixClientPeg.get().joinRoom(
this.state.roomAlias || this.state.roomId, payload.opts, const cli = MatrixClientPeg.get();
).then(() => { const address = this.state.roomAlias || this.state.roomId;
try {
await retry<void, MatrixError>(() => cli.joinRoom(address, payload.opts), NUM_JOIN_RETRY, (err) => {
// if we received a Gateway timeout then retry
return err.httpStatus === 504;
});
// We do *not* clear the 'joining' flag because the Room object and/or our 'joined' member event may not // We do *not* clear the 'joining' flag because the Room object and/or our 'joined' member event may not
// have come down the sync stream yet, and that's the point at which we'd consider the user joined to the // have come down the sync stream yet, and that's the point at which we'd consider the user joined to the
// room. // room.
dis.dispatch({ action: 'join_room_ready' }); dis.dispatch({ action: 'join_room_ready' });
}, (err) => { } catch (err) {
dis.dispatch({ dis.dispatch({
action: 'join_room_error', action: 'join_room_error',
err: err, err: err,
}); });
let msg = err.message ? err.message : JSON.stringify(err); let msg = err.message ? err.message : JSON.stringify(err);
console.log("Failed to join room:", msg); console.log("Failed to join room:", msg);
if (err.name === "ConnectionError") { if (err.name === "ConnectionError") {
msg = _t("There was an error joining the room"); msg = _t("There was an error joining the room");
} else if (err.errcode === 'M_INCOMPATIBLE_ROOM_VERSION') { } else if (err.errcode === 'M_INCOMPATIBLE_ROOM_VERSION') {
@ -296,12 +308,13 @@ class RoomViewStore extends Store<ActionPayload> {
} }
} }
} }
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to join room', '', ErrorDialog, { Modal.createTrackedDialog('Failed to join room', '', ErrorDialog, {
title: _t("Failed to join room"), title: _t("Failed to join room"),
description: msg, description: msg,
}); });
}); }
} }
private getInvitingUserId(roomId: string): string { private getInvitingUserId(roomId: string): string {

View file

@ -68,3 +68,21 @@ export function allSettled<T>(promises: Promise<T>[]): Promise<Array<ISettledFul
})); }));
})); }));
} }
// Helper method to retry a Promise a given number of times or until a predicate fails
export async function retry<T, E extends Error>(fn: () => Promise<T>, num: number, predicate?: (e: E) => boolean) {
let lastErr: E;
for (let i = 0; i < num; i++) {
try {
const v = await fn();
// If `await fn()` throws then we won't reach here
return v;
} catch (err) {
if (predicate && !predicate(err)) {
throw err;
}
lastErr = err;
}
}
throw lastErr;
}