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/TextForEvent.js b/src/TextForEvent.js index a21eb5c251..6345403f09 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(", "), }); } @@ -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 : handlers)[ev.getType()]; + if (handler) return handler(ev); + return ''; }, }; diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index eda05ef514..e85457e6aa 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/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/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 89303a2e41..3fa628b8a3 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -773,15 +773,13 @@ module.exports = React.createClass({ dis.dispatch({action: 'view_set_mxid'}); return; } - const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog"); - Modal.createTrackedDialog('Create Room', '', TextInputDialog, { - title: _t('Create Room'), - description: _t('Room name (optional)'), - button: _t('Create Room'), - onFinished: (shouldCreate, name) => { + const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog'); + Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, { + onFinished: (shouldCreate, name, noFederate) => { if (shouldCreate) { const createOpts = {}; if (name) createOpts.name = name; + if (noFederate) createOpts.creation_content = {'m.federate': false}; createRoom({createOpts}).done(); } }, diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index f003ca2b6f..171a0dd0fd 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', }, ]; @@ -793,7 +793,7 @@ module.exports = React.createClass({ onChange={onChange} /> ; }, 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/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/dialogs/CreateRoomDialog.js b/src/components/views/dialogs/CreateRoomDialog.js new file mode 100644 index 0000000000..f7be47b3eb --- /dev/null +++ b/src/components/views/dialogs/CreateRoomDialog.js @@ -0,0 +1,81 @@ +/* +Copyright 2017 Michael Telatynski <7t3chguy@gmail.com> + +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'; +import SdkConfig from '../../../SdkConfig'; +import { _t } from '../../../languageHandler'; + +export default React.createClass({ + displayName: 'CreateRoomDialog', + propTypes: { + onFinished: React.PropTypes.func.isRequired, + }, + + componentDidMount: function() { + const config = SdkConfig.get(); + // Dialog shows inverse of m.federate (noFederate) strict false check to skip undefined check (default = true) + this.defaultNoFederate = config.default_federate === false; + }, + + onOk: function() { + this.props.onFinished(true, this.refs.textinput.value, this.refs.checkbox.checked); + }, + + onCancel: function() { + this.props.onFinished(false); + }, + + render: function() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + return ( + +
+
+ +
+
+ +
+
+ +
+ { _t('Advanced options') } +
+ + +
+
+
+
+ + +
+
+ ); + }, +}); diff --git a/src/components/views/dialogs/KeyShareDialog.js b/src/components/views/dialogs/KeyShareDialog.js index bc84d52021..b0dc0a304e 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}); 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({
- +
diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 2101d4016d..3e343e098c 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -23,7 +23,7 @@ import PlatformPeg from '../../../PlatformPeg'; 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'; @@ -195,9 +195,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/elements/Dropdown.js b/src/components/views/elements/Dropdown.js index c049c38a68..0fb5a37414 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, @@ -294,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; @@ -329,4 +338,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/elements/EditableItemList.js b/src/components/views/elements/EditableItemList.js new file mode 100644 index 0000000000..35e207daef --- /dev/null +++ b/src/components/views/elements/EditableItemList.js @@ -0,0 +1,149 @@ +/* +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, + + 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() { + if (this.props.onRemove) this.props.onRemove(this.props.index); + }, + + onAdd: function() { + if (this.props.onAdd) 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: () => {}, + onNewItemChanged: () => {}, + }; + }, + + onItemAdded: function(value) { + this.props.onItemAdded(value); + }, + + onItemEdited: function(value, index) { + if (value.length === 0) { + this.onItemRemoved(index); + } else { + this.props.onItemEdited(value, index); + } + }, + + onItemRemoved: function(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; + + return (
+
+ { label } +
+ { editableItems } + +
); + }, +}); 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); 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/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 452f862d16..bb0fdb03f4 100644 --- a/src/components/views/groups/GroupRoomTile.js +++ b/src/components/views/groups/GroupRoomTile.js @@ -21,6 +21,8 @@ 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({ displayName: 'GroupRoomTile', @@ -31,7 +33,35 @@ 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"); + }, + + 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; + groupStore.removeRoomFromGroup(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) { @@ -49,20 +79,34 @@ const GroupRoomTile = React.createClass({ }); }, + onDeleteClick: function(e) { + const groupId = this.props.groupId; + const roomName = this.state.name; + 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() { 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 = ( - @@ -74,8 +118,11 @@ const GroupRoomTile = React.createClass({ { av }
- { name } + { this.state.name }
+ + + ); }, 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 9f855616fc..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,14 +190,25 @@ class PasswordLogin extends React.Component { ); } + 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) { + // pass + } + } + 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); + const loginField = this.renderLoginField(this.state.loginType, matrixIdText === ''); return (
@@ -194,8 +218,9 @@ class PasswordLogin extends React.Component { - { _t('my Matrix ID') } + {matrixIdText} { _t('Email address') } { _t('Phone') } @@ -204,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} - +
); 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 }
); diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.js index f37bd4271a..94301c432d 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,24 @@ 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 } ); - } + }, }); diff --git a/src/components/views/room_settings/RelatedGroupSettings.js b/src/components/views/room_settings/RelatedGroupSettings.js new file mode 100644 index 0000000000..60bdbf1481 --- /dev/null +++ b/src/components/views/room_settings/RelatedGroupSettings.js @@ -0,0 +1,125 @@ +/* +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 {MatrixEvent, MatrixClient} from 'matrix-js-sdk'; +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) { + if (!GROUP_ID_REGEX.test(groupId)) { + 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'); + return (
+

{ _t('Related Groups') }

+ +
); + }, +}); 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() { 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/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/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index e3fe50713b..b3b27875a0 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -312,6 +312,9 @@ module.exports = React.createClass({ promises.push(ps); } + // related groups + promises.push(this.saveRelatedGroups()); + // encryption p = this.saveEnableEncryption(); if (!p.isFulfilled()) { @@ -329,6 +332,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(); @@ -417,11 +425,12 @@ module.exports = React.createClass({ _yankValueFromEvent: function(stateEventType, keyName, defaultValue) { // E.g.("m.room.name","name") would yank the "name" content key from "m.room.name" - var event = this.props.room.currentState.getStateEvents(stateEventType, ''); + const event = this.props.room.currentState.getStateEvents(stateEventType, ''); if (!event) { return defaultValue; } - return event.getContent()[keyName] || defaultValue; + const content = event.getContent(); + return keyName in content ? content[keyName] : defaultValue; }, _onHistoryRadioToggle: function(ev) { @@ -628,6 +637,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"); @@ -662,6 +672,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 = @@ -701,7 +719,7 @@ module.exports = React.createClass({ } var unfederatableSection; - if (this._yankValueFromEvent("m.room.create", "m.federate") === false) { + if (this._yankValueFromEvent("m.room.create", "m.federate", true) === false) { unfederatableSection = (
{ _t('This room is not accessible by remote Matrix servers') }. @@ -886,6 +904,8 @@ module.exports = React.createClass({ canonicalAliasEvent={this.props.room.currentState.getStateEvents('m.room.canonical_alias', '')} aliasEvents={this.props.room.currentState.getStateEvents('m.room.aliases')} /> + { relatedGroupsSection } +

{ _t('Permissions') }

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, }; diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 1a05530890..661cd4c78f 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 048de7e73f..99ae411d94 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 792626ca7c..e7fb18d3e9 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -26,6 +26,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", @@ -58,6 +59,8 @@ "Banned users": "Banned users", "Bans user with given id": "Bans user with given id", "Blacklisted": "Blacklisted", + "Block users on other matrix homeservers from joining this room": "Block users on other matrix homeservers from joining this room", + "This setting cannot be changed later!": "This setting cannot be changed later!", "Bug Report": "Bug Report", "Bulk Options": "Bulk Options", "Call Timeout": "Call Timeout", @@ -295,7 +298,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", @@ -888,6 +890,9 @@ "The room '%(roomName)s' could not be removed from the summary.": "The room '%(roomName)s' 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.", + "Light theme": "Light theme", + "Dark theme": "Dark theme", + "Unknown": "Unknown", "Failed to add the following rooms to the summary of %(groupId)s:": "Failed to add the following rooms to the summary of %(groupId)s:", "The room '%(roomName)s' could not be removed from the summary.": "The room '%(roomName)s' could not be removed from the summary.", "Add rooms to the group": "Add rooms to the group", @@ -902,5 +907,14 @@ "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", + "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.", + "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)", + "%(serverName)s Matrix ID": "%(serverName)s Matrix ID" } diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json index 8a78a18407..96d7cd4a88 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 9838b915a4..9f2005fc99 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 fb2ec9f7b7..fcad984ff5 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 8ab78d0436..3faaed8f51 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 fb295535ca..1186c4f8bb 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 96655ccf4f..307ee762ef 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 fea8065a5f..0a5e999015 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 10f7f4900e..e800d9e57f 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 0805c8fb66..0b11e26c44 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 a1e4cce4e9..0ee57bc4d5 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 0cefe77aa6..c9dce9af74 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 3ff06070c3..1de1afb6e0 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 9ff6e5b151..e4ebb862ec 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 5c1017d177..98be1c9a64 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 cad8834052..6903c35d8c 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": "從不在此房間傳送加密的訊息到未驗證的裝置", 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 diff --git a/src/stores/GroupSummaryStore.js b/src/stores/GroupStore.js similarity index 56% rename from src/stores/GroupSummaryStore.js rename to src/stores/GroupStore.js index aa6e74529b..73118993f9 100644 --- a/src/stores/GroupSummaryStore.js +++ b/src/stores/GroupStore.js @@ -17,19 +17,22 @@ 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 that 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; + this.groupId = groupId; this._matrixClient = matrixClient; this._summary = {}; + this._rooms = []; this._fetchSummary(); + this._fetchRooms(); } _fetchSummary() { - this._matrixClient.getGroupSummary(this._groupId).then((resp) => { + this._matrixClient.getGroupSummary(this.groupId).then((resp) => { this._summary = resp; this._notifyListeners(); }).catch((err) => { @@ -37,6 +40,15 @@ export default class GroupSummaryStore 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'); } @@ -45,33 +57,51 @@ export default class GroupSummaryStore extends EventEmitter { return this._summary; } + getGroupRooms() { + return this._rooms; + } + + addRoomToGroup(roomId) { + return this._matrixClient + .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) { 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 new file mode 100644 index 0000000000..551b155615 --- /dev/null +++ b/src/stores/GroupStoreCache.js @@ -0,0 +1,40 @@ +/* +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.groupStore = null; + } + + getGroupStore(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); + } + this.groupStore._fetchSummary(); + return this.groupStore; + } +} + +let singletonGroupStoreCache = null; +if (!singletonGroupStoreCache) { + singletonGroupStoreCache = new GroupStoreCache(); +} +module.exports = singletonGroupStoreCache;