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.
This commit is contained in:
Luke Barnard 2018-05-01 11:18:45 +01:00
parent fba8a7d7d6
commit 023daef4b7
13 changed files with 245 additions and 281 deletions

View file

@ -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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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, {

View file

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

View file

@ -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),

View file

@ -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;

View file

@ -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;

View file

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