Merge remote-tracking branch 'origin/develop' into dbkr/hold_ui
This commit is contained in:
commit
31044b68d1
31 changed files with 692 additions and 120 deletions
|
@ -29,7 +29,7 @@ import LogoutDialog from "../views/dialogs/LogoutDialog";
|
|||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import {getCustomTheme} from "../../theme";
|
||||
import {getHostingLink} from "../../utils/HostingLink";
|
||||
import {ButtonEvent} from "../views/elements/AccessibleButton";
|
||||
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
import {getHomePageUrl} from "../../utils/pages";
|
||||
import { OwnProfileStore } from "../../stores/OwnProfileStore";
|
||||
|
@ -205,6 +205,16 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
this.setState({contextMenuPosition: null}); // also close the menu
|
||||
};
|
||||
|
||||
private onSignInClick = () => {
|
||||
dis.dispatch({ action: 'start_login' });
|
||||
this.setState({contextMenuPosition: null}); // also close the menu
|
||||
};
|
||||
|
||||
private onRegisterClick = () => {
|
||||
dis.dispatch({ action: 'start_registration' });
|
||||
this.setState({contextMenuPosition: null}); // also close the menu
|
||||
};
|
||||
|
||||
private onHomeClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
@ -261,10 +271,29 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
|
||||
const prototypeCommunityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
||||
|
||||
let hostingLink;
|
||||
let topSection;
|
||||
const signupLink = getHostingLink("user-context-menu");
|
||||
if (signupLink) {
|
||||
hostingLink = (
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
topSection = (
|
||||
<div className="mx_UserMenu_contextMenu_header mx_UserMenu_contextMenu_guestPrompts">
|
||||
{_t("Got an account? <a>Sign in</a>", {}, {
|
||||
a: sub => (
|
||||
<AccessibleButton kind="link" onClick={this.onSignInClick}>
|
||||
{sub}
|
||||
</AccessibleButton>
|
||||
),
|
||||
})}
|
||||
{_t("New here? <a>Create an account</a>", {}, {
|
||||
a: sub => (
|
||||
<AccessibleButton kind="link" onClick={this.onRegisterClick}>
|
||||
{sub}
|
||||
</AccessibleButton>
|
||||
),
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
} else if (signupLink) {
|
||||
topSection = (
|
||||
<div className="mx_UserMenu_contextMenu_header mx_UserMenu_contextMenu_hostingLink">
|
||||
{_t(
|
||||
"<a>Upgrade</a> to your own domain", {},
|
||||
|
@ -422,6 +451,20 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
</IconizedContextMenuOptionList>
|
||||
</React.Fragment>
|
||||
)
|
||||
} else if (MatrixClientPeg.get().isGuest()) {
|
||||
primaryOptionList = (
|
||||
<React.Fragment>
|
||||
<IconizedContextMenuOptionList>
|
||||
{ homeButton }
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconSettings"
|
||||
label={_t("Settings")}
|
||||
onClick={(e) => this.onSettingsOpen(e, null)}
|
||||
/>
|
||||
{ feedbackButton }
|
||||
</IconizedContextMenuOptionList>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const classes = classNames({
|
||||
|
@ -451,7 +494,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
/>
|
||||
</AccessibleTooltipButton>
|
||||
</div>
|
||||
{hostingLink}
|
||||
{topSection}
|
||||
{primaryOptionList}
|
||||
{secondarySection}
|
||||
</IconizedContextMenu>;
|
||||
|
|
|
@ -38,6 +38,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
|||
import RoomViewStore from "../../../stores/RoomViewStore";
|
||||
import {OwnProfileStore} from "../../../stores/OwnProfileStore";
|
||||
import { arrayFastClone } from "../../../utils/arrays";
|
||||
import { ElementWidget } from "../../../stores/widgets/StopGapWidget";
|
||||
|
||||
interface IProps {
|
||||
widgetDefinition: IModalWidgetOpenRequestData;
|
||||
|
@ -64,7 +65,7 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.widget = new Widget({
|
||||
this.widget = new ElementWidget({
|
||||
...this.props.widgetDefinition,
|
||||
creatorUserId: MatrixClientPeg.get().getUserId(),
|
||||
id: `modal_${this.props.sourceWidgetId}`,
|
||||
|
@ -161,7 +162,9 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
|
|||
this.state.messaging.notifyModalWidgetButtonClicked(def.id);
|
||||
};
|
||||
|
||||
return <AccessibleButton key={def.id} kind={kind} onClick={onClick}>
|
||||
const isDisabled = this.state.disabledButtonIds.includes(def.id);
|
||||
|
||||
return <AccessibleButton key={def.id} kind={kind} onClick={onClick} disabled={isDisabled}>
|
||||
{ def.label }
|
||||
</AccessibleButton>;
|
||||
});
|
||||
|
|
|
@ -17,18 +17,17 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import * as sdk from "../../../index";
|
||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||
import WidgetUtils from "../../../utils/WidgetUtils";
|
||||
import {SettingLevel} from "../../../settings/SettingLevel";
|
||||
import {Widget} from "matrix-widget-api";
|
||||
import {OIDCState, WidgetPermissionStore} from "../../../stores/widgets/WidgetPermissionStore";
|
||||
|
||||
export default class WidgetOpenIDPermissionsDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
widgetUrl: PropTypes.string.isRequired,
|
||||
widgetId: PropTypes.string.isRequired,
|
||||
isUserWidget: PropTypes.bool.isRequired,
|
||||
widget: PropTypes.objectOf(Widget).isRequired,
|
||||
widgetKind: PropTypes.string.isRequired, // WidgetKind from widget-api
|
||||
inRoomId: PropTypes.string,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
|
@ -51,16 +50,10 @@ export default class WidgetOpenIDPermissionsDialog extends React.Component {
|
|||
if (this.state.rememberSelection) {
|
||||
console.log(`Remembering ${this.props.widgetId} as allowed=${allowed} for OpenID`);
|
||||
|
||||
const currentValues = SettingsStore.getValue("widgetOpenIDPermissions");
|
||||
if (!currentValues.allow) currentValues.allow = [];
|
||||
if (!currentValues.deny) currentValues.deny = [];
|
||||
|
||||
const securityKey = WidgetUtils.getWidgetSecurityKey(
|
||||
this.props.widgetId,
|
||||
this.props.widgetUrl,
|
||||
this.props.isUserWidget);
|
||||
(allowed ? currentValues.allow : currentValues.deny).push(securityKey);
|
||||
SettingsStore.setValue("widgetOpenIDPermissions", null, SettingLevel.DEVICE, currentValues);
|
||||
WidgetPermissionStore.instance.setOIDCState(
|
||||
this.props.widget, this.props.widgetKind, this.props.inRoomId,
|
||||
allowed ? OIDCState.Allowed : OIDCState.Denied,
|
||||
);
|
||||
}
|
||||
|
||||
this.props.onFinished(allowed);
|
||||
|
@ -84,7 +77,7 @@ export default class WidgetOpenIDPermissionsDialog extends React.Component {
|
|||
"A widget located at %(widgetUrl)s would like to verify your identity. " +
|
||||
"By allowing this, the widget will be able to verify your user ID, but not " +
|
||||
"perform actions as you.", {
|
||||
widgetUrl: this.props.widgetUrl.split("?")[0],
|
||||
widgetUrl: this.props.widget.templateUrl.split("?")[0],
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
|
|
|
@ -375,17 +375,20 @@ export default class AppTile extends React.Component {
|
|||
</div>
|
||||
);
|
||||
|
||||
// all widgets can theoretically be allowed to remain on screen, so we wrap
|
||||
// them all in a PersistedElement from the get-go. If we wait, the iframe will
|
||||
// be re-mounted later, which means the widget has to start over, which is bad.
|
||||
if (!this.props.userWidget) {
|
||||
// All room widgets can theoretically be allowed to remain on screen, so we
|
||||
// wrap them all in a PersistedElement from the get-go. If we wait, the iframe
|
||||
// will be re-mounted later, which means the widget has to start over, which is
|
||||
// bad.
|
||||
|
||||
// Also wrap the PersistedElement in a div to fix the height, otherwise
|
||||
// AppTile's border is in the wrong place
|
||||
appTileBody = <div className="mx_AppTile_persistedWrapper">
|
||||
<PersistedElement persistKey={this._persistKey}>
|
||||
{appTileBody}
|
||||
</PersistedElement>
|
||||
</div>;
|
||||
// Also wrap the PersistedElement in a div to fix the height, otherwise
|
||||
// AppTile's border is in the wrong place
|
||||
appTileBody = <div className="mx_AppTile_persistedWrapper">
|
||||
<PersistedElement persistKey={this._persistKey}>
|
||||
{appTileBody}
|
||||
</PersistedElement>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -745,13 +745,22 @@ export default class EventTile extends React.Component {
|
|||
}
|
||||
|
||||
if (this.props.mxEvent.sender && avatarSize) {
|
||||
let member;
|
||||
// set member to receiver (target) if it is a 3PID invite
|
||||
// so that the correct avatar is shown as the text is
|
||||
// `$target accepted the invitation for $email`
|
||||
if (this.props.mxEvent.getContent().third_party_invite) {
|
||||
member = this.props.mxEvent.target;
|
||||
} else {
|
||||
member = this.props.mxEvent.sender;
|
||||
}
|
||||
avatar = (
|
||||
<div className="mx_EventTile_avatar">
|
||||
<MemberAvatar member={this.props.mxEvent.sender}
|
||||
width={avatarSize} height={avatarSize}
|
||||
viewUserOnClick={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_EventTile_avatar">
|
||||
<MemberAvatar member={member}
|
||||
width={avatarSize} height={avatarSize}
|
||||
viewUserOnClick={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -21,9 +21,18 @@ import PropTypes from 'prop-types';
|
|||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import Spinner from '../elements/Spinner';
|
||||
import withValidation from '../elements/Validation';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from "../../../index";
|
||||
import Modal from "../../../Modal";
|
||||
import PassphraseField from "../auth/PassphraseField";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
|
||||
const FIELD_OLD_PASSWORD = 'field_old_password';
|
||||
const FIELD_NEW_PASSWORD = 'field_new_password';
|
||||
const FIELD_NEW_PASSWORD_CONFIRM = 'field_new_password_confirm';
|
||||
|
||||
const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from offline slow-hash scenario.
|
||||
|
||||
export default class ChangePassword extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -63,6 +72,7 @@ export default class ChangePassword extends React.Component {
|
|||
}
|
||||
|
||||
state = {
|
||||
fieldValid: {},
|
||||
phase: ChangePassword.Phases.Edit,
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
|
@ -168,26 +178,84 @@ export default class ChangePassword extends React.Component {
|
|||
);
|
||||
};
|
||||
|
||||
markFieldValid(fieldID, valid) {
|
||||
const { fieldValid } = this.state;
|
||||
fieldValid[fieldID] = valid;
|
||||
this.setState({
|
||||
fieldValid,
|
||||
});
|
||||
}
|
||||
|
||||
onChangeOldPassword = (ev) => {
|
||||
this.setState({
|
||||
oldPassword: ev.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
onOldPasswordValidate = async fieldState => {
|
||||
const result = await this.validateOldPasswordRules(fieldState);
|
||||
this.markFieldValid(FIELD_OLD_PASSWORD, result.valid);
|
||||
return result;
|
||||
};
|
||||
|
||||
validateOldPasswordRules = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test: ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||
invalid: () => _t("Passwords can't be empty"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
onChangeNewPassword = (ev) => {
|
||||
this.setState({
|
||||
newPassword: ev.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
onNewPasswordValidate = result => {
|
||||
this.markFieldValid(FIELD_NEW_PASSWORD, result.valid);
|
||||
};
|
||||
|
||||
onChangeNewPasswordConfirm = (ev) => {
|
||||
this.setState({
|
||||
newPasswordConfirm: ev.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
onClickChange = (ev) => {
|
||||
onNewPasswordConfirmValidate = async fieldState => {
|
||||
const result = await this.validatePasswordConfirmRules(fieldState);
|
||||
this.markFieldValid(FIELD_NEW_PASSWORD_CONFIRM, result.valid);
|
||||
return result;
|
||||
};
|
||||
|
||||
validatePasswordConfirmRules = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test: ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||
invalid: () => _t("Confirm password"),
|
||||
},
|
||||
{
|
||||
key: "match",
|
||||
test({ value }) {
|
||||
return !value || value === this.state.newPassword;
|
||||
},
|
||||
invalid: () => _t("Passwords don't match"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
onClickChange = async (ev) => {
|
||||
ev.preventDefault();
|
||||
|
||||
const allFieldsValid = await this.verifyFieldsBeforeSubmit();
|
||||
if (!allFieldsValid) {
|
||||
CountlyAnalytics.instance.track("onboarding_registration_submit_failed");
|
||||
return;
|
||||
}
|
||||
|
||||
const oldPassword = this.state.oldPassword;
|
||||
const newPassword = this.state.newPassword;
|
||||
const confirmPassword = this.state.newPasswordConfirm;
|
||||
|
@ -201,9 +269,75 @@ export default class ChangePassword extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
// TODO: Live validation on `new pw == confirm pw`
|
||||
async verifyFieldsBeforeSubmit() {
|
||||
// Blur the active element if any, so we first run its blur validation,
|
||||
// which is less strict than the pass we're about to do below for all fields.
|
||||
const activeElement = document.activeElement;
|
||||
if (activeElement) {
|
||||
activeElement.blur();
|
||||
}
|
||||
|
||||
const fieldIDsInDisplayOrder = [
|
||||
FIELD_OLD_PASSWORD,
|
||||
FIELD_NEW_PASSWORD,
|
||||
FIELD_NEW_PASSWORD_CONFIRM,
|
||||
];
|
||||
|
||||
// Run all fields with stricter validation that no longer allows empty
|
||||
// values for required fields.
|
||||
for (const fieldID of fieldIDsInDisplayOrder) {
|
||||
const field = this[fieldID];
|
||||
if (!field) {
|
||||
continue;
|
||||
}
|
||||
// We must wait for these validations to finish before queueing
|
||||
// up the setState below so our setState goes in the queue after
|
||||
// all the setStates from these validate calls (that's how we
|
||||
// know they've finished).
|
||||
await field.validate({ allowEmpty: false });
|
||||
}
|
||||
|
||||
// Validation and state updates are async, so we need to wait for them to complete
|
||||
// first. Queue a `setState` callback and wait for it to resolve.
|
||||
await new Promise(resolve => this.setState({}, resolve));
|
||||
|
||||
if (this.allFieldsValid()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const invalidField = this.findFirstInvalidField(fieldIDsInDisplayOrder);
|
||||
|
||||
if (!invalidField) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Focus the first invalid field and show feedback in the stricter mode
|
||||
// that no longer allows empty values for required fields.
|
||||
invalidField.focus();
|
||||
invalidField.validate({ allowEmpty: false, focused: true });
|
||||
return false;
|
||||
}
|
||||
|
||||
allFieldsValid() {
|
||||
const keys = Object.keys(this.state.fieldValid);
|
||||
for (let i = 0; i < keys.length; ++i) {
|
||||
if (!this.state.fieldValid[keys[i]]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
findFirstInvalidField(fieldIDs) {
|
||||
for (const fieldID of fieldIDs) {
|
||||
if (!this.state.fieldValid[fieldID] && this[fieldID]) {
|
||||
return this[fieldID];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const rowClassName = this.props.rowClassName;
|
||||
const buttonClassName = this.props.buttonClassName;
|
||||
|
||||
|
@ -213,28 +347,35 @@ export default class ChangePassword extends React.Component {
|
|||
<form className={this.props.className} onSubmit={this.onClickChange}>
|
||||
<div className={rowClassName}>
|
||||
<Field
|
||||
ref={field => this[FIELD_OLD_PASSWORD] = field}
|
||||
type="password"
|
||||
label={_t('Current password')}
|
||||
value={this.state.oldPassword}
|
||||
onChange={this.onChangeOldPassword}
|
||||
onValidate={this.onOldPasswordValidate}
|
||||
/>
|
||||
</div>
|
||||
<div className={rowClassName}>
|
||||
<Field
|
||||
<PassphraseField
|
||||
fieldRef={field => this[FIELD_NEW_PASSWORD] = field}
|
||||
type="password"
|
||||
label={_t('New Password')}
|
||||
label='New Password'
|
||||
minScore={PASSWORD_MIN_SCORE}
|
||||
value={this.state.newPassword}
|
||||
autoFocus={this.props.autoFocusNewPasswordInput}
|
||||
onChange={this.onChangeNewPassword}
|
||||
onValidate={this.onNewPasswordValidate}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
<div className={rowClassName}>
|
||||
<Field
|
||||
ref={field => this[FIELD_NEW_PASSWORD_CONFIRM] = field}
|
||||
type="password"
|
||||
label={_t("Confirm password")}
|
||||
value={this.state.newPasswordConfirm}
|
||||
onChange={this.onChangeNewPasswordConfirm}
|
||||
onValidate={this.onNewPasswordConfirmValidate}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -394,7 +394,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
className="mx_AppearanceUserSettingsTab_AdvancedToggle"
|
||||
onClick={() => this.setState({showAdvanced: !this.state.showAdvanced})}
|
||||
>
|
||||
{this.state.showAdvanced ? "Hide advanced" : "Show advanced"}
|
||||
{this.state.showAdvanced ? _t("Hide advanced") : _t("Show advanced")}
|
||||
</div>;
|
||||
|
||||
let advanced: React.ReactNode;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue