From 6cd07731c4b35bcaae70d026d3890f7b36a6e87e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 14 Oct 2017 14:37:47 -0600 Subject: [PATCH 001/119] Add MemberPresenceAvatar and control presence ourselves Includes rudimentary support for custom statuses and user-controlled status. Some minor tweaks have also been made to better control how we advertise our presence. Signed-off-by: Travis Ralston --- src/MatrixClientPeg.js | 1 + src/Presence.js | 39 ++++- .../views/avatars/MemberPresenceAvatar.js | 135 ++++++++++++++++++ src/components/views/rooms/MessageComposer.js | 4 +- 4 files changed, 173 insertions(+), 6 deletions(-) create mode 100644 src/components/views/avatars/MemberPresenceAvatar.js diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 0c3d5b3775..7a4f0b99b0 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -93,6 +93,7 @@ class MatrixClientPeg { const opts = utils.deepCopy(this.opts); // the react sdk doesn't work without this, so don't allow opts.pendingEventOrdering = "detached"; + opts.disablePresence = true; // we do this manually try { const promise = this.matrixClient.store.startup(); diff --git a/src/Presence.js b/src/Presence.js index fab518e1cb..2652c64c96 100644 --- a/src/Presence.js +++ b/src/Presence.js @@ -56,13 +56,27 @@ class Presence { return this.state; } + /** + * Get the current status message. + * @returns {String} the status message, may be null + */ + getStatusMessage() { + return this.statusMessage; + } + /** * Set the presence state. * If the state has changed, the Home Server will be notified. * @param {string} newState the new presence state (see PRESENCE enum) + * @param {String} statusMessage an optional status message for the presence + * @param {boolean} maintain true to have this status maintained by this tracker */ - setState(newState) { - if (newState === this.state) { + setState(newState, statusMessage=null, maintain=false) { + if (this.maintain) { + // Don't update presence if we're maintaining a particular status + return; + } + if (newState === this.state && statusMessage === this.statusMessage) { return; } if (PRESENCE_STATES.indexOf(newState) === -1) { @@ -72,21 +86,37 @@ class Presence { return; } const old_state = this.state; + const old_message = this.statusMessage; this.state = newState; + this.statusMessage = statusMessage; + this.maintain = maintain; if (MatrixClientPeg.get().isGuest()) { return; // don't try to set presence when a guest; it won't work. } + const updateContent = { + presence: this.state, + status_msg: this.statusMessage ? this.statusMessage : '', + }; + const self = this; - MatrixClientPeg.get().setPresence(this.state).done(function() { + MatrixClientPeg.get().setPresence(updateContent).done(function() { console.log("Presence: %s", newState); + + // We have to dispatch because the js-sdk is unreliable at telling us about our own presence + dis.dispatch({action: "self_presence_updated", statusInfo: updateContent}); }, function(err) { console.error("Failed to set presence: %s", err); self.state = old_state; + self.statusMessage = old_message; }); } + stopMaintainingStatus() { + this.maintain = false; + } + /** * Callback called when the user made no action on the page for UNAVAILABLE_TIME ms. * @private @@ -95,7 +125,8 @@ class Presence { this.setState("unavailable"); } - _onUserActivity() { + _onUserActivity(payload) { + if (payload.action === "sync_state" || payload.action === "self_presence_updated") return; this._resetTimer(); } diff --git a/src/components/views/avatars/MemberPresenceAvatar.js b/src/components/views/avatars/MemberPresenceAvatar.js new file mode 100644 index 0000000000..e90f2e3e62 --- /dev/null +++ b/src/components/views/avatars/MemberPresenceAvatar.js @@ -0,0 +1,135 @@ +/* + Copyright 2017 Travis Ralston + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +'use strict'; + +import React from "react"; +import * as sdk from "../../../index"; +import MatrixClientPeg from "../../../MatrixClientPeg"; +import AccessibleButton from '../elements/AccessibleButton'; +import Presence from "../../../Presence"; +import dispatcher from "../../../dispatcher"; + +module.exports = React.createClass({ + displayName: 'MemberPresenceAvatar', + + propTypes: { + member: React.PropTypes.object.isRequired, + width: React.PropTypes.number, + height: React.PropTypes.number, + resizeMethod: React.PropTypes.string, + }, + + getDefaultProps: function() { + return { + width: 40, + height: 40, + resizeMethod: 'crop', + }; + }, + + getInitialState: function() { + const presenceState = this.props.member.user.presence; + return { + status: presenceState, + }; + }, + + componentWillMount: function() { + MatrixClientPeg.get().on("User.presence", this.onUserPresence); + this.dispatcherRef = dispatcher.register(this.onAction); + }, + + componentWillUnmount: function() { + if (MatrixClientPeg.get()) { + MatrixClientPeg.get().removeListener("User.presence", this.onUserPresence); + } + dispatcher.unregister(this.dispatcherRef); + }, + + onAction: function(payload) { + if (payload.action !== "self_presence_updated") return; + if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) return; + this.setState({ + status: payload.statusInfo.presence, + message: payload.statusInfo.status_msg, + }); + }, + + onUserPresence: function(event, user) { + if (user.userId !== MatrixClientPeg.get().getUserId()) return; + this.setState({ + status: user.presence, + message: user.presenceStatusMsg, + }); + }, + + onClick: function() { + if (Presence.getState() === "online") { + Presence.setState("unavailable", "This is a message", true); + } else { + Presence.stopMaintainingStatus(); + } + console.log("CLICK"); + + const presenceState = this.props.member.user.presence; + const presenceLastActiveAgo = this.props.member.user.lastActiveAgo; + const presenceLastTs = this.props.member.user.lastPresenceTs; + const presenceCurrentlyActive = this.props.member.user.currentlyActive; + const presenceMessage = this.props.member.user.presenceStatusMsg; + + console.log({ + presenceState, + presenceLastActiveAgo, + presenceLastTs, + presenceCurrentlyActive, + presenceMessage, + }); + }, + + render: function() { + const MemberAvatar = sdk.getComponent("avatars.MemberAvatar"); + + let onClickFn = null; + if (this.props.member.userId === MatrixClientPeg.get().getUserId()) { + onClickFn = this.onClick; + } + + const avatarNode = ( + + ); + const statusNode = ( + + ); + + let avatar = ( +
+ {avatarNode} + {statusNode} +
+ ); + if (onClickFn) { + avatar = ( + + {avatarNode} + {statusNode} + + ); + } + return avatar; + }, +}); diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 8e27520d89..d06cd76bdb 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -238,7 +238,7 @@ export default class MessageComposer extends React.Component { render() { const me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId); const uploadInputStyle = {display: 'none'}; - const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); + const MemberPresenceAvatar = sdk.getComponent('avatars.MemberPresenceAvatar'); const TintableSvg = sdk.getComponent("elements.TintableSvg"); const MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput"); @@ -246,7 +246,7 @@ export default class MessageComposer extends React.Component { controls.push(
- +
, ); From 0b20681f6a2963a260f4c2031123a5b3975995d4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 14 Oct 2017 19:13:46 -0600 Subject: [PATCH 002/119] Put presence management behind a labs setting Signed-off-by: Travis Ralston --- src/UserSettingsStore.js | 4 ++++ src/components/views/avatars/MemberPresenceAvatar.js | 9 ++++++++- src/i18n/strings/en_EN.json | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index b274e6a594..cb4d184eff 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -34,6 +34,10 @@ const FEATURES = [ id: 'feature_pinning', name: _td("Message Pinning"), }, + { + id: 'feature_presence_management', + name: _td("Presence Management"), + }, ]; export default { diff --git a/src/components/views/avatars/MemberPresenceAvatar.js b/src/components/views/avatars/MemberPresenceAvatar.js index e90f2e3e62..486688250e 100644 --- a/src/components/views/avatars/MemberPresenceAvatar.js +++ b/src/components/views/avatars/MemberPresenceAvatar.js @@ -22,6 +22,7 @@ import MatrixClientPeg from "../../../MatrixClientPeg"; import AccessibleButton from '../elements/AccessibleButton'; import Presence from "../../../Presence"; import dispatcher from "../../../dispatcher"; +import UserSettingsStore from "../../../UserSettingsStore"; module.exports = React.createClass({ displayName: 'MemberPresenceAvatar', @@ -112,10 +113,16 @@ module.exports = React.createClass({ ); - const statusNode = ( + let statusNode = ( ); + // LABS: Disable presence management functions for now + if (!UserSettingsStore.isFeatureEnabled("feature_presence_management")) { + statusNode = null; + onClickFn = null; + } + let avatar = (
{avatarNode} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index df236636a2..1a9e59e3cd 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -620,6 +620,7 @@ "Cancel": "Cancel", "or": "or", "Message Pinning": "Message Pinning", + "Presence Management": "Presence Management", "Active call": "Active call", "Monday": "Monday", "Tuesday": "Tuesday", From 788e16a716dcef588f5d73cfc799df3dd91972c4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 14 Oct 2017 20:23:50 -0600 Subject: [PATCH 003/119] Linting Signed-off-by: Travis Ralston --- src/components/views/avatars/MemberPresenceAvatar.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/views/avatars/MemberPresenceAvatar.js b/src/components/views/avatars/MemberPresenceAvatar.js index 486688250e..8005dcd405 100644 --- a/src/components/views/avatars/MemberPresenceAvatar.js +++ b/src/components/views/avatars/MemberPresenceAvatar.js @@ -111,10 +111,10 @@ module.exports = React.createClass({ const avatarNode = ( + resizeMethod={this.props.resizeMethod} /> ); let statusNode = ( - + ); // LABS: Disable presence management functions for now @@ -125,15 +125,15 @@ module.exports = React.createClass({ let avatar = (
- {avatarNode} - {statusNode} + { avatarNode } + { statusNode }
); if (onClickFn) { avatar = ( - {avatarNode} - {statusNode} + { avatarNode } + { statusNode } ); } From 03800b747608873f242446a330ba2387db2f871d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 14 Oct 2017 21:43:47 -0600 Subject: [PATCH 004/119] Support more positioning options on context menus Signed-off-by: Travis Ralston --- src/components/structures/ContextualMenu.js | 50 ++++++++++++++------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/src/components/structures/ContextualMenu.js b/src/components/structures/ContextualMenu.js index c3ad7f9cd1..3c2308e6a7 100644 --- a/src/components/structures/ContextualMenu.js +++ b/src/components/structures/ContextualMenu.js @@ -33,6 +33,7 @@ module.exports = { menuHeight: React.PropTypes.number, chevronOffset: React.PropTypes.number, menuColour: React.PropTypes.string, + chevronFace: React.PropTypes.string, // top, bottom, left, right }, getOrCreateContainer: function() { @@ -58,12 +59,30 @@ module.exports = { } }; - const position = { - top: props.top, - }; + const position = {}; + let chevronFace = null; + + if (props.top) { + position.top = props.top; + } else { + position.bottom = props.bottom; + } + + if (props.left) { + position.left = props.left; + chevronFace = 'left'; + } else { + position.right = props.right; + chevronFace = 'right'; + } const chevronOffset = {}; - if (props.chevronOffset) { + if (props.chevronFace) { + chevronFace = props.chevronFace; + } + if (chevronFace === 'top' || chevronFace === 'bottom') { + chevronOffset.left = props.chevronOffset; + } else { chevronOffset.top = props.chevronOffset; } @@ -74,28 +93,27 @@ module.exports = { .mx_ContextualMenu_chevron_left:after { border-right-color: ${props.menuColour}; } - .mx_ContextualMenu_chevron_right:after { border-left-color: ${props.menuColour}; } + .mx_ContextualMenu_chevron_top:after { + border-left-color: ${props.menuColour}; + } + .mx_ContextualMenu_chevron_bottom:after { + border-left-color: ${props.menuColour}; + } `; } - let chevron = null; - if (props.left) { - chevron =
; - position.left = props.left; - } else { - chevron =
; - position.right = props.right; - } - + const chevron =
; const className = 'mx_ContextualMenu_wrapper'; const menuClasses = classNames({ 'mx_ContextualMenu': true, - 'mx_ContextualMenu_left': props.left, - 'mx_ContextualMenu_right': !props.left, + 'mx_ContextualMenu_left': chevronFace === 'left', + 'mx_ContextualMenu_right': chevronFace === 'right', + 'mx_ContextualMenu_top': chevronFace === 'top', + 'mx_ContextualMenu_bottom': chevronFace === 'bottom', }); const menuStyle = {}; From c4837172821bed0be652abd64663c46c031f60c0 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 14 Oct 2017 21:44:07 -0600 Subject: [PATCH 005/119] Make onClick be a context menu for presence Signed-off-by: Travis Ralston --- .../views/avatars/MemberPresenceAvatar.js | 50 ++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/src/components/views/avatars/MemberPresenceAvatar.js b/src/components/views/avatars/MemberPresenceAvatar.js index 8005dcd405..de7c28154f 100644 --- a/src/components/views/avatars/MemberPresenceAvatar.js +++ b/src/components/views/avatars/MemberPresenceAvatar.js @@ -23,6 +23,7 @@ import AccessibleButton from '../elements/AccessibleButton'; import Presence from "../../../Presence"; import dispatcher from "../../../dispatcher"; import UserSettingsStore from "../../../UserSettingsStore"; +import * as ContextualMenu from "../../structures/ContextualMenu"; module.exports = React.createClass({ displayName: 'MemberPresenceAvatar', @@ -44,8 +45,10 @@ module.exports = React.createClass({ getInitialState: function() { const presenceState = this.props.member.user.presence; + const presenceMessage = this.props.member.user.presenceStatusMsg; return { status: presenceState, + message: presenceMessage, }; }, @@ -78,27 +81,38 @@ module.exports = React.createClass({ }); }, - onClick: function() { - if (Presence.getState() === "online") { - Presence.setState("unavailable", "This is a message", true); - } else { - Presence.stopMaintainingStatus(); - } - console.log("CLICK"); + onStatusChange: function(newStatus) { + console.log(this.state); + console.log(newStatus); + }, - const presenceState = this.props.member.user.presence; - const presenceLastActiveAgo = this.props.member.user.lastActiveAgo; - const presenceLastTs = this.props.member.user.lastPresenceTs; - const presenceCurrentlyActive = this.props.member.user.currentlyActive; - const presenceMessage = this.props.member.user.presenceStatusMsg; + onClick: function(e) { + const PresenceContextMenu = sdk.getComponent('context_menus.PresenceContextMenu'); + const elementRect = e.target.getBoundingClientRect(); - console.log({ - presenceState, - presenceLastActiveAgo, - presenceLastTs, - presenceCurrentlyActive, - presenceMessage, + // The window X and Y offsets are to adjust position when zoomed in to page + const x = (elementRect.left + window.pageXOffset) - (elementRect.width / 2) + 3; + const chevronOffset = 12; + let y = elementRect.top + (elementRect.height / 2) + window.pageYOffset; + y = y - (chevronOffset + 4); // where 4 is 1/4 the height of the chevron + + const self = this; + ContextualMenu.createMenu(PresenceContextMenu, { + chevronOffset: chevronOffset, + chevronFace: 'bottom', + left: x, + top: y, + menuWidth: 300, + currentStatus: this.state.status, + onChange: this.onStatusChange, }); + + e.stopPropagation(); + // const presenceState = this.props.member.user.presence; + // const presenceLastActiveAgo = this.props.member.user.lastActiveAgo; + // const presenceLastTs = this.props.member.user.lastPresenceTs; + // const presenceCurrentlyActive = this.props.member.user.currentlyActive; + // const presenceMessage = this.props.member.user.presenceStatusMsg; }, render: function() { From 7307bc412f794e06f4035c9210e6394f9158ce17 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 14 Oct 2017 23:16:12 -0600 Subject: [PATCH 006/119] Respond to updates from presence context menu Signed-off-by: Travis Ralston --- src/components/views/avatars/MemberPresenceAvatar.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/views/avatars/MemberPresenceAvatar.js b/src/components/views/avatars/MemberPresenceAvatar.js index de7c28154f..19342f3492 100644 --- a/src/components/views/avatars/MemberPresenceAvatar.js +++ b/src/components/views/avatars/MemberPresenceAvatar.js @@ -82,8 +82,10 @@ module.exports = React.createClass({ }, onStatusChange: function(newStatus) { - console.log(this.state); - console.log(newStatus); + Presence.stopMaintainingStatus(); + if (newStatus === "online") { + Presence.setState(newStatus); + } else Presence.setState(newStatus, null, true); }, onClick: function(e) { @@ -96,13 +98,12 @@ module.exports = React.createClass({ let y = elementRect.top + (elementRect.height / 2) + window.pageYOffset; y = y - (chevronOffset + 4); // where 4 is 1/4 the height of the chevron - const self = this; ContextualMenu.createMenu(PresenceContextMenu, { chevronOffset: chevronOffset, chevronFace: 'bottom', left: x, top: y, - menuWidth: 300, + menuWidth: 125, currentStatus: this.state.status, onChange: this.onStatusChange, }); From 37fd19290f0cdbe07052cee2b31e9696d69a6a47 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 20 Oct 2017 18:42:29 +0100 Subject: [PATCH 007/119] concept of default theme --- src/components/structures/MatrixChat.js | 2 +- src/components/structures/UserSettings.js | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 3fa628b8a3..9e476714ec 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -888,7 +888,7 @@ module.exports = React.createClass({ */ _onSetTheme: function(theme) { if (!theme) { - theme = 'light'; + theme = this.props.config.default_theme || 'light'; } // look for the stylesheet elements. diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index b69bea9282..e98bb844cc 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -179,6 +179,11 @@ const THEMES = [ label: _td('Dark theme'), value: 'dark', }, + { + id: 'theme', + label: _td('Status.im theme'), + value: 'status', + }, ]; const IgnoredUser = React.createClass({ @@ -279,7 +284,7 @@ module.exports = React.createClass({ const syncedSettings = UserSettingsStore.getSyncedSettings(); if (!syncedSettings.theme) { - syncedSettings.theme = 'light'; + syncedSettings.theme = SdkConfig.get().default_theme || 'light'; } this._syncedSettings = syncedSettings; From f09fbccc1917ff6d9da5d5169c795a9826c84381 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 23 Oct 2017 00:56:15 +0100 Subject: [PATCH 008/119] WIP --- src/UserSettingsStore.js | 15 ++++++ src/components/structures/login/Login.js | 37 ++++++++++----- src/components/views/login/LoginPageFooter.js | 30 ++++++++++++ src/components/views/login/LoginPageHeader.js | 47 +++++++++++++++++++ src/i18n/strings/en_EN.json | 1 + 5 files changed, 117 insertions(+), 13 deletions(-) create mode 100644 src/components/views/login/LoginPageFooter.js create mode 100644 src/components/views/login/LoginPageHeader.js diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index 68f463c373..ede5a157a8 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -180,6 +180,21 @@ export default { }); }, + getTheme: function() { + let syncedSettings; + let theme; + if (MatrixClientPeg.get()) { + syncedSettings = this.getSyncedSettings(); + } + if (!syncedSettings || !syncedSettings.theme) { + theme = SdkConfig.get().default_theme || 'light'; + } + else { + theme = syncedSettings.theme; + } + return theme; + }, + getSyncedSettings: function() { const event = MatrixClientPeg.get().getAccountData('im.vector.web.settings'); return event ? event.getContent() : {}; diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 8ee6eafad4..eb131989a8 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -329,13 +329,17 @@ module.exports = React.createClass({ render: function() { const Loader = sdk.getComponent("elements.Spinner"); + const LoginPageHeader = sdk.getComponent("login.LoginPageHeader"); + const LoginPageFooter = sdk.getComponent("login.LoginPageFooter"); const LoginHeader = sdk.getComponent("login.LoginHeader"); const LoginFooter = sdk.getComponent("login.LoginFooter"); const ServerConfig = sdk.getComponent("login.ServerConfig"); const loader = this.state.busy ?
: null; + const theme = UserSettingsStore.getTheme(); + let loginAsGuestJsx; - if (this.props.enableGuest) { + if (this.props.enableGuest && theme !== 'status') { loginAsGuestJsx = { _t('Login as guest') } @@ -343,42 +347,49 @@ module.exports = React.createClass({ } let returnToAppJsx; - if (this.props.onCancelClick) { + if (this.props.onCancelClick && theme !== 'status') { returnToAppJsx = { _t('Return to app') } ; } + let serverConfig; + if (theme !== 'status') { + serverConfig = ; + } + return (
+
-

{ _t('Sign in') } +

{ theme !== 'status' ? _t('Sign in') : _t('Sign in to get started') } { loader }

- { this.componentForStep(this.state.currentFlow) } -
{ this.state.errorText }
+ { this.componentForStep(this.state.currentFlow) } + { serverConfig } { _t('Create an account') } { loginAsGuestJsx } { returnToAppJsx } - { this._renderLanguageSetting() } + { theme !== 'status' ? this._renderLanguageSetting() : '' }
+
); }, diff --git a/src/components/views/login/LoginPageFooter.js b/src/components/views/login/LoginPageFooter.js new file mode 100644 index 0000000000..e4eca9b8b3 --- /dev/null +++ b/src/components/views/login/LoginPageFooter.js @@ -0,0 +1,30 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +'use strict'; + +import { _t } from '../../../languageHandler'; +import React from 'react'; + +module.exports = React.createClass({ + displayName: 'LoginPageFooter', + + render: function() { + return ( +
+ ); + }, +}); diff --git a/src/components/views/login/LoginPageHeader.js b/src/components/views/login/LoginPageHeader.js new file mode 100644 index 0000000000..bef7fa5e0b --- /dev/null +++ b/src/components/views/login/LoginPageHeader.js @@ -0,0 +1,47 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +'use strict'; + +import UserSettingsStore from '../../../UserSettingsStore'; + +const React = require('react'); + +module.exports = React.createClass({ + displayName: 'LoginPageHeader', + + render: function() { + let themeBranding; + if (UserSettingsStore.getTheme() === 'status') { + themeBranding =
+
+ Status +
+
+

Status Community Chat

+
+ A safer, decentralised communication platform powered by Riot +
+
+
; + } + else { + themeBranding =
; + } + + return themeBranding; + }, +}); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f071d42f56..db13972c9f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -475,6 +475,7 @@ "Sign in with": "Sign in with", "Email address": "Email address", "Sign in": "Sign in", + "Sign in to get started": "Sign in to get started", "If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?", "Email address (optional)": "Email address (optional)", "You are registering with %(SelectedTeamName)s": "You are registering with %(SelectedTeamName)s", From 26e8b2c1b3af8d6a2b8559ec3b4644d18746b7c2 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 24 Oct 2017 23:37:26 +0100 Subject: [PATCH 009/119] switch to a LoginPage wrapper component as it's much nicer in the CSS to wrap the LoginBox as needed rather than have separate header & footer divs floating above and below it which need to be correctly vertically centered --- .../structures/login/ForgotPassword.js | 5 +- src/components/structures/login/Login.js | 9 +-- .../structures/login/PostRegistration.js | 5 +- .../structures/login/Registration.js | 5 +- src/components/views/login/LoginPage.js | 58 +++++++++++++++++++ src/components/views/login/LoginPageFooter.js | 30 ---------- src/components/views/login/LoginPageHeader.js | 47 --------------- 7 files changed, 70 insertions(+), 89 deletions(-) create mode 100644 src/components/views/login/LoginPage.js delete mode 100644 src/components/views/login/LoginPageFooter.js delete mode 100644 src/components/views/login/LoginPageHeader.js diff --git a/src/components/structures/login/ForgotPassword.js b/src/components/structures/login/ForgotPassword.js index 3e76291d20..f14d64528f 100644 --- a/src/components/structures/login/ForgotPassword.js +++ b/src/components/structures/login/ForgotPassword.js @@ -154,6 +154,7 @@ module.exports = React.createClass({ }, render: function() { + const LoginPage = sdk.getComponent("login.LoginPage"); const LoginHeader = sdk.getComponent("login.LoginHeader"); const LoginFooter = sdk.getComponent("login.LoginFooter"); const ServerConfig = sdk.getComponent("login.ServerConfig"); @@ -233,12 +234,12 @@ module.exports = React.createClass({ return ( -
+
{ resetPasswordJsx }
-
+ ); }, }); diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index eb131989a8..f5f4f8cd69 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -329,8 +329,7 @@ module.exports = React.createClass({ render: function() { const Loader = sdk.getComponent("elements.Spinner"); - const LoginPageHeader = sdk.getComponent("login.LoginPageHeader"); - const LoginPageFooter = sdk.getComponent("login.LoginPageFooter"); + const LoginPage = sdk.getComponent("login.LoginPage"); const LoginHeader = sdk.getComponent("login.LoginHeader"); const LoginFooter = sdk.getComponent("login.LoginFooter"); const ServerConfig = sdk.getComponent("login.ServerConfig"); @@ -367,8 +366,7 @@ module.exports = React.createClass({ } return ( -
- +
@@ -389,8 +387,7 @@ module.exports = React.createClass({
- -
+ ); }, }); diff --git a/src/components/structures/login/PostRegistration.js b/src/components/structures/login/PostRegistration.js index 194995150c..184356e852 100644 --- a/src/components/structures/login/PostRegistration.js +++ b/src/components/structures/login/PostRegistration.js @@ -59,9 +59,10 @@ module.exports = React.createClass({ render: function() { const ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName'); const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar'); + const LoginPage = sdk.getComponent('login.LoginPage'); const LoginHeader = sdk.getComponent('login.LoginHeader'); return ( -
+
@@ -74,7 +75,7 @@ module.exports = React.createClass({ { this.state.errorString }
-
+ ); }, }); diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index db488ea237..8151c1e65c 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -322,6 +322,7 @@ module.exports = React.createClass({ render: function() { const LoginHeader = sdk.getComponent('login.LoginHeader'); const LoginFooter = sdk.getComponent('login.LoginFooter'); + const LoginPage = sdk.getComponent('login.LoginPage'); const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth'); const Spinner = sdk.getComponent("elements.Spinner"); const ServerConfig = sdk.getComponent('views.login.ServerConfig'); @@ -385,7 +386,7 @@ module.exports = React.createClass({ ); } return ( -
+
-
+ ); }, }); diff --git a/src/components/views/login/LoginPage.js b/src/components/views/login/LoginPage.js new file mode 100644 index 0000000000..a07997727b --- /dev/null +++ b/src/components/views/login/LoginPage.js @@ -0,0 +1,58 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +'use strict'; + +import UserSettingsStore from '../../../UserSettingsStore'; + +const React = require('react'); + +module.exports = React.createClass({ + displayName: 'LoginPage', + + render: function() { + if (UserSettingsStore.getTheme() === 'status') { + return ( +
+
+ Status +
+
+
+

Status Community Chat

+
+ A safer, decentralised communication platform powered by Riot +
+
+ { this.props.children } +
+

This channel is for our development community.

+

Interested in SNT and discussions on the cryptocurrency market?

+

Join Telegram Chat

+
+
+
+ ); + } + else { + return ( +
+ { this.props.children } +
+ ); + } + }, +}); diff --git a/src/components/views/login/LoginPageFooter.js b/src/components/views/login/LoginPageFooter.js deleted file mode 100644 index e4eca9b8b3..0000000000 --- a/src/components/views/login/LoginPageFooter.js +++ /dev/null @@ -1,30 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -'use strict'; - -import { _t } from '../../../languageHandler'; -import React from 'react'; - -module.exports = React.createClass({ - displayName: 'LoginPageFooter', - - render: function() { - return ( -
- ); - }, -}); diff --git a/src/components/views/login/LoginPageHeader.js b/src/components/views/login/LoginPageHeader.js deleted file mode 100644 index bef7fa5e0b..0000000000 --- a/src/components/views/login/LoginPageHeader.js +++ /dev/null @@ -1,47 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -'use strict'; - -import UserSettingsStore from '../../../UserSettingsStore'; - -const React = require('react'); - -module.exports = React.createClass({ - displayName: 'LoginPageHeader', - - render: function() { - let themeBranding; - if (UserSettingsStore.getTheme() === 'status') { - themeBranding =
-
- Status -
-
-

Status Community Chat

-
- A safer, decentralised communication platform powered by Riot -
-
-
; - } - else { - themeBranding =
; - } - - return themeBranding; - }, -}); From 59b2189909ba9ff074c613987e18b5ca01d3c591 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 24 Oct 2017 23:58:54 +0100 Subject: [PATCH 010/119] unbreak tests --- src/UserSettingsStore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index ede5a157a8..eca6d916a9 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -187,7 +187,7 @@ export default { syncedSettings = this.getSyncedSettings(); } if (!syncedSettings || !syncedSettings.theme) { - theme = SdkConfig.get().default_theme || 'light'; + theme = (SdkConfig.get() ? SdkConfig.get().default_theme : undefined) || 'light'; } else { theme = syncedSettings.theme; From adbea44d70826dad20ffbcdf50098ff807248173 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 25 Oct 2017 01:30:09 +0100 Subject: [PATCH 011/119] hide login options for status --- src/components/views/login/PasswordLogin.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 484ee01f4e..eb89bc00f1 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -20,7 +20,7 @@ import classNames from 'classnames'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import {field_input_incorrect} from '../../../UiEffects'; - +import UserSettingsStore from '../../../UserSettingsStore'; /** * A pure UI component which displays a username/password form. @@ -210,9 +210,10 @@ class PasswordLogin extends React.Component { const loginField = this.renderLoginField(this.state.loginType, matrixIdText === ''); - return ( -
-
+ const theme = UserSettingsStore.getTheme(); + let loginType; + if (theme !== 'status') { + loginType = (
{ _t('Phone') }
+ ); + } + + return ( +
+ + { loginType } { loginField } {this._passwordField = e;}} type="password" name="password" From 67cc02df3b3a938972fcf33969fa9bdb00b8e841 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 25 Oct 2017 01:37:06 +0100 Subject: [PATCH 012/119] hide header when error is up --- src/components/structures/login/Login.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index f5f4f8cd69..c31bdb1cca 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -354,6 +354,7 @@ module.exports = React.createClass({ } let serverConfig; + let header; if (theme !== 'status') { serverConfig = ; + + header =

{ _t('Sign in') }

; + } + else { + if (!this.state.errorText) { + header =

{ _t('Sign in to get started') }

; + } } return ( @@ -370,11 +378,9 @@ module.exports = React.createClass({
-

{ theme !== 'status' ? _t('Sign in') : _t('Sign in to get started') } - { loader } -

+ { header }
- { this.state.errorText } + { this.state.errorText }
{ this.componentForStep(this.state.currentFlow) } { serverConfig } From eb4b7c78a048b5ad2494e48ec6ab34c4d170bf52 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 25 Oct 2017 02:04:02 +0100 Subject: [PATCH 013/119] skin register screen --- .../structures/login/Registration.js | 46 +++++++++++------ .../views/login/RegistrationForm.js | 50 +++++++++++-------- 2 files changed, 59 insertions(+), 37 deletions(-) diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 8151c1e65c..15e6b536c4 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -26,6 +26,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg'; import RegistrationForm from '../../views/login/RegistrationForm'; import RtsClient from '../../../RtsClient'; import { _t } from '../../../languageHandler'; +import UserSettingsStore from '../../../UserSettingsStore'; const MIN_PASSWORD_LENGTH = 6; @@ -327,6 +328,8 @@ module.exports = React.createClass({ const Spinner = sdk.getComponent("elements.Spinner"); const ServerConfig = sdk.getComponent('views.login.ServerConfig'); + const theme = UserSettingsStore.getTheme(); + let registerBody; if (this.state.doingUIAuth) { registerBody = ( @@ -345,9 +348,19 @@ module.exports = React.createClass({ } else if (this.state.busy || this.state.teamServerBusy) { registerBody = ; } else { - let errorSection; - if (this.state.errorText) { - errorSection =
{ this.state.errorText }
; + let serverConfigSection; + if (theme !== 'status') { + serverConfigSection = ( + + ); } registerBody = (
@@ -363,16 +376,7 @@ module.exports = React.createClass({ onRegisterClick={this.onFormSubmit} onTeamSelected={this.onTeamSelected} /> - { errorSection } - + { serverConfigSection }
); } @@ -385,6 +389,17 @@ module.exports = React.createClass({ ); } + + let header; + let errorText; + if (theme === 'status' && this.state.errorText) { + header =
{ this.state.errorText }
; + } + else { + header =

{ _t('Create an account') }

; + errorText =
{ this.state.errorText }
; + } + return (
@@ -394,11 +409,12 @@ module.exports = React.createClass({ this.state.teamSelected.domain + "/icon.png" : null} /> -

{ _t('Create an account') }

+ { header } { registerBody } - { _t('I already have an account') } + { theme === 'status' ? _t('Sign in') : _t('I already have an account') } + { errorText } { returnToAppJsx }
diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index 9c7c75b125..7574b68418 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -22,6 +22,7 @@ import Email from '../../../email'; import { looksValid as phoneNumberLooksValid } from '../../../phonenumber'; import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; +import UserSettingsStore from '../../../UserSettingsStore'; const FIELD_EMAIL = 'field_email'; const FIELD_PHONE_COUNTRY = 'field_phone_country'; @@ -305,29 +306,34 @@ module.exports = React.createClass({ } } + const theme = UserSettingsStore.getTheme(); + const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); - const phoneSection = ( -
- - -
- ); + let phoneSection; + if (theme !== "status") { + phoneSection = ( +
+ + +
+ ); + } const registerButton = ( From ae40ef44606a34e362229a2b1746cd71456ee548 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 25 Oct 2017 02:18:14 +0100 Subject: [PATCH 014/119] fix error layouts --- src/components/structures/login/Login.js | 13 ++++++++++--- src/components/structures/login/Registration.js | 4 +++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index c31bdb1cca..44363d83ad 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -373,15 +373,22 @@ module.exports = React.createClass({ } } + let errorTextSection; + if (this.state.errorText) { + errorTextSection = ( +
+ { this.state.errorText } +
+ ); + } + return (
{ header } -
- { this.state.errorText } -
+ { errorTextSection } { this.componentForStep(this.state.currentFlow) } { serverConfig } diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 15e6b536c4..4e95b62be3 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -397,7 +397,9 @@ module.exports = React.createClass({ } else { header =

{ _t('Create an account') }

; - errorText =
{ this.state.errorText }
; + if (this.state.errorText) { + errorText =
{ this.state.errorText }
; + } } return ( From 6beb604cd0dd14172ab7655c4027597cb84d305f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 25 Oct 2017 02:31:30 +0100 Subject: [PATCH 015/119] fascist linting >:( --- src/UserSettingsStore.js | 3 ++- src/components/views/login/LoginPage.js | 8 +++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index eca6d916a9..ec6184f61a 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -189,7 +189,8 @@ export default { if (!syncedSettings || !syncedSettings.theme) { theme = (SdkConfig.get() ? SdkConfig.get().default_theme : undefined) || 'light'; } - else { + else + { theme = syncedSettings.theme; } return theme; diff --git a/src/components/views/login/LoginPage.js b/src/components/views/login/LoginPage.js index a07997727b..fa51426059 100644 --- a/src/components/views/login/LoginPage.js +++ b/src/components/views/login/LoginPage.js @@ -28,13 +28,14 @@ module.exports = React.createClass({ return (
- Status + Status
{ this.props.children } @@ -47,7 +48,8 @@ module.exports = React.createClass({
); } - else { + else + { return (
{ this.props.children } From 5c9970950bde6581fae7623fd8b656b47e81f2ba Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 25 Oct 2017 11:21:12 -0600 Subject: [PATCH 016/119] Undo i18n change to make merge easier Signed-off-by: Travis Ralston --- src/i18n/strings/en_EN.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 1a9e59e3cd..df236636a2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -620,7 +620,6 @@ "Cancel": "Cancel", "or": "or", "Message Pinning": "Message Pinning", - "Presence Management": "Presence Management", "Active call": "Active call", "Monday": "Monday", "Tuesday": "Tuesday", From 24000ee45689d397f35249ec8c66418e10b5741b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 25 Oct 2017 11:23:04 -0600 Subject: [PATCH 017/119] Re-add i18n for labs Signed-off-by: Travis Ralston --- src/i18n/strings/en_EN.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index fbff51299a..a33560a052 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -152,6 +152,7 @@ "%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s", "Communities": "Communities", "Message Pinning": "Message Pinning", + "Presence Management": "Presence Management", "%(displayName)s is typing": "%(displayName)s is typing", "%(names)s and one other are typing": "%(names)s and one other are typing", "%(names)s and %(count)s others are typing|other": "%(names)s and %(count)s others are typing", From b3a85934682b54e56734d6a26c909c25bffc355e Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 25 Oct 2017 22:48:27 +0100 Subject: [PATCH 018/119] make tinter coarsely theme aware --- src/Tinter.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Tinter.js b/src/Tinter.js index 6b23df8c9b..81cc48c7c0 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -1,5 +1,6 @@ /* Copyright 2015 OpenMarket Ltd +Copyright 2017 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,6 +15,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import UserSettingsStore from './UserSettingsStore'; + // FIXME: these vars should be bundled up and attached to // module.exports otherwise this will break when included by both // react-sdk and apps layered on top. @@ -178,8 +181,11 @@ module.exports = { } if (!primaryColor) { - primaryColor = "#76CFA6"; // Vector green - secondaryColor = "#EAF5F0"; // Vector light green + const theme = UserSettingsStore.getTheme(); + // FIXME: get this out of the theme CSS itself somehow? + // we could store it in a string CSS attrib somewhere we could sniff... + primaryColor = theme === 'status' ? "#6CC1F6" : "#76CFA6"; // Vector green + secondaryColor = theme === 'status' ? "#586C7B" : "#EAF5F0"; // Vector light green } if (!secondaryColor) { From ac7a94afb22a77f61973c2d5e5c3fe82fc4ecd29 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 26 Oct 2017 01:43:42 +0100 Subject: [PATCH 019/119] apply theme tint at launch --- src/components/structures/MatrixChat.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 9e476714ec..d4759a0e23 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -276,6 +276,9 @@ module.exports = React.createClass({ this._windowWidth = 10000; this.handleResize(); window.addEventListener('resize', this.handleResize); + + // check we have the right tint applied for this theme + Tinter.tint(); }, componentDidMount: function() { From 5a9dae5ff1f1e990e05c132a404502eb58ee82a5 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 26 Oct 2017 01:44:05 +0100 Subject: [PATCH 020/119] use generic 'text button' for delete buttons in UserSettings --- src/components/structures/UserSettings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index e98bb844cc..beaf1b04b5 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -205,7 +205,7 @@ const IgnoredUser = React.createClass({ render: function() { return (
  • - + { _t("Unignore") } { this.props.userId } From deb0f902e31541f83eeaa84b482a053c7e2006c1 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 26 Oct 2017 01:59:18 +0100 Subject: [PATCH 021/119] linting --- src/UserSettingsStore.js | 4 +--- src/components/structures/UserSettings.js | 2 +- src/components/views/login/LoginPage.js | 4 +--- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index 65973c0763..c1d77b120b 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -188,9 +188,7 @@ export default { } if (!syncedSettings || !syncedSettings.theme) { theme = (SdkConfig.get() ? SdkConfig.get().default_theme : undefined) || 'light'; - } - else - { + } else { theme = syncedSettings.theme; } return theme; diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index beaf1b04b5..30828bdc85 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -181,7 +181,7 @@ const THEMES = [ }, { id: 'theme', - label: _td('Status.im theme'), + label: 'Status.im theme', value: 'status', }, ]; diff --git a/src/components/views/login/LoginPage.js b/src/components/views/login/LoginPage.js index fa51426059..a1a5000227 100644 --- a/src/components/views/login/LoginPage.js +++ b/src/components/views/login/LoginPage.js @@ -47,9 +47,7 @@ module.exports = React.createClass({
  • ); - } - else - { + } else { return (
    { this.props.children } From 5c32b5b11a94cef53b7bf31f64eeef9f00446b7a Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 26 Oct 2017 02:10:03 +0100 Subject: [PATCH 022/119] fix i18n --- src/components/structures/UserSettings.js | 2 +- src/i18n/strings/en_EN.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 30828bdc85..beaf1b04b5 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -181,7 +181,7 @@ const THEMES = [ }, { id: 'theme', - label: 'Status.im theme', + label: _td('Status.im theme'), value: 'status', }, ]; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d69374acd1..cf6ee70ddf 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -904,5 +904,6 @@ "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.", "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.", "File to import": "File to import", - "Import": "Import" + "Import": "Import", + "Status.im theme": "Status.im theme" } From e6c3483c8b37df93f239aa1735d4858ac43a7d73 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 26 Oct 2017 09:58:05 +0100 Subject: [PATCH 023/119] Add correct telegram link --- src/components/views/login/LoginPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/login/LoginPage.js b/src/components/views/login/LoginPage.js index a1a5000227..32f090d361 100644 --- a/src/components/views/login/LoginPage.js +++ b/src/components/views/login/LoginPage.js @@ -42,7 +42,7 @@ module.exports = React.createClass({

    This channel is for our development community.

    Interested in SNT and discussions on the cryptocurrency market?

    -

    Join Telegram Chat

    +

    Join Telegram Chat

    From 14d9743e303097f26182f80d45d0e956a15cebbe Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 26 Oct 2017 17:22:17 +0100 Subject: [PATCH 024/119] unbreak reg --- src/components/views/login/RegistrationForm.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index 7574b68418..07418c4843 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -123,7 +123,7 @@ module.exports = React.createClass({ password: this.refs.password.value.trim(), email: email, phoneCountry: this.state.phoneCountry, - phoneNumber: this.refs.phoneNumber.value.trim(), + phoneNumber: this.refs.phoneNumber ? this.refs.phoneNumber.value.trim() : '', }); if (promise) { @@ -181,7 +181,7 @@ module.exports = React.createClass({ this.markFieldValid(field_id, emailValid, "RegistrationForm.ERR_EMAIL_INVALID"); break; case FIELD_PHONE_NUMBER: - const phoneNumber = this.refs.phoneNumber.value; + const phoneNumber = this.refs.phoneNumber ? this.refs.phoneNumber.value : ''; const phoneNumberValid = phoneNumber === '' || phoneNumberLooksValid(phoneNumber); this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID"); break; From 655d0c615a6bfb627440971cc337de307c22a024 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 26 Oct 2017 17:57:49 +0100 Subject: [PATCH 025/119] remove spurious Sign In button and legacy Return to App buttons --- src/components/structures/login/Login.js | 5 ++++- src/components/structures/login/Registration.js | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 44363d83ad..789b066074 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -346,12 +346,15 @@ module.exports = React.createClass({ } let returnToAppJsx; - if (this.props.onCancelClick && theme !== 'status') { + /* + // with the advent of ILAG I don't think we need this any more + if (this.props.onCancelClick) { returnToAppJsx = { _t('Return to app') } ; } + */ let serverConfig; let header; diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 4e95b62be3..5634c28197 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -382,6 +382,8 @@ module.exports = React.createClass({ } let returnToAppJsx; + /* + // with the advent of ILAG I don't think we need this any more if (this.props.onCancelClick) { returnToAppJsx = ( @@ -389,6 +391,7 @@ module.exports = React.createClass({ ); } + */ let header; let errorText; @@ -402,6 +405,15 @@ module.exports = React.createClass({ } } + let signIn; + if (!this.state.doingUIAuth) { + signIn = ( + + { theme === 'status' ? _t('Sign in') : _t('I already have an account') } + + ); + } + return (
    @@ -413,9 +425,7 @@ module.exports = React.createClass({ /> { header } { registerBody } - - { theme === 'status' ? _t('Sign in') : _t('I already have an account') } - + { signIn } { errorText } { returnToAppJsx } From 015aed05973e28c7b3f75ed415af0479f72277e2 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 27 Oct 2017 01:03:04 +0100 Subject: [PATCH 026/119] hide optionality of email for status --- src/components/views/login/RegistrationForm.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index 07418c4843..426cc41dea 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -274,10 +274,13 @@ module.exports = React.createClass({ render: function() { const self = this; + const theme = UserSettingsStore.getTheme(); + const emailPlaceholder = theme === 'status' ? _t("Email address") : _t("Email address (optional)"); + const emailSection = (
    Date: Fri, 27 Oct 2017 01:09:37 +0100 Subject: [PATCH 027/119] target blank for tg --- src/components/views/login/LoginPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/login/LoginPage.js b/src/components/views/login/LoginPage.js index 32f090d361..b5e2faedec 100644 --- a/src/components/views/login/LoginPage.js +++ b/src/components/views/login/LoginPage.js @@ -42,7 +42,7 @@ module.exports = React.createClass({

    This channel is for our development community.

    Interested in SNT and discussions on the cryptocurrency market?

    -

    Join Telegram Chat

    +

    Join Telegram Chat

    From 1bf3ef6de4128a1bf2211c5f6d11db6aed05869b Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 27 Oct 2017 01:23:50 +0100 Subject: [PATCH 028/119] fix password reset --- .../structures/login/ForgotPassword.js | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/src/components/structures/login/ForgotPassword.js b/src/components/structures/login/ForgotPassword.js index f14d64528f..0e49a59936 100644 --- a/src/components/structures/login/ForgotPassword.js +++ b/src/components/structures/login/ForgotPassword.js @@ -17,13 +17,14 @@ limitations under the License. 'use strict'; -const React = require('react'); +import React from 'react'; import { _t } from '../../../languageHandler'; -const sdk = require('../../../index'); -const Modal = require("../../../Modal"); -const MatrixClientPeg = require('../../../MatrixClientPeg'); +import sdk from '../../../index'; +import Modal from "../../../Modal"; +import MatrixClientPeg from "../../../MatrixClientPeg"; -const PasswordReset = require("../../../PasswordReset"); +import PasswordReset from "../../../PasswordReset"; +import UserSettingsStore from "../../../UserSettingsStore"; module.exports = React.createClass({ displayName: 'ForgotPassword', @@ -183,6 +184,22 @@ module.exports = React.createClass({
    ); } else { + let theme = UserSettingsStore.getTheme(); + + let serverConfigSection; + if (theme !== 'status') { + serverConfigSection = ( + + ); + } + resetPasswordJsx = (
    @@ -210,16 +227,7 @@ module.exports = React.createClass({
    - -
    -
    + { serverConfigSection } { _t('Return to login screen') } From e3f896c5e091cb0199afe9d9766c844afea7f3ba Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 27 Oct 2017 01:35:21 +0100 Subject: [PATCH 029/119] don't forget login prompt class --- src/components/structures/login/ForgotPassword.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/login/ForgotPassword.js b/src/components/structures/login/ForgotPassword.js index 0e49a59936..851d13b23e 100644 --- a/src/components/structures/login/ForgotPassword.js +++ b/src/components/structures/login/ForgotPassword.js @@ -167,7 +167,7 @@ module.exports = React.createClass({ resetPasswordJsx = ; } else if (this.state.progress === "sent_email") { resetPasswordJsx = ( -
    +
    { _t('An email has been sent to') } { this.state.email }. { _t("Once you've followed the link it contains, click below") }.
    +

    { _t('Your password has been reset') }.

    { _t('You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device') }.

    Date: Fri, 27 Oct 2017 14:23:16 +0100 Subject: [PATCH 030/119] resolve matrix.status.im v. matrix.org confusion --- src/components/structures/login/Login.js | 12 +++++++++++- src/components/views/login/PasswordLogin.js | 4 +++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 789b066074..fa1bd2e6b5 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -105,7 +105,17 @@ module.exports = React.createClass({ if (error.httpStatus == 400 && usingEmail) { errorText = _t('This Home Server does not support login using email address.'); } else if (error.httpStatus === 401 || error.httpStatus === 403) { - errorText = _t('Incorrect username and/or password.'); + const theme = UserSettingsStore.getTheme(); + if (theme === "status") { + errorText = ( +
    +
    Incorrect username and/or password.
    +
    Please note you are logging into the matrix.status.im server, not matrix.org.
    +
    + ); + } else { + errorText = _t('Incorrect username and/or password.'); + } } else { // other errors, not specific to doing a password login errorText = this._errorTextFromError(error); diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index eb89bc00f1..8af148dc6c 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -122,6 +122,8 @@ class PasswordLogin extends React.Component { mx_Login_field_disabled: disabled, }; + const theme = UserSettingsStore.getTheme(); + switch(loginType) { case PasswordLogin.LOGIN_FIELD_EMAIL: classes.mx_Login_email = true; @@ -144,7 +146,7 @@ class PasswordLogin extends React.Component { type="text" name="username" // make it a little easier for browser's remember-password onChange={this.onUsernameChanged} - placeholder={_t('User name')} + placeholder={theme === 'status' ? "Username on matrix.status.im" : _t("User name")} value={this.state.username} autoFocus disabled={disabled} From 8c3e5ebbad4ff59ce7f28f08b8942e0cf6355d89 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 22 Oct 2017 20:15:20 -0600 Subject: [PATCH 031/119] Create GranularSettingStore GranularSettingStore is a class to manage settings of varying granularity, such as URL previews at the device, room, and account levels. Signed-off-by: Travis Ralston --- src/GranularSettingStore.js | 426 ++++++++++++++++++++++++++++++++++++ 1 file changed, 426 insertions(+) create mode 100644 src/GranularSettingStore.js diff --git a/src/GranularSettingStore.js b/src/GranularSettingStore.js new file mode 100644 index 0000000000..9e8bbf093e --- /dev/null +++ b/src/GranularSettingStore.js @@ -0,0 +1,426 @@ +/* +Copyright 2017 Travis Ralston + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import Promise from 'bluebird'; +import MatrixClientPeg from './MatrixClientPeg'; + +const SETTINGS = [ + /* + // EXAMPLE SETTING + { + name: "my-setting", + type: "room", // or "account" + ignoreLevels: [], // options: "device", "room-account", "account", "room" + // "room-account" and "room" don't apply for `type: account`. + defaults: { + your: "defaults", + }, + }, + */ + + // TODO: Populate this +]; + +// This controls the priority of particular handlers. Handler order should match the +// documentation throughout this file, as should the `types`. The priority is directly +// related to the index in the map, where index 0 is highest preference. +const PRIORITY_MAP = [ + {level: 'device', settingClass: DeviceSetting, types: ['room', 'account']}, + {level: 'room-account', settingClass: RoomAccountSetting, types: ['room']}, + {level: 'account', settingClass: AccountSetting, types: ['room', 'account']}, + {level: 'room', settingClass: RoomSetting, types: ['room']}, + {level: 'default', settingClass: DefaultSetting, types: ['room', 'account']}, + + // TODO: Add support for 'legacy' settings (old events, etc) + // TODO: Labs handler? (or make UserSettingsStore use this as a backend) +]; + +/** + * Controls and manages Granular Settings through use of localStorage, account data, + * and room state. Granular Settings are user settings that can have overrides at + * particular levels, notably the device, account, and room level. With the topmost + * level being the preferred setting, the override procedure is: + * - localstorage (per-device) + * - room account data (per-account in room) + * - account data (per-account) + * - room state event (per-room) + * - default (defined by Riot) + * + * There are two types of settings: Account and Room. + * + * Account Settings use the same override procedure described above, but drop the room + * account data and room state event checks. Account Settings are best used for things + * like which theme the user would prefer. + * + * Room Settings use the exact override procedure described above. Room Settings are + * best suited for settings which room administrators may want to define a default + * for members of the room, such as the case is with URL previews. Room Settings may + * also elect to not allow the room state event check, allowing for per-room settings + * that are not defaulted by the room administrator. + */ +export default class GranularSettingStore { + /** + * Gets the content for an account setting. + * @param {string} name The name of the setting to lookup + * @returns {Promise<*>} Resolves to the content for the setting, or null if the + * value cannot be found. + */ + static getAccountSetting(name) { + const handlers = GranularSettingStore._getHandlers('account'); + const initFn = settingClass => new settingClass('account', name); + return GranularSettingStore._iterateHandlers(handlers, initFn); + } + + /** + * Gets the content for a room setting. + * @param {string} name The name of the setting to lookup + * @param {string} roomId The room ID to lookup the setting for + * @returns {Promise<*>} Resolves to the content for the setting, or null if the + * value cannot be found. + */ + static getRoomSetting(name, roomId) { + const handlers = GranularSettingStore._getHandlers('room'); + const initFn = settingClass => new settingClass('room', name, roomId); + return GranularSettingStore._iterateHandlers(handlers, initFn); + } + + static _iterateHandlers(handlers, initFn) { + let index = 0; + const wrapperFn = () => { + // If we hit the end with no result, return 'not found' + if (handlers.length >= index) return null; + + // Get the handler, increment the index, and create a setting object + const handler = handlers[index++]; + const setting = initFn(handler.settingClass); + + // Try to read the value of the setting. If we get nothing for a value, + // then try the next handler. Otherwise, return the value early. + return Promise.resolve(setting.getValue()).then(value => { + if (!value) return wrapperFn(); + return value; + }); + }; + return wrapperFn(); + } + + /** + * Sets the content for a particular account setting at a given level in the hierarchy. + * If the setting does not exist at the given level, this will attempt to create it. The + * default level may not be modified. + * @param {string} name The name of the setting. + * @param {string} level The level to set the value of. Either 'device' or 'account'. + * @param {Object} content The value for the setting, or null to clear the level's value. + * @returns {Promise} Resolves when completed + */ + static setAccountSetting(name, level, content) { + const handler = GranularSettingStore._getHandler('account', level); + if (!handler) throw new Error("Missing account setting handler for " + name + " at " + level); + + const setting = new handler.settingClass('account', name); + return Promise.resolve(setting.setValue(content)); + } + + /** + * Sets the content for a particular room setting at a given level in the hierarchy. If + * the setting does not exist at the given level, this will attempt to create it. The + * default level may not be modified. + * @param {string} name The name of the setting. + * @param {string} level The level to set the value of. One of 'device', 'room-account', + * 'account', or 'room'. + * @param {string} roomId The room ID to set the value of. + * @param {Object} content The value for the setting, or null to clear the level's value. + * @returns {Promise} Resolves when completed + */ + static setRoomSetting(name, level, roomId, content) { + const handler = GranularSettingStore._getHandler('room', level); + if (!handler) throw new Error("Missing room setting handler for " + name + " at " + level); + + const setting = new handler.settingClass('room', name, roomId); + return Promise.resolve(setting.setValue(content)); + } + + /** + * Checks to ensure the current user may set the given account setting. + * @param {string} name The name of the setting. + * @param {string} level The level to check at. Either 'device' or 'account'. + * @returns {boolean} Whether or not the current user may set the account setting value. + */ + static canSetAccountSetting(name, level) { + const handler = GranularSettingStore._getHandler('account', level); + if (!handler) return false; + + const setting = new handler.settingClass('account', name); + return setting.canSetValue(); + } + + /** + * Checks to ensure the current user may set the given room setting. + * @param {string} name The name of the setting. + * @param {string} level The level to check at. One of 'device', 'room-account', 'account', + * or 'room'. + * @param {string} roomId The room ID to check in. + * @returns {boolean} Whether or not the current user may set the room setting value. + */ + static canSetRoomSetting(name, level, roomId) { + const handler = GranularSettingStore._getHandler('room', level); + if (!handler) return false; + + const setting = new handler.settingClass('room', name, roomId); + return setting.canSetValue(); + } + + /** + * Removes an account setting at a given level, forcing the level to inherit from an + * earlier stage in the hierarchy. + * @param {string} name The name of the setting. + * @param {string} level The level to clear. Either 'device' or 'account'. + */ + static removeAccountSetting(name, level) { + // This is just a convenience method. + GranularSettingStore.setAccountSetting(name, level, null); + } + + /** + * Removes a room setting at a given level, forcing the level to inherit from an earlier + * stage in the hierarchy. + * @param {string} name The name of the setting. + * @param {string} level The level to clear. One of 'device', 'room-account', 'account', + * or 'room'. + * @param {string} roomId The room ID to clear the setting on. + */ + static removeRoomSetting(name, level, roomId) { + // This is just a convenience method. + GranularSettingStore.setRoomSetting(name, level, roomId, null); + } + + /** + * Determines whether or not a particular level is supported on the current platform. + * @param {string} level The level to check. One of 'device', 'room-account', 'account', + * 'room', or 'default'. + * @returns {boolean} Whether or not the level is supported. + */ + static isLevelSupported(level) { + return GranularSettingStore._getHandlersAtLevel(level).length > 0; + } + + static _getHandlersAtLevel(level) { + return PRIORITY_MAP.filter(h => h.level === level && h.settingClass.isSupported()); + } + + static _getHandlers(type) { + return PRIORITY_MAP.filter(h => { + if (!h.types.includes(type)) return false; + if (!h.settingClass.isSupported()) return false; + + return true; + }); + } + + static _getHandler(type, level) { + const handlers = GranularSettingStore._getHandlers(type); + return handlers.filter(h => h.level === level)[0]; + } +} + +// Validate of properties is assumed to be done well prior to instantiation of these classes, +// therefore these classes don't do any sanity checking. The following interface is assumed: +// constructor(type, name, roomId) - roomId may be null for type=='account' +// getValue() - returns a promise for the value. Falsey resolves are treated as 'not found'. +// setValue(content) - sets the new value for the setting. Falsey should remove the value. +// canSetValue() - returns true if the current user can set this setting. +// static isSupported() - returns true if the setting type is supported + +class DefaultSetting { + constructor(type, name, roomId = null) { + this.type = type; + this.name = name; + this.roomId = roomId; + } + + getValue() { + for (let setting of SETTINGS) { + if (setting.type === this.type && setting.name === this.name) { + return setting.defaults; + } + } + + return null; + } + + setValue() { + throw new Error("Operation not permitted: Cannot set value of a default setting."); + } + + canSetValue() { + // It's a default, so no, you can't. + return false; + } + + static isSupported() { + return true; // defaults are always accepted + } +} + +class DeviceSetting { + constructor(type, name, roomId = null) { + this.type = type; + this.name = name; + this.roomId = roomId; + } + + _getKey() { + return "mx_setting_" + this.name + "_" + this.type; + } + + getValue() { + if (!localStorage) return null; + const value = localStorage.getItem(this._getKey()); + if (!value) return null; + return JSON.parse(value); + } + + setValue(content) { + if (!localStorage) throw new Error("Operation not possible: No device storage available."); + if (!content) localStorage.removeItem(this._getKey()); + else localStorage.setItem(this._getKey(), JSON.stringify(content)); + } + + canSetValue() { + // The user likely has control over their own localstorage. + return true; + } + + static isSupported() { + // We can only do something if we have localstorage + return !!localStorage; + } +} + +class RoomAccountSetting { + constructor(type, name, roomId = null) { + this.type = type; + this.name = name; + this.roomId = roomId; + } + + _getEventType() { + return "im.vector.setting." + this.type + "." + this.name; + } + + getValue() { + if (!MatrixClientPeg.get()) return null; + + const room = MatrixClientPeg.getRoom(this.roomId); + if (!room) return null; + + const event = room.getAccountData(this._getEventType()); + if (!event || !event.getContent()) return null; + + return event.getContent(); + } + + setValue(content) { + if (!MatrixClientPeg.get()) throw new Error("Operation not possible: No client peg"); + return MatrixClientPeg.get().setRoomAccountData(this.roomId, this._getEventType(), content); + } + + canSetValue() { + // It's their own room account data, so they should be able to set it. + return true; + } + + static isSupported() { + // We can only do something if we have a client + return !!MatrixClientPeg.get(); + } +} + +class AccountSetting { + constructor(type, name, roomId = null) { + this.type = type; + this.name = name; + this.roomId = roomId; + } + + _getEventType() { + return "im.vector.setting." + this.type + "." + this.name; + } + + getValue() { + if (!MatrixClientPeg.get()) return null; + return MatrixClientPeg.getAccountData(this._getEventType()); + } + + setValue(content) { + if (!MatrixClientPeg.get()) throw new Error("Operation not possible: No client peg"); + return MatrixClientPeg.setAccountData(this._getEventType(), content); + } + + canSetValue() { + // It's their own account data, so they should be able to set it + return true; + } + + static isSupported() { + // We can only do something if we have a client + return !!MatrixClientPeg.get(); + } +} + +class RoomSetting { + constructor(type, name, roomId = null) { + this.type = type; + this.name = name; + this.roomId = roomId; + } + + _getEventType() { + return "im.vector.setting." + this.type + "." + this.name; + } + + getValue() { + if (!MatrixClientPeg.get()) return null; + + const room = MatrixClientPeg.get().getRoom(this.roomId); + if (!room) return null; + + const stateEvent = room.currentState.getStateEvents(this._getEventType(), ""); + if (!stateEvent || !stateEvent.getContent()) return null; + + return stateEvent.getContent(); + } + + setValue(content) { + if (!MatrixClientPeg.get()) throw new Error("Operation not possible: No client peg"); + + return MatrixClientPeg.get().sendStateEvent(this.roomId, this._getEventType(), content, ""); + } + + canSetValue() { + const cli = MatrixClientPeg.get(); + + const room = cli.getRoom(this.roomId); + if (!room) return false; // They're not in the room, likely. + + return room.currentState.maySendStateEvent(this._getEventType(), cli.getUserId()); + } + + static isSupported() { + // We can only do something if we have a client + return !!MatrixClientPeg.get(); + } +} From e02dcae3b65aec1d3bb8502ef9b815254d449ebd Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 22 Oct 2017 21:00:35 -0600 Subject: [PATCH 032/119] Change wording to better describe the class Signed-off-by: Travis Ralston --- src/GranularSettingStore.js | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/GranularSettingStore.js b/src/GranularSettingStore.js index 9e8bbf093e..c6e7de2b1e 100644 --- a/src/GranularSettingStore.js +++ b/src/GranularSettingStore.js @@ -49,27 +49,28 @@ const PRIORITY_MAP = [ ]; /** - * Controls and manages Granular Settings through use of localStorage, account data, - * and room state. Granular Settings are user settings that can have overrides at - * particular levels, notably the device, account, and room level. With the topmost - * level being the preferred setting, the override procedure is: - * - localstorage (per-device) - * - room account data (per-account in room) - * - account data (per-account) - * - room state event (per-room) - * - default (defined by Riot) + * Controls and manages application settings at different levels through a variety of + * backends. Settings may be overridden at each level to provide the user with more + * options for customization and tailoring of their experience. These levels are most + * notably at the device, room, and account levels. The preferred order of levels is: + * - per-device + * - per-account in a particular room + * - per-account + * - per-room + * - defaults (as defined here) * * There are two types of settings: Account and Room. * - * Account Settings use the same override procedure described above, but drop the room - * account data and room state event checks. Account Settings are best used for things - * like which theme the user would prefer. + * Account Settings use the same preferences described above, but do not look at the + * per-account in a particular room or the per-room levels. Account Settings are best + * used for things like which theme the user would prefer. * - * Room Settings use the exact override procedure described above. Room Settings are - * best suited for settings which room administrators may want to define a default - * for members of the room, such as the case is with URL previews. Room Settings may - * also elect to not allow the room state event check, allowing for per-room settings - * that are not defaulted by the room administrator. + * Room settings use the exact preferences described above. Room Settings are best + * suited for settings which room administrators may want to define a default for the + * room members, or where users may want an individual room to be different. Using the + * setting definitions, particular preferences may be excluded to prevent, for example, + * room administrators from defining that all messages should have timestamps when the + * user may not want that. An example of a Room Setting would be URL previews. */ export default class GranularSettingStore { /** From c43bf336a932654003612e5e73075223be8e1dca Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 22 Oct 2017 21:07:01 -0600 Subject: [PATCH 033/119] Appease the linter Signed-off-by: Travis Ralston --- src/GranularSettingStore.js | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/GranularSettingStore.js b/src/GranularSettingStore.js index c6e7de2b1e..96b06fc66c 100644 --- a/src/GranularSettingStore.js +++ b/src/GranularSettingStore.js @@ -81,7 +81,7 @@ export default class GranularSettingStore { */ static getAccountSetting(name) { const handlers = GranularSettingStore._getHandlers('account'); - const initFn = settingClass => new settingClass('account', name); + const initFn = (SettingClass) => new SettingClass('account', name); return GranularSettingStore._iterateHandlers(handlers, initFn); } @@ -94,7 +94,7 @@ export default class GranularSettingStore { */ static getRoomSetting(name, roomId) { const handlers = GranularSettingStore._getHandlers('room'); - const initFn = settingClass => new settingClass('room', name, roomId); + const initFn = (SettingClass) => new SettingClass('room', name, roomId); return GranularSettingStore._iterateHandlers(handlers, initFn); } @@ -110,7 +110,7 @@ export default class GranularSettingStore { // Try to read the value of the setting. If we get nothing for a value, // then try the next handler. Otherwise, return the value early. - return Promise.resolve(setting.getValue()).then(value => { + return Promise.resolve(setting.getValue()).then((value) => { if (!value) return wrapperFn(); return value; }); @@ -131,7 +131,8 @@ export default class GranularSettingStore { const handler = GranularSettingStore._getHandler('account', level); if (!handler) throw new Error("Missing account setting handler for " + name + " at " + level); - const setting = new handler.settingClass('account', name); + const SettingClass = handler.settingClass; + const setting = new SettingClass('account', name); return Promise.resolve(setting.setValue(content)); } @@ -150,7 +151,8 @@ export default class GranularSettingStore { const handler = GranularSettingStore._getHandler('room', level); if (!handler) throw new Error("Missing room setting handler for " + name + " at " + level); - const setting = new handler.settingClass('room', name, roomId); + const SettingClass = handler.settingClass; + const setting = new SettingClass('room', name, roomId); return Promise.resolve(setting.setValue(content)); } @@ -164,7 +166,8 @@ export default class GranularSettingStore { const handler = GranularSettingStore._getHandler('account', level); if (!handler) return false; - const setting = new handler.settingClass('account', name); + const SettingClass = handler.settingClass; + const setting = new SettingClass('account', name); return setting.canSetValue(); } @@ -180,7 +183,8 @@ export default class GranularSettingStore { const handler = GranularSettingStore._getHandler('room', level); if (!handler) return false; - const setting = new handler.settingClass('room', name, roomId); + const SettingClass = handler.settingClass; + const setting = new SettingClass('room', name, roomId); return setting.canSetValue(); } @@ -219,11 +223,11 @@ export default class GranularSettingStore { } static _getHandlersAtLevel(level) { - return PRIORITY_MAP.filter(h => h.level === level && h.settingClass.isSupported()); + return PRIORITY_MAP.filter((h) => h.level === level && h.settingClass.isSupported()); } static _getHandlers(type) { - return PRIORITY_MAP.filter(h => { + return PRIORITY_MAP.filter((h) => { if (!h.types.includes(type)) return false; if (!h.settingClass.isSupported()) return false; @@ -233,7 +237,7 @@ export default class GranularSettingStore { static _getHandler(type, level) { const handlers = GranularSettingStore._getHandlers(type); - return handlers.filter(h => h.level === level)[0]; + return handlers.filter((h) => h.level === level)[0]; } } @@ -253,7 +257,7 @@ class DefaultSetting { } getValue() { - for (let setting of SETTINGS) { + for (const setting of SETTINGS) { if (setting.type === this.type && setting.name === this.name) { return setting.defaults; } From 672fbb287316641bdcccf0b6314a40bd165f23a9 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 28 Oct 2017 18:33:38 +0100 Subject: [PATCH 034/119] hopefully fix NPE on toLowerCase --- src/components/structures/login/Registration.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 17204ee4e8..2403610416 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -303,7 +303,9 @@ module.exports = React.createClass({ } : {}; return this._matrixClient.register( - this.state.formVals.username.toLowerCase(), + (this.state.formVals.username ? + this.state.formVals.username.toLowerCase() : + this.state.formVals.username), this.state.formVals.password, undefined, // session id: included in the auth dict already auth, From 989bdcf5fbe800f4e64fa952608aeef6cfe3b2a3 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 28 Oct 2017 19:13:06 -0600 Subject: [PATCH 035/119] Rebuild SettingsStore to be better supported This does away with the room- and account-style settings, and just replaces them with `supportedLevels`. The handlers have also been moved out to be in better support of the other options, like SdkConfig and per-room-per-device. Signed-off-by: Travis Ralston --- src/GranularSettingStore.js | 431 --------------------- src/settings/AccountSettingsHandler.js | 47 +++ src/settings/ConfigSettingsHandler.js | 43 ++ src/settings/DefaultSettingsHandler.js | 51 +++ src/settings/DeviceSettingsHandler.js | 90 +++++ src/settings/RoomAccountSettingsHandler.js | 52 +++ src/settings/RoomDeviceSettingsHandler.js | 52 +++ src/settings/RoomSettingsHandler.js | 56 +++ src/settings/SettingsHandler.js | 70 ++++ src/settings/SettingsStore.js | 275 +++++++++++++ 10 files changed, 736 insertions(+), 431 deletions(-) delete mode 100644 src/GranularSettingStore.js create mode 100644 src/settings/AccountSettingsHandler.js create mode 100644 src/settings/ConfigSettingsHandler.js create mode 100644 src/settings/DefaultSettingsHandler.js create mode 100644 src/settings/DeviceSettingsHandler.js create mode 100644 src/settings/RoomAccountSettingsHandler.js create mode 100644 src/settings/RoomDeviceSettingsHandler.js create mode 100644 src/settings/RoomSettingsHandler.js create mode 100644 src/settings/SettingsHandler.js create mode 100644 src/settings/SettingsStore.js diff --git a/src/GranularSettingStore.js b/src/GranularSettingStore.js deleted file mode 100644 index 96b06fc66c..0000000000 --- a/src/GranularSettingStore.js +++ /dev/null @@ -1,431 +0,0 @@ -/* -Copyright 2017 Travis Ralston - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import Promise from 'bluebird'; -import MatrixClientPeg from './MatrixClientPeg'; - -const SETTINGS = [ - /* - // EXAMPLE SETTING - { - name: "my-setting", - type: "room", // or "account" - ignoreLevels: [], // options: "device", "room-account", "account", "room" - // "room-account" and "room" don't apply for `type: account`. - defaults: { - your: "defaults", - }, - }, - */ - - // TODO: Populate this -]; - -// This controls the priority of particular handlers. Handler order should match the -// documentation throughout this file, as should the `types`. The priority is directly -// related to the index in the map, where index 0 is highest preference. -const PRIORITY_MAP = [ - {level: 'device', settingClass: DeviceSetting, types: ['room', 'account']}, - {level: 'room-account', settingClass: RoomAccountSetting, types: ['room']}, - {level: 'account', settingClass: AccountSetting, types: ['room', 'account']}, - {level: 'room', settingClass: RoomSetting, types: ['room']}, - {level: 'default', settingClass: DefaultSetting, types: ['room', 'account']}, - - // TODO: Add support for 'legacy' settings (old events, etc) - // TODO: Labs handler? (or make UserSettingsStore use this as a backend) -]; - -/** - * Controls and manages application settings at different levels through a variety of - * backends. Settings may be overridden at each level to provide the user with more - * options for customization and tailoring of their experience. These levels are most - * notably at the device, room, and account levels. The preferred order of levels is: - * - per-device - * - per-account in a particular room - * - per-account - * - per-room - * - defaults (as defined here) - * - * There are two types of settings: Account and Room. - * - * Account Settings use the same preferences described above, but do not look at the - * per-account in a particular room or the per-room levels. Account Settings are best - * used for things like which theme the user would prefer. - * - * Room settings use the exact preferences described above. Room Settings are best - * suited for settings which room administrators may want to define a default for the - * room members, or where users may want an individual room to be different. Using the - * setting definitions, particular preferences may be excluded to prevent, for example, - * room administrators from defining that all messages should have timestamps when the - * user may not want that. An example of a Room Setting would be URL previews. - */ -export default class GranularSettingStore { - /** - * Gets the content for an account setting. - * @param {string} name The name of the setting to lookup - * @returns {Promise<*>} Resolves to the content for the setting, or null if the - * value cannot be found. - */ - static getAccountSetting(name) { - const handlers = GranularSettingStore._getHandlers('account'); - const initFn = (SettingClass) => new SettingClass('account', name); - return GranularSettingStore._iterateHandlers(handlers, initFn); - } - - /** - * Gets the content for a room setting. - * @param {string} name The name of the setting to lookup - * @param {string} roomId The room ID to lookup the setting for - * @returns {Promise<*>} Resolves to the content for the setting, or null if the - * value cannot be found. - */ - static getRoomSetting(name, roomId) { - const handlers = GranularSettingStore._getHandlers('room'); - const initFn = (SettingClass) => new SettingClass('room', name, roomId); - return GranularSettingStore._iterateHandlers(handlers, initFn); - } - - static _iterateHandlers(handlers, initFn) { - let index = 0; - const wrapperFn = () => { - // If we hit the end with no result, return 'not found' - if (handlers.length >= index) return null; - - // Get the handler, increment the index, and create a setting object - const handler = handlers[index++]; - const setting = initFn(handler.settingClass); - - // Try to read the value of the setting. If we get nothing for a value, - // then try the next handler. Otherwise, return the value early. - return Promise.resolve(setting.getValue()).then((value) => { - if (!value) return wrapperFn(); - return value; - }); - }; - return wrapperFn(); - } - - /** - * Sets the content for a particular account setting at a given level in the hierarchy. - * If the setting does not exist at the given level, this will attempt to create it. The - * default level may not be modified. - * @param {string} name The name of the setting. - * @param {string} level The level to set the value of. Either 'device' or 'account'. - * @param {Object} content The value for the setting, or null to clear the level's value. - * @returns {Promise} Resolves when completed - */ - static setAccountSetting(name, level, content) { - const handler = GranularSettingStore._getHandler('account', level); - if (!handler) throw new Error("Missing account setting handler for " + name + " at " + level); - - const SettingClass = handler.settingClass; - const setting = new SettingClass('account', name); - return Promise.resolve(setting.setValue(content)); - } - - /** - * Sets the content for a particular room setting at a given level in the hierarchy. If - * the setting does not exist at the given level, this will attempt to create it. The - * default level may not be modified. - * @param {string} name The name of the setting. - * @param {string} level The level to set the value of. One of 'device', 'room-account', - * 'account', or 'room'. - * @param {string} roomId The room ID to set the value of. - * @param {Object} content The value for the setting, or null to clear the level's value. - * @returns {Promise} Resolves when completed - */ - static setRoomSetting(name, level, roomId, content) { - const handler = GranularSettingStore._getHandler('room', level); - if (!handler) throw new Error("Missing room setting handler for " + name + " at " + level); - - const SettingClass = handler.settingClass; - const setting = new SettingClass('room', name, roomId); - return Promise.resolve(setting.setValue(content)); - } - - /** - * Checks to ensure the current user may set the given account setting. - * @param {string} name The name of the setting. - * @param {string} level The level to check at. Either 'device' or 'account'. - * @returns {boolean} Whether or not the current user may set the account setting value. - */ - static canSetAccountSetting(name, level) { - const handler = GranularSettingStore._getHandler('account', level); - if (!handler) return false; - - const SettingClass = handler.settingClass; - const setting = new SettingClass('account', name); - return setting.canSetValue(); - } - - /** - * Checks to ensure the current user may set the given room setting. - * @param {string} name The name of the setting. - * @param {string} level The level to check at. One of 'device', 'room-account', 'account', - * or 'room'. - * @param {string} roomId The room ID to check in. - * @returns {boolean} Whether or not the current user may set the room setting value. - */ - static canSetRoomSetting(name, level, roomId) { - const handler = GranularSettingStore._getHandler('room', level); - if (!handler) return false; - - const SettingClass = handler.settingClass; - const setting = new SettingClass('room', name, roomId); - return setting.canSetValue(); - } - - /** - * Removes an account setting at a given level, forcing the level to inherit from an - * earlier stage in the hierarchy. - * @param {string} name The name of the setting. - * @param {string} level The level to clear. Either 'device' or 'account'. - */ - static removeAccountSetting(name, level) { - // This is just a convenience method. - GranularSettingStore.setAccountSetting(name, level, null); - } - - /** - * Removes a room setting at a given level, forcing the level to inherit from an earlier - * stage in the hierarchy. - * @param {string} name The name of the setting. - * @param {string} level The level to clear. One of 'device', 'room-account', 'account', - * or 'room'. - * @param {string} roomId The room ID to clear the setting on. - */ - static removeRoomSetting(name, level, roomId) { - // This is just a convenience method. - GranularSettingStore.setRoomSetting(name, level, roomId, null); - } - - /** - * Determines whether or not a particular level is supported on the current platform. - * @param {string} level The level to check. One of 'device', 'room-account', 'account', - * 'room', or 'default'. - * @returns {boolean} Whether or not the level is supported. - */ - static isLevelSupported(level) { - return GranularSettingStore._getHandlersAtLevel(level).length > 0; - } - - static _getHandlersAtLevel(level) { - return PRIORITY_MAP.filter((h) => h.level === level && h.settingClass.isSupported()); - } - - static _getHandlers(type) { - return PRIORITY_MAP.filter((h) => { - if (!h.types.includes(type)) return false; - if (!h.settingClass.isSupported()) return false; - - return true; - }); - } - - static _getHandler(type, level) { - const handlers = GranularSettingStore._getHandlers(type); - return handlers.filter((h) => h.level === level)[0]; - } -} - -// Validate of properties is assumed to be done well prior to instantiation of these classes, -// therefore these classes don't do any sanity checking. The following interface is assumed: -// constructor(type, name, roomId) - roomId may be null for type=='account' -// getValue() - returns a promise for the value. Falsey resolves are treated as 'not found'. -// setValue(content) - sets the new value for the setting. Falsey should remove the value. -// canSetValue() - returns true if the current user can set this setting. -// static isSupported() - returns true if the setting type is supported - -class DefaultSetting { - constructor(type, name, roomId = null) { - this.type = type; - this.name = name; - this.roomId = roomId; - } - - getValue() { - for (const setting of SETTINGS) { - if (setting.type === this.type && setting.name === this.name) { - return setting.defaults; - } - } - - return null; - } - - setValue() { - throw new Error("Operation not permitted: Cannot set value of a default setting."); - } - - canSetValue() { - // It's a default, so no, you can't. - return false; - } - - static isSupported() { - return true; // defaults are always accepted - } -} - -class DeviceSetting { - constructor(type, name, roomId = null) { - this.type = type; - this.name = name; - this.roomId = roomId; - } - - _getKey() { - return "mx_setting_" + this.name + "_" + this.type; - } - - getValue() { - if (!localStorage) return null; - const value = localStorage.getItem(this._getKey()); - if (!value) return null; - return JSON.parse(value); - } - - setValue(content) { - if (!localStorage) throw new Error("Operation not possible: No device storage available."); - if (!content) localStorage.removeItem(this._getKey()); - else localStorage.setItem(this._getKey(), JSON.stringify(content)); - } - - canSetValue() { - // The user likely has control over their own localstorage. - return true; - } - - static isSupported() { - // We can only do something if we have localstorage - return !!localStorage; - } -} - -class RoomAccountSetting { - constructor(type, name, roomId = null) { - this.type = type; - this.name = name; - this.roomId = roomId; - } - - _getEventType() { - return "im.vector.setting." + this.type + "." + this.name; - } - - getValue() { - if (!MatrixClientPeg.get()) return null; - - const room = MatrixClientPeg.getRoom(this.roomId); - if (!room) return null; - - const event = room.getAccountData(this._getEventType()); - if (!event || !event.getContent()) return null; - - return event.getContent(); - } - - setValue(content) { - if (!MatrixClientPeg.get()) throw new Error("Operation not possible: No client peg"); - return MatrixClientPeg.get().setRoomAccountData(this.roomId, this._getEventType(), content); - } - - canSetValue() { - // It's their own room account data, so they should be able to set it. - return true; - } - - static isSupported() { - // We can only do something if we have a client - return !!MatrixClientPeg.get(); - } -} - -class AccountSetting { - constructor(type, name, roomId = null) { - this.type = type; - this.name = name; - this.roomId = roomId; - } - - _getEventType() { - return "im.vector.setting." + this.type + "." + this.name; - } - - getValue() { - if (!MatrixClientPeg.get()) return null; - return MatrixClientPeg.getAccountData(this._getEventType()); - } - - setValue(content) { - if (!MatrixClientPeg.get()) throw new Error("Operation not possible: No client peg"); - return MatrixClientPeg.setAccountData(this._getEventType(), content); - } - - canSetValue() { - // It's their own account data, so they should be able to set it - return true; - } - - static isSupported() { - // We can only do something if we have a client - return !!MatrixClientPeg.get(); - } -} - -class RoomSetting { - constructor(type, name, roomId = null) { - this.type = type; - this.name = name; - this.roomId = roomId; - } - - _getEventType() { - return "im.vector.setting." + this.type + "." + this.name; - } - - getValue() { - if (!MatrixClientPeg.get()) return null; - - const room = MatrixClientPeg.get().getRoom(this.roomId); - if (!room) return null; - - const stateEvent = room.currentState.getStateEvents(this._getEventType(), ""); - if (!stateEvent || !stateEvent.getContent()) return null; - - return stateEvent.getContent(); - } - - setValue(content) { - if (!MatrixClientPeg.get()) throw new Error("Operation not possible: No client peg"); - - return MatrixClientPeg.get().sendStateEvent(this.roomId, this._getEventType(), content, ""); - } - - canSetValue() { - const cli = MatrixClientPeg.get(); - - const room = cli.getRoom(this.roomId); - if (!room) return false; // They're not in the room, likely. - - return room.currentState.maySendStateEvent(this._getEventType(), cli.getUserId()); - } - - static isSupported() { - // We can only do something if we have a client - return !!MatrixClientPeg.get(); - } -} diff --git a/src/settings/AccountSettingsHandler.js b/src/settings/AccountSettingsHandler.js new file mode 100644 index 0000000000..6352da5ccc --- /dev/null +++ b/src/settings/AccountSettingsHandler.js @@ -0,0 +1,47 @@ +/* +Copyright 2017 Travis Ralston + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import Promise from 'bluebird'; +import SettingsHandler from "./SettingsHandler"; +import MatrixClientPeg from '../MatrixClientPeg'; + +/** + * Gets and sets settings at the "account" level for the current user. + * This handler does not make use of the roomId parameter. + */ +export default class AccountSettingHandler extends SettingsHandler { + getValue(settingName, roomId) { + const value = MatrixClientPeg.get().getAccountData(this._getEventType(settingName)); + if (!value) return Promise.reject(); + return Promise.resolve(value); + } + + setValue(settingName, roomId, newValue) { + return MatrixClientPeg.get().setAccountData(this._getEventType(settingName), newValue); + } + + canSetValue(settingName, roomId) { + return true; // It's their account, so they should be able to + } + + isSupported() { + return !!MatrixClientPeg.get(); + } + + _getEventType(settingName) { + return "im.vector.setting." + settingName; + } +} \ No newline at end of file diff --git a/src/settings/ConfigSettingsHandler.js b/src/settings/ConfigSettingsHandler.js new file mode 100644 index 0000000000..8f0cc9041b --- /dev/null +++ b/src/settings/ConfigSettingsHandler.js @@ -0,0 +1,43 @@ +/* +Copyright 2017 Travis Ralston + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import Promise from 'bluebird'; +import SettingsHandler from "./SettingsHandler"; +import SdkConfig from "../SdkConfig"; + +/** + * Gets and sets settings at the "config" level. This handler does not make use of the + * roomId parameter. + */ +export default class ConfigSettingsHandler extends SettingsHandler { + getValue(settingName, roomId) { + const settingsConfig = SdkConfig.get()["settingDefaults"]; + if (!settingsConfig || !settingsConfig[settingName]) return Promise.reject(); + return Promise.resolve(settingsConfig[settingName]); + } + + setValue(settingName, roomId, newValue) { + throw new Error("Cannot change settings at the config level"); + } + + canSetValue(settingName, roomId) { + return false; + } + + isSupported() { + return true; // SdkConfig is always there + } +} \ No newline at end of file diff --git a/src/settings/DefaultSettingsHandler.js b/src/settings/DefaultSettingsHandler.js new file mode 100644 index 0000000000..06937fd957 --- /dev/null +++ b/src/settings/DefaultSettingsHandler.js @@ -0,0 +1,51 @@ +/* +Copyright 2017 Travis Ralston + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import Promise from 'bluebird'; +import SettingsHandler from "./SettingsHandler"; + +/** + * Gets settings at the "default" level. This handler does not support setting values. + * This handler does not make use of the roomId parameter. + */ +export default class DefaultSettingsHandler extends SettingsHandler { + /** + * Creates a new default settings handler with the given defaults + * @param {object} defaults The default setting values, keyed by setting name. + */ + constructor(defaults) { + super(); + this._defaults = defaults; + } + + getValue(settingName, roomId) { + const value = this._defaults[settingName]; + if (!value) return Promise.reject(); + return Promise.resolve(value); + } + + setValue(settingName, roomId, newValue) { + throw new Error("Cannot set values on the default level handler"); + } + + canSetValue(settingName, roomId) { + return false; + } + + isSupported() { + return true; + } +} \ No newline at end of file diff --git a/src/settings/DeviceSettingsHandler.js b/src/settings/DeviceSettingsHandler.js new file mode 100644 index 0000000000..83cf88bcba --- /dev/null +++ b/src/settings/DeviceSettingsHandler.js @@ -0,0 +1,90 @@ +/* +Copyright 2017 Travis Ralston + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import Promise from 'bluebird'; +import SettingsHandler from "./SettingsHandler"; +import MatrixClientPeg from "../MatrixClientPeg"; + +/** + * Gets and sets settings at the "device" level for the current device. + * This handler does not make use of the roomId parameter. This handler + * will special-case features to support legacy settings. + */ +export default class DeviceSettingsHandler extends SettingsHandler { + /** + * Creates a new device settings handler + * @param {string[]} featureNames The names of known features. + */ + constructor(featureNames) { + super(); + this._featureNames = featureNames; + } + + getValue(settingName, roomId) { + if (this._featureNames.includes(settingName)) { + return Promise.resolve(this._readFeature(settingName)); + } + + const value = localStorage.getItem(this._getKey(settingName)); + if (!value) return Promise.reject(); + return Promise.resolve(value); + } + + setValue(settingName, roomId, newValue) { + if (this._featureNames.includes(settingName)) { + return Promise.resolve(this._writeFeature(settingName)); + } + + if (newValue === null) { + localStorage.removeItem(this._getKey(settingName)); + } else { + localStorage.setItem(this._getKey(settingName), newValue); + } + + return Promise.resolve(); + } + + canSetValue(settingName, roomId) { + return true; // It's their device, so they should be able to + } + + isSupported() { + return !!localStorage; + } + + _getKey(settingName) { + return "mx_setting_" + settingName; + } + + // Note: features intentionally don't use the same key as settings to avoid conflicts + // and to be backwards compatible. + + _readFeature(featureName) { + if (MatrixClientPeg.get() && MatrixClientPeg.get().isGuest()) { + // Guests should not have any labs features enabled. + return {enabled: false}; + } + + const value = localStorage.getItem("mx_labs_feature_" + featureName); + const enabled = value === "true"; + + return {enabled}; + } + + _writeFeature(featureName, enabled) { + localStorage.setItem("mx_labs_feature_" + featureName, enabled); + } +} \ No newline at end of file diff --git a/src/settings/RoomAccountSettingsHandler.js b/src/settings/RoomAccountSettingsHandler.js new file mode 100644 index 0000000000..7157d86c34 --- /dev/null +++ b/src/settings/RoomAccountSettingsHandler.js @@ -0,0 +1,52 @@ +/* +Copyright 2017 Travis Ralston + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import Promise from 'bluebird'; +import SettingsHandler from "./SettingsHandler"; +import MatrixClientPeg from '../MatrixClientPeg'; + +/** + * Gets and sets settings at the "room-account" level for the current user. + */ +export default class RoomAccountSettingsHandler extends SettingsHandler { + getValue(settingName, roomId) { + const room = MatrixClientPeg.get().getRoom(roomId); + if (!room) return Promise.reject(); + + const value = room.getAccountData(this._getEventType(settingName)); + if (!value) return Promise.reject(); + return Promise.resolve(value); + } + + setValue(settingName, roomId, newValue) { + return MatrixClientPeg.get().setRoomAccountData( + roomId, this._getEventType(settingName), newValue + ); + } + + canSetValue(settingName, roomId) { + const room = MatrixClientPeg.get().getRoom(roomId); + return !!room; // If they have the room, they can set their own account data + } + + isSupported() { + return !!MatrixClientPeg.get(); + } + + _getEventType(settingName) { + return "im.vector.setting." + settingName; + } +} \ No newline at end of file diff --git a/src/settings/RoomDeviceSettingsHandler.js b/src/settings/RoomDeviceSettingsHandler.js new file mode 100644 index 0000000000..fe477564f6 --- /dev/null +++ b/src/settings/RoomDeviceSettingsHandler.js @@ -0,0 +1,52 @@ +/* +Copyright 2017 Travis Ralston + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import Promise from 'bluebird'; +import SettingsHandler from "./SettingsHandler"; + +/** + * Gets and sets settings at the "room-device" level for the current device in a particular + * room. + */ +export default class RoomDeviceSettingsHandler extends SettingsHandler { + getValue(settingName, roomId) { + const value = localStorage.getItem(this._getKey(settingName, roomId)); + if (!value) return Promise.reject(); + return Promise.resolve(value); + } + + setValue(settingName, roomId, newValue) { + if (newValue === null) { + localStorage.removeItem(this._getKey(settingName, roomId)); + } else { + localStorage.setItem(this._getKey(settingName, roomId), newValue); + } + + return Promise.resolve(); + } + + canSetValue(settingName, roomId) { + return true; // It's their device, so they should be able to + } + + isSupported() { + return !!localStorage; + } + + _getKey(settingName, roomId) { + return "mx_setting_" + settingName + "_" + roomId; + } +} \ No newline at end of file diff --git a/src/settings/RoomSettingsHandler.js b/src/settings/RoomSettingsHandler.js new file mode 100644 index 0000000000..dcd7a76e87 --- /dev/null +++ b/src/settings/RoomSettingsHandler.js @@ -0,0 +1,56 @@ +/* +Copyright 2017 Travis Ralston + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import Promise from 'bluebird'; +import SettingsHandler from "./SettingsHandler"; +import MatrixClientPeg from '../MatrixClientPeg'; + +/** + * Gets and sets settings at the "room" level. + */ +export default class RoomSettingsHandler extends SettingsHandler { + getValue(settingName, roomId) { + const room = MatrixClientPeg.get().getRoom(roomId); + if (!room) return Promise.reject(); + + const event = room.currentState.getStateEvents(this._getEventType(settingName), ""); + if (!event || !event.getContent()) return Promise.reject(); + return Promise.resolve(event.getContent()); + } + + setValue(settingName, roomId, newValue) { + return MatrixClientPeg.get().sendStateEvent( + roomId, this._getEventType(settingName), newValue, "" + ); + } + + canSetValue(settingName, roomId) { + const cli = MatrixClientPeg.get(); + const room = cli.getRoom(roomId); + const eventType = this._getEventType(settingName); + + if (!room) return false; + return room.currentState.maySendStateEvent(eventType, cli.getUserId()); + } + + isSupported() { + return !!MatrixClientPeg.get(); + } + + _getEventType(settingName) { + return "im.vector.setting." + settingName; + } +} \ No newline at end of file diff --git a/src/settings/SettingsHandler.js b/src/settings/SettingsHandler.js new file mode 100644 index 0000000000..7387712367 --- /dev/null +++ b/src/settings/SettingsHandler.js @@ -0,0 +1,70 @@ +/* +Copyright 2017 Travis Ralston + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import Promise from 'bluebird'; + +/** + * Represents the base class for all level handlers. This class performs no logic + * and should be overridden. + */ +export default class SettingsHandler { + /** + * Gets the value for a particular setting at this level for a particular room. + * If no room is applicable, the roomId may be null. The roomId may not be + * applicable to this level and may be ignored by the handler. + * @param {string} settingName The name of the setting. + * @param {String} roomId The room ID to read from, may be null. + * @return {Promise} Resolves to the setting value. Rejected if the value + * could not be found. + */ + getValue(settingName, roomId) { + throw new Error("Operation not possible: getValue was not overridden"); + } + + /** + * Sets the value for a particular setting at this level for a particular room. + * If no room is applicable, the roomId may be null. The roomId may not be + * applicable to this level and may be ignored by the handler. Setting a value + * to null will cause the level to remove the value. The current user should be + * able to set the value prior to calling this. + * @param {string} settingName The name of the setting to change. + * @param {String} roomId The room ID to set the value in, may be null. + * @param {Object} newValue The new value for the setting, may be null. + * @return {Promise} Resolves when the setting has been saved. + */ + setValue(settingName, roomId, newValue) { + throw new Error("Operation not possible: setValue was not overridden"); + } + + /** + * Determines if the current user is able to set the value of the given setting + * in the given room at this level. + * @param {string} settingName The name of the setting to check. + * @param {String} roomId The room ID to check in, may be null + * @returns {boolean} True if the setting can be set by the user, false otherwise. + */ + canSetValue(settingName, roomId) { + return false; + } + + /** + * Determines if this level is supported on this device. + * @returns {boolean} True if this level is supported on the current device. + */ + isSupported() { + return false; + } +} \ No newline at end of file diff --git a/src/settings/SettingsStore.js b/src/settings/SettingsStore.js new file mode 100644 index 0000000000..eea91345d8 --- /dev/null +++ b/src/settings/SettingsStore.js @@ -0,0 +1,275 @@ +/* +Copyright 2017 Travis Ralston + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import Promise from 'bluebird'; +import DeviceSettingsHandler from "./DeviceSettingsHandler"; +import RoomDeviceSettingsHandler from "./RoomDeviceSettingsHandler"; +import DefaultSettingsHandler from "./DefaultSettingsHandler"; +import RoomAccountSettingsHandler from "./RoomAccountSettingsHandler"; +import AccountSettingsHandler from "./AccountSettingsHandler"; +import RoomSettingsHandler from "./RoomSettingsHandler"; +import ConfigSettingsHandler from "./ConfigSettingsHandler"; +import {_t, _td} from '../languageHandler'; +import SdkConfig from "../SdkConfig"; + +// Preset levels for room-based settings (eg: URL previews). +// Doesn't include 'room' because most settings don't need it. Use .concat('room') to add. +const LEVELS_PRESET_ROOM = ['device', 'room-device', 'room-account', 'account']; + +// Preset levels for account-based settings (eg: interface language). +const LEVELS_PRESET_ACCOUNT = ['device', 'account']; + +// Preset levels for features (labs) settings. +const LEVELS_PRESET_FEATURE = ['device']; + +const SETTINGS = { + "my-setting": { + isFeature: false, // optional + displayName: _td("Cool Name"), + supportedLevels: [ + // The order does not matter. + + "device", // Affects the current device only + "room-device", // Affects the current room on the current device + "room-account", // Affects the current room for the current account + "account", // Affects the current account + "room", // Affects the current room (controlled by room admins) + + // "default" and "config" are always supported and do not get listed here. + ], + defaults: { + your: "value", + }, + }, + + // TODO: Populate this +}; + +// Convert the above into simpler formats for the handlers +let defaultSettings = {}; +let featureNames = []; +for (let key of Object.keys(SETTINGS)) { + defaultSettings[key] = SETTINGS[key].defaults; + if (SETTINGS[key].isFeature) featureNames.push(key); +} + +const LEVEL_HANDLERS = { + "device": new DeviceSettingsHandler(featureNames), + "room-device": new RoomDeviceSettingsHandler(), + "room-account": new RoomAccountSettingsHandler(), + "account": new AccountSettingsHandler(), + "room": new RoomSettingsHandler(), + "config": new ConfigSettingsHandler(), + "default": new DefaultSettingsHandler(defaultSettings), +}; + +/** + * Controls and manages application settings by providing varying levels at which the + * setting value may be specified. The levels are then used to determine what the setting + * value should be given a set of circumstances. The levels, in priority order, are: + * - "device" - Values are determined by the current device + * - "room-device" - Values are determined by the current device for a particular room + * - "room-account" - Values are determined by the current account for a particular room + * - "account" - Values are determined by the current account + * - "room" - Values are determined by a particular room (by the room admins) + * - "config" - Values are determined by the config.json + * - "default" - Values are determined by the hardcoded defaults + * + * Each level has a different method to storing the setting value. For implementation + * specific details, please see the handlers. The "config" and "default" levels are + * both always supported on all platforms. All other settings should be guarded by + * isLevelSupported() prior to attempting to set the value. + * + * Settings can also represent features. Features are significant portions of the + * application that warrant a dedicated setting to toggle them on or off. Features are + * special-cased to ensure that their values respect the configuration (for example, a + * feature may be reported as disabled even though a user has specifically requested it + * be enabled). + */ +export default class SettingsStore { + /** + * Gets the translated display name for a given setting + * @param {string} settingName The setting to look up. + * @return {String} The display name for the setting, or null if not found. + */ + static getDisplayName(settingName) { + if (!SETTINGS[settingName] || !SETTINGS[settingName].displayName) return null; + return _t(SETTINGS[settingName].displayName); + } + + /** + * Determines if a setting is also a feature. + * @param {string} settingName The setting to look up. + * @return {boolean} True if the setting is a feature. + */ + static isFeature(settingName) { + if (!SETTINGS[settingName]) return false; + return SETTINGS[settingName].isFeature; + } + + /** + * Determines if a given feature is enabled. The feature given must be a known + * feature. + * @param {string} settingName The name of the setting that is a feature. + * @param {String} roomId The optional room ID to validate in, may be null. + * @return {boolean} True if the feature is enabled, false otherwise + */ + static isFeatureEnabled(settingName, roomId = null) { + if (!SettingsStore.isFeature(settingName)) { + throw new Error("Setting " + settingName + " is not a feature"); + } + + // Synchronously get the setting value (which should be {enabled: true/false}) + const value = Promise.coroutine(function* () { + return yield SettingsStore.getValue(settingName, roomId); + })(); + + return value.enabled; + } + + /** + * Gets the value of a setting. The room ID is optional if the setting is not to + * be applied to any particular room, otherwise it should be supplied. + * @param {string} settingName The name of the setting to read the value of. + * @param {String} roomId The room ID to read the setting value in, may be null. + * @return {Promise<*>} Resolves to the value for the setting. May result in null. + */ + static getValue(settingName, roomId) { + const levelOrder = [ + 'device', 'room-device', 'room-account', 'account', 'room', 'config', 'default' + ]; + + if (SettingsStore.isFeature(settingName)) { + const configValue = SettingsStore._getFeatureState(settingName); + if (configValue === "enable") return Promise.resolve({enabled: true}); + if (configValue === "disable") return Promise.resolve({enabled: false}); + // else let it fall through the default process + } + + const handlers = SettingsStore._getHandlers(settingName); + + // This wrapper function allows for iterating over the levelOrder to find a suitable + // handler that is supported by the setting. It does this by building the promise chain + // on the fly, wrapping the rejection from handler.getValue() to try the next handler. + // If the last handler also rejects the getValue() call, then this wrapper will convert + // the reply to `null` as per our contract to the caller. + let index = 0; + const wrapperFn = () => { + // Find the next handler that we can use + let handler = null; + while (!handler && index < levelOrder.length) { + handler = handlers[levelOrder[index++]]; + } + + // No handler == no reply (happens when the last available handler rejects) + if (!handler) return null; + + // Get the value and see if the handler will reject us (meaning it doesn't have + // a value for us). + const value = handler.getValue(settingName, roomId); + return value.then(null, () => wrapperFn()); // pass success through + }; + + return wrapperFn(); + } + + /** + * Sets the value for a setting. The room ID is optional if the setting is not being + * set for a particular room, otherwise it should be supplied. The value may be null + * to indicate that the level should no longer have an override. + * @param {string} settingName The name of the setting to change. + * @param {String} roomId The room ID to change the value in, may be null. + * @param {"device"|"room-device"|"room-account"|"account"|"room"} level The level + * to change the value at. + * @param {Object} value The new value of the setting, may be null. + * @return {Promise} Resolves when the setting has been changed. + */ + static setValue(settingName, roomId, level, value) { + const handler = SettingsStore._getHandler(settingName, level); + if (!handler) { + throw new Error("Setting " + settingName + " does not have a handler for " + level); + } + + if (!handler.canSetValue(settingName, roomId)) { + throw new Error("User cannot set " + settingName + " at level " + level); + } + + return handler.setValue(settingName, roomId, value); + } + + /** + * Determines if the current user is permitted to set the given setting at the given + * level for a particular room. The room ID is optional if the setting is not being + * set for a particular room, otherwise it should be supplied. + * @param {string} settingName The name of the setting to check. + * @param {String} roomId The room ID to check in, may be null. + * @param {"device"|"room-device"|"room-account"|"account"|"room"} level The level to + * check at. + * @return {boolean} True if the user may set the setting, false otherwise. + */ + static canSetValue(settingName, roomId, level) { + const handler = SettingsStore._getHandler(settingName, level); + if (!handler) return false; + return handler.canSetValue(settingName, roomId); + } + + /** + * Determines if the given level is supported on this device. + * @param {"device"|"room-device"|"room-account"|"account"|"room"} level The level + * to check the feasibility of. + * @return {boolean} True if the level is supported, false otherwise. + */ + static isLevelSupported(level) { + if (!LEVEL_HANDLERS[level]) return false; + return LEVEL_HANDLERS[level].isSupported(); + } + + static _getHandler(settingName, level) { + const handlers = SettingsStore._getHandlers(settingName); + if (!handlers[level]) return null; + return handlers[level]; + } + + static _getHandlers(settingName) { + if (!SETTINGS[settingName]) return {}; + + const handlers = {}; + for (let level of SETTINGS[settingName].supportedLevels) { + if (!LEVEL_HANDLERS[level]) throw new Error("Unexpected level " + level); + handlers[level] = LEVEL_HANDLERS[level]; + } + + return handlers; + } + + static _getFeatureState(settingName) { + const featuresConfig = SdkConfig.get()['features']; + const enableLabs = SdkConfig.get()['enableLabs']; // we'll honour the old flag + + let featureState = enableLabs ? "labs" : "disable"; + if (featuresConfig && featuresConfig[settingName] !== undefined) { + featureState = featuresConfig[settingName]; + } + + const allowedStates = ['enable', 'disable', 'labs']; + if (!allowedStates.contains(featureState)) { + console.warn("Feature state '" + featureState + "' is invalid for " + settingName); + featureState = "disable"; // to prevent accidental features. + } + + return featureState; + } +} From 23d159e21cf2f2c623fe63dcb88468ed3b6a6ff7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 28 Oct 2017 19:45:48 -0600 Subject: [PATCH 036/119] Make reading settings synchronous Signed-off-by: Travis Ralston --- src/settings/AccountSettingsHandler.js | 14 ++++---- src/settings/ConfigSettingsHandler.js | 4 +-- src/settings/DefaultSettingsHandler.js | 4 +-- src/settings/DeviceSettingsHandler.js | 12 +++---- src/settings/RoomAccountSettingsHandler.js | 22 ++++++------- src/settings/RoomDeviceSettingsHandler.js | 5 +-- src/settings/RoomSettingsHandler.js | 26 +++++++-------- src/settings/SettingsHandler.js | 2 +- src/settings/SettingsStore.js | 38 +++++++--------------- 9 files changed, 56 insertions(+), 71 deletions(-) diff --git a/src/settings/AccountSettingsHandler.js b/src/settings/AccountSettingsHandler.js index 6352da5ccc..c505afe31d 100644 --- a/src/settings/AccountSettingsHandler.js +++ b/src/settings/AccountSettingsHandler.js @@ -24,13 +24,13 @@ import MatrixClientPeg from '../MatrixClientPeg'; */ export default class AccountSettingHandler extends SettingsHandler { getValue(settingName, roomId) { - const value = MatrixClientPeg.get().getAccountData(this._getEventType(settingName)); - if (!value) return Promise.reject(); - return Promise.resolve(value); + return this._getSettings()[settingName]; } setValue(settingName, roomId, newValue) { - return MatrixClientPeg.get().setAccountData(this._getEventType(settingName), newValue); + const content = this._getSettings(); + content[settingName] = newValue; + return MatrixClientPeg.get().setAccountData("im.vector.web.settings", content); } canSetValue(settingName, roomId) { @@ -41,7 +41,9 @@ export default class AccountSettingHandler extends SettingsHandler { return !!MatrixClientPeg.get(); } - _getEventType(settingName) { - return "im.vector.setting." + settingName; + _getSettings() { + const event = MatrixClientPeg.get().getAccountData("im.vector.web.settings"); + if (!event || !event.getContent()) return {}; + return event.getContent(); } } \ No newline at end of file diff --git a/src/settings/ConfigSettingsHandler.js b/src/settings/ConfigSettingsHandler.js index 8f0cc9041b..5307a1dac1 100644 --- a/src/settings/ConfigSettingsHandler.js +++ b/src/settings/ConfigSettingsHandler.js @@ -25,8 +25,8 @@ import SdkConfig from "../SdkConfig"; export default class ConfigSettingsHandler extends SettingsHandler { getValue(settingName, roomId) { const settingsConfig = SdkConfig.get()["settingDefaults"]; - if (!settingsConfig || !settingsConfig[settingName]) return Promise.reject(); - return Promise.resolve(settingsConfig[settingName]); + if (!settingsConfig || !settingsConfig[settingName]) return null; + return settingsConfig[settingName]; } setValue(settingName, roomId, newValue) { diff --git a/src/settings/DefaultSettingsHandler.js b/src/settings/DefaultSettingsHandler.js index 06937fd957..0a4b8d91d3 100644 --- a/src/settings/DefaultSettingsHandler.js +++ b/src/settings/DefaultSettingsHandler.js @@ -32,9 +32,7 @@ export default class DefaultSettingsHandler extends SettingsHandler { } getValue(settingName, roomId) { - const value = this._defaults[settingName]; - if (!value) return Promise.reject(); - return Promise.resolve(value); + return this._defaults[settingName]; } setValue(settingName, roomId, newValue) { diff --git a/src/settings/DeviceSettingsHandler.js b/src/settings/DeviceSettingsHandler.js index 83cf88bcba..dbb833c570 100644 --- a/src/settings/DeviceSettingsHandler.js +++ b/src/settings/DeviceSettingsHandler.js @@ -35,12 +35,13 @@ export default class DeviceSettingsHandler extends SettingsHandler { getValue(settingName, roomId) { if (this._featureNames.includes(settingName)) { - return Promise.resolve(this._readFeature(settingName)); + return this._readFeature(settingName); } const value = localStorage.getItem(this._getKey(settingName)); - if (!value) return Promise.reject(); - return Promise.resolve(value); + if (!value) return null; + + return JSON.parse(value).value; } setValue(settingName, roomId, newValue) { @@ -51,6 +52,7 @@ export default class DeviceSettingsHandler extends SettingsHandler { if (newValue === null) { localStorage.removeItem(this._getKey(settingName)); } else { + newValue = JSON.stringify({value: newValue}); localStorage.setItem(this._getKey(settingName), newValue); } @@ -79,9 +81,7 @@ export default class DeviceSettingsHandler extends SettingsHandler { } const value = localStorage.getItem("mx_labs_feature_" + featureName); - const enabled = value === "true"; - - return {enabled}; + return value === "true"; } _writeFeature(featureName, enabled) { diff --git a/src/settings/RoomAccountSettingsHandler.js b/src/settings/RoomAccountSettingsHandler.js index 7157d86c34..3c775f3ff0 100644 --- a/src/settings/RoomAccountSettingsHandler.js +++ b/src/settings/RoomAccountSettingsHandler.js @@ -23,18 +23,13 @@ import MatrixClientPeg from '../MatrixClientPeg'; */ export default class RoomAccountSettingsHandler extends SettingsHandler { getValue(settingName, roomId) { - const room = MatrixClientPeg.get().getRoom(roomId); - if (!room) return Promise.reject(); - - const value = room.getAccountData(this._getEventType(settingName)); - if (!value) return Promise.reject(); - return Promise.resolve(value); + return this._getSettings(roomId)[settingName]; } setValue(settingName, roomId, newValue) { - return MatrixClientPeg.get().setRoomAccountData( - roomId, this._getEventType(settingName), newValue - ); + const content = this._getSettings(roomId); + content[settingName] = newValue; + return MatrixClientPeg.get().setRoomAccountData(roomId, "im.vector.web.settings", content); } canSetValue(settingName, roomId) { @@ -46,7 +41,12 @@ export default class RoomAccountSettingsHandler extends SettingsHandler { return !!MatrixClientPeg.get(); } - _getEventType(settingName) { - return "im.vector.setting." + settingName; + _getSettings(roomId) { + const room = MatrixClientPeg.get().getRoom(roomId); + if (!room) return {}; + + const event = room.getAccountData("im.vector.settings"); + if (!event || !event.getContent()) return {}; + return event.getContent(); } } \ No newline at end of file diff --git a/src/settings/RoomDeviceSettingsHandler.js b/src/settings/RoomDeviceSettingsHandler.js index fe477564f6..3ee83f2362 100644 --- a/src/settings/RoomDeviceSettingsHandler.js +++ b/src/settings/RoomDeviceSettingsHandler.js @@ -24,14 +24,15 @@ import SettingsHandler from "./SettingsHandler"; export default class RoomDeviceSettingsHandler extends SettingsHandler { getValue(settingName, roomId) { const value = localStorage.getItem(this._getKey(settingName, roomId)); - if (!value) return Promise.reject(); - return Promise.resolve(value); + if (!value) return null; + return JSON.parse(value).value; } setValue(settingName, roomId, newValue) { if (newValue === null) { localStorage.removeItem(this._getKey(settingName, roomId)); } else { + newValue = JSON.stringify({value: newValue}); localStorage.setItem(this._getKey(settingName, roomId), newValue); } diff --git a/src/settings/RoomSettingsHandler.js b/src/settings/RoomSettingsHandler.js index dcd7a76e87..c41a510646 100644 --- a/src/settings/RoomSettingsHandler.js +++ b/src/settings/RoomSettingsHandler.js @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; import SettingsHandler from "./SettingsHandler"; import MatrixClientPeg from '../MatrixClientPeg'; @@ -23,34 +22,33 @@ import MatrixClientPeg from '../MatrixClientPeg'; */ export default class RoomSettingsHandler extends SettingsHandler { getValue(settingName, roomId) { - const room = MatrixClientPeg.get().getRoom(roomId); - if (!room) return Promise.reject(); - - const event = room.currentState.getStateEvents(this._getEventType(settingName), ""); - if (!event || !event.getContent()) return Promise.reject(); - return Promise.resolve(event.getContent()); + return this._getSettings(roomId)[settingName]; } setValue(settingName, roomId, newValue) { - return MatrixClientPeg.get().sendStateEvent( - roomId, this._getEventType(settingName), newValue, "" - ); + const content = this._getSettings(roomId); + content[settingName] = newValue; + return MatrixClientPeg.get().sendStateEvent(roomId, "im.vector.web.settings", content, ""); } canSetValue(settingName, roomId) { const cli = MatrixClientPeg.get(); const room = cli.getRoom(roomId); - const eventType = this._getEventType(settingName); if (!room) return false; - return room.currentState.maySendStateEvent(eventType, cli.getUserId()); + return room.currentState.maySendStateEvent("im.vector.web.settings", cli.getUserId()); } isSupported() { return !!MatrixClientPeg.get(); } - _getEventType(settingName) { - return "im.vector.setting." + settingName; + _getSettings(roomId) { + const room = MatrixClientPeg.get().getRoom(roomId); + if (!room) return {}; + + const event = room.currentState.getStateEvents("im.vector.web.settings"); + if (!event || !event.getContent()) return {}; + return event.getContent(); } } \ No newline at end of file diff --git a/src/settings/SettingsHandler.js b/src/settings/SettingsHandler.js index 7387712367..20f09cb1f2 100644 --- a/src/settings/SettingsHandler.js +++ b/src/settings/SettingsHandler.js @@ -42,7 +42,7 @@ export default class SettingsHandler { * able to set the value prior to calling this. * @param {string} settingName The name of the setting to change. * @param {String} roomId The room ID to set the value in, may be null. - * @param {Object} newValue The new value for the setting, may be null. + * @param {*} newValue The new value for the setting, may be null. * @return {Promise} Resolves when the setting has been saved. */ setValue(settingName, roomId, newValue) { diff --git a/src/settings/SettingsStore.js b/src/settings/SettingsStore.js index eea91345d8..330c2def05 100644 --- a/src/settings/SettingsStore.js +++ b/src/settings/SettingsStore.js @@ -137,7 +137,7 @@ export default class SettingsStore { return yield SettingsStore.getValue(settingName, roomId); })(); - return value.enabled; + return value; } /** @@ -145,7 +145,7 @@ export default class SettingsStore { * be applied to any particular room, otherwise it should be supplied. * @param {string} settingName The name of the setting to read the value of. * @param {String} roomId The room ID to read the setting value in, may be null. - * @return {Promise<*>} Resolves to the value for the setting. May result in null. + * @return {*} The value, or null if not found */ static getValue(settingName, roomId) { const levelOrder = [ @@ -154,36 +154,22 @@ export default class SettingsStore { if (SettingsStore.isFeature(settingName)) { const configValue = SettingsStore._getFeatureState(settingName); - if (configValue === "enable") return Promise.resolve({enabled: true}); - if (configValue === "disable") return Promise.resolve({enabled: false}); + if (configValue === "enable") return true; + if (configValue === "disable") return false; // else let it fall through the default process } const handlers = SettingsStore._getHandlers(settingName); - // This wrapper function allows for iterating over the levelOrder to find a suitable - // handler that is supported by the setting. It does this by building the promise chain - // on the fly, wrapping the rejection from handler.getValue() to try the next handler. - // If the last handler also rejects the getValue() call, then this wrapper will convert - // the reply to `null` as per our contract to the caller. - let index = 0; - const wrapperFn = () => { - // Find the next handler that we can use - let handler = null; - while (!handler && index < levelOrder.length) { - handler = handlers[levelOrder[index++]]; - } + for (let level of levelOrder) { + let handler = handlers[level]; + if (!handler) continue; - // No handler == no reply (happens when the last available handler rejects) - if (!handler) return null; - - // Get the value and see if the handler will reject us (meaning it doesn't have - // a value for us). const value = handler.getValue(settingName, roomId); - return value.then(null, () => wrapperFn()); // pass success through - }; - - return wrapperFn(); + if (!value) continue; + return value; + } + return null; } /** @@ -194,7 +180,7 @@ export default class SettingsStore { * @param {String} roomId The room ID to change the value in, may be null. * @param {"device"|"room-device"|"room-account"|"account"|"room"} level The level * to change the value at. - * @param {Object} value The new value of the setting, may be null. + * @param {*} value The new value of the setting, may be null. * @return {Promise} Resolves when the setting has been changed. */ static setValue(settingName, roomId, level, value) { From 7dda5e9196710d82bd64284a9f895a1d29c46d69 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 28 Oct 2017 19:53:12 -0600 Subject: [PATCH 037/119] Appease the linter round 1 Signed-off-by: Travis Ralston --- src/settings/AccountSettingsHandler.js | 2 +- src/settings/ConfigSettingsHandler.js | 2 +- src/settings/DefaultSettingsHandler.js | 2 +- src/settings/DeviceSettingsHandler.js | 2 +- src/settings/RoomAccountSettingsHandler.js | 3 +-- src/settings/RoomDeviceSettingsHandler.js | 2 +- src/settings/RoomSettingsHandler.js | 2 +- src/settings/SettingsHandler.js | 9 +++------ src/settings/SettingsStore.js | 17 ++++++----------- 9 files changed, 16 insertions(+), 25 deletions(-) diff --git a/src/settings/AccountSettingsHandler.js b/src/settings/AccountSettingsHandler.js index c505afe31d..db25cd0737 100644 --- a/src/settings/AccountSettingsHandler.js +++ b/src/settings/AccountSettingsHandler.js @@ -46,4 +46,4 @@ export default class AccountSettingHandler extends SettingsHandler { if (!event || !event.getContent()) return {}; return event.getContent(); } -} \ No newline at end of file +} diff --git a/src/settings/ConfigSettingsHandler.js b/src/settings/ConfigSettingsHandler.js index 5307a1dac1..5cd6d22411 100644 --- a/src/settings/ConfigSettingsHandler.js +++ b/src/settings/ConfigSettingsHandler.js @@ -40,4 +40,4 @@ export default class ConfigSettingsHandler extends SettingsHandler { isSupported() { return true; // SdkConfig is always there } -} \ No newline at end of file +} diff --git a/src/settings/DefaultSettingsHandler.js b/src/settings/DefaultSettingsHandler.js index 0a4b8d91d3..2c3a05a18a 100644 --- a/src/settings/DefaultSettingsHandler.js +++ b/src/settings/DefaultSettingsHandler.js @@ -46,4 +46,4 @@ export default class DefaultSettingsHandler extends SettingsHandler { isSupported() { return true; } -} \ No newline at end of file +} diff --git a/src/settings/DeviceSettingsHandler.js b/src/settings/DeviceSettingsHandler.js index dbb833c570..329634a810 100644 --- a/src/settings/DeviceSettingsHandler.js +++ b/src/settings/DeviceSettingsHandler.js @@ -87,4 +87,4 @@ export default class DeviceSettingsHandler extends SettingsHandler { _writeFeature(featureName, enabled) { localStorage.setItem("mx_labs_feature_" + featureName, enabled); } -} \ No newline at end of file +} diff --git a/src/settings/RoomAccountSettingsHandler.js b/src/settings/RoomAccountSettingsHandler.js index 3c775f3ff0..e1edaffb90 100644 --- a/src/settings/RoomAccountSettingsHandler.js +++ b/src/settings/RoomAccountSettingsHandler.js @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; import SettingsHandler from "./SettingsHandler"; import MatrixClientPeg from '../MatrixClientPeg'; @@ -49,4 +48,4 @@ export default class RoomAccountSettingsHandler extends SettingsHandler { if (!event || !event.getContent()) return {}; return event.getContent(); } -} \ No newline at end of file +} diff --git a/src/settings/RoomDeviceSettingsHandler.js b/src/settings/RoomDeviceSettingsHandler.js index 3ee83f2362..b61e266a4a 100644 --- a/src/settings/RoomDeviceSettingsHandler.js +++ b/src/settings/RoomDeviceSettingsHandler.js @@ -50,4 +50,4 @@ export default class RoomDeviceSettingsHandler extends SettingsHandler { _getKey(settingName, roomId) { return "mx_setting_" + settingName + "_" + roomId; } -} \ No newline at end of file +} diff --git a/src/settings/RoomSettingsHandler.js b/src/settings/RoomSettingsHandler.js index c41a510646..91bcff384a 100644 --- a/src/settings/RoomSettingsHandler.js +++ b/src/settings/RoomSettingsHandler.js @@ -51,4 +51,4 @@ export default class RoomSettingsHandler extends SettingsHandler { if (!event || !event.getContent()) return {}; return event.getContent(); } -} \ No newline at end of file +} diff --git a/src/settings/SettingsHandler.js b/src/settings/SettingsHandler.js index 20f09cb1f2..49265feb9a 100644 --- a/src/settings/SettingsHandler.js +++ b/src/settings/SettingsHandler.js @@ -14,8 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; - /** * Represents the base class for all level handlers. This class performs no logic * and should be overridden. @@ -27,8 +25,7 @@ export default class SettingsHandler { * applicable to this level and may be ignored by the handler. * @param {string} settingName The name of the setting. * @param {String} roomId The room ID to read from, may be null. - * @return {Promise} Resolves to the setting value. Rejected if the value - * could not be found. + * @returns {*} The setting value, or null if not found. */ getValue(settingName, roomId) { throw new Error("Operation not possible: getValue was not overridden"); @@ -43,7 +40,7 @@ export default class SettingsHandler { * @param {string} settingName The name of the setting to change. * @param {String} roomId The room ID to set the value in, may be null. * @param {*} newValue The new value for the setting, may be null. - * @return {Promise} Resolves when the setting has been saved. + * @returns {Promise} Resolves when the setting has been saved. */ setValue(settingName, roomId, newValue) { throw new Error("Operation not possible: setValue was not overridden"); @@ -67,4 +64,4 @@ export default class SettingsHandler { isSupported() { return false; } -} \ No newline at end of file +} diff --git a/src/settings/SettingsStore.js b/src/settings/SettingsStore.js index 330c2def05..e73be42887 100644 --- a/src/settings/SettingsStore.js +++ b/src/settings/SettingsStore.js @@ -59,9 +59,9 @@ const SETTINGS = { }; // Convert the above into simpler formats for the handlers -let defaultSettings = {}; -let featureNames = []; -for (let key of Object.keys(SETTINGS)) { +const defaultSettings = {}; +const featureNames = []; +for (const key of Object.keys(SETTINGS)) { defaultSettings[key] = SETTINGS[key].defaults; if (SETTINGS[key].isFeature) featureNames.push(key); } @@ -132,12 +132,7 @@ export default class SettingsStore { throw new Error("Setting " + settingName + " is not a feature"); } - // Synchronously get the setting value (which should be {enabled: true/false}) - const value = Promise.coroutine(function* () { - return yield SettingsStore.getValue(settingName, roomId); - })(); - - return value; + return SettingsStore.getValue(settingName, roomId); } /** @@ -149,7 +144,7 @@ export default class SettingsStore { */ static getValue(settingName, roomId) { const levelOrder = [ - 'device', 'room-device', 'room-account', 'account', 'room', 'config', 'default' + 'device', 'room-device', 'room-account', 'account', 'room', 'config', 'default', ]; if (SettingsStore.isFeature(settingName)) { @@ -161,7 +156,7 @@ export default class SettingsStore { const handlers = SettingsStore._getHandlers(settingName); - for (let level of levelOrder) { + for (const level of levelOrder) { let handler = handlers[level]; if (!handler) continue; From bf815f4be961ba09a80fde1cac1e72a6735918b8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 28 Oct 2017 20:21:34 -0600 Subject: [PATCH 038/119] Support labs features Signed-off-by: Travis Ralston --- src/UserSettingsStore.js | 76 ---------------------- src/components/structures/UserSettings.js | 10 +-- src/components/views/elements/Flair.js | 4 +- src/components/views/rooms/RoomHeader.js | 4 +- src/components/views/rooms/RoomSettings.js | 3 +- src/settings/DeviceSettingsHandler.js | 3 +- src/settings/SettingsStore.js | 74 +++++++++++++++------ 7 files changed, 68 insertions(+), 106 deletions(-) diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index ce39939bc0..2d2045d15b 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -25,50 +25,7 @@ import SdkConfig from './SdkConfig'; * TODO: Make things use this. This is all WIP - see UserSettings.js for usage. */ -const FEATURES = [ - { - id: 'feature_groups', - name: _td("Communities"), - }, - { - id: 'feature_pinning', - name: _td("Message Pinning"), - }, -]; - export default { - getLabsFeatures() { - const featuresConfig = SdkConfig.get()['features'] || {}; - - // The old flag: honoured for backwards compatibility - const enableLabs = SdkConfig.get()['enableLabs']; - - let labsFeatures; - if (enableLabs) { - labsFeatures = FEATURES; - } else { - labsFeatures = FEATURES.filter((f) => { - const sdkConfigValue = featuresConfig[f.id]; - if (sdkConfigValue === 'labs') { - return true; - } - }); - } - return labsFeatures.map((f) => { - return f.id; - }); - }, - - translatedNameForFeature(featureId) { - const feature = FEATURES.filter((f) => { - return f.id === featureId; - })[0]; - - if (feature === undefined) return null; - - return _t(feature.name); - }, - loadProfileInfo: function() { const cli = MatrixClientPeg.get(); return cli.getProfileInfo(cli.credentials.userId); @@ -213,37 +170,4 @@ export default { // FIXME: handle errors localStorage.setItem('mx_local_settings', JSON.stringify(settings)); }, - - isFeatureEnabled: function(featureId: string): boolean { - const featuresConfig = SdkConfig.get()['features']; - - // The old flag: honoured for backwards compatibility - const enableLabs = SdkConfig.get()['enableLabs']; - - let sdkConfigValue = enableLabs ? 'labs' : 'disable'; - if (featuresConfig && featuresConfig[featureId] !== undefined) { - sdkConfigValue = featuresConfig[featureId]; - } - - if (sdkConfigValue === 'enable') { - return true; - } else if (sdkConfigValue === 'disable') { - return false; - } else if (sdkConfigValue === 'labs') { - if (!MatrixClientPeg.get().isGuest()) { - // Make it explicit that guests get the defaults (although they shouldn't - // have been able to ever toggle the flags anyway) - const userValue = localStorage.getItem(`mx_labs_feature_${featureId}`); - return userValue === 'true'; - } - return false; - } else { - console.warn(`Unknown features config for ${featureId}: ${sdkConfigValue}`); - return false; - } - }, - - setFeatureEnabled: function(featureId: string, enabled: boolean) { - localStorage.setItem(`mx_labs_feature_${featureId}`, enabled); - }, }; diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 68ea932f93..9c28f7a869 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -15,6 +15,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ +import SettingsStore from "../../settings/SettingsStore"; + const React = require('react'); const ReactDOM = require('react-dom'); const sdk = require('../../index'); @@ -934,11 +936,11 @@ module.exports = React.createClass({ _renderLabs: function() { const features = []; - UserSettingsStore.getLabsFeatures().forEach((featureId) => { + SettingsStore.getLabsFeatures().forEach((featureId) => { // TODO: this ought to be a separate component so that we don't need // to rebind the onChange each time we render const onChange = (e) => { - UserSettingsStore.setFeatureEnabled(featureId, e.target.checked); + SettingsStore.setFeatureEnabled(featureId, e.target.checked); this.forceUpdate(); }; @@ -948,10 +950,10 @@ module.exports = React.createClass({ type="checkbox" id={featureId} name={featureId} - defaultChecked={UserSettingsStore.isFeatureEnabled(featureId)} + defaultChecked={SettingsStore.isFeatureEnabled(featureId)} onChange={onChange} /> - + ); }); diff --git a/src/components/views/elements/Flair.js b/src/components/views/elements/Flair.js index 69d9aa35b7..2e2c7f2595 100644 --- a/src/components/views/elements/Flair.js +++ b/src/components/views/elements/Flair.js @@ -19,9 +19,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import {MatrixClient} from 'matrix-js-sdk'; -import UserSettingsStore from '../../../UserSettingsStore'; import FlairStore from '../../../stores/FlairStore'; import dis from '../../../dispatcher'; +import SettingsStore from "../../../settings/SettingsStore"; class FlairAvatar extends React.Component { @@ -79,7 +79,7 @@ export default class Flair extends React.Component { componentWillMount() { this._unmounted = false; - if (UserSettingsStore.isFeatureEnabled('feature_groups') && FlairStore.groupSupport()) { + if (SettingsStore.isFeatureEnabled('feature_groups') && FlairStore.groupSupport()) { this._generateAvatars(); } this.context.matrixClient.on('RoomState.events', this.onRoomStateEvents); diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 4dfbdb3644..fbfe7ebe18 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -31,7 +31,7 @@ import linkifyMatrix from '../../../linkify-matrix'; import AccessibleButton from '../elements/AccessibleButton'; import ManageIntegsButton from '../elements/ManageIntegsButton'; import {CancelButton} from './SimpleRoomHeader'; -import UserSettingsStore from "../../../UserSettingsStore"; +import SettingsStore from "../../../settings/SettingsStore"; linkifyMatrix(linkify); @@ -304,7 +304,7 @@ module.exports = React.createClass({ ; } - if (this.props.onPinnedClick && UserSettingsStore.isFeatureEnabled('feature_pinning')) { + if (this.props.onPinnedClick && SettingsStore.isFeatureEnabled('feature_pinning')) { pinnedEventsButton = diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index dbdcdf596a..f582cc29ef 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -25,6 +25,7 @@ import ObjectUtils from '../../../ObjectUtils'; import dis from '../../../dispatcher'; import UserSettingsStore from '../../../UserSettingsStore'; import AccessibleButton from '../elements/AccessibleButton'; +import SettingsStore from "../../../settings/SettingsStore"; // parse a string as an integer; if the input is undefined, or cannot be parsed @@ -671,7 +672,7 @@ module.exports = React.createClass({ const self = this; let relatedGroupsSection; - if (UserSettingsStore.isFeatureEnabled('feature_groups')) { + if (SettingsStore.isFeatureEnabled('feature_groups')) { relatedGroupsSection = SettingsStore.isFeature(s)); + + const enableLabs = SdkConfig.get()["enableLabs"]; + if (enableLabs) return possibleFeatures; + + return possibleFeatures.filter((s) => SettingsStore._getFeatureState(s) === "labs"); + } + /** * Determines if a setting is also a feature. * @param {string} settingName The setting to look up. @@ -135,6 +159,16 @@ export default class SettingsStore { return SettingsStore.getValue(settingName, roomId); } + /** + * Sets a feature as enabled or disabled on the current device. + * @param {string} settingName The name of the setting. + * @param {boolean} value True to enable the feature, false otherwise. + * @returns {Promise} Resolves when the setting has been set. + */ + static setFeatureEnabled(settingName, value) { + return SettingsStore.setValue(settingName, null, "device", value); + } + /** * Gets the value of a setting. The room ID is optional if the setting is not to * be applied to any particular room, otherwise it should be supplied. @@ -228,7 +262,7 @@ export default class SettingsStore { if (!SETTINGS[settingName]) return {}; const handlers = {}; - for (let level of SETTINGS[settingName].supportedLevels) { + for (const level of SETTINGS[settingName].supportedLevels) { if (!LEVEL_HANDLERS[level]) throw new Error("Unexpected level " + level); handlers[level] = LEVEL_HANDLERS[level]; } @@ -246,7 +280,7 @@ export default class SettingsStore { } const allowedStates = ['enable', 'disable', 'labs']; - if (!allowedStates.contains(featureState)) { + if (!allowedStates.includes(featureState)) { console.warn("Feature state '" + featureState + "' is invalid for " + settingName); featureState = "disable"; // to prevent accidental features. } From ae10a11ac4c1b3c96be16f630800c389117a6e50 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 29 Oct 2017 01:43:52 -0600 Subject: [PATCH 039/119] Convert synced settings to granular settings Signed-off-by: Travis Ralston --- src/Unread.js | 4 +- src/UserSettingsStore.js | 17 --- src/autocomplete/EmojiProvider.js | 4 +- src/components/structures/LoggedInView.js | 4 +- src/components/structures/MessagePanel.js | 5 +- src/components/structures/RoomView.js | 8 +- src/components/structures/TimelinePanel.js | 9 +- src/components/structures/UserSettings.js | 116 +++++------------- src/components/views/messages/MImageBody.js | 8 +- src/components/views/messages/MVideoBody.js | 4 +- src/components/views/messages/TextualBody.js | 8 +- src/components/views/rooms/AuxPanel.js | 1 - src/components/views/rooms/MessageComposer.js | 8 +- .../views/rooms/MessageComposerInput.js | 12 +- src/components/views/rooms/RoomTile.js | 1 - src/components/views/voip/VideoView.js | 4 +- src/settings/RoomSettingsHandler.js | 3 +- src/settings/SettingsStore.js | 100 ++++++++++++++- src/shouldHideEvent.js | 13 +- .../structures/MessagePanel-test.js | 8 +- .../views/rooms/MessageComposerInput-test.js | 1 - 21 files changed, 177 insertions(+), 161 deletions(-) diff --git a/src/Unread.js b/src/Unread.js index 20e876ad88..383b5c2e5a 100644 --- a/src/Unread.js +++ b/src/Unread.js @@ -15,7 +15,6 @@ limitations under the License. */ const MatrixClientPeg = require('./MatrixClientPeg'); -import UserSettingsStore from './UserSettingsStore'; import shouldHideEvent from './shouldHideEvent'; const sdk = require('./index'); @@ -64,7 +63,6 @@ module.exports = { // we have and the read receipt. We could fetch more history to try & find out, // but currently we just guess. - const syncedSettings = UserSettingsStore.getSyncedSettings(); // Loop through messages, starting with the most recent... for (let i = room.timeline.length - 1; i >= 0; --i) { const ev = room.timeline[i]; @@ -74,7 +72,7 @@ module.exports = { // that counts and we can stop looking because the user's read // this and everything before. return false; - } else if (!shouldHideEvent(ev, syncedSettings) && this.eventTriggersUnreadCount(ev)) { + } else if (!shouldHideEvent(ev) && this.eventTriggersUnreadCount(ev)) { // We've found a message that counts before we hit // the read marker, so this room is definitely unread. return true; diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index 2d2045d15b..591eaa1f0f 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -137,23 +137,6 @@ export default { }); }, - getSyncedSettings: function() { - const event = MatrixClientPeg.get().getAccountData('im.vector.web.settings'); - return event ? event.getContent() : {}; - }, - - getSyncedSetting: function(type, defaultValue = null) { - const settings = this.getSyncedSettings(); - return settings.hasOwnProperty(type) ? settings[type] : defaultValue; - }, - - setSyncedSetting: function(type, value) { - const settings = this.getSyncedSettings(); - settings[type] = value; - // FIXME: handle errors - return MatrixClientPeg.get().setAccountData('im.vector.web.settings', settings); - }, - getLocalSettings: function() { const localSettingsString = localStorage.getItem('mx_local_settings') || '{}'; return JSON.parse(localSettingsString); diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index a5b80e3b0e..b2ec73faca 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -25,7 +25,7 @@ import {PillCompletion} from './Components'; import type {SelectionRange, Completion} from './Autocompleter'; import _uniq from 'lodash/uniq'; import _sortBy from 'lodash/sortBy'; -import UserSettingsStore from '../UserSettingsStore'; +import SettingsStore from "../settings/SettingsStore"; import EmojiData from '../stripped-emoji.json'; @@ -97,7 +97,7 @@ export default class EmojiProvider extends AutocompleteProvider { } async getCompletions(query: string, selection: SelectionRange) { - if (UserSettingsStore.getSyncedSetting("MessageComposerInput.dontSuggestEmoji")) { + if (SettingsStore.getValue("MessageComposerInput.dontSuggestEmoji")) { return []; // don't give any suggestions if the user doesn't want them } diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 5d1d47c5b2..31f59e4849 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -19,7 +19,6 @@ limitations under the License. import * as Matrix from 'matrix-js-sdk'; import React from 'react'; -import UserSettingsStore from '../../UserSettingsStore'; import KeyCode from '../../KeyCode'; import Notifier from '../../Notifier'; import PageTypes from '../../PageTypes'; @@ -28,6 +27,7 @@ import sdk from '../../index'; import dis from '../../dispatcher'; import sessionStore from '../../stores/SessionStore'; import MatrixClientPeg from '../../MatrixClientPeg'; +import SettingsStore from "../../settings/SettingsStore"; /** * This is what our MatrixChat shows when we are logged in. The precise view is @@ -74,7 +74,7 @@ export default React.createClass({ getInitialState: function() { return { // use compact timeline view - useCompactLayout: UserSettingsStore.getSyncedSetting('useCompactLayout'), + useCompactLayout: SettingsStore.getValue('useCompactLayout'), }; }, diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 2331e096c0..53cc660a9b 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -17,7 +17,6 @@ limitations under the License. import React from 'react'; import ReactDOM from 'react-dom'; import classNames from 'classnames'; -import UserSettingsStore from '../../UserSettingsStore'; import shouldHideEvent from '../../shouldHideEvent'; import dis from "../../dispatcher"; import sdk from '../../index'; @@ -110,8 +109,6 @@ module.exports = React.createClass({ // Velocity requires this._readMarkerGhostNode = null; - this._syncedSettings = UserSettingsStore.getSyncedSettings(); - this._isMounted = true; }, @@ -251,7 +248,7 @@ module.exports = React.createClass({ // Always show highlighted event if (this.props.highlightedEventId === mxEv.getId()) return true; - return !shouldHideEvent(mxEv, this._syncedSettings); + return !shouldHideEvent(mxEv); }, _getEventTiles: function() { diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 9b6dbb4c27..4256c19f4d 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -29,7 +29,6 @@ const classNames = require("classnames"); const Matrix = require("matrix-js-sdk"); import { _t } from '../../languageHandler'; -const UserSettingsStore = require('../../UserSettingsStore'); const MatrixClientPeg = require("../../MatrixClientPeg"); const ContentMessages = require("../../ContentMessages"); const Modal = require("../../Modal"); @@ -48,6 +47,7 @@ import UserProvider from '../../autocomplete/UserProvider'; import RoomViewStore from '../../stores/RoomViewStore'; import RoomScrollStateStore from '../../stores/RoomScrollStateStore'; +import SettingsStore from "../../settings/SettingsStore"; const DEBUG = false; let debuglog = function() {}; @@ -151,8 +151,6 @@ module.exports = React.createClass({ MatrixClientPeg.get().on("RoomMember.membership", this.onRoomMemberMembership); MatrixClientPeg.get().on("accountData", this.onAccountData); - this._syncedSettings = UserSettingsStore.getSyncedSettings(); - // Start listening for RoomViewStore updates this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); this._onRoomViewStoreUpdate(true); @@ -535,7 +533,7 @@ module.exports = React.createClass({ // update unread count when scrolled up if (!this.state.searchResults && this.state.atEndOfLiveTimeline) { // no change - } else if (!shouldHideEvent(ev, this._syncedSettings)) { + } else if (!shouldHideEvent(ev)) { this.setState((state, props) => { return {numUnreadMessages: state.numUnreadMessages + 1}; }); @@ -1778,7 +1776,7 @@ module.exports = React.createClass({ const messagePanel = (