diff --git a/css/base.css b/css/base.css index 168d98a5..22c0b714 100644 --- a/css/base.css +++ b/css/base.css @@ -324,6 +324,11 @@ li.right:hover .menuTrigger { inset 0 -0.25rem 0.25rem -0.25rem rgba(0,0,0,0.15); } +/* Element which can take focus but shows no outline */ +.group-focus { + outline: none; +} + /* END generic text colors */ /* END of basic elements */ diff --git a/src/webapp.jsx b/src/webapp.jsx index 5f037b47..7250a69e 100644 --- a/src/webapp.jsx +++ b/src/webapp.jsx @@ -193,6 +193,21 @@ function base64DecodedLen(n) { return Math.floor(n / 4) * 3; } +// Re-encode string to standard base64 encoding with padding. +// The string may be base64-URL encoded without the padding. +function base64ReEncode(str) { + if (str) { + str = str.replace("-", "+").replace("_", "/"); + try { + str = btoa(atob(str)); + } catch(err) { + console.log("failed to base64 re-decode string"); + str = null; + } + } + return str; +} + // Convert uploaded image into a base64-encoded string possibly scaling // linear dimensions or constraining to a square. function imageFileScaledToBase64(file, width, height, forceSquare, onSuccess, onError) { @@ -752,6 +767,7 @@ class InPlaceEdit extends React.Component { this.handleKeyDown = this.handleKeyDown.bind(this); this.handleStartEditing = this.handleStartEditing.bind(this); this.handleEditingFinished = this.handleEditingFinished.bind(this); + this.handlePasswordFinished = this.handlePasswordFinished.bind(this); } static getDerivedStateFromProps(nextProps, prevState) { @@ -795,8 +811,17 @@ class InPlaceEdit extends React.Component { } } + handlePasswordFinished(value) { + this.setState({active: false}); + if (value && (value !== this.props.value)) { + this.props.onFinished(value); + } + } + render() { - if (!this.state.active) { + if (this.state.active) { + var fieldType = this.props.type || "text"; + } else { var spanText = this.props.type == "password" ? "••••••••" : this.state.value; var spanClass = "in-place-edit" + (this.props.readOnly ? " disabled" : ""); @@ -810,15 +835,25 @@ class InPlaceEdit extends React.Component { } return ( this.state.active ? - + (fieldType == "password" ? + + : + + ) : {spanText} @@ -855,24 +890,37 @@ class VisiblePassword extends React.PureComponent { } handleKeyDown(e) { - if (e.keyCode === 27) { + if (e.keyCode == 27) { // Escape pressed - this.setState({value: this.props.value, active: false}); - } else if (e.keyCode === 13) { + this.setState({value: this.props.value, visible: false}); + if (this.props.onFinished) { + this.props.onFinished(); + } + } else if (e.keyCode == 13) { // Enter pressed this.handleEditingFinished(); } } - handleEditingFinished() { - if (this.props.onFinished) { - this.props.onFinished(this.state.value); + handleEditingFinished(e) { + if (e) { + let currentTarget = e.currentTarget; + setTimeout(() => { + if (!currentTarget.contains(document.activeElement)) { + if (this.props.onFinished) { + this.props.onFinished(this.state.value); + } + } + }, 0); + } else if (this.props.onFinished) { + this.props.onFinished(this.state.value.trim()); } } render() { return ( -
+
+ onKeyDown={this.handleKeyDown} /> {this.state.visible ? 'visibility' : 'visibility_off'} @@ -1531,14 +1578,20 @@ class PasswordResetView extends React.PureComponent { this.handleSubmit = this.handleSubmit.bind(this); this.handleEmailChange = this.handleEmailChange.bind(this); + this.handlePasswordChange = this.handlePasswordChange.bind(this); + } + + componentDidMount() { + let parsed = parseUrlHash(window.location.hash); + this.setState({token: parsed.params.token, scheme: parsed.params.scheme}); } handleSubmit(e) { e.preventDefault(); - if (this.props.token) { - this.props.onSubmit(null, this.state.password.trim()); + if (this.state.token) { + this.props.onReset(this.state.scheme, this.state.password.trim(), this.state.token); } else { - this.props.onSubmit("email", this.state.email.trim()); + this.props.onRequest("email", this.state.email.trim()); } } @@ -1551,10 +1604,17 @@ class PasswordResetView extends React.PureComponent { } render() { - let buttonText = this.props.token ? "Reset" : "Send request"; + let reset = (this.state.token && this.state.scheme); return (
- {!this.props.token ? + {reset ? + + : - : - }
- +
); @@ -1832,7 +1885,8 @@ class SidepanelView extends React.Component { view === 'reset' ? : + onRequest={this.props.onPasswordResetRequest} + onReset={this.props.onResetPassword} /> : null}
); @@ -1890,8 +1944,8 @@ class EditAccountView extends React.Component { this.state = { fullName: me.public ? me.public.fn : undefined, avatar: makeImageUrl(me.public ? me.public.photo : null), - auth: defacs.auth, - anon: defacs.anon, + auth: defacs ? defacs.auth : null, + anon: defacs ? defacs.anon : null, tags: fnd.tags(), previousOnTags: fnd.onTagsUpdated }; @@ -4474,6 +4528,7 @@ class TinodeWeb extends React.Component { this.handleMemberUpdateRequest = this.handleMemberUpdateRequest.bind(this); this.handleValidateCredentialsRequest = this.handleValidateCredentialsRequest.bind(this); this.handlePasswordResetRequest = this.handlePasswordResetRequest.bind(this); + this.handleResetPassword = this.handleResetPassword.bind(this); } getBlankState() { @@ -4571,11 +4626,11 @@ class TinodeWeb extends React.Component { this.tinode.enableLogging(true, true); this.tinode.onConnect = this.handleConnected; this.tinode.onDisconnect = this.handleDisconnect; - let token, firebasePushToken; - if (localStorage.getObject("keep-logged-in")) { - token = localStorage.getObject("auth-token"); - } + let token = localStorage.getObject("keep-logged-in") ? + localStorage.getObject("auth-token") : undefined; + + let parsedNav = parseUrlHash(window.location.hash); if (token) { // When reading from storage, date is returned as string. token.expires = new Date(token.expires); @@ -4584,12 +4639,11 @@ class TinodeWeb extends React.Component { // Socket error this.handleError(err.message, "err"); }); - var parsed = parseUrlHash(window.location.hash); - delete parsed.params.info; - delete parsed.params.tab; - parsed.path[0] = ''; - navigateTo(composeUrlHash(parsed.path, parsed.params)); - } else { + delete parsedNav.params.info; + delete parsedNav.params.tab; + parsedNav.path[0] = ''; + navigateTo(composeUrlHash(parsedNav.path, parsedNav.params)); + } else if (!parsedNav.params.token) { navigateTo(""); } @@ -5365,7 +5419,7 @@ class TinodeWeb extends React.Component { // If already connected, connnect() will return a resolved promise. this.tinode.connect() .then(() => { - this.tinode.requestResetAuthSecret("basic", method, value) + return this.tinode.requestResetAuthSecret("basic", method, value) }) .catch((err) => { // Socket error @@ -5373,6 +5427,22 @@ class TinodeWeb extends React.Component { }); } + handleResetPassword(scheme, newPassword, token) { + token = base64ReEncode(token); + if (!token) { + this.handleError("Invalid security token", "err"); + } else { + this.tinode.connect() + .then(() => { + return this.tinode.updateAccountBasic(null, null, newPassword, {token: token}); + }) + .catch((err) => { + // Socket error + this.handleError(err.message, "err"); + }); + } + } + render() { return (
@@ -5430,7 +5500,8 @@ class TinodeWeb extends React.Component { onCancel={this.handleSidepanelCancel} onError={this.handleError} onValidateCredentials={this.handleValidateCredentialsRequest} - onPasswordReset={this.handlePasswordResetRequest} + onPasswordResetRequest={this.handlePasswordResetRequest} + onResetPassword={this.handleResetPassword} onInitFind={this.tnInitFind} searchResults={this.state.searchResults}