Fix widgets re-appearing after being deleted

Widgets would sometimes briefly re-appear after having been deleted.
This was because of the following race:
 * User presses delete, send POST req, we set `deleting`. Widget hides.
 * POST request completes, we unset `deleting` so widget unhides.
 * State event comes down sync so widget hides again.

This fixes this by introducing `waitForRoomWidget` and using it to
wait until the state event comes down the sync until clearing the
`deleting` flag.

Since we now have `waitForRoomWidget`, this also uses it when adding
a widget so the 'widget saved' appears at the same time the widget
does.
This commit is contained in:
David Baker 2018-06-13 15:50:19 +01:00
parent 1cb794753e
commit 94125fb566
3 changed files with 64 additions and 9 deletions

View file

@ -237,6 +237,7 @@ import MatrixClientPeg from './MatrixClientPeg';
import { MatrixEvent } from 'matrix-js-sdk';
import dis from './dispatcher';
import Widgets from './utils/widgets';
import WidgetUtils from './WidgetUtils';
import RoomViewStore from './stores/RoomViewStore';
import { _t } from './languageHandler';
@ -362,7 +363,7 @@ function setWidget(event, roomId) {
// wait for this, the action will complete but if the user is fast enough,
// the widget still won't actually be there.
client.setAccountData('m.widgets', userWidgets).then(() => {
return waitForUserWidget(widgetId, widgetUrl !== null);
return WidgetUtils.waitForUserWidget(widgetId, widgetUrl !== null);
}).then(() => {
sendResponse(event, {
success: true,
@ -382,9 +383,9 @@ function setWidget(event, roomId) {
}
// TODO - Room widgets need to be moved to 'm.widget' state events
// https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit?usp=sharing
client.sendStateEvent(roomId, "im.vector.modular.widgets", content, widgetId).done(() => {
// XXX: We should probably wait for the echo of the state event to come back from the server,
// as we do with user widgets.
client.sendStateEvent(roomId, "im.vector.modular.widgets", content, widgetId).then(() => {
return WidgetUtils.waitForRoomWidget(widgetId, roomId, widgetUrl !== null);
}).then(() => {
sendResponse(event, {
success: true,
});

View file

@ -104,12 +104,10 @@ export default class WidgetUtils {
*/
static waitForUserWidget(widgetId, add) {
return new Promise((resolve, reject) => {
const currentAccountDataEvent = MatrixClientPeg.get().getAccountData('m.widgets');
// Tests an account data event, returning true if it's in the state
// we're waiting for it to be in
function eventInIntendedState(ev) {
if (!ev || !currentAccountDataEvent.getContent()) return false;
if (!ev || !ev.getContent()) return false;
if (add) {
return ev.getContent()[widgetId] !== undefined;
} else {
@ -117,12 +115,14 @@ export default class WidgetUtils {
}
}
if (eventInIntendedState(currentAccountDataEvent)) {
const startingAccountDataEvent = MatrixClientPeg.get().getAccountData('m.widgets');
if (eventInIntendedState(startingAccountDataEvent)) {
resolve();
return;
}
function onAccountData(ev) {
const currentAccountDataEvent = MatrixClientPeg.get().getAccountData('m.widgets');
if (eventInIntendedState(currentAccountDataEvent)) {
MatrixClientPeg.get().removeListener('accountData', onAccountData);
clearTimeout(timerId);
@ -136,4 +136,56 @@ export default class WidgetUtils {
MatrixClientPeg.get().on('accountData', onAccountData);
});
}
/**
* Returns a promise that resolves when a widget with the given
* ID has been added as a room widget in the given room (ie. the
* room state event arrives) or rejects after a timeout
*
* @param {string} widgetId The ID of the widget to wait for
* @param {string} roomId The ID of the room to wait for the widget in
* @param {boolean} add True to wait for the widget to be added,
* false to wait for it to be deleted.
* @returns {Promise} that resolves when the widget is available
*/
static waitForRoomWidget(widgetId, roomId, add) {
return new Promise((resolve, reject) => {
// Tests a list of state events, returning true if it's in the state
// we're waiting for it to be in
function eventsInIntendedState(evList) {
const widgetPresent = evList.some((ev) => {
return ev.getContent() && ev.getContent()['id'] === widgetId;
});
if (add) {
return widgetPresent;
} else {
return !widgetPresent;
}
}
const room = MatrixClientPeg.get().getRoom(roomId);
const startingWidgetEvents = room.currentState.getStateEvents('im.vector.modular.widgets');
if (eventsInIntendedState(startingWidgetEvents)) {
resolve();
return;
}
function onRoomStateEvents(ev) {
if (ev.getRoomId() !== roomId) return;
const currentWidgetEvents = room.currentState.getStateEvents('im.vector.modular.widgets');
if (eventsInIntendedState(currentWidgetEvents)) {
MatrixClientPeg.get().removeListener('RoomState.events', onRoomStateEvents);
clearTimeout(timerId);
resolve();
}
}
const timerId = setTimeout(() => {
MatrixClientPeg.get().removeListener('RoomState.events', onRoomStateEvents);
reject(new Error("Timed out waiting for widget ID " + widgetId + " to appear"));
}, 10000);
MatrixClientPeg.get().on('RoomState.events', onRoomStateEvents);
});
}
}

View file

@ -324,7 +324,9 @@ export default class AppTile extends React.Component {
'im.vector.modular.widgets',
{}, // empty content
this.props.id,
).catch((e) => {
).then(() => {
return WidgetUtils.waitForRoomWidget(this.props.id, this.props.room.roomId, false);
}).catch((e) => {
console.error('Failed to delete widget', e);
}).finally(() => {
this.setState({deleting: false});