From 023daef4b784f1891f83aeca5fc8a427af8df658 Mon Sep 17 00:00:00 2001
From: Luke Barnard <luke@matrix.org>
Date: Tue, 1 May 2018 11:18:45 +0100
Subject: [PATCH] Refactor GroupStores into one global GroupStore

Take a step closer to a flux-like architecture for group data, for
the purposes of providing features that require it.

Now the app has a single GroupStore that can be poked to fetch
updates for a particular group.
---
 src/GroupAddressPicker.js                     |   7 +-
 src/components/structures/GroupView.js        | 103 ++++----
 src/components/structures/RightPanel.js       |  15 +-
 .../views/dialogs/AddressPickerDialog.js      |   5 +-
 .../views/groups/GroupMemberInfo.js           |  26 +-
 .../views/groups/GroupMemberList.js           |  13 +-
 .../views/groups/GroupPublicityToggle.js      |  10 +-
 src/components/views/groups/GroupRoomInfo.js  |  23 +-
 src/components/views/groups/GroupRoomList.js  |  25 +-
 src/components/views/rooms/RoomList.js        |  32 +--
 src/stores/GroupStore.js                      | 223 +++++++++---------
 src/stores/GroupStoreCache.js                 |  38 ---
 src/utils/MultiInviter.js                     |   6 +-
 13 files changed, 245 insertions(+), 281 deletions(-)
 delete mode 100644 src/stores/GroupStoreCache.js

diff --git a/src/GroupAddressPicker.js b/src/GroupAddressPicker.js
index c45a335ab6..91380b6eed 100644
--- a/src/GroupAddressPicker.js
+++ b/src/GroupAddressPicker.js
@@ -19,7 +19,7 @@ import sdk from './';
 import MultiInviter from './utils/MultiInviter';
 import { _t } from './languageHandler';
 import MatrixClientPeg from './MatrixClientPeg';
-import GroupStoreCache from './stores/GroupStoreCache';
+import GroupStore from './stores/GroupStore';
 
 export function showGroupInviteDialog(groupId) {
     return new Promise((resolve, reject) => {
@@ -116,11 +116,10 @@ function _onGroupInviteFinished(groupId, addrs) {
 
 function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) {
     const matrixClient = MatrixClientPeg.get();
-    const groupStore = GroupStoreCache.getGroupStore(groupId);
     const errorList = [];
     return Promise.all(addrs.map((addr) => {
-        return groupStore
-            .addRoomToGroup(addr.address, addRoomsPublicly)
+        return GroupStore
+            .addRoomToGroup(groupId, addr.address, addRoomsPublicly)
             .catch(() => { errorList.push(addr.address); })
             .then(() => {
                 const roomId = addr.address;
diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js
index 62fdb1070a..534d8d3df8 100644
--- a/src/components/structures/GroupView.js
+++ b/src/components/structures/GroupView.js
@@ -27,7 +27,6 @@ import AccessibleButton from '../views/elements/AccessibleButton';
 import Modal from '../../Modal';
 import classnames from 'classnames';
 
-import GroupStoreCache from '../../stores/GroupStoreCache';
 import GroupStore from '../../stores/GroupStore';
 import FlairStore from '../../stores/FlairStore';
 import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
@@ -93,8 +92,8 @@ const CategoryRoomList = React.createClass({
                 if (!success) return;
                 const errorList = [];
                 Promise.all(addrs.map((addr) => {
-                    return this.context.groupStore
-                        .addRoomToGroupSummary(addr.address)
+                    return GroupStore
+                        .addRoomToGroupSummary(this.props.groupId, addr.address)
                         .catch(() => { errorList.push(addr.address); })
                         .reflect();
                 })).then(() => {
@@ -174,7 +173,8 @@ const FeaturedRoom = React.createClass({
     onDeleteClicked: function(e) {
         e.preventDefault();
         e.stopPropagation();
-        this.context.groupStore.removeRoomFromGroupSummary(
+        GroupStore.removeRoomFromGroupSummary(
+            this.props.groupId,
             this.props.summaryInfo.room_id,
         ).catch((err) => {
             console.error('Error whilst removing room from group summary', err);
@@ -269,7 +269,7 @@ const RoleUserList = React.createClass({
                 if (!success) return;
                 const errorList = [];
                 Promise.all(addrs.map((addr) => {
-                    return this.context.groupStore
+                    return GroupStore
                         .addUserToGroupSummary(addr.address)
                         .catch(() => { errorList.push(addr.address); })
                         .reflect();
@@ -344,7 +344,8 @@ const FeaturedUser = React.createClass({
     onDeleteClicked: function(e) {
         e.preventDefault();
         e.stopPropagation();
-        this.context.groupStore.removeUserFromGroupSummary(
+        GroupStore.removeUserFromGroupSummary(
+            this.props.groupId,
             this.props.summaryInfo.user_id,
         ).catch((err) => {
             console.error('Error whilst removing user from group summary', err);
@@ -390,15 +391,6 @@ const FeaturedUser = React.createClass({
     },
 });
 
-const GroupContext = {
-    groupStore: PropTypes.instanceOf(GroupStore).isRequired,
-};
-
-CategoryRoomList.contextTypes = GroupContext;
-FeaturedRoom.contextTypes = GroupContext;
-RoleUserList.contextTypes = GroupContext;
-FeaturedUser.contextTypes = GroupContext;
-
 const GROUP_JOINPOLICY_OPEN = "open";
 const GROUP_JOINPOLICY_INVITE = "invite";
 
@@ -415,12 +407,6 @@ export default React.createClass({
         groupStore: PropTypes.instanceOf(GroupStore),
     },
 
-    getChildContext: function() {
-        return {
-            groupStore: this._groupStore,
-        };
-    },
-
     getInitialState: function() {
         return {
             summary: null,
@@ -440,6 +426,7 @@ export default React.createClass({
     },
 
     componentWillMount: function() {
+        this._unmounted = false;
         this._matrixClient = MatrixClientPeg.get();
         this._matrixClient.on("Group.myMembership", this._onGroupMyMembership);
 
@@ -448,8 +435,8 @@ export default React.createClass({
     },
 
     componentWillUnmount: function() {
+        this._unmounted = true;
         this._matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
-        this._groupStore.removeAllListeners();
     },
 
     componentWillReceiveProps: function(newProps) {
@@ -464,8 +451,7 @@ export default React.createClass({
     },
 
     _onGroupMyMembership: function(group) {
-        if (group.groupId !== this.props.groupId) return;
-
+        if (this._unmounted || group.groupId !== this.props.groupId) return;
         if (group.myMembership === 'leave') {
             // Leave settings - the user might have clicked the "Leave" button
             this._closeSettings();
@@ -478,34 +464,11 @@ export default React.createClass({
         if (group && group.inviter && group.inviter.userId) {
             this._fetchInviterProfile(group.inviter.userId);
         }
-        this._groupStore = GroupStoreCache.getGroupStore(groupId);
-        this._groupStore.registerListener(() => {
-            const summary = this._groupStore.getSummary();
-            if (summary.profile) {
-                // Default profile fields should be "" for later sending to the server (which
-                // requires that the fields are strings, not null)
-                ["avatar_url", "long_description", "name", "short_description"].forEach((k) => {
-                    summary.profile[k] = summary.profile[k] || "";
-                });
-            }
-            this.setState({
-                summary,
-                summaryLoading: !this._groupStore.isStateReady(GroupStore.STATE_KEY.Summary),
-                isGroupPublicised: this._groupStore.getGroupPublicity(),
-                isUserPrivileged: this._groupStore.isUserPrivileged(),
-                groupRooms: this._groupStore.getGroupRooms(),
-                groupRoomsLoading: !this._groupStore.isStateReady(GroupStore.STATE_KEY.GroupRooms),
-                isUserMember: this._groupStore.getGroupMembers().some(
-                    (m) => m.userId === this._matrixClient.credentials.userId,
-                ),
-                error: null,
-            });
-            if (this.props.groupIsNew && firstInit) {
-                this._onEditClick();
-            }
-        });
+        GroupStore.registerListener(groupId, this.onGroupStoreUpdated.bind(this, firstInit));
         let willDoOnboarding = false;
-        this._groupStore.on('error', (err) => {
+        // XXX: This should be more fluxy - let's get the error from GroupStore .getError or something
+        GroupStore.on('error', (err, errorGroupId) => {
+            if (this._unmounted || groupId !== errorGroupId) return;
             if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN' && !willDoOnboarding) {
                 dis.dispatch({
                     action: 'do_after_sync_prepared',
@@ -524,11 +487,40 @@ export default React.createClass({
         });
     },
 
+    onGroupStoreUpdated(firstInit) {
+        if (this._unmounted) return;
+        const summary = GroupStore.getSummary(this.props.groupId);
+        if (summary.profile) {
+            // Default profile fields should be "" for later sending to the server (which
+            // requires that the fields are strings, not null)
+            ["avatar_url", "long_description", "name", "short_description"].forEach((k) => {
+                summary.profile[k] = summary.profile[k] || "";
+            });
+        }
+        this.setState({
+            summary,
+            summaryLoading: !GroupStore.isStateReady(this.props.groupId, GroupStore.STATE_KEY.Summary),
+            isGroupPublicised: GroupStore.getGroupPublicity(this.props.groupId),
+            isUserPrivileged: GroupStore.isUserPrivileged(this.props.groupId),
+            groupRooms: GroupStore.getGroupRooms(this.props.groupId),
+            groupRoomsLoading: !GroupStore.isStateReady(this.props.groupId, GroupStore.STATE_KEY.GroupRooms),
+            isUserMember: GroupStore.getGroupMembers(this.props.groupId).some(
+                (m) => m.userId === this._matrixClient.credentials.userId,
+            ),
+            error: null,
+        });
+        // XXX: This might not work but this.props.groupIsNew unused anyway
+        if (this.props.groupIsNew && firstInit) {
+            this._onEditClick();
+        }
+    },
+
     _fetchInviterProfile(userId) {
         this.setState({
             inviterProfileBusy: true,
         });
         this._matrixClient.getProfileInfo(userId).then((resp) => {
+            if (this._unmounted) return;
             this.setState({
                 inviterProfile: {
                     avatarUrl: resp.avatar_url,
@@ -538,6 +530,7 @@ export default React.createClass({
         }).catch((e) => {
             console.error('Error getting group inviter profile', e);
         }).finally(() => {
+            if (this._unmounted) return;
             this.setState({
                 inviterProfileBusy: false,
             });
@@ -677,7 +670,7 @@ export default React.createClass({
         // spinner disappearing after we have fetched new group data.
         await Promise.delay(500);
 
-        this._groupStore.acceptGroupInvite().then(() => {
+        GroupStore.acceptGroupInvite(this.props.groupId).then(() => {
             // don't reset membershipBusy here: wait for the membership change to come down the sync
         }).catch((e) => {
             this.setState({membershipBusy: false});
@@ -696,7 +689,7 @@ export default React.createClass({
         // spinner disappearing after we have fetched new group data.
         await Promise.delay(500);
 
-        this._groupStore.leaveGroup().then(() => {
+        GroupStore.leaveGroup(this.props.groupId).then(() => {
             // don't reset membershipBusy here: wait for the membership change to come down the sync
         }).catch((e) => {
             this.setState({membershipBusy: false});
@@ -715,7 +708,7 @@ export default React.createClass({
         // spinner disappearing after we have fetched new group data.
         await Promise.delay(500);
 
-        this._groupStore.joinGroup().then(() => {
+        GroupStore.joinGroup(this.props.groupId).then(() => {
             // don't reset membershipBusy here: wait for the membership change to come down the sync
         }).catch((e) => {
             this.setState({membershipBusy: false});
@@ -743,7 +736,7 @@ export default React.createClass({
                 // spinner disappearing after we have fetched new group data.
                 await Promise.delay(500);
 
-                this._groupStore.leaveGroup().then(() => {
+                GroupStore.leaveGroup(this.props.groupId).then(() => {
                     // don't reset membershipBusy here: wait for the membership change to come down the sync
                 }).catch((e) => {
                     this.setState({membershipBusy: false});
diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js
index ca1e331d15..18523ceb59 100644
--- a/src/components/structures/RightPanel.js
+++ b/src/components/structures/RightPanel.js
@@ -27,7 +27,7 @@ import Analytics from '../../Analytics';
 import RateLimitedFunc from '../../ratelimitedfunc';
 import AccessibleButton from '../../components/views/elements/AccessibleButton';
 import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
-import GroupStoreCache from '../../stores/GroupStoreCache';
+import GroupStore from '../../stores/GroupStore';
 
 import { formatCount } from '../../utils/FormattingUtils';
 
@@ -120,7 +120,7 @@ module.exports = React.createClass({
         if (this.context.matrixClient) {
             this.context.matrixClient.removeListener("RoomState.members", this.onRoomStateMember);
         }
-        this._unregisterGroupStore();
+        this._unregisterGroupStore(this.props.groupId);
     },
 
     getInitialState: function() {
@@ -132,26 +132,23 @@ module.exports = React.createClass({
 
     componentWillReceiveProps(newProps) {
         if (newProps.groupId !== this.props.groupId) {
-            this._unregisterGroupStore();
+            this._unregisterGroupStore(this.props.groupId);
             this._initGroupStore(newProps.groupId);
         }
     },
 
     _initGroupStore(groupId) {
         if (!groupId) return;
-        this._groupStore = GroupStoreCache.getGroupStore(groupId);
-        this._groupStore.registerListener(this.onGroupStoreUpdated);
+        GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
     },
 
     _unregisterGroupStore() {
-        if (this._groupStore) {
-            this._groupStore.unregisterListener(this.onGroupStoreUpdated);
-        }
+        GroupStore.unregisterListener(this.onGroupStoreUpdated);
     },
 
     onGroupStoreUpdated: function() {
         this.setState({
-            isUserPrivilegedInGroup: this._groupStore.isUserPrivileged(),
+            isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
         });
     },
 
diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js
index 685c4fcde3..0d0b7456b5 100644
--- a/src/components/views/dialogs/AddressPickerDialog.js
+++ b/src/components/views/dialogs/AddressPickerDialog.js
@@ -22,7 +22,7 @@ import sdk from '../../../index';
 import MatrixClientPeg from '../../../MatrixClientPeg';
 import Promise from 'bluebird';
 import { addressTypes, getAddressType } from '../../../UserAddress.js';
-import GroupStoreCache from '../../../stores/GroupStoreCache';
+import GroupStore from '../../../stores/GroupStore';
 
 const TRUNCATE_QUERY_LIST = 40;
 const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
@@ -243,9 +243,8 @@ module.exports = React.createClass({
 
     _doNaiveGroupRoomSearch: function(query) {
         const lowerCaseQuery = query.toLowerCase();
-        const groupStore = GroupStoreCache.getGroupStore(this.props.groupId);
         const results = [];
-        groupStore.getGroupRooms().forEach((r) => {
+        GroupStore.getGroupRooms(this.props.groupId).forEach((r) => {
             const nameMatch = (r.name || '').toLowerCase().includes(lowerCaseQuery);
             const topicMatch = (r.topic || '').toLowerCase().includes(lowerCaseQuery);
             const aliasMatch = (r.canonical_alias || '').toLowerCase().includes(lowerCaseQuery);
diff --git a/src/components/views/groups/GroupMemberInfo.js b/src/components/views/groups/GroupMemberInfo.js
index 4970a26e5b..4fed293bec 100644
--- a/src/components/views/groups/GroupMemberInfo.js
+++ b/src/components/views/groups/GroupMemberInfo.js
@@ -23,7 +23,7 @@ import Modal from '../../../Modal';
 import sdk from '../../../index';
 import { _t } from '../../../languageHandler';
 import { GroupMemberType } from '../../../groups';
-import GroupStoreCache from '../../../stores/GroupStoreCache';
+import GroupStore from '../../../stores/GroupStore';
 import AccessibleButton from '../elements/AccessibleButton';
 
 module.exports = React.createClass({
@@ -47,33 +47,37 @@ module.exports = React.createClass({
     },
 
     componentWillMount: function() {
+        this._unmounted = false;
         this._initGroupStore(this.props.groupId);
     },
 
     componentWillReceiveProps(newProps) {
         if (newProps.groupId !== this.props.groupId) {
-            this._unregisterGroupStore();
+            this._unregisterGroupStore(this.props.groupId);
             this._initGroupStore(newProps.groupId);
         }
     },
 
-    _initGroupStore(groupId) {
-        this._groupStore = GroupStoreCache.getGroupStore(this.props.groupId);
-        this._groupStore.registerListener(this.onGroupStoreUpdated);
+    componentWillUnmount() {
+        this._unmounted = true;
+        this._unregisterGroupStore(this.props.groupId);
     },
 
-    _unregisterGroupStore() {
-        if (this._groupStore) {
-            this._groupStore.unregisterListener(this.onGroupStoreUpdated);
-        }
+    _initGroupStore(groupId) {
+        GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
+    },
+
+    _unregisterGroupStore(groupId) {
+        GroupStore.unregisterListener(this.onGroupStoreUpdated);
     },
 
     onGroupStoreUpdated: function() {
+        if (this._unmounted) return;
         this.setState({
-            isUserInvited: this._groupStore.getGroupInvitedMembers().some(
+            isUserInvited: GroupStore.getGroupInvitedMembers(this.props.groupId).some(
                 (m) => m.userId === this.props.groupMember.userId,
             ),
-            isUserPrivilegedInGroup: this._groupStore.isUserPrivileged(),
+            isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
         });
     },
 
diff --git a/src/components/views/groups/GroupMemberList.js b/src/components/views/groups/GroupMemberList.js
index 17a91d83fa..faf172083f 100644
--- a/src/components/views/groups/GroupMemberList.js
+++ b/src/components/views/groups/GroupMemberList.js
@@ -17,7 +17,7 @@ limitations under the License.
 import React from 'react';
 import { _t } from '../../../languageHandler';
 import sdk from '../../../index';
-import GroupStoreCache from '../../../stores/GroupStoreCache';
+import GroupStore from '../../../stores/GroupStore';
 import PropTypes from 'prop-types';
 
 const INITIAL_LOAD_NUM_MEMBERS = 30;
@@ -42,9 +42,12 @@ export default React.createClass({
         this._initGroupStore(this.props.groupId);
     },
 
+    componentWillUnmount: function() {
+        this._unmounted = true;
+    },
+
     _initGroupStore: function(groupId) {
-        this._groupStore = GroupStoreCache.getGroupStore(groupId);
-        this._groupStore.registerListener(() => {
+        GroupStore.registerListener(groupId, () => {
             this._fetchMembers();
         });
     },
@@ -52,8 +55,8 @@ export default React.createClass({
     _fetchMembers: function() {
         if (this._unmounted) return;
         this.setState({
-            members: this._groupStore.getGroupMembers(),
-            invitedMembers: this._groupStore.getGroupInvitedMembers(),
+            members: GroupStore.getGroupMembers(this.props.groupId),
+            invitedMembers: GroupStore.getGroupInvitedMembers(this.props.groupId),
         });
     },
 
diff --git a/src/components/views/groups/GroupPublicityToggle.js b/src/components/views/groups/GroupPublicityToggle.js
index 0fcabb4ef8..0dd35784a0 100644
--- a/src/components/views/groups/GroupPublicityToggle.js
+++ b/src/components/views/groups/GroupPublicityToggle.js
@@ -17,7 +17,6 @@ limitations under the License.
 import React from 'react';
 import PropTypes from 'prop-types';
 import sdk from '../../../index';
-import GroupStoreCache from '../../../stores/GroupStoreCache';
 import GroupStore from '../../../stores/GroupStore';
 import { _t } from '../../../languageHandler.js';
 
@@ -41,11 +40,10 @@ export default React.createClass({
     },
 
     _initGroupStore: function(groupId) {
-        this._groupStore = GroupStoreCache.getGroupStore(groupId);
-        this._groupStore.registerListener(() => {
+        GroupStore.registerListener(groupId, () => {
             this.setState({
-                isGroupPublicised: this._groupStore.getGroupPublicity(),
-                ready: this._groupStore.isStateReady(GroupStore.STATE_KEY.Summary),
+                isGroupPublicised: GroupStore.getGroupPublicity(groupId),
+                ready: GroupStore.isStateReady(groupId, GroupStore.STATE_KEY.Summary),
             });
         });
     },
@@ -57,7 +55,7 @@ export default React.createClass({
             // Optimistic early update
             isGroupPublicised: !this.state.isGroupPublicised,
         });
-        this._groupStore.setGroupPublicity(!this.state.isGroupPublicised).then(() => {
+        GroupStore.setGroupPublicity(this.props.groupId, !this.state.isGroupPublicised).then(() => {
             this.setState({
                 busy: false,
             });
diff --git a/src/components/views/groups/GroupRoomInfo.js b/src/components/views/groups/GroupRoomInfo.js
index 2d2b4e655c..41e5f68736 100644
--- a/src/components/views/groups/GroupRoomInfo.js
+++ b/src/components/views/groups/GroupRoomInfo.js
@@ -21,7 +21,7 @@ import dis from '../../../dispatcher';
 import Modal from '../../../Modal';
 import sdk from '../../../index';
 import { _t } from '../../../languageHandler';
-import GroupStoreCache from '../../../stores/GroupStoreCache';
+import GroupStore from '../../../stores/GroupStore';
 
 module.exports = React.createClass({
     displayName: 'GroupRoomInfo',
@@ -50,29 +50,26 @@ module.exports = React.createClass({
 
     componentWillReceiveProps(newProps) {
         if (newProps.groupId !== this.props.groupId) {
-            this._unregisterGroupStore();
+            this._unregisterGroupStore(this.props.groupId);
             this._initGroupStore(newProps.groupId);
         }
     },
 
     componentWillUnmount() {
-        this._unregisterGroupStore();
+        this._unregisterGroupStore(this.props.groupId);
     },
 
     _initGroupStore(groupId) {
-        this._groupStore = GroupStoreCache.getGroupStore(this.props.groupId);
-        this._groupStore.registerListener(this.onGroupStoreUpdated);
+        GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
     },
 
-    _unregisterGroupStore() {
-        if (this._groupStore) {
-            this._groupStore.unregisterListener(this.onGroupStoreUpdated);
-        }
+    _unregisterGroupStore(groupId) {
+        GroupStore.unregisterListener(this.onGroupStoreUpdated);
     },
 
     _updateGroupRoom() {
         this.setState({
-            groupRoom: this._groupStore.getGroupRooms().find(
+            groupRoom: GroupStore.getGroupRooms(this.props.groupId).find(
                 (r) => r.roomId === this.props.groupRoomId,
             ),
         });
@@ -80,7 +77,7 @@ module.exports = React.createClass({
 
     onGroupStoreUpdated: function() {
         this.setState({
-            isUserPrivilegedInGroup: this._groupStore.isUserPrivileged(),
+            isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
         });
         this._updateGroupRoom();
     },
@@ -100,7 +97,7 @@ module.exports = React.createClass({
                 this.setState({groupRoomRemoveLoading: true});
                 const groupId = this.props.groupId;
                 const roomId = this.props.groupRoomId;
-                this._groupStore.removeRoomFromGroup(roomId).then(() => {
+                GroupStore.removeRoomFromGroup(this.props.groupId, roomId).then(() => {
                     dis.dispatch({
                         action: "view_group_room_list",
                     });
@@ -134,7 +131,7 @@ module.exports = React.createClass({
         const groupId = this.props.groupId;
         const roomId = this.props.groupRoomId;
         const roomName = this.state.groupRoom.displayname;
-        this._groupStore.updateGroupRoomVisibility(roomId, isPublic).catch((err) => {
+        GroupStore.updateGroupRoomVisibility(this.props.groupId, roomId, isPublic).catch((err) => {
             console.error(`Error whilst changing visibility of ${roomId} in ${groupId} to ${isPublic}`, err);
             const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
             Modal.createTrackedDialog('Failed to remove room from group', '', ErrorDialog, {
diff --git a/src/components/views/groups/GroupRoomList.js b/src/components/views/groups/GroupRoomList.js
index 0515865c6b..cfd2b806d4 100644
--- a/src/components/views/groups/GroupRoomList.js
+++ b/src/components/views/groups/GroupRoomList.js
@@ -16,7 +16,7 @@ limitations under the License.
 import React from 'react';
 import { _t } from '../../../languageHandler';
 import sdk from '../../../index';
-import GroupStoreCache from '../../../stores/GroupStoreCache';
+import GroupStore from '../../../stores/GroupStore';
 import PropTypes from 'prop-types';
 
 const INITIAL_LOAD_NUM_ROOMS = 30;
@@ -39,22 +39,31 @@ export default React.createClass({
         this._initGroupStore(this.props.groupId);
     },
 
+    componentWillUnmount() {
+        this._unmounted = true;
+        this._unregisterGroupStore();
+    },
+
+    _unregisterGroupStore() {
+        GroupStore.unregisterListener(this.onGroupStoreUpdated);
+    },
+
     _initGroupStore: function(groupId) {
-        this._groupStore = GroupStoreCache.getGroupStore(groupId);
-        this._groupStore.registerListener(() => {
-            this._fetchRooms();
-        });
-        this._groupStore.on('error', (err) => {
+        GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
+        // XXX: This should be more fluxy - let's get the error from GroupStore .getError or something
+        // XXX: This is also leaked - we should remove it when unmounting
+        GroupStore.on('error', (err, errorGroupId) => {
+            if (errorGroupId !== groupId) return;
             this.setState({
                 rooms: null,
             });
         });
     },
 
-    _fetchRooms: function() {
+    onGroupStoreUpdated: function() {
         if (this._unmounted) return;
         this.setState({
-            rooms: this._groupStore.getGroupRooms(),
+            rooms: GroupStore.getGroupRooms(this.props.groupId),
         });
     },
 
diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js
index acf04831e8..5be28ae6fa 100644
--- a/src/components/views/rooms/RoomList.js
+++ b/src/components/views/rooms/RoomList.js
@@ -30,7 +30,7 @@ import DMRoomMap from '../../../utils/DMRoomMap';
 const Receipt = require('../../../utils/Receipt');
 import TagOrderStore from '../../../stores/TagOrderStore';
 import RoomListStore from '../../../stores/RoomListStore';
-import GroupStoreCache from '../../../stores/GroupStoreCache';
+import GroupStore from '../../../stores/GroupStore';
 
 const HIDE_CONFERENCE_CHANS = true;
 const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
@@ -83,8 +83,6 @@ module.exports = React.createClass({
         cli.on("Group.myMembership", this._onGroupMyMembership);
 
         const dmRoomMap = DMRoomMap.shared();
-        this._groupStores = {};
-        this._groupStoreTokens = [];
         // A map between tags which are group IDs and the room IDs of rooms that should be kept
         // in the room list when filtering by that tag.
         this._visibleRoomsForGroup = {
@@ -96,17 +94,14 @@ module.exports = React.createClass({
         // When the selected tags are changed, initialise a group store if necessary
         this._tagStoreToken = TagOrderStore.addListener(() => {
             (TagOrderStore.getOrderedTags() || []).forEach((tag) => {
-                if (tag[0] !== '+' || this._groupStores[tag]) {
+                if (tag[0] !== '+') {
                     return;
                 }
-                this._groupStores[tag] = GroupStoreCache.getGroupStore(tag);
-                this._groupStoreTokens.push(
-                    this._groupStores[tag].registerListener(() => {
-                        // This group's rooms or members may have updated, update rooms for its tag
-                        this.updateVisibleRoomsForTag(dmRoomMap, tag);
-                        this.updateVisibleRooms();
-                    }),
-                );
+                this.groupStoreToken = GroupStore.registerListener(tag, () => {
+                    // This group's rooms or members may have updated, update rooms for its tag
+                    this.updateVisibleRoomsForTag(dmRoomMap, tag);
+                    this.updateVisibleRooms();
+                });
             });
             // Filters themselves have changed, refresh the selected tags
             this.updateVisibleRooms();
@@ -183,10 +178,8 @@ module.exports = React.createClass({
             this._roomListStoreToken.remove();
         }
 
-        if (this._groupStoreTokens.length > 0) {
-            // NB: GroupStore is not a Flux.Store
-            this._groupStoreTokens.forEach((token) => token.unregister());
-        }
+        // NB: GroupStore is not a Flux.Store
+        this._groupStoreToken.unregister();
 
         // cancel any pending calls to the rate_limited_funcs
         this._delayedRefreshRoomList.cancelPendingCall();
@@ -259,12 +252,11 @@ module.exports = React.createClass({
     updateVisibleRoomsForTag: function(dmRoomMap, tag) {
         if (!this.mounted) return;
         // For now, only handle group tags
-        const store = this._groupStores[tag];
-        if (!store) return;
+        if (tag[0] !== '+') return;
 
         this._visibleRoomsForGroup[tag] = [];
-        store.getGroupRooms().forEach((room) => this._visibleRoomsForGroup[tag].push(room.roomId));
-        store.getGroupMembers().forEach((member) => {
+        GroupStore.getGroupRooms(tag).forEach((room) => this._visibleRoomsForGroup[tag].push(room.roomId));
+        GroupStore.getGroupMembers(tag).forEach((member) => {
             if (member.userId === MatrixClientPeg.get().credentials.userId) return;
             dmRoomMap.getDMRoomsForUserId(member.userId).forEach(
                 (roomId) => this._visibleRoomsForGroup[tag].push(roomId),
diff --git a/src/stores/GroupStore.js b/src/stores/GroupStore.js
index d4f0b09ff9..23ce5314ec 100644
--- a/src/stores/GroupStore.js
+++ b/src/stores/GroupStore.js
@@ -70,84 +70,90 @@ function limitConcurrency(fn) {
 }
 
 /**
- * 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.
+ * Global store for tracking group summary, members, invited members and rooms.
  */
-export default class GroupStore extends EventEmitter {
-
-    static STATE_KEY = {
+class GroupStore extends EventEmitter {
+    STATE_KEY = {
         GroupMembers: 'GroupMembers',
         GroupInvitedMembers: 'GroupInvitedMembers',
         Summary: 'Summary',
         GroupRooms: 'GroupRooms',
     };
 
-    constructor(groupId) {
+    constructor() {
         super();
-        if (!groupId) {
-            throw new Error('GroupStore needs a valid groupId to be created');
-        }
-        this.groupId = groupId;
         this._state = {};
-        this._state[GroupStore.STATE_KEY.Summary] = {};
-        this._state[GroupStore.STATE_KEY.GroupRooms] = [];
-        this._state[GroupStore.STATE_KEY.GroupMembers] = [];
-        this._state[GroupStore.STATE_KEY.GroupInvitedMembers] = [];
-        this._ready = {};
+        this._state[this.STATE_KEY.Summary] = {};
+        this._state[this.STATE_KEY.GroupRooms] = {};
+        this._state[this.STATE_KEY.GroupMembers] = {};
+        this._state[this.STATE_KEY.GroupInvitedMembers] = {};
+
+        this._ready = {};
+        this._ready[this.STATE_KEY.Summary] = {};
+        this._ready[this.STATE_KEY.GroupRooms] = {};
+        this._ready[this.STATE_KEY.GroupMembers] = {};
+        this._ready[this.STATE_KEY.GroupInvitedMembers] = {};
+
+        this._fetchResourcePromise = {
+            [this.STATE_KEY.Summary]: {},
+            [this.STATE_KEY.GroupRooms]: {},
+            [this.STATE_KEY.GroupMembers]: {},
+            [this.STATE_KEY.GroupInvitedMembers]: {},
+        };
 
-        this._fetchResourcePromise = {};
         this._resourceFetcher = {
-            [GroupStore.STATE_KEY.Summary]: () => {
+            [this.STATE_KEY.Summary]: (groupId) => {
                 return limitConcurrency(
-                    () => MatrixClientPeg.get().getGroupSummary(this.groupId),
+                    () => MatrixClientPeg.get().getGroupSummary(groupId),
                 );
             },
-            [GroupStore.STATE_KEY.GroupRooms]: () => {
+            [this.STATE_KEY.GroupRooms]: (groupId) => {
                 return limitConcurrency(
-                    () => MatrixClientPeg.get().getGroupRooms(this.groupId).then(parseRoomsResponse),
+                    () => MatrixClientPeg.get().getGroupRooms(groupId).then(parseRoomsResponse),
                 );
             },
-            [GroupStore.STATE_KEY.GroupMembers]: () => {
+            [this.STATE_KEY.GroupMembers]: (groupId) => {
                 return limitConcurrency(
-                    () => MatrixClientPeg.get().getGroupUsers(this.groupId).then(parseMembersResponse),
+                    () => MatrixClientPeg.get().getGroupUsers(groupId).then(parseMembersResponse),
                 );
             },
-            [GroupStore.STATE_KEY.GroupInvitedMembers]: () => {
+            [this.STATE_KEY.GroupInvitedMembers]: (groupId) => {
                 return limitConcurrency(
-                    () => MatrixClientPeg.get().getGroupInvitedUsers(this.groupId).then(parseMembersResponse),
+                    () => MatrixClientPeg.get().getGroupInvitedUsers(groupId).then(parseMembersResponse),
                 );
             },
         };
 
-        this.on('error', (err) => {
-            console.error(`GroupStore for ${this.groupId} encountered error`, err);
+        this.on('error', (err, groupId) => {
+            console.error(`GroupStore encountered error whilst fetching data for ${groupId}`, err);
         });
     }
 
-    _fetchResource(stateKey) {
+    _fetchResource(stateKey, groupId) {
         // Ongoing request, ignore
-        if (this._fetchResourcePromise[stateKey]) return;
+        if (this._fetchResourcePromise[stateKey][groupId]) return;
 
-        const clientPromise = this._resourceFetcher[stateKey]();
+        const clientPromise = this._resourceFetcher[stateKey](groupId);
 
         // Indicate ongoing request
-        this._fetchResourcePromise[stateKey] = clientPromise;
+        this._fetchResourcePromise[stateKey][groupId] = clientPromise;
 
         clientPromise.then((result) => {
-            this._state[stateKey] = result;
-            this._ready[stateKey] = true;
+            this._state[stateKey][groupId] = result;
+            console.info(this._state);
+            this._ready[stateKey][groupId] = true;
             this._notifyListeners();
         }).catch((err) => {
             // Invited users not visible to non-members
-            if (stateKey === GroupStore.STATE_KEY.GroupInvitedMembers && err.httpStatus === 403) {
+            if (stateKey === this.STATE_KEY.GroupInvitedMembers && err.httpStatus === 403) {
                 return;
             }
 
-            console.error("Failed to get resource " + stateKey + ":" + err);
-            this.emit('error', err);
+            console.error(`Failed to get resource ${stateKey} for ${groupId}`, err);
+            this.emit('error', err, groupId);
         }).finally(() => {
             // Indicate finished request, allow for future fetches
-            delete this._fetchResourcePromise[stateKey];
+            delete this._fetchResourcePromise[stateKey][groupId];
         });
 
         return clientPromise;
@@ -162,25 +168,26 @@ export default class GroupStore extends EventEmitter {
      * immediately triggers an update to send the current state of the
      * store (which could be the initial state).
      *
-     * This also causes a fetch of all group data, which might cause
-     * 4 separate HTTP requests, but only said requests aren't already
-     * ongoing.
+     * This also causes a fetch of all data of the specified group,
+     * which might cause 4 separate HTTP requests, but only if said
+     * requests aren't already ongoing.
      *
+     * @param {string} groupId the ID of the group to fetch data for.
      * @param {function} fn the function to call when the store updates.
      * @return {Object} tok a registration "token" with a single
      *                      property `unregister`, a function that can
      *                      be called to unregister the listener such
      *                      that it won't be called any more.
      */
-    registerListener(fn) {
+    registerListener(groupId, fn) {
         this.on('update', fn);
         // Call to set initial state (before fetching starts)
         this.emit('update');
 
-        this._fetchResource(GroupStore.STATE_KEY.Summary);
-        this._fetchResource(GroupStore.STATE_KEY.GroupRooms);
-        this._fetchResource(GroupStore.STATE_KEY.GroupMembers);
-        this._fetchResource(GroupStore.STATE_KEY.GroupInvitedMembers);
+        this._fetchResource(this.STATE_KEY.Summary, groupId);
+        this._fetchResource(this.STATE_KEY.GroupRooms, groupId);
+        this._fetchResource(this.STATE_KEY.GroupMembers, groupId);
+        this._fetchResource(this.STATE_KEY.GroupInvitedMembers, groupId);
 
         // Similar to the Store of flux/utils, we return a "token" that
         // can be used to unregister the listener.
@@ -195,123 +202,129 @@ export default class GroupStore extends EventEmitter {
         this.removeListener('update', fn);
     }
 
-    isStateReady(id) {
-        return this._ready[id];
+    isStateReady(groupId, id) {
+        return this._ready[id][groupId];
     }
 
-    getSummary() {
-        return this._state[GroupStore.STATE_KEY.Summary];
+    getSummary(groupId) {
+        return this._state[this.STATE_KEY.Summary][groupId] || {};
     }
 
-    getGroupRooms() {
-        return this._state[GroupStore.STATE_KEY.GroupRooms];
+    getGroupRooms(groupId) {
+        return this._state[this.STATE_KEY.GroupRooms][groupId] || [];
     }
 
-    getGroupMembers() {
-        return this._state[GroupStore.STATE_KEY.GroupMembers];
+    getGroupMembers(groupId) {
+        return this._state[this.STATE_KEY.GroupMembers][groupId] || [];
     }
 
-    getGroupInvitedMembers() {
-        return this._state[GroupStore.STATE_KEY.GroupInvitedMembers];
+    getGroupInvitedMembers(groupId) {
+        return this._state[this.STATE_KEY.GroupInvitedMembers][groupId] || [];
     }
 
-    getGroupPublicity() {
-        return this._state[GroupStore.STATE_KEY.Summary].user ?
-            this._state[GroupStore.STATE_KEY.Summary].user.is_publicised : null;
+    getGroupPublicity(groupId) {
+        return (this._state[this.STATE_KEY.Summary][groupId] || {}).user ?
+            (this._state[this.STATE_KEY.Summary][groupId] || {}).user.is_publicised : null;
     }
 
-    isUserPrivileged() {
-        return this._state[GroupStore.STATE_KEY.Summary].user ?
-            this._state[GroupStore.STATE_KEY.Summary].user.is_privileged : null;
+    isUserPrivileged(groupId) {
+        return (this._state[this.STATE_KEY.Summary][groupId] || {}).user ?
+            (this._state[this.STATE_KEY.Summary][groupId] || {}).user.is_privileged : null;
     }
 
-    addRoomToGroup(roomId, isPublic) {
+    addRoomToGroup(groupId, roomId, isPublic) {
         return MatrixClientPeg.get()
-            .addRoomToGroup(this.groupId, roomId, isPublic)
-            .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupRooms));
+            .addRoomToGroup(groupId, roomId, isPublic)
+            .then(this._fetchResource.bind(this, this.STATE_KEY.GroupRooms, groupId));
     }
 
-    updateGroupRoomVisibility(roomId, isPublic) {
+    updateGroupRoomVisibility(groupId, roomId, isPublic) {
         return MatrixClientPeg.get()
-            .updateGroupRoomVisibility(this.groupId, roomId, isPublic)
-            .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupRooms));
+            .updateGroupRoomVisibility(groupId, roomId, isPublic)
+            .then(this._fetchResource.bind(this, this.STATE_KEY.GroupRooms, groupId));
     }
 
-    removeRoomFromGroup(roomId) {
+    removeRoomFromGroup(groupId, roomId) {
         return MatrixClientPeg.get()
-            .removeRoomFromGroup(this.groupId, roomId)
+            .removeRoomFromGroup(groupId, roomId)
             // Room might be in the summary, refresh just in case
-            .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary))
-            .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupRooms));
+            .then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId))
+            .then(this._fetchResource.bind(this, this.STATE_KEY.GroupRooms, groupId));
     }
 
-    inviteUserToGroup(userId) {
-        return MatrixClientPeg.get().inviteUserToGroup(this.groupId, userId)
-            .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupInvitedMembers));
+    inviteUserToGroup(groupId, userId) {
+        return MatrixClientPeg.get().inviteUserToGroup(groupId, userId)
+            .then(this._fetchResource.bind(this, this.STATE_KEY.GroupInvitedMembers, groupId));
     }
 
-    acceptGroupInvite() {
-        return MatrixClientPeg.get().acceptGroupInvite(this.groupId)
+    acceptGroupInvite(groupId) {
+        return MatrixClientPeg.get().acceptGroupInvite(groupId)
             // The user should now be able to access (personal) group settings
-            .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary))
+            .then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId))
             // The user might be able to see more rooms now
-            .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupRooms))
+            .then(this._fetchResource.bind(this, this.STATE_KEY.GroupRooms, groupId))
             // The user should now appear as a member
-            .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupMembers))
+            .then(this._fetchResource.bind(this, this.STATE_KEY.GroupMembers, groupId))
             // The user should now not appear as an invited member
-            .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupInvitedMembers));
+            .then(this._fetchResource.bind(this, this.STATE_KEY.GroupInvitedMembers, groupId));
     }
 
-    joinGroup() {
-        return MatrixClientPeg.get().joinGroup(this.groupId)
+    joinGroup(groupId) {
+        return MatrixClientPeg.get().joinGroup(groupId)
             // The user should now be able to access (personal) group settings
-            .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary))
+            .then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId))
             // The user might be able to see more rooms now
-            .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupRooms))
+            .then(this._fetchResource.bind(this, this.STATE_KEY.GroupRooms, groupId))
             // The user should now appear as a member
-            .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupMembers))
+            .then(this._fetchResource.bind(this, this.STATE_KEY.GroupMembers, groupId))
             // The user should now not appear as an invited member
-            .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupInvitedMembers));
+            .then(this._fetchResource.bind(this, this.STATE_KEY.GroupInvitedMembers, groupId));
     }
 
-    leaveGroup() {
-        return MatrixClientPeg.get().leaveGroup(this.groupId)
+    leaveGroup(groupId) {
+        return MatrixClientPeg.get().leaveGroup(groupId)
             // The user should now not be able to access group settings
-            .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary))
+            .then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId))
             // The user might only be able to see a subset of rooms now
-            .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupRooms))
+            .then(this._fetchResource.bind(this, this.STATE_KEY.GroupRooms, groupId))
             // The user should now not appear as a member
-            .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupMembers));
+            .then(this._fetchResource.bind(this, this.STATE_KEY.GroupMembers, groupId));
     }
 
-    addRoomToGroupSummary(roomId, categoryId) {
+    addRoomToGroupSummary(groupId, roomId, categoryId) {
         return MatrixClientPeg.get()
-            .addRoomToGroupSummary(this.groupId, roomId, categoryId)
-            .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary));
+            .addRoomToGroupSummary(groupId, roomId, categoryId)
+            .then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId));
     }
 
-    addUserToGroupSummary(userId, roleId) {
+    addUserToGroupSummary(groupId, userId, roleId) {
         return MatrixClientPeg.get()
-            .addUserToGroupSummary(this.groupId, userId, roleId)
-            .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary));
+            .addUserToGroupSummary(groupId, userId, roleId)
+            .then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId));
     }
 
-    removeRoomFromGroupSummary(roomId) {
+    removeRoomFromGroupSummary(groupId, roomId) {
         return MatrixClientPeg.get()
-            .removeRoomFromGroupSummary(this.groupId, roomId)
-            .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary));
+            .removeRoomFromGroupSummary(groupId, roomId)
+            .then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId));
     }
 
-    removeUserFromGroupSummary(userId) {
+    removeUserFromGroupSummary(groupId, userId) {
         return MatrixClientPeg.get()
-            .removeUserFromGroupSummary(this.groupId, userId)
-            .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary));
+            .removeUserFromGroupSummary(groupId, userId)
+            .then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId));
     }
 
-    setGroupPublicity(isPublished) {
+    setGroupPublicity(groupId, isPublished) {
         return MatrixClientPeg.get()
-            .setGroupPublicity(this.groupId, isPublished)
+            .setGroupPublicity(groupId, isPublished)
             .then(() => { FlairStore.invalidatePublicisedGroups(MatrixClientPeg.get().credentials.userId); })
-            .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary));
+            .then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId));
     }
 }
+
+let singletonGroupStore = null;
+if (!singletonGroupStore) {
+    singletonGroupStore = new GroupStore();
+}
+module.exports = singletonGroupStore;
diff --git a/src/stores/GroupStoreCache.js b/src/stores/GroupStoreCache.js
deleted file mode 100644
index 8b4286831b..0000000000
--- a/src/stores/GroupStoreCache.js
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
-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(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(groupId);
-        }
-        return this.groupStore;
-    }
-}
-
-if (global.singletonGroupStoreCache === undefined) {
-    global.singletonGroupStoreCache = new GroupStoreCache();
-}
-export default global.singletonGroupStoreCache;
diff --git a/src/utils/MultiInviter.js b/src/utils/MultiInviter.js
index a0f33f5c39..b3e7fc495a 100644
--- a/src/utils/MultiInviter.js
+++ b/src/utils/MultiInviter.js
@@ -18,7 +18,7 @@ limitations under the License.
 import MatrixClientPeg from '../MatrixClientPeg';
 import {getAddressType} from '../UserAddress';
 import {inviteToRoom} from '../RoomInvite';
-import GroupStoreCache from '../stores/GroupStoreCache';
+import GroupStore from '../stores/GroupStore';
 import Promise from 'bluebird';
 
 /**
@@ -118,9 +118,7 @@ export default class MultiInviter {
 
         let doInvite;
         if (this.groupId !== null) {
-            doInvite = GroupStoreCache
-                .getGroupStore(this.groupId)
-                .inviteUserToGroup(addr);
+            doInvite = GroupStore.inviteUserToGroup(this.groupId, addr);
         } else {
             doInvite = inviteToRoom(this.roomId, addr);
         }