Merge branches 'develop' and 't3chguy/roomColor' of github.com:matrix-org/matrix-react-sdk into t3chguy/roomColor

This commit is contained in:
Michael Telatynski 2018-01-17 18:20:35 +00:00
commit 2f42e69b57
12 changed files with 219 additions and 182 deletions

View file

@ -3,7 +3,10 @@ dist: trusty
# we don't need sudo, so can run in a container, which makes startup much # we don't need sudo, so can run in a container, which makes startup much
# quicker. # quicker.
sudo: false #
# unfortunately we do temporarily require sudo as a workaround for
# https://github.com/travis-ci/travis-ci/issues/8836
sudo: required
language: node_js language: node_js
node_js: node_js:

View file

@ -77,6 +77,7 @@
"querystring": "^0.2.0", "querystring": "^0.2.0",
"react": "^15.4.0", "react": "^15.4.0",
"react-addons-css-transition-group": "15.3.2", "react-addons-css-transition-group": "15.3.2",
"react-beautiful-dnd": "^4.0.0",
"react-dnd": "^2.1.4", "react-dnd": "^2.1.4",
"react-dnd-html5-backend": "^2.1.2", "react-dnd-html5-backend": "^2.1.2",
"react-dom": "^15.4.0", "react-dom": "^15.4.0",

View file

@ -82,6 +82,17 @@ export function formatDate(date, showTwelveHour=false) {
return formatFullDate(date, showTwelveHour); return formatFullDate(date, showTwelveHour);
} }
export function formatFullDateNoTime(date) {
const days = getDaysArray();
const months = getMonthsArray();
return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s', {
weekDayName: days[date.getDay()],
monthName: months[date.getMonth()],
day: date.getDate(),
fullYear: date.getFullYear(),
});
}
export function formatFullDate(date, showTwelveHour=false) { export function formatFullDate(date, showTwelveHour=false) {
const days = getDaysArray(); const days = getDaysArray();
const months = getMonthsArray(); const months = getMonthsArray();

View file

@ -22,25 +22,37 @@ const TagOrderActions = {};
/** /**
* Creates an action thunk that will do an asynchronous request to * Creates an action thunk that will do an asynchronous request to
* commit TagOrderStore.getOrderedTags() to account data and dispatch * move a tag in TagOrderStore to destinationIx.
* actions to indicate the status of the request.
* *
* @param {MatrixClient} matrixClient the matrix client to set the * @param {MatrixClient} matrixClient the matrix client to set the
* account data on. * account data on.
* @param {string} tag the tag to move.
* @param {number} destinationIx the new position of the tag.
* @returns {function} an action thunk that will dispatch actions * @returns {function} an action thunk that will dispatch actions
* indicating the status of the request. * indicating the status of the request.
* @see asyncAction * @see asyncAction
*/ */
TagOrderActions.commitTagOrdering = function(matrixClient) { TagOrderActions.moveTag = function(matrixClient, tag, destinationIx) {
return asyncAction('TagOrderActions.commitTagOrdering', () => { // Only commit tags if the state is ready, i.e. not null
// Only commit tags if the state is ready, i.e. not null let tags = TagOrderStore.getOrderedTags();
const tags = TagOrderStore.getOrderedTags(); if (!tags) {
if (!tags) { return;
return; }
}
tags = tags.filter((t) => t !== tag);
tags = [...tags.slice(0, destinationIx), tag, ...tags.slice(destinationIx)];
const storeId = TagOrderStore.getStoreId();
return asyncAction('TagOrderActions.moveTag', () => {
Analytics.trackEvent('TagOrderActions', 'commitTagOrdering'); Analytics.trackEvent('TagOrderActions', 'commitTagOrdering');
return matrixClient.setAccountData('im.vector.web.tag_ordering', {tags}); return matrixClient.setAccountData(
'im.vector.web.tag_ordering',
{tags, _storeId: storeId},
);
}, () => {
// For an optimistic update
return {tags};
}); });
}; };

View file

@ -22,6 +22,9 @@ limitations under the License.
* suffix determining whether it is pending, successful or * suffix determining whether it is pending, successful or
* a failure. * a failure.
* @param {function} fn a function that returns a Promise. * @param {function} fn a function that returns a Promise.
* @param {function?} pendingFn a function that returns an object to assign
* to the `request` key of the ${id}.pending
* payload.
* @returns {function} an action thunk - a function that uses its single * @returns {function} an action thunk - a function that uses its single
* argument as a dispatch function to dispatch the * argument as a dispatch function to dispatch the
* following actions: * following actions:
@ -29,9 +32,13 @@ limitations under the License.
* `${id}.success` or * `${id}.success` or
* `${id}.failure`. * `${id}.failure`.
*/ */
export function asyncAction(id, fn) { export function asyncAction(id, fn, pendingFn) {
return (dispatch) => { return (dispatch) => {
dispatch({action: id + '.pending'}); dispatch({
action: id + '.pending',
request:
typeof pendingFn === 'function' ? pendingFn() : undefined,
});
fn().then((result) => { fn().then((result) => {
dispatch({action: id + '.success', result}); dispatch({action: id + '.success', result});
}).catch((err) => { }).catch((err) => {

View file

@ -325,7 +325,7 @@ module.exports = React.createClass({
const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial"); const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial");
if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) { if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) {
const dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1} showTwelveHour={this.props.isTwelveHour} /></li>; const dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1} /></li>;
ret.push(dateSeparator); ret.push(dateSeparator);
} }
@ -479,7 +479,7 @@ module.exports = React.createClass({
// do we need a date separator since the last event? // do we need a date separator since the last event?
if (this._wantsDateSeparator(prevEvent, eventDate)) { if (this._wantsDateSeparator(prevEvent, eventDate)) {
const dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} showTwelveHour={this.props.isTwelveHour} /></li>; const dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} /></li>;
ret.push(dateSeparator); ret.push(dateSeparator);
continuation = false; continuation = false;
} }

View file

@ -25,6 +25,8 @@ import TagOrderActions from '../../actions/TagOrderActions';
import sdk from '../../index'; import sdk from '../../index';
import dis from '../../dispatcher'; import dis from '../../dispatcher';
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
const TagPanel = React.createClass({ const TagPanel = React.createClass({
displayName: 'TagPanel', displayName: 'TagPanel',
@ -69,7 +71,9 @@ const TagPanel = React.createClass({
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient)); dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
}, },
onClick() { onClick(e) {
// Ignore clicks on children
if (e.target !== e.currentTarget) return;
dis.dispatch({action: 'deselect_tags'}); dis.dispatch({action: 'deselect_tags'});
}, },
@ -78,8 +82,20 @@ const TagPanel = React.createClass({
dis.dispatch({action: 'view_create_group'}); dis.dispatch({action: 'view_create_group'});
}, },
onTagTileEndDrag() { onTagTileEndDrag(result) {
dis.dispatch(TagOrderActions.commitTagOrdering(this.context.matrixClient)); // Dragged to an invalid destination, not onto a droppable
if (!result.destination) {
return;
}
// Dispatch synchronously so that the TagPanel receives an
// optimistic update from TagOrderStore before the previous
// state is shown.
dis.dispatch(TagOrderActions.moveTag(
this.context.matrixClient,
result.draggableId,
result.destination.index,
), true);
}, },
render() { render() {
@ -89,16 +105,31 @@ const TagPanel = React.createClass({
const tags = this.state.orderedTags.map((tag, index) => { const tags = this.state.orderedTags.map((tag, index) => {
return <DNDTagTile return <DNDTagTile
key={tag + '_' + index} key={tag}
tag={tag} tag={tag}
index={index}
selected={this.state.selectedTags.includes(tag)} selected={this.state.selectedTags.includes(tag)}
onEndDrag={this.onTagTileEndDrag}
/>; />;
}); });
return <div className="mx_TagPanel" onClick={this.onClick}> return <div className="mx_TagPanel">
<div className="mx_TagPanel_tagTileContainer"> <DragDropContext onDragEnd={this.onTagTileEndDrag}>
{ tags } <Droppable droppableId="tag-panel-droppable">
</div> { (provided, snapshot) => (
<div
className="mx_TagPanel_tagTileContainer"
ref={provided.innerRef}
// react-beautiful-dnd has a bug that emits a click to the parent
// of draggables upon dropping
// https://github.com/atlassian/react-beautiful-dnd/issues/273
// so we use onMouseDown here as a workaround.
onMouseDown={this.onClick}
>
{ tags }
{ provided.placeholder }
</div>
) }
</Droppable>
</DragDropContext>
<AccessibleButton className="mx_TagPanel_createGroupButton" onClick={this.onCreateGroupClick}> <AccessibleButton className="mx_TagPanel_createGroupButton" onClick={this.onCreateGroupClick}>
<TintableSvg src="img/icons-create-room.svg" width="25" height="25" /> <TintableSvg src="img/icons-create-room.svg" width="25" height="25" />
</AccessibleButton> </AccessibleButton>

View file

@ -15,71 +15,29 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { DragSource, DropTarget } from 'react-dnd';
import TagTile from './TagTile'; import TagTile from './TagTile';
import dis from '../../../dispatcher';
import { findDOMNode } from 'react-dom';
const tagTileSource = { import { Draggable } from 'react-beautiful-dnd';
canDrag: function(props, monitor) {
return true;
},
beginDrag: function(props) { export default function DNDTagTile(props) {
// Return the data describing the dragged item return <div>
return { <Draggable
tag: props.tag, key={props.tag}
}; draggableId={props.tag}
}, index={props.index}
>
endDrag: function(props, monitor, component) { { (provided, snapshot) => (
const dropResult = monitor.getDropResult(); <div>
if (!monitor.didDrop() || !dropResult) { <div
return; ref={provided.innerRef}
} {...provided.draggableProps}
props.onEndDrag(); {...provided.dragHandleProps}
}, >
}; <TagTile {...props} />
</div>
const tagTileTarget = { { provided.placeholder }
canDrop(props, monitor) { </div>
return true; ) }
}, </Draggable>
</div>;
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(
<div>
<TagTile {...otherProps} />
</div>,
));
}));

View file

@ -324,12 +324,7 @@ module.exports = React.createClass({
// Show all rooms // Show all rooms
this._visibleRooms = MatrixClientPeg.get().getRooms(); this._visibleRooms = MatrixClientPeg.get().getRooms();
} }
this._delayedRefreshRoomList();
this.setState({
selectedTags,
}, () => {
this.refreshRoomList();
});
}, },
refreshRoomList: function() { refreshRoomList: function() {
@ -345,6 +340,9 @@ module.exports = React.createClass({
this.setState({ this.setState({
lists: this.getRoomLists(), lists: this.getRoomLists(),
totalRoomCount: totalRooms, totalRoomCount: totalRooms,
// Do this here so as to not render every time the selected tags
// themselves change.
selectedTags: TagOrderStore.getSelectedTags(),
}); });
// this._lastRefreshRoomListTs = Date.now(); // this._lastRefreshRoomListTs = Date.now();

View file

@ -49,6 +49,7 @@
"AM": "AM", "AM": "AM",
"%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s", "%(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 %(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", "%(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?", "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", "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",

View file

@ -19,6 +19,14 @@ import { groupMemberFromApiObject, groupRoomFromApiObject } from '../groups';
import FlairStore from './FlairStore'; import FlairStore from './FlairStore';
import MatrixClientPeg from '../MatrixClientPeg'; 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 * 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. * 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'); throw new Error('GroupStore needs a valid groupId to be created');
} }
this.groupId = groupId; this.groupId = groupId;
this._summary = {}; this._state = {};
this._rooms = []; this._state[GroupStore.STATE_KEY.Summary] = {};
this._members = []; this._state[GroupStore.STATE_KEY.GroupRooms] = [];
this._invitedMembers = []; this._state[GroupStore.STATE_KEY.GroupMembers] = [];
this._state[GroupStore.STATE_KEY.GroupInvitedMembers] = [];
this._ready = {}; 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) => { this.on('error', (err) => {
console.error(`GroupStore for ${this.groupId} encountered error`, err); console.error(`GroupStore for ${this.groupId} encountered error`, err);
}); });
} }
_fetchMembers() { _fetchResource(stateKey) {
MatrixClientPeg.get().getGroupUsers(this.groupId).then((result) => { // Ongoing request, ignore
this._members = result.chunk.map((apiMember) => { if (this._fetchResourcePromise[stateKey]) return;
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);
});
MatrixClientPeg.get().getGroupInvitedUsers(this.groupId).then((result) => { const clientPromise = this._resourceFetcher[stateKey]();
this._invitedMembers = result.chunk.map((apiMember) => {
return groupMemberFromApiObject(apiMember); // Indicate ongoing request
}); this._fetchResourcePromise[stateKey] = clientPromise;
this._ready[GroupStore.STATE_KEY.GroupInvitedMembers] = true;
clientPromise.then((result) => {
this._state[stateKey] = result;
this._ready[stateKey] = true;
this._notifyListeners(); this._notifyListeners();
}).catch((err) => { }).catch((err) => {
// Invited users not visible to non-members // Invited users not visible to non-members
if (err.httpStatus === 403) { if (stateKey === GroupStore.STATE_KEY.GroupInvitedMembers && err.httpStatus === 403) {
return; return;
} }
console.error("Failed to get group invited member list: " + err);
this.emit('error', err);
});
}
_fetchSummary() { console.error("Failed to get resource " + stateKey + ":" + err);
MatrixClientPeg.get().getGroupSummary(this.groupId).then((resp) => {
this._summary = resp;
this._ready[GroupStore.STATE_KEY.Summary] = true;
this._notifyListeners();
}).catch((err) => {
this.emit('error', err); this.emit('error', err);
}).finally(() => {
// Indicate finished request, allow for future fetches
delete this._fetchResourcePromise[stateKey];
}); });
}
_fetchRooms() { return clientPromise;
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);
});
} }
_notifyListeners() { _notifyListeners() {
@ -108,10 +119,9 @@ export default class GroupStore extends EventEmitter {
* immediately triggers an update to send the current state of the * immediately triggers an update to send the current state of the
* store (which could be the initial state). * store (which could be the initial state).
* *
* XXX: This also causes a fetch of all group data, which effectively * This also causes a fetch of all group data, which might cause
* causes 4 separate HTTP requests. This is bad, we should at least * 4 separate HTTP requests, but only said requests aren't already
* deduplicate these in order to fix: * ongoing.
* https://github.com/vector-im/riot-web/issues/5901
* *
* @param {function} fn the function to call when the store updates. * @param {function} fn the function to call when the store updates.
* @return {Object} tok a registration "token" with a single * @return {Object} tok a registration "token" with a single
@ -123,9 +133,11 @@ export default class GroupStore extends EventEmitter {
this.on('update', fn); this.on('update', fn);
// Call to set initial state (before fetching starts) // Call to set initial state (before fetching starts)
this.emit('update'); this.emit('update');
this._fetchSummary();
this._fetchRooms(); this._fetchResource(GroupStore.STATE_KEY.Summary);
this._fetchMembers(); 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 // Similar to the Store of flux/utils, we return a "token" that
// can be used to unregister the listener. // can be used to unregister the listener.
@ -145,90 +157,94 @@ export default class GroupStore extends EventEmitter {
} }
getSummary() { getSummary() {
return this._summary; return this._state[GroupStore.STATE_KEY.Summary];
} }
getGroupRooms() { getGroupRooms() {
return this._rooms; return this._state[GroupStore.STATE_KEY.GroupRooms];
} }
getGroupMembers( ) { getGroupMembers() {
return this._members; return this._state[GroupStore.STATE_KEY.GroupMembers];
} }
getGroupInvitedMembers( ) { getGroupInvitedMembers() {
return this._invitedMembers; return this._state[GroupStore.STATE_KEY.GroupInvitedMembers];
} }
getGroupPublicity() { 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() { 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) { addRoomToGroup(roomId, isPublic) {
return MatrixClientPeg.get() return MatrixClientPeg.get()
.addRoomToGroup(this.groupId, roomId, isPublic) .addRoomToGroup(this.groupId, roomId, isPublic)
.then(this._fetchRooms.bind(this)); .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupRooms));
} }
updateGroupRoomVisibility(roomId, isPublic) { updateGroupRoomVisibility(roomId, isPublic) {
return MatrixClientPeg.get() return MatrixClientPeg.get()
.updateGroupRoomVisibility(this.groupId, roomId, isPublic) .updateGroupRoomVisibility(this.groupId, roomId, isPublic)
.then(this._fetchRooms.bind(this)); .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupRooms));
} }
removeRoomFromGroup(roomId) { removeRoomFromGroup(roomId) {
return MatrixClientPeg.get() return MatrixClientPeg.get()
.removeRoomFromGroup(this.groupId, roomId) .removeRoomFromGroup(this.groupId, roomId)
// Room might be in the summary, refresh just in case // Room might be in the summary, refresh just in case
.then(this._fetchSummary.bind(this)) .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary))
.then(this._fetchRooms.bind(this)); .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupRooms));
} }
inviteUserToGroup(userId) { inviteUserToGroup(userId) {
return MatrixClientPeg.get().inviteUserToGroup(this.groupId, userId) return MatrixClientPeg.get().inviteUserToGroup(this.groupId, userId)
.then(this._fetchMembers.bind(this)); .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.GroupInvitedMembers));
} }
acceptGroupInvite() { acceptGroupInvite() {
return MatrixClientPeg.get().acceptGroupInvite(this.groupId) return MatrixClientPeg.get().acceptGroupInvite(this.groupId)
// The user might be able to see more rooms now // 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 // 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) { addRoomToGroupSummary(roomId, categoryId) {
return MatrixClientPeg.get() return MatrixClientPeg.get()
.addRoomToGroupSummary(this.groupId, roomId, categoryId) .addRoomToGroupSummary(this.groupId, roomId, categoryId)
.then(this._fetchSummary.bind(this)); .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary));
} }
addUserToGroupSummary(userId, roleId) { addUserToGroupSummary(userId, roleId) {
return MatrixClientPeg.get() return MatrixClientPeg.get()
.addUserToGroupSummary(this.groupId, userId, roleId) .addUserToGroupSummary(this.groupId, userId, roleId)
.then(this._fetchSummary.bind(this)); .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary));
} }
removeRoomFromGroupSummary(roomId) { removeRoomFromGroupSummary(roomId) {
return MatrixClientPeg.get() return MatrixClientPeg.get()
.removeRoomFromGroupSummary(this.groupId, roomId) .removeRoomFromGroupSummary(this.groupId, roomId)
.then(this._fetchSummary.bind(this)); .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary));
} }
removeUserFromGroupSummary(userId) { removeUserFromGroupSummary(userId) {
return MatrixClientPeg.get() return MatrixClientPeg.get()
.removeUserFromGroupSummary(this.groupId, userId) .removeUserFromGroupSummary(this.groupId, userId)
.then(this._fetchSummary.bind(this)); .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary));
} }
setGroupPublicity(isPublished) { setGroupPublicity(isPublished) {
return MatrixClientPeg.get() return MatrixClientPeg.get()
.setGroupPublicity(this.groupId, isPublished) .setGroupPublicity(this.groupId, isPublished)
.then(() => { FlairStore.invalidatePublicisedGroups(MatrixClientPeg.get().credentials.userId); }) .then(() => { FlairStore.invalidatePublicisedGroups(MatrixClientPeg.get().credentials.userId); })
.then(this._fetchSummary.bind(this)); .then(this._fetchResource.bind(this, GroupStore.STATE_KEY.Summary));
} }
} }

View file

@ -63,6 +63,11 @@ class TagOrderStore extends Store {
// Get ordering from account data // Get ordering from account data
case 'MatrixActions.accountData': { case 'MatrixActions.accountData': {
if (payload.event_type !== 'im.vector.web.tag_ordering') break; 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({ this._setState({
orderedTagsAccountData: payload.event_content ? payload.event_content.tags : null, orderedTagsAccountData: payload.event_content ? payload.event_content.tags : null,
}); });
@ -78,24 +83,11 @@ class TagOrderStore extends Store {
this._updateOrderedTags(); this._updateOrderedTags();
break; break;
} }
// Puts payload.tag at payload.targetTag, placing the targetTag before or after the tag case 'TagOrderActions.moveTag.pending': {
case 'order_tag': { // Optimistic update of a moved tag
if (!this._state.orderedTags || this._setState({
!payload.tag || orderedTags: payload.request.tags,
!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});
break; break;
} }
case 'select_tag': { case 'select_tag': {
@ -189,6 +181,13 @@ class TagOrderStore extends Store {
return this._state.orderedTags; 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() { getSelectedTags() {
return this._state.selectedTags; return this._state.selectedTags;
} }