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 (
+
+
+
+ );
+ },
+});
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,