diff --git a/lib/client.js b/lib/client.js index be453083e..d2dfe9435 100644 --- a/lib/client.js +++ b/lib/client.js @@ -725,6 +725,28 @@ MatrixClient.prototype.resendEvent = function(event, room) { return _sendEvent(this, room, event); }; +/** + * Cancel a queued or unsent event. + * + * @param {MatrixEvent} event Event to cancel + * @throws Error if the event is not in QUEUED or NOT_SENT state + */ +MatrixClient.prototype.cancelPendingEvent = function(event) { + if ([EventStatus.QUEUED, EventStatus.NOT_SENT].indexOf(event.status) < 0) { + throw new Error("cannot cancel an event with status " + event.status); + } + + // first tell the scheduler to forget about it, if it's queued + if (this.scheduler) { + this.scheduler.removeEventFromQueue(event); + } + + // then tell the room about the change of state, which will remove it + // from the room's list of pending events. + var room = this.getRoom(event.getRoomId()); + _updatePendingEventStatus(room, event, EventStatus.CANCELLED); +}; + /** * @param {string} roomId * @param {string} name diff --git a/lib/models/event.js b/lib/models/event.js index de5287dcd..d39a8daf2 100644 --- a/lib/models/event.js +++ b/lib/models/event.js @@ -36,6 +36,9 @@ module.exports.EventStatus = { /** The event has been sent to the server, but we have not yet received the * echo. */ SENT: "sent", + + /** The event was cancelled before it was successfully sent. */ + CANCELLED: "cancelled", }; /** diff --git a/lib/models/room.js b/lib/models/room.js index dc4a9ed74..0f5881ba8 100644 --- a/lib/models/room.js +++ b/lib/models/room.js @@ -729,13 +729,16 @@ ALLOWED_TRANSITIONS[EventStatus.SENDING] = [EventStatus.QUEUED, EventStatus.NOT_SENT, EventStatus.SENT]; ALLOWED_TRANSITIONS[EventStatus.QUEUED] = - [EventStatus.SENDING]; + [EventStatus.SENDING, EventStatus.CANCELLED]; ALLOWED_TRANSITIONS[EventStatus.SENT] = []; ALLOWED_TRANSITIONS[EventStatus.NOT_SENT] = - [EventStatus.SENDING, EventStatus.QUEUED]; + [EventStatus.SENDING, EventStatus.QUEUED, EventStatus.CANCELLED]; + +ALLOWED_TRANSITIONS[EventStatus.CANCELLED] = + []; /** * Update the status / event id on a pending event, to reflect its transmission @@ -797,6 +800,17 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) { this._eventIdToTimeline[newEventId] = existingTimeline; } } + else if (newStatus == EventStatus.CANCELLED) { + // remove it from the pending event list, or the timeline. + if (this._pendingEventList) { + utils.removeElement( + this._pendingEventList, + function(ev) { return ev.getId() == oldEventId; }, + false + ); + } + this.removeEvent(oldEventId); + } this.emit("Room.localEchoUpdated", event, this, event.getId(), oldStatus); }; @@ -1479,9 +1493,10 @@ module.exports = Room; * arrived, the event is updated with a new event id and the status is set to * 'SENT'. The server-generated fields are of course not updated yet. * - *
Finally, the /send might fail. In this case, the event's status is set to + *
If the /send fails, In this case, the event's status is set to * 'NOT_SENT'. If it is later resent, the process starts again, setting the - * status to 'SENDING'. + * status to 'SENDING'. Alternatively, the message may be cancelled, which + * removes the event from the room, and sets the status to 'CANCELLED'. * *
This event is raised to reflect each of the transitions above. * diff --git a/spec/integ/matrix-client-retrying.spec.js b/spec/integ/matrix-client-retrying.spec.js index 37544c1e3..4eabbdcfb 100644 --- a/spec/integ/matrix-client-retrying.spec.js +++ b/spec/integ/matrix-client-retrying.spec.js @@ -2,22 +2,30 @@ var sdk = require("../.."); var HttpBackend = require("../mock-request"); var utils = require("../test-utils"); +var EventStatus = sdk.EventStatus; describe("MatrixClient retrying", function() { var baseUrl = "http://localhost.or.something"; var client, httpBackend; + var scheduler; var userId = "@alice:localhost"; var accessToken = "aseukfgwef"; + var roomId = "!room:here"; + var room; beforeEach(function() { utils.beforeEach(this); httpBackend = new HttpBackend(); sdk.request(httpBackend.requestFn); + scheduler = new sdk.MatrixScheduler(); client = sdk.createClient({ baseUrl: baseUrl, userId: userId, - accessToken: accessToken + accessToken: accessToken, + scheduler: scheduler, }); + room = new sdk.Room(roomId); + client.store.storeRoom(room); }); afterEach(function() { @@ -40,6 +48,49 @@ describe("MatrixClient retrying", function() { }); + it("should mark events as EventStatus.CANCELLED when cancelled", function(done) { + + // send a couple of events; the second will be queued + var ev1, ev2; + client.sendMessage(roomId, "m1").then(function(ev) { + expect(ev).toEqual(ev1); + }); + client.sendMessage(roomId, "m2").then(function(ev) { + expect(ev).toEqual(ev2); + }); + + // both events should be in the timeline at this point + var tl = room.getLiveTimeline().getEvents(); + expect(tl.length).toEqual(2); + ev1 = tl[0]; + ev2 = tl[1]; + + expect(ev1.status).toEqual(EventStatus.SENDING); + expect(ev2.status).toEqual(EventStatus.QUEUED); + + // now we can cancel the second and check everything looks sane + client.cancelPendingEvent(ev2); + expect(ev2.status).toEqual(EventStatus.CANCELLED); + expect(tl.length).toEqual(1); + + // shouldn't be able to cancel the first message yet + expect(function() { client.cancelPendingEvent(ev1); }) + .toThrow(); + + // fail the first send + httpBackend.when("PUT", "/send/m.room.message/") + .respond(400); + httpBackend.flush().then(function() { + expect(ev1.status).toEqual(EventStatus.NOT_SENT); + expect(tl.length).toEqual(1); + + // cancel the first message + client.cancelPendingEvent(ev1); + expect(ev1.status).toEqual(EventStatus.CANCELLED); + expect(tl.length).toEqual(0); + }).catch(utils.failTest).done(done); + }); + describe("resending", function() { xit("should be able to resend a NOT_SENT event", function() { diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index e7a5848fa..fb0fd6c19 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -1225,4 +1225,76 @@ describe("Room", function() { ); }); }); + + describe("updatePendingEvent", function() { + it("should remove cancelled events from the pending list", function() { + var room = new Room(roomId, { + pendingEventOrdering: "detached" + }); + var eventA = utils.mkMessage({ + room: roomId, user: userA, event: true + }); + eventA.status = EventStatus.SENDING; + var eventId = eventA.getId(); + + room.addPendingEvent(eventA, "TXN1"); + expect(room.getPendingEvents()).toEqual( + [eventA] + ); + + // the event has to have been failed or queued before it can be + // cancelled + room.updatePendingEvent(eventA, EventStatus.NOT_SENT); + + var callCount = 0; + room.on("Room.localEchoUpdated", + function(event, emitRoom, oldEventId, oldStatus) { + expect(event).toEqual(eventA); + expect(event.status).toEqual(EventStatus.CANCELLED); + expect(emitRoom).toEqual(room); + expect(oldEventId).toEqual(eventId); + expect(oldStatus).toEqual(EventStatus.NOT_SENT); + callCount++; + }); + + room.updatePendingEvent(eventA, EventStatus.CANCELLED); + expect(room.getPendingEvents()).toEqual([]); + expect(callCount).toEqual(1); + }); + + + it("should remove cancelled events from the timeline", function() { + var room = new Room(roomId); + var eventA = utils.mkMessage({ + room: roomId, user: userA, event: true + }); + eventA.status = EventStatus.SENDING; + var eventId = eventA.getId(); + + room.addPendingEvent(eventA, "TXN1"); + expect(room.getLiveTimeline().getEvents()).toEqual( + [eventA] + ); + + // the event has to have been failed or queued before it can be + // cancelled + room.updatePendingEvent(eventA, EventStatus.NOT_SENT); + + var callCount = 0; + room.on("Room.localEchoUpdated", + function(event, emitRoom, oldEventId, oldStatus) { + expect(event).toEqual(eventA); + expect(event.status).toEqual(EventStatus.CANCELLED); + expect(emitRoom).toEqual(room); + expect(oldEventId).toEqual(eventId); + expect(oldStatus).toEqual(EventStatus.NOT_SENT); + callCount++; + }); + + room.updatePendingEvent(eventA, EventStatus.CANCELLED); + expect(room.getLiveTimeline().getEvents()).toEqual([]); + expect(callCount).toEqual(1); + }); + + }); });