diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js
new file mode 100644
index 0000000000..a3459a16c7
--- /dev/null
+++ b/src/components/structures/MyGroups.js
@@ -0,0 +1,112 @@
+/*
+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 } 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};
+ }
+});
+
+module.exports = WithMatrixClient(React.createClass({
+ displayName: 'GroupList',
+
+ propTypes: {
+ matrixClient: React.PropTypes.object.isRequired,
+ },
+
+ getInitialState: function() {
+ return {
+ groups: null,
+ error: null,
+ };
+ },
+
+ componentWillMount: function() {
+ this._fetch();
+ },
+
+ componentWillUnmount: function() {
+ },
+
+ _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({result: null, error: err});
+ });
+ },
+
+ render: function() {
+ const Loader = sdk.getComponent("elements.Spinner");
+ const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
+
+ let content;
+ if (this.state.groups) {
+ let groupNodes = [];
+ this.state.groups.forEach((g) => {
+ groupNodes.push(
+
+
+
+ );
+ });
+ content = {groupNodes}
;
+ } else if (this.state.error) {
+ content =
+ Error whilst fetching joined groups
+
;
+ }
+
+ return
+
+
+
+ {_t('Create a new group')}
+
+
+ You are a member of these groups:
+ {content}
+
;
+ },
+}));
diff --git a/src/components/views/dialogs/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.js
new file mode 100644
index 0000000000..64984ebf5c
--- /dev/null
+++ b/src/components/views/dialogs/CreateGroupDialog.js
@@ -0,0 +1,190 @@
+/*
+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 dis from '../../../dispatcher';
+import { _t } from '../../../languageHandler';
+import MatrixClientPeg from '../../../MatrixClientPeg';
+import AccessibleButton from '../elements/AccessibleButton';
+
+// 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: React.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 localpart = parsedGroupId[0];
+ 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.
+ */
+ _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 Loader = 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 (
+
+
+
+ );
+ },
+});