+
+
+ { (provided, snapshot) => (
+
+ { tags }
+ { provided.placeholder }
+
+ ) }
+
+
diff --git a/src/components/views/elements/DNDTagTile.js b/src/components/views/elements/DNDTagTile.js
index 539163d0dc..e17ea52976 100644
--- a/src/components/views/elements/DNDTagTile.js
+++ b/src/components/views/elements/DNDTagTile.js
@@ -15,71 +15,29 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import { DragSource, DropTarget } from 'react-dnd';
-
import TagTile from './TagTile';
-import dis from '../../../dispatcher';
-import { findDOMNode } from 'react-dom';
-const tagTileSource = {
- canDrag: function(props, monitor) {
- return true;
- },
+import { Draggable } from 'react-beautiful-dnd';
- beginDrag: function(props) {
- // Return the data describing the dragged item
- return {
- tag: props.tag,
- };
- },
-
- endDrag: function(props, monitor, component) {
- const dropResult = monitor.getDropResult();
- if (!monitor.didDrop() || !dropResult) {
- return;
- }
- props.onEndDrag();
- },
-};
-
-const tagTileTarget = {
- canDrop(props, monitor) {
- return true;
- },
-
- hover(props, monitor, component) {
- if (!monitor.canDrop()) return;
- const draggedY = monitor.getClientOffset().y;
- const {top, bottom} = findDOMNode(component).getBoundingClientRect();
- const targetY = (top + bottom) / 2;
- dis.dispatch({
- action: 'order_tag',
- tag: monitor.getItem().tag,
- targetTag: props.tag,
- // Note: we indicate that the tag should be after the target when
- // it's being dragged over the top half of the target.
- after: draggedY < targetY,
- });
- },
-
- drop(props) {
- // Return the data to be returned by getDropResult
- return {
- tag: props.tag,
- };
- },
-};
-
-export default
- DropTarget('TagTile', tagTileTarget, (connect, monitor) => ({
- connectDropTarget: connect.dropTarget(),
- }))(DragSource('TagTile', tagTileSource, (connect, monitor) => ({
- connectDragSource: connect.dragSource(),
- }))((props) => {
- const { connectDropTarget, connectDragSource, ...otherProps } = props;
- return connectDropTarget(connectDragSource(
-
-
-
,
- ));
- }));
+export default function DNDTagTile(props) {
+ return
+
+ { (provided, snapshot) => (
+
+
+
+
+ { provided.placeholder }
+
+ ) }
+
+
;
+}
diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js
index d1e00bd302..d1ef6c2f2c 100644
--- a/src/components/views/rooms/RoomList.js
+++ b/src/components/views/rooms/RoomList.js
@@ -324,12 +324,7 @@ module.exports = React.createClass({
// Show all rooms
this._visibleRooms = MatrixClientPeg.get().getRooms();
}
-
- this.setState({
- selectedTags,
- }, () => {
- this.refreshRoomList();
- });
+ this._delayedRefreshRoomList();
},
refreshRoomList: function() {
@@ -345,6 +340,9 @@ module.exports = React.createClass({
this.setState({
lists: this.getRoomLists(),
totalRoomCount: totalRooms,
+ // Do this here so as to not render every time the selected tags
+ // themselves change.
+ selectedTags: TagOrderStore.getSelectedTags(),
});
// this._lastRefreshRoomListTs = Date.now();
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 0d686ad490..73e3f3a014 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -49,6 +49,7 @@
"AM": "AM",
"%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s",
"%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(time)s",
+ "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s",
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s",
"Who would you like to add to this community?": "Who would you like to add to this community?",
"Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID",
diff --git a/src/stores/GroupStore.js b/src/stores/GroupStore.js
index 9dce15fb53..c3465b684c 100644
--- a/src/stores/GroupStore.js
+++ b/src/stores/GroupStore.js
@@ -19,6 +19,14 @@ import { groupMemberFromApiObject, groupRoomFromApiObject } from '../groups';
import FlairStore from './FlairStore';
import MatrixClientPeg from '../MatrixClientPeg';
+function parseMembersResponse(response) {
+ return response.chunk.map((apiMember) => groupMemberFromApiObject(apiMember));
+}
+
+function parseRoomsResponse(response) {
+ return response.chunk.map((apiRoom) => groupRoomFromApiObject(apiRoom));
+}
+
/**
* 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.
@@ -38,65 +46,68 @@ export default class GroupStore extends EventEmitter {
throw new Error('GroupStore needs a valid groupId to be created');
}
this.groupId = groupId;
- this._summary = {};
- this._rooms = [];
- this._members = [];
- this._invitedMembers = [];
+ 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._fetchResourcePromise = {};
+ this._resourceFetcher = {
+ [GroupStore.STATE_KEY.Summary]: () => {
+ return MatrixClientPeg.get()
+ .getGroupSummary(this.groupId);
+ },
+ [GroupStore.STATE_KEY.GroupRooms]: () => {
+ return MatrixClientPeg.get()
+ .getGroupRooms(this.groupId)
+ .then(parseRoomsResponse);
+ },
+ [GroupStore.STATE_KEY.GroupMembers]: () => {
+ return MatrixClientPeg.get()
+ .getGroupUsers(this.groupId)
+ .then(parseMembersResponse);
+ },
+ [GroupStore.STATE_KEY.GroupInvitedMembers]: () => {
+ return MatrixClientPeg.get()
+ .getGroupInvitedUsers(this.groupId)
+ .then(parseMembersResponse);
+ },
+ };
+
this.on('error', (err) => {
console.error(`GroupStore for ${this.groupId} encountered error`, err);
});
}
- _fetchMembers() {
- MatrixClientPeg.get().getGroupUsers(this.groupId).then((result) => {
- this._members = result.chunk.map((apiMember) => {
- return groupMemberFromApiObject(apiMember);
- });
- this._ready[GroupStore.STATE_KEY.GroupMembers] = true;
- this._notifyListeners();
- }).catch((err) => {
- console.error("Failed to get group member list: " + err);
- this.emit('error', err);
- });
+ _fetchResource(stateKey) {
+ // Ongoing request, ignore
+ if (this._fetchResourcePromise[stateKey]) return;
- MatrixClientPeg.get().getGroupInvitedUsers(this.groupId).then((result) => {
- this._invitedMembers = result.chunk.map((apiMember) => {
- return groupMemberFromApiObject(apiMember);
- });
- this._ready[GroupStore.STATE_KEY.GroupInvitedMembers] = true;
+ const clientPromise = this._resourceFetcher[stateKey]();
+
+ // Indicate ongoing request
+ this._fetchResourcePromise[stateKey] = clientPromise;
+
+ clientPromise.then((result) => {
+ this._state[stateKey] = result;
+ this._ready[stateKey] = true;
this._notifyListeners();
}).catch((err) => {
// Invited users not visible to non-members
- if (err.httpStatus === 403) {
+ if (stateKey === GroupStore.STATE_KEY.GroupInvitedMembers && err.httpStatus === 403) {
return;
}
- console.error("Failed to get group invited member list: " + err);
- this.emit('error', err);
- });
- }
- _fetchSummary() {
- MatrixClientPeg.get().getGroupSummary(this.groupId).then((resp) => {
- this._summary = resp;
- this._ready[GroupStore.STATE_KEY.Summary] = true;
- this._notifyListeners();
- }).catch((err) => {
+ console.error("Failed to get resource " + stateKey + ":" + err);
this.emit('error', err);
+ }).finally(() => {
+ // Indicate finished request, allow for future fetches
+ delete this._fetchResourcePromise[stateKey];
});
- }
- _fetchRooms() {
- MatrixClientPeg.get().getGroupRooms(this.groupId).then((resp) => {
- this._rooms = resp.chunk.map((apiRoom) => {
- return groupRoomFromApiObject(apiRoom);
- });
- this._ready[GroupStore.STATE_KEY.GroupRooms] = true;
- this._notifyListeners();
- }).catch((err) => {
- this.emit('error', err);
- });
+ return clientPromise;
}
_notifyListeners() {
@@ -108,10 +119,9 @@ export default class GroupStore extends EventEmitter {
* immediately triggers an update to send the current state of the
* store (which could be the initial state).
*
- * XXX: This also causes a fetch of all group data, which effectively
- * causes 4 separate HTTP requests. This is bad, we should at least
- * deduplicate these in order to fix:
- * https://github.com/vector-im/riot-web/issues/5901
+ * This also causes a fetch of all group data, which might cause
+ * 4 separate HTTP requests, but only said requests aren't already
+ * ongoing.
*
* @param {function} fn the function to call when the store updates.
* @return {Object} tok a registration "token" with a single
@@ -123,9 +133,11 @@ export default class GroupStore extends EventEmitter {
this.on('update', fn);
// Call to set initial state (before fetching starts)
this.emit('update');
- this._fetchSummary();
- this._fetchRooms();
- this._fetchMembers();
+
+ this._fetchResource(GroupStore.STATE_KEY.Summary);
+ this._fetchResource(GroupStore.STATE_KEY.GroupRooms);
+ this._fetchResource(GroupStore.STATE_KEY.GroupMembers);
+ this._fetchResource(GroupStore.STATE_KEY.GroupInvitedMembers);
// Similar to the Store of flux/utils, we return a "token" that
// can be used to unregister the listener.
@@ -145,90 +157,94 @@ export default class GroupStore extends EventEmitter {
}
getSummary() {
- return this._summary;
+ return this._state[GroupStore.STATE_KEY.Summary];
}
getGroupRooms() {
- return this._rooms;
+ return this._state[GroupStore.STATE_KEY.GroupRooms];
}
- getGroupMembers( ) {
- return this._members;
+ getGroupMembers() {
+ return this._state[GroupStore.STATE_KEY.GroupMembers];
}
- getGroupInvitedMembers( ) {
- return this._invitedMembers;
+ getGroupInvitedMembers() {
+ return this._state[GroupStore.STATE_KEY.GroupInvitedMembers];
}
getGroupPublicity() {
- return this._summary.user ? this._summary.user.is_publicised : null;
+ return this._state[GroupStore.STATE_KEY.Summary].user ?
+ this._state[GroupStore.STATE_KEY.Summary].user.is_publicised : null;
}
isUserPrivileged() {
- return this._summary.user ? this._summary.user.is_privileged : null;
+ return this._state[GroupStore.STATE_KEY.Summary].user ?
+ this._state[GroupStore.STATE_KEY.Summary].user.is_privileged : null;
}
addRoomToGroup(roomId, isPublic) {
return MatrixClientPeg.get()
.addRoomToGroup(this.groupId, roomId, isPublic)
- .then(this._fetchRooms.bind(this));
+ .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupRooms));
}
updateGroupRoomVisibility(roomId, isPublic) {
return MatrixClientPeg.get()
.updateGroupRoomVisibility(this.groupId, roomId, isPublic)
- .then(this._fetchRooms.bind(this));
+ .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupRooms));
}
removeRoomFromGroup(roomId) {
return MatrixClientPeg.get()
.removeRoomFromGroup(this.groupId, roomId)
// Room might be in the summary, refresh just in case
- .then(this._fetchSummary.bind(this))
- .then(this._fetchRooms.bind(this));
+ .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary))
+ .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupRooms));
}
inviteUserToGroup(userId) {
return MatrixClientPeg.get().inviteUserToGroup(this.groupId, userId)
- .then(this._fetchMembers.bind(this));
+ .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupInvitedMembers));
}
acceptGroupInvite() {
return MatrixClientPeg.get().acceptGroupInvite(this.groupId)
// The user might be able to see more rooms now
- .then(this._fetchRooms.bind(this))
+ .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupRooms))
// The user should now appear as a member
- .then(this._fetchMembers.bind(this));
+ .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupMembers))
+ // The user should now not appear as an invited member
+ .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupInvitedMembers));
}
addRoomToGroupSummary(roomId, categoryId) {
return MatrixClientPeg.get()
.addRoomToGroupSummary(this.groupId, roomId, categoryId)
- .then(this._fetchSummary.bind(this));
+ .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary));
}
addUserToGroupSummary(userId, roleId) {
return MatrixClientPeg.get()
.addUserToGroupSummary(this.groupId, userId, roleId)
- .then(this._fetchSummary.bind(this));
+ .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary));
}
removeRoomFromGroupSummary(roomId) {
return MatrixClientPeg.get()
.removeRoomFromGroupSummary(this.groupId, roomId)
- .then(this._fetchSummary.bind(this));
+ .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary));
}
removeUserFromGroupSummary(userId) {
return MatrixClientPeg.get()
.removeUserFromGroupSummary(this.groupId, userId)
- .then(this._fetchSummary.bind(this));
+ .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary));
}
setGroupPublicity(isPublished) {
return MatrixClientPeg.get()
.setGroupPublicity(this.groupId, isPublished)
.then(() => { FlairStore.invalidatePublicisedGroups(MatrixClientPeg.get().credentials.userId); })
- .then(this._fetchSummary.bind(this));
+ .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary));
}
}
diff --git a/src/stores/TagOrderStore.js b/src/stores/TagOrderStore.js
index 9c9427284e..effd8287c7 100644
--- a/src/stores/TagOrderStore.js
+++ b/src/stores/TagOrderStore.js
@@ -63,6 +63,11 @@ class TagOrderStore extends Store {
// Get ordering from account data
case 'MatrixActions.accountData': {
if (payload.event_type !== 'im.vector.web.tag_ordering') break;
+
+ // Ignore remote echos caused by this store so as to avoid setting
+ // state back to old state.
+ if (payload.event_content._storeId === this.getStoreId()) break;
+
this._setState({
orderedTagsAccountData: payload.event_content ? payload.event_content.tags : null,
});
@@ -78,24 +83,11 @@ class TagOrderStore extends Store {
this._updateOrderedTags();
break;
}
- // Puts payload.tag at payload.targetTag, placing the targetTag before or after the tag
- case 'order_tag': {
- if (!this._state.orderedTags ||
- !payload.tag ||
- !payload.targetTag ||
- payload.tag === payload.targetTag
- ) return;
-
- const tags = this._state.orderedTags;
-
- let orderedTags = tags.filter((t) => t !== payload.tag);
- const newIndex = orderedTags.indexOf(payload.targetTag) + (payload.after ? 1 : 0);
- orderedTags = [
- ...orderedTags.slice(0, newIndex),
- payload.tag,
- ...orderedTags.slice(newIndex),
- ];
- this._setState({orderedTags});
+ case 'TagOrderActions.moveTag.pending': {
+ // Optimistic update of a moved tag
+ this._setState({
+ orderedTags: payload.request.tags,
+ });
break;
}
case 'select_tag': {
@@ -189,6 +181,13 @@ class TagOrderStore extends Store {
return this._state.orderedTags;
}
+ getStoreId() {
+ // Generate a random ID to prevent this store from clobbering its
+ // state with redundant remote echos.
+ if (!this._id) this._id = Math.random().toString(16).slice(2, 10);
+ return this._id;
+ }
+
getSelectedTags() {
return this._state.selectedTags;
}