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

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