1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-07-31 15:24:23 +03:00

Make local echo work for threads (#2026)

This commit is contained in:
Germain
2021-11-24 08:19:17 +00:00
committed by GitHub
parent 0ccde7807f
commit ddd6a05198
5 changed files with 383 additions and 90 deletions

View File

@ -23,7 +23,7 @@ describe("MatrixClient retrying", function() {
); );
httpBackend = testClient.httpBackend; httpBackend = testClient.httpBackend;
client = testClient.client; client = testClient.client;
room = new Room(roomId); room = new Room(roomId, client, userId);
client.store.storeRoom(room); client.store.storeRoom(room);
}); });
@ -50,7 +50,10 @@ describe("MatrixClient retrying", function() {
it("should mark events as EventStatus.CANCELLED when cancelled", function() { it("should mark events as EventStatus.CANCELLED when cancelled", function() {
// send a couple of events; the second will be queued // send a couple of events; the second will be queued
const p1 = client.sendMessage(roomId, "m1").then(function(ev) { const p1 = client.sendMessage(roomId, {
"msgtype": "m.text",
"body": "m1",
}).then(function(ev) {
// we expect the first message to fail // we expect the first message to fail
throw new Error('Message 1 unexpectedly sent successfully'); throw new Error('Message 1 unexpectedly sent successfully');
}, (e) => { }, (e) => {
@ -60,7 +63,10 @@ describe("MatrixClient retrying", function() {
// XXX: it turns out that the promise returned by this message // XXX: it turns out that the promise returned by this message
// never gets resolved. // never gets resolved.
// https://github.com/matrix-org/matrix-js-sdk/issues/496 // https://github.com/matrix-org/matrix-js-sdk/issues/496
client.sendMessage(roomId, "m2"); client.sendMessage(roomId, {
"msgtype": "m.text",
"body": "m2",
});
// both events should be in the timeline at this point // both events should be in the timeline at this point
const tl = room.getLiveTimeline().getEvents(); const tl = room.getLiveTimeline().getEvents();
@ -88,7 +94,7 @@ describe("MatrixClient retrying", function() {
}).respond(400); // fail the first message }).respond(400); // fail the first message
// wait for the localecho of ev1 to be updated // wait for the localecho of ev1 to be updated
const p3 = new Promise((resolve, reject) => { const p3 = new Promise<void>((resolve, reject) => {
room.on("Room.localEchoUpdated", (ev0) => { room.on("Room.localEchoUpdated", (ev0) => {
if (ev0 === ev1) { if (ev0 === ev1) {
resolve(); resolve();

View File

@ -689,6 +689,12 @@ interface IRoomsKeysResponse {
} }
/* eslint-enable camelcase */ /* eslint-enable camelcase */
// We're using this constant for methods overloading and inspect whether a variable
// contains an eventId or not. This was required to ensure backwards compatibility
// of methods for threads
// Probably not the most graceful solution but does a good enough job for now
const EVENT_ID_PREFIX = "$";
/** /**
* Represents a Matrix Client. Only directly construct this if you want to use * Represents a Matrix Client. Only directly construct this if you want to use
* custom modules. Normally, {@link createClient} should be used * custom modules. Normally, {@link createClient} should be used
@ -3392,10 +3398,11 @@ export class MatrixClient extends EventEmitter {
/** /**
* @param {string} roomId * @param {string} roomId
* @param {string} threadId
* @param {string} eventType * @param {string} eventType
* @param {Object} content * @param {Object} content
* @param {string} txnId Optional. * @param {string} txnId Optional.
* @param {module:client.callback} callback Optional. * @param {module:client.callback} callback Optional. Deprecated
* @return {Promise} Resolves: to an empty object {} * @return {Promise} Resolves: to an empty object {}
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
*/ */
@ -3405,20 +3412,45 @@ export class MatrixClient extends EventEmitter {
content: IContent, content: IContent,
txnId?: string, txnId?: string,
callback?: Callback, callback?: Callback,
);
public sendEvent(
roomId: string,
threadId: string | null,
eventType: string,
content: IContent,
txnId?: string,
callback?: Callback,
)
public sendEvent(
roomId: string,
threadId: string | null,
eventType: string | IContent,
content: IContent | string,
txnId?: string | Callback,
callback?: Callback,
): Promise<ISendEventResponse> { ): Promise<ISendEventResponse> {
return this.sendCompleteEvent(roomId, { type: eventType, content }, txnId, callback); if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) {
callback = txnId as Callback;
txnId = content as string;
content = eventType as IContent;
eventType = threadId;
threadId = null;
}
return this.sendCompleteEvent(roomId, threadId, { type: eventType, content }, txnId as string, callback);
} }
/** /**
* @param {string} roomId * @param {string} roomId
* @param {string} threadId
* @param {object} eventObject An object with the partial structure of an event, to which event_id, user_id, room_id and origin_server_ts will be added. * @param {object} eventObject An object with the partial structure of an event, to which event_id, user_id, room_id and origin_server_ts will be added.
* @param {string} txnId Optional. * @param {string} txnId Optional.
* @param {module:client.callback} callback Optional. * @param {module:client.callback} callback Optional. Deprecated
* @return {Promise} Resolves: to an empty object {} * @return {Promise} Resolves: to an empty object {}
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
*/ */
private sendCompleteEvent( private sendCompleteEvent(
roomId: string, roomId: string,
threadId: string | null,
eventObject: any, eventObject: any,
txnId?: string, txnId?: string,
callback?: Callback, callback?: Callback,
@ -3444,6 +3476,10 @@ export class MatrixClient extends EventEmitter {
})); }));
const room = this.getRoom(roomId); const room = this.getRoom(roomId);
const thread = room?.threads.get(threadId);
if (thread) {
localEvent.setThread(thread);
}
// if this is a relation or redaction of an event // if this is a relation or redaction of an event
// that hasn't been sent yet (e.g. with a local id starting with a ~) // that hasn't been sent yet (e.g. with a local id starting with a ~)
@ -3460,12 +3496,12 @@ export class MatrixClient extends EventEmitter {
const type = localEvent.getType(); const type = localEvent.getType();
logger.log(`sendEvent of type ${type} in ${roomId} with txnId ${txnId}`); logger.log(`sendEvent of type ${type} in ${roomId} with txnId ${txnId}`);
localEvent.setTxnId(txnId); localEvent.setTxnId(txnId as string);
localEvent.setStatus(EventStatus.SENDING); localEvent.setStatus(EventStatus.SENDING);
// add this event immediately to the local store as 'sending'. // add this event immediately to the local store as 'sending'.
if (room) { if (room) {
room.addPendingEvent(localEvent, txnId); room.addPendingEvent(localEvent, txnId as string);
} }
// addPendingEvent can change the state to NOT_SENT if it believes // addPendingEvent can change the state to NOT_SENT if it believes
@ -3655,7 +3691,7 @@ export class MatrixClient extends EventEmitter {
* supplied. * supplied.
* @param {object|module:client.callback} cbOrOpts * @param {object|module:client.callback} cbOrOpts
* Options to pass on, may contain `reason`. * Options to pass on, may contain `reason`.
* Can be callback for backwards compatibility. * Can be callback for backwards compatibility. Deprecated
* @return {Promise} Resolves: TODO * @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
*/ */
@ -3664,22 +3700,43 @@ export class MatrixClient extends EventEmitter {
eventId: string, eventId: string,
txnId?: string, txnId?: string,
cbOrOpts?: Callback | IRedactOpts, cbOrOpts?: Callback | IRedactOpts,
);
public redactEvent(
roomId: string,
threadId: string | null,
eventId: string,
txnId?: string,
cbOrOpts?: Callback | IRedactOpts,
);
public redactEvent(
roomId: string,
threadId: string | null,
eventId: string,
txnId?: string | Callback | IRedactOpts,
cbOrOpts?: Callback | IRedactOpts,
): Promise<ISendEventResponse> { ): Promise<ISendEventResponse> {
if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) {
cbOrOpts = txnId as (Callback | IRedactOpts);
txnId = eventId;
eventId = threadId;
threadId = null;
}
const opts = typeof (cbOrOpts) === 'object' ? cbOrOpts : {}; const opts = typeof (cbOrOpts) === 'object' ? cbOrOpts : {};
const reason = opts.reason; const reason = opts.reason;
const callback = typeof (cbOrOpts) === 'function' ? cbOrOpts : undefined; const callback = typeof (cbOrOpts) === 'function' ? cbOrOpts : undefined;
return this.sendCompleteEvent(roomId, { return this.sendCompleteEvent(roomId, threadId, {
type: EventType.RoomRedaction, type: EventType.RoomRedaction,
content: { reason: reason }, content: { reason: reason },
redacts: eventId, redacts: eventId,
}, txnId, callback); }, txnId as string, callback);
} }
/** /**
* @param {string} roomId * @param {string} roomId
* @param {string} threadId
* @param {Object} content * @param {Object} content
* @param {string} txnId Optional. * @param {string} txnId Optional.
* @param {module:client.callback} callback Optional. * @param {module:client.callback} callback Optional. Deprecated
* @return {Promise} Resolves: to an ISendEventResponse object * @return {Promise} Resolves: to an ISendEventResponse object
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
*/ */
@ -3688,19 +3745,47 @@ export class MatrixClient extends EventEmitter {
content: IContent, content: IContent,
txnId?: string, txnId?: string,
callback?: Callback, callback?: Callback,
)
public sendMessage(
roomId: string,
threadId: string | null,
content: IContent,
txnId?: string,
callback?: Callback,
)
public sendMessage(
roomId: string,
threadId: string | null | IContent,
content: IContent | string,
txnId?: string | Callback,
callback?: Callback,
): Promise<ISendEventResponse> { ): Promise<ISendEventResponse> {
if (typeof threadId !== "string" && threadId !== null) {
callback = txnId as Callback;
txnId = content as string;
content = threadId as IContent;
threadId = null;
}
if (utils.isFunction(txnId)) { if (utils.isFunction(txnId)) {
callback = txnId as any as Callback; // for legacy callback = txnId as any as Callback; // for legacy
txnId = undefined; txnId = undefined;
} }
return this.sendEvent(roomId, EventType.RoomMessage, content, txnId, callback); return this.sendEvent(
roomId,
threadId as (string | null),
EventType.RoomMessage,
content as IContent,
txnId as string,
callback,
);
} }
/** /**
* @param {string} roomId * @param {string} roomId
* @param {string} threadId
* @param {string} body * @param {string} body
* @param {string} txnId Optional. * @param {string} txnId Optional.
* @param {module:client.callback} callback Optional. * @param {module:client.callback} callback Optional. Deprecated
* @return {Promise} Resolves: to an empty object {} * @return {Promise} Resolves: to an empty object {}
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
*/ */
@ -3709,29 +3794,76 @@ export class MatrixClient extends EventEmitter {
body: string, body: string,
txnId?: string, txnId?: string,
callback?: Callback, callback?: Callback,
)
public sendTextMessage(
roomId: string,
threadId: string | null,
body: string,
txnId?: string,
callback?: Callback,
)
public sendTextMessage(
roomId: string,
threadId: string | null,
body: string,
txnId?: string | Callback,
callback?: Callback,
): Promise<ISendEventResponse> { ): Promise<ISendEventResponse> {
if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) {
callback = txnId as Callback;
txnId = body;
body = threadId;
threadId = null;
}
const content = ContentHelpers.makeTextMessage(body); const content = ContentHelpers.makeTextMessage(body);
return this.sendMessage(roomId, content, txnId, callback); return this.sendMessage(roomId, threadId, content, txnId as string, callback);
} }
/** /**
* @param {string} roomId * @param {string} roomId
* @param {string} threadId
* @param {string} body * @param {string} body
* @param {string} txnId Optional. * @param {string} txnId Optional.
* @param {module:client.callback} callback Optional. * @param {module:client.callback} callback Optional. Deprecated
* @return {Promise} Resolves: to a ISendEventResponse object * @return {Promise} Resolves: to a ISendEventResponse object
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
*/ */
public sendNotice(roomId: string, body: string, txnId?: string, callback?: Callback): Promise<ISendEventResponse> { public sendNotice(
roomId: string,
body: string,
txnId?: string,
callback?: Callback,
)
public sendNotice(
roomId: string,
threadId: string | null,
body: string,
txnId?: string,
callback?: Callback,
);
public sendNotice(
roomId: string,
threadId: string | null,
body: string,
txnId?: string | Callback,
callback?: Callback,
): Promise<ISendEventResponse> {
if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) {
callback = txnId as Callback;
txnId = body;
body = threadId;
threadId = null;
}
const content = ContentHelpers.makeNotice(body); const content = ContentHelpers.makeNotice(body);
return this.sendMessage(roomId, content, txnId, callback); return this.sendMessage(roomId, threadId, content, txnId as string, callback);
} }
/** /**
* @param {string} roomId * @param {string} roomId
* @param {string} threadId
* @param {string} body * @param {string} body
* @param {string} txnId Optional. * @param {string} txnId Optional.
* @param {module:client.callback} callback Optional. * @param {module:client.callback} callback Optional. Deprecated
* @return {Promise} Resolves: to a ISendEventResponse object * @return {Promise} Resolves: to a ISendEventResponse object
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
*/ */
@ -3740,17 +3872,38 @@ export class MatrixClient extends EventEmitter {
body: string, body: string,
txnId?: string, txnId?: string,
callback?: Callback, callback?: Callback,
)
public sendEmoteMessage(
roomId: string,
threadId: string | null,
body: string,
txnId?: string,
callback?: Callback,
);
public sendEmoteMessage(
roomId: string,
threadId: string | null,
body: string,
txnId?: string | Callback,
callback?: Callback,
): Promise<ISendEventResponse> { ): Promise<ISendEventResponse> {
if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) {
callback = txnId as Callback;
txnId = body;
body = threadId;
threadId = null;
}
const content = ContentHelpers.makeEmoteMessage(body); const content = ContentHelpers.makeEmoteMessage(body);
return this.sendMessage(roomId, content, txnId, callback); return this.sendMessage(roomId, threadId, content, txnId as string, callback);
} }
/** /**
* @param {string} roomId * @param {string} roomId
* @param {string} threadId
* @param {string} url * @param {string} url
* @param {Object} info * @param {Object} info
* @param {string} text * @param {string} text
* @param {module:client.callback} callback Optional. * @param {module:client.callback} callback Optional. Deprecated
* @return {Promise} Resolves: to a ISendEventResponse object * @return {Promise} Resolves: to a ISendEventResponse object
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
*/ */
@ -3758,9 +3911,32 @@ export class MatrixClient extends EventEmitter {
roomId: string, roomId: string,
url: string, url: string,
info?: IImageInfo, info?: IImageInfo,
text = "Image", text?: string,
callback?: Callback,
);
public sendImageMessage(
roomId: string,
threadId: string | null,
url: string,
info?: IImageInfo,
text?: string,
callback?: Callback,
);
public sendImageMessage(
roomId: string,
threadId: string | null,
url: string | IImageInfo,
info?: IImageInfo | string,
text: Callback | string = "Image",
callback?: Callback, callback?: Callback,
): Promise<ISendEventResponse> { ): Promise<ISendEventResponse> {
if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) {
callback = text as Callback;
text = info as string || "Image";
info = url as IImageInfo;
url = threadId as string;
threadId = null;
}
if (utils.isFunction(text)) { if (utils.isFunction(text)) {
callback = text as any as Callback; // legacy callback = text as any as Callback; // legacy
text = undefined; text = undefined;
@ -3771,15 +3947,16 @@ export class MatrixClient extends EventEmitter {
info: info, info: info,
body: text, body: text,
}; };
return this.sendMessage(roomId, content, undefined, callback); return this.sendMessage(roomId, threadId, content, undefined, callback);
} }
/** /**
* @param {string} roomId * @param {string} roomId
* @param {string} threadId
* @param {string} url * @param {string} url
* @param {Object} info * @param {Object} info
* @param {string} text * @param {string} text
* @param {module:client.callback} callback Optional. * @param {module:client.callback} callback Optional. Deprecated
* @return {Promise} Resolves: to a ISendEventResponse object * @return {Promise} Resolves: to a ISendEventResponse object
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
*/ */
@ -3787,9 +3964,32 @@ export class MatrixClient extends EventEmitter {
roomId: string, roomId: string,
url: string, url: string,
info?: IImageInfo, info?: IImageInfo,
text = "Sticker", text?: string,
callback?: Callback,
);
public sendStickerMessage(
roomId: string,
threadId: string | null,
url: string,
info?: IImageInfo,
text?: string,
callback?: Callback,
);
public sendStickerMessage(
roomId: string,
threadId: string | null,
url: string | IImageInfo,
info?: IImageInfo | string,
text: Callback | string = "Sticker",
callback?: Callback, callback?: Callback,
): Promise<ISendEventResponse> { ): Promise<ISendEventResponse> {
if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) {
callback = text as Callback;
text = info as string || "Sticker";
info = url as IImageInfo;
url = threadId as string;
threadId = null;
}
if (utils.isFunction(text)) { if (utils.isFunction(text)) {
callback = text as any as Callback; // legacy callback = text as any as Callback; // legacy
text = undefined; text = undefined;
@ -3799,14 +3999,15 @@ export class MatrixClient extends EventEmitter {
info: info, info: info,
body: text, body: text,
}; };
return this.sendEvent(roomId, EventType.Sticker, content, undefined, callback); return this.sendEvent(roomId, threadId, EventType.Sticker, content, undefined, callback);
} }
/** /**
* @param {string} roomId * @param {string} roomId
* @param {string} threadId
* @param {string} body * @param {string} body
* @param {string} htmlBody * @param {string} htmlBody
* @param {module:client.callback} callback Optional. * @param {module:client.callback} callback Optional. Deprecated
* @return {Promise} Resolves: to a ISendEventResponse object * @return {Promise} Resolves: to a ISendEventResponse object
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
*/ */
@ -3815,16 +4016,36 @@ export class MatrixClient extends EventEmitter {
body: string, body: string,
htmlBody: string, htmlBody: string,
callback?: Callback, callback?: Callback,
);
public sendHtmlMessage(
roomId: string,
threadId: string | null,
body: string,
htmlBody: string,
callback?: Callback,
)
public sendHtmlMessage(
roomId: string,
threadId: string | null,
body: string,
htmlBody: string | Callback,
callback?: Callback,
): Promise<ISendEventResponse> { ): Promise<ISendEventResponse> {
const content = ContentHelpers.makeHtmlMessage(body, htmlBody); if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) {
return this.sendMessage(roomId, content, undefined, callback); callback = htmlBody as Callback;
htmlBody = body as string;
body = threadId;
threadId = null;
}
const content = ContentHelpers.makeHtmlMessage(body, htmlBody as string);
return this.sendMessage(roomId, threadId, content, undefined, callback);
} }
/** /**
* @param {string} roomId * @param {string} roomId
* @param {string} body * @param {string} body
* @param {string} htmlBody * @param {string} htmlBody
* @param {module:client.callback} callback Optional. * @param {module:client.callback} callback Optional. Deprecated
* @return {Promise} Resolves: to a ISendEventResponse object * @return {Promise} Resolves: to a ISendEventResponse object
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
*/ */
@ -3833,16 +4054,37 @@ export class MatrixClient extends EventEmitter {
body: string, body: string,
htmlBody: string, htmlBody: string,
callback?: Callback, callback?: Callback,
);
public sendHtmlNotice(
roomId: string,
threadId: string | null,
body: string,
htmlBody: string,
callback?: Callback,
)
public sendHtmlNotice(
roomId: string,
threadId: string | null,
body: string,
htmlBody: string | Callback,
callback?: Callback,
): Promise<ISendEventResponse> { ): Promise<ISendEventResponse> {
const content = ContentHelpers.makeHtmlNotice(body, htmlBody); if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) {
return this.sendMessage(roomId, content, undefined, callback); callback = htmlBody as Callback;
htmlBody = body as string;
body = threadId;
threadId = null;
}
const content = ContentHelpers.makeHtmlNotice(body, htmlBody as string);
return this.sendMessage(roomId, threadId, content, undefined, callback);
} }
/** /**
* @param {string} roomId * @param {string} roomId
* @param {string} threadId
* @param {string} body * @param {string} body
* @param {string} htmlBody * @param {string} htmlBody
* @param {module:client.callback} callback Optional. * @param {module:client.callback} callback Optional. Deprecated
* @return {Promise} Resolves: to a ISendEventResponse object * @return {Promise} Resolves: to a ISendEventResponse object
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
*/ */
@ -3851,9 +4093,29 @@ export class MatrixClient extends EventEmitter {
body: string, body: string,
htmlBody: string, htmlBody: string,
callback?: Callback, callback?: Callback,
);
public sendHtmlEmote(
roomId: string,
threadId: string | null,
body: string,
htmlBody: string,
callback?: Callback,
)
public sendHtmlEmote(
roomId: string,
threadId: string | null,
body: string,
htmlBody: string | Callback,
callback?: Callback,
): Promise<ISendEventResponse> { ): Promise<ISendEventResponse> {
const content = ContentHelpers.makeHtmlEmote(body, htmlBody); if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) {
return this.sendMessage(roomId, content, undefined, callback); callback = htmlBody as Callback;
htmlBody = body as string;
body = threadId;
threadId = null;
}
const content = ContentHelpers.makeHtmlEmote(body, htmlBody as string);
return this.sendMessage(roomId, threadId, content, undefined, callback);
} }
/** /**

View File

@ -28,6 +28,7 @@ import { Room } from "./room";
import { Filter } from "../filter"; import { Filter } from "../filter";
import { EventType, RelationType } from "../@types/event"; import { EventType, RelationType } from "../@types/event";
import { RoomState } from "./room-state"; import { RoomState } from "./room-state";
import { Thread } from "./thread";
// var DEBUG = false; // var DEBUG = false;
const DEBUG = true; const DEBUG = true;
@ -153,18 +154,18 @@ export class EventTimelineSet extends EventEmitter {
* *
* @throws If <code>opts.pendingEventOrdering</code> was not 'detached' * @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
*/ */
public getPendingEvents(): MatrixEvent[] { public getPendingEvents(thread?: Thread): MatrixEvent[] {
if (!this.room || !this.displayPendingEvents) { if (!this.room || !this.displayPendingEvents) {
return []; return [];
} }
const pendingEvents = this.room.getPendingEvents(thread);
if (this.filter) { if (this.filter) {
return this.filter.filterRoomTimeline(this.room.getPendingEvents()); return this.filter.filterRoomTimeline(pendingEvents);
} else { } else {
return this.room.getPendingEvents(); return pendingEvents;
} }
} }
/** /**
* Get the live timeline for this room. * Get the live timeline for this room.
* *

View File

@ -225,12 +225,6 @@ export class Room extends EventEmitter {
this.reEmitter = new ReEmitter(this); this.reEmitter = new ReEmitter(this);
opts.pendingEventOrdering = opts.pendingEventOrdering || PendingEventOrdering.Chronological; opts.pendingEventOrdering = opts.pendingEventOrdering || PendingEventOrdering.Chronological;
if (["chronological", "detached"].indexOf(opts.pendingEventOrdering) === -1) {
throw new Error(
"opts.pendingEventOrdering MUST be either 'chronological' or " +
"'detached'. Got: '" + opts.pendingEventOrdering + "'",
);
}
this.name = roomId; this.name = roomId;
@ -241,7 +235,7 @@ export class Room extends EventEmitter {
this.fixUpLegacyTimelineFields(); this.fixUpLegacyTimelineFields();
if (this.opts.pendingEventOrdering == "detached") { if (this.opts.pendingEventOrdering === PendingEventOrdering.Detached) {
this.pendingEventList = []; this.pendingEventList = [];
const serializedPendingEventList = client.sessionStore.store.getItem(pendingEventsKey(this.roomId)); const serializedPendingEventList = client.sessionStore.store.getItem(pendingEventsKey(this.roomId));
if (serializedPendingEventList) { if (serializedPendingEventList) {
@ -452,14 +446,16 @@ export class Room extends EventEmitter {
* *
* @throws If <code>opts.pendingEventOrdering</code> was not 'detached' * @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
*/ */
public getPendingEvents(): MatrixEvent[] { public getPendingEvents(thread?: Thread): MatrixEvent[] {
if (this.opts.pendingEventOrdering !== "detached") { if (this.opts.pendingEventOrdering !== PendingEventOrdering.Detached) {
throw new Error( throw new Error(
"Cannot call getPendingEvents with pendingEventOrdering == " + "Cannot call getPendingEvents with pendingEventOrdering == " +
this.opts.pendingEventOrdering); this.opts.pendingEventOrdering);
} }
return this.pendingEventList; return this.pendingEventList.filter(event => {
return !thread || thread.id === event.threadRootId;
});
} }
/** /**
@ -469,7 +465,7 @@ export class Room extends EventEmitter {
* @return {boolean} True if an element was removed. * @return {boolean} True if an element was removed.
*/ */
public removePendingEvent(eventId: string): boolean { public removePendingEvent(eventId: string): boolean {
if (this.opts.pendingEventOrdering !== "detached") { if (this.opts.pendingEventOrdering !== PendingEventOrdering.Detached) {
throw new Error( throw new Error(
"Cannot call removePendingEvent with pendingEventOrdering == " + "Cannot call removePendingEvent with pendingEventOrdering == " +
this.opts.pendingEventOrdering); this.opts.pendingEventOrdering);
@ -495,7 +491,7 @@ export class Room extends EventEmitter {
* @return {boolean} * @return {boolean}
*/ */
public hasPendingEvent(eventId: string): boolean { public hasPendingEvent(eventId: string): boolean {
if (this.opts.pendingEventOrdering !== "detached") { if (this.opts.pendingEventOrdering !== PendingEventOrdering.Detached) {
return false; return false;
} }
@ -509,7 +505,7 @@ export class Room extends EventEmitter {
* @return {MatrixEvent} * @return {MatrixEvent}
*/ */
public getPendingEvent(eventId: string): MatrixEvent | null { public getPendingEvent(eventId: string): MatrixEvent | null {
if (this.opts.pendingEventOrdering !== "detached") { if (this.opts.pendingEventOrdering !== PendingEventOrdering.Detached) {
return null; return null;
} }
@ -856,7 +852,13 @@ export class Room extends EventEmitter {
* the given event, or null if unknown * the given event, or null if unknown
*/ */
public getTimelineForEvent(eventId: string): EventTimeline { public getTimelineForEvent(eventId: string): EventTimeline {
return this.getUnfilteredTimelineSet().getTimelineForEvent(eventId); const event = this.findEventById(eventId);
const thread = this.findThreadForEvent(event);
if (thread) {
return thread.timelineSet.getLiveTimeline();
} else {
return this.getUnfilteredTimelineSet().getTimelineForEvent(eventId);
}
} }
/** /**
@ -1403,13 +1405,6 @@ export class Room extends EventEmitter {
* unique transaction id. * unique transaction id.
*/ */
public addPendingEvent(event: MatrixEvent, txnId: string): void { public addPendingEvent(event: MatrixEvent, txnId: string): void {
// TODO: Enable "pending events" for threads
// There's a fair few things to update to make them work with Threads
// Will get back to it when the plan is to build a more polished UI ready for production
if (this.client?.supportsExperimentalThreads() && event.threadRootId) {
return;
}
if (event.status !== EventStatus.SENDING && event.status !== EventStatus.NOT_SENT) { if (event.status !== EventStatus.SENDING && event.status !== EventStatus.NOT_SENT) {
throw new Error("addPendingEvent called on an event with status " + throw new Error("addPendingEvent called on an event with status " +
event.status); event.status);
@ -1426,8 +1421,8 @@ export class Room extends EventEmitter {
EventTimeline.setEventMetadata(event, this.getLiveTimeline().getState(EventTimeline.FORWARDS), false); EventTimeline.setEventMetadata(event, this.getLiveTimeline().getState(EventTimeline.FORWARDS), false);
this.txnToEvent[txnId] = event; this.txnToEvent[txnId] = event;
const thread = this.threads.get(event.threadRootId);
if (this.opts.pendingEventOrdering == "detached") { if (this.opts.pendingEventOrdering === PendingEventOrdering.Detached && !thread) {
if (this.pendingEventList.some((e) => e.status === EventStatus.NOT_SENT)) { if (this.pendingEventList.some((e) => e.status === EventStatus.NOT_SENT)) {
logger.warn("Setting event as NOT_SENT due to messages in the same state"); logger.warn("Setting event as NOT_SENT due to messages in the same state");
event.setStatus(EventStatus.NOT_SENT); event.setStatus(EventStatus.NOT_SENT);
@ -1446,7 +1441,7 @@ export class Room extends EventEmitter {
let redactedEvent = this.pendingEventList && let redactedEvent = this.pendingEventList &&
this.pendingEventList.find(e => e.getId() === redactId); this.pendingEventList.find(e => e.getId() === redactId);
if (!redactedEvent) { if (!redactedEvent) {
redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId); redactedEvent = this.findEventById(redactId);
} }
if (redactedEvent) { if (redactedEvent) {
redactedEvent.markLocallyRedacted(event); redactedEvent.markLocallyRedacted(event);
@ -1454,16 +1449,21 @@ export class Room extends EventEmitter {
} }
} }
} else { } else {
for (let i = 0; i < this.timelineSets.length; i++) { if (thread) {
const timelineSet = this.timelineSets[i]; thread.timelineSet.addEventToTimeline(event,
if (timelineSet.getFilter()) { thread.timelineSet.getLiveTimeline(), false);
if (timelineSet.getFilter().filterRoomTimeline([event]).length) { } else {
for (let i = 0; i < this.timelineSets.length; i++) {
const timelineSet = this.timelineSets[i];
if (timelineSet.getFilter()) {
if (timelineSet.getFilter().filterRoomTimeline([event]).length) {
timelineSet.addEventToTimeline(event,
timelineSet.getLiveTimeline(), false);
}
} else {
timelineSet.addEventToTimeline(event, timelineSet.addEventToTimeline(event,
timelineSet.getLiveTimeline(), false); timelineSet.getLiveTimeline(), false);
} }
} else {
timelineSet.addEventToTimeline(event,
timelineSet.getLiveTimeline(), false);
} }
} }
} }
@ -1521,16 +1521,21 @@ export class Room extends EventEmitter {
* @param {module:models/event.MatrixEvent} event the relation event that needs to be aggregated. * @param {module:models/event.MatrixEvent} event the relation event that needs to be aggregated.
*/ */
private aggregateNonLiveRelation(event: MatrixEvent): void { private aggregateNonLiveRelation(event: MatrixEvent): void {
// TODO: We should consider whether this means it would be a better const thread = this.findThreadForEvent(event);
// design to lift the relations handling up to the room instead. if (thread) {
for (let i = 0; i < this.timelineSets.length; i++) { thread.timelineSet.aggregateRelations(event);
const timelineSet = this.timelineSets[i]; } else {
if (timelineSet.getFilter()) { // TODO: We should consider whether this means it would be a better
if (timelineSet.getFilter().filterRoomTimeline([event]).length) { // design to lift the relations handling up to the room instead.
for (let i = 0; i < this.timelineSets.length; i++) {
const timelineSet = this.timelineSets[i];
if (timelineSet.getFilter()) {
if (timelineSet.getFilter().filterRoomTimeline([event]).length) {
timelineSet.aggregateRelations(event);
}
} else {
timelineSet.aggregateRelations(event); timelineSet.aggregateRelations(event);
} }
} else {
timelineSet.aggregateRelations(event);
} }
} }
} }
@ -1571,11 +1576,16 @@ export class Room extends EventEmitter {
// any, which is good, because we don't want to try decoding it again). // any, which is good, because we don't want to try decoding it again).
localEvent.handleRemoteEcho(remoteEvent.event); localEvent.handleRemoteEcho(remoteEvent.event);
for (let i = 0; i < this.timelineSets.length; i++) { const thread = this.threads.get(remoteEvent.threadRootId);
const timelineSet = this.timelineSets[i]; if (thread) {
thread.timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
} else {
for (let i = 0; i < this.timelineSets.length; i++) {
const timelineSet = this.timelineSets[i];
// if it's already in the timeline, update the timeline map. If it's not, add it. // if it's already in the timeline, update the timeline map. If it's not, add it.
timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId); timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
}
} }
this.emit("Room.localEchoUpdated", localEvent, this, this.emit("Room.localEchoUpdated", localEvent, this,
@ -1608,7 +1618,7 @@ export class Room extends EventEmitter {
// SENT races against /sync, so we have to special-case it. // SENT races against /sync, so we have to special-case it.
if (newStatus == EventStatus.SENT) { if (newStatus == EventStatus.SENT) {
const timeline = this.getUnfilteredTimelineSet().eventIdToTimeline(newEventId); const timeline = this.getTimelineForEvent(newEventId);
if (timeline) { if (timeline) {
// we've already received the event via the event stream. // we've already received the event via the event stream.
// nothing more to do here. // nothing more to do here.
@ -1636,11 +1646,16 @@ export class Room extends EventEmitter {
// update the event id // update the event id
event.replaceLocalEventId(newEventId); event.replaceLocalEventId(newEventId);
// if the event was already in the timeline (which will be the case if const thread = this.findThreadForEvent(event);
// opts.pendingEventOrdering==chronological), we need to update the if (thread) {
// timeline map. thread.timelineSet.replaceEventId(oldEventId, newEventId);
for (let i = 0; i < this.timelineSets.length; i++) { } else {
this.timelineSets[i].replaceEventId(oldEventId, newEventId); // if the event was already in the timeline (which will be the case if
// opts.pendingEventOrdering==chronological), we need to update the
// timeline map.
for (let i = 0; i < this.timelineSets.length; i++) {
this.timelineSets[i].replaceEventId(oldEventId, newEventId);
}
} }
} else if (newStatus == EventStatus.CANCELLED) { } else if (newStatus == EventStatus.CANCELLED) {
// remove it from the pending event list, or the timeline. // remove it from the pending event list, or the timeline.

View File

@ -55,11 +55,20 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
this.timelineSet = new EventTimelineSet(this.room, { this.timelineSet = new EventTimelineSet(this.room, {
unstableClientRelationAggregation: true, unstableClientRelationAggregation: true,
timelineSupport: true, timelineSupport: true,
pendingEvents: false, pendingEvents: true,
}); });
events.forEach(event => this.addEvent(event)); events.forEach(event => this.addEvent(event));
room.on("Room.localEchoUpdated", this.onEcho);
room.on("Room.timeline", this.onEcho);
} }
onEcho = (event: MatrixEvent) => {
if (this.timelineSet.eventIdToTimeline(event.getId())) {
this.emit(ThreadEvent.Update, this);
}
};
/** /**
* Add an event to the thread and updates * Add an event to the thread and updates
* the tail/root references if needed * the tail/root references if needed