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); }