From 17262ad80d4a7596c698f6c13caa2842903e36bc Mon Sep 17 00:00:00 2001 From: Pablo Saavedra Date: Mon, 8 May 2017 12:18:31 +0200 Subject: [PATCH 01/40] Added TextInputWithCheckbox dialog --- src/component-index.js | 2 + src/components/structures/MatrixChat.js | 15 ++- .../dialogs/TextInputWithCheckboxDialog.js | 108 ++++++++++++++++++ 3 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 src/components/views/dialogs/TextInputWithCheckboxDialog.js diff --git a/src/component-index.js b/src/component-index.js index d6873c6dfd..ed538a6fcb 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -99,6 +99,8 @@ import views$dialogs$SetDisplayNameDialog from './components/views/dialogs/SetDi views$dialogs$SetDisplayNameDialog && (module.exports.components['views.dialogs.SetDisplayNameDialog'] = views$dialogs$SetDisplayNameDialog); import views$dialogs$TextInputDialog from './components/views/dialogs/TextInputDialog'; views$dialogs$TextInputDialog && (module.exports.components['views.dialogs.TextInputDialog'] = views$dialogs$TextInputDialog); +import views$dialogs$TextInputWithCheckboxDialog from './components/views/dialogs/TextInputWithCheckboxDialog'; +views$dialogs$TextInputWithCheckboxDialog && (module.exports.components['views.dialogs.TextInputWithCheckboxDialog'] = views$dialogs$TextInputWithCheckboxDialog); import views$dialogs$UnknownDeviceDialog from './components/views/dialogs/UnknownDeviceDialog'; views$dialogs$UnknownDeviceDialog && (module.exports.components['views.dialogs.UnknownDeviceDialog'] = views$dialogs$UnknownDeviceDialog); import views$elements$AccessibleButton from './components/views/elements/AccessibleButton'; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index b449ff3094..d87ca203ce 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -191,6 +191,10 @@ module.exports = React.createClass({ return this.props.config.default_is_url || "https://vector.im"; }, + getDefaultFederate() { + return this.props.config.default_federate && true; + }, + componentWillMount: function() { SdkConfig.put(this.props.config); @@ -501,15 +505,20 @@ module.exports = React.createClass({ //this._setPage(PageTypes.CreateRoom); //this.notifyNewScreen('new'); - var TextInputDialog = sdk.getComponent("dialogs.TextInputDialog"); - Modal.createDialog(TextInputDialog, { + var TextInputWithCheckboxDialog = sdk.getComponent("dialogs.TextInputWithCheckboxDialog"); + Modal.createDialog(TextInputWithCheckboxDialog, { title: "Create Room", description: "Room name (optional)", button: "Create Room", - onFinished: (should_create, name) => { + check: this.getDefaultFederate(), + checkLabel: "Federate room in domain " + MatrixClientPeg.get().getDomain(), + onFinished: (should_create, name, isFederate) => { if (should_create) { const createOpts = {}; if (name) createOpts.name = name; + if (isFederate) { + createOpts.creation_content = {"m.federate": isFederate} + } createRoom({createOpts}).done(); } } diff --git a/src/components/views/dialogs/TextInputWithCheckboxDialog.js b/src/components/views/dialogs/TextInputWithCheckboxDialog.js new file mode 100644 index 0000000000..916de16af5 --- /dev/null +++ b/src/components/views/dialogs/TextInputWithCheckboxDialog.js @@ -0,0 +1,108 @@ +/* +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. +*/ + +import React from 'react'; +import sdk from '../../../index'; + +export default React.createClass({ + displayName: 'TextInputWithCheckboxDialog', + propTypes: { + title: React.PropTypes.string, + description: React.PropTypes.oneOfType([ + React.PropTypes.element, + React.PropTypes.string, + ]), + value: React.PropTypes.string, + button: React.PropTypes.string, + focus: React.PropTypes.bool, + checkLabel: React.PropTypes.string, + check: React.PropTypes.bool, + onFinished: React.PropTypes.func.isRequired, + }, + + getDefaultProps: function() { + return { + title: "", + value: "", + description: "", + button: "OK", + focus: true, + checkLabel: "", + check: true, + }; + }, + + getInitialState: function() { + return { + isChecked: this.props.check, + }; + }, + + componentDidMount: function() { + if (this.props.focus) { + // Set the cursor at the end of the text input + this.refs.textinput.value = this.props.value; + } + }, + + onOk: function() { + this.props.onFinished(true, this.refs.textinput.value, this.state.isChecked); + }, + + onCancel: function() { + this.props.onFinished(false); + }, + + _onToggle: function(keyName, checkedValue, uncheckedValue, ev) { + console.log("Checkbox toggle: %s %s", keyName, ev.target.checked); + var state = {}; + state[keyName] = ev.target.checked ? checkedValue : uncheckedValue; + this.setState(state); + }, + + render: function() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + return ( + +
+
+ +
+
+ +
+ +
+
+ + +
+
+ ); + }, +}); From 4b4b7302331b195e199e44bd2abddbf1a2c68b29 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 2 Aug 2017 13:41:26 +0100 Subject: [PATCH 02/40] fix and i18n the impl Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/MatrixChat.js | 24 ++++++++------ .../dialogs/TextInputWithCheckboxDialog.js | 33 +++++++------------ src/i18n/strings/en_EN.json | 2 ++ 3 files changed, 27 insertions(+), 32 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 50ee9963a6..22119585d9 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -214,10 +214,6 @@ module.exports = React.createClass({ return this.props.config.default_is_url || "https://vector.im"; }, - getDefaultFederate() { - return this.props.config.default_federate && true; - }, - componentWillMount: function() { SdkConfig.put(this.props.config); @@ -790,19 +786,27 @@ module.exports = React.createClass({ dis.dispatch({action: 'view_set_mxid'}); return; } + // Dialog shows inverse of m.federate (noFederate) strict false check to skip undefined check (default = true) + const defaultNoFederate = this.props.config.default_federate === false; const TextInputWithCheckboxDialog = sdk.getComponent("dialogs.TextInputWithCheckboxDialog"); Modal.createDialog(TextInputWithCheckboxDialog, { title: _t('Create Room'), description: _t('Room name (optional)'), button: _t('Create Room'), - // TODO i18n below. - check: this.getDefaultFederate(), - checkLabel: 'Federate room in domain ' + MatrixClientPeg.get().getDomain(), - onFinished: (shouldCreate, name, federate) => { + check: defaultNoFederate, + checkLabel: + {_t('Block users on other matrix homeservers from joining this room')} +
+ ({_t('This setting cannot be changed later!')}) +
, + onFinished: (shouldCreate, name, noFederate) => { if (shouldCreate) { - const createOpts = {}; + const createOpts = { + creation_content: { + "m.federate": !noFederate, + }, + }; if (name) createOpts.name = name; - if (federate) createOpts.creation_content = {"m.federate": federate}; createRoom({createOpts}).done(); } }, diff --git a/src/components/views/dialogs/TextInputWithCheckboxDialog.js b/src/components/views/dialogs/TextInputWithCheckboxDialog.js index 916de16af5..613fab4cdb 100644 --- a/src/components/views/dialogs/TextInputWithCheckboxDialog.js +++ b/src/components/views/dialogs/TextInputWithCheckboxDialog.js @@ -16,6 +16,7 @@ limitations under the License. import React from 'react'; import sdk from '../../../index'; +import { _t } from '../../../languageHandler'; export default React.createClass({ displayName: 'TextInputWithCheckboxDialog', @@ -28,7 +29,10 @@ export default React.createClass({ value: React.PropTypes.string, button: React.PropTypes.string, focus: React.PropTypes.bool, - checkLabel: React.PropTypes.string, + checkLabel: React.PropTypes.oneOfType([ + React.PropTypes.element, + React.PropTypes.string, + ]), check: React.PropTypes.bool, onFinished: React.PropTypes.func.isRequired, }, @@ -38,16 +42,9 @@ export default React.createClass({ title: "", value: "", description: "", - button: "OK", focus: true, checkLabel: "", - check: true, - }; - }, - - getInitialState: function() { - return { - isChecked: this.props.check, + check: false, }; }, @@ -59,20 +56,13 @@ export default React.createClass({ }, onOk: function() { - this.props.onFinished(true, this.refs.textinput.value, this.state.isChecked); + this.props.onFinished(true, this.refs.textinput.value, this.refs.checkbox.value); }, onCancel: function() { this.props.onFinished(false); }, - _onToggle: function(keyName, checkedValue, uncheckedValue, ev) { - console.log("Checkbox toggle: %s %s", keyName, ev.target.checked); - var state = {}; - state[keyName] = ev.target.checked ? checkedValue : uncheckedValue; - this.setState(state); - }, - render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( @@ -87,16 +77,15 @@ export default React.createClass({
+
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 87eb189ad0..f7fb221072 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -146,6 +146,7 @@ "Microphone": "Microphone", "Camera": "Camera", "Advanced": "Advanced", + "Advanced options": "Advanced options", "Algorithm": "Algorithm", "Hide removed messages": "Hide removed messages", "Always show message timestamps": "Always show message timestamps", From d4929b558edec1a473ae2c4e431bd74cd1f2e7fd Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Fri, 22 Sep 2017 21:43:27 +0200 Subject: [PATCH 06/40] Add dummy translation function to mark translatable strings Signed-off-by: Stefan Parviainen --- src/autocomplete/CommandProvider.js | 34 ++++++++--------- src/components/structures/UserSettings.js | 38 +++++++++---------- src/components/views/elements/AppTile.js | 6 +-- .../views/rooms/MessageComposerInput.js | 18 ++++----- src/languageHandler.js | 6 +++ 5 files changed, 54 insertions(+), 48 deletions(-) diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index 011ad0a7dc..42347d3955 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -16,7 +16,7 @@ limitations under the License. */ import React from 'react'; -import { _t } from '../languageHandler'; +import { _t, _td } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; import FuzzyMatcher from './FuzzyMatcher'; import {TextualCompletion} from './Components'; @@ -27,82 +27,82 @@ const COMMANDS = [ { command: '/me', args: '', - description: 'Displays action', + description: _td('Displays action'), }, { command: '/ban', args: ' [reason]', - description: 'Bans user with given id', + description: _td('Bans user with given id'), }, { command: '/unban', args: '', - description: 'Unbans user with given id', + description: _td('Unbans user with given id'), }, { command: '/op', args: ' []', - description: 'Define the power level of a user', + description: _td('Define the power level of a user'), }, { command: '/deop', args: '', - description: 'Deops user with given id', + description: _td('Deops user with given id'), }, { command: '/invite', args: '', - description: 'Invites user with given id to current room', + description: _td('Invites user with given id to current room'), }, { command: '/join', args: '', - description: 'Joins room with given alias', + description: _td('Joins room with given alias'), }, { command: '/part', args: '[]', - description: 'Leave room', + description: _td('Leave room'), }, { command: '/topic', args: '', - description: 'Sets the room topic', + description: _td('Sets the room topic'), }, { command: '/kick', args: ' [reason]', - description: 'Kicks user with given id', + description: _td('Kicks user with given id'), }, { command: '/nick', args: '', - description: 'Changes your display nickname', + description: _td('Changes your display nickname'), }, { command: '/ddg', args: '', - description: 'Searches DuckDuckGo for results', + description: _td('Searches DuckDuckGo for results'), }, { command: '/tint', args: ' []', - description: 'Changes colour scheme of current room', + description: _td('Changes colour scheme of current room'), }, { command: '/verify', args: ' ', - description: 'Verifies a user, device, and pubkey tuple', + description: _td('Verifies a user, device, and pubkey tuple'), }, { command: '/ignore', args: '', - description: 'Ignores a user, hiding their messages from you', + description: _td('Ignores a user, hiding their messages from you'), }, { command: '/unignore', args: '', - description: 'Stops ignoring a user, showing their messages going forward', + description: _td('Stops ignoring a user, showing their messages going forward'), }, // Omitting `/markdown` as it only seems to apply to OldComposer ]; diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index d2f27b63e1..572ee79a69 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -32,7 +32,7 @@ const AddThreepid = require('../../AddThreepid'); const SdkConfig = require('../../SdkConfig'); import Analytics from '../../Analytics'; import AccessibleButton from '../views/elements/AccessibleButton'; -import { _t } from '../../languageHandler'; +import { _t, _td } from '../../languageHandler'; import * as languageHandler from '../../languageHandler'; import * as FormattingUtils from '../../utils/FormattingUtils'; @@ -63,55 +63,55 @@ const gHVersionLabel = function(repo, token='') { const SETTINGS_LABELS = [ { id: 'autoplayGifsAndVideos', - label: 'Autoplay GIFs and videos', + label: _td('Autoplay GIFs and videos'), }, { id: 'hideReadReceipts', - label: 'Hide read receipts', + label: _td('Hide read receipts'), }, { id: 'dontSendTypingNotifications', - label: "Don't send typing notifications", + label: _td("Don't send typing notifications"), }, { id: 'alwaysShowTimestamps', - label: 'Always show message timestamps', + label: _td('Always show message timestamps'), }, { id: 'showTwelveHourTimestamps', - label: 'Show timestamps in 12 hour format (e.g. 2:30pm)', + label: _td('Show timestamps in 12 hour format (e.g. 2:30pm)'), }, { id: 'hideJoinLeaves', - label: 'Hide join/leave messages (invites/kicks/bans unaffected)', + label: _td('Hide join/leave messages (invites/kicks/bans unaffected)'), }, { id: 'hideAvatarDisplaynameChanges', - label: 'Hide avatar and display name changes', + label: _td('Hide avatar and display name changes'), }, { id: 'useCompactLayout', - label: 'Use compact timeline layout', + label: _td('Use compact timeline layout'), }, { id: 'hideRedactions', - label: 'Hide removed messages', + label: _td('Hide removed messages'), }, { id: 'enableSyntaxHighlightLanguageDetection', - label: 'Enable automatic language detection for syntax highlighting', + label: _td('Enable automatic language detection for syntax highlighting'), }, { id: 'MessageComposerInput.autoReplaceEmoji', - label: 'Automatically replace plain text Emoji', + label: _td('Automatically replace plain text Emoji'), }, { id: 'MessageComposerInput.dontSuggestEmoji', - label: 'Disable Emoji suggestions while typing', + label: _td('Disable Emoji suggestions while typing'), }, { id: 'Pill.shouldHidePillAvatar', - label: 'Hide avatars in user and room mentions', + label: _td('Hide avatars in user and room mentions'), }, /* { @@ -124,7 +124,7 @@ const SETTINGS_LABELS = [ const ANALYTICS_SETTINGS_LABELS = [ { id: 'analyticsOptOut', - label: 'Opt out of analytics', + label: _td('Opt out of analytics'), fn: function(checked) { Analytics[checked ? 'disable' : 'enable'](); }, @@ -134,7 +134,7 @@ const ANALYTICS_SETTINGS_LABELS = [ const WEBRTC_SETTINGS_LABELS = [ { id: 'webRtcForceTURN', - label: 'Disable Peer-to-Peer for 1:1 calls', + label: _td('Disable Peer-to-Peer for 1:1 calls'), }, ]; @@ -143,7 +143,7 @@ const WEBRTC_SETTINGS_LABELS = [ const CRYPTO_SETTINGS_LABELS = [ { id: 'blacklistUnverifiedDevices', - label: 'Never send encrypted messages to unverified devices from this device', + label: _td('Never send encrypted messages to unverified devices from this device'), fn: function(checked) { MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked); }, @@ -166,12 +166,12 @@ const CRYPTO_SETTINGS_LABELS = [ const THEMES = [ { id: 'theme', - label: 'Light theme', + label: _td('Light theme'), value: 'light', }, { id: 'theme', - label: 'Dark theme', + label: _td('Dark theme'), value: 'dark', }, ]; diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 7436f84f69..1d7e4bd217 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -22,7 +22,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg'; import ScalarAuthClient from '../../../ScalarAuthClient'; import SdkConfig from '../../../SdkConfig'; import Modal from '../../../Modal'; -import { _t } from '../../../languageHandler'; +import { _t, _td } from '../../../languageHandler'; import sdk from '../../../index'; import AppPermission from './AppPermission'; import AppWarning from './AppWarning'; @@ -170,9 +170,9 @@ export default React.createClass({ // These strings are translated at the point that they are inserted in to the DOM, in the render method _deleteWidgetLabel() { if (this._canUserModify()) { - return 'Delete widget'; + return _td('Delete widget'); } - return 'Revoke widget access'; + return _td('Revoke widget access'); }, /* TODO -- Store permission in account data so that it is persisted across multiple devices */ diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 37602a94ca..39666c94a4 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -30,7 +30,7 @@ import SlashCommands from '../../../SlashCommands'; import KeyCode from '../../../KeyCode'; import Modal from '../../../Modal'; import sdk from '../../../index'; -import { _t } from '../../../languageHandler'; +import { _t, _td } from '../../../languageHandler'; import Analytics from '../../../Analytics'; import dis from '../../../dispatcher'; @@ -1032,10 +1032,10 @@ export default class MessageComposerInput extends React.Component { buttons. */ getSelectionInfo(editorState: EditorState) { const styleName = { - BOLD: 'bold', - ITALIC: 'italic', - STRIKETHROUGH: 'strike', - UNDERLINE: 'underline', + BOLD: _td('bold'), + ITALIC: _td('italic'), + STRIKETHROUGH: _td('strike'), + UNDERLINE: _td('underline'), }; const originalStyle = editorState.getCurrentInlineStyle().toArray(); @@ -1044,10 +1044,10 @@ export default class MessageComposerInput extends React.Component { .filter((styleName) => !!styleName); const blockName = { - 'code-block': 'code', - 'blockquote': 'quote', - 'unordered-list-item': 'bullet', - 'ordered-list-item': 'numbullet', + 'code-block': _td('code'), + 'blockquote': _td('quote'), + 'unordered-list-item': _td('bullet'), + 'ordered-list-item': _td('numbullet'), }; const originalBlockType = editorState.getCurrentContent() .getBlockForKey(editorState.getSelection().getStartKey()) diff --git a/src/languageHandler.js b/src/languageHandler.js index 4455d58b04..12242a2e15 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -29,6 +29,12 @@ counterpart.setSeparator('|'); // Fall back to English counterpart.setFallbackLocale('en'); +// Function which only purpose is to mark that a string is translatable +// Does not actually do anything. It's helpful for automatic extraction of translatable strings +export function _td(s) { + return s; +} + // The translation function. This is just a simple wrapper to counterpart, // but exists mostly because we must use the same counterpart instance // between modules (ie. here (react-sdk) and the app (riot-web), and if we From cc441f42f9594f3e59b58ece05ccfaa2b99e4422 Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Sat, 23 Sep 2017 09:02:51 +0200 Subject: [PATCH 07/40] Some more translatable strings Signed-off-by: Stefan Parviainen --- src/components/views/dialogs/KeyShareDialog.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/views/dialogs/KeyShareDialog.js b/src/components/views/dialogs/KeyShareDialog.js index aed8e6a5af..c44e856382 100644 --- a/src/components/views/dialogs/KeyShareDialog.js +++ b/src/components/views/dialogs/KeyShareDialog.js @@ -18,7 +18,7 @@ import Modal from '../../../Modal'; import React from 'react'; import sdk from '../../../index'; -import { _t } from '../../../languageHandler'; +import { _t, _td } from '../../../languageHandler'; /** * Dialog which asks the user whether they want to share their keys with @@ -116,11 +116,11 @@ export default React.createClass({ let text; if (this.state.wasNewDevice) { - text = "You added a new device '%(displayName)s', which is" - + " requesting encryption keys."; + text = _td("You added a new device '%(displayName)s', which is" + + " requesting encryption keys."); } else { - text = "Your unverified device '%(displayName)s' is requesting" - + " encryption keys."; + text = _td("Your unverified device '%(displayName)s' is requesting" + + " encryption keys."); } text = _t(text, {displayName: displayName}); From 14bce1119c15feefee5813c8f4cd50ded7f76526 Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Sat, 23 Sep 2017 21:36:10 +0200 Subject: [PATCH 08/40] Make theme names translatable Signed-off-by: Stefan Parviainen --- src/components/structures/UserSettings.js | 2 +- src/i18n/strings/en_EN.json | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 572ee79a69..5e7658f056 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -793,7 +793,7 @@ module.exports = React.createClass({ onChange={ onChange } /> ; }, diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a0945d7f50..8b53f17d8c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -879,5 +879,7 @@ "Failed to remove the room from the summary of %(groupId)s": "Failed to remove the room from the summary of %(groupId)s", "The room '%(roomName)' could not be removed from the summary.": "The room '%(roomName)' could not be removed from the summary.", "Failed to remove a user from the summary of %(groupId)s": "Failed to remove a user from the summary of %(groupId)s", - "The user '%(displayName)s' could not be removed from the summary.": "The user '%(displayName)s' could not be removed from the summary." + "The user '%(displayName)s' could not be removed from the summary.": "The user '%(displayName)s' could not be removed from the summary.", + "Light theme": "Light theme", + "Dark theme": "Dark theme" } From dbae5a66e3a3b1b83bd08304c408a3939b11ed59 Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Sun, 24 Sep 2017 09:43:52 +0200 Subject: [PATCH 09/40] Use translation in img alt text Signed-off-by: Stefan Parviainen --- src/components/views/room_settings/AliasSettings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.js index f37bd4271a..ea3bad390f 100644 --- a/src/components/views/room_settings/AliasSettings.js +++ b/src/components/views/room_settings/AliasSettings.js @@ -299,7 +299,7 @@ module.exports = React.createClass({ blurToCancel={ false } onValueChanged={ self.onAliasAdded } />
- Add
: "" From 2b5b7080572940fed75960b76ede1725e7cb6b35 Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Sun, 24 Sep 2017 10:14:04 +0200 Subject: [PATCH 10/40] Translate unknown presence label Signed-off-by: Stefan Parviainen --- src/components/views/rooms/PresenceLabel.js | 2 +- src/i18n/strings/en_EN.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/PresenceLabel.js b/src/components/views/rooms/PresenceLabel.js index 47a723f5cd..87b218e2e2 100644 --- a/src/components/views/rooms/PresenceLabel.js +++ b/src/components/views/rooms/PresenceLabel.js @@ -70,7 +70,7 @@ module.exports = React.createClass({ if (presence === "online") return _t("Online"); if (presence === "unavailable") return _t("Idle"); // XXX: is this actually right? if (presence === "offline") return _t("Offline"); - return "Unknown"; + return _t("Unknown"); }, render: function() { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8b53f17d8c..f3e4b93e9c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -881,5 +881,6 @@ "Failed to remove a user from the summary of %(groupId)s": "Failed to remove a user from the summary of %(groupId)s", "The user '%(displayName)s' could not be removed from the summary.": "The user '%(displayName)s' could not be removed from the summary.", "Light theme": "Light theme", - "Dark theme": "Dark theme" + "Dark theme": "Dark theme", + "Unknown": "Unknown" } From 955ca6cd2b19dfab7cc30b2303ba05f9885876b3 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 29 Sep 2017 17:59:24 +0100 Subject: [PATCH 11/40] Implement button to remove a room from a group NB: This doesn't provide any feedback to the user. We should use a GroupSummaryStore-style component to refresh the view after a successful hit to the API. This could affect the summary view as well, because when rooms are removed from a group, they are also removed from the summary (if necessary). --- src/components/views/groups/GroupRoomTile.js | 10 ++++++++++ src/groups.js | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/views/groups/GroupRoomTile.js b/src/components/views/groups/GroupRoomTile.js index 452f862d16..1081890f40 100644 --- a/src/components/views/groups/GroupRoomTile.js +++ b/src/components/views/groups/GroupRoomTile.js @@ -49,6 +49,13 @@ const GroupRoomTile = React.createClass({ }); }, + onDeleteClick: function(e) { + e.preventDefault(); + e.stopPropagation(); + this.context.matrixClient + .removeRoomFromGroup(this.props.groupId, this.props.groupRoom.roomId); + }, + render: function() { const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); @@ -76,6 +83,9 @@ const GroupRoomTile = React.createClass({
{ name }
+ + + ); }, diff --git a/src/groups.js b/src/groups.js index 2ff95f7d65..69871c45e9 100644 --- a/src/groups.js +++ b/src/groups.js @@ -24,8 +24,7 @@ export const GroupMemberType = PropTypes.shape({ export const GroupRoomType = PropTypes.shape({ name: PropTypes.string, - // TODO: API doesn't return this yet - // roomId: PropTypes.string.isRequired, + roomId: PropTypes.string.isRequired, canonicalAlias: PropTypes.string, avatarUrl: PropTypes.string, }); @@ -41,6 +40,7 @@ export function groupMemberFromApiObject(apiObject) { export function groupRoomFromApiObject(apiObject) { return { name: apiObject.name, + roomId: apiObject.room_id, canonicalAlias: apiObject.canonical_alias, avatarUrl: apiObject.avatar_url, }; From b202601d650d981103fee66be869c47506a407d1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 2 Oct 2017 15:10:32 +0100 Subject: [PATCH 12/40] Fix showing 3pid invites in member list --- src/components/views/rooms/MemberList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index 86d44f29d6..d1b456025f 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -372,7 +372,7 @@ module.exports = React.createClass({ }, _getChildrenInvited: function(start, end) { - return this._makeMemberTiles(this.state.filteredInvitedMembers.slice(start, end)); + return this._makeMemberTiles(this.state.filteredInvitedMembers.slice(start, end), 'invite'); }, _getChildCountInvited: function() { From 3e34a460a411935ed994c469b8e87e6fa8b2f6d4 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 3 Oct 2017 10:06:18 +0100 Subject: [PATCH 13/40] Add error dialog for when removing room fails --- src/components/views/groups/GroupRoomTile.js | 34 +++++++++++++++----- src/i18n/strings/en_EN.json | 4 ++- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/components/views/groups/GroupRoomTile.js b/src/components/views/groups/GroupRoomTile.js index 1081890f40..eeba9d6d8a 100644 --- a/src/components/views/groups/GroupRoomTile.js +++ b/src/components/views/groups/GroupRoomTile.js @@ -21,6 +21,7 @@ import PropTypes from 'prop-types'; import sdk from '../../../index'; import dis from '../../../dispatcher'; import { GroupRoomType } from '../../../groups'; +import Modal from '../../../Modal'; const GroupRoomTile = React.createClass({ displayName: 'GroupRoomTile', @@ -31,7 +32,19 @@ const GroupRoomTile = React.createClass({ }, getInitialState: function() { - return {}; + return { + name: this.calculateRoomName(this.props.groupRoom), + }; + }, + + componentWillReceiveProps: function(newProps) { + this.setState({ + name: this.calculateRoomName(newProps.groupRoom), + }); + }, + + calculateRoomName: function(groupRoom) { + return groupRoom.name || groupRoom.canonicalAlias || _t("Unnamed Room"); }, onClick: function(e) { @@ -52,24 +65,29 @@ const GroupRoomTile = React.createClass({ onDeleteClick: function(e) { e.preventDefault(); e.stopPropagation(); + const groupId = this.props.groupId; + const roomName = this.state.name; this.context.matrixClient - .removeRoomFromGroup(this.props.groupId, this.props.groupRoom.roomId); + .removeRoomFromGroup(groupId, this.props.groupRoom.roomId) + .catch((err) => { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to remove room from group', '', ErrorDialog, { + title: _t("Failed to remove room from group"), + description: _t("Failed to remove '%(roomName)s' from %(groupId)s", {groupId, roomName}), + }); + }); }, render: function() { const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - - const name = this.props.groupRoom.name || - this.props.groupRoom.canonicalAlias || - _t("Unnamed Room"); const avatarUrl = this.context.matrixClient.mxcUrlToHttp( this.props.groupRoom.avatarUrl, 36, 36, 'crop', ); const av = ( - @@ -81,7 +99,7 @@ const GroupRoomTile = React.createClass({ { av }
- { name } + { this.state.name }
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 87fd6d4364..10e135fc6e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -895,5 +895,7 @@ "Matrix Room ID": "Matrix Room ID", "email address": "email address", "Try using one of the following valid address types: %(validTypesList)s.": "Try using one of the following valid address types: %(validTypesList)s.", - "You have entered an invalid address.": "You have entered an invalid address." + "You have entered an invalid address.": "You have entered an invalid address.", + "Failed to remove room from group": "Failed to remove room from group", + "Failed to remove '%(roomName)s' from %(groupId)s": "Failed to remove '%(roomName)s' from %(groupId)s" } From 0116c4b48681a9ba90348010d7691befcdbcf386 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 3 Oct 2017 10:14:08 +0100 Subject: [PATCH 14/40] Log the error when failing to removie room from group --- src/components/views/groups/GroupRoomTile.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/groups/GroupRoomTile.js b/src/components/views/groups/GroupRoomTile.js index eeba9d6d8a..ea8b5b9993 100644 --- a/src/components/views/groups/GroupRoomTile.js +++ b/src/components/views/groups/GroupRoomTile.js @@ -67,9 +67,11 @@ const GroupRoomTile = React.createClass({ e.stopPropagation(); const groupId = this.props.groupId; const roomName = this.state.name; + const roomId = this.props.groupRoom.roomId; this.context.matrixClient - .removeRoomFromGroup(groupId, this.props.groupRoom.roomId) + .removeRoomFromGroup(groupId, roomId) .catch((err) => { + console.error(`Error whilst removing ${roomId} from ${groupId}`, err); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Failed to remove room from group', '', ErrorDialog, { title: _t("Failed to remove room from group"), From 6b834bc72e5bd073e572928e3aa8c0ed3e533123 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 3 Oct 2017 11:16:22 +0100 Subject: [PATCH 15/40] Add confirmation dialog for removing room from group --- src/components/views/groups/GroupRoomTile.js | 42 ++++++++++++++------ src/i18n/strings/en_EN.json | 4 +- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/components/views/groups/GroupRoomTile.js b/src/components/views/groups/GroupRoomTile.js index ea8b5b9993..b6bdb9735b 100644 --- a/src/components/views/groups/GroupRoomTile.js +++ b/src/components/views/groups/GroupRoomTile.js @@ -47,6 +47,22 @@ const GroupRoomTile = React.createClass({ return groupRoom.name || groupRoom.canonicalAlias || _t("Unnamed Room"); }, + removeRoomFromGroup: function() { + const groupId = this.props.groupId; + const roomName = this.state.name; + const roomId = this.props.groupRoom.roomId; + this.context.matrixClient + .removeRoomFromGroup(groupId, roomId) + .catch((err) => { + console.error(`Error whilst removing ${roomId} from ${groupId}`, err); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to remove room from group', '', ErrorDialog, { + title: _t("Failed to remove room from group"), + description: _t("Failed to remove '%(roomName)s' from %(groupId)s", {groupId, roomName}), + }); + }); + }, + onClick: function(e) { let roomId; let roomAlias; @@ -63,21 +79,21 @@ const GroupRoomTile = React.createClass({ }, onDeleteClick: function(e) { - e.preventDefault(); - e.stopPropagation(); const groupId = this.props.groupId; const roomName = this.state.name; - const roomId = this.props.groupRoom.roomId; - this.context.matrixClient - .removeRoomFromGroup(groupId, roomId) - .catch((err) => { - console.error(`Error whilst removing ${roomId} from ${groupId}`, err); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to remove room from group', '', ErrorDialog, { - title: _t("Failed to remove room from group"), - description: _t("Failed to remove '%(roomName)s' from %(groupId)s", {groupId, roomName}), - }); - }); + e.preventDefault(); + e.stopPropagation(); + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + Modal.createTrackedDialog('Confirm removal of group from room', '', QuestionDialog, { + title: _t("Are you sure you want to remove '%(roomName)s' from %(groupId)s?", {roomName, groupId}), + description: _t("Removing a room from the group will also remove it from the group page."), + button: _t("Remove"), + onFinished: (success) => { + if (success) { + this.removeRoomFromGroup(); + } + }, + }); }, render: function() { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 10e135fc6e..bc377d8359 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -897,5 +897,7 @@ "Try using one of the following valid address types: %(validTypesList)s.": "Try using one of the following valid address types: %(validTypesList)s.", "You have entered an invalid address.": "You have entered an invalid address.", "Failed to remove room from group": "Failed to remove room from group", - "Failed to remove '%(roomName)s' from %(groupId)s": "Failed to remove '%(roomName)s' from %(groupId)s" + "Failed to remove '%(roomName)s' from %(groupId)s": "Failed to remove '%(roomName)s' from %(groupId)s", + "Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Are you sure you want to remove '%(roomName)s' from %(groupId)s?", + "Removing a room from the group will also remove it from the group page.": "Removing a room from the group will also remove it from the group page." } From 8243c39d830bfc6366fbe68b2856ea2c2277c29b Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 4 Oct 2017 10:00:01 +0100 Subject: [PATCH 16/40] Factor out EditableItemList from AliasSettings Such that we can reuse the same UI elsewhere, namely when editing related groups of a room (which is an upcoming feature). --- .../views/elements/EditableItemList.js | 162 ++++++++++++++++++ .../views/room_settings/AliasSettings.js | 116 +++++-------- 2 files changed, 204 insertions(+), 74 deletions(-) create mode 100644 src/components/views/elements/EditableItemList.js diff --git a/src/components/views/elements/EditableItemList.js b/src/components/views/elements/EditableItemList.js new file mode 100644 index 0000000000..1d269f26e2 --- /dev/null +++ b/src/components/views/elements/EditableItemList.js @@ -0,0 +1,162 @@ +/* +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. +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 React from 'react'; +import PropTypes from 'prop-types'; +import sdk from '../../../index'; +import {_t} from '../../../languageHandler.js'; + +const EditableItem = React.createClass({ + displayName: 'EditableItem', + + propTypes: { + initialValue: PropTypes.string, + index: PropTypes.number, + placeholder: PropTypes.string, + + onChange: PropTypes.func, + onRemove: PropTypes.func, + onAdd: PropTypes.func, + }, + + onChange: function(value) { + this.setState({ value }); + if (this.props.onChange) this.props.onChange(value, this.props.index); + }, + + onRemove: function() { + this.props.onRemove(this.props.index); + }, + + onAdd: function() { + this.props.onAdd(this.state.value); + }, + + render: function() { + const EditableText = sdk.getComponent('elements.EditableText'); + return
+ + { this.props.onAdd ? +
+ {_t("Add")} +
+ : +
+ {_t("Delete")} +
+ } +
; + }, +}); + +module.exports = React.createClass({ + displayName: 'EditableItemList', + + propTypes: { + items: PropTypes.arrayOf(PropTypes.string).isRequired, + onNewItemChanged: PropTypes.func, + onItemAdded: PropTypes.func, + onItemEdited: PropTypes.func, + onItemRemoved: PropTypes. func, + }, + + getDefaultProps: function() { + return { + onItemAdded: () => {}, + onItemEdited: () => {}, + onItemRemoved: () => {}, + }; + }, + + getInitialState: function() { + return {}; + }, + + componentWillReceiveProps: function(nextProps) { + }, + + componentWillMount: function() { + }, + + componentDidMount: function() { + }, + + onItemAdded: function(value) { + console.info('onItemAdded', value); + this.props.onItemAdded(value); + }, + + onItemEdited: function(value, index) { + console.info('onItemEdited', value, index); + if (value.length === 0) { + this.onItemRemoved(index); + } else { + this.onItemEdited(value, index); + } + }, + + onItemRemoved: function(index) { + console.info('onItemRemoved', index); + this.props.onItemRemoved(index); + }, + + onNewItemChanged: function(value) { + this.props.onNewItemChanged(value); + }, + + render: function() { + const editableItems = this.props.items.map((item, index) => { + return ; + }); + + const label = this.props.items.length > 0 ? + this.props.itemsLabel : this.props.noItemsLabel; + + console.info('New item:', this.props.newItem); + + return (
+
+ { label } +
+ { editableItems } + +
); + }, +}); diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.js index f37bd4271a..8a6abec35d 100644 --- a/src/components/views/room_settings/AliasSettings.js +++ b/src/components/views/room_settings/AliasSettings.js @@ -136,24 +136,25 @@ module.exports = React.createClass({ return ObjectUtils.getKeyValueArrayDiffs(oldAliases, this.state.domainToAliases); }, - onAliasAdded: function(alias) { + onNewAliasChanged: function(value) { + this.setState({newAlias: value}); + }, + + onLocalAliasAdded: function(alias) { if (!alias || alias.length === 0) return; // ignore attempts to create blank aliases - if (this.isAliasValid(alias)) { - // add this alias to the domain to aliases dict - var domain = alias.replace(/^.*?:/, ''); - // XXX: do we need to deep copy aliases before editing it? - this.state.domainToAliases[domain] = this.state.domainToAliases[domain] || []; - this.state.domainToAliases[domain].push(alias); - this.setState({ - domainToAliases: this.state.domainToAliases - }); + const localDomain = MatrixClientPeg.get().getDomain(); + if (this.isAliasValid(alias) && alias.endsWith(localDomain)) { + this.state.domainToAliases[localDomain] = this.state.domainToAliases[localDomain] || []; + this.state.domainToAliases[localDomain].push(alias); - // reset the add field - this.refs.add_alias.setValue(''); // FIXME - } - else { - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + this.setState({ + domainToAliases: this.state.domainToAliases, + // Reset the add field + newAlias: "", + }); + } else { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Invalid alias format', '', ErrorDialog, { title: _t('Invalid alias format'), description: _t('\'%(alias)s\' is not a valid format for an alias', { alias: alias }), @@ -161,15 +162,13 @@ module.exports = React.createClass({ } }, - onAliasChanged: function(domain, index, alias) { + onLocalAliasChanged: function(alias, index) { if (alias === "") return; // hit the delete button to delete please - var oldAlias; - if (this.isAliasValid(alias)) { - oldAlias = this.state.domainToAliases[domain][index]; - this.state.domainToAliases[domain][index] = alias; - } - else { - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + const localDomain = MatrixClientPeg.get().getDomain(); + if (this.isAliasValid(alias) && alias.endsWith(localDomain)) { + this.state.domainToAliases[localDomain][index] = alias; + } else { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Invalid address format', '', ErrorDialog, { title: _t('Invalid address format'), description: _t('\'%(alias)s\' is not a valid format for an address', { alias: alias }), @@ -177,15 +176,16 @@ module.exports = React.createClass({ } }, - onAliasDeleted: function(domain, index) { + onLocalAliasDeleted: function(index) { + const localDomain = MatrixClientPeg.get().getDomain(); // It's a bit naughty to directly manipulate this.state, and React would // normally whine at you, but it can't see us doing the splice. Given we // promptly setState anyway, it's just about acceptable. The alternative // would be to arbitrarily deepcopy to a temp variable and then setState // that, but why bother when we can cut this corner. - var alias = this.state.domainToAliases[domain].splice(index, 1); + this.state.domainToAliases[localDomain].splice(index, 1); this.setState({ - domainToAliases: this.state.domainToAliases + domainToAliases: this.state.domainToAliases, }); }, @@ -198,6 +198,7 @@ module.exports = React.createClass({ render: function() { var self = this; var EditableText = sdk.getComponent("elements.EditableText"); + var EditableItemList = sdk.getComponent("elements.EditableItemList"); var localDomain = MatrixClientPeg.get().getDomain(); var canonical_alias_section; @@ -257,58 +258,25 @@ module.exports = React.createClass({
{ _t('The main address for this room is') }: { canonical_alias_section }
-
- { (this.state.domainToAliases[localDomain] && - this.state.domainToAliases[localDomain].length > 0) - ? _t('Local addresses for this room:') - : _t('This room has no local addresses') } -
-
- { (this.state.domainToAliases[localDomain] || []).map((alias, i) => { - var deleteButton; - if (this.props.canSetAliases) { - deleteButton = ( - { - ); - } - return ( -
- -
- { deleteButton } -
-
- ); - })} - { this.props.canSetAliases ? -
- -
- Add -
-
: "" - } -
+ { remote_aliases_section } ); - } + }, }); From d25ebfb8447385d53a37aaaaceca31798003808a Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 4 Oct 2017 10:15:44 +0100 Subject: [PATCH 17/40] Remove cruft from EIL --- .../views/elements/EditableItemList.js | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/src/components/views/elements/EditableItemList.js b/src/components/views/elements/EditableItemList.js index 1d269f26e2..6675e162c4 100644 --- a/src/components/views/elements/EditableItemList.js +++ b/src/components/views/elements/EditableItemList.js @@ -89,29 +89,15 @@ module.exports = React.createClass({ onItemAdded: () => {}, onItemEdited: () => {}, onItemRemoved: () => {}, + onNewItemChanged: () => {}, }; }, - getInitialState: function() { - return {}; - }, - - componentWillReceiveProps: function(nextProps) { - }, - - componentWillMount: function() { - }, - - componentDidMount: function() { - }, - onItemAdded: function(value) { - console.info('onItemAdded', value); this.props.onItemAdded(value); }, onItemEdited: function(value, index) { - console.info('onItemEdited', value, index); if (value.length === 0) { this.onItemRemoved(index); } else { @@ -120,7 +106,6 @@ module.exports = React.createClass({ }, onItemRemoved: function(index) { - console.info('onItemRemoved', index); this.props.onItemRemoved(index); }, @@ -143,8 +128,6 @@ module.exports = React.createClass({ const label = this.props.items.length > 0 ? this.props.itemsLabel : this.props.noItemsLabel; - console.info('New item:', this.props.newItem); - return (
{ label } From 407a632a8d51add81d4fd9df97f93406170618a8 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 4 Oct 2017 10:26:43 +0100 Subject: [PATCH 18/40] Fix typo --- src/components/views/room_settings/AliasSettings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.js index 8a6abec35d..8bccac0f93 100644 --- a/src/components/views/room_settings/AliasSettings.js +++ b/src/components/views/room_settings/AliasSettings.js @@ -263,7 +263,7 @@ module.exports = React.createClass({ className={"mx_RoomSettings_localAliases"} items={this.state.domainToAliases[localDomain] || []} newItem={this.state.newAlias} - onNewItemChanged={onNewAliasChanged} + onNewItemChanged={this.onNewAliasChanged} onItemAdded={this.onLocalAliasAdded} onItemEdited={this.onLocalAliasChanged} onItemRemoved={this.onLocalAliasDeleted} From 4e9694be6d4f7817d7f39536b55349c127c59765 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 4 Oct 2017 10:28:26 +0100 Subject: [PATCH 19/40] Maintain "blur to add" function to match previous UX --- src/components/views/elements/EditableItemList.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/EditableItemList.js b/src/components/views/elements/EditableItemList.js index 6675e162c4..267244759a 100644 --- a/src/components/views/elements/EditableItemList.js +++ b/src/components/views/elements/EditableItemList.js @@ -30,19 +30,22 @@ const EditableItem = React.createClass({ onChange: PropTypes.func, onRemove: PropTypes.func, onAdd: PropTypes.func, + + addOnChange: PropTypes.bool, }, onChange: function(value) { this.setState({ value }); if (this.props.onChange) this.props.onChange(value, this.props.index); + if (this.props.addOnChange && this.props.onAdd) this.props.onAdd(value); }, onRemove: function() { - this.props.onRemove(this.props.index); + if (this.props.onRemove) this.props.onRemove(this.props.index); }, onAdd: function() { - this.props.onAdd(this.state.value); + if (this.props.onAdd) this.props.onAdd(this.state.value); }, render: function() { @@ -138,6 +141,7 @@ module.exports = React.createClass({ initialValue={this.props.newItem} onAdd={this.onItemAdded} onChange={this.onNewItemChanged} + addOnChange={true} placeholder={this.props.placeholder} />
); From 9e3954865a2de182503422c19f93648c35c5eccf Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 4 Oct 2017 13:15:38 +0100 Subject: [PATCH 20/40] Fix a couple of bugs with EditableItemList - fix entering the same thing twice (which had the bug of not emptying the "new" field) - fix editing items in the list (which would stack overflow because of typo) --- src/components/views/elements/EditableItemList.js | 2 +- src/components/views/elements/EditableText.js | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/EditableItemList.js b/src/components/views/elements/EditableItemList.js index 267244759a..35e207daef 100644 --- a/src/components/views/elements/EditableItemList.js +++ b/src/components/views/elements/EditableItemList.js @@ -104,7 +104,7 @@ module.exports = React.createClass({ if (value.length === 0) { this.onItemRemoved(index); } else { - this.onItemEdited(value, index); + this.props.onItemEdited(value, index); } }, diff --git a/src/components/views/elements/EditableText.js b/src/components/views/elements/EditableText.js index 3ce8c90447..b6a0ec1d5c 100644 --- a/src/components/views/elements/EditableText.js +++ b/src/components/views/elements/EditableText.js @@ -65,7 +65,9 @@ module.exports = React.createClass({ }, componentWillReceiveProps: function(nextProps) { - if (nextProps.initialValue !== this.props.initialValue) { + if (nextProps.initialValue !== this.props.initialValue || + nextProps.initialValue !== this.value + ) { this.value = nextProps.initialValue; if (this.refs.editable_div) { this.showPlaceholder(!this.value); From 7be5e685f74be2e79b341a1e61a8be478d05d2f6 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 4 Oct 2017 13:19:57 +0100 Subject: [PATCH 21/40] Implement UI for editing related groups of a room (using the new EditableItemList) --- .../room_settings/RelatedGroupSettings.js | 130 ++++++++++++++++++ src/components/views/rooms/RoomSettings.js | 14 ++ src/i18n/strings/en_EN.json | 6 +- 3 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 src/components/views/room_settings/RelatedGroupSettings.js diff --git a/src/components/views/room_settings/RelatedGroupSettings.js b/src/components/views/room_settings/RelatedGroupSettings.js new file mode 100644 index 0000000000..942251e54b --- /dev/null +++ b/src/components/views/room_settings/RelatedGroupSettings.js @@ -0,0 +1,130 @@ +/* +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. +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 React from 'react'; +import {MatrixEvent, MatrixClient} from 'matrix-js-sdk'; +// var ObjectUtils = require("../../../ObjectUtils"); +// var MatrixClientPeg = require('../../../MatrixClientPeg'); +import sdk from '../../../index'; +import { _t } from '../../../languageHandler'; +import Modal from '../../../Modal'; + +const GROUP_ID_REGEX = /\+\S+\:\S+/; + +module.exports = React.createClass({ + displayName: 'RelatedGroupSettings', + + propTypes: { + roomId: React.PropTypes.string.isRequired, + canSetRelatedRooms: React.PropTypes.bool.isRequired, + relatedGroupsEvent: React.PropTypes.instanceOf(MatrixEvent), + }, + + contextTypes: { + matrixClient: React.PropTypes.instanceOf(MatrixClient), + }, + + getDefaultProps: function() { + return { + canSetRelatedRooms: false, + }; + }, + + getInitialState: function() { + return { + newGroupsList: this.props.relatedGroupsEvent ? + (this.props.relatedGroupsEvent.getContent().groups || []) : [], + newGroupId: null, + }; + }, + + saveSettings: function() { + return this.context.matrixClient.sendStateEvent( + this.props.roomId, + 'm.room.related_groups', + { + groups: this.state.newGroupsList, + }, + '', + ); + }, + + validateGroupId: function(groupId) { + const localDomain = this.context.matrixClient.getDomain(); + if (!GROUP_ID_REGEX.test(groupId) || !groupId.endsWith(localDomain)) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Invalid related group ID', '', ErrorDialog, { + title: _t('Invalid group ID'), + description: _t('\'%(groupId)s\' is not a valid group ID', { groupId }), + }); + return false; + } + return true; + }, + + onNewGroupChanged: function(newGroupId) { + this.setState({ newGroupId }); + }, + + onGroupAdded: function(groupId) { + if (groupId.length === 0 || !this.validateGroupId(groupId)) { + return; + } + this.setState({ + newGroupsList: this.state.newGroupsList.concat([groupId]), + newGroupId: '', + }); + }, + + onGroupEdited: function(groupId, index) { + if (groupId.length === 0 || !this.validateGroupId(groupId)) { + return; + } + this.setState({ + newGroupsList: Object.assign(this.state.newGroupsList, {[index]: groupId}), + }); + }, + + onGroupDeleted: function(index) { + const newGroupsList = this.state.newGroupsList.slice(); + newGroupsList.splice(index, 1), + this.setState({ newGroupsList }); + }, + + render: function() { + const localDomain = this.context.matrixClient.getDomain(); + const EditableItemList = sdk.getComponent('elements.EditableItemList'); + console.info(this.state.newGroupsList); + return (
+

{ _t('Related Groups') }

+ +
); + }, +}); diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 0fea50d2fa..d1d32d8c71 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -287,6 +287,9 @@ module.exports = React.createClass({ promises.push(ps); } + // related groups + promises.push(this.saveRelatedGroups()); + // encryption p = this.saveEnableEncryption(); if (!p.isFulfilled()) { @@ -304,6 +307,11 @@ module.exports = React.createClass({ return this.refs.alias_settings.saveSettings(); }, + saveRelatedGroups: function() { + if (!this.refs.related_groups) { return Promise.resolve(); } + return this.refs.related_groups.saveSettings(); + }, + saveColor: function() { if (!this.refs.color_settings) { return Promise.resolve(); } return this.refs.color_settings.saveSettings(); @@ -590,6 +598,7 @@ module.exports = React.createClass({ var AliasSettings = sdk.getComponent("room_settings.AliasSettings"); var ColorSettings = sdk.getComponent("room_settings.ColorSettings"); var UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings"); + var RelatedGroupSettings = sdk.getComponent("room_settings.RelatedGroupSettings"); var EditableText = sdk.getComponent('elements.EditableText'); var PowerSelector = sdk.getComponent('elements.PowerSelector'); var Loader = sdk.getComponent("elements.Spinner"); @@ -846,6 +855,11 @@ module.exports = React.createClass({ canonicalAliasEvent={this.props.room.currentState.getStateEvents('m.room.canonical_alias', '')} aliasEvents={this.props.room.currentState.getStateEvents('m.room.aliases')} /> + +

{ _t('Permissions') }

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 6a8c848f14..6acaba9fae 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -902,5 +902,9 @@ "Failed to remove room from group": "Failed to remove room from group", "Failed to remove '%(roomName)s' from %(groupId)s": "Failed to remove '%(roomName)s' from %(groupId)s", "Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Are you sure you want to remove '%(roomName)s' from %(groupId)s?", - "Removing a room from the group will also remove it from the group page.": "Removing a room from the group will also remove it from the group page." + "Removing a room from the group will also remove it from the group page.": "Removing a room from the group will also remove it from the group page.", + "Related Groups": "Related Groups", + "Related groups for this room:": "Related groups for this room:", + "This room has no related groups": "This room has no related groups", + "New group ID (e.g. +foo:%(localDomain)s)": "New group ID (e.g. +foo:%(localDomain)s)" } From 8d46b199168ec54c373317e7e53a51dbfe0409d9 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 4 Oct 2017 14:06:49 +0100 Subject: [PATCH 22/40] Restrict Flair in the timeline to related groups of the room --- src/components/views/elements/Flair.js | 30 +++++++++++++++++++ .../views/messages/SenderProfile.js | 8 ++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/Flair.js b/src/components/views/elements/Flair.js index 61df660fd5..84c0c2a187 100644 --- a/src/components/views/elements/Flair.js +++ b/src/components/views/elements/Flair.js @@ -183,10 +183,12 @@ export default class Flair extends React.Component { this.state = { profiles: [], }; + this.onRoomStateEvents = this.onRoomStateEvents.bind(this); } componentWillUnmount() { this._unmounted = true; + this.context.matrixClient.removeListener('RoomState.events', this.onRoomStateEvents); } componentWillMount() { @@ -194,6 +196,13 @@ export default class Flair extends React.Component { if (UserSettingsStore.isFeatureEnabled('feature_groups') && groupSupport) { this._generateAvatars(); } + this.context.matrixClient.on('RoomState.events', this.onRoomStateEvents); + } + + onRoomStateEvents(event) { + if (event.getType() === 'm.room.related_groups' && groupSupport) { + this._generateAvatars(); + } } async _getGroupProfiles(groups) { @@ -224,6 +233,21 @@ export default class Flair extends React.Component { } console.error('Could not get groups for user', this.props.userId, err); } + if (this.props.roomId && this.props.showRelated) { + const relatedGroupsEvent = this.context.matrixClient + .getRoom(this.props.roomId) + .currentState + .getStateEvents('m.room.related_groups', ''); + const relatedGroups = relatedGroupsEvent ? + relatedGroupsEvent.getContent().groups || [] : []; + if (relatedGroups && relatedGroups.length > 0) { + groups = groups.filter((groupId) => { + return relatedGroups.includes(groupId); + }); + } else { + groups = []; + } + } if (!groups || groups.length === 0) { return; } @@ -250,6 +274,12 @@ export default class Flair extends React.Component { Flair.propTypes = { userId: PropTypes.string, + + // Whether to show only the flair associated with related groups of the given room, + // or all flair associated with a user. + showRelated: PropTypes.bool, + // The room that this flair will be displayed in. Optional. Only applies when showRelated = true. + roomId: PropTypes.string, }; // TODO: We've decided that all components should follow this pattern, which means removing withMatrixClient and using diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index 0311239e7a..63e3144115 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -33,7 +33,13 @@ export default function SenderProfile(props) { return (
{ name || '' } - { props.enableFlair ? : null } + { props.enableFlair ? + + : null + } { props.aux ? { props.aux } : null }
); From 02e7287123b9fe70e2baef9bd581e5ab2132077c Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 4 Oct 2017 14:08:31 +0100 Subject: [PATCH 23/40] Remove constraint on groups being local, remove logging --- src/components/views/room_settings/RelatedGroupSettings.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/views/room_settings/RelatedGroupSettings.js b/src/components/views/room_settings/RelatedGroupSettings.js index 942251e54b..d3be2c88b5 100644 --- a/src/components/views/room_settings/RelatedGroupSettings.js +++ b/src/components/views/room_settings/RelatedGroupSettings.js @@ -64,8 +64,7 @@ module.exports = React.createClass({ }, validateGroupId: function(groupId) { - const localDomain = this.context.matrixClient.getDomain(); - if (!GROUP_ID_REGEX.test(groupId) || !groupId.endsWith(localDomain)) { + if (!GROUP_ID_REGEX.test(groupId)) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Invalid related group ID', '', ErrorDialog, { title: _t('Invalid group ID'), @@ -108,7 +107,6 @@ module.exports = React.createClass({ render: function() { const localDomain = this.context.matrixClient.getDomain(); const EditableItemList = sdk.getComponent('elements.EditableItemList'); - console.info(this.state.newGroupsList); return (

{ _t('Related Groups') }

Date: Wed, 4 Oct 2017 14:09:51 +0100 Subject: [PATCH 24/40] Remove commented imports --- src/components/views/room_settings/RelatedGroupSettings.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/views/room_settings/RelatedGroupSettings.js b/src/components/views/room_settings/RelatedGroupSettings.js index d3be2c88b5..60bdbf1481 100644 --- a/src/components/views/room_settings/RelatedGroupSettings.js +++ b/src/components/views/room_settings/RelatedGroupSettings.js @@ -14,11 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -// import Promise from 'bluebird'; import React from 'react'; import {MatrixEvent, MatrixClient} from 'matrix-js-sdk'; -// var ObjectUtils = require("../../../ObjectUtils"); -// var MatrixClientPeg = require('../../../MatrixClientPeg'); import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import Modal from '../../../Modal'; From ed74ac394bfa5d44240ac0cedfccda22576edddb Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 4 Oct 2017 14:35:13 +0100 Subject: [PATCH 25/40] Put related groups UI behind groups labs flag --- src/components/views/rooms/RoomSettings.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index d1d32d8c71..b542dbaf55 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -631,6 +631,14 @@ module.exports = React.createClass({ var self = this; + let relatedGroupsSection; + if (UserSettingsStore.isFeatureEnabled('feature_groups')) { + relatedGroupsSection = ; + } + var userLevelsSection; if (Object.keys(user_levels).length) { userLevelsSection = @@ -855,10 +863,7 @@ module.exports = React.createClass({ canonicalAliasEvent={this.props.room.currentState.getStateEvents('m.room.canonical_alias', '')} aliasEvents={this.props.room.currentState.getStateEvents('m.room.aliases')} /> - + { relatedGroupsSection } From 4017fa7f1d4581423feff35cdf070bcddfe0b5e2 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 4 Oct 2017 16:56:35 +0100 Subject: [PATCH 26/40] Factor-out GroupStore and create GroupStoreCache In order to provide feedback when adding a room to a group, the group summarry store needs to be extended to store the list of rooms in a group. This commit is the first step required. The next step is to get the GroupRoomList listening to updates from GroupStore and expose the list of rooms from GroupStore. (We're running out of words to describe the hierachy of things that store things) --- src/GroupAddressPicker.js | 6 ++- src/components/structures/GroupView.js | 49 +++++++++---------- .../{GroupSummaryStore.js => GroupStore.js} | 10 +++- src/stores/GroupStoreCache.js | 37 ++++++++++++++ 4 files changed, 73 insertions(+), 29 deletions(-) rename src/stores/{GroupSummaryStore.js => GroupStore.js} (89%) create mode 100644 src/stores/GroupStoreCache.js diff --git a/src/GroupAddressPicker.js b/src/GroupAddressPicker.js index 00eed78f76..cfd2590780 100644 --- a/src/GroupAddressPicker.js +++ b/src/GroupAddressPicker.js @@ -19,6 +19,7 @@ import sdk from './'; import MultiInviter from './utils/MultiInviter'; import { _t } from './languageHandler'; import MatrixClientPeg from './MatrixClientPeg'; +import GroupStoreCache from './stores/GroupStoreCache'; export function showGroupInviteDialog(groupId) { const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog"); @@ -86,10 +87,11 @@ function _onGroupInviteFinished(groupId, addrs) { } function _onGroupAddRoomFinished(groupId, addrs) { + const groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), groupId); const errorList = []; return Promise.all(addrs.map((addr) => { - return MatrixClientPeg.get() - .addRoomToGroup(groupId, addr.address) + return groupStore + .addRoomToGroup(addr.address) .catch(() => { errorList.push(addr.address); }) .reflect(); })).then(() => { diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 5381f9add3..337ac6ab75 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -27,7 +27,8 @@ import AccessibleButton from '../views/elements/AccessibleButton'; import Modal from '../../Modal'; import classnames from 'classnames'; -import GroupSummaryStore from '../../stores/GroupSummaryStore'; +import GroupStoreCache from '../../stores/GroupStoreCache'; +import GroupStore from '../../stores/GroupStore'; const RoomSummaryType = PropTypes.shape({ room_id: PropTypes.string.isRequired, @@ -78,7 +79,7 @@ const CategoryRoomList = React.createClass({ if (!success) return; const errorList = []; Promise.all(addrs.map((addr) => { - return this.context.groupSummaryStore + return this.context.groupStore .addRoomToGroupSummary(addr.address) .catch(() => { errorList.push(addr.address); }) .reflect(); @@ -157,7 +158,7 @@ const FeaturedRoom = React.createClass({ onDeleteClicked: function(e) { e.preventDefault(); e.stopPropagation(); - this.context.groupSummaryStore.removeRoomFromGroupSummary( + this.context.groupStore.removeRoomFromGroupSummary( this.props.summaryInfo.room_id, ).catch((err) => { console.error('Error whilst removing room from group summary', err); @@ -252,7 +253,7 @@ const RoleUserList = React.createClass({ if (!success) return; const errorList = []; Promise.all(addrs.map((addr) => { - return this.context.groupSummaryStore + return this.context.groupStore .addUserToGroupSummary(addr.address) .catch(() => { errorList.push(addr.address); }) .reflect(); @@ -327,7 +328,7 @@ const FeaturedUser = React.createClass({ onDeleteClicked: function(e) { e.preventDefault(); e.stopPropagation(); - this.context.groupSummaryStore.removeUserFromGroupSummary( + this.context.groupStore.removeUserFromGroupSummary( this.props.summaryInfo.user_id, ).catch((err) => { console.error('Error whilst removing user from group summary', err); @@ -373,14 +374,14 @@ const FeaturedUser = React.createClass({ }, }); -const GroupSummaryContext = { - groupSummaryStore: React.PropTypes.instanceOf(GroupSummaryStore).isRequired, +const GroupContext = { + groupStore: React.PropTypes.instanceOf(GroupStore).isRequired, }; -CategoryRoomList.contextTypes = GroupSummaryContext; -FeaturedRoom.contextTypes = GroupSummaryContext; -RoleUserList.contextTypes = GroupSummaryContext; -FeaturedUser.contextTypes = GroupSummaryContext; +CategoryRoomList.contextTypes = GroupContext; +FeaturedRoom.contextTypes = GroupContext; +RoleUserList.contextTypes = GroupContext; +FeaturedUser.contextTypes = GroupContext; export default React.createClass({ displayName: 'GroupView', @@ -390,12 +391,12 @@ export default React.createClass({ }, childContextTypes: { - groupSummaryStore: React.PropTypes.instanceOf(GroupSummaryStore), + groupStore: React.PropTypes.instanceOf(GroupStore), }, getChildContext: function() { return { - groupSummaryStore: this._groupSummaryStore, + groupStore: this._groupStore, }; }, @@ -413,14 +414,14 @@ export default React.createClass({ componentWillMount: function() { this._changeAvatarComponent = null; - this._initGroupSummaryStore(this.props.groupId); + this._initGroupStore(this.props.groupId); MatrixClientPeg.get().on("Group.myMembership", this._onGroupMyMembership); }, componentWillUnmount: function() { MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership); - this._groupSummaryStore.removeAllListeners(); + this._groupStore.removeAllListeners(); }, componentWillReceiveProps: function(newProps) { @@ -429,7 +430,7 @@ export default React.createClass({ summary: null, error: null, }, () => { - this._initGroupSummaryStore(newProps.groupId); + this._initGroupStore(newProps.groupId); }); } }, @@ -440,17 +441,15 @@ export default React.createClass({ this.setState({membershipBusy: false}); }, - _initGroupSummaryStore: function(groupId) { - this._groupSummaryStore = new GroupSummaryStore( - MatrixClientPeg.get(), this.props.groupId, - ); - this._groupSummaryStore.on('update', () => { + _initGroupStore: function(groupId) { + this._groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), groupId); + this._groupStore.on('update', () => { this.setState({ - summary: this._groupSummaryStore.getSummary(), + summary: this._groupStore.getSummary(), error: null, }); }); - this._groupSummaryStore.on('error', (err) => { + this._groupStore.on('error', (err) => { this.setState({ summary: null, error: err, @@ -527,7 +526,7 @@ export default React.createClass({ editing: false, summary: null, }); - this._initGroupSummaryStore(this.props.groupId); + this._initGroupStore(this.props.groupId); }).catch((e) => { this.setState({ saving: false, @@ -606,7 +605,7 @@ export default React.createClass({ this.setState({ publicityBusy: true, }); - this._groupSummaryStore.setGroupPublicity(publicity).then(() => { + this._groupStore.setGroupPublicity(publicity).then(() => { this.setState({ publicityBusy: false, }); diff --git a/src/stores/GroupSummaryStore.js b/src/stores/GroupStore.js similarity index 89% rename from src/stores/GroupSummaryStore.js rename to src/stores/GroupStore.js index aa6e74529b..cffffd8b2e 100644 --- a/src/stores/GroupSummaryStore.js +++ b/src/stores/GroupStore.js @@ -17,9 +17,10 @@ limitations under the License. import EventEmitter from 'events'; /** - * Stores the group summary for a room and provides an API to change it + * Stores the group summary for a room and provides an API to change it and + * other useful group APIs may have an effect on the group summary. */ -export default class GroupSummaryStore extends EventEmitter { +export default class GroupStore extends EventEmitter { constructor(matrixClient, groupId) { super(); this._groupId = groupId; @@ -45,6 +46,11 @@ export default class GroupSummaryStore extends EventEmitter { return this._summary; } + addRoomToGroup(roomId) { + return this._matrixClient + .addRoomToGroup(this._groupId, roomId); + } + addRoomToGroupSummary(roomId, categoryId) { return this._matrixClient .addRoomToGroupSummary(this._groupId, roomId, categoryId) diff --git a/src/stores/GroupStoreCache.js b/src/stores/GroupStoreCache.js new file mode 100644 index 0000000000..ade0445e97 --- /dev/null +++ b/src/stores/GroupStoreCache.js @@ -0,0 +1,37 @@ +/* +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. +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 GroupStore from './GroupStore'; + +class GroupStoreCache { + constructor() { + this.groupStores = {}; + } + + getGroupStore(matrixClient, groupId) { + if (!this.groupStores[groupId]) { + this.groupStores[groupId] = new GroupStore(matrixClient, groupId); + } + return this.groupStores[groupId]; + } +} + + +let singletonGroupStoreCache = null; +if (!singletonGroupStoreCache) { + singletonGroupStoreCache = new GroupStoreCache(); +} +module.exports = singletonGroupStoreCache; From b16eb1713e3ad231d9b578d9b3259813d8cff60e Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 4 Oct 2017 17:01:44 +0100 Subject: [PATCH 27/40] Typo --- src/stores/GroupStore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/GroupStore.js b/src/stores/GroupStore.js index cffffd8b2e..f15561bbb0 100644 --- a/src/stores/GroupStore.js +++ b/src/stores/GroupStore.js @@ -18,7 +18,7 @@ import EventEmitter from 'events'; /** * Stores the group summary for a room and provides an API to change it and - * other useful group APIs may have an effect on the group summary. + * other useful group APIs that may have an effect on the group summary. */ export default class GroupStore extends EventEmitter { constructor(matrixClient, groupId) { From c1318e91024323fe466a3092f2d664dc7716b8f7 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 4 Oct 2017 17:51:38 +0100 Subject: [PATCH 28/40] Only maintain one GroupStore in the GroupStoreCache So that the group store data is up-to-date and to prevent group stores hanging around in memory --- src/stores/GroupStore.js | 16 ++++++++-------- src/stores/GroupStoreCache.js | 12 +++++++----- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/stores/GroupStore.js b/src/stores/GroupStore.js index f15561bbb0..941f4c8ec2 100644 --- a/src/stores/GroupStore.js +++ b/src/stores/GroupStore.js @@ -23,14 +23,14 @@ import EventEmitter from 'events'; export default class GroupStore extends EventEmitter { constructor(matrixClient, groupId) { super(); - this._groupId = groupId; + this.groupId = groupId; this._matrixClient = matrixClient; this._summary = {}; this._fetchSummary(); } _fetchSummary() { - this._matrixClient.getGroupSummary(this._groupId).then((resp) => { + this._matrixClient.getGroupSummary(this.groupId).then((resp) => { this._summary = resp; this._notifyListeners(); }).catch((err) => { @@ -48,36 +48,36 @@ export default class GroupStore extends EventEmitter { addRoomToGroup(roomId) { return this._matrixClient - .addRoomToGroup(this._groupId, roomId); + .addRoomToGroup(this.groupId, roomId); } addRoomToGroupSummary(roomId, categoryId) { return this._matrixClient - .addRoomToGroupSummary(this._groupId, roomId, categoryId) + .addRoomToGroupSummary(this.groupId, roomId, categoryId) .then(this._fetchSummary.bind(this)); } addUserToGroupSummary(userId, roleId) { return this._matrixClient - .addUserToGroupSummary(this._groupId, userId, roleId) + .addUserToGroupSummary(this.groupId, userId, roleId) .then(this._fetchSummary.bind(this)); } removeRoomFromGroupSummary(roomId) { return this._matrixClient - .removeRoomFromGroupSummary(this._groupId, roomId) + .removeRoomFromGroupSummary(this.groupId, roomId) .then(this._fetchSummary.bind(this)); } removeUserFromGroupSummary(userId) { return this._matrixClient - .removeUserFromGroupSummary(this._groupId, userId) + .removeUserFromGroupSummary(this.groupId, userId) .then(this._fetchSummary.bind(this)); } setGroupPublicity(isPublished) { return this._matrixClient - .setGroupPublicity(this._groupId, isPublished) + .setGroupPublicity(this.groupId, isPublished) .then(this._fetchSummary.bind(this)); } } diff --git a/src/stores/GroupStoreCache.js b/src/stores/GroupStoreCache.js index ade0445e97..fbe456f5dc 100644 --- a/src/stores/GroupStoreCache.js +++ b/src/stores/GroupStoreCache.js @@ -18,18 +18,20 @@ import GroupStore from './GroupStore'; class GroupStoreCache { constructor() { - this.groupStores = {}; + this.groupStore = null; } getGroupStore(matrixClient, groupId) { - if (!this.groupStores[groupId]) { - this.groupStores[groupId] = new GroupStore(matrixClient, groupId); + if (!this.groupStore || this.groupStore._groupId !== groupId) { + // This effectively throws away the reference to any previous GroupStore, + // allowing it to be GCd once the components referencing it have stopped + // referencing it. + this.groupStore = new GroupStore(matrixClient, groupId); } - return this.groupStores[groupId]; + return this.groupStore; } } - let singletonGroupStoreCache = null; if (!singletonGroupStoreCache) { singletonGroupStoreCache = new GroupStoreCache(); From cbb36b163b3d2dbd6f9a95bae1fab994287d0721 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 4 Oct 2017 18:05:40 +0100 Subject: [PATCH 29/40] Typo --- src/stores/GroupStoreCache.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/GroupStoreCache.js b/src/stores/GroupStoreCache.js index fbe456f5dc..bf340521b5 100644 --- a/src/stores/GroupStoreCache.js +++ b/src/stores/GroupStoreCache.js @@ -22,7 +22,7 @@ class GroupStoreCache { } getGroupStore(matrixClient, groupId) { - if (!this.groupStore || this.groupStore._groupId !== groupId) { + if (!this.groupStore || this.groupStore.groupId !== groupId) { // This effectively throws away the reference to any previous GroupStore, // allowing it to be GCd once the components referencing it have stopped // referencing it. From 38de4ae1524bb34e3d54b0c12f802ac2f4c95097 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Oct 2017 08:00:22 +0100 Subject: [PATCH 30/40] delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/dialogs/CreateRoomDialog.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/views/dialogs/CreateRoomDialog.js b/src/components/views/dialogs/CreateRoomDialog.js index 20eeb5b591..d5a99dd4bf 100644 --- a/src/components/views/dialogs/CreateRoomDialog.js +++ b/src/components/views/dialogs/CreateRoomDialog.js @@ -48,21 +48,21 @@ export default React.createClass({ >
- +
- +
-
+
{ _t('Advanced options') }
- +
From c115980f21c83c598786f4deed375a1b469b6b53 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Oct 2017 08:08:39 +0100 Subject: [PATCH 31/40] remove redundant&stale onKeyDown Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/dialogs/CreateRoomDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/CreateRoomDialog.js b/src/components/views/dialogs/CreateRoomDialog.js index d5a99dd4bf..f7be47b3eb 100644 --- a/src/components/views/dialogs/CreateRoomDialog.js +++ b/src/components/views/dialogs/CreateRoomDialog.js @@ -51,7 +51,7 @@ export default React.createClass({
- +

From a8231f7bf9b8a0d3bdaf7fc1d6ce6ee1d61df4dc Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Oct 2017 08:26:57 +0100 Subject: [PATCH 32/40] Remove redundant stale onKeyDown Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/dialogs/TextInputDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/TextInputDialog.js b/src/components/views/dialogs/TextInputDialog.js index c924da7745..5ea4191e5e 100644 --- a/src/components/views/dialogs/TextInputDialog.js +++ b/src/components/views/dialogs/TextInputDialog.js @@ -68,7 +68,7 @@ export default React.createClass({
- +
From 917957c1dcb8911c2013fa8862a885a26227f3c1 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 5 Oct 2017 14:30:04 +0100 Subject: [PATCH 33/40] Modify the group store to include group rooms and modify components to use this new part of the store such that feedback can be given when adding or removing a room from the room list. --- .../views/dialogs/AddressPickerDialog.js | 42 ++++++++----------- src/components/views/groups/GroupRoomList.js | 39 +++++++++-------- src/components/views/groups/GroupRoomTile.js | 5 ++- src/stores/GroupStore.js | 26 +++++++++++- src/stores/GroupStoreCache.js | 1 + 5 files changed, 68 insertions(+), 45 deletions(-) diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index 6a027ac034..2637f9d466 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -23,6 +23,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg'; import AccessibleButton from '../elements/AccessibleButton'; import Promise from 'bluebird'; import { addressTypes, getAddressType } from '../../../UserAddress.js'; +import GroupStoreCache from '../../../stores/GroupStoreCache'; const TRUNCATE_QUERY_LIST = 40; const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200; @@ -241,32 +242,25 @@ module.exports = React.createClass({ _doNaiveGroupRoomSearch: function(query) { const lowerCaseQuery = query.toLowerCase(); - MatrixClientPeg.get().getGroupRooms(this.props.groupId).then((resp) => { - const results = []; - resp.chunk.forEach((r) => { - const nameMatch = (r.name || '').toLowerCase().includes(lowerCaseQuery); - const topicMatch = (r.topic || '').toLowerCase().includes(lowerCaseQuery); - const aliasMatch = (r.canonical_alias || '').toLowerCase().includes(lowerCaseQuery); - if (!(nameMatch || topicMatch || aliasMatch)) { - return; - } - results.push({ - room_id: r.room_id, - avatar_url: r.avatar_url, - name: r.name || r.canonical_alias, - }); - }); - this._processResults(results, query); - }).catch((err) => { - console.error('Error whilst searching group users: ', err); - this.setState({ - searchError: err.errcode ? err.message : _t('Something went wrong!'), - }); - }).done(() => { - this.setState({ - busy: false, + const groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), this.props.groupId); + const results = []; + groupStore.getGroupRooms().forEach((r) => { + const nameMatch = (r.name || '').toLowerCase().includes(lowerCaseQuery); + const topicMatch = (r.topic || '').toLowerCase().includes(lowerCaseQuery); + const aliasMatch = (r.canonical_alias || '').toLowerCase().includes(lowerCaseQuery); + if (!(nameMatch || topicMatch || aliasMatch)) { + return; + } + results.push({ + room_id: r.room_id, + avatar_url: r.avatar_url, + name: r.name || r.canonical_alias, }); }); + this._processResults(results, query); + this.setState({ + busy: false, + }); }, _doRoomSearch: function(query) { diff --git a/src/components/views/groups/GroupRoomList.js b/src/components/views/groups/GroupRoomList.js index 39ff3e4a07..4ff68a7f4d 100644 --- a/src/components/views/groups/GroupRoomList.js +++ b/src/components/views/groups/GroupRoomList.js @@ -17,6 +17,7 @@ import React from 'react'; import { _t } from '../../../languageHandler'; import sdk from '../../../index'; import { groupRoomFromApiObject } from '../../../groups'; +import GroupStoreCache from '../../../stores/GroupStoreCache'; import GeminiScrollbar from 'react-gemini-scrollbar'; import PropTypes from 'prop-types'; import {MatrixClient} from 'matrix-js-sdk'; @@ -34,7 +35,6 @@ export default React.createClass({ getInitialState: function() { return { - fetching: false, rooms: null, truncateAt: INITIAL_LOAD_NUM_ROOMS, searchQuery: "", @@ -43,21 +43,29 @@ export default React.createClass({ componentWillMount: function() { this._unmounted = false; + this._initGroupStore(this.props.groupId); + }, + + _initGroupStore: function(groupId) { + this._groupStore = GroupStoreCache.getGroupStore(this.context.matrixClient, groupId); + this._groupStore.on('update', () => { + this._fetchRooms(); + }); + this._groupStore.on('error', (err) => { + console.error('Error in group store (listened to by GroupRoomList)', err); + this.setState({ + rooms: null, + }); + }); this._fetchRooms(); }, _fetchRooms: function() { - this.setState({fetching: true}); - this.context.matrixClient.getGroupRooms(this.props.groupId).then((result) => { - this.setState({ - rooms: result.chunk.map((apiRoom) => { - return groupRoomFromApiObject(apiRoom); - }), - fetching: false, - }); - }).catch((e) => { - this.setState({fetching: false}); - console.error("Failed to get group room list: ", e); + if (this._unmounted) return; + this.setState({ + rooms: this._groupStore.getGroupRooms().map((apiRoom) => { + return groupRoomFromApiObject(apiRoom); + }), }); }, @@ -110,12 +118,7 @@ export default React.createClass({ }, render: function() { - if (this.state.fetching) { - const Spinner = sdk.getComponent("elements.Spinner"); - return (
- -
); - } else if (this.state.rooms === null) { + if (this.state.rooms === null) { return null; } diff --git a/src/components/views/groups/GroupRoomTile.js b/src/components/views/groups/GroupRoomTile.js index b6bdb9735b..bb0fdb03f4 100644 --- a/src/components/views/groups/GroupRoomTile.js +++ b/src/components/views/groups/GroupRoomTile.js @@ -21,6 +21,7 @@ import PropTypes from 'prop-types'; import sdk from '../../../index'; import dis from '../../../dispatcher'; import { GroupRoomType } from '../../../groups'; +import GroupStoreCache from '../../../stores/GroupStoreCache'; import Modal from '../../../Modal'; const GroupRoomTile = React.createClass({ @@ -49,10 +50,10 @@ const GroupRoomTile = React.createClass({ removeRoomFromGroup: function() { const groupId = this.props.groupId; + const groupStore = GroupStoreCache.getGroupStore(this.context.matrixClient, groupId); const roomName = this.state.name; const roomId = this.props.groupRoom.roomId; - this.context.matrixClient - .removeRoomFromGroup(groupId, roomId) + groupStore.removeRoomFromGroup(roomId) .catch((err) => { console.error(`Error whilst removing ${roomId} from ${groupId}`, err); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); diff --git a/src/stores/GroupStore.js b/src/stores/GroupStore.js index 941f4c8ec2..73118993f9 100644 --- a/src/stores/GroupStore.js +++ b/src/stores/GroupStore.js @@ -26,7 +26,9 @@ export default class GroupStore extends EventEmitter { this.groupId = groupId; this._matrixClient = matrixClient; this._summary = {}; + this._rooms = []; this._fetchSummary(); + this._fetchRooms(); } _fetchSummary() { @@ -38,6 +40,15 @@ export default class GroupStore extends EventEmitter { }); } + _fetchRooms() { + this._matrixClient.getGroupRooms(this.groupId).then((resp) => { + this._rooms = resp.chunk; + this._notifyListeners(); + }).catch((err) => { + this.emit('error', err); + }); + } + _notifyListeners() { this.emit('update'); } @@ -46,9 +57,22 @@ export default class GroupStore extends EventEmitter { return this._summary; } + getGroupRooms() { + return this._rooms; + } + addRoomToGroup(roomId) { return this._matrixClient - .addRoomToGroup(this.groupId, roomId); + .addRoomToGroup(this.groupId, roomId) + .then(this._fetchRooms.bind(this)); + } + + removeRoomFromGroup(roomId) { + return this._matrixClient + .removeRoomFromGroup(this.groupId, roomId) + // Room might be in the summary, refresh just in case + .then(this._fetchSummary.bind(this)) + .then(this._fetchRooms.bind(this)); } addRoomToGroupSummary(roomId, categoryId) { diff --git a/src/stores/GroupStoreCache.js b/src/stores/GroupStoreCache.js index bf340521b5..551b155615 100644 --- a/src/stores/GroupStoreCache.js +++ b/src/stores/GroupStoreCache.js @@ -28,6 +28,7 @@ class GroupStoreCache { // referencing it. this.groupStore = new GroupStore(matrixClient, groupId); } + this.groupStore._fetchSummary(); return this.groupStore; } } From 6a4e3792d4aa563c521eadfd1bc858a343894742 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 6 Oct 2017 12:07:38 +0100 Subject: [PATCH 34/40] split handlers into state and non-states Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/TextForEvent.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/TextForEvent.js b/src/TextForEvent.js index a21eb5c251..fa8efe028f 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -291,12 +291,15 @@ function textForWidgetEvent(event) { const handlers = { 'm.room.message': textForMessageEvent, - 'm.room.name': textForRoomNameEvent, - 'm.room.topic': textForTopicEvent, - 'm.room.member': textForMemberEvent, 'm.call.invite': textForCallInviteEvent, 'm.call.answer': textForCallAnswerEvent, 'm.call.hangup': textForCallHangupEvent, +}; + +const stateHandlers = { + 'm.room.name': textForRoomNameEvent, + 'm.room.topic': textForTopicEvent, + 'm.room.member': textForMemberEvent, 'm.room.third_party_invite': textForThreePidInviteEvent, 'm.room.history_visibility': textForHistoryVisibilityEvent, 'm.room.encryption': textForEncryptionEvent, @@ -307,8 +310,8 @@ const handlers = { module.exports = { textForEvent: function(ev) { - const hdlr = handlers[ev.getType()]; - if (!hdlr) return ''; - return hdlr(ev); + const handler = ev.isState() ? stateHandlers[ev.getType()] : handlers[ev.getType()]; + if (handler) return handler(ev); + return ''; }, }; From 91ba939e23eef52634a5618041008cbe30c73e2b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 6 Oct 2017 12:10:07 +0100 Subject: [PATCH 35/40] tiny bit of de-lint for consistency Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/TextForEvent.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/TextForEvent.js b/src/TextForEvent.js index fa8efe028f..ccb4c29a9c 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -243,7 +243,7 @@ function textForPowerEvent(event) { if (to !== from) { diff.push( _t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', { - userId: userId, + userId, fromPowerLevel: Roles.textualPowerLevel(from, userDefault), toPowerLevel: Roles.textualPowerLevel(to, userDefault), }), @@ -254,7 +254,7 @@ function textForPowerEvent(event) { return ''; } return _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', { - senderName: senderName, + senderName, powerLevelDiffText: diff.join(", "), }); } From 152499a17d0683a565608a7f019ae99740a29b97 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 6 Oct 2017 12:16:54 +0100 Subject: [PATCH 36/40] DRY map lookup Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/TextForEvent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TextForEvent.js b/src/TextForEvent.js index ccb4c29a9c..6345403f09 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -310,7 +310,7 @@ const stateHandlers = { module.exports = { textForEvent: function(ev) { - const handler = ev.isState() ? stateHandlers[ev.getType()] : handlers[ev.getType()]; + const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()]; if (handler) return handler(ev); return ''; }, From 92be3af990ecd96cfa1bbef3bc2cdab3554b819b Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 10 Oct 2017 19:16:42 +0100 Subject: [PATCH 37/40] Make it clearer which HS you're logging into Otherwise there's no indication without clicking 'custom server' --- src/components/structures/login/Login.js | 1 + src/components/views/elements/Dropdown.js | 10 ++++++++++ src/components/views/login/PasswordLogin.js | 14 +++++++++++++- src/i18n/strings/en_EN.json | 3 ++- 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index a6c0a70c66..b88aa094dc 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -290,6 +290,7 @@ module.exports = React.createClass({ onPhoneNumberChanged={this.onPhoneNumberChanged} onForgotPasswordClick={this.props.onForgotPasswordClick} loginIncorrect={this.state.loginIncorrect} + hsUrl={this.state.enteredHomeserverUrl} /> ); case 'm.login.cas': diff --git a/src/components/views/elements/Dropdown.js b/src/components/views/elements/Dropdown.js index c049c38a68..1b2117bb6a 100644 --- a/src/components/views/elements/Dropdown.js +++ b/src/components/views/elements/Dropdown.js @@ -26,6 +26,12 @@ class MenuOption extends React.Component { this._onClick = this._onClick.bind(this); } + getDefaultProps() { + return { + disabled: false, + } + } + _onMouseEnter() { this.props.onMouseEnter(this.props.dropdownKey); } @@ -153,6 +159,8 @@ export default class Dropdown extends React.Component { } _onInputClick(ev) { + if (this.props.disabled) return; + if (!this.state.expanded) { this.setState({ expanded: true, @@ -329,4 +337,6 @@ Dropdown.propTypes = { // in the dropped-down menu. getShortOption: React.PropTypes.func, value: React.PropTypes.string, + // negative for consistency with HTML + disabled: React.PropTypes.bool, } diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 9f855616fc..4e37e30f65 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -186,6 +186,17 @@ class PasswordLogin extends React.Component { const loginField = this.renderLoginField(this.state.loginType); + let matrixIdText = ''; + if (this.props.hsUrl) { + try { + const parsedHsUrl = new URL(this.props.hsUrl); + matrixIdText = _t('%(serverName)s Matrix ID', {serverName: parsedHsUrl.hostname}); + } catch (e) { + console.log(e); + // pass + } + } + return (
@@ -194,8 +205,9 @@ class PasswordLogin extends React.Component { - { _t('my Matrix ID') } + {matrixIdText} { _t('Email address') } { _t('Phone') } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 6acaba9fae..8ec975987c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -906,5 +906,6 @@ "Related Groups": "Related Groups", "Related groups for this room:": "Related groups for this room:", "This room has no related groups": "This room has no related groups", - "New group ID (e.g. +foo:%(localDomain)s)": "New group ID (e.g. +foo:%(localDomain)s)" + "New group ID (e.g. +foo:%(localDomain)s)": "New group ID (e.g. +foo:%(localDomain)s)", + "%(serverName)s Matrix ID": "%(serverName)s Matrix ID" } From 80ad7d4ad670f23e8c7ae3e6d8146df5f50541f9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 10 Oct 2017 19:27:51 +0100 Subject: [PATCH 38/40] Update translations --- src/i18n/strings/de_DE.json | 2 +- src/i18n/strings/el.json | 1 - src/i18n/strings/en_EN.json | 1 - src/i18n/strings/en_US.json | 1 - src/i18n/strings/es.json | 2 +- src/i18n/strings/eu.json | 1 - src/i18n/strings/fi.json | 2 +- src/i18n/strings/fr.json | 2 +- src/i18n/strings/hu.json | 2 +- src/i18n/strings/id.json | 1 - src/i18n/strings/ko.json | 1 - src/i18n/strings/lv.json | 2 +- src/i18n/strings/nl.json | 2 +- src/i18n/strings/pl.json | 2 +- src/i18n/strings/pt.json | 1 - src/i18n/strings/pt_BR.json | 1 - src/i18n/strings/ru.json | 2 +- src/i18n/strings/sv.json | 2 +- src/i18n/strings/th.json | 1 - src/i18n/strings/tr.json | 1 - src/i18n/strings/zh_Hans.json | 1 - src/i18n/strings/zh_Hant.json | 1 - 22 files changed, 10 insertions(+), 22 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index aa114e241d..71adaba704 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -146,7 +146,7 @@ "Members only": "Nur Mitglieder", "Mobile phone number": "Mobiltelefonnummer", "Moderator": "Moderator", - "my Matrix ID": "meiner Matrix-ID", + "%(serverName)s Matrix ID": "%(serverName)s Matrix-ID", "Never send encrypted messages to unverified devices from this device": "Niemals verschlüsselte Nachrichten an unverifizierte Geräte von diesem Gerät aus versenden", "Never send encrypted messages to unverified devices in this room from this device": "Niemals verschlüsselte Nachrichten an unverifizierte Geräte in diesem Raum von diesem Gerät aus senden", "New password": "Neues Passwort", diff --git a/src/i18n/strings/el.json b/src/i18n/strings/el.json index bc45e6da9e..2a51f6cae5 100644 --- a/src/i18n/strings/el.json +++ b/src/i18n/strings/el.json @@ -188,7 +188,6 @@ "Failure to create room": "Δεν ήταν δυνατή η δημιουργία δωματίου", "Join Room": "Είσοδος σε δωμάτιο", "Moderator": "Συντονιστής", - "my Matrix ID": "το Matrix ID μου", "Name": "Όνομα", "New address (e.g. #foo:%(localDomain)s)": "Νέα διεύθυνση (e.g. #όνομα:%(localDomain)s)", "New password": "Νέος κωδικός πρόσβασης", diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8ec975987c..37c7655ed6 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -295,7 +295,6 @@ "Moderator": "Moderator", "Must be viewing a room": "Must be viewing a room", "Mute": "Mute", - "my Matrix ID": "my Matrix ID", "Name": "Name", "Never send encrypted messages to unverified devices from this device": "Never send encrypted messages to unverified devices from this device", "Never send encrypted messages to unverified devices in this room": "Never send encrypted messages to unverified devices in this room", diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json index 928f1a9d0f..3954b2b6fa 100644 --- a/src/i18n/strings/en_US.json +++ b/src/i18n/strings/en_US.json @@ -262,7 +262,6 @@ "Moderator": "Moderator", "Must be viewing a room": "Must be viewing a room", "Mute": "Mute", - "my Matrix ID": "my Matrix ID", "Name": "Name", "Never send encrypted messages to unverified devices from this device": "Never send encrypted messages to unverified devices from this device", "Never send encrypted messages to unverified devices in this room": "Never send encrypted messages to unverified devices in this room", diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index bc2391a5c7..68f27c23e9 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -379,7 +379,7 @@ "Moderator": "Moderador", "Must be viewing a room": "Debe estar viendo una sala", "Mute": "Silenciar", - "my Matrix ID": "Mi ID de Matrix", + "%(serverName)s Matrix ID": "%(serverName)s ID de Matrix", "Name": "Nombre", "Never send encrypted messages to unverified devices from this device": "No enviar nunca mensajes cifrados, desde este dispositivo, a dispositivos sin verificar", "Never send encrypted messages to unverified devices in this room": "No enviar nunca mensajes cifrados a dispositivos no verificados, en esta sala", diff --git a/src/i18n/strings/eu.json b/src/i18n/strings/eu.json index 9f3d06ec52..9ef02d7b9b 100644 --- a/src/i18n/strings/eu.json +++ b/src/i18n/strings/eu.json @@ -346,7 +346,6 @@ "Missing room_id in request": "Gelaren ID-a falta da eskaeran", "Missing user_id in request": "Erabiltzailearen ID-a falta da eskaeran", "Mobile phone number": "Mugikorraren telefono zenbakia", - "my Matrix ID": "Nire Matrix ID-a", "Never send encrypted messages to unverified devices in this room": "Ez bidali inoiz zifratutako mezuak egiaztatu gabeko gailuetara gela honetan", "Never send encrypted messages to unverified devices in this room from this device": "Ez bidali inoiz zifratutako mezuak egiaztatu gabeko gailuetara gela honetan gailu honetatik", "New address (e.g. #foo:%(localDomain)s)": "Helbide berria (adib. #foo:%(localDomain)s)", diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index a59e5b1edd..7ffbf39e8e 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -245,7 +245,7 @@ "Mobile phone number": "Matkapuhelinnumero", "Mobile phone number (optional)": "Matkapuhelinnumero (valinnainen)", "Moderator": "Moderaattori", - "my Matrix ID": "minun Matrix tunniste", + "%(serverName)s Matrix ID": "%(serverName)s Matrix tunniste", "Name": "Nimi", "New password": "Uusi salasana", "New passwords don't match": "Uudet salasanat eivät täsmää", diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 585e47f5a3..51c496ab96 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -224,7 +224,7 @@ "Mobile phone number": "Numéro de téléphone mobile", "Moderator": "Modérateur", "Must be viewing a room": "Doit être en train de visualiser un salon", - "my Matrix ID": "mon identifiant Matrix", + "%(serverName)s Matrix ID": "%(serverName)s identifiant Matrix", "Name": "Nom", "Never send encrypted messages to unverified devices from this device": "Ne jamais envoyer de message chiffré aux appareils non-vérifiés depuis cet appareil", "Never send encrypted messages to unverified devices in this room": "Ne jamais envoyer de message chiffré aux appareils non-vérifiés dans ce salon", diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 2c34e05b1a..edf0a83ac6 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -293,7 +293,7 @@ "Mobile phone number (optional)": "Mobill telefonszám (opcionális)", "Moderator": "Moderátor", "Must be viewing a room": "Meg kell nézni a szobát", - "my Matrix ID": "Matrix azonosítóm", + "%(serverName)s Matrix ID": "%(serverName)s Matrix azonosítóm", "Name": "Név", "Never send encrypted messages to unverified devices from this device": "Soha ne küldj titkosított üzenetet ellenőrizetlen eszközre erről az eszközről", "Never send encrypted messages to unverified devices in this room": "Soha ne küldj titkosított üzenetet ebből a szobából ellenőrizetlen eszközre", diff --git a/src/i18n/strings/id.json b/src/i18n/strings/id.json index dc057c2a95..27bcc41dc8 100644 --- a/src/i18n/strings/id.json +++ b/src/i18n/strings/id.json @@ -78,7 +78,6 @@ "Members only": "Hanya anggota", "Mobile phone number": "Nomor telpon seluler", "Mute": "Bisu", - "my Matrix ID": "ID Matrix saya", "Name": "Nama", "New password": "Password Baru", "New passwords don't match": "Password baru tidak cocok", diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index 8b6e233437..5933b6abc2 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -293,7 +293,6 @@ "Mobile phone number (optional)": "휴대 전화번호 (선택)", "Moderator": "조정자", "Must be viewing a room": "방을 둘러봐야만 해요", - "my Matrix ID": "내 매트릭스 ID", "Name": "이름", "Never send encrypted messages to unverified devices from this device": "이 장치에서 인증받지 않은 장치로 암호화한 메시지를 보내지 마세요", "Never send encrypted messages to unverified devices in this room": "이 방에서 인증받지 않은 장치로 암호화한 메시지를 보내지 마세요", diff --git a/src/i18n/strings/lv.json b/src/i18n/strings/lv.json index 5f58fd9515..424a831ac5 100644 --- a/src/i18n/strings/lv.json +++ b/src/i18n/strings/lv.json @@ -274,7 +274,7 @@ "Moderator": "Moderators", "Must be viewing a room": "Jāapskata istaba", "Mute": "Apklusināt", - "my Matrix ID": "mans Matrix ID", + "%(serverName)s Matrix ID": "%(serverName)s Matrix ID", "Name": "Vārds", "Never send encrypted messages to unverified devices from this device": "Nekad nesūti no šīs ierīces šifrētas ziņas uz neverificētām ierīcēm", "Never send encrypted messages to unverified devices in this room": "Nekad nesūti šifrētas ziņas uz neverificētām ierīcēm šajā istabā", diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index f770e335cf..67fa97a5d7 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -126,7 +126,7 @@ "disabled": "uitgeschakeld", "Moderator": "Moderator", "Must be viewing a room": "Moet een ruimte weergeven", - "my Matrix ID": "mijn Matrix-ID", + "%(serverName)s Matrix ID": "%(serverName)s Matrix-ID", "Name": "Naam", "New password": "Nieuw wachtwoord", "none": "geen", diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index bd1e4c5c24..a3e2af956f 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -363,7 +363,7 @@ "Mobile phone number": "Numer telefonu komórkowego", "Mobile phone number (optional)": "Numer telefonu komórkowego (opcjonalne)", "Moderator": "Moderator", - "my Matrix ID": "mój Matrix ID", + "%(serverName)s Matrix ID": "%(serverName)s Matrix ID", "Name": "Imię", "Never send encrypted messages to unverified devices from this device": "Nigdy nie wysyłaj zaszyfrowanych wiadomości do niezweryfikowanych urządzeń z tego urządzenia", "Never send encrypted messages to unverified devices in this room": "Nigdy nie wysyłaj zaszyfrowanych wiadomości do niezweryfikowanych urządzeń w tym pokoju", diff --git a/src/i18n/strings/pt.json b/src/i18n/strings/pt.json index ba4968b7ad..1bf7fd00b1 100644 --- a/src/i18n/strings/pt.json +++ b/src/i18n/strings/pt.json @@ -127,7 +127,6 @@ "Members only": "Apenas integrantes da sala", "Mobile phone number": "Telefone celular", "Moderator": "Moderador/a", - "my Matrix ID": "com meu ID do Matrix", "Name": "Nome", "Never send encrypted messages to unverified devices from this device": "Nunca envie mensagens criptografada para um dispositivo não verificado a partir deste dispositivo", "Never send encrypted messages to unverified devices in this room from this device": "Nunca envie mensagens criptografadas para dispositivos não verificados nesta sala a partir deste dispositivo", diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json index af4804bd85..116142c29c 100644 --- a/src/i18n/strings/pt_BR.json +++ b/src/i18n/strings/pt_BR.json @@ -127,7 +127,6 @@ "Members only": "Apenas integrantes da sala", "Mobile phone number": "Telefone celular", "Moderator": "Moderador/a", - "my Matrix ID": "com meu ID do Matrix", "Name": "Nome", "Never send encrypted messages to unverified devices from this device": "Nunca envie mensagens criptografada para um dispositivo não verificado a partir deste dispositivo", "Never send encrypted messages to unverified devices in this room from this device": "Nunca envie mensagens criptografadas para dispositivos não verificados nesta sala a partir deste dispositivo", diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index cfab960e32..8dc2d80001 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -117,7 +117,7 @@ "Members only": "Только участники", "Mobile phone number": "Номер мобильного телефона", "Moderator": "Модератор", - "my Matrix ID": "мой Matrix ID", + "%(serverName)s Matrix ID": "%(serverName)s Matrix ID", "Name": "Имя", "Never send encrypted messages to unverified devices from this device": "Никогда не отправлять зашифрованные сообщения на непроверенные устройства с этого устройства", "Never send encrypted messages to unverified devices in this room from this device": "Никогда не отправлять зашифрованные сообщения на непроверенные устройства в этой комнате с этого устройства", diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index fb7257ecf9..fd15771cec 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -273,7 +273,7 @@ "Moderator": "Moderator", "Must be viewing a room": "Du måste ha ett öppet rum", "Mute": "Dämpa", - "my Matrix ID": "mitt Matrix-ID", + "%(serverName)s Matrix ID": "%(serverName)s Matrix-ID", "Name": "Namn", "Never send encrypted messages to unverified devices from this device": "Skicka aldrig krypterade meddelanden till overifierade enheter från den här enheten", "Never send encrypted messages to unverified devices in this room": "Skicka aldrig krypterade meddelanden till overifierade enheter i det här rummet", diff --git a/src/i18n/strings/th.json b/src/i18n/strings/th.json index d45cb86986..47a5fd3049 100644 --- a/src/i18n/strings/th.json +++ b/src/i18n/strings/th.json @@ -219,7 +219,6 @@ "Markdown is enabled": "เปิดใช้งาน Markdown แล้ว", "Missing user_id in request": "ไม่พบ user_id ในคำขอ", "Moderator": "ผู้ช่วยดูแล", - "my Matrix ID": "Matrix ID ของฉัน", "New address (e.g. #foo:%(localDomain)s)": "ที่อยู่ใหม่ (เช่น #foo:%(localDomain)s)", "New password": "รหัสผ่านใหม่", "New passwords don't match": "รหัสผ่านใหม่ไม่ตรงกัน", diff --git a/src/i18n/strings/tr.json b/src/i18n/strings/tr.json index 23d4e284bc..6e8e4f25f8 100644 --- a/src/i18n/strings/tr.json +++ b/src/i18n/strings/tr.json @@ -269,7 +269,6 @@ "Moderator": "Moderatör", "Must be viewing a room": "Bir oda görüntülemeli olmalı", "Mute": "Sessiz", - "my Matrix ID": "Benim Matrix ID'm", "Name": "İsim", "Never send encrypted messages to unverified devices from this device": "Bu cihazdan doğrulanmamış cihazlara asla şifrelenmiş mesajlar göndermeyin", "Never send encrypted messages to unverified devices in this room": "Bu odada doğrulanmamış cihazlara asla şifreli mesajlar göndermeyin", diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json index 69ba19ca27..f185640572 100644 --- a/src/i18n/strings/zh_Hans.json +++ b/src/i18n/strings/zh_Hans.json @@ -289,7 +289,6 @@ "Mobile phone number (optional)": "手机号码 (可选)", "Moderator": "管理员", "Mute": "静音", - "my Matrix ID": "我的 Matrix ID", "Name": "姓名", "Never send encrypted messages to unverified devices from this device": "不要从此设备向未验证的设备发送消息", "Never send encrypted messages to unverified devices in this room": "不要在此聊天室里向未验证的设备发送消息", diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 596bc55a01..49890005a1 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -403,7 +403,6 @@ "Mobile phone number (optional)": "行動電話號碼(選擇性)", "Moderator": "仲裁者", "Must be viewing a room": "必須檢視房間", - "my Matrix ID": "我的 Matrix ID", "Name": "名稱", "Never send encrypted messages to unverified devices from this device": "從不自此裝置傳送加密的訊息到未驗證的裝置", "Never send encrypted messages to unverified devices in this room": "從不在此房間傳送加密的訊息到未驗證的裝置", From fa24b4bd2de325154cc32d0237b9fe5904c794cd Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 11 Oct 2017 09:48:12 +0100 Subject: [PATCH 39/40] Remove this log - it's not an error worth logging --- src/components/views/login/PasswordLogin.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 4e37e30f65..7e78de3f54 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -192,7 +192,6 @@ class PasswordLogin extends React.Component { const parsedHsUrl = new URL(this.props.hsUrl); matrixIdText = _t('%(serverName)s Matrix ID', {serverName: parsedHsUrl.hostname}); } catch (e) { - console.log(e); // pass } } From 0f84216a9fea9c0cf38d6ae1ab30e0062bf707a1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 11 Oct 2017 14:05:34 +0100 Subject: [PATCH 40/40] Grey out login form when no valid HS --- src/components/views/elements/Dropdown.js | 1 + src/components/views/login/CountryDropdown.js | 3 +- src/components/views/login/PasswordLogin.js | 46 +++++++++++++------ 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/src/components/views/elements/Dropdown.js b/src/components/views/elements/Dropdown.js index 1b2117bb6a..0fb5a37414 100644 --- a/src/components/views/elements/Dropdown.js +++ b/src/components/views/elements/Dropdown.js @@ -302,6 +302,7 @@ export default class Dropdown extends React.Component { const dropdownClasses = { mx_Dropdown: true, + mx_Dropdown_disabled: this.props.disabled, }; if (this.props.className) { dropdownClasses[this.props.className] = true; diff --git a/src/components/views/login/CountryDropdown.js b/src/components/views/login/CountryDropdown.js index 7024db339c..56ab962d98 100644 --- a/src/components/views/login/CountryDropdown.js +++ b/src/components/views/login/CountryDropdown.js @@ -123,7 +123,7 @@ export default class CountryDropdown extends React.Component { return {options} ; @@ -137,4 +137,5 @@ CountryDropdown.propTypes = { showPrefix: React.PropTypes.bool, onOptionChange: React.PropTypes.func.isRequired, value: React.PropTypes.string, + disabled: React.PropTypes.bool, }; diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 7e78de3f54..d532c400bc 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -116,11 +116,17 @@ class PasswordLogin extends React.Component { this.props.onPasswordChanged(ev.target.value); } - renderLoginField(loginType) { + renderLoginField(loginType, disabled) { + const classes = { + mx_Login_field: true, + mx_Login_field_disabled: disabled, + }; + switch(loginType) { case PasswordLogin.LOGIN_FIELD_EMAIL: + classes.mx_Login_email = true; return ; case PasswordLogin.LOGIN_FIELD_MXID: + classes.mx_Login_username = true; return ; case PasswordLogin.LOGIN_FIELD_PHONE: const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); + classes.mx_Login_phoneNumberField = true; + classes.mx_Login_field_has_prefix = true; return
; } @@ -177,15 +190,6 @@ class PasswordLogin extends React.Component { ); } - const pwFieldClass = classNames({ - mx_Login_field: true, - error: this.props.loginIncorrect, - }); - - const Dropdown = sdk.getComponent('elements.Dropdown'); - - const loginField = this.renderLoginField(this.state.loginType); - let matrixIdText = ''; if (this.props.hsUrl) { try { @@ -196,6 +200,16 @@ class PasswordLogin extends React.Component { } } + const pwFieldClass = classNames({ + mx_Login_field: true, + mx_Login_field_disabled: matrixIdText === '', + error: this.props.loginIncorrect, + }); + + const Dropdown = sdk.getComponent('elements.Dropdown'); + + const loginField = this.renderLoginField(this.state.loginType, matrixIdText === ''); + return (
@@ -215,10 +229,12 @@ class PasswordLogin extends React.Component { {this._passwordField = e;}} type="password" name="password" value={this.state.password} onChange={this.onPasswordChanged} - placeholder={ _t('Password') } /> + placeholder={ _t('Password') } + disabled={matrixIdText === ''} + />
{forgotPasswordJsx} - +
);