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:
commit
ae44a6d1fa
18 changed files with 219 additions and 103 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,15 +1373,19 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
ready: true,
|
ready: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
cli.on('Call.incoming', function(call) {
|
|
||||||
// we dispatch this synchronously to make sure that the event
|
if (SettingsStore.getValue(UIFeature.Voip)) {
|
||||||
// handlers on the call are set up immediately (so that if
|
cli.on('Call.incoming', function(call) {
|
||||||
// we get an immediate hangup, we don't get a stuck call)
|
// we dispatch this synchronously to make sure that the event
|
||||||
dis.dispatch({
|
// handlers on the call are set up immediately (so that if
|
||||||
action: 'incoming_call',
|
// we get an immediate hangup, we don't get a stuck call)
|
||||||
call: call,
|
dis.dispatch({
|
||||||
}, true);
|
action: 'incoming_call',
|
||||||
});
|
call: call,
|
||||||
|
}, 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()}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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') }
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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 />,
|
||||||
));
|
));
|
||||||
tabs.push(new Tab(
|
|
||||||
USER_VOICE_TAB,
|
if (SettingsStore.getValue(UIFeature.Voip)) {
|
||||||
_td("Voice & Video"),
|
tabs.push(new Tab(
|
||||||
"mx_UserSettingsDialog_voiceIcon",
|
USER_VOICE_TAB,
|
||||||
<VoiceUserSettingsTab />,
|
_td("Voice & Video"),
|
||||||
));
|
"mx_UserSettingsDialog_voiceIcon",
|
||||||
|
<VoiceUserSettingsTab />,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
tabs.push(new Tab(
|
tabs.push(new Tab(
|
||||||
USER_SECURITY_TAB,
|
USER_SECURITY_TAB,
|
||||||
_td("Security & Privacy"),
|
_td("Security & Privacy"),
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>);
|
||||||
|
|
|
@ -952,30 +952,26 @@ 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}
|
onFinished={() => setEditing(false)} />);
|
||||||
onFinished={() => setEditing(false)} />);
|
|
||||||
} else {
|
|
||||||
const IconButton = sdk.getComponent('elements.IconButton');
|
|
||||||
const powerLevelUsersDefault = powerLevels.users_default || 0;
|
|
||||||
const powerLevel = parseInt(user.powerLevel, 10);
|
|
||||||
const modifyButton = roomPermissions.canEdit ?
|
|
||||||
(<IconButton icon="edit" onClick={() => setEditing(true)} />) : null;
|
|
||||||
const role = textualPowerLevel(powerLevel, powerLevelUsersDefault);
|
|
||||||
const label = _t("<strong>%(role)s</strong> in %(roomName)s",
|
|
||||||
{role, roomName: room.name},
|
|
||||||
{strong: label => <strong>{label}</strong>},
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<div className="mx_UserInfo_profileField">
|
|
||||||
<div className="mx_UserInfo_roleDescription">{label}{modifyButton}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return null;
|
const IconButton = sdk.getComponent('elements.IconButton');
|
||||||
|
const powerLevelUsersDefault = powerLevels.users_default || 0;
|
||||||
|
const powerLevel = parseInt(user.powerLevel, 10);
|
||||||
|
const modifyButton = roomPermissions.canEdit ?
|
||||||
|
(<IconButton icon="edit" onClick={() => setEditing(true)} />) : null;
|
||||||
|
const role = textualPowerLevel(powerLevel, powerLevelUsersDefault);
|
||||||
|
const label = _t("<strong>%(role)s</strong> in %(roomName)s",
|
||||||
|
{role, roomName: room.name},
|
||||||
|
{strong: label => <strong>{label}</strong>},
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div className="mx_UserInfo_profileField">
|
||||||
|
<div className="mx_UserInfo_roleDescription">{label}{modifyButton}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 },
|
||||||
|
)}
|
||||||
|
|
||||||
|
{_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 },
|
|
||||||
)}
|
|
||||||
|
|
||||||
{_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()}
|
||||||
|
|
|
@ -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 you’re logged in": "Where you’re 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 you’re logged in": "Where you’re 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",
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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",
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue