Compare commits

...

22 Commits

Author SHA1 Message Date
RiotRobot 738876a563 v17.1.0 2022-04-26 11:13:47 +01:00
RiotRobot 2d9b4b3896 Prepare changelog for v17.1.0 2022-04-26 11:13:46 +01:00
RiotRobot 91a67bdac3 v17.1.0-rc.1 2022-04-19 14:41:27 +01:00
RiotRobot 48d3fce22d Prepare changelog for v17.1.0-rc.1 2022-04-19 14:41:26 +01:00
Hugh Nimmo-Smith 9f4598638d Add MatrixClient.doesServerSupportLogoutDevices() for MSC2457 (#2297) 2022-04-15 10:27:12 +01:00
CommanderRoot cde935629d Replace deprecated String.prototype.substr() (#2298)
.substr() is deprecated so we replace it with .slice() which works similarily but isn't deprecated

Signed-off-by: Tobias Speicher <rootcommander@gmail.com>
2022-04-14 15:23:27 -06:00
Kerry fbe81ad823 Live location sharing - expose room liveBeaconIds (#2296)
* updates rooms live beacon ids on destroy

Signed-off-by: Kerry Archibald <kerrya@element.io>

* expose live beacons ids

Signed-off-by: Kerry Archibald <kerrya@element.io>

* room state emit all the time on beacon liveness change

Signed-off-by: Kerry Archibald <kerrya@element.io>

* update comment

Signed-off-by: Kerry Archibald <kerrya@element.io>
2022-04-14 15:04:43 +02:00
Michael Telatynski c3d7a4977a Stop tracking threads if threads support is disabled (#2295) 2022-04-13 16:23:15 +01:00
Robin 9aab917836 Fix coverage diffs for PRs that aren't up to date, take 3 (#2294) 2022-04-12 20:22:14 -04:00
Jonathan de Jong 54661cca95 Change _unstable_getSharedRooms to _unstable_getMutualRooms (#2271)
* change shared to mutual

* revert name change

* use new unstable feature flag

* support both unstable endpoints
2022-04-12 17:48:48 -06:00
Michael Telatynski b58d09aa9a Prevent duplicated re-emitter setups in event-mapper (#2293) 2022-04-12 09:42:55 +01:00
RiotRobot 3b33237e51 Resetting package fields for development 2022-04-11 16:31:44 +01:00
RiotRobot b8ec62e786 Merge master 2022-04-11 16:31:21 +01:00
Michael Telatynski 286500e335 Fix issues around echo & redaction handling in threads (#2286) 2022-04-11 08:58:13 +01:00
Hugh Nimmo-Smith 5937e6a6a8 Support for MSC2457 logout_devices param for setPassword() (#2285) 2022-04-09 14:07:33 +01:00
Eric Eastwood c6c22e394b Ignore eslint --fix formatting changes in git blame (#2287)
* Ignore eslint --fix formatting changes in git blame

Docs: https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view

* Conform on white-space spelling
2022-04-08 23:52:05 -06:00
Callum Brown 378802a5ab Stabilise token authenticated registration support (#2181)
* Stabilise token authenticated registration support

Token authenticated registration was added to the Matrix specification in v1.2:
https://spec.matrix.org/v1.2/client-server-api/#token-authenticated-registration

Signed-off-by: Callum Brown <callum@calcuode.com>

* Backwards compatibility with unstable auth type

Servers are not yet widely updated with support for the stable
version of the registration token UIA type.
Clients should check if the authentication type is either
`RegistrationToken` or `UnstableRegistrationToken`.

Signed-off-by: Callum Brown <callum@calcuode.com>
2022-04-08 22:53:57 -06:00
Kerry f963feab0f Live location sharing - Aggregate beacon locations on beacons (#2268)
* add timestamp sorting util

Signed-off-by: Kerry Archibald <kerrya@element.io>

* basic wiring

Signed-off-by: Kerry Archibald <kerrya@element.io>

* quick handle for redacted beacons

Signed-off-by: Kerry Archibald <kerrya@element.io>

* remove fdescribe

Signed-off-by: Kerry Archibald <kerrya@element.io>

* test adding locations

Signed-off-by: Kerry Archibald <kerrya@element.io>

* tidy comments

Signed-off-by: Kerry Archibald <kerrya@element.io>

* test client

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix monitorLiveness for update

Signed-off-by: Kerry Archibald <kerrya@element.io>

* lint

Signed-off-by: Kerry Archibald <kerrya@element.io>
2022-04-08 12:26:05 +01:00
Germain 6d0f4e537e Fix notification panel not loading (#2283) 2022-04-08 10:34:01 +01:00
Kerry 781fdf4fdc Live location sharing - update beacon_info implementation to latest MSC (#2281)
* remove M_BEACON_INFO_VARIABLE

Signed-off-by: Kerry Archibald <kerrya@element.io>

* create beacon_info events with non-variable event type

Signed-off-by: Kerry Archibald <kerrya@element.io>

* remove isBeaconInfoEventType

Signed-off-by: Kerry Archibald <kerrya@element.io>

* refer to msc3673 instead of msc3489

Signed-off-by: Kerry Archibald <kerrya@element.io>

* remove event type suffix

Signed-off-by: Kerry Archibald <kerrya@element.io>

* update beacon identifier to use state key

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix beacon spec

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix room-state tests

Signed-off-by: Kerry Archibald <kerrya@element.io>

* add beacon identifier

Signed-off-by: Kerry Archibald <kerrya@element.io>

* dont allow update to older beacon event

Signed-off-by: Kerry Archibald <kerrya@element.io>

* lint

Signed-off-by: Kerry Archibald <kerrya@element.io>

* unnest beacon_info content

Signed-off-by: Kerry Archibald <kerrya@element.io>

* lint

Signed-off-by: Kerry Archibald <kerrya@element.io>

* check redaction event id

Signed-off-by: Kerry Archibald <kerrya@element.io>
2022-04-08 10:50:06 +02:00
Michael Telatynski dde4285cdf Fix handling of threaded messages around edits & echoes (#2267) 2022-04-07 13:46:50 +01:00
Robin 3322b47b6d Make self membership less prone to races (#2277) 2022-04-06 08:49:09 -04:00
34 changed files with 1278 additions and 526 deletions
+41
View File
@@ -0,0 +1,41 @@
# Minor white-space adjustments
1d1d59c75744e1f6a2be1cb3e0d1bd9ded5f8025
# Import ordering and spacing: eslint-plugin-import
80aaa6c32b50601f82e0c991c24e5a4590f39463
# Minor white-space adjustment
8fb036ba2d01fab66dc4373802ccf19b5cac8541
# Minor white-space adjustment
b63de6a902a9e1f8ffd7697dea33820fc04f028e
3ca84cfc491b0987eec1f13f13cae58d2032bf54
# Conform to new typescript eslint rules
a87858840b57514603f63e2abbbda4f107f05a77
5cf6684129a921295f5593173f16f192336fe0a2
# Comply with new member-delimiter-style rule
b2ad957d298720d3e026b6bd91be0c403338361a
# Fix semicolons in TS files
e2ec8952e38b8fea3f0ccaa09ecb42feeba0d923
# Migrate to `eslint-plugin-matrix-org`
# and `babel/...` to `@babel/...` migration
09fac77ce0d9bcf6637088c29afab84084f0e739
102704e91a70643bcc09721e14b0d909f0ef55c6
# Eslint formatting
cec00cd303787fa9008b6c48826e75ed438036fa
# Minor eslint changes
68bb8182e4e62d8f450f80c408c4b231b8725f1b
c979ff6696e30ab8983ac416a3590996d84d3560
f4a7395e3a3751a1a8e92dd302c49175a3296ad2
# eslint --fix for dangley commas on function calls
423175f5397910b0afe3112d6fb18283fc7d27d4
# eslint ---fix for prefer-const
7bca05af644e8b997dae81e568a3913d8f18d7ca
# Fix linting on tests
cee7f7a280a8c20bafc21c0a2911f60851f7a7ca
# eslint --fix
0fa9f7c6098822db1ae214f352fd1fe5c248b02c
# eslint --fix for lots of white-space
5abf6b9f208801c5022a47023150b5846cb0b309
# eslint --fix
7ed65407e6cdf292ce3cf659310c68d19dcd52b2
# Switch to ESLint from JSHint (Google eslint rules as a base)
e057956ede9ad1a931ff8050c411aca7907e0394
+1
View File
@@ -29,3 +29,4 @@ jobs:
with:
fail_ci_if_error: false
verbose: true
override_commit: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || '' }}
+50
View File
@@ -1,3 +1,47 @@
Changes in [17.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v17.1.0) (2022-04-26)
==================================================================================================
## ✨ Features
* Add MatrixClient.doesServerSupportLogoutDevices() for MSC2457 ([\#2297](https://github.com/matrix-org/matrix-js-sdk/pull/2297)).
* Live location sharing - expose room liveBeaconIds ([\#2296](https://github.com/matrix-org/matrix-js-sdk/pull/2296)).
* Support for MSC2457 logout_devices param for setPassword() ([\#2285](https://github.com/matrix-org/matrix-js-sdk/pull/2285)).
* Stabilise token authenticated registration support ([\#2181](https://github.com/matrix-org/matrix-js-sdk/pull/2181)). Contributed by @govynnus.
* Live location sharing - Aggregate beacon locations on beacons ([\#2268](https://github.com/matrix-org/matrix-js-sdk/pull/2268)).
## 🐛 Bug Fixes
* Prevent duplicated re-emitter setups in event-mapper ([\#2293](https://github.com/matrix-org/matrix-js-sdk/pull/2293)).
* Make self membership less prone to races ([\#2277](https://github.com/matrix-org/matrix-js-sdk/pull/2277)). Fixes vector-im/element-web#21661.
Changes in [17.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v17.0.0) (2022-04-11)
==================================================================================================
## 🚨 BREAKING CHANGES
* Remove groups and groups-related APIs ([\#2234](https://github.com/matrix-org/matrix-js-sdk/pull/2234)).
## ✨ Features
* Add Element video room type ([\#2273](https://github.com/matrix-org/matrix-js-sdk/pull/2273)).
* Live location sharing - handle redacted beacons ([\#2269](https://github.com/matrix-org/matrix-js-sdk/pull/2269)).
## 🐛 Bug Fixes
* Fix getSessionsNeedingBackup() limit support ([\#2270](https://github.com/matrix-org/matrix-js-sdk/pull/2270)). Contributed by @adamvy.
* Fix issues with /search and /context API handling for threads ([\#2261](https://github.com/matrix-org/matrix-js-sdk/pull/2261)). Fixes vector-im/element-web#21543.
* Prevent exception 'Unable to set up secret storage' ([\#2260](https://github.com/matrix-org/matrix-js-sdk/pull/2260)).
Changes in [16.0.2-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v16.0.2-rc.1) (2022-04-05)
============================================================================================================
## 🚨 BREAKING CHANGES
* Remove groups and groups-related APIs ([\#2234](https://github.com/matrix-org/matrix-js-sdk/pull/2234)).
## ✨ Features
* Add Element video room type ([\#2273](https://github.com/matrix-org/matrix-js-sdk/pull/2273)).
* Live location sharing - handle redacted beacons ([\#2269](https://github.com/matrix-org/matrix-js-sdk/pull/2269)).
## 🐛 Bug Fixes
* Fix getSessionsNeedingBackup() limit support ([\#2270](https://github.com/matrix-org/matrix-js-sdk/pull/2270)). Contributed by @adamvy.
* Fix issues with /search and /context API handling for threads ([\#2261](https://github.com/matrix-org/matrix-js-sdk/pull/2261)). Fixes vector-im/element-web#21543.
* Prevent exception 'Unable to set up secret storage' ([\#2260](https://github.com/matrix-org/matrix-js-sdk/pull/2260)).
Changes in [16.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v16.0.1) (2022-03-28)
==================================================================================================
@@ -1988,6 +2032,12 @@ All Changes
* [BREAKING] Refactor the entire build process
[\#1113](https://github.com/matrix-org/matrix-js-sdk/pull/1113)
Changes in [3.42.2-rc.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v3.42.2-rc.3) (2022-04-08)
============================================================================================================
## 🐛 Bug Fixes
* Make self membership less prone to races ([\#2277](https://github.com/matrix-org/matrix-js-sdk/pull/2277)). Fixes vector-im/element-web#21661.
Changes in [3.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v3.0.0) (2020-01-13)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v3.0.0-rc.1...v3.0.0)
+2 -2
View File
@@ -341,7 +341,7 @@ function printLine(event) {
var maxNameWidth = 15;
if (name.length > maxNameWidth) {
name = name.substr(0, maxNameWidth-1) + "\u2026";
name = name.slice(0, maxNameWidth-1) + "\u2026";
}
if (event.getType() === "m.room.message") {
@@ -398,7 +398,7 @@ function print(str, formatter) {
function fixWidth(str, len) {
if (str.length > len) {
return str.substr(0, len-2) + "\u2026";
return str.substring(0, len-2) + "\u2026";
}
else if (str.length < len) {
return str + new Array(len - str.length).join(" ");
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "16.0.1",
"version": "17.1.0",
"description": "Matrix Client-Server SDK for Javascript",
"scripts": {
"prepublishOnly": "yarn build",
@@ -29,7 +29,7 @@
"keywords": [
"matrix-org"
],
"main": "./src/index.ts",
"main": "./lib/index.js",
"browser": "./lib/browser-index.js",
"matrix_src_main": "./src/index.ts",
"matrix_src_browser": "./src/browser-index.js",
@@ -116,5 +116,6 @@
"text",
"json"
]
}
},
"typings": "./lib/index.d.ts"
}
@@ -496,6 +496,7 @@ describe("MatrixClient event timelines", function() {
});
it("should handle thread replies with server support by fetching a contiguous thread timeline", async () => {
client.clientOpts.experimentalThreadSupport = true;
Thread.setServerSideSupport(true);
client.stopClient(); // we don't need the client to be syncing at this time
const room = client.getRoom(roomId);
+16 -11
View File
@@ -145,12 +145,14 @@ describe("MatrixClient", function() {
describe("joinRoom", function() {
it("should no-op if you've already joined a room", function() {
const roomId = "!foo:bar";
const room = new Room(roomId, userId);
const room = new Room(roomId, client, userId);
client.fetchRoomEvent = () => Promise.resolve({});
room.addLiveEvents([
utils.mkMembership({
user: userId, room: roomId, mship: "join", event: true,
}),
]);
httpBackend.verifyNoOutstandingRequests();
store.storeRoom(room);
client.joinRoom(roomId);
httpBackend.verifyNoOutstandingRequests();
@@ -556,11 +558,14 @@ describe("MatrixClient", function() {
});
describe("partitionThreadedEvents", function() {
const room = new Room("!STrMRsukXHtqQdSeHa:matrix.org", client, userId);
let room;
beforeEach(() => {
room = new Room("!STrMRsukXHtqQdSeHa:matrix.org", client, userId);
});
it("returns empty arrays when given an empty arrays", function() {
const events = [];
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
const [timeline, threaded] = room.partitionThreadedEvents(events);
expect(timeline).toEqual([]);
expect(threaded).toEqual([]);
});
@@ -580,7 +585,7 @@ describe("MatrixClient", function() {
// Vote has no threadId yet
expect(eventPollResponseReference.threadId).toBeFalsy();
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
const [timeline, threaded] = room.partitionThreadedEvents(events);
expect(timeline).toEqual([
// The message that was sent in a thread is missing
@@ -613,7 +618,7 @@ describe("MatrixClient", function() {
eventReaction,
];
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
const [timeline, threaded] = room.partitionThreadedEvents(events);
expect(timeline).toEqual([
eventPollStartThreadRoot,
@@ -640,7 +645,7 @@ describe("MatrixClient", function() {
eventMessageInThread,
];
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
const [timeline, threaded] = room.partitionThreadedEvents(events);
expect(timeline).toEqual([
eventPollStartThreadRoot,
@@ -667,7 +672,7 @@ describe("MatrixClient", function() {
eventReaction,
];
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
const [timeline, threaded] = room.partitionThreadedEvents(events);
expect(timeline).toEqual([
eventPollStartThreadRoot,
@@ -710,7 +715,7 @@ describe("MatrixClient", function() {
eventMember,
eventCreate,
];
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
const [timeline, threaded] = room.partitionThreadedEvents(events);
expect(timeline).toEqual([
// The message that was sent in a thread is missing
@@ -749,7 +754,7 @@ describe("MatrixClient", function() {
threadedReactionRedaction,
];
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
const [timeline, threaded] = room.partitionThreadedEvents(events);
expect(timeline).toEqual([
threadRootEvent,
@@ -778,7 +783,7 @@ describe("MatrixClient", function() {
replyToReply,
];
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
const [timeline, threaded] = room.partitionThreadedEvents(events);
expect(timeline).toEqual([
threadRootEvent,
@@ -805,7 +810,7 @@ describe("MatrixClient", function() {
replyToThreadResponse,
];
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
const [timeline, threaded] = room.partitionThreadedEvents(events);
expect(timeline).toEqual([
threadRootEvent,
+3 -2
View File
@@ -42,7 +42,6 @@ export const makeBeaconInfoEvent = (
roomId: string,
contentProps: Partial<InfoContentProps> = {},
eventId?: string,
eventTypeSuffix?: string,
): MatrixEvent => {
const {
timeout, isLive, description, assetType,
@@ -51,12 +50,14 @@ export const makeBeaconInfoEvent = (
...contentProps,
};
const event = new MatrixEvent({
type: `${M_BEACON_INFO.name}.${sender}.${eventTypeSuffix || Date.now()}`,
type: M_BEACON_INFO.name,
room_id: roomId,
state_key: sender,
content: makeBeaconInfoContent(timeout, isLive, description, assetType),
});
event.event.origin_server_ts = Date.now();
// live beacons use the beacon_info event id
// set or default this
event.replaceLocalEventId(eventId || `$${Math.random()}-${Math.random()}`);
+14 -4
View File
@@ -8,6 +8,7 @@ import { logger } from '../../src/logger';
import { IContent, IEvent, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import { ClientEvent, EventType, MatrixClient } from "../../src";
import { SyncState } from "../../src/sync";
import { eventMapperFor } from "../../src/event-mapper";
/**
* Return a promise that is resolved when the client next emits a
@@ -79,6 +80,7 @@ interface IEventOpts {
redacts?: string;
}
let testEventIndex = 1; // counter for events, easier for comparison of randomly generated events
/**
* Create an Event.
* @param {Object} opts Values for the event.
@@ -88,9 +90,10 @@ interface IEventOpts {
* @param {string} opts.skey Optional. The state key (auto inserts empty string)
* @param {Object} opts.content The event.content
* @param {boolean} opts.event True to make a MatrixEvent.
* @param {MatrixClient} client If passed along with opts.event=true will be used to set up re-emitters.
* @return {Object} a JSON object representing this event.
*/
export function mkEvent(opts: IEventOpts): object | MatrixEvent {
export function mkEvent(opts: IEventOpts, client?: MatrixClient): object | MatrixEvent {
if (!opts.type || !opts.content) {
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
}
@@ -100,7 +103,8 @@ export function mkEvent(opts: IEventOpts): object | MatrixEvent {
sender: opts.sender || opts.user, // opts.user for backwards-compat
content: opts.content,
unsigned: opts.unsigned || {},
event_id: "$" + Math.random() + "-" + Math.random(),
event_id: "$" + testEventIndex++ + "-" + Math.random() + "-" + Math.random(),
txn_id: "~" + Math.random(),
redacts: opts.redacts,
};
if (opts.skey !== undefined) {
@@ -116,6 +120,11 @@ export function mkEvent(opts: IEventOpts): object | MatrixEvent {
].includes(opts.type)) {
event.state_key = "";
}
if (opts.event && client) {
return eventMapperFor(client, {})(event);
}
return opts.event ? new MatrixEvent(event) : event;
}
@@ -208,9 +217,10 @@ interface IMessageOpts {
* @param {string} opts.user The user ID for the event.
* @param {string} opts.msg Optional. The content.body for the event.
* @param {boolean} opts.event True to make a MatrixEvent.
* @param {MatrixClient} client If passed along with opts.event=true will be used to set up re-emitters.
* @return {Object|MatrixEvent} The event
*/
export function mkMessage(opts: IMessageOpts): object | MatrixEvent {
export function mkMessage(opts: IMessageOpts, client?: MatrixClient): object | MatrixEvent {
const eventOpts: IEventOpts = {
...opts,
type: EventType.RoomMessage,
@@ -223,7 +233,7 @@ export function mkMessage(opts: IMessageOpts): object | MatrixEvent {
if (!eventOpts.content.body) {
eventOpts.content.body = "Random->" + Math.random();
}
return mkEvent(eventOpts);
return mkEvent(eventOpts, client);
}
/**
+3 -6
View File
@@ -16,7 +16,6 @@ limitations under the License.
import { REFERENCE_RELATION } from "matrix-events-sdk";
import { M_BEACON_INFO } from "../../src/@types/beacon";
import { LocationAssetType, M_ASSET, M_LOCATION, M_TIMESTAMP } from "../../src/@types/location";
import { makeBeaconContent, makeBeaconInfoContent } from "../../src/content-helpers";
@@ -36,11 +35,9 @@ describe('Beacon content helpers', () => {
'nice beacon_info',
LocationAssetType.Pin,
)).toEqual({
[M_BEACON_INFO.name]: {
description: 'nice beacon_info',
timeout: 1234,
live: true,
},
description: 'nice beacon_info',
timeout: 1234,
live: true,
[M_TIMESTAMP.name]: mockDateNow,
[M_ASSET.name]: {
type: LocationAssetType.Pin,
+91 -7
View File
@@ -33,6 +33,7 @@ import * as testUtils from "../test-utils/test-utils";
import { makeBeaconInfoContent } from "../../src/content-helpers";
import { M_BEACON_INFO } from "../../src/@types/beacon";
import { Room } from "../../src";
import { makeBeaconEvent } from "../test-utils/beacon";
jest.useFakeTimers();
@@ -815,6 +816,9 @@ describe("MatrixClient", function() {
},
addPendingEvent: jest.fn(),
updatePendingEvent: jest.fn(),
reEmitter: {
reEmit: jest.fn(),
},
};
beforeEach(() => {
@@ -981,7 +985,7 @@ describe("MatrixClient", function() {
expect(rootEvent.isThreadRoot).toBe(true);
const [roomEvents, threadEvents] = client.partitionThreadedEvents(room, [rootEvent]);
const [roomEvents, threadEvents] = room.partitionThreadedEvents([rootEvent]);
expect(roomEvents).toHaveLength(1);
expect(threadEvents).toHaveLength(1);
@@ -999,10 +1003,10 @@ describe("MatrixClient", function() {
});
it("creates new beacon info", async () => {
await client.unstable_createLiveBeacon(roomId, content, '123');
await client.unstable_createLiveBeacon(roomId, content);
// event type combined
const expectedEventType = `${M_BEACON_INFO.name}.${userId}.123`;
const expectedEventType = M_BEACON_INFO.name;
const [callback, method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
expect(callback).toBeFalsy();
expect(method).toBe('PUT');
@@ -1015,17 +1019,97 @@ describe("MatrixClient", function() {
});
it("updates beacon info with specific event type", async () => {
const eventType = `${M_BEACON_INFO.name}.${userId}.456`;
await client.unstable_setLiveBeacon(roomId, eventType, content);
await client.unstable_setLiveBeacon(roomId, content);
// event type combined
const [, , path, , requestContent] = client.http.authedRequest.mock.calls[0];
expect(path).toEqual(
`/rooms/${encodeURIComponent(roomId)}/state/` +
`${encodeURIComponent(eventType)}/${encodeURIComponent(userId)}`,
`${encodeURIComponent(M_BEACON_INFO.name)}/${encodeURIComponent(userId)}`,
);
expect(requestContent).toEqual(content);
});
describe('processBeaconEvents()', () => {
it('does nothing when events is falsy', () => {
const room = new Room(roomId, client, userId);
const roomStateProcessSpy = jest.spyOn(room.currentState, 'processBeaconEvents');
client.processBeaconEvents(room, undefined);
expect(roomStateProcessSpy).not.toHaveBeenCalled();
});
it('does nothing when events is of length 0', () => {
const room = new Room(roomId, client, userId);
const roomStateProcessSpy = jest.spyOn(room.currentState, 'processBeaconEvents');
client.processBeaconEvents(room, []);
expect(roomStateProcessSpy).not.toHaveBeenCalled();
});
it('calls room states processBeaconEvents with m.beacon events', () => {
const room = new Room(roomId, client, userId);
const roomStateProcessSpy = jest.spyOn(room.currentState, 'processBeaconEvents');
const messageEvent = testUtils.mkMessage({ room: roomId, user: userId, event: true });
const beaconEvent = makeBeaconEvent(userId);
client.processBeaconEvents(room, [messageEvent, beaconEvent]);
expect(roomStateProcessSpy).toHaveBeenCalledWith([beaconEvent]);
});
});
});
describe("setPassword", () => {
const auth = { session: 'abcdef', type: 'foo' };
const newPassword = 'newpassword';
const callback = () => {};
const passwordTest = (expectedRequestContent: any, expectedCallback?: Function) => {
const [callback, method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
if (expectedCallback) {
expect(callback).toBe(expectedCallback);
} else {
expect(callback).toBeFalsy();
}
expect(method).toBe('POST');
expect(path).toEqual('/account/password');
expect(queryParams).toBeFalsy();
expect(requestContent).toEqual(expectedRequestContent);
};
beforeEach(() => {
client.http.authedRequest.mockClear().mockResolvedValue({});
});
it("no logout_devices specified", async () => {
await client.setPassword(auth, newPassword);
passwordTest({ auth, new_password: newPassword });
});
it("no logout_devices specified + callback", async () => {
await client.setPassword(auth, newPassword, callback);
passwordTest({ auth, new_password: newPassword }, callback);
});
it("overload logoutDevices=true", async () => {
await client.setPassword(auth, newPassword, true);
passwordTest({ auth, new_password: newPassword, logout_devices: true });
});
it("overload logoutDevices=true + callback", async () => {
await client.setPassword(auth, newPassword, true, callback);
passwordTest({ auth, new_password: newPassword, logout_devices: true }, callback);
});
it("overload logoutDevices=false", async () => {
await client.setPassword(auth, newPassword, false);
passwordTest({ auth, new_password: newPassword, logout_devices: false });
});
it("overload logoutDevices=false + callback", async () => {
await client.setPassword(auth, newPassword, false, callback);
passwordTest({ auth, new_password: newPassword, logout_devices: false }, callback);
});
});
});
+123 -30
View File
@@ -14,15 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { EventType } from "../../../src";
import { M_BEACON_INFO } from "../../../src/@types/beacon";
import {
isTimestampInDuration,
isBeaconInfoEventType,
Beacon,
BeaconEvent,
} from "../../../src/models/beacon";
import { makeBeaconInfoEvent } from "../../test-utils/beacon";
import { makeBeaconEvent, makeBeaconInfoEvent } from "../../test-utils/beacon";
jest.useFakeTimers();
@@ -57,27 +54,9 @@ describe('Beacon', () => {
});
});
describe('isBeaconInfoEventType', () => {
it.each([
EventType.CallAnswer,
`prefix.${M_BEACON_INFO.name}`,
`prefix.${M_BEACON_INFO.altName}`,
])('returns false for %s', (type) => {
expect(isBeaconInfoEventType(type)).toBe(false);
});
it.each([
M_BEACON_INFO.name,
M_BEACON_INFO.altName,
`${M_BEACON_INFO.name}.@test:server.org.12345`,
`${M_BEACON_INFO.altName}.@test:server.org.12345`,
])('returns true for %s', (type) => {
expect(isBeaconInfoEventType(type)).toBe(true);
});
});
describe('Beacon', () => {
const userId = '@user:server.org';
const userId2 = '@user2:server.org';
const roomId = '$room:server.org';
// 14.03.2022 16:15
const now = 1647270879403;
@@ -88,6 +67,7 @@ describe('Beacon', () => {
// without timeout of 3 hours
let liveBeaconEvent;
let notLiveBeaconEvent;
let user2BeaconEvent;
const advanceDateAndTime = (ms: number) => {
// bc liveness check uses Date.now we have to advance this mock
@@ -107,14 +87,21 @@ describe('Beacon', () => {
isLive: true,
},
'$live123',
'$live123',
);
notLiveBeaconEvent = makeBeaconInfoEvent(
userId,
roomId,
{ timeout: HOUR_MS * 3, isLive: false },
'$dead123',
'$dead123',
);
user2BeaconEvent = makeBeaconInfoEvent(
userId2,
roomId,
{
timeout: HOUR_MS * 3,
isLive: true,
},
'$user2live123',
);
// back to now
@@ -133,7 +120,7 @@ describe('Beacon', () => {
expect(beacon.isLive).toEqual(true);
expect(beacon.beaconInfoOwner).toEqual(userId);
expect(beacon.beaconInfoEventType).toEqual(liveBeaconEvent.getType());
expect(beacon.identifier).toEqual(liveBeaconEvent.getType());
expect(beacon.identifier).toEqual(`${roomId}_${userId}`);
expect(beacon.beaconInfo).toBeTruthy();
});
@@ -171,8 +158,27 @@ describe('Beacon', () => {
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId());
expect(() => beacon.update(notLiveBeaconEvent)).toThrow();
expect(beacon.isLive).toEqual(true);
expect(() => beacon.update(user2BeaconEvent)).toThrow();
// didnt update
expect(beacon.identifier).toEqual(`${roomId}_${userId}`);
});
it('does not update with an older event', () => {
const beacon = new Beacon(liveBeaconEvent);
const emitSpy = jest.spyOn(beacon, 'emit').mockClear();
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId());
const oldUpdateEvent = makeBeaconInfoEvent(
userId,
roomId,
);
// less than the original event
oldUpdateEvent.event.origin_server_ts = liveBeaconEvent.event.origin_server_ts - 1000;
beacon.update(oldUpdateEvent);
// didnt update
expect(emitSpy).not.toHaveBeenCalled();
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId());
});
it('updates event', () => {
@@ -182,7 +188,7 @@ describe('Beacon', () => {
expect(beacon.isLive).toEqual(true);
const updatedBeaconEvent = makeBeaconInfoEvent(
userId, roomId, { timeout: HOUR_MS * 3, isLive: false }, '$live123', '$live123');
userId, roomId, { timeout: HOUR_MS * 3, isLive: false }, '$live123');
beacon.update(updatedBeaconEvent);
expect(beacon.isLive).toEqual(false);
@@ -200,7 +206,6 @@ describe('Beacon', () => {
roomId,
{ timeout: HOUR_MS * 3, isLive: false },
beacon.beaconInfoId,
'$live123',
);
beacon.update(updatedBeaconEvent);
@@ -277,5 +282,93 @@ describe('Beacon', () => {
expect(emitSpy).toHaveBeenCalledTimes(1);
});
});
describe('addLocations', () => {
it('ignores locations when beacon is not live', () => {
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: false }));
const emitSpy = jest.spyOn(beacon, 'emit');
beacon.addLocations([
makeBeaconEvent(userId, { beaconInfoId: beacon.beaconInfoId, timestamp: now + 1 }),
]);
expect(beacon.latestLocationState).toBeFalsy();
expect(emitSpy).not.toHaveBeenCalled();
});
it('ignores locations outside the beacon live duration', () => {
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
const emitSpy = jest.spyOn(beacon, 'emit');
beacon.addLocations([
// beacon has now + 60000 live period
makeBeaconEvent(userId, { beaconInfoId: beacon.beaconInfoId, timestamp: now + 100000 }),
]);
expect(beacon.latestLocationState).toBeFalsy();
expect(emitSpy).not.toHaveBeenCalled();
});
it('sets latest location state to most recent location', () => {
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
const emitSpy = jest.spyOn(beacon, 'emit');
const locations = [
// older
makeBeaconEvent(
userId, { beaconInfoId: beacon.beaconInfoId, uri: 'geo:foo', timestamp: now + 1 },
),
// newer
makeBeaconEvent(
userId, { beaconInfoId: beacon.beaconInfoId, uri: 'geo:bar', timestamp: now + 10000 },
),
// not valid
makeBeaconEvent(
userId, { beaconInfoId: beacon.beaconInfoId, uri: 'geo:baz', timestamp: now - 5 },
),
];
beacon.addLocations(locations);
const expectedLatestLocation = {
description: undefined,
timestamp: now + 10000,
uri: 'geo:bar',
};
// the newest valid location
expect(beacon.latestLocationState).toEqual(expectedLatestLocation);
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.LocationUpdate, expectedLatestLocation);
});
it('ignores locations that are less recent that the current latest location', () => {
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
const olderLocation = makeBeaconEvent(
userId, { beaconInfoId: beacon.beaconInfoId, uri: 'geo:foo', timestamp: now + 1 },
);
const newerLocation = makeBeaconEvent(
userId, { beaconInfoId: beacon.beaconInfoId, uri: 'geo:bar', timestamp: now + 10000 },
);
beacon.addLocations([newerLocation]);
// latest location set to newerLocation
expect(beacon.latestLocationState).toEqual(expect.objectContaining({
uri: 'geo:bar',
}));
const emitSpy = jest.spyOn(beacon, 'emit').mockClear();
// add older location
beacon.addLocations([olderLocation]);
// no change
expect(beacon.latestLocationState).toEqual(expect.objectContaining({
uri: 'geo:bar',
}));
// no emit
expect(emitSpy).not.toHaveBeenCalled();
});
});
});
});
+70 -17
View File
@@ -1,8 +1,8 @@
import * as utils from "../test-utils/test-utils";
import { makeBeaconInfoEvent } from "../test-utils/beacon";
import { makeBeaconEvent, makeBeaconInfoEvent } from "../test-utils/beacon";
import { filterEmitCallsByEventType } from "../test-utils/emitter";
import { RoomState, RoomStateEvent } from "../../src/models/room-state";
import { BeaconEvent } from "../../src/models/beacon";
import { BeaconEvent, getBeaconInfoIdentifier } from "../../src/models/beacon";
describe("RoomState", function() {
const roomId = "!foo:bar";
@@ -260,7 +260,7 @@ describe("RoomState", function() {
state.setStateEvents([beaconEvent]);
expect(state.beacons.size).toEqual(1);
const beaconInstance = state.beacons.get(beaconEvent.getType());
const beaconInstance = state.beacons.get(`${roomId}_${userA}`);
expect(beaconInstance).toBeTruthy();
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.New, beaconEvent, beaconInstance);
});
@@ -275,49 +275,49 @@ describe("RoomState", function() {
// no beacon added
expect(state.beacons.size).toEqual(0);
expect(state.beacons.get(redactedBeaconEvent.getType)).toBeFalsy();
expect(state.beacons.get(getBeaconInfoIdentifier(redactedBeaconEvent))).toBeFalsy();
// no new beacon emit
expect(filterEmitCallsByEventType(BeaconEvent.New, emitSpy).length).toBeFalsy();
});
it('updates existing beacon info events in state', () => {
const beaconId = '$beacon1';
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId, beaconId);
const updatedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: false }, beaconId, beaconId);
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
const updatedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: false }, beaconId);
state.setStateEvents([beaconEvent]);
const beaconInstance = state.beacons.get(beaconEvent.getType());
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
expect(beaconInstance.isLive).toEqual(true);
state.setStateEvents([updatedBeaconEvent]);
// same Beacon
expect(state.beacons.get(beaconEvent.getType())).toBe(beaconInstance);
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))).toBe(beaconInstance);
// updated liveness
expect(state.beacons.get(beaconEvent.getType()).isLive).toEqual(false);
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent)).isLive).toEqual(false);
});
it('destroys and removes redacted beacon events', () => {
const beaconId = '$beacon1';
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId, beaconId);
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId, beaconId);
const redactionEvent = { event: { type: 'm.room.redaction' } };
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
const redactionEvent = { event: { type: 'm.room.redaction', redacts: beaconEvent.getId() } };
redactedBeaconEvent.makeRedacted(redactionEvent);
state.setStateEvents([beaconEvent]);
const beaconInstance = state.beacons.get(beaconEvent.getType());
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
const destroySpy = jest.spyOn(beaconInstance, 'destroy');
expect(beaconInstance.isLive).toEqual(true);
state.setStateEvents([redactedBeaconEvent]);
expect(destroySpy).toHaveBeenCalled();
expect(state.beacons.get(beaconEvent.getType())).toBe(undefined);
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))).toBe(undefined);
});
it('updates live beacon ids once after setting state events', () => {
const liveBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, '$beacon1', '$beacon1');
const deadBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: false }, '$beacon2', '$beacon2');
const liveBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, '$beacon1');
const deadBeaconEvent = makeBeaconInfoEvent(userB, roomId, { isLive: false }, '$beacon2');
const emitSpy = jest.spyOn(state, 'emit');
@@ -334,7 +334,7 @@ describe("RoomState", function() {
state.setStateEvents([updatedLiveBeaconEvent]);
expect(state.hasLiveBeacons).toBe(false);
expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(2);
expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(3);
expect(emitSpy).toHaveBeenCalledWith(RoomStateEvent.BeaconLiveness, state, false);
});
});
@@ -712,4 +712,57 @@ describe("RoomState", function() {
expect(state.maySendEvent('m.room.other_thing', userB)).toEqual(false);
});
});
describe('processBeaconEvents', () => {
const beacon1 = makeBeaconInfoEvent(userA, roomId, {}, '$beacon1', '$beacon1');
const beacon2 = makeBeaconInfoEvent(userB, roomId, {}, '$beacon2', '$beacon2');
it('does nothing when state has no beacons', () => {
const emitSpy = jest.spyOn(state, 'emit');
state.processBeaconEvents([makeBeaconEvent(userA, { beaconInfoId: '$beacon1' })]);
expect(emitSpy).not.toHaveBeenCalled();
});
it('does nothing when there are no events', () => {
state.setStateEvents([beacon1, beacon2]);
const emitSpy = jest.spyOn(state, 'emit').mockClear();
state.processBeaconEvents([]);
expect(emitSpy).not.toHaveBeenCalled();
});
it('discards events for beacons that are not in state', () => {
const location = makeBeaconEvent(userA, {
beaconInfoId: 'some-other-beacon',
});
state.setStateEvents([beacon1, beacon2]);
const emitSpy = jest.spyOn(state, 'emit').mockClear();
state.processBeaconEvents([location]);
expect(emitSpy).not.toHaveBeenCalled();
});
it('adds locations to beacons', () => {
const location1 = makeBeaconEvent(userA, {
beaconInfoId: '$beacon1', timestamp: Date.now() + 1,
});
const location2 = makeBeaconEvent(userA, {
beaconInfoId: '$beacon1', timestamp: Date.now() + 2,
});
const location3 = makeBeaconEvent(userB, {
beaconInfoId: 'some-other-beacon',
});
state.setStateEvents([beacon1, beacon2]);
expect(state.beacons.size).toEqual(2);
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beacon1));
const addLocationsSpy = jest.spyOn(beaconInstance, 'addLocations');
state.processBeaconEvents([location1, location2, location3]);
expect(addLocationsSpy).toHaveBeenCalledTimes(1);
// only called with locations for beacon1
expect(addLocationsSpy).toHaveBeenCalledWith([location1, location2]);
});
});
});
+303 -133
View File
@@ -35,6 +35,8 @@ import { Room } from "../../src/models/room";
import { RoomState } from "../../src/models/room-state";
import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
import { TestClient } from "../TestClient";
import { emitPromise } from "../test-utils/test-utils";
import { ThreadEvent } from "../../src/models/thread";
describe("Room", function() {
const roomId = "!foo:bar";
@@ -44,8 +46,86 @@ describe("Room", function() {
const userD = "@dorothy:bar";
let room;
const mkMessage = () => utils.mkMessage({
event: true,
user: userA,
room: roomId,
}, room.client) as MatrixEvent;
const mkReply = (target: MatrixEvent) => utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: userA,
room: roomId,
content: {
"body": "Reply :: " + Math.random(),
"m.relates_to": {
"m.in_reply_to": {
"event_id": target.getId(),
},
},
},
}, room.client) as MatrixEvent;
const mkEdit = (target: MatrixEvent, salt = Math.random()) => utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: userA,
room: roomId,
content: {
"body": "* Edit of :: " + target.getId() + " :: " + salt,
"m.new_content": {
body: "Edit of :: " + target.getId() + " :: " + salt,
},
"m.relates_to": {
rel_type: RelationType.Replace,
event_id: target.getId(),
},
},
}, room.client) as MatrixEvent;
const mkThreadResponse = (root: MatrixEvent) => utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: userA,
room: roomId,
content: {
"body": "Thread response :: " + Math.random(),
"m.relates_to": {
"event_id": root.getId(),
"m.in_reply_to": {
"event_id": root.getId(),
},
"rel_type": "m.thread",
},
},
}, room.client) as MatrixEvent;
const mkReaction = (target: MatrixEvent) => utils.mkEvent({
event: true,
type: EventType.Reaction,
user: userA,
room: roomId,
content: {
"m.relates_to": {
"rel_type": RelationType.Annotation,
"event_id": target.getId(),
"key": Math.random().toString(),
},
},
}, room.client) as MatrixEvent;
const mkRedaction = (target: MatrixEvent) => utils.mkEvent({
event: true,
type: EventType.RoomRedaction,
user: userA,
room: roomId,
redacts: target.getId(),
content: {},
}, room.client) as MatrixEvent;
beforeEach(function() {
room = new Room(roomId, null, userA);
room = new Room(roomId, new TestClient(userA, "device").client, userA);
// mock RoomStates
room.oldState = room.getLiveTimeline().startState = utils.mock(RoomState, "oldState");
room.currentState = room.getLiveTimeline().endState = utils.mock(RoomState, "currentState");
@@ -157,19 +237,18 @@ describe("Room", function() {
expect(room.timeline[0]).toEqual(events[0]);
});
it("should emit 'Room.timeline' events",
function() {
let callCount = 0;
room.on("Room.timeline", function(event, emitRoom, toStart) {
callCount += 1;
expect(room.timeline.length).toEqual(callCount);
expect(event).toEqual(events[callCount - 1]);
expect(emitRoom).toEqual(room);
expect(toStart).toBeFalsy();
});
room.addLiveEvents(events);
expect(callCount).toEqual(2);
it("should emit 'Room.timeline' events", function() {
let callCount = 0;
room.on("Room.timeline", function(event, emitRoom, toStart) {
callCount += 1;
expect(room.timeline.length).toEqual(callCount);
expect(event).toEqual(events[callCount - 1]);
expect(emitRoom).toEqual(room);
expect(toStart).toBeFalsy();
});
room.addLiveEvents(events);
expect(callCount).toEqual(2);
});
it("should call setStateEvents on the right RoomState with the right forwardLooking value for new events",
function() {
@@ -338,42 +417,41 @@ describe("Room", function() {
expect(oldEv.sender).toEqual(oldSentinel);
});
it("should set event.target for new and old m.room.member events",
function() {
const sentinel = {
userId: userA,
membership: "join",
name: "Alice",
};
const oldSentinel = {
userId: userA,
membership: "join",
name: "Old Alice",
};
room.currentState.getSentinelMember.mockImplementation(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
room.oldState.getSentinelMember.mockImplementation(function(uid) {
if (uid === userA) {
return oldSentinel;
}
return null;
});
const newEv = utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true,
}) as MatrixEvent;
const oldEv = utils.mkMembership({
room: roomId, mship: "ban", user: userB, skey: userA, event: true,
}) as MatrixEvent;
room.addLiveEvents([newEv]);
expect(newEv.target).toEqual(sentinel);
room.addEventsToTimeline([oldEv], true, room.getLiveTimeline());
expect(oldEv.target).toEqual(oldSentinel);
it("should set event.target for new and old m.room.member events", function() {
const sentinel = {
userId: userA,
membership: "join",
name: "Alice",
};
const oldSentinel = {
userId: userA,
membership: "join",
name: "Old Alice",
};
room.currentState.getSentinelMember.mockImplementation(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
room.oldState.getSentinelMember.mockImplementation(function(uid) {
if (uid === userA) {
return oldSentinel;
}
return null;
});
const newEv = utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true,
}) as MatrixEvent;
const oldEv = utils.mkMembership({
room: roomId, mship: "ban", user: userB, skey: userA, event: true,
}) as MatrixEvent;
room.addLiveEvents([newEv]);
expect(newEv.target).toEqual(sentinel);
room.addEventsToTimeline([oldEv], true, room.getLiveTimeline());
expect(oldEv.target).toEqual(oldSentinel);
});
it("should call setStateEvents on the right RoomState with the right " +
"forwardLooking value for old events", function() {
@@ -406,7 +484,7 @@ describe("Room", function() {
let events = null;
beforeEach(function() {
room = new Room(roomId, null, null, { timelineSupport: timelineSupport });
room = new Room(roomId, new TestClient(userA).client, userA, { timelineSupport: timelineSupport });
// set events each time to avoid resusing Event objects (which
// doesn't work because they get frozen)
events = [
@@ -469,8 +547,7 @@ describe("Room", function() {
expect(callCount).toEqual(1);
});
it("should " + (timelineSupport ? "remember" : "forget") +
" old timelines", function() {
it("should " + (timelineSupport ? "remember" : "forget") + " old timelines", function() {
room.addLiveEvents([events[0]]);
expect(room.timeline.length).toEqual(1);
const firstLiveTimeline = room.getLiveTimeline();
@@ -486,7 +563,7 @@ describe("Room", function() {
describe("compareEventOrdering", function() {
beforeEach(function() {
room = new Room(roomId, null, null, { timelineSupport: true });
room = new Room(roomId, new TestClient(userA).client, userA, { timelineSupport: true });
});
const events: MatrixEvent[] = [
@@ -673,7 +750,7 @@ describe("Room", function() {
beforeEach(function() {
// no mocking
room = new Room(roomId, null, userA);
room = new Room(roomId, new TestClient(userA).client, userA);
});
describe("Room.recalculate => Stripped State Events", function() {
@@ -1259,6 +1336,7 @@ describe("Room", function() {
const client = (new TestClient(
"@alice:example.com", "alicedevice",
)).client;
client.supportsExperimentalThreads = () => true;
const room = new Room(roomId, client, userA, {
pendingEventOrdering: PendingEventOrdering.Detached,
});
@@ -1285,7 +1363,7 @@ describe("Room", function() {
it("should add pending events to the timeline if " +
"pendingEventOrdering == 'chronological'", function() {
room = new Room(roomId, null, userA, {
const room = new Room(roomId, new TestClient(userA).client, userA, {
pendingEventOrdering: PendingEventOrdering.Chronological,
});
const eventA = utils.mkMessage({
@@ -1504,7 +1582,7 @@ describe("Room", function() {
describe("guessDMUserId", function() {
it("should return first hero id", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
room.setSummary({
'm.heroes': [userB],
'm.joined_member_count': 1,
@@ -1513,7 +1591,7 @@ describe("Room", function() {
expect(room.guessDMUserId()).toEqual(userB);
});
it("should return first member that isn't self", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([utils.mkMembership({
user: userB,
mship: "join",
@@ -1523,7 +1601,7 @@ describe("Room", function() {
expect(room.guessDMUserId()).toEqual(userB);
});
it("should return self if only member present", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
expect(room.guessDMUserId()).toEqual(userA);
});
});
@@ -1542,12 +1620,12 @@ describe("Room", function() {
describe("getDefaultRoomName", function() {
it("should return 'Empty room' if a user is the only member", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
expect(room.getDefaultRoomName(userA)).toEqual("Empty room");
});
it("should return a display name if one other member is in the room", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
@@ -1562,7 +1640,7 @@ describe("Room", function() {
});
it("should return a display name if one other member is banned", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
@@ -1577,7 +1655,7 @@ describe("Room", function() {
});
it("should return a display name if one other member is invited", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
@@ -1592,7 +1670,7 @@ describe("Room", function() {
});
it("should return 'Empty room (was User B)' if User B left the room", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
@@ -1607,7 +1685,7 @@ describe("Room", function() {
});
it("should return 'User B and User C' if in a room with two other users", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
@@ -1626,7 +1704,7 @@ describe("Room", function() {
});
it("should return 'User B and 2 others' if in a room with three other users", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
@@ -1651,7 +1729,7 @@ describe("Room", function() {
describe("io.element.functional_users", function() {
it("should return a display name (default behaviour) if no one is marked as a functional member", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
@@ -1673,7 +1751,7 @@ describe("Room", function() {
});
it("should return a display name (default behaviour) if service members is a number (invalid)", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
@@ -1697,7 +1775,7 @@ describe("Room", function() {
});
it("should return a display name (default behaviour) if service members is a string (invalid)", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
@@ -1719,7 +1797,7 @@ describe("Room", function() {
});
it("should return 'Empty room' if the only other member is a functional member", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
@@ -1741,7 +1819,7 @@ describe("Room", function() {
});
it("should return 'User B' if User B is the only other member who isn't a functional member", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
@@ -1767,7 +1845,7 @@ describe("Room", function() {
});
it("should return 'Empty room' if all other members are functional members", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
@@ -1793,7 +1871,7 @@ describe("Room", function() {
});
it("should not break if an unjoined user is marked as a service user", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
@@ -1821,6 +1899,7 @@ describe("Room", function() {
"@alice:example.com", "alicedevice",
)).client;
room = new Room(roomId, client, userA);
client.getRoom = () => room;
});
it("allow create threads without a root event", function() {
@@ -1858,71 +1937,162 @@ describe("Room", function() {
expect(() => room.createThread(rootEvent, [])).not.toThrow();
});
it("Edits update the lastReply event", async () => {
room.client.supportsExperimentalThreads = () => true;
const randomMessage = mkMessage();
const threadRoot = mkMessage();
const threadResponse = mkThreadResponse(threadRoot);
threadResponse.localTimestamp += 1000;
const threadResponseEdit = mkEdit(threadResponse);
threadResponseEdit.localTimestamp += 2000;
room.client.fetchRoomEvent = (eventId: string) => Promise.resolve({
...threadRoot.event,
unsigned: {
"age": 123,
"m.relations": {
"m.thread": {
latest_event: threadResponse.event,
count: 2,
current_user_participated: true,
},
},
},
});
room.addLiveEvents([randomMessage, threadRoot, threadResponse]);
const thread = await emitPromise(room, ThreadEvent.New);
expect(thread.replyToEvent).toBe(threadResponse);
expect(thread.replyToEvent.getContent().body).toBe(threadResponse.getContent().body);
room.addLiveEvents([threadResponseEdit]);
await emitPromise(thread, ThreadEvent.Update);
expect(thread.replyToEvent.getContent().body).toBe(threadResponseEdit.getContent()["m.new_content"].body);
});
it("Redactions to thread responses decrement the length", async () => {
room.client.supportsExperimentalThreads = () => true;
const threadRoot = mkMessage();
const threadResponse1 = mkThreadResponse(threadRoot);
threadResponse1.localTimestamp += 1000;
const threadResponse2 = mkThreadResponse(threadRoot);
threadResponse2.localTimestamp += 2000;
room.client.fetchRoomEvent = (eventId: string) => Promise.resolve({
...threadRoot.event,
unsigned: {
"age": 123,
"m.relations": {
"m.thread": {
latest_event: threadResponse2.event,
count: 2,
current_user_participated: true,
},
},
},
});
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2]);
const thread = await emitPromise(room, ThreadEvent.New);
expect(thread).toHaveLength(2);
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
const threadResponse1Redaction = mkRedaction(threadResponse1);
room.addLiveEvents([threadResponse1Redaction]);
await emitPromise(thread, ThreadEvent.Update);
expect(thread).toHaveLength(1);
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
});
it("Redactions to reactions in threads do not decrement the length", async () => {
room.client.supportsExperimentalThreads = () => true;
const threadRoot = mkMessage();
const threadResponse1 = mkThreadResponse(threadRoot);
threadResponse1.localTimestamp += 1000;
const threadResponse2 = mkThreadResponse(threadRoot);
threadResponse2.localTimestamp += 2000;
const threadResponse2Reaction = mkReaction(threadResponse2);
room.client.fetchRoomEvent = (eventId: string) => Promise.resolve({
...threadRoot.event,
unsigned: {
"age": 123,
"m.relations": {
"m.thread": {
latest_event: threadResponse2.event,
count: 2,
current_user_participated: true,
},
},
},
});
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]);
const thread = await emitPromise(room, ThreadEvent.New);
expect(thread).toHaveLength(2);
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
const threadResponse2ReactionRedaction = mkRedaction(threadResponse2Reaction);
room.addLiveEvents([threadResponse2ReactionRedaction]);
await emitPromise(thread, ThreadEvent.Update);
expect(thread).toHaveLength(2);
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
});
it("Redacting the lastEvent finds a new lastEvent", async () => {
room.client.supportsExperimentalThreads = () => true;
const threadRoot = mkMessage();
const threadResponse1 = mkThreadResponse(threadRoot);
threadResponse1.localTimestamp += 1000;
const threadResponse2 = mkThreadResponse(threadRoot);
threadResponse2.localTimestamp += 2000;
room.client.fetchRoomEvent = (eventId: string) => Promise.resolve({
...threadRoot.event,
unsigned: {
"age": 123,
"m.relations": {
"m.thread": {
latest_event: threadResponse2.event,
count: 2,
current_user_participated: true,
},
},
},
});
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2]);
const thread = await emitPromise(room, ThreadEvent.New);
expect(thread).toHaveLength(2);
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
const threadResponse2Redaction = mkRedaction(threadResponse2);
room.addLiveEvents([threadResponse2Redaction]);
await emitPromise(thread, ThreadEvent.Update);
expect(thread).toHaveLength(1);
expect(thread.replyToEvent.getId()).toBe(threadResponse1.getId());
const threadResponse1Redaction = mkRedaction(threadResponse1);
room.addLiveEvents([threadResponse1Redaction]);
await emitPromise(thread, ThreadEvent.Update);
expect(thread).toHaveLength(0);
expect(thread.replyToEvent.getId()).toBe(threadRoot.getId());
});
});
describe("eventShouldLiveIn", () => {
const room = new Room(roomId, null, userA);
const mkMessage = () => utils.mkMessage({
event: true,
user: userA,
room: roomId,
}) as MatrixEvent;
const mkReply = (target: MatrixEvent) => utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: userA,
room: roomId,
content: {
"body": "Reply :: " + Math.random(),
"m.relates_to": {
"m.in_reply_to": {
"event_id": target.getId(),
},
},
},
}) as MatrixEvent;
const mkThreadResponse = (root: MatrixEvent) => utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: userA,
room: roomId,
content: {
"body": "Thread response :: " + Math.random(),
"m.relates_to": {
"event_id": root.getId(),
"m.in_reply_to": {
"event_id": root.getId(),
},
"rel_type": "m.thread",
},
},
}) as MatrixEvent;
const mkReaction = (target: MatrixEvent) => utils.mkEvent({
event: true,
type: EventType.Reaction,
user: userA,
room: roomId,
content: {
"m.relates_to": {
"rel_type": RelationType.Annotation,
"event_id": target.getId(),
"key": Math.random().toString(),
},
},
}) as MatrixEvent;
const mkRedaction = (target: MatrixEvent) => utils.mkEvent({
event: true,
type: EventType.RoomRedaction,
user: userA,
room: roomId,
redacts: target.getId(),
content: {},
}) as MatrixEvent;
const client = new TestClient(userA).client;
client.supportsExperimentalThreads = () => true;
const room = new Room(roomId, client, userA);
it("thread root and its relations&redactions should be in both", () => {
const randomMessage = mkMessage();
+29
View File
@@ -10,8 +10,11 @@ import {
prevString,
simpleRetryOperation,
stringToBase,
sortEventsByLatestContentTimestamp,
} from "../../src/utils";
import { logger } from "../../src/logger";
import { mkMessage } from "../test-utils/test-utils";
import { makeBeaconEvent } from "../test-utils/beacon";
// TODO: Fix types throughout
@@ -506,4 +509,30 @@ describe("utils", function() {
});
});
});
describe('sortEventsByLatestContentTimestamp', () => {
const roomId = '!room:server';
const userId = '@user:server';
const eventWithoutContentTimestamp = mkMessage({ room: roomId, user: userId, event: true });
// m.beacon events have timestamp in content
const beaconEvent1 = makeBeaconEvent(userId, { timestamp: 1648804528557 });
const beaconEvent2 = makeBeaconEvent(userId, { timestamp: 1648804528558 });
const beaconEvent3 = makeBeaconEvent(userId, { timestamp: 1648804528000 });
const beaconEvent4 = makeBeaconEvent(userId, { timestamp: 0 });
it('sorts events with timestamps as later than events without', () => {
expect(
[beaconEvent4, eventWithoutContentTimestamp, beaconEvent1]
.sort(utils.sortEventsByLatestContentTimestamp),
).toEqual([
beaconEvent1, beaconEvent4, eventWithoutContentTimestamp,
]);
});
it('sorts by content timestamps correctly', () => {
expect(
[beaconEvent1, beaconEvent2, beaconEvent3].sort(sortEventsByLatestContentTimestamp),
).toEqual([beaconEvent2, beaconEvent1, beaconEvent3]);
});
});
});
+9 -19
View File
@@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { EitherAnd, RELATES_TO_RELATIONSHIP, REFERENCE_RELATION } from "matrix-events-sdk";
import { RELATES_TO_RELATIONSHIP, REFERENCE_RELATION } from "matrix-events-sdk";
import { UnstableValue } from "../NamespacedValue";
import { MAssetEvent, MLocationEvent, MTimestampEvent } from "./location";
/**
* Beacon info and beacon event types as described in MSC3489
* https://github.com/matrix-org/matrix-spec-proposals/pull/3489
* Beacon info and beacon event types as described in MSC3672
* https://github.com/matrix-org/matrix-spec-proposals/pull/3672
*/
/**
@@ -60,16 +60,11 @@ import { MAssetEvent, MLocationEvent, MTimestampEvent } from "./location";
* }
*/
/**
* Variable event type for m.beacon_info
*/
export const M_BEACON_INFO_VARIABLE = new UnstableValue("m.beacon_info.*", "org.matrix.msc3489.beacon_info.*");
/**
* Non-variable type for m.beacon_info event content
*/
export const M_BEACON_INFO = new UnstableValue("m.beacon_info", "org.matrix.msc3489.beacon_info");
export const M_BEACON = new UnstableValue("m.beacon", "org.matrix.msc3489.beacon");
export const M_BEACON_INFO = new UnstableValue("m.beacon_info", "org.matrix.msc3672.beacon_info");
export const M_BEACON = new UnstableValue("m.beacon", "org.matrix.msc3672.beacon");
export type MBeaconInfoContent = {
description?: string;
@@ -80,16 +75,11 @@ export type MBeaconInfoContent = {
live?: boolean;
};
export type MBeaconInfoEvent = EitherAnd<
{ [M_BEACON_INFO.name]: MBeaconInfoContent },
{ [M_BEACON_INFO.altName]: MBeaconInfoContent }
>;
/**
* m.beacon_info Event example from the spec
* https://github.com/matrix-org/matrix-spec-proposals/pull/3489
* https://github.com/matrix-org/matrix-spec-proposals/pull/3672
* {
"type": "m.beacon_info.@matthew:matrix.org.1",
"type": "m.beacon_info",
"state_key": "@matthew:matrix.org",
"content": {
"m.beacon_info": {
@@ -108,7 +98,7 @@ export type MBeaconInfoEvent = EitherAnd<
* m.beacon_info.* event content
*/
export type MBeaconInfoEventContent = &
MBeaconInfoEvent &
MBeaconInfoContent &
// creation timestamp of the beacon on the client
MTimestampEvent &
// the type of asset being tracked as per MSC3488
@@ -116,7 +106,7 @@ export type MBeaconInfoEventContent = &
/**
* m.beacon event example
* https://github.com/matrix-org/matrix-spec-proposals/pull/3489
* https://github.com/matrix-org/matrix-spec-proposals/pull/3672
*
* {
"type": "m.beacon",
+89 -87
View File
@@ -180,7 +180,7 @@ import { MediaHandler } from "./webrtc/mediaHandler";
import { IRefreshTokenResponse } from "./@types/auth";
import { TypedEventEmitter } from "./models/typed-event-emitter";
import { Thread, THREAD_RELATION_TYPE } from "./models/thread";
import { MBeaconInfoEventContent, M_BEACON_INFO_VARIABLE } from "./@types/beacon";
import { MBeaconInfoEventContent, M_BEACON, M_BEACON_INFO } from "./@types/beacon";
export type Store = IStore;
export type SessionStore = WebStorageSessionStore;
@@ -3649,39 +3649,30 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* Create an m.beacon_info event
* @param {string} roomId
* @param {MBeaconInfoEventContent} beaconInfoContent
* @param {string} eventTypeSuffix - string to suffix event type
* to make event type unique.
* See MSC3489 for more context
* https://github.com/matrix-org/matrix-spec-proposals/pull/3489
* @returns {ISendEventResponse}
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
public async unstable_createLiveBeacon(
roomId: Room["roomId"],
beaconInfoContent: MBeaconInfoEventContent,
eventTypeSuffix: string,
) {
const userId = this.getUserId();
const eventType = M_BEACON_INFO_VARIABLE.name.replace('*', `${userId}.${eventTypeSuffix}`);
return this.unstable_setLiveBeacon(roomId, eventType, beaconInfoContent);
return this.unstable_setLiveBeacon(roomId, beaconInfoContent);
}
/**
* Upsert a live beacon event
* using a specific m.beacon_info.* event variable type
* @param {string} roomId string
* @param {string} beaconInfoEventType event type including variable suffix
* @param {MBeaconInfoEventContent} beaconInfoContent
* @returns {ISendEventResponse}
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
public async unstable_setLiveBeacon(
roomId: string,
beaconInfoEventType: string,
beaconInfoContent: MBeaconInfoEventContent,
) {
const userId = this.getUserId();
return this.sendStateEvent(roomId, beaconInfoEventType, beaconInfoContent, userId);
return this.sendStateEvent(roomId, M_BEACON_INFO.name, beaconInfoContent, userId);
}
/**
@@ -3771,9 +3762,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
txnId = this.makeTxnId();
}
// we always construct a MatrixEvent when sending because the store and
// scheduler use them. We'll extract the params back out if it turns out
// the client has no scheduler or store.
// We always construct a MatrixEvent when sending because the store and scheduler use them.
// We'll extract the params back out if it turns out the client has no scheduler or store.
const localEvent = new MatrixEvent(Object.assign(eventObject, {
event_id: "~" + roomId + ":" + txnId,
user_id: this.credentials.userId,
@@ -3786,9 +3776,17 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const thread = room?.threads.get(threadId);
if (thread) {
localEvent.setThread(thread);
localEvent.setThreadId(thread.id);
}
// set up re-emitter for this new event - this is normally the job of EventMapper but we don't use it here
this.reEmitter.reEmit(localEvent, [
MatrixEventEvent.Replaced,
MatrixEventEvent.VisibilityChange,
]);
room?.reEmitter.reEmit(localEvent, [
MatrixEventEvent.BeforeRedaction,
]);
// 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 ~)
// then listen for the remote echo of that event so that by the time
@@ -3808,9 +3806,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
localEvent.setStatus(EventStatus.SENDING);
// add this event immediately to the local store as 'sending'.
if (room) {
room.addPendingEvent(localEvent, txnId);
}
room?.addPendingEvent(localEvent, txnId);
// addPendingEvent can change the state to NOT_SENT if it believes
// that there's other events that have failed. We won't bother to
@@ -5179,8 +5175,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
room.currentState.setUnknownStateEvents(stateEvents);
}
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(room, matrixEvents);
const [timelineEvents, threadedEvents] = room.partitionThreadedEvents(matrixEvents);
this.processBeaconEvents(room, matrixEvents);
room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline());
await this.processThreadEvents(room, threadedEvents, true);
@@ -5280,8 +5277,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// Where the event is a thread reply (not a root) and running in MSC-enabled mode the Thread timeline only
// functions contiguously, so we have to jump through some hoops to get our target event in it.
// XXX: workaround for https://github.com/vector-im/element-meta/issues/150
if (Thread.hasServerSideSupport && event.isRelation(THREAD_RELATION_TYPE.name)) {
const [, threadedEvents] = this.partitionThreadedEvents(timelineSet.room, events);
if (Thread.hasServerSideSupport &&
this.supportsExperimentalThreads() &&
event.isRelation(THREAD_RELATION_TYPE.name)
) {
const [, threadedEvents] = timelineSet.room.partitionThreadedEvents(events);
const thread = await timelineSet.room.createThreadFetchRoot(event.threadRootId, threadedEvents, true);
let nextBatch: string;
@@ -5316,10 +5316,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
timeline.getState(EventTimeline.FORWARDS).paginationToken = res.end;
}
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(timelineSet.room, events);
const [timelineEvents, threadedEvents] = timelineSet.room.partitionThreadedEvents(events);
timelineSet.addEventsToTimeline(timelineEvents, true, timeline, res.start);
// The target event is not in a thread but process the contextual events, so we can show any threads around it.
await this.processThreadEvents(timelineSet.room, threadedEvents, true);
this.processBeaconEvents(timelineSet.room, events);
// There is no guarantee that the event ended up in "timeline" (we might have switched to a neighbouring
// timeline) - so check the room's index again. On the other hand, there's no guarantee the event ended up
@@ -5446,10 +5447,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
matrixEvents[i] = event;
}
// No need to partition events for threads here, everything lives
// in the notification timeline set
const timelineSet = eventTimeline.getTimelineSet();
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(timelineSet.room, matrixEvents);
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
await this.processThreadEvents(timelineSet.room, threadedEvents, backwards);
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
this.processBeaconEvents(timelineSet.room, matrixEvents);
// if we've hit the end of the timeline, we need to stop trying to
// paginate. We need to keep the 'forwards' token though, to make sure
@@ -5484,8 +5486,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const matrixEvents = res.chunk.map(this.getEventMapper());
const timelineSet = eventTimeline.getTimelineSet();
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(timelineSet.room, matrixEvents);
const [timelineEvents, threadedEvents] = timelineSet.room.partitionThreadedEvents(matrixEvents);
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
this.processBeaconEvents(timelineSet.room, matrixEvents);
await this.processThreadEvents(room, threadedEvents, backwards);
// if we've hit the end of the timeline, we need to stop trying to
@@ -6409,12 +6412,18 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public async _unstable_getSharedRooms(userId: string): Promise<string[]> { // eslint-disable-line
if (!(await this.doesServerSupportUnstableFeature("uk.half-shot.msc2666"))) {
throw Error('Server does not support shared_rooms API');
const sharedRoomsSupport = await this.doesServerSupportUnstableFeature("uk.half-shot.msc2666");
const mutualRoomsSupport = await this.doesServerSupportUnstableFeature("uk.half-shot.msc2666.mutual_rooms");
if (!sharedRoomsSupport && !mutualRoomsSupport) {
throw Error('Server does not support mutual_rooms API');
}
const path = utils.encodeUri("/uk.half-shot.msc2666/user/shared_rooms/$userId", {
$userId: userId,
});
const path = utils.encodeUri(
`/uk.half-shot.msc2666/user/${mutualRoomsSupport ? 'mutual_rooms' : 'shared_rooms'}/$userId`,
{ $userId: userId },
);
const res = await this.http.authedRequest<{ joined: string[] }>(
undefined, Method.Get, path, undefined, undefined,
{ prefix: PREFIX_UNSTABLE },
@@ -6584,6 +6593,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
}
/**
* Query the server to see if it supports the MSC2457 `logout_devices` parameter when setting password
* @return {Promise<boolean>} true if server supports the `logout_devices` parameter
*/
public doesServerSupportLogoutDevices(): Promise<boolean> {
return this.isVersionSupported("r0.6.1");
}
/**
* Get if lazy loading members is being used.
* @return {boolean} Whether or not members are lazy loaded by this client
@@ -7827,18 +7844,45 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* Make a request to change your password.
* @param {Object} authDict
* @param {string} newPassword The new desired password.
* @param {boolean} logoutDevices Should all sessions be logged out after the password change. Defaults to true.
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public setPassword(authDict: any, newPassword: string, callback?: Callback): Promise<any> { // TODO: Types
public setPassword(
authDict: any,
newPassword: string,
callback?: Callback,
): Promise<{}>;
public setPassword(
authDict: any,
newPassword: string,
logoutDevices: boolean,
callback?: Callback,
): Promise<{}>;
public setPassword(
authDict: any,
newPassword: string,
logoutDevices?: Callback | boolean,
callback?: Callback,
): Promise<{}> {
if (typeof logoutDevices === 'function') {
callback = logoutDevices;
}
if (typeof logoutDevices !== 'boolean') {
// Use backwards compatible behaviour of not specifying logout_devices
// This way it is left up to the server:
logoutDevices = undefined;
}
const path = "/account/password";
const data = {
'auth': authDict,
'new_password': newPassword,
'logout_devices': logoutDevices,
};
return this.http.authedRequest(
return this.http.authedRequest<{}>(
callback, Method.Post, path, null, data,
);
}
@@ -8852,57 +8896,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
});
}
/**
* Given some events, find the IDs of all the thread roots that are
* referred to by them.
*/
private findThreadRoots(events: MatrixEvent[]): Set<string> {
const threadRoots = new Set<string>();
for (const event of events) {
if (event.isThreadRelation) {
threadRoots.add(event.relationEventId);
}
}
return threadRoots;
}
public partitionThreadedEvents(room: Room, events: MatrixEvent[]): [
timelineEvents: MatrixEvent[],
threadedEvents: MatrixEvent[],
] {
// Indices to the events array, for readability
const ROOM = 0;
const THREAD = 1;
if (this.supportsExperimentalThreads()) {
const threadRoots = this.findThreadRoots(events);
return events.reduce((memo, event: MatrixEvent) => {
const {
shouldLiveInRoom,
shouldLiveInThread,
threadId,
} = room.eventShouldLiveIn(event, events, threadRoots);
if (shouldLiveInRoom) {
memo[ROOM].push(event);
}
if (shouldLiveInThread) {
event.setThreadId(threadId);
memo[THREAD].push(event);
}
return memo;
}, [[], []]);
} else {
// When `experimentalThreadSupport` is disabled
// treat all events as timelineEvents
return [
events,
[],
];
}
}
/**
* @experimental
*/
@@ -8911,9 +8904,18 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
threadedEvents: MatrixEvent[],
toStartOfTimeline: boolean,
): Promise<void> {
for (const event of threadedEvents) {
await room.addThreadedEvent(event, toStartOfTimeline);
await room.processThreadedEvents(threadedEvents, toStartOfTimeline);
}
public processBeaconEvents(
room: Room,
events?: MatrixEvent[],
): void {
if (!events?.length) {
return;
}
const beaconEvents = events.filter(event => M_BEACON.matches(event.getType()));
room.currentState.processBeaconEvents(beaconEvents);
}
/**
+23 -10
View File
@@ -18,7 +18,7 @@ limitations under the License.
import { REFERENCE_RELATION } from "matrix-events-sdk";
import { MBeaconEventContent, MBeaconInfoContent, MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon";
import { MBeaconEventContent, MBeaconInfoContent, MBeaconInfoEventContent } from "./@types/beacon";
import { MsgType } from "./@types/event";
import { TEXT_NODE_TYPE } from "./@types/extensible_events";
import {
@@ -208,11 +208,9 @@ export const makeBeaconInfoContent: MakeBeaconInfoContent = (
assetType,
timestamp,
) => ({
[M_BEACON_INFO.name]: {
description,
timeout,
live: isLive,
},
description,
timeout,
live: isLive,
[M_TIMESTAMP.name]: timestamp || Date.now(),
[M_ASSET.name]: {
type: assetType ?? LocationAssetType.Self,
@@ -227,7 +225,7 @@ export type BeaconInfoState = MBeaconInfoContent & {
* Flatten beacon info event content
*/
export const parseBeaconInfoContent = (content: MBeaconInfoEventContent): BeaconInfoState => {
const { description, timeout, live } = M_BEACON_INFO.findIn<MBeaconInfoContent>(content);
const { description, timeout, live } = content;
const { type: assetType } = M_ASSET.findIn<MAssetContent>(content);
const timestamp = M_TIMESTAMP.findIn<number>(content);
@@ -243,14 +241,14 @@ export const parseBeaconInfoContent = (content: MBeaconInfoEventContent): Beacon
export type MakeBeaconContent = (
uri: string,
timestamp: number,
beaconInfoId: string,
beaconInfoEventId: string,
description?: string,
) => MBeaconEventContent;
export const makeBeaconContent: MakeBeaconContent = (
uri,
timestamp,
beaconInfoId,
beaconInfoEventId,
description,
) => ({
[M_LOCATION.name]: {
@@ -260,6 +258,21 @@ export const makeBeaconContent: MakeBeaconContent = (
[M_TIMESTAMP.name]: timestamp,
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: beaconInfoId,
event_id: beaconInfoEventId,
},
});
export type BeaconLocationState = MLocationContent & {
timestamp: number;
};
export const parseBeaconContent = (content: MBeaconEventContent): BeaconLocationState => {
const { description, uri } = M_LOCATION.findIn<MLocationContent>(content);
const timestamp = M_TIMESTAMP.findIn<number>(content);
return {
description,
uri,
timestamp,
};
};
+2 -2
View File
@@ -73,8 +73,8 @@ export function getHttpUriForMxc(
const fragmentOffset = serverAndMediaId.indexOf("#");
let fragment = "";
if (fragmentOffset >= 0) {
fragment = serverAndMediaId.substr(fragmentOffset);
serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset);
fragment = serverAndMediaId.slice(fragmentOffset);
serverAndMediaId = serverAndMediaId.slice(0, fragmentOffset);
}
const urlParams = (Object.keys(params).length === 0 ? "" : ("?" + utils.encodeParams(params)));
@@ -228,8 +228,8 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
// (hence 43 characters long).
func({
senderKey: key.substr(KEY_INBOUND_SESSION_PREFIX.length, 43),
sessionId: key.substr(KEY_INBOUND_SESSION_PREFIX.length + 44),
senderKey: key.slice(KEY_INBOUND_SESSION_PREFIX.length, KEY_INBOUND_SESSION_PREFIX.length + 43),
sessionId: key.slice(KEY_INBOUND_SESSION_PREFIX.length + 44),
sessionData: getJsonItem(this.store, key),
});
}
@@ -299,7 +299,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
for (let i = 0; i < this.store.length; ++i) {
const key = this.store.key(i);
if (key.startsWith(prefix)) {
const roomId = key.substr(prefix.length);
const roomId = key.slice(prefix.length);
result[roomId] = getJsonItem(this.store, key);
}
}
@@ -313,8 +313,8 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
for (const session in sessionsNeedingBackup) {
if (Object.prototype.hasOwnProperty.call(sessionsNeedingBackup, session)) {
// see getAllEndToEndInboundGroupSessions for the magic number explanations
const senderKey = session.substr(0, 43);
const sessionId = session.substr(44);
const senderKey = session.slice(0, 43);
const sessionId = session.slice(44);
this.getEndToEndInboundGroupSession(
senderKey, sessionId, null,
(sessionData) => {
+4 -4
View File
@@ -418,8 +418,8 @@ export class MemoryCryptoStore implements CryptoStore {
// (hence 43 characters long).
func({
senderKey: key.substr(0, 43),
sessionId: key.substr(44),
senderKey: key.slice(0, 43),
sessionId: key.slice(44),
sessionData: this.inboundGroupSessions[key],
});
}
@@ -482,8 +482,8 @@ export class MemoryCryptoStore implements CryptoStore {
for (const session in this.sessionsNeedingBackup) {
if (this.inboundGroupSessions[session]) {
sessions.push({
senderKey: session.substr(0, 43),
sessionId: session.substr(44),
senderKey: session.slice(0, 43),
sessionId: session.slice(44),
sessionData: this.inboundGroupSessions[session],
});
if (limit && session.length >= limit) {
+9 -3
View File
@@ -25,7 +25,7 @@ export interface MapperOpts {
}
export function eventMapperFor(client: MatrixClient, options: MapperOpts): EventMapper {
const preventReEmit = Boolean(options.preventReEmit);
let preventReEmit = Boolean(options.preventReEmit);
const decrypt = options.decrypt !== false;
function mapper(plainOldJsObject: Partial<IEvent>) {
@@ -43,10 +43,13 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
} else {
// merge the latest unsigned data from the server
event.setUnsigned({ ...event.getUnsigned(), ...plainOldJsObject.unsigned });
// prevent doubling up re-emitters
preventReEmit = true;
}
if (room?.threads.has(event.getId())) {
event.setThread(room.threads.get(event.getId()));
const thread = room?.findThreadForEvent(event);
if (thread) {
event.setThread(thread);
}
if (event.isEncrypted()) {
@@ -65,6 +68,9 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
MatrixEventEvent.Replaced,
MatrixEventEvent.VisibilityChange,
]);
room?.reEmitter.reEmit(event, [
MatrixEventEvent.BeforeRedaction,
]);
}
return event;
}
+1 -1
View File
@@ -36,7 +36,7 @@ import {
function matchesWildcard(actualValue: string, filterValue: string): boolean {
if (filterValue.endsWith("*")) {
const typePrefix = filterValue.slice(0, -1);
return actualValue.substr(0, typePrefix.length) === typePrefix;
return actualValue.slice(0, typePrefix.length) === typePrefix;
} else {
return actualValue === filterValue;
}
+7 -2
View File
@@ -60,7 +60,11 @@ export enum AuthType {
Sso = "m.login.sso",
SsoUnstable = "org.matrix.login.sso",
Dummy = "m.login.dummy",
RegistrationToken = "org.matrix.msc3231.login.registration_token",
RegistrationToken = "m.login.registration_token",
// For backwards compatability with servers that have not yet updated to
// use the stable "m.login.registration_token" type.
// The authentication flow is the same in both cases.
UnstableRegistrationToken = "org.matrix.msc3231.login.registration_token",
}
export interface IAuthDict {
@@ -79,7 +83,8 @@ export interface IAuthDict {
// eslint-disable-next-line camelcase
threepid_creds?: any;
threepidCreds?: any;
registrationToken?: string;
// For m.login.registration_token type
token?: string;
}
class NoAuthFlowFoundError extends Error {
+66 -12
View File
@@ -14,22 +14,27 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { M_BEACON_INFO } from "../@types/beacon";
import { BeaconInfoState, parseBeaconInfoContent } from "../content-helpers";
import { MBeaconEventContent } from "../@types/beacon";
import { M_TIMESTAMP } from "../@types/location";
import { BeaconInfoState, BeaconLocationState, parseBeaconContent, parseBeaconInfoContent } from "../content-helpers";
import { MatrixEvent } from "../matrix";
import { sortEventsByLatestContentTimestamp } from "../utils";
import { TypedEventEmitter } from "./typed-event-emitter";
export enum BeaconEvent {
New = "Beacon.new",
Update = "Beacon.update",
LivenessChange = "Beacon.LivenessChange",
Destroy = "Destroy",
Destroy = "Beacon.Destroy",
LocationUpdate = "Beacon.LocationUpdate",
}
export type BeaconEventHandlerMap = {
[BeaconEvent.Update]: (event: MatrixEvent, beacon: Beacon) => void;
[BeaconEvent.LivenessChange]: (isLive: boolean, beacon: Beacon) => void;
[BeaconEvent.Destroy]: (beaconIdentifier: string) => void;
[BeaconEvent.LocationUpdate]: (locationState: BeaconLocationState) => void;
[BeaconEvent.Destroy]: (beaconIdentifier: string) => void;
};
export const isTimestampInDuration = (
@@ -38,16 +43,19 @@ export const isTimestampInDuration = (
timestamp: number,
): boolean => timestamp >= startTimestamp && startTimestamp + durationMs >= timestamp;
export const isBeaconInfoEventType = (type: string) =>
type.startsWith(M_BEACON_INFO.name) ||
type.startsWith(M_BEACON_INFO.altName);
// beacon info events are uniquely identified by
// `<roomId>_<state_key>`
export type BeaconIdentifier = string;
export const getBeaconInfoIdentifier = (event: MatrixEvent): BeaconIdentifier =>
`${event.getRoomId()}_${event.getStateKey()}`;
// https://github.com/matrix-org/matrix-spec-proposals/pull/3489
// https://github.com/matrix-org/matrix-spec-proposals/pull/3672
export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.New>, BeaconEventHandlerMap> {
public readonly roomId: string;
private _beaconInfo: BeaconInfoState;
private _isLive: boolean;
private livenessWatchInterval: number;
private _latestLocationState: BeaconLocationState | undefined;
constructor(
private rootEvent: MatrixEvent,
@@ -61,8 +69,8 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
return this._isLive;
}
public get identifier(): string {
return this.beaconInfoEventType;
public get identifier(): BeaconIdentifier {
return getBeaconInfoIdentifier(this.rootEvent);
}
public get beaconInfoId(): string {
@@ -81,14 +89,23 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
return this._beaconInfo;
}
public get latestLocationState(): BeaconLocationState | undefined {
return this._latestLocationState;
}
public update(beaconInfoEvent: MatrixEvent): void {
if (beaconInfoEvent.getType() !== this.beaconInfoEventType) {
if (getBeaconInfoIdentifier(beaconInfoEvent) !== this.identifier) {
throw new Error('Invalid updating event');
}
// don't update beacon with an older event
if (beaconInfoEvent.event.origin_server_ts < this.rootEvent.event.origin_server_ts) {
return;
}
this.rootEvent = beaconInfoEvent;
this.setBeaconInfo(this.rootEvent);
this.emit(BeaconEvent.Update, beaconInfoEvent, this);
this.clearLatestLocation();
}
public destroy(): void {
@@ -109,14 +126,51 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
clearInterval(this.livenessWatchInterval);
}
this.checkLiveness();
if (this.isLive) {
const expiryInMs = (this._beaconInfo?.timestamp + this._beaconInfo?.timeout + 1) - Date.now();
const expiryInMs = (this._beaconInfo?.timestamp + this._beaconInfo?.timeout) - Date.now();
if (expiryInMs > 1) {
this.livenessWatchInterval = setInterval(this.checkLiveness.bind(this), expiryInMs);
this.livenessWatchInterval = setInterval(
() => { this.monitorLiveness(); },
expiryInMs,
);
}
}
}
/**
* Process Beacon locations
* Emits BeaconEvent.LocationUpdate
*/
public addLocations(beaconLocationEvents: MatrixEvent[]): void {
// discard locations for beacons that are not live
if (!this.isLive) {
return;
}
const validLocationEvents = beaconLocationEvents.filter(event => {
const content = event.getContent<MBeaconEventContent>();
const timestamp = M_TIMESTAMP.findIn<number>(content);
return (
// only include positions that were taken inside the beacon's live period
isTimestampInDuration(this._beaconInfo.timestamp, this._beaconInfo.timeout, timestamp) &&
// ignore positions older than our current latest location
(!this.latestLocationState || timestamp > this.latestLocationState.timestamp)
);
});
const latestLocationEvent = validLocationEvents.sort(sortEventsByLatestContentTimestamp)?.[0];
if (latestLocationEvent) {
this._latestLocationState = parseBeaconContent(latestLocationEvent.getContent());
this.emit(BeaconEvent.LocationUpdate, this.latestLocationState);
}
}
private clearLatestLocation = () => {
this._latestLocationState = undefined;
this.emit(BeaconEvent.LocationUpdate, this.latestLocationState);
};
private setBeaconInfo(event: MatrixEvent): void {
this._beaconInfo = parseBeaconInfoContent(event.getContent());
this.checkLiveness();
+2 -2
View File
@@ -775,7 +775,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
}
public getAllRelationsEventForEvent(eventId: string): MatrixEvent[] {
const relationsForEvent = this.relations[eventId] || {};
const relationsForEvent = this.relations?.[eventId] || {};
const events = [];
for (const relationsRecord of Object.values(relationsForEvent)) {
for (const relations of Object.values(relationsRecord)) {
@@ -852,7 +852,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
}
let relationsWithEventType = relationsWithRelType[eventType];
let relatesToEvent;
let relatesToEvent: MatrixEvent;
if (!relationsWithEventType) {
relationsWithEventType = relationsWithRelType[eventType] = new Relations(
relationType,
-5
View File
@@ -119,11 +119,6 @@ export interface IEventRelation {
key?: string;
}
export interface IVisibilityEventRelation extends IEventRelation {
visibility: "visible" | "hidden";
reason?: string;
}
/**
* When an event is a visibility change event, as per MSC3531,
* the visibility change implied by the event.
+1 -1
View File
@@ -331,7 +331,7 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
// the all-knowning server tells us that the event at some point had
// this timestamp for its replacement, so any following replacement should definitely not be less
const replaceRelation = this.targetEvent.getServerAggregatedRelation<IAggregatedRelation>(RelationType.Replace);
const minTs = replaceRelation && replaceRelation.origin_server_ts;
const minTs = replaceRelation?.origin_server_ts;
const lastReplacement = this.getRelations().reduce((last, event) => {
if (event.getSender() !== this.targetEvent.getSender()) {
+57 -18
View File
@@ -26,8 +26,11 @@ import { MatrixEvent } from "./event";
import { MatrixClient } from "../client";
import { GuestAccess, HistoryVisibility, IJoinRuleEventContent, JoinRule } from "../@types/partials";
import { TypedEventEmitter } from "./typed-event-emitter";
import { Beacon, BeaconEvent, isBeaconInfoEventType, BeaconEventHandlerMap } from "./beacon";
import { Beacon, BeaconEvent, BeaconEventHandlerMap } from "./beacon";
import { TypedReEmitter } from "../ReEmitter";
import { M_BEACON_INFO } from "../@types/beacon";
import { getBeaconInfoIdentifier } from "./beacon";
import { BeaconIdentifier } from "..";
// possible statuses for out-of-band member loading
enum OobStatus {
@@ -80,8 +83,8 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
public events = new Map<string, Map<string, MatrixEvent>>(); // Map<eventType, Map<stateKey, MatrixEvent>>
public paginationToken: string = null;
public readonly beacons = new Map<string, Beacon>();
private liveBeaconIds: string[] = [];
public readonly beacons = new Map<BeaconIdentifier, Beacon>();
private _liveBeaconIds: BeaconIdentifier[] = [];
/**
* Construct room state.
@@ -248,6 +251,10 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
return !!this.liveBeaconIds?.length;
}
public get liveBeaconIds(): BeaconIdentifier[] {
return this._liveBeaconIds;
}
/**
* Creates a copy of this room state so that mutations to either won't affect the other.
* @return {RoomState} the copy of the room state
@@ -330,7 +337,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
return;
}
if (isBeaconInfoEventType(event.getType())) {
if (M_BEACON_INFO.matches(event.getType())) {
this.setBeacon(event);
}
@@ -404,6 +411,37 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
this.emit(RoomStateEvent.Update, this);
}
public processBeaconEvents(events: MatrixEvent[]): void {
if (
!events.length ||
// discard locations if we have no beacons
!this.beacons.size
) {
return;
}
// names are confusing here
// a Beacon is the parent event, but event type is 'm.beacon_info'
// a location is the 'child' related to the Beacon, but the event type is 'm.beacon'
// group locations by beaconInfo event id
const locationEventsByBeaconEventId = events.reduce<Record<string, MatrixEvent[]>>((acc, event) => {
const beaconInfoEventId = event.getRelation()?.event_id;
if (!acc[beaconInfoEventId]) {
acc[beaconInfoEventId] = [];
}
acc[beaconInfoEventId].push(event);
return acc;
}, {});
Object.entries(locationEventsByBeaconEventId).forEach(([beaconInfoEventId, events]) => {
const beacon = [...this.beacons.values()].find(beacon => beacon.beaconInfoId === beaconInfoEventId);
if (beacon) {
beacon.addLocations(events);
}
});
}
/**
* Looks up a member by the given userId, and if it doesn't exist,
* create it and emit the `RoomState.newMember` event.
@@ -437,12 +475,16 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
* @experimental
*/
private setBeacon(event: MatrixEvent): void {
if (this.beacons.has(event.getType())) {
const beacon = this.beacons.get(event.getType());
const beaconIdentifier = getBeaconInfoIdentifier(event);
if (this.beacons.has(beaconIdentifier)) {
const beacon = this.beacons.get(beaconIdentifier);
if (event.isRedacted()) {
beacon.destroy();
this.beacons.delete(event.getType());
if (beacon.beaconInfoId === event.getRedactionEvent()?.['redacts']) {
beacon.destroy();
this.beacons.delete(beaconIdentifier);
}
return;
}
@@ -464,25 +506,22 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
this.emit(BeaconEvent.New, event, beacon);
beacon.on(BeaconEvent.LivenessChange, this.onBeaconLivenessChange.bind(this));
this.beacons.set(beacon.beaconInfoEventType, beacon);
beacon.on(BeaconEvent.Destroy, this.onBeaconLivenessChange.bind(this));
this.beacons.set(beacon.identifier, beacon);
}
/**
* @experimental
* Check liveness of room beacons
* emit RoomStateEvent.BeaconLiveness when
* roomstate.hasLiveBeacons has changed
* emit RoomStateEvent.BeaconLiveness event
*/
private onBeaconLivenessChange(): void {
const prevHasLiveBeacons = !!this.liveBeaconIds?.length;
this.liveBeaconIds = Array.from(this.beacons.values())
this._liveBeaconIds = Array.from(this.beacons.values())
.filter(beacon => beacon.isLive)
.map(beacon => beacon.beaconInfoId);
.map(beacon => beacon.identifier);
const hasLiveBeacons = !!this.liveBeaconIds.length;
if (prevHasLiveBeacons !== hasLiveBeacons) {
this.emit(RoomStateEvent.BeaconLiveness, this, hasLiveBeacons);
}
this.emit(RoomStateEvent.BeaconLiveness, this, this.hasLiveBeacons);
}
private getStateEventMatching(event: MatrixEvent): MatrixEvent | null {
+180 -64
View File
@@ -22,8 +22,8 @@ import { EventTimelineSet, DuplicateStrategy } from "./event-timeline-set";
import { Direction, EventTimeline } from "./event-timeline";
import { getHttpUriForMxc } from "../content-repo";
import * as utils from "../utils";
import { normalize } from "../utils";
import { IEvent, IThreadBundledRelationship, MatrixEvent } from "./event";
import { defer, normalize } from "../utils";
import { IEvent, IThreadBundledRelationship, MatrixEvent, MatrixEventEvent, MatrixEventHandlerMap } from "./event";
import { EventStatus } from "./event-status";
import { RoomMember } from "./room-member";
import { IRoomSummary, RoomSummary } from "./room-summary";
@@ -171,7 +171,8 @@ type EmittedEvents = RoomEvent
| ThreadEvent.Update
| ThreadEvent.NewReply
| RoomEvent.Timeline
| RoomEvent.TimelineReset;
| RoomEvent.TimelineReset
| MatrixEventEvent.BeforeRedaction;
export type RoomEventHandlerMap = {
[RoomEvent.MyMembership]: (room: Room, membership: string, prevMembership?: string) => void;
@@ -188,10 +189,10 @@ export type RoomEventHandlerMap = {
oldStatus?: EventStatus,
) => void;
[ThreadEvent.New]: (thread: Thread, toStartOfTimeline: boolean) => void;
} & ThreadHandlerMap;
} & ThreadHandlerMap & MatrixEventHandlerMap;
export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap> {
private readonly reEmitter: TypedReEmitter<EmittedEvents, RoomEventHandlerMap>;
public readonly reEmitter: TypedReEmitter<EmittedEvents, RoomEventHandlerMap>;
private txnToEvent: Record<string, MatrixEvent> = {}; // Pending in-flight requests { string: MatrixEvent }
// receipts should clobber based on receipt_type and user_id pairs hence
// the form of this structure. This is sub-optimal for the exposed APIs
@@ -213,6 +214,8 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
private getTypeWarning = false;
private getVersionWarning = false;
private membersPromise?: Promise<boolean>;
// Map from threadId to pending Thread instance created by createThreadFetchRoot
private threadPromises = new Map<string, Promise<Thread>>();
// XXX: These should be read-only
/**
@@ -381,7 +384,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
return this.threadTimelineSetsPromise;
}
if (this.client?.supportsExperimentalThreads) {
if (this.client?.supportsExperimentalThreads()) {
try {
this.threadTimelineSetsPromise = Promise.all([
this.createThreadTimelineSet(),
@@ -1471,7 +1474,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
public threadsReady = false;
public async fetchRoomThreads(): Promise<void> {
if (this.threadsReady) {
if (this.threadsReady || !this.client.supportsExperimentalThreads()) {
return;
}
@@ -1567,6 +1570,13 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
shouldLiveInThread: boolean;
threadId?: string;
} {
if (!this.client.supportsExperimentalThreads()) {
return {
shouldLiveInRoom: true,
shouldLiveInThread: false,
};
}
// A thread root is always shown in both timelines
if (event.isThreadRoot || roots?.has(event.getId())) {
return {
@@ -1581,7 +1591,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
return {
shouldLiveInRoom: false,
shouldLiveInThread: true,
threadId: event.relationEventId,
threadId: event.threadRootId,
};
}
@@ -1630,48 +1640,77 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
threadId: string,
events?: MatrixEvent[],
toStartOfTimeline?: boolean,
): Promise<Thread> {
): Promise<Thread | null> {
let thread = this.getThread(threadId);
if (!thread) {
const deferred = defer<Thread | null>();
this.threadPromises.set(threadId, deferred.promise);
let rootEvent = this.findEventById(threadId);
// If the rootEvent does not exist in the local stores, then fetch it from the server.
try {
const eventData = await this.client.fetchRoomEvent(this.roomId, threadId);
if (!rootEvent) {
rootEvent = new MatrixEvent(eventData);
} else {
rootEvent.setUnsigned(eventData.unsigned);
}
const mapper = this.client.getEventMapper();
rootEvent = mapper(eventData); // will merge with existing event object if such is known
} catch (e) {
logger.error("Failed to fetch thread root to construct thread with", e);
} finally {
this.threadPromises.delete(threadId);
// The root event might be not be visible to the person requesting it.
// If it wasn't fetched successfully the thread will work in "limited" mode and won't
// benefit from all the APIs a homeserver can provide to enhance the thread experience
thread = this.createThread(rootEvent, events, toStartOfTimeline);
if (thread) {
rootEvent.setThread(thread);
rootEvent?.setThread(thread);
}
deferred.resolve(thread);
}
}
return thread;
}
/**
* Add an event to a thread's timeline. Will fire "Thread.update"
* @experimental
*/
public async addThreadedEvent(event: MatrixEvent, toStartOfTimeline: boolean): Promise<void> {
this.applyRedaction(event);
let thread = this.findThreadForEvent(event);
if (thread) {
await thread.addEvent(event, toStartOfTimeline);
} else {
thread = await this.createThreadFetchRoot(event.threadRootId, [event], toStartOfTimeline);
private async addThreadedEvents(events: MatrixEvent[], threadId: string, toStartOfTimeline = false): Promise<void> {
let thread = this.getThread(threadId);
if (this.threadPromises.has(threadId)) {
thread = await this.threadPromises.get(threadId);
}
this.emit(ThreadEvent.Update, thread);
events = events.filter(e => e.getId() !== threadId); // filter out any root events
if (thread) {
for (const event of events) {
await thread.addEvent(event, toStartOfTimeline);
}
} else {
thread = await this.createThreadFetchRoot(threadId, events, toStartOfTimeline);
}
if (thread) {
this.emit(ThreadEvent.Update, thread);
}
}
/**
* Adds events to a thread's timeline. Will fire "Thread.update"
* @experimental
*/
public async processThreadedEvents(events: MatrixEvent[], toStartOfTimeline: boolean): Promise<unknown> {
events.forEach(this.applyRedaction);
const eventsByThread: { [threadId: string]: MatrixEvent[] } = {};
for (const event of events) {
const { threadId } = this.eventShouldLiveIn(event);
if (!eventsByThread[threadId]) {
eventsByThread[threadId] = [];
}
eventsByThread[threadId].push(event);
}
return Promise.all(Object.entries(eventsByThread).map(([threadId, events]) => (
this.addThreadedEvents(events, threadId, toStartOfTimeline)
)));
}
public createThread(
@@ -1728,7 +1767,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
}
}
private applyRedaction(event: MatrixEvent): void {
private applyRedaction = (event: MatrixEvent): void => {
if (event.isRedaction()) {
const redactId = event.event.redacts;
@@ -1738,7 +1777,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
redactedEvent.makeRedacted(event);
// If this is in the current state, replace it with the redacted version
if (redactedEvent.getStateKey()) {
if (redactedEvent.isState()) {
const currentStateEvent = this.currentState.getStateEvents(
redactedEvent.getType(),
redactedEvent.getStateKey(),
@@ -1772,19 +1811,9 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
// clients can say "so and so redacted an event" if they wish to. Also
// this may be needed to trigger an update.
}
}
};
/**
* Add an event to the end of this room's live timelines. Will fire
* "Room.timeline".
*
* @param {MatrixEvent} event Event to be added
* @param {string?} duplicateStrategy 'ignore' or 'replace'
* @param {boolean} fromCache whether the sync response came from cache
* @fires module:client~MatrixClient#event:"Room.timeline"
* @private
*/
private addLiveEvent(event: MatrixEvent, duplicateStrategy?: DuplicateStrategy, fromCache = false): void {
private processLiveEvent(event: MatrixEvent): void {
this.applyRedaction(event);
// Implement MSC3531: hiding messages.
@@ -1801,10 +1830,21 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
if (existingEvent) {
// remote echo of an event we sent earlier
this.handleRemoteEcho(event, existingEvent);
return;
}
}
}
/**
* Add an event to the end of this room's live timelines. Will fire
* "Room.timeline".
*
* @param {MatrixEvent} event Event to be added
* @param {string?} duplicateStrategy 'ignore' or 'replace'
* @param {boolean} fromCache whether the sync response came from cache
* @fires module:client~MatrixClient#event:"Room.timeline"
* @private
*/
private addLiveEvent(event: MatrixEvent, duplicateStrategy: DuplicateStrategy, fromCache = false): void {
// add to our timeline sets
for (let i = 0; i < this.timelineSets.length; i++) {
this.timelineSets[i].addLiveEvent(event, duplicateStrategy, fromCache);
@@ -1998,10 +2038,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
const newEventId = remoteEvent.getId();
const oldStatus = localEvent.status;
logger.debug(
`Got remote echo for event ${oldEventId} -> ${newEventId} ` +
`old status ${oldStatus}`,
);
logger.debug(`Got remote echo for event ${oldEventId} -> ${newEventId} old status ${oldStatus}`);
// no longer pending
delete this.txnToEvent[remoteEvent.getUnsigned().transaction_id];
@@ -2167,10 +2204,84 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
}
}
const threadRoots = this.findThreadRoots(events);
const threadInfos = events.map(e => this.eventShouldLiveIn(e, events, threadRoots));
const eventsByThread: { [threadId: string]: MatrixEvent[] } = {};
for (let i = 0; i < events.length; i++) {
// TODO: We should have a filter to say "only add state event types X Y Z to the timeline".
this.addLiveEvent(events[i], duplicateStrategy, fromCache);
this.processLiveEvent(events[i]);
const {
shouldLiveInRoom,
shouldLiveInThread,
threadId,
} = threadInfos[i];
if (shouldLiveInThread) {
if (!eventsByThread[threadId]) {
eventsByThread[threadId] = [];
}
eventsByThread[threadId].push(events[i]);
}
if (shouldLiveInRoom) {
this.addLiveEvent(events[i], duplicateStrategy, fromCache);
}
}
Object.entries(eventsByThread).forEach(([threadId, threadEvents]) => {
this.addThreadedEvents(threadEvents, threadId, false);
});
}
public partitionThreadedEvents(events: MatrixEvent[]): [
timelineEvents: MatrixEvent[],
threadedEvents: MatrixEvent[],
] {
// Indices to the events array, for readability
const ROOM = 0;
const THREAD = 1;
if (this.client.supportsExperimentalThreads()) {
const threadRoots = this.findThreadRoots(events);
return events.reduce((memo, event: MatrixEvent) => {
const {
shouldLiveInRoom,
shouldLiveInThread,
threadId,
} = this.eventShouldLiveIn(event, events, threadRoots);
if (shouldLiveInRoom) {
memo[ROOM].push(event);
}
if (shouldLiveInThread) {
event.setThreadId(threadId);
memo[THREAD].push(event);
}
return memo;
}, [[], []]);
} else {
// When `experimentalThreadSupport` is disabled treat all events as timelineEvents
return [
events,
[],
];
}
}
/**
* Given some events, find the IDs of all the thread roots that are referred to by them.
*/
private findThreadRoots(events: MatrixEvent[]): Set<string> {
const threadRoots = new Set<string>();
for (const event of events) {
if (event.isThreadRelation) {
threadRoots.add(event.relationEventId);
}
}
return threadRoots;
}
/**
@@ -2228,22 +2339,27 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
// set fake stripped state events if this is an invite room so logic remains
// consistent elsewhere.
const membershipEvent = this.currentState.getStateEvents(EventType.RoomMember, this.myUserId);
if (membershipEvent && membershipEvent.getContent().membership === "invite") {
const strippedStateEvents = membershipEvent.getUnsigned().invite_room_state || [];
strippedStateEvents.forEach((strippedEvent) => {
const existingEvent = this.currentState.getStateEvents(strippedEvent.type, strippedEvent.state_key);
if (!existingEvent) {
// set the fake stripped event instead
this.currentState.setStateEvents([new MatrixEvent({
type: strippedEvent.type,
state_key: strippedEvent.state_key,
content: strippedEvent.content,
event_id: "$fake" + Date.now(),
room_id: this.roomId,
user_id: this.myUserId, // technically a lie
})]);
}
});
if (membershipEvent) {
const membership = membershipEvent.getContent().membership;
this.updateMyMembership(membership);
if (membership === "invite") {
const strippedStateEvents = membershipEvent.getUnsigned().invite_room_state || [];
strippedStateEvents.forEach((strippedEvent) => {
const existingEvent = this.currentState.getStateEvents(strippedEvent.type, strippedEvent.state_key);
if (!existingEvent) {
// set the fake stripped event instead
this.currentState.setStateEvents([new MatrixEvent({
type: strippedEvent.type,
state_key: strippedEvent.state_key,
content: strippedEvent.content,
event_id: "$fake" + Date.now(),
room_id: this.roomId,
user_id: this.myUserId, // technically a lie
})]);
}
});
}
}
const oldName = this.name;
+50 -37
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixClient, RelationType, RoomEvent } from "../matrix";
import { MatrixClient, MatrixEventEvent, RelationType, RoomEvent } from "../matrix";
import { TypedReEmitter } from "../ReEmitter";
import { IRelationsRequestOpts } from "../@types/requests";
import { IThreadBundledRelationship, MatrixEvent } from "./event";
@@ -94,15 +94,17 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
RoomEvent.TimelineReset,
]);
this.room.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
this.room.on(RoomEvent.Redaction, this.onRedaction);
this.room.on(RoomEvent.LocalEchoUpdated, this.onEcho);
this.timelineSet.on(RoomEvent.Timeline, this.onEcho);
// If we weren't able to find the root event, it's probably missing,
// and we define the thread ID from one of the thread relation
this.id = rootEvent?.getId() ?? opts?.initialEvents?.find(event => event.isThreadRelation)?.relationEventId;
this.initialiseThread(this.rootEvent);
opts?.initialEvents?.forEach(event => this.addEvent(event, false));
this.room.on(RoomEvent.LocalEchoUpdated, this.onEcho);
this.room.on(RoomEvent.Timeline, this.onEcho);
}
public static setServerSideSupport(hasServerSideSupport: boolean, useStable: boolean): void {
@@ -114,26 +116,58 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
}
}
private onEcho = (event: MatrixEvent) => {
if (this.timelineSet.eventIdToTimeline(event.getId())) {
private onBeforeRedaction = (event: MatrixEvent, redaction: MatrixEvent) => {
if (event?.isRelation(THREAD_RELATION_TYPE.name) &&
this.room.eventShouldLiveIn(event).threadId === this.id &&
!redaction.status // only respect it when it succeeds
) {
this.replyCount--;
this.emit(ThreadEvent.Update, this);
}
};
private onRedaction = (event: MatrixEvent) => {
if (event.threadRootId !== this.id) return; // ignore redactions for other timelines
const events = [...this.timelineSet.getLiveTimeline().getEvents()].reverse();
this.lastEvent = events.find(e => (
!e.isRedacted() &&
e.isRelation(THREAD_RELATION_TYPE.name)
)) ?? this.rootEvent;
this.emit(ThreadEvent.Update, this);
};
private onEcho = (event: MatrixEvent) => {
if (event.threadRootId !== this.id) return; // ignore echoes for other timelines
if (this.lastEvent === event) return;
// There is a risk that the `localTimestamp` approximation will not be accurate
// when threads are used over federation. That could result in the reply
// count value drifting away from the value returned by the server
const isThreadReply = event.isRelation(THREAD_RELATION_TYPE.name);
if (!this.lastEvent || this.lastEvent.isRedacted() || (isThreadReply
&& (event.getId() !== this.lastEvent.getId())
&& (event.localTimestamp > this.lastEvent.localTimestamp))
) {
this.lastEvent = event;
if (this.lastEvent.getId() !== this.id) {
// This counting only works when server side support is enabled as we started the counting
// from the value returned within the bundled relationship
if (Thread.hasServerSideSupport) {
this.replyCount++;
}
this.emit(ThreadEvent.NewReply, this, event);
}
}
this.emit(ThreadEvent.Update, this);
};
public get roomState(): RoomState {
return this.room.getLiveTimeline().getState(EventTimeline.FORWARDS);
}
private addEventToTimeline(event: MatrixEvent, toStartOfTimeline: boolean): void {
if (event.getUnsigned().transaction_id) {
const existingEvent = this.room.getEventForTxnId(event.getUnsigned().transaction_id);
if (existingEvent) {
// remote echo of an event we sent earlier
this.room.handleRemoteEcho(event, existingEvent);
return;
}
}
if (!this.findEventById(event.getId())) {
this.timelineSet.addEventToTimeline(
event,
@@ -177,33 +211,12 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
this._currentUserParticipated = true;
}
const isThreadReply = event.getRelation()?.rel_type === THREAD_RELATION_TYPE.name;
// If no thread support exists we want to count all thread relation
// added as a reply. We can't rely on the bundled relationships count
if (!Thread.hasServerSideSupport && isThreadReply) {
if (!Thread.hasServerSideSupport && event.isRelation(THREAD_RELATION_TYPE.name)) {
this.replyCount++;
}
// There is a risk that the `localTimestamp` approximation will not be accurate
// when threads are used over federation. That could results in the reply
// count value drifting away from the value returned by the server
if (!this.lastEvent || (isThreadReply
&& (event.getId() !== this.lastEvent.getId())
&& (event.localTimestamp > this.lastEvent.localTimestamp))
) {
this.lastEvent = event;
if (this.lastEvent.getId() !== this.id) {
// This counting only works when server side support is enabled
// as we started the counting from the value returned in the
// bundled relationship
if (Thread.hasServerSideSupport) {
this.replyCount++;
}
this.emit(ThreadEvent.NewReply, this, event);
}
}
this.emit(ThreadEvent.Update, this);
}
+5 -5
View File
@@ -78,7 +78,7 @@ WebStorageSessionStore.prototype = {
const devices = {};
for (let i = 0; i < this.store.length; ++i) {
const key = this.store.key(i);
const userId = key.substr(prefix.length);
const userId = key.slice(prefix.length);
if (key.startsWith(prefix)) devices[userId] = getJsonItem(this.store, key);
}
return devices;
@@ -125,7 +125,7 @@ WebStorageSessionStore.prototype = {
const deviceKeys = getKeysWithPrefix(this.store, keyEndToEndSessions(''));
const results = {};
for (const k of deviceKeys) {
const unprefixedKey = k.substr(keyEndToEndSessions('').length);
const unprefixedKey = k.slice(keyEndToEndSessions('').length);
results[unprefixedKey] = getJsonItem(this.store, k);
}
return results;
@@ -158,8 +158,8 @@ WebStorageSessionStore.prototype = {
// (hence 43 characters long).
result.push({
senderKey: key.substr(prefix.length, 43),
sessionId: key.substr(prefix.length + 44),
senderKey: key.slice(prefix.length, prefix.length + 43),
sessionId: key.slice(prefix.length + 44),
});
}
return result;
@@ -182,7 +182,7 @@ WebStorageSessionStore.prototype = {
const roomKeys = getKeysWithPrefix(this.store, keyEndToEndRoom(''));
const results = {};
for (const k of roomKeys) {
const unprefixedKey = k.substr(keyEndToEndRoom('').length);
const unprefixedKey = k.slice(keyEndToEndRoom('').length);
results[unprefixedKey] = getJsonItem(this.store, k);
}
return results;
+2 -33
View File
@@ -1170,7 +1170,6 @@ export class SyncApi {
stateEvents.forEach(function(e) {
client.emit(ClientEvent.Event, e);
});
room.updateMyMembership("invite");
});
// Handle joins
@@ -1317,8 +1316,6 @@ export class SyncApi {
client.emit(ClientEvent.Event, e);
});
room.updateMyMembership("join");
// Decrypt only the last message in all rooms to make sure we can generate a preview
// And decrypt all events after the recorded read receipt to ensure an accurate
// notification count
@@ -1352,8 +1349,6 @@ export class SyncApi {
accountDataEvents.forEach(function(e) {
client.emit(ClientEvent.Event, e);
});
room.updateMyMembership("leave");
});
// update the notification timeline, if appropriate.
@@ -1640,36 +1635,10 @@ export class SyncApi {
// if the timeline has any state events in it.
// This also needs to be done before running push rules on the events as they need
// to be decorated with sender etc.
const [mainTimelineEvents, threadedEvents] = this.client.partitionThreadedEvents(room, timelineEventList || []);
room.addLiveEvents(mainTimelineEvents, null, fromCache);
await this.processThreadEvents(room, threadedEvents, false);
room.addLiveEvents(timelineEventList || [], null, fromCache);
this.client.processBeaconEvents(room, timelineEventList);
}
/**
* @experimental
*/
private processThreadEvents(
room: Room,
threadedEvents: MatrixEvent[],
toStartOfTimeline: boolean,
): Promise<void> {
return this.client.processThreadEvents(room, threadedEvents, toStartOfTimeline);
}
// extractRelatedEvents(event: MatrixEvent, events: MatrixEvent[], relatedEvents: MatrixEvent[] = []): MatrixEvent[] {
// relatedEvents.push(event);
// const parentEventId = event.getAssociatedId();
// const parentEventIndex = events.findIndex(event => event.getId() === parentEventId);
// if (parentEventIndex > -1) {
// const [relatedEvent] = events.splice(parentEventIndex, 1);
// return this.extractRelatedEvents(relatedEvent, events, relatedEvents);
// } else {
// return relatedEvents;
// }
// }
/**
* Takes a list of timelineEvents and adds and adds to notifEvents
* as appropriate.
+15 -1
View File
@@ -24,6 +24,8 @@ import unhomoglyph from "unhomoglyph";
import promiseRetry from "p-retry";
import type NodeCrypto from "crypto";
import { MatrixEvent } from ".";
import { M_TIMESTAMP } from "./@types/location";
/**
* Encode a dictionary of query parameters.
@@ -425,7 +427,7 @@ export function globToRegexp(glob: string, extended?: any): string {
export function ensureNoTrailingSlash(url: string): string {
if (url && url.endsWith("/")) {
return url.substr(0, url.length - 1);
return url.slice(0, -1);
} else {
return url;
}
@@ -708,3 +710,15 @@ export function recursivelyAssign(target: Object, source: Object, ignoreNullish
}
return target;
}
function getContentTimestampWithFallback(event: MatrixEvent): number {
return M_TIMESTAMP.findIn<number>(event.getContent()) ?? -1;
}
/**
* Sort events by their content m.ts property
* Latest timestamp first
*/
export function sortEventsByLatestContentTimestamp(left: MatrixEvent, right: MatrixEvent): number {
return getContentTimestampWithFallback(right) - getContentTimestampWithFallback(left);
}