diff --git a/src/PageTypes.js b/src/PageTypes.js index b2346c62c3..66d930c288 100644 --- a/src/PageTypes.js +++ b/src/PageTypes.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -23,4 +24,5 @@ export default { RoomDirectory: "room_directory", UserView: "user_view", GroupView: "group_view", + MyGroups: "my_groups", }; diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index d3a06b915b..3f321d453d 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -15,17 +15,18 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import MatrixClientPeg from '../../MatrixClientPeg'; import sdk from '../../index'; import { sanitizedHtmlNode } from '../../HtmlUtils'; import { _t } from '../../languageHandler'; -module.exports = React.createClass({ +export default React.createClass({ displayName: 'GroupView', propTypes: { - groupId: React.PropTypes.string.isRequired, + groupId: PropTypes.string.isRequired, }, getInitialState: function() { @@ -65,36 +66,47 @@ module.exports = React.createClass({ }, render: function() { - const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); + const GroupAvatar = sdk.getComponent("avatars.GroupAvatar"); const Loader = sdk.getComponent("elements.Spinner"); if (this.state.summary === null && this.state.error === null) { return ; } else if (this.state.summary) { const summary = this.state.summary; - let avatarUrl = null; - if (summary.profile && summary.profile.avatar_url) { - avatarUrl = MatrixClientPeg.get().mxcUrlToHttp(summary.profile.avatar_url); - } let description = null; if (summary.profile && summary.profile.long_description) { description = sanitizedHtmlNode(summary.profile.long_description); } + + let nameNode; + if (summary.profile && summary.profile.name) { + nameNode =
+ {summary.profile.name} + + ({this.props.groupId}) + +
; + } else { + nameNode =
+ {this.props.groupId} +
; + } + + const groupAvatarUrl = summary.profile ? summary.profile.avatar_url : null; + return (
- +
-
- {summary.profile.name} - - ({this.props.groupId}) - -
+ {nameNode}
{summary.profile.short_description}
diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index aef7fe9cce..f1053618dc 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -211,6 +211,7 @@ export default React.createClass({ const RoomDirectory = sdk.getComponent('structures.RoomDirectory'); const HomePage = sdk.getComponent('structures.HomePage'); const GroupView = sdk.getComponent('structures.GroupView'); + const MyGroups = sdk.getComponent('structures.MyGroups'); const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar'); const NewVersionBar = sdk.getComponent('globals.NewVersionBar'); const UpdateCheckBar = sdk.getComponent('globals.UpdateCheckBar'); @@ -248,6 +249,10 @@ export default React.createClass({ if (!this.props.collapse_rhs) right_panel = ; break; + case PageTypes.MyGroups: + page_element = ; + break; + case PageTypes.CreateRoom: page_element = ; + page_element = ; + } break; case PageTypes.UserView: diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 025805d921..f0337fdd8e 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -486,6 +486,10 @@ module.exports = React.createClass({ this._setPage(PageTypes.RoomDirectory); this.notifyNewScreen('directory'); break; + case 'view_my_groups': + this._setPage(PageTypes.MyGroups); + this.notifyNewScreen('groups'); + break; case 'view_group': { const groupId = payload.group_id; @@ -1151,6 +1155,10 @@ module.exports = React.createClass({ dis.dispatch({ action: 'view_room_directory', }); + } else if (screen == 'groups') { + dis.dispatch({ + action: 'view_my_groups', + }); } else if (screen == 'post_registration') { dis.dispatch({ action: 'start_post_registration', diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js new file mode 100644 index 0000000000..3eb694acce --- /dev/null +++ b/src/components/structures/MyGroups.js @@ -0,0 +1,141 @@ +/* +Copyright 2017 Vector Creations 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'; +import { _t, _tJsx } from '../../languageHandler'; +import withMatrixClient from '../../wrappers/withMatrixClient'; +import AccessibleButton from '../views/elements/AccessibleButton'; +import dis from '../../dispatcher'; +import PropTypes from 'prop-types'; +import Modal from '../../Modal'; + +const GroupTile = React.createClass({ + displayName: 'GroupTile', + + propTypes: { + groupId: PropTypes.string.isRequired, + }, + + onClick: function(e) { + e.preventDefault(); + dis.dispatch({ + action: 'view_group', + group_id: this.props.groupId, + }); + }, + + render: function() { + return {this.props.groupId}; + }, +}); + +export default withMatrixClient(React.createClass({ + displayName: 'MyGroups', + + propTypes: { + matrixClient: React.PropTypes.object.isRequired, + }, + + getInitialState: function() { + return { + groups: null, + error: null, + }; + }, + + componentWillMount: function() { + this._fetch(); + }, + + _onCreateGroupClick: function() { + const CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog"); + Modal.createDialog(CreateGroupDialog); + }, + + _fetch: function() { + this.props.matrixClient.getJoinedGroups().done((result) => { + this.setState({groups: result.groups, error: null}); + }, (err) => { + this.setState({groups: null, error: err}); + }); + }, + + render: function() { + const Loader = sdk.getComponent("elements.Spinner"); + const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader'); + const TintableSvg = sdk.getComponent("elements.TintableSvg"); + + let content; + if (this.state.groups) { + const groupNodes = []; + this.state.groups.forEach((g) => { + groupNodes.push( +
+ +
, + ); + }); + content =
+
{_t('You are a member of these groups:')}
+ {groupNodes} +
; + } else if (this.state.error) { + content =
+ {_t('Error whilst fetching joined groups')} +
; + } else { + content = ; + } + + return
+ +
+
+
+ {_t('Create a new group')} +
+ + + + {_t( + 'Create a group to represent your community! '+ + 'Define a set of rooms and your own custom homepage '+ + 'to mark out your space in the Matrix universe.', + )} +
+
+
+ {_t('Join an existing group')} +
+ + + + {_tJsx( + 'To join an exisitng group you\'ll have to '+ + 'know its group identifier; this will look '+ + 'something like +example:matrix.org.', + /(.*)<\/i>/, + (sub) => {sub}, + )} +
+
+
+ {content} +
+
; + }, +})); diff --git a/src/components/views/avatars/GroupAvatar.js b/src/components/views/avatars/GroupAvatar.js new file mode 100644 index 0000000000..506714e857 --- /dev/null +++ b/src/components/views/avatars/GroupAvatar.js @@ -0,0 +1,66 @@ +/* +Copyright 2017 Vector Creations 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 MatrixClientPeg from '../../../MatrixClientPeg'; + +export default React.createClass({ + displayName: 'GroupAvatar', + + propTypes: { + groupId: PropTypes.string, + groupAvatarUrl: PropTypes.string, + width: PropTypes.number, + height: PropTypes.number, + resizeMethod: PropTypes.string, + }, + + getDefaultProps: function() { + return { + width: 36, + height: 36, + resizeMethod: 'crop', + }; + }, + + getGroupAvatarUrl: function() { + return MatrixClientPeg.get().mxcUrlToHttp( + this.props.groupAvatarUrl, + this.props.width, + this.props.height, + this.props.resizeMethod, + ); + }, + + render: function() { + const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); + // extract the props we use from props so we can pass any others through + // should consider adding this as a global rule in js-sdk? + /*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/ + const {groupId, groupAvatarUrl, ...otherProps} = this.props; + + return ( + + ); + }, +}); diff --git a/src/components/views/dialogs/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.js new file mode 100644 index 0000000000..23194f20a5 --- /dev/null +++ b/src/components/views/dialogs/CreateGroupDialog.js @@ -0,0 +1,199 @@ +/* +Copyright 2017 Vector Creations 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 dis from '../../../dispatcher'; +import { _t } from '../../../languageHandler'; +import MatrixClientPeg from '../../../MatrixClientPeg'; + +// We match fairly liberally and leave it up to the server to reject if +// there are invalid characters etc. +const GROUP_REGEX = /^\+(.*?):(.*)$/; + +export default React.createClass({ + displayName: 'CreateGroupDialog', + propTypes: { + onFinished: PropTypes.func.isRequired, + }, + + getInitialState: function() { + return { + groupName: '', + groupId: '', + groupError: null, + creating: false, + createError: null, + }; + }, + + _onGroupNameChange: function(e) { + this.setState({ + groupName: e.target.value, + }); + }, + + _onGroupIdChange: function(e) { + this.setState({ + groupId: e.target.value, + }); + }, + + _onGroupIdBlur: function(e) { + this._checkGroupId(); + }, + + _checkGroupId: function(e) { + const parsedGroupId = this._parseGroupId(this.state.groupId); + let error = null; + if (parsedGroupId === null) { + error = _t( + "Group IDs must be of the form +localpart:%(domain)s", + {domain: MatrixClientPeg.get().getDomain()}, + ); + } else { + const domain = parsedGroupId[1]; + if (domain !== MatrixClientPeg.get().getDomain()) { + error = _t( + "It is currently only possible to create groups on your own home server: "+ + "use a group ID ending with %(domain)s", + {domain: MatrixClientPeg.get().getDomain()}, + ); + } + } + this.setState({ + groupIdError: error, + }); + return error; + }, + + _onFormSubmit: function(e) { + e.preventDefault(); + + if (this._checkGroupId()) return; + + const parsedGroupId = this._parseGroupId(this.state.groupId); + const profile = {}; + if (this.state.groupName !== '') { + profile.name = this.state.groupName; + } + this.setState({creating: true}); + MatrixClientPeg.get().createGroup({ + localpart: parsedGroupId[0], + profile: profile, + }).then((result) => { + dis.dispatch({ + action: 'view_group', + group_id: result.group_id, + }); + this.props.onFinished(true); + }).catch((e) => { + this.setState({createError: e}); + }).finally(() => { + this.setState({creating: false}); + }).done(); + }, + + _onCancel: function() { + this.props.onFinished(false); + }, + + /** + * Parse a string that may be a group ID + * If the string is a valid group ID, return a list of [localpart, domain], + * otherwise return null. + * + * @param {string} groupId The ID of the group + * @return {string[]} array of localpart, domain + */ + _parseGroupId: function(groupId) { + const matches = GROUP_REGEX.exec(this.state.groupId); + if (!matches || matches.length < 3) { + return null; + } + return [matches[1], matches[2]]; + }, + + render: function() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const Spinner = sdk.getComponent('elements.Spinner'); + + if (this.state.creating) { + return ; + } + + let createErrorNode; + if (this.state.createError) { + // XXX: We should catch errcodes and give sensible i18ned messages for them, + // rather than displaying what the server gives us, but synapse doesn't give + // any yet. + createErrorNode =
+
{_t('Room creation failed')}
+
{this.state.createError.message}
+
; + } + + return ( + +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+ {this.state.groupIdError} +
+ {createErrorNode} +
+
+ + +
+
+
+ ); + }, +}); diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 170925999d..bb085279e8 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -24,7 +24,7 @@ var Modal = require('../../../Modal'); var sdk = require('../../../index'); var TextForEvent = require('../../../TextForEvent'); -import WithMatrixClient from '../../../wrappers/WithMatrixClient'; +import withMatrixClient from '../../../wrappers/withMatrixClient'; var ContextualMenu = require('../../structures/ContextualMenu'); import dis from '../../../dispatcher'; @@ -59,7 +59,7 @@ var MAX_READ_AVATARS = 5; // | '--------------------------------------' | // '----------------------------------------------------------' -module.exports = WithMatrixClient(React.createClass({ +module.exports = withMatrixClient(React.createClass({ displayName: 'EventTile', propTypes: { diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 6dc86f9a97..c034f0e704 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -36,12 +36,12 @@ import createRoom from '../../../createRoom'; import DMRoomMap from '../../../utils/DMRoomMap'; import Unread from '../../../Unread'; import { findReadReceiptFromUserId } from '../../../utils/Receipt'; -import WithMatrixClient from '../../../wrappers/WithMatrixClient'; +import withMatrixClient from '../../../wrappers/withMatrixClient'; import AccessibleButton from '../elements/AccessibleButton'; import GeminiScrollbar from 'react-gemini-scrollbar'; -module.exports = WithMatrixClient(React.createClass({ +module.exports = withMatrixClient(React.createClass({ displayName: 'MemberInfo', propTypes: { diff --git a/src/components/views/settings/AddPhoneNumber.js b/src/components/views/settings/AddPhoneNumber.js index dd79720e80..7bc551477e 100644 --- a/src/components/views/settings/AddPhoneNumber.js +++ b/src/components/views/settings/AddPhoneNumber.js @@ -19,10 +19,10 @@ import { _t } from '../../../languageHandler'; import sdk from '../../../index'; import AddThreepid from '../../../AddThreepid'; -import WithMatrixClient from '../../../wrappers/WithMatrixClient'; +import withMatrixClient from '../../../wrappers/withMatrixClient'; import Modal from '../../../Modal'; -export default WithMatrixClient(React.createClass({ +export default withMatrixClient(React.createClass({ displayName: 'AddPhoneNumber', propTypes: { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 15ae37c94e..fc4257cbc1 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -934,5 +934,23 @@ "You added a new device '%(displayName)s', which is requesting encryption keys.": "You added a new device '%(displayName)s', which is requesting encryption keys.", "Your unverified device '%(displayName)s' is requesting encryption keys.": "Your unverified device '%(displayName)s' is requesting encryption keys.", "Encryption key request": "Encryption key request", - "Autocomplete Delay (ms):": "Autocomplete Delay (ms):" + "Autocomplete Delay (ms):": "Autocomplete Delay (ms):", + "This Home server does not support groups": "This Home server does not support groups", + "Loading device info...": "Loading device info...", + "Groups": "Groups", + "Create a new group": "Create a new group", + "Create Group": "Create Group", + "Group Name": "Group Name", + "Example": "Example", + "Create": "Create", + "Group ID": "Group ID", + "+example:%(domain)s": "+example:%(domain)s", + "Group IDs must be of the form +localpart:%(domain)s": "Group IDs must be of the form +localpart:%(domain)s", + "It is currently only possible to create groups on your own home server: use a group ID ending with %(domain)s": "It is currently only possible to create groups on your own home server: use a group ID ending with %(domain)s", + "Room creation failed": "Room creation failed", + "You are a member of these groups:": "You are a member of these groups:", + "Create a group to represent your community! Define a set of rooms and your own custom homepage to mark out your space in the Matrix universe.": "Create a group to represent your community! Define a set of rooms and your own custom homepage to mark out your space in the Matrix universe.", + "Join an existing group": "Join an existing group", + "To join an exisitng group you'll have to know its group identifier; this will look something like +example:matrix.org.": "To join an exisitng group you'll have to know its group identifier; this will look something like +example:matrix.org.", + "Error whilst fetching joined groups": "Error whilst fetching joined groups" } diff --git a/src/wrappers/WithMatrixClient.js b/src/wrappers/withMatrixClient.js similarity index 92% rename from src/wrappers/WithMatrixClient.js rename to src/wrappers/withMatrixClient.js index 8e56d17dff..2333358817 100644 --- a/src/wrappers/WithMatrixClient.js +++ b/src/wrappers/withMatrixClient.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -26,7 +27,7 @@ import React from 'react'; */ export default function(WrappedComponent) { return React.createClass({ - displayName: "WithMatrixClient<" + WrappedComponent.displayName + ">", + displayName: "withMatrixClient<" + WrappedComponent.displayName + ">", contextTypes: { matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired,