Compare commits

..

24 Commits

Author SHA1 Message Date
RiotRobot d705a0ed9e v17.0.0-rc.2 2022-04-08 12:08:04 +01:00
RiotRobot 7023fb1c99 Prepare changelog for v17.0.0-rc.2 2022-04-08 12:08:03 +01:00
RiotRobot fe36dafcc7 v3.42.2-rc.3 2022-04-08 10:40:56 +01:00
RiotRobot 04a6dbbedf Prepare changelog for v3.42.2-rc.3 2022-04-08 10:40:56 +01:00
RiotRobot 29b427aab7 v17.0.0-rc.1 2022-04-08 10:36:14 +01:00
RiotRobot 643b783dec Prepare changelog for v17.0.0-rc.1 2022-04-08 10:36:13 +01:00
Germain 872033a552 Port #2283 to release (#2284) 2022-04-08 10:34:09 +01:00
RiotRobot d457fd6db0 v16.0.2-rc.1 2022-04-05 18:07:38 +01:00
RiotRobot 03f4700bd7 Prepare changelog for v16.0.2-rc.1 2022-04-05 18:07:37 +01:00
Robin b8321290f8 Add Element video room type (#2273) 2022-04-04 10:29:35 -04:00
Kerry 71b7521f42 Live location sharing - handle redacted beacons (#2269)
* emit beacon destroy event on destroy

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

* handle redacted beacon events in room-state

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

* empty line

Signed-off-by: Kerry Archibald <kerrya@element.io>
2022-04-04 10:17:49 +02:00
adamvy 106f7beb48 Fix getSessionsNeedingBackup() limit support (#2270) 2022-04-01 19:56:07 -06:00
Michael Telatynski d6f1c6cfdc Fix thread & main timeline partitioning logic (#2264) 2022-03-31 13:57:37 +01:00
Robin 4360ae7ff8 Fix coverage diffs for PRs that aren't up to date (#2263) 2022-03-30 08:02:14 -04:00
Michael Telatynski 26cbe02a7f Instantiate Thread objects when running fetchRoomThreads (#2262) 2022-03-29 15:28:08 +01:00
Michael Telatynski 85b8d4f83a Fix issues with /search and /context API handling for threads (#2261) 2022-03-29 09:24:45 +01:00
RiotRobot bdc3da1fac Resetting package fields for development 2022-03-28 14:38:44 +01:00
RiotRobot 95fcd5aa26 Merge branch 'master' into develop 2022-03-28 14:38:43 +01:00
Andy Balaam f03a391f80 Prevent exception 'Unable to set up secret storage' (#2260) 2022-03-28 11:48:34 +01:00
Andy Balaam e90f12ee32 Port codecov improvements matrix-react-sdk -> matrix-js-sdk (#2258) 2022-03-25 09:09:35 +00:00
Michael Telatynski c541b3f1ce Fix issues with duplicated MatrixEvent objects around threads (#2256) 2022-03-24 12:24:19 +00:00
Germain 6192325fe0 Thread list ordering by last reply (#2253) 2022-03-23 14:43:30 +00:00
Travis Ralston d0b964837f Remove groups (#2234)
This API is due for removal in Synapse and has been deprecated for a very long time. People should move away from it soon, but just in case we'll declare this as a breaking change.

There is no impact on sync storage here: we happen to store the data in a way that is backwards-compatible for group-supporting clients, and the code guards against missing data from the stores. So, if someone were to revert, they'd be "safe" (probably lose all their group info, but the app wouldn't crash).
2022-03-22 22:20:32 +00:00
Robin 65316ffb5c Voice rooms prototype (#2249)
* Support call room type from MSC3417

Signed-off-by: Robin Townsend <robin@robin.town>

* Make it more clear that call room type is unstable

Signed-off-by: Robin Townsend <robin@robin.town>
2022-03-22 16:14:23 -06:00
39 changed files with 2228 additions and 2071 deletions
+6
View File
@@ -1,3 +1,9 @@
codecov:
allow_coverage_offsets: True
coverage:
status:
project: off
patch: off
comment:
layout: "diff, files"
behavior: default
+12
View File
@@ -6,9 +6,20 @@ on:
jobs:
test-coverage:
runs-on: ubuntu-latest
env:
# This must be set for fetchdep.sh to get the right branch
PR_NUMBER: ${{github.event.number}}
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
# If this is a pull request, make sure we check out its head rather than the
# automatically generated merge commit, so that the coverage diff excludes
# unrelated changes in the base branch
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || '' }}
- name: Yarn cache
uses: c-hive/gha-yarn-cache@v2
- name: Run tests with coverage
run: "yarn install && yarn build && yarn coverage"
@@ -16,4 +27,5 @@ jobs:
- name: Upload coverage
uses: codecov/codecov-action@v2
with:
fail_ci_if_error: false
verbose: true
+27
View File
@@ -1,3 +1,24 @@
Changes in [17.0.0-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v17.0.0-rc.2) (2022-04-08)
============================================================================================================
Changes in [17.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v17.0.0-rc.1) (2022-04-08)
============================================================================================================
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 +2009,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)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "16.0.1",
"version": "17.0.0-rc.2",
"description": "Matrix Client-Server SDK for Javascript",
"scripts": {
"prepublishOnly": "yarn build",
+71 -11
View File
@@ -2,6 +2,7 @@ import * as utils from "../test-utils/test-utils";
import { EventTimeline } from "../../src/matrix";
import { logger } from "../../src/logger";
import { TestClient } from "../TestClient";
import { Thread, THREAD_RELATION_TYPE } from "../../src/models/thread";
const userId = "@alice:localhost";
const userName = "Alice";
@@ -69,6 +70,27 @@ const EVENTS = [
}),
];
const THREAD_ROOT = utils.mkMessage({
room: roomId,
user: userId,
msg: "thread root",
});
const THREAD_REPLY = utils.mkEvent({
room: roomId,
user: userId,
type: "m.room.message",
content: {
"body": "thread reply",
"msgtype": "m.text",
"m.relates_to": {
// We can't use the const here because we change server support mode for test
rel_type: "io.element.thread",
event_id: THREAD_ROOT.event_id,
},
},
});
// start the client, and wait for it to initialise
function startClient(httpBackend, client) {
httpBackend.when("GET", "/versions").respond(200, {});
@@ -116,9 +138,7 @@ describe("getEventTimeline support", function() {
return startClient(httpBackend, client).then(function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
expect(function() {
client.getEventTimeline(timelineSet, "event");
}).toThrow();
expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeTruthy();
});
});
@@ -136,16 +156,12 @@ describe("getEventTimeline support", function() {
return startClient(httpBackend, client).then(() => {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
expect(function() {
client.getEventTimeline(timelineSet, "event");
}).not.toThrow();
expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeFalsy();
});
});
it("scrollback should be able to scroll back to before a gappy /sync",
function() {
it("scrollback should be able to scroll back to before a gappy /sync", function() {
// need a client with timelineSupport disabled to make this work
let room;
return startClient(httpBackend, client).then(function() {
@@ -229,6 +245,7 @@ describe("MatrixClient event timelines", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
Thread.setServerSideSupport(false);
});
describe("getEventTimeline", function() {
@@ -355,8 +372,7 @@ describe("MatrixClient event timelines", function() {
]);
});
it("should join timelines where they overlap a previous /context",
function() {
it("should join timelines where they overlap a previous /context", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
@@ -478,6 +494,50 @@ describe("MatrixClient event timelines", function() {
httpBackend.flushAllExpected(),
]);
});
it("should handle thread replies with server support by fetching a contiguous thread timeline", async () => {
Thread.setServerSideSupport(true);
client.stopClient(); // we don't need the client to be syncing at this time
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_REPLY.event_id))
.respond(200, function() {
return {
start: "start_token0",
events_before: [],
event: THREAD_REPLY,
events_after: [],
end: "end_token0",
state: [],
};
});
httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id))
.respond(200, function() {
return THREAD_ROOT;
});
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
encodeURIComponent(THREAD_ROOT.event_id) + "/" +
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?limit=20")
.respond(200, function() {
return {
original_event: THREAD_ROOT,
chunk: [THREAD_REPLY],
next_batch: "next_batch_token0",
prev_batch: "prev_batch_token0",
};
});
const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id);
await httpBackend.flushAllExpected();
const timeline = await timelinePromise;
expect(timeline.getEvents().find(e => e.getId() === THREAD_ROOT.event_id));
expect(timeline.getEvents().find(e => e.getId() === THREAD_REPLY.event_id));
});
});
describe("paginateEventTimeline", function() {
+340 -51
View File
@@ -3,6 +3,7 @@ import { CRYPTO_ENABLED } from "../../src/client";
import { MatrixEvent } from "../../src/models/event";
import { Filter, MemoryStore, Room } from "../../src/matrix";
import { TestClient } from "../TestClient";
import { THREAD_RELATION_TYPE } from "../../src/models/thread";
describe("MatrixClient", function() {
let client = null;
@@ -14,9 +15,7 @@ describe("MatrixClient", function() {
beforeEach(function() {
store = new MemoryStore();
const testClient = new TestClient(userId, "aliceDevice", accessToken, undefined, {
store: store,
});
const testClient = new TestClient(userId, "aliceDevice", accessToken, undefined, { store });
httpBackend = testClient.httpBackend;
client = testClient.client;
});
@@ -244,14 +243,15 @@ describe("MatrixClient", function() {
});
describe("searching", function() {
const response = {
search_categories: {
room_events: {
count: 24,
results: {
"$flibble:localhost": {
it("searchMessageText should perform a /search for room_events", function() {
const response = {
search_categories: {
room_events: {
count: 24,
results: [{
rank: 0.1,
result: {
event_id: "$flibble:localhost",
type: "m.room.message",
user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost",
@@ -260,13 +260,11 @@ describe("MatrixClient", function() {
msgtype: "m.text",
},
},
},
}],
},
},
},
};
};
it("searchMessageText should perform a /search for room_events", function(done) {
client.searchMessageText({
query: "monkeys",
});
@@ -280,8 +278,171 @@ describe("MatrixClient", function() {
});
}).respond(200, response);
httpBackend.flush().then(function() {
done();
return httpBackend.flush();
});
describe("should filter out context from different timelines (threads)", () => {
it("filters out thread replies when result is in the main timeline", async () => {
const response = {
search_categories: {
room_events: {
count: 24,
results: [{
rank: 0.1,
result: {
event_id: "$flibble:localhost",
type: "m.room.message",
user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost",
content: {
body: "main timeline",
msgtype: "m.text",
},
},
context: {
events_after: [{
event_id: "$ev-after:server",
type: "m.room.message",
user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost",
content: {
"body": "thread reply",
"msgtype": "m.text",
"m.relates_to": {
"event_id": "$some-thread:server",
"rel_type": THREAD_RELATION_TYPE.name,
},
},
}],
events_before: [{
event_id: "$ev-before:server",
type: "m.room.message",
user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost",
content: {
body: "main timeline again",
msgtype: "m.text",
},
}],
},
}],
},
},
};
const data = {
results: [],
highlights: [],
};
client.processRoomEventsSearch(data, response);
expect(data.results).toHaveLength(1);
expect(data.results[0].context.timeline).toHaveLength(2);
expect(data.results[0].context.timeline.find(e => e.getId() === "$ev-after:server")).toBeFalsy();
});
it("filters out thread replies from threads other than the thread the result replied to", () => {
const response = {
search_categories: {
room_events: {
count: 24,
results: [{
rank: 0.1,
result: {
event_id: "$flibble:localhost",
type: "m.room.message",
user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost",
content: {
"body": "thread 1 reply 1",
"msgtype": "m.text",
"m.relates_to": {
"event_id": "$thread1:server",
"rel_type": THREAD_RELATION_TYPE.name,
},
},
},
context: {
events_after: [{
event_id: "$ev-after:server",
type: "m.room.message",
user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost",
content: {
"body": "thread 2 reply 2",
"msgtype": "m.text",
"m.relates_to": {
"event_id": "$thread2:server",
"rel_type": THREAD_RELATION_TYPE.name,
},
},
}],
events_before: [],
},
}],
},
},
};
const data = {
results: [],
highlights: [],
};
client.processRoomEventsSearch(data, response);
expect(data.results).toHaveLength(1);
expect(data.results[0].context.timeline).toHaveLength(1);
expect(data.results[0].context.timeline.find(e => e.getId() === "$flibble:localhost")).toBeTruthy();
});
it("filters out main timeline events when result is a thread reply", () => {
const response = {
search_categories: {
room_events: {
count: 24,
results: [{
rank: 0.1,
result: {
event_id: "$flibble:localhost",
type: "m.room.message",
user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost",
content: {
"body": "thread 1 reply 1",
"msgtype": "m.text",
"m.relates_to": {
"event_id": "$thread1:server",
"rel_type": THREAD_RELATION_TYPE.name,
},
},
},
context: {
events_after: [{
event_id: "$ev-after:server",
type: "m.room.message",
user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost",
content: {
"body": "main timeline",
"msgtype": "m.text",
},
}],
events_before: [],
},
}],
},
},
};
const data = {
results: [],
highlights: [],
};
client.processRoomEventsSearch(data, response);
expect(data.results).toHaveLength(1);
expect(data.results[0].context.timeline).toHaveLength(1);
expect(data.results[0].context.timeline.find(e => e.getId() === "$flibble:localhost")).toBeTruthy();
});
});
});
@@ -395,9 +556,11 @@ describe("MatrixClient", function() {
});
describe("partitionThreadedEvents", function() {
const 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(events);
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
expect(timeline).toEqual([]);
expect(threaded).toEqual([]);
});
@@ -405,24 +568,24 @@ describe("MatrixClient", function() {
it("copies pre-thread in-timeline vote events onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true };
const eventMessageInThread = buildEventMessageInThread();
const eventPollResponseReference = buildEventPollResponseReference();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
const events = [
eventPollStartThreadRoot,
eventMessageInThread,
eventPollResponseReference,
eventPollStartThreadRoot,
];
// Vote has no threadId yet
expect(eventPollResponseReference.threadId).toBeFalsy();
const [timeline, threaded] = client.partitionThreadedEvents(events);
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
expect(timeline).toEqual([
// The message that was sent in a thread is missing
eventPollResponseReference,
eventPollStartThreadRoot,
eventPollResponseReference,
]);
// The vote event has been copied into the thread
@@ -431,33 +594,34 @@ describe("MatrixClient", function() {
expect(eventRefWithThreadId.threadId).toBeTruthy();
expect(threaded).toEqual([
eventPollStartThreadRoot,
eventMessageInThread,
eventRefWithThreadId,
// Thread does not see thread root
]);
});
it("copies pre-thread in-timeline reactions onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true };
const eventMessageInThread = buildEventMessageInThread();
const eventReaction = buildEventReaction();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
const eventReaction = buildEventReaction(eventPollStartThreadRoot);
const events = [
eventPollStartThreadRoot,
eventMessageInThread,
eventReaction,
eventPollStartThreadRoot,
];
const [timeline, threaded] = client.partitionThreadedEvents(events);
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
expect(timeline).toEqual([
eventReaction,
eventPollStartThreadRoot,
eventReaction,
]);
expect(threaded).toEqual([
eventPollStartThreadRoot,
eventMessageInThread,
withThreadId(eventReaction, eventPollStartThreadRoot.getId()),
]);
@@ -467,23 +631,24 @@ describe("MatrixClient", function() {
client.clientOpts = { experimentalThreadSupport: true };
const eventPollResponseReference = buildEventPollResponseReference();
const eventMessageInThread = buildEventMessageInThread();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
const events = [
eventPollStartThreadRoot,
eventPollResponseReference,
eventMessageInThread,
eventPollStartThreadRoot,
];
const [timeline, threaded] = client.partitionThreadedEvents(events);
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
expect(timeline).toEqual([
eventPollResponseReference,
eventPollStartThreadRoot,
eventPollResponseReference,
]);
expect(threaded).toEqual([
eventPollStartThreadRoot,
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()),
eventMessageInThread,
]);
@@ -492,26 +657,27 @@ describe("MatrixClient", function() {
it("copies post-thread in-timeline reactions onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true };
const eventReaction = buildEventReaction();
const eventMessageInThread = buildEventMessageInThread();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
const eventReaction = buildEventReaction(eventPollStartThreadRoot);
const events = [
eventReaction,
eventMessageInThread,
eventPollStartThreadRoot,
eventMessageInThread,
eventReaction,
];
const [timeline, threaded] = client.partitionThreadedEvents(events);
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
expect(timeline).toEqual([
eventReaction,
eventPollStartThreadRoot,
eventReaction,
]);
expect(threaded).toEqual([
withThreadId(eventReaction, eventPollStartThreadRoot.getId()),
eventPollStartThreadRoot,
eventMessageInThread,
withThreadId(eventReaction, eventPollStartThreadRoot.getId()),
]);
});
@@ -519,9 +685,9 @@ describe("MatrixClient", function() {
client.clientOpts = { experimentalThreadSupport: true };
// This is based on recording the events in a real room:
const eventMessageInThread = buildEventMessageInThread();
const eventPollResponseReference = buildEventPollResponseReference();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const eventPollResponseReference = buildEventPollResponseReference();
const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
const eventRoomName = buildEventRoomName();
const eventEncryption = buildEventEncryption();
const eventGuestAccess = buildEventGuestAccess();
@@ -532,9 +698,9 @@ describe("MatrixClient", function() {
const eventCreate = buildEventCreate();
const events = [
eventMessageInThread,
eventPollResponseReference,
eventPollStartThreadRoot,
eventPollResponseReference,
eventMessageInThread,
eventRoomName,
eventEncryption,
eventGuestAccess,
@@ -544,12 +710,12 @@ describe("MatrixClient", function() {
eventMember,
eventCreate,
];
const [timeline, threaded] = client.partitionThreadedEvents(events);
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
expect(timeline).toEqual([
// The message that was sent in a thread is missing
eventPollResponseReference,
eventPollStartThreadRoot,
eventPollResponseReference,
eventRoomName,
eventEncryption,
eventGuestAccess,
@@ -560,11 +726,95 @@ describe("MatrixClient", function() {
eventCreate,
]);
// Thread should contain only stuff that happened in the thread -
// no thread root, and no room state events
// Thread should contain only stuff that happened in the thread - no room state events
expect(threaded).toEqual([
eventMessageInThread,
eventPollStartThreadRoot,
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()),
eventMessageInThread,
]);
});
it("sends redactions of reactions to thread responses to thread timeline only", () => {
client.clientOpts = { experimentalThreadSupport: true };
const threadRootEvent = buildEventPollStartThreadRoot();
const eventMessageInThread = buildEventMessageInThread(threadRootEvent);
const threadedReaction = buildEventReaction(eventMessageInThread);
const threadedReactionRedaction = buildEventRedaction(threadedReaction);
const events = [
threadRootEvent,
eventMessageInThread,
threadedReaction,
threadedReactionRedaction,
];
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
expect(timeline).toEqual([
threadRootEvent,
]);
expect(threaded).toEqual([
threadRootEvent,
eventMessageInThread,
threadedReaction,
threadedReactionRedaction,
]);
});
it("sends reply to reply to thread root outside of thread to main timeline only", () => {
client.clientOpts = { experimentalThreadSupport: true };
const threadRootEvent = buildEventPollStartThreadRoot();
const eventMessageInThread = buildEventMessageInThread(threadRootEvent);
const directReplyToThreadRoot = buildEventReply(threadRootEvent);
const replyToReply = buildEventReply(directReplyToThreadRoot);
const events = [
threadRootEvent,
eventMessageInThread,
directReplyToThreadRoot,
replyToReply,
];
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
expect(timeline).toEqual([
threadRootEvent,
directReplyToThreadRoot,
replyToReply,
]);
expect(threaded).toEqual([
threadRootEvent,
eventMessageInThread,
]);
});
it("sends reply to thread responses to thread timeline only", () => {
client.clientOpts = { experimentalThreadSupport: true };
const threadRootEvent = buildEventPollStartThreadRoot();
const eventMessageInThread = buildEventMessageInThread(threadRootEvent);
const replyToThreadResponse = buildEventReply(eventMessageInThread);
const events = [
threadRootEvent,
eventMessageInThread,
replyToThreadResponse,
];
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
expect(timeline).toEqual([
threadRootEvent,
]);
expect(threaded).toEqual([
threadRootEvent,
eventMessageInThread,
replyToThreadResponse,
]);
});
});
@@ -576,16 +826,16 @@ function withThreadId(event, newThreadId) {
return ret;
}
const buildEventMessageInThread = () => new MatrixEvent({
const buildEventMessageInThread = (root) => new MatrixEvent({
"age": 80098509,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "ENCRYPTEDSTUFF",
"device_id": "XISFUZSKHH",
"m.relates_to": {
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
"event_id": root.getId(),
"m.in_reply_to": {
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
"event_id": root.getId(),
},
"rel_type": "m.thread",
},
@@ -623,10 +873,10 @@ const buildEventPollResponseReference = () => new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org",
});
const buildEventReaction = () => new MatrixEvent({
const buildEventReaction = (event) => new MatrixEvent({
"content": {
"m.relates_to": {
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
"event_id": event.getId(),
"key": "🤗",
"rel_type": "m.annotation",
},
@@ -642,6 +892,22 @@ const buildEventReaction = () => new MatrixEvent({
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
});
const buildEventRedaction = (event) => new MatrixEvent({
"content": {
},
"origin_server_ts": 1643977249239,
"sender": "@andybalaam-test1:matrix.org",
"redacts": event.getId(),
"type": "m.room.redaction",
"unsigned": {
"age": 22597,
"transaction_id": "m1643977249073.17",
},
"event_id": "$86B2b-x3LgE4DlV4y24b7UHnt72LIA3rzjvMysTtAfB",
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
});
const buildEventPollStartThreadRoot = () => new MatrixEvent({
"age": 80108647,
"content": {
@@ -660,6 +926,29 @@ const buildEventPollStartThreadRoot = () => new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org",
});
const buildEventReply = (target) => new MatrixEvent({
"age": 80098509,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "ENCRYPTEDSTUFF",
"device_id": "XISFUZSKHH",
"m.relates_to": {
"m.in_reply_to": {
"event_id": target.getId(),
},
},
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
},
"event_id": target.getId() + Math.random(),
"origin_server_ts": 1643815466378,
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
"sender": "@andybalaam-test1:matrix.org",
"type": "m.room.encrypted",
"unsigned": { "age": 80098509 },
"user_id": "@andybalaam-test1:matrix.org",
});
const buildEventRoomName = () => new MatrixEvent({
"age": 80123249,
"content": {
+1 -2
View File
@@ -735,8 +735,7 @@ describe("MatrixClient syncing", function() {
expect(tok).toEqual("pagTok");
}),
// first flush the filter request; this will make syncLeftRooms
// make its /sync call
// first flush the filter request; this will make syncLeftRooms make its /sync call
httpBackend.flush("/filter").then(function() {
return httpBackend.flushAllExpected();
}),
-369
View File
@@ -1,369 +0,0 @@
// load olm before the sdk if possible
import '../olm-loader';
import { logger } from '../../src/logger';
import { MatrixEvent } from "../../src/models/event";
/**
* Return a promise that is resolved when the client next emits a
* SYNCING event.
* @param {Object} client The client
* @param {Number=} count Number of syncs to wait for (default 1)
* @return {Promise} Resolves once the client has emitted a SYNCING event
*/
export function syncPromise(client, count) {
if (count === undefined) {
count = 1;
}
if (count <= 0) {
return Promise.resolve();
}
const p = new Promise((resolve, reject) => {
const cb = (state) => {
logger.log(`${Date.now()} syncPromise(${count}): ${state}`);
if (state === 'SYNCING') {
resolve();
} else {
client.once('sync', cb);
}
};
client.once('sync', cb);
});
return p.then(() => {
return syncPromise(client, count-1);
});
}
/**
* Create a spy for an object and automatically spy its methods.
* @param {*} constr The class constructor (used with 'new')
* @param {string} name The name of the class
* @return {Object} An instantiated object with spied methods/properties.
*/
export function mock(constr, name) {
// Based on
// http://eclipsesource.com/blogs/2014/03/27/mocks-in-jasmine-tests/
const HelperConstr = new Function(); // jshint ignore:line
HelperConstr.prototype = constr.prototype;
const result = new HelperConstr();
result.toString = function() {
return "mock" + (name ? " of " + name : "");
};
for (const key of Object.getOwnPropertyNames(constr.prototype)) { // eslint-disable-line guard-for-in
try {
if (constr.prototype[key] instanceof Function) {
result[key] = jest.fn();
}
} catch (ex) {
// Direct access to some non-function fields of DOM prototypes may
// cause exceptions.
// Overwriting will not work either in that case.
}
}
return result;
}
/**
* Create an Event.
* @param {Object} opts Values for the event.
* @param {string} opts.type The event.type
* @param {string} opts.room The event.room_id
* @param {string} opts.sender The event.sender
* @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.
* @return {Object} a JSON object representing this event.
*/
export function mkEvent(opts) {
if (!opts.type || !opts.content) {
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
}
const event = {
type: opts.type,
room_id: opts.room,
sender: opts.sender || opts.user, // opts.user for backwards-compat
content: opts.content,
unsigned: opts.unsigned,
event_id: "$" + Math.random() + "-" + Math.random(),
};
if (opts.skey !== undefined) {
event.state_key = opts.skey;
} else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
"m.room.power_levels", "m.room.topic",
"com.example.state"].includes(opts.type)) {
event.state_key = "";
}
return opts.event ? new MatrixEvent(event) : event;
}
/**
* Create an m.presence event.
* @param {Object} opts Values for the presence.
* @return {Object|MatrixEvent} The event
*/
export function mkPresence(opts) {
if (!opts.user) {
throw new Error("Missing user");
}
const event = {
event_id: "$" + Math.random() + "-" + Math.random(),
type: "m.presence",
sender: opts.sender || opts.user, // opts.user for backwards-compat
content: {
avatar_url: opts.url,
displayname: opts.name,
last_active_ago: opts.ago,
presence: opts.presence || "offline",
},
};
return opts.event ? new MatrixEvent(event) : event;
}
/**
* Create an m.room.member event.
* @param {Object} opts Values for the membership.
* @param {string} opts.room The room ID for the event.
* @param {string} opts.mship The content.membership for the event.
* @param {string} opts.sender The sender user ID for the event.
* @param {string} opts.skey The target user ID for the event if applicable
* e.g. for invites/bans.
* @param {string} opts.name The content.displayname for the event.
* @param {string} opts.url The content.avatar_url for the event.
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object|MatrixEvent} The event
*/
export function mkMembership(opts) {
opts.type = "m.room.member";
if (!opts.skey) {
opts.skey = opts.sender || opts.user;
}
if (!opts.mship) {
throw new Error("Missing .mship => " + JSON.stringify(opts));
}
opts.content = {
membership: opts.mship,
};
if (opts.name) {
opts.content.displayname = opts.name;
}
if (opts.url) {
opts.content.avatar_url = opts.url;
}
return mkEvent(opts);
}
/**
* Create an m.room.message event.
* @param {Object} opts Values for the message
* @param {string} opts.room The room ID for the event.
* @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.
* @return {Object|MatrixEvent} The event
*/
export function mkMessage(opts) {
opts.type = "m.room.message";
if (!opts.msg) {
opts.msg = "Random->" + Math.random();
}
if (!opts.room || !opts.user) {
throw new Error("Missing .room or .user from %s", opts);
}
opts.content = {
msgtype: "m.text",
body: opts.msg,
};
return mkEvent(opts);
}
/**
* A mock implementation of webstorage
*
* @constructor
*/
export function MockStorageApi() {
this.data = {};
}
MockStorageApi.prototype = {
get length() {
return Object.keys(this.data).length;
},
key: function(i) {
return Object.keys(this.data)[i];
},
setItem: function(k, v) {
this.data[k] = v;
},
getItem: function(k) {
return this.data[k] || null;
},
removeItem: function(k) {
delete this.data[k];
},
};
/**
* If an event is being decrypted, wait for it to finish being decrypted.
*
* @param {MatrixEvent} event
* @returns {Promise} promise which resolves (to `event`) when the event has been decrypted
*/
export function awaitDecryption(event) {
// An event is not always decrypted ahead of time
// getClearContent is a good signal to know whether an event has been decrypted
// already
if (event.getClearContent() !== null) {
return event;
} else {
logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
return new Promise((resolve, reject) => {
event.once('Event.decrypted', (ev) => {
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
resolve(ev);
});
});
}
}
export function HttpResponse(
httpLookups, acceptKeepalives, ignoreUnhandledSync,
) {
this.httpLookups = httpLookups;
this.acceptKeepalives = acceptKeepalives === undefined ? true : acceptKeepalives;
this.ignoreUnhandledSync = ignoreUnhandledSync;
this.pendingLookup = null;
}
HttpResponse.prototype.request = function(
cb, method, path, qp, data, prefix,
) {
if (path === HttpResponse.KEEP_ALIVE_PATH && this.acceptKeepalives) {
return Promise.resolve();
}
const next = this.httpLookups.shift();
const logLine = (
"MatrixClient[UT] RECV " + method + " " + path + " " +
"EXPECT " + (next ? next.method : next) + " " + (next ? next.path : next)
);
logger.log(logLine);
if (!next) { // no more things to return
if (method === "GET" && path === "/sync" && this.ignoreUnhandledSync) {
logger.log("MatrixClient[UT] Ignoring.");
return new Promise(() => {});
}
if (this.pendingLookup) {
if (this.pendingLookup.method === method
&& this.pendingLookup.path === path) {
return this.pendingLookup.promise;
}
// >1 pending thing, and they are different, whine.
expect(false).toBe(
true, ">1 pending request. You should probably handle them. " +
"PENDING: " + JSON.stringify(this.pendingLookup) + " JUST GOT: " +
method + " " + path,
);
}
this.pendingLookup = {
promise: new Promise(() => {}),
method: method,
path: path,
};
return this.pendingLookup.promise;
}
if (next.path === path && next.method === method) {
logger.log(
"MatrixClient[UT] Matched. Returning " +
(next.error ? "BAD" : "GOOD") + " response",
);
if (next.expectBody) {
expect(next.expectBody).toEqual(data);
}
if (next.expectQueryParams) {
Object.keys(next.expectQueryParams).forEach(function(k) {
expect(qp[k]).toEqual(next.expectQueryParams[k]);
});
}
if (next.thenCall) {
process.nextTick(next.thenCall, 0); // next tick so we return first.
}
if (next.error) {
return Promise.reject({
errcode: next.error.errcode,
httpStatus: next.error.httpStatus,
name: next.error.errcode,
message: "Expected testing error",
data: next.error,
});
}
return Promise.resolve(next.data);
} else if (method === "GET" && path === "/sync" && this.ignoreUnhandledSync) {
logger.log("MatrixClient[UT] Ignoring.");
this.httpLookups.unshift(next);
return new Promise(() => {});
}
expect(true).toBe(false, "Expected different request. " + logLine);
return new Promise(() => {});
};
HttpResponse.KEEP_ALIVE_PATH = "/_matrix/client/versions";
HttpResponse.PUSH_RULES_RESPONSE = {
method: "GET",
path: "/pushrules/",
data: {},
};
HttpResponse.PUSH_RULES_RESPONSE = {
method: "GET",
path: "/pushrules/",
data: {},
};
HttpResponse.USER_ID = "@alice:bar";
HttpResponse.filterResponse = function(userId) {
const filterPath = "/user/" + encodeURIComponent(userId) + "/filter";
return {
method: "POST",
path: filterPath,
data: { filter_id: "f1lt3r" },
};
};
HttpResponse.SYNC_DATA = {
next_batch: "s_5_3",
presence: { events: [] },
rooms: {},
};
HttpResponse.SYNC_RESPONSE = {
method: "GET",
path: "/sync",
data: HttpResponse.SYNC_DATA,
};
HttpResponse.defaultResponses = function(userId) {
return [
HttpResponse.PUSH_RULES_RESPONSE,
HttpResponse.filterResponse(userId),
HttpResponse.SYNC_RESPONSE,
];
};
export function setHttpResponses(
httpBackend, responses,
) {
responses.forEach(response => {
httpBackend
.when(response.method, response.path)
.respond(200, response.data);
});
}
export const emitPromise = (e, k) => new Promise(r => e.once(k, r));
+282
View File
@@ -0,0 +1,282 @@
// eslint-disable-next-line no-restricted-imports
import EventEmitter from "events";
// load olm before the sdk if possible
import '../olm-loader';
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";
/**
* Return a promise that is resolved when the client next emits a
* SYNCING event.
* @param {Object} client The client
* @param {Number=} count Number of syncs to wait for (default 1)
* @return {Promise} Resolves once the client has emitted a SYNCING event
*/
export function syncPromise(client: MatrixClient, count = 1): Promise<void> {
if (count <= 0) {
return Promise.resolve();
}
const p = new Promise<void>((resolve) => {
const cb = (state: SyncState) => {
logger.log(`${Date.now()} syncPromise(${count}): ${state}`);
if (state === SyncState.Syncing) {
resolve();
} else {
client.once(ClientEvent.Sync, cb);
}
};
client.once(ClientEvent.Sync, cb);
});
return p.then(() => {
return syncPromise(client, count - 1);
});
}
/**
* Create a spy for an object and automatically spy its methods.
* @param {*} constr The class constructor (used with 'new')
* @param {string} name The name of the class
* @return {Object} An instantiated object with spied methods/properties.
*/
export function mock<T>(constr: { new(...args: any[]): T }, name: string): T {
// Based on http://eclipsesource.com/blogs/2014/03/27/mocks-in-jasmine-tests/
const HelperConstr = new Function(); // jshint ignore:line
HelperConstr.prototype = constr.prototype;
// @ts-ignore
const result = new HelperConstr();
result.toString = function() {
return "mock" + (name ? " of " + name : "");
};
for (const key of Object.getOwnPropertyNames(constr.prototype)) { // eslint-disable-line guard-for-in
try {
if (constr.prototype[key] instanceof Function) {
result[key] = jest.fn();
}
} catch (ex) {
// Direct access to some non-function fields of DOM prototypes may
// cause exceptions.
// Overwriting will not work either in that case.
}
}
return result;
}
interface IEventOpts {
type: EventType | string;
room: string;
sender?: string;
skey?: string;
content: IContent;
event?: boolean;
user?: string;
unsigned?: IUnsigned;
redacts?: string;
}
/**
* Create an Event.
* @param {Object} opts Values for the event.
* @param {string} opts.type The event.type
* @param {string} opts.room The event.room_id
* @param {string} opts.sender The event.sender
* @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.
* @return {Object} a JSON object representing this event.
*/
export function mkEvent(opts: IEventOpts): object | MatrixEvent {
if (!opts.type || !opts.content) {
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
}
const event: Partial<IEvent> = {
type: opts.type as string,
room_id: opts.room,
sender: opts.sender || opts.user, // opts.user for backwards-compat
content: opts.content,
unsigned: opts.unsigned || {},
event_id: "$" + Math.random() + "-" + Math.random(),
redacts: opts.redacts,
};
if (opts.skey !== undefined) {
event.state_key = opts.skey;
} else if ([
EventType.RoomName,
EventType.RoomTopic,
EventType.RoomCreate,
EventType.RoomJoinRules,
EventType.RoomPowerLevels,
EventType.RoomTopic,
"com.example.state",
].includes(opts.type)) {
event.state_key = "";
}
return opts.event ? new MatrixEvent(event) : event;
}
interface IPresenceOpts {
user?: string;
sender?: string;
url: string;
name: string;
ago: number;
presence?: string;
event?: boolean;
}
/**
* Create an m.presence event.
* @param {Object} opts Values for the presence.
* @return {Object|MatrixEvent} The event
*/
export function mkPresence(opts: IPresenceOpts): object | MatrixEvent {
const event = {
event_id: "$" + Math.random() + "-" + Math.random(),
type: "m.presence",
sender: opts.sender || opts.user, // opts.user for backwards-compat
content: {
avatar_url: opts.url,
displayname: opts.name,
last_active_ago: opts.ago,
presence: opts.presence || "offline",
},
};
return opts.event ? new MatrixEvent(event) : event;
}
interface IMembershipOpts {
room: string;
mship: string;
sender?: string;
user?: string;
skey?: string;
name?: string;
url?: string;
event?: boolean;
}
/**
* Create an m.room.member event.
* @param {Object} opts Values for the membership.
* @param {string} opts.room The room ID for the event.
* @param {string} opts.mship The content.membership for the event.
* @param {string} opts.sender The sender user ID for the event.
* @param {string} opts.skey The target user ID for the event if applicable
* e.g. for invites/bans.
* @param {string} opts.name The content.displayname for the event.
* @param {string} opts.url The content.avatar_url for the event.
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object|MatrixEvent} The event
*/
export function mkMembership(opts: IMembershipOpts): object | MatrixEvent {
const eventOpts: IEventOpts = {
...opts,
type: EventType.RoomMember,
content: {
membership: opts.mship,
},
};
if (!opts.skey) {
eventOpts.skey = opts.sender || opts.user;
}
if (opts.name) {
eventOpts.content.displayname = opts.name;
}
if (opts.url) {
eventOpts.content.avatar_url = opts.url;
}
return mkEvent(eventOpts);
}
interface IMessageOpts {
room: string;
user: string;
msg?: string;
event?: boolean;
}
/**
* Create an m.room.message event.
* @param {Object} opts Values for the message
* @param {string} opts.room The room ID for the event.
* @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.
* @return {Object|MatrixEvent} The event
*/
export function mkMessage(opts: IMessageOpts): object | MatrixEvent {
const eventOpts: IEventOpts = {
...opts,
type: EventType.RoomMessage,
content: {
msgtype: "m.text",
body: opts.msg,
},
};
if (!eventOpts.content.body) {
eventOpts.content.body = "Random->" + Math.random();
}
return mkEvent(eventOpts);
}
/**
* A mock implementation of webstorage
*
* @constructor
*/
export class MockStorageApi {
private data: Record<string, any> = {};
public get length() {
return Object.keys(this.data).length;
}
public key(i: number): any {
return Object.keys(this.data)[i];
}
public setItem(k: string, v: any): void {
this.data[k] = v;
}
public getItem(k: string): any {
return this.data[k] || null;
}
public removeItem(k: string): void {
delete this.data[k];
}
}
/**
* If an event is being decrypted, wait for it to finish being decrypted.
*
* @param {MatrixEvent} event
* @returns {Promise} promise which resolves (to `event`) when the event has been decrypted
*/
export async function awaitDecryption(event: MatrixEvent): Promise<MatrixEvent> {
// An event is not always decrypted ahead of time
// getClearContent is a good signal to know whether an event has been decrypted
// already
if (event.getClearContent() !== null) {
return event;
} else {
logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
return new Promise((resolve) => {
event.once(MatrixEventEvent.Decrypted, (ev) => {
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
resolve(ev);
});
});
}
}
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise(r => e.once(k, r));
+25
View File
@@ -13,6 +13,7 @@ import * as olmlib from "../../src/crypto/olmlib";
import { sleep } from "../../src/utils";
import { CRYPTO_ENABLED } from "../../src/client";
import { DeviceInfo } from "../../src/crypto/deviceinfo";
import { logger } from '../../src/logger';
const Olm = global.Olm;
@@ -400,4 +401,28 @@ describe("Crypto", function() {
expect(aliceClient.sendToDevice.mock.calls[2][2]).not.toBe(txnId);
});
});
describe('Secret storage', function() {
it("creates secret storage even if there is no keyInfo", async function() {
jest.spyOn(logger, 'log').mockImplementation(() => {});
jest.setTimeout(10000);
const client = (new TestClient("@a:example.com", "dev")).client;
await client.initCrypto();
client.crypto.getSecretStorageKey = async () => null;
client.crypto.isCrossSigningReady = async () => false;
client.crypto.baseApis.uploadDeviceSigningKeys = () => null;
client.crypto.baseApis.setAccountData = () => null;
client.crypto.baseApis.uploadKeySignatures = () => null;
client.crypto.baseApis.http.authedRequest = () => null;
const createSecretStorageKey = async () => {
return {
keyInfo: undefined, // Returning undefined here used to cause a crash
privateKey: Uint8Array.of(32, 33),
};
};
await client.crypto.bootstrapSecretStorage({
createSecretStorageKey,
});
});
});
});
+27 -5
View File
@@ -20,11 +20,33 @@ import anotherjson from 'another-json';
import * as olmlib from "../../../src/crypto/olmlib";
import { TestClient } from '../../TestClient';
import { HttpResponse, setHttpResponses } from '../../test-utils/test-utils';
import { resetCrossSigningKeys } from "./crypto-utils";
import { MatrixError } from '../../../src/http-api';
import { logger } from '../../../src/logger';
const PUSH_RULES_RESPONSE = {
method: "GET",
path: "/pushrules/",
data: {},
};
const filterResponse = function(userId) {
const filterPath = "/user/" + encodeURIComponent(userId) + "/filter";
return {
method: "POST",
path: filterPath,
data: { filter_id: "f1lt3r" },
};
};
function setHttpResponses(httpBackend, responses) {
responses.forEach(response => {
httpBackend
.when(response.method, response.path)
.respond(200, response.data);
});
}
async function makeTestClient(userInfo, options, keys) {
if (!keys) keys = {};
@@ -237,7 +259,7 @@ describe("Cross Signing", function() {
// feed sync result that includes master key, ssk, device key
const responses = [
HttpResponse.PUSH_RULES_RESPONSE,
PUSH_RULES_RESPONSE,
{
method: "POST",
path: "/keys/upload",
@@ -248,7 +270,7 @@ describe("Cross Signing", function() {
},
},
},
HttpResponse.filterResponse("@alice:example.com"),
filterResponse("@alice:example.com"),
{
method: "GET",
path: "/sync",
@@ -493,7 +515,7 @@ describe("Cross Signing", function() {
// - master key signed by her usk (pretend that it was signed by another
// of Alice's devices)
const responses = [
HttpResponse.PUSH_RULES_RESPONSE,
PUSH_RULES_RESPONSE,
{
method: "POST",
path: "/keys/upload",
@@ -504,7 +526,7 @@ describe("Cross Signing", function() {
},
},
},
HttpResponse.filterResponse("@alice:example.com"),
filterResponse("@alice:example.com"),
{
method: "GET",
path: "/sync",
+180
View File
@@ -0,0 +1,180 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixClient, MatrixEvent, MatrixEventEvent, MatrixScheduler, Room } from "../../src";
import { eventMapperFor } from "../../src/event-mapper";
import { IStore } from "../../src/store";
describe("eventMapperFor", function() {
let rooms: Room[] = [];
const userId = "@test:example.org";
let client: MatrixClient;
beforeEach(() => {
client = new MatrixClient({
baseUrl: "https://my.home.server",
accessToken: "my.access.token",
request: function() {} as any, // NOP
store: {
getRoom(roomId: string): Room | null {
return rooms.find(r => r.roomId === roomId);
},
} as IStore,
scheduler: {
setProcessFunction: jest.fn(),
} as unknown as MatrixScheduler,
userId: userId,
});
rooms = [];
});
it("should de-duplicate MatrixEvent instances by means of findEventById on the room object", async () => {
const roomId = "!room:example.org";
const room = new Room(roomId, client, userId);
rooms.push(room);
const mapper = eventMapperFor(client, {
preventReEmit: true,
decrypt: false,
});
const eventId = "$event1:server";
const eventDefinition = {
type: "m.room.message",
room_id: roomId,
sender: userId,
content: {
body: "body",
},
unsigned: {},
event_id: eventId,
};
const event = mapper(eventDefinition);
expect(event).toBeInstanceOf(MatrixEvent);
room.addLiveEvents([event]);
expect(room.findEventById(eventId)).toBe(event);
const event2 = mapper(eventDefinition);
expect(event).toBe(event2);
});
it("should not de-duplicate state events due to directionality of sentinel members", async () => {
const roomId = "!room:example.org";
const room = new Room(roomId, client, userId);
rooms.push(room);
const mapper = eventMapperFor(client, {
preventReEmit: true,
decrypt: false,
});
const eventId = "$event1:server";
const eventDefinition = {
type: "m.room.name",
room_id: roomId,
sender: userId,
content: {
name: "Room name",
},
unsigned: {},
event_id: eventId,
state_key: "",
};
const event = mapper(eventDefinition);
expect(event).toBeInstanceOf(MatrixEvent);
room.oldState.setStateEvents([event]);
room.currentState.setStateEvents([event]);
room.addLiveEvents([event]);
expect(room.findEventById(eventId)).toBe(event);
const event2 = mapper(eventDefinition);
expect(event).not.toBe(event2);
});
it("should decrypt appropriately", async () => {
const roomId = "!room:example.org";
const room = new Room(roomId, client, userId);
rooms.push(room);
const eventId = "$event1:server";
const eventDefinition = {
type: "m.room.encrypted",
room_id: roomId,
sender: userId,
content: {
ciphertext: "",
},
unsigned: {},
event_id: eventId,
};
const decryptEventIfNeededSpy = jest.spyOn(client, "decryptEventIfNeeded");
decryptEventIfNeededSpy.mockResolvedValue(); // stub it out
const mapper = eventMapperFor(client, {
decrypt: true,
});
const event = mapper(eventDefinition);
expect(event).toBeInstanceOf(MatrixEvent);
expect(decryptEventIfNeededSpy).toHaveBeenCalledWith(event);
});
it("should configure re-emitter appropriately", async () => {
const roomId = "!room:example.org";
const room = new Room(roomId, client, userId);
rooms.push(room);
const eventId = "$event1:server";
const eventDefinition = {
type: "m.room.message",
room_id: roomId,
sender: userId,
content: {
body: "body",
},
unsigned: {},
event_id: eventId,
};
const evListener = jest.fn();
client.on(MatrixEventEvent.Replaced, evListener);
const noReEmitMapper = eventMapperFor(client, {
preventReEmit: true,
});
const event1 = noReEmitMapper(eventDefinition);
expect(event1).toBeInstanceOf(MatrixEvent);
event1.emit(MatrixEventEvent.Replaced, event1);
expect(evListener).not.toHaveBeenCalled();
const reEmitMapper = eventMapperFor(client, {
preventReEmit: false,
});
const event2 = reEmitMapper(eventDefinition);
expect(event2).toBeInstanceOf(MatrixEvent);
event2.emit(MatrixEventEvent.Replaced, event2);
expect(evListener.mock.calls[0][0]).toEqual(event2);
expect(event1).not.toBe(event2); // the event wasn't added to a room so de-duplication wouldn't occur
});
});
+1 -1
View File
@@ -1,6 +1,6 @@
/*
Copyright 2017 New Vector Ltd
Copyright 2019 The Matrix.org Foundaction C.I.C.
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+9 -8
View File
@@ -1,4 +1,5 @@
import {
MatrixEvent,
RelationType,
} from "../../src";
import { FilterComponent } from "../../src/filter-component";
@@ -13,7 +14,7 @@ describe("Filter Component", function() {
content: { },
room: 'roomId',
event: true,
});
}) as MatrixEvent;
const checkResult = filter.check(event);
@@ -27,7 +28,7 @@ describe("Filter Component", function() {
content: { },
room: 'roomId',
event: true,
});
}) as MatrixEvent;
const checkResult = filter.check(event);
@@ -54,7 +55,7 @@ describe("Filter Component", function() {
},
},
},
});
}) as MatrixEvent;
expect(filter.check(threadRootNotParticipated)).toBe(false);
});
@@ -79,7 +80,7 @@ describe("Filter Component", function() {
user: '@someone-else:server.org',
room: 'roomId',
event: true,
});
}) as MatrixEvent;
expect(filter.check(threadRootParticipated)).toBe(true);
});
@@ -99,7 +100,7 @@ describe("Filter Component", function() {
[RelationType.Reference]: {},
},
},
});
}) as MatrixEvent;
expect(filter.check(referenceRelationEvent)).toBe(false);
});
@@ -122,7 +123,7 @@ describe("Filter Component", function() {
},
room: 'roomId',
event: true,
});
}) as MatrixEvent;
const eventWithMultipleRelations = mkEvent({
"type": "m.room.message",
@@ -147,7 +148,7 @@ describe("Filter Component", function() {
},
"room": 'roomId',
"event": true,
});
}) as MatrixEvent;
const noMatchEvent = mkEvent({
"type": "m.room.message",
@@ -159,7 +160,7 @@ describe("Filter Component", function() {
},
"room": 'roomId',
"event": true,
});
}) as MatrixEvent;
expect(filter.check(threadRootEvent)).toBe(true);
expect(filter.check(eventWithMultipleRelations)).toBe(true);
+21 -3
View File
@@ -1,3 +1,19 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { logger } from "../../src/logger";
import { MatrixClient } from "../../src/client";
import { Filter } from "../../src/filter";
@@ -16,6 +32,7 @@ import { Preset } from "../../src/@types/partials";
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";
jest.useFakeTimers();
@@ -941,6 +958,7 @@ describe("MatrixClient", function() {
it("partitions root events to room timeline and thread timeline", () => {
const supportsExperimentalThreads = client.supportsExperimentalThreads;
client.supportsExperimentalThreads = () => true;
const room = new Room("!room1:matrix.org", client, userId);
const rootEvent = new MatrixEvent({
"content": {},
@@ -963,9 +981,9 @@ describe("MatrixClient", function() {
expect(rootEvent.isThreadRoot).toBe(true);
const [room, threads] = client.partitionThreadedEvents([rootEvent]);
expect(room).toHaveLength(1);
expect(threads).toHaveLength(1);
const [roomEvents, threadEvents] = client.partitionThreadedEvents(room, [rootEvent]);
expect(roomEvents).toHaveLength(1);
expect(threadEvents).toHaveLength(1);
// Restore method
client.supportsExperimentalThreads = supportsExperimentalThreads;
+6 -2
View File
@@ -256,7 +256,7 @@ describe('Beacon', () => {
expect(beacon.livenessWatchInterval).not.toEqual(oldMonitor);
});
it('destroy kills liveness monitor', () => {
it('destroy kills liveness monitor and emits', () => {
// live beacon was created an hour ago
// and has a 3hr duration
const beacon = new Beacon(liveBeaconEvent);
@@ -267,10 +267,14 @@ describe('Beacon', () => {
// destroy the beacon
beacon.destroy();
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.Destroy, beacon.identifier);
// live forced to false
expect(beacon.isLive).toBe(false);
advanceDateAndTime(HOUR_MS * 2 + 1);
expect(emitSpy).not.toHaveBeenCalled();
// no additional calls
expect(emitSpy).toHaveBeenCalledTimes(1);
});
});
});
+72 -37
View File
@@ -252,56 +252,91 @@ describe("RoomState", function() {
);
});
it('adds new beacon info events to state and emits', () => {
const beaconEvent = makeBeaconInfoEvent(userA, roomId);
const emitSpy = jest.spyOn(state, 'emit');
describe('beacon events', () => {
it('adds new beacon info events to state and emits', () => {
const beaconEvent = makeBeaconInfoEvent(userA, roomId);
const emitSpy = jest.spyOn(state, 'emit');
state.setStateEvents([beaconEvent]);
state.setStateEvents([beaconEvent]);
expect(state.beacons.size).toEqual(1);
const beaconInstance = state.beacons.get(beaconEvent.getType());
expect(beaconInstance).toBeTruthy();
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.New, beaconEvent, beaconInstance);
});
expect(state.beacons.size).toEqual(1);
const beaconInstance = state.beacons.get(beaconEvent.getType());
expect(beaconInstance).toBeTruthy();
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.New, beaconEvent, beaconInstance);
});
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);
it('does not add redacted beacon info events to state', () => {
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId);
const redactionEvent = { event: { type: 'm.room.redaction' } };
redactedBeaconEvent.makeRedacted(redactionEvent);
const emitSpy = jest.spyOn(state, 'emit');
state.setStateEvents([beaconEvent]);
const beaconInstance = state.beacons.get(beaconEvent.getType());
expect(beaconInstance.isLive).toEqual(true);
state.setStateEvents([redactedBeaconEvent]);
state.setStateEvents([updatedBeaconEvent]);
// no beacon added
expect(state.beacons.size).toEqual(0);
expect(state.beacons.get(redactedBeaconEvent.getType)).toBeFalsy();
// no new beacon emit
expect(filterEmitCallsByEventType(BeaconEvent.New, emitSpy).length).toBeFalsy();
});
// same Beacon
expect(state.beacons.get(beaconEvent.getType())).toBe(beaconInstance);
// updated liveness
expect(state.beacons.get(beaconEvent.getType()).isLive).toEqual(false);
});
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);
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');
state.setStateEvents([beaconEvent]);
const beaconInstance = state.beacons.get(beaconEvent.getType());
expect(beaconInstance.isLive).toEqual(true);
const emitSpy = jest.spyOn(state, 'emit');
state.setStateEvents([updatedBeaconEvent]);
state.setStateEvents([liveBeaconEvent, deadBeaconEvent]);
// same Beacon
expect(state.beacons.get(beaconEvent.getType())).toBe(beaconInstance);
// updated liveness
expect(state.beacons.get(beaconEvent.getType()).isLive).toEqual(false);
});
// called once
expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(1);
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' } };
redactedBeaconEvent.makeRedacted(redactionEvent);
// live beacon is now not live
const updatedLiveBeaconEvent = makeBeaconInfoEvent(
userA, roomId, { isLive: false }, liveBeaconEvent.getId(), '$beacon1',
);
state.setStateEvents([beaconEvent]);
const beaconInstance = state.beacons.get(beaconEvent.getType());
const destroySpy = jest.spyOn(beaconInstance, 'destroy');
expect(beaconInstance.isLive).toEqual(true);
state.setStateEvents([updatedLiveBeaconEvent]);
state.setStateEvents([redactedBeaconEvent]);
expect(state.hasLiveBeacons).toBe(false);
expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(2);
expect(emitSpy).toHaveBeenCalledWith(RoomStateEvent.BeaconLiveness, state, false);
expect(destroySpy).toHaveBeenCalled();
expect(state.beacons.get(beaconEvent.getType())).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 emitSpy = jest.spyOn(state, 'emit');
state.setStateEvents([liveBeaconEvent, deadBeaconEvent]);
// called once
expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(1);
// live beacon is now not live
const updatedLiveBeaconEvent = makeBeaconInfoEvent(
userA, roomId, { isLive: false }, liveBeaconEvent.getId(), '$beacon1',
);
state.setStateEvents([updatedLiveBeaconEvent]);
expect(state.hasLiveBeacons).toBe(false);
expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(2);
expect(emitSpy).toHaveBeenCalledWith(RoomStateEvent.BeaconLiveness, state, false);
});
});
});
+654 -535
View File
File diff suppressed because it is too large Load Diff
+3 -8
View File
@@ -93,14 +93,7 @@ export enum RelationType {
Annotation = "m.annotation",
Replace = "m.replace",
Reference = "m.reference",
/**
* Note, "io.element.thread" is hardcoded
* Should be replaced with "m.thread" once MSC3440 lands
* Can not use `UnstableValue` as TypeScript does not
* allow computed values in enums
* https://github.com/microsoft/TypeScript/issues/27976
*/
Thread = "io.element.thread",
Thread = "m.thread",
}
export enum MsgType {
@@ -119,6 +112,8 @@ export const RoomCreateTypeField = "type";
export enum RoomType {
Space = "m.space",
UnstableCall = "org.matrix.msc3417.call",
ElementVideo = "io.element.video",
}
/**
+88 -524
View File
@@ -37,7 +37,6 @@ import { Filter, IFilterDefinition } from "./filter";
import { CallEventHandlerEvent, CallEventHandler, CallEventHandlerEventHandlerMap } from './webrtc/callEventHandler';
import * as utils from './utils';
import { sleep } from './utils';
import { Group } from "./models/group";
import { Direction, EventTimeline } from "./models/event-timeline";
import { IActionsObject, PushProcessor } from "./pushprocessor";
import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery";
@@ -783,11 +782,6 @@ export enum ClientEvent {
DeleteRoom = "deleteRoom",
SyncUnexpectedError = "sync.unexpectedError",
ClientWellKnown = "WellKnown.client",
/* @deprecated */
Group = "Group",
// The following enum members are both deprecated and in the wrong place, Groups haven't been TSified
GroupProfile = "Group.profile",
GroupMyMembership = "Group.myMembership",
}
type RoomEvents = RoomEvent.Name
@@ -856,9 +850,6 @@ export type ClientEventHandlerMap = {
[ClientEvent.DeleteRoom]: (roomId: string) => void;
[ClientEvent.SyncUnexpectedError]: (error: Error) => void;
[ClientEvent.ClientWellKnown]: (data: IClientWellKnown) => void;
[ClientEvent.Group]: (group: Group) => void;
[ClientEvent.GroupProfile]: (group: Group) => void;
[ClientEvent.GroupMyMembership]: (group: Group) => void;
} & RoomEventHandlerMap
& RoomStateEventHandlerMap
& CryptoEventHandlerMap
@@ -3241,27 +3232,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
}
/**
* Get the group for the given group ID.
* This function will return a valid group for any group for which a Group event
* has been emitted.
* @param {string} groupId The group ID
* @return {Group} The Group or null if the group is not known or there is no data store.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroup(groupId: string): Group {
return this.store.getGroup(groupId);
}
/**
* Retrieve all known groups.
* @return {Group[]} A list of groups, or an empty list if there is no data store.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroups(): Group[] {
return this.store.getGroups();
}
/**
* Get the config for the media repository.
* @param {module:client.callback} callback Optional.
@@ -3981,7 +3951,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
/**
* Returns the eventType that should be used taking encryption into account
* for a given eventType.
* @param {MatrixClient} client the client
* @param {string} roomId the room for the events `eventType` relates to
* @param {string} eventType the event type
* @return {string} the event type taking encryption into account
@@ -5210,7 +5179,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
room.currentState.setUnknownStateEvents(stateEvents);
}
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(matrixEvents);
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(room, matrixEvents);
room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline());
await this.processThreadEvents(room, threadedEvents, true);
@@ -5262,19 +5231,17 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param {string} eventId The ID of the event to look for
*
* @return {Promise} Resolves:
* {@link module:models/event-timeline~EventTimeline} including the given
* event
* {@link module:models/event-timeline~EventTimeline} including the given event
*/
public getEventTimeline(timelineSet: EventTimelineSet, eventId: string): Promise<EventTimeline> {
public async getEventTimeline(timelineSet: EventTimelineSet, eventId: string): Promise<EventTimeline> {
// don't allow any timeline support unless it's been enabled.
if (!this.timelineSupport) {
throw new Error("timeline support is disabled. Set the 'timelineSupport'" +
" parameter to true when creating MatrixClient to enable" +
" it.");
" parameter to true when creating MatrixClient to enable it.");
}
if (timelineSet.getTimelineForEvent(eventId)) {
return Promise.resolve(timelineSet.getTimelineForEvent(eventId));
return timelineSet.getTimelineForEvent(eventId);
}
const path = utils.encodeUri(
@@ -5284,56 +5251,82 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
},
);
let params = undefined;
let params: Record<string, string | string[]> = undefined;
if (this.clientOpts.lazyLoadMembers) {
params = { filter: JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER) };
}
// TODO: we should implement a backoff (as per scrollback()) to deal more
// nicely with HTTP errors.
const promise = this.http.authedRequest<any>(undefined, Method.Get, path, params).then(async (res) => { // TODO types
if (!res.event) {
throw new Error("'event' not in '/context' result - homeserver too old?");
}
// TODO: we should implement a backoff (as per scrollback()) to deal more nicely with HTTP errors.
const res = await this.http.authedRequest<any>(undefined, Method.Get, path, params); // TODO types
if (!res.event) {
throw new Error("'event' not in '/context' result - homeserver too old?");
}
// by the time the request completes, the event might have ended up in
// the timeline.
if (timelineSet.getTimelineForEvent(eventId)) {
return timelineSet.getTimelineForEvent(eventId);
}
// by the time the request completes, the event might have ended up in the timeline.
if (timelineSet.getTimelineForEvent(eventId)) {
return timelineSet.getTimelineForEvent(eventId);
}
// we start with the last event, since that's the point at which we
// have known state.
const mapper = this.getEventMapper();
const event = mapper(res.event);
const events = [
// we start with the last event, since that's the point at which we have known state.
// events_after is already backwards; events_before is forwards.
res.events_after.reverse();
const events = res.events_after
.concat([res.event])
.concat(res.events_before);
const matrixEvents = events.map(this.getEventMapper());
...res.events_after.reverse().map(mapper),
event,
...res.events_before.map(mapper),
];
let timeline = timelineSet.getTimelineForEvent(matrixEvents[0].getId());
if (!timeline) {
timeline = timelineSet.addTimeline();
timeline.initialiseState(res.state.map(this.getEventMapper()));
timeline.getState(EventTimeline.FORWARDS).paginationToken = res.end;
} else {
const stateEvents = res.state.map(this.getEventMapper());
timeline.getState(EventTimeline.BACKWARDS).setUnknownStateEvents(stateEvents);
// 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);
const thread = await timelineSet.room.createThreadFetchRoot(event.threadRootId, threadedEvents, true);
let nextBatch: string;
const response = await thread.fetchInitialEvents();
if (response?.nextBatch) {
nextBatch = response.nextBatch;
}
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(matrixEvents);
const opts: IRelationsRequestOpts = {
limit: 50,
};
timelineSet.addEventsToTimeline(timelineEvents, true, timeline, res.start);
await this.processThreadEvents(timelineSet.room, threadedEvents, true);
// Fetch events until we find the one we were asked for
while (!thread.findEventById(eventId)) {
if (nextBatch) {
opts.from = nextBatch;
}
// 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 anywhere, if it was later redacted, so we just
// return the timeline we first thought of.
return timelineSet.getTimelineForEvent(eventId) || timeline;
});
return promise;
({ nextBatch } = await thread.fetchEvents(opts));
}
return thread.liveTimeline;
}
// Here we handle non-thread timelines only, but still process any thread events to populate thread summaries.
let timeline = timelineSet.getTimelineForEvent(events[0].getId());
if (timeline) {
timeline.getState(EventTimeline.BACKWARDS).setUnknownStateEvents(res.state.map(mapper));
} else {
timeline = timelineSet.addTimeline();
timeline.initialiseState(res.state.map(mapper));
timeline.getState(EventTimeline.FORWARDS).paginationToken = res.end;
}
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(timelineSet.room, 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);
// 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
// anywhere, if it was later redacted, so we just return the timeline we first thought of.
return timelineSet.getTimelineForEvent(eventId)
?? timelineSet.room.findThreadForEvent(event)?.liveTimeline // for Threads degraded support
?? timeline;
}
/**
@@ -5453,11 +5446,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
matrixEvents[i] = event;
}
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(matrixEvents);
const timelineSet = eventTimeline.getTimelineSet();
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
await this.processThreadEvents(timelineSet.room, threadedEvents, backwards);
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
// 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
@@ -5491,10 +5481,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const token = res.end;
const matrixEvents = res.chunk.map(this.getEventMapper());
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(matrixEvents);
eventTimeline.getTimelineSet()
.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
const timelineSet = eventTimeline.getTimelineSet();
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(timelineSet.room, matrixEvents);
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
await this.processThreadEvents(room, threadedEvents, backwards);
// if we've hit the end of the timeline, we need to stop trying to
@@ -5971,7 +5960,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public searchRoomEvents(opts: IEventSearchOpts): Promise<ISearchResults> {
// TODO: support groups
// TODO: support search groups
const body = {
search_categories: {
@@ -6057,10 +6046,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// turn it back into a list.
searchResults.highlights = Array.from(highlights);
const mapper = this.getEventMapper();
// append the new results to our existing results
const resultsLength = roomEvents.results ? roomEvents.results.length : 0;
const resultsLength = roomEvents.results?.length ?? 0;
for (let i = 0; i < resultsLength; i++) {
const sr = SearchResult.fromJson(roomEvents.results[i], this.getEventMapper());
const sr = SearchResult.fromJson(roomEvents.results[i], mapper);
const room = this.getRoom(sr.context.getEvent().getRoomId());
if (room) {
// Copy over a known event sender if we can
@@ -6651,11 +6642,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
fetchedEventType,
opts);
const mapper = this.getEventMapper();
let originalEvent: MatrixEvent;
if (result.original_event) {
originalEvent = mapper(result.original_event);
}
const originalEvent = result.original_event ? mapper(result.original_event) : undefined;
let events = result.chunk.map(mapper);
if (fetchedEventType === EventType.RoomMessageEncrypted) {
const allEvents = originalEvent ? events.concat(originalEvent) : events;
await Promise.all(allEvents.map(e => this.decryptEventIfNeeded(e)));
@@ -6663,6 +6653,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
events = events.filter(e => e.getType() === eventType);
}
}
if (originalEvent && relationType === RelationType.Replace) {
events = events.filter(e => e.getSender() === originalEvent.getSender());
}
@@ -8838,368 +8829,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return new MSC3089TreeSpace(this, roomId);
}
// TODO: Remove this warning, alongside the functions
// See https://github.com/vector-im/element-web/issues/17532
// ======================================================
// ** ANCIENT APIS BELOW **
// ======================================================
/**
* @param {string} groupId
* @return {Promise} Resolves: Group summary object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroupSummary(groupId: string): Promise<any> {
const path = utils.encodeUri("/groups/$groupId/summary", { $groupId: groupId });
return this.http.authedRequest(undefined, Method.Get, path);
}
/**
* @param {string} groupId
* @return {Promise} Resolves: Group profile object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroupProfile(groupId: string): Promise<any> {
const path = utils.encodeUri("/groups/$groupId/profile", { $groupId: groupId });
return this.http.authedRequest(undefined, Method.Get, path);
}
/**
* @param {string} groupId
* @param {Object} profile The group profile object
* @param {string=} profile.name Name of the group
* @param {string=} profile.avatar_url MXC avatar URL
* @param {string=} profile.short_description A short description of the room
* @param {string=} profile.long_description A longer HTML description of the room
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public setGroupProfile(groupId: string, profile: any): Promise<any> {
const path = utils.encodeUri("/groups/$groupId/profile", { $groupId: groupId });
return this.http.authedRequest(
undefined, Method.Post, path, undefined, profile,
);
}
/**
* @param {string} groupId
* @param {object} policy The join policy for the group. Must include at
* least a 'type' field which is 'open' if anyone can join the group
* the group without prior approval, or 'invite' if an invite is
* required to join.
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public setGroupJoinPolicy(groupId: string, policy: any): Promise<any> {
const path = utils.encodeUri(
"/groups/$groupId/settings/m.join_policy",
{ $groupId: groupId },
);
return this.http.authedRequest(
undefined, Method.Put, path, undefined, {
'm.join_policy': policy,
},
);
}
/**
* @param {string} groupId
* @return {Promise} Resolves: Group users list object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroupUsers(groupId: string): Promise<any> {
const path = utils.encodeUri("/groups/$groupId/users", { $groupId: groupId });
return this.http.authedRequest(undefined, Method.Get, path);
}
/**
* @param {string} groupId
* @return {Promise} Resolves: Group users list object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroupInvitedUsers(groupId: string): Promise<any> {
const path = utils.encodeUri("/groups/$groupId/invited_users", { $groupId: groupId });
return this.http.authedRequest(undefined, Method.Get, path);
}
/**
* @param {string} groupId
* @return {Promise} Resolves: Group rooms list object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroupRooms(groupId: string): Promise<any> {
const path = utils.encodeUri("/groups/$groupId/rooms", { $groupId: groupId });
return this.http.authedRequest(undefined, Method.Get, path);
}
/**
* @param {string} groupId
* @param {string} userId
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public inviteUserToGroup(groupId: string, userId: string): Promise<any> {
const path = utils.encodeUri(
"/groups/$groupId/admin/users/invite/$userId",
{ $groupId: groupId, $userId: userId },
);
return this.http.authedRequest(undefined, Method.Put, path, undefined, {});
}
/**
* @param {string} groupId
* @param {string} userId
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public removeUserFromGroup(groupId: string, userId: string): Promise<any> {
const path = utils.encodeUri(
"/groups/$groupId/admin/users/remove/$userId",
{ $groupId: groupId, $userId: userId },
);
return this.http.authedRequest(undefined, Method.Put, path, undefined, {});
}
/**
* @param {string} groupId
* @param {string} userId
* @param {string} roleId Optional.
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public addUserToGroupSummary(groupId: string, userId: string, roleId: string): Promise<any> {
const path = utils.encodeUri(
roleId ?
"/groups/$groupId/summary/$roleId/users/$userId" :
"/groups/$groupId/summary/users/$userId",
{ $groupId: groupId, $roleId: roleId, $userId: userId },
);
return this.http.authedRequest(undefined, Method.Put, path, undefined, {});
}
/**
* @param {string} groupId
* @param {string} userId
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public removeUserFromGroupSummary(groupId: string, userId: string): Promise<any> {
const path = utils.encodeUri(
"/groups/$groupId/summary/users/$userId",
{ $groupId: groupId, $userId: userId },
);
return this.http.authedRequest(undefined, Method.Delete, path, undefined, {});
}
/**
* @param {string} groupId
* @param {string} roomId
* @param {string} categoryId Optional.
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public addRoomToGroupSummary(groupId: string, roomId: string, categoryId: string): Promise<any> {
const path = utils.encodeUri(
categoryId ?
"/groups/$groupId/summary/$categoryId/rooms/$roomId" :
"/groups/$groupId/summary/rooms/$roomId",
{ $groupId: groupId, $categoryId: categoryId, $roomId: roomId },
);
return this.http.authedRequest(undefined, Method.Put, path, undefined, {});
}
/**
* @param {string} groupId
* @param {string} roomId
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public removeRoomFromGroupSummary(groupId: string, roomId: string): Promise<any> {
const path = utils.encodeUri(
"/groups/$groupId/summary/rooms/$roomId",
{ $groupId: groupId, $roomId: roomId },
);
return this.http.authedRequest(undefined, Method.Delete, path, undefined, {});
}
/**
* @param {string} groupId
* @param {string} roomId
* @param {boolean} isPublic Whether the room-group association is visible to non-members
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public addRoomToGroup(groupId: string, roomId: string, isPublic: boolean): Promise<any> {
if (isPublic === undefined) {
isPublic = true;
}
const path = utils.encodeUri(
"/groups/$groupId/admin/rooms/$roomId",
{ $groupId: groupId, $roomId: roomId },
);
return this.http.authedRequest(undefined, Method.Put, path, undefined,
{ "m.visibility": { type: isPublic ? "public" : "private" } },
);
}
/**
* Configure the visibility of a room-group association.
* @param {string} groupId
* @param {string} roomId
* @param {boolean} isPublic Whether the room-group association is visible to non-members
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public updateGroupRoomVisibility(groupId: string, roomId: string, isPublic: boolean): Promise<any> {
// NB: The /config API is generic but there's not much point in exposing this yet as synapse
// is the only server to implement this. In future we should consider an API that allows
// arbitrary configuration, i.e. "config/$configKey".
const path = utils.encodeUri(
"/groups/$groupId/admin/rooms/$roomId/config/m.visibility",
{ $groupId: groupId, $roomId: roomId },
);
return this.http.authedRequest(undefined, Method.Put, path, undefined,
{ type: isPublic ? "public" : "private" },
);
}
/**
* @param {string} groupId
* @param {string} roomId
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public removeRoomFromGroup(groupId: string, roomId: string): Promise<any> {
const path = utils.encodeUri(
"/groups/$groupId/admin/rooms/$roomId",
{ $groupId: groupId, $roomId: roomId },
);
return this.http.authedRequest(undefined, Method.Delete, path, undefined, {});
}
/**
* @param {string} groupId
* @param {Object} opts Additional options to send alongside the acceptance.
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public acceptGroupInvite(groupId: string, opts = null): Promise<any> {
const path = utils.encodeUri(
"/groups/$groupId/self/accept_invite",
{ $groupId: groupId },
);
return this.http.authedRequest(undefined, Method.Put, path, undefined, opts || {});
}
/**
* @param {string} groupId
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public joinGroup(groupId: string): Promise<any> {
const path = utils.encodeUri(
"/groups/$groupId/self/join",
{ $groupId: groupId },
);
return this.http.authedRequest(undefined, Method.Put, path, undefined, {});
}
/**
* @param {string} groupId
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public leaveGroup(groupId: string): Promise<any> {
const path = utils.encodeUri(
"/groups/$groupId/self/leave",
{ $groupId: groupId },
);
return this.http.authedRequest(undefined, Method.Put, path, undefined, {});
}
/**
* @return {Promise} Resolves: The groups to which the user is joined
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getJoinedGroups(): Promise<any> {
const path = utils.encodeUri("/joined_groups", {});
return this.http.authedRequest(undefined, Method.Get, path);
}
/**
* @param {Object} content Request content
* @param {string} content.localpart The local part of the desired group ID
* @param {Object} content.profile Group profile object
* @return {Promise} Resolves: Object with key group_id: id of the created group
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public createGroup(content: any): Promise<any> {
const path = utils.encodeUri("/create_group", {});
return this.http.authedRequest(
undefined, Method.Post, path, undefined, content,
);
}
/**
* @param {string[]} userIds List of user IDs
* @return {Promise} Resolves: Object as exmaple below
*
* {
* "users": {
* "@bob:example.com": {
* "+example:example.com"
* }
* }
* }
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getPublicisedGroups(userIds: string[]): Promise<any> {
const path = utils.encodeUri("/publicised_groups", {});
return this.http.authedRequest(
undefined, Method.Post, path, undefined, { user_ids: userIds },
);
}
/**
* @param {string} groupId
* @param {boolean} isPublic Whether the user's membership of this group is made public
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public setGroupPublicity(groupId: string, isPublic: boolean): Promise<any> {
const path = utils.encodeUri(
"/groups/$groupId/self/update_publicity",
{ $groupId: groupId },
);
return this.http.authedRequest(undefined, Method.Put, path, undefined, {
publicise: isPublic,
});
}
/**
* @experimental
*/
@@ -9235,74 +8864,21 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return threadRoots;
}
private eventShouldLiveIn(event: MatrixEvent, room: Room, events: MatrixEvent[], roots: Set<string>): {
shouldLiveInRoom: boolean;
shouldLiveInThread: boolean;
threadId?: string;
} {
if (event.isThreadRoot) {
return {
shouldLiveInRoom: true,
shouldLiveInThread: true,
threadId: event.getId(),
};
}
// A thread relation is always only shown in a thread
if (event.isThreadRelation) {
return {
shouldLiveInRoom: false,
shouldLiveInThread: true,
threadId: event.relationEventId,
};
}
const parentEventId = event.getAssociatedId();
const parentEvent = room?.findEventById(parentEventId) || events.find((mxEv: MatrixEvent) => (
mxEv.getId() === parentEventId
));
// A reaction targetting the thread root needs to be routed to both the
// the main timeline and the associated thread
const targetingThreadRoot = parentEvent?.isThreadRoot || roots.has(event.relationEventId);
if (targetingThreadRoot) {
return {
shouldLiveInRoom: true,
shouldLiveInThread: true,
threadId: event.relationEventId,
};
}
// If the parent event also has an associated ID we want to re-run the
// computation for that parent event.
// In the case of the redaction of a reaction that targets a root event
// we want that redaction to be pushed to both timeline
if (parentEvent?.getAssociatedId()) {
return this.eventShouldLiveIn(parentEvent, room, events, roots);
} else {
// We've exhausted all scenarios, can safely assume that this event
// should live in the room timeline
return {
shouldLiveInRoom: true,
shouldLiveInThread: false,
};
}
}
public partitionThreadedEvents(events: MatrixEvent[]): [MatrixEvent[], MatrixEvent[]] {
// Indices to the events array, for readibility
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 room = this.getRoom(event.getRoomId());
const {
shouldLiveInRoom,
shouldLiveInThread,
threadId,
} = this.eventShouldLiveIn(event, room, events, threadRoots);
} = room.eventShouldLiveIn(event, events, threadRoots);
if (shouldLiveInRoom) {
memo[ROOM].push(event);
@@ -9523,18 +9099,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* });
*/
/**
* Fires whenever the sdk learns about a new group. <strong>This event
* is experimental and may change.</strong>
* @event module:client~MatrixClient#"Group"
* @param {Group} group The newly created, fully populated group.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
* @example
* matrixClient.on("Group", function(group){
* var groupId = group.groupId;
* });
*/
/**
* Fires whenever a new Room is added. This will fire when you are invited to a
* room, as well as when you join a room. <strong>This event is experimental and
+1 -1
View File
@@ -920,7 +920,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// secrets using it, in theory. We could move them to the new key but a)
// that would mean we'd need to prompt for the old passphrase, and b)
// it's not clear that would be the right thing to do anyway.
const { keyInfo, privateKey } = await createSecretStorageKey();
const { keyInfo = {} as IAddSecretStorageKeyOpts, privateKey } = await createSecretStorageKey();
newKeyId = await createSSSS(keyInfo, privateKey);
} else if (!storageExists && keyBackupInfo) {
// we have an existing backup, but no SSSS
@@ -325,7 +325,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
});
},
);
if (limit && session.length >= limit) {
if (limit && sessions.length >= limit) {
break;
}
}
+16 -2
View File
@@ -29,9 +29,22 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
const decrypt = options.decrypt !== false;
function mapper(plainOldJsObject: Partial<IEvent>) {
const event = new MatrixEvent(plainOldJsObject);
const room = client.getRoom(plainOldJsObject.room_id);
let event: MatrixEvent;
// If the event is already known to the room, let's re-use the model rather than duplicating.
// We avoid doing this to state events as they may be forward or backwards looking which tweaks behaviour.
if (room && plainOldJsObject.state_key === undefined) {
event = room.findEventById(plainOldJsObject.event_id);
}
if (!event || event.status) {
event = new MatrixEvent(plainOldJsObject);
} else {
// merge the latest unsigned data from the server
event.setUnsigned({ ...event.getUnsigned(), ...plainOldJsObject.unsigned });
}
const room = client.getRoom(event.getRoomId());
if (room?.threads.has(event.getId())) {
event.setThread(room.threads.get(event.getId()));
}
@@ -46,6 +59,7 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
client.decryptEventIfNeeded(event);
}
}
if (!preventReEmit) {
client.reEmitter.reEmit(event, [
MatrixEventEvent.Replaced,
-1
View File
@@ -30,7 +30,6 @@ export * from "./errors";
export * from "./models/beacon";
export * from "./models/event";
export * from "./models/room";
export * from "./models/group";
export * from "./models/event-timeline";
export * from "./models/event-timeline-set";
export * from "./models/room-member";
+5
View File
@@ -23,11 +23,13 @@ export enum BeaconEvent {
New = "Beacon.new",
Update = "Beacon.update",
LivenessChange = "Beacon.LivenessChange",
Destroy = "Destroy",
}
export type BeaconEventHandlerMap = {
[BeaconEvent.Update]: (event: MatrixEvent, beacon: Beacon) => void;
[BeaconEvent.LivenessChange]: (isLive: boolean, beacon: Beacon) => void;
[BeaconEvent.Destroy]: (beaconIdentifier: string) => void;
};
export const isTimestampInDuration = (
@@ -93,6 +95,9 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
if (this.livenessWatchInterval) {
clearInterval(this.livenessWatchInterval);
}
this._isLive = false;
this.emit(BeaconEvent.Destroy, this.identifier);
}
/**
+1 -1
View File
@@ -42,7 +42,7 @@ export class EventContext {
*
* @constructor
*/
constructor(ourEvent: MatrixEvent) {
constructor(public readonly ourEvent: MatrixEvent) {
this.timeline = [ourEvent];
}
+10 -2
View File
@@ -86,9 +86,17 @@ export interface IEvent {
unsigned: IUnsigned;
redacts?: string;
// v1 legacy fields
/**
* @deprecated
*/
user_id?: string;
/**
* @deprecated
*/
prev_content?: IContent;
/**
* @deprecated
*/
age?: number;
}
@@ -279,7 +287,7 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
public target: RoomMember = null;
public status: EventStatus = null;
public error: MatrixError = null;
public forwardLooking = true;
public forwardLooking = true; // only state events may be backwards looking
/* If the event is a `m.key.verification.request` (or to_device `m.key.verification.start`) event,
* `Crypto` will set this the `VerificationRequest` for the event
-100
View File
@@ -1,100 +0,0 @@
/*
Copyright 2017 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @module models/group
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
// eslint-disable-next-line no-restricted-imports
import { EventEmitter } from "events";
import * as utils from "../utils";
/**
* Construct a new Group.
*
* @param {string} groupId The ID of this group.
*
* @prop {string} groupId The ID of this group.
* @prop {string} name The human-readable display name for this group.
* @prop {string} avatarUrl The mxc URL for this group's avatar.
* @prop {string} myMembership The logged in user's membership of this group
* @prop {Object} inviter Infomation about the user who invited the logged in user
* to the group, if myMembership is 'invite'.
* @prop {string} inviter.userId The user ID of the inviter
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
export function Group(groupId) {
this.groupId = groupId;
this.name = null;
this.avatarUrl = null;
this.myMembership = null;
this.inviter = null;
}
utils.inherits(Group, EventEmitter);
Group.prototype.setProfile = function(name, avatarUrl) {
if (this.name === name && this.avatarUrl === avatarUrl) return;
this.name = name || this.groupId;
this.avatarUrl = avatarUrl;
this.emit("Group.profile", this);
};
Group.prototype.setMyMembership = function(membership) {
if (this.myMembership === membership) return;
this.myMembership = membership;
this.emit("Group.myMembership", this);
};
/**
* Sets the 'inviter' property. This does not emit an event (the inviter
* will only change when the user is revited / reinvited to a room),
* so set this before setting myMembership.
* @param {Object} inviter Infomation about who invited us to the room
*/
Group.prototype.setInviter = function(inviter) {
this.inviter = inviter;
};
/**
* Fires whenever a group's profile information is updated.
* This means the 'name' and 'avatarUrl' properties.
* @event module:client~MatrixClient#"Group.profile"
* @param {Group} group The group whose profile was updated.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
* @example
* matrixClient.on("Group.profile", function(group){
* var name = group.name;
* });
*/
/**
* Fires whenever the logged in user's membership status of
* the group is updated.
* @event module:client~MatrixClient#"Group.myMembership"
* @param {Group} group The group in which the user's membership changed
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
* @example
* matrixClient.on("Group.myMembership", function(group){
* var myMembership = group.myMembership;
* });
*/
+14 -1
View File
@@ -438,7 +438,19 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
*/
private setBeacon(event: MatrixEvent): void {
if (this.beacons.has(event.getType())) {
return this.beacons.get(event.getType()).update(event);
const beacon = this.beacons.get(event.getType());
if (event.isRedacted()) {
beacon.destroy();
this.beacons.delete(event.getType());
return;
}
return beacon.update(event);
}
if (event.isRedacted()) {
return;
}
const beacon = new Beacon(event);
@@ -446,6 +458,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
this.reEmitter.reEmit<BeaconEvent, BeaconEvent>(beacon, [
BeaconEvent.New,
BeaconEvent.Update,
BeaconEvent.Destroy,
BeaconEvent.LivenessChange,
]);
+272 -119
View File
@@ -23,7 +23,7 @@ import { Direction, EventTimeline } from "./event-timeline";
import { getHttpUriForMxc } from "../content-repo";
import * as utils from "../utils";
import { normalize } from "../utils";
import { IEvent, MatrixEvent } from "./event";
import { IEvent, IThreadBundledRelationship, MatrixEvent } from "./event";
import { EventStatus } from "./event-status";
import { RoomMember } from "./room-member";
import { IRoomSummary, RoomSummary } from "./room-summary";
@@ -32,6 +32,7 @@ import { TypedReEmitter } from '../ReEmitter';
import {
EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS,
EVENT_VISIBILITY_CHANGE_TYPE,
RelationType,
} from "../@types/event";
import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client";
import { GuestAccess, HistoryVisibility, JoinRule, ResizeMethod } from "../@types/partials";
@@ -148,6 +149,7 @@ export interface ICreateFilterOpts {
// timeline. Useful to disable for some filters that can't be achieved by the
// client in an efficient manner
prepopulateTimeline?: boolean;
useSyncEvents?: boolean;
pendingEvents?: boolean;
}
@@ -167,6 +169,7 @@ export enum RoomEvent {
type EmittedEvents = RoomEvent
| ThreadEvent.New
| ThreadEvent.Update
| ThreadEvent.NewReply
| RoomEvent.Timeline
| RoomEvent.TimelineReset;
@@ -1002,17 +1005,15 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
}
/**
* Get an event which is stored in our unfiltered timeline set or in a thread
* Get an event which is stored in our unfiltered timeline set, or in a thread
*
* @param {string} eventId event ID to look for
* @param {string} eventId event ID to look for
* @return {?module:models/event.MatrixEvent} the given event, or undefined if unknown
*/
public findEventById(eventId: string): MatrixEvent | undefined {
let event = this.getUnfilteredTimelineSet().findEventById(eventId);
if (event) {
return event;
} else {
if (!event) {
const threads = this.getThreads();
for (let i = 0; i < threads.length; i++) {
const thread = threads[i];
@@ -1022,6 +1023,8 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
}
}
}
return event;
}
/**
@@ -1201,10 +1204,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
timeline: EventTimeline,
paginationToken?: string,
): void {
timeline.getTimelineSet().addEventsToTimeline(
events, toStartOfTimeline,
timeline, paginationToken,
);
timeline.getTimelineSet().addEventsToTimeline(events, toStartOfTimeline, timeline, paginationToken);
}
/**
@@ -1346,6 +1346,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
filter: Filter,
{
prepopulateTimeline = true,
useSyncEvents = true,
pendingEvents = true,
}: ICreateFilterOpts = {},
): EventTimelineSet {
@@ -1358,8 +1359,10 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
RoomEvent.Timeline,
RoomEvent.TimelineReset,
]);
this.filteredTimelineSets[filter.filterId] = timelineSet;
this.timelineSets.push(timelineSet);
if (useSyncEvents) {
this.filteredTimelineSets[filter.filterId] = timelineSet;
this.timelineSets.push(timelineSet);
}
const unfilteredLiveTimeline = this.getLiveTimeline();
// Not all filter are possible to replicate client-side only
@@ -1387,7 +1390,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
timeline.getPaginationToken(EventTimeline.BACKWARDS),
EventTimeline.BACKWARDS,
);
} else {
} else if (useSyncEvents) {
const livePaginationToken = unfilteredLiveTimeline.getPaginationToken(Direction.Forward);
timelineSet
.getLiveTimeline()
@@ -1405,41 +1408,46 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
return timelineSet;
}
private async getThreadListFilter(filterType = ThreadFilterType.All): Promise<Filter> {
const myUserId = this.client.getUserId();
const filter = new Filter(myUserId);
const definition: IFilterDefinition = {
"room": {
"timeline": {
[FILTER_RELATED_BY_REL_TYPES.name]: [THREAD_RELATION_TYPE.name],
},
},
};
if (filterType === ThreadFilterType.My) {
definition.room.timeline[FILTER_RELATED_BY_SENDERS.name] = [myUserId];
}
filter.setDefinition(definition);
const filterId = await this.client.getOrCreateFilter(
`THREAD_PANEL_${this.roomId}_${filterType}`,
filter,
);
filter.filterId = filterId;
return filter;
}
private async createThreadTimelineSet(filterType?: ThreadFilterType): Promise<EventTimelineSet> {
let timelineSet: EventTimelineSet;
if (Thread.hasServerSideSupport) {
const myUserId = this.client.getUserId();
const filter = new Filter(myUserId);
const filter = await this.getThreadListFilter(filterType);
const definition: IFilterDefinition = {
"room": {
"timeline": {
[FILTER_RELATED_BY_REL_TYPES.name]: [THREAD_RELATION_TYPE.name],
},
},
};
if (filterType === ThreadFilterType.My) {
definition.room.timeline[FILTER_RELATED_BY_SENDERS.name] = [myUserId];
}
filter.setDefinition(definition);
const filterId = await this.client.getOrCreateFilter(
`THREAD_PANEL_${this.roomId}_${filterType}`,
filter,
);
filter.filterId = filterId;
timelineSet = this.getOrCreateFilteredTimelineSet(
filter,
{
prepopulateTimeline: false,
useSyncEvents: false,
pendingEvents: false,
},
);
// An empty pagination token allows to paginate from the very bottom of
// the timeline set.
timelineSet.getLiveTimeline().setPaginationToken("", EventTimeline.BACKWARDS);
} else {
timelineSet = new EventTimelineSet(this, {
pendingEvents: false,
@@ -1460,6 +1468,86 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
return timelineSet;
}
public threadsReady = false;
public async fetchRoomThreads(): Promise<void> {
if (this.threadsReady) {
return;
}
const allThreadsFilter = await this.getThreadListFilter();
const { chunk: events } = await this.client.createMessagesRequest(
this.roomId,
"",
Number.MAX_SAFE_INTEGER,
Direction.Backward,
allThreadsFilter,
);
if (!events.length) return;
// Sorted by last_reply origin_server_ts
const threadRoots = events
.map(this.client.getEventMapper())
.sort((eventA, eventB) => {
/**
* `origin_server_ts` in a decentralised world is far from ideal
* but for lack of any better, we will have to use this
* Long term the sorting should be handled by homeservers and this
* is only meant as a short term patch
*/
const threadAMetadata = eventA
.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread);
const threadBMetadata = eventB
.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread);
return threadAMetadata.latest_event.origin_server_ts - threadBMetadata.latest_event.origin_server_ts;
});
let latestMyThreadsRootEvent: MatrixEvent;
const roomState = this.getLiveTimeline().getState(EventTimeline.FORWARDS);
for (const rootEvent of threadRoots) {
this.threadsTimelineSets[0].addLiveEvent(
rootEvent,
DuplicateStrategy.Ignore,
false,
roomState,
);
const threadRelationship = rootEvent
.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread);
if (threadRelationship.current_user_participated) {
this.threadsTimelineSets[1].addLiveEvent(
rootEvent,
DuplicateStrategy.Ignore,
false,
roomState,
);
latestMyThreadsRootEvent = rootEvent;
}
if (!this.getThread(rootEvent.getId())) {
this.createThread(rootEvent, [], true);
}
}
this.client.decryptEventIfNeeded(threadRoots[threadRoots.length -1]);
if (latestMyThreadsRootEvent) {
this.client.decryptEventIfNeeded(latestMyThreadsRootEvent);
}
this.threadsReady = true;
this.on(ThreadEvent.NewReply, this.onThreadNewReply);
}
private onThreadNewReply(thread: Thread): void {
for (const timelineSet of this.threadsTimelineSets) {
timelineSet.removeEvent(thread.id);
timelineSet.addLiveEvent(thread.rootEvent);
}
}
/**
* Forget the timelineSet for this room with the given filter
*
@@ -1474,19 +1562,100 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
}
}
public findThreadForEvent(event: MatrixEvent): Thread | null {
if (!event) {
return null;
public eventShouldLiveIn(event: MatrixEvent, events?: MatrixEvent[], roots?: Set<string>): {
shouldLiveInRoom: boolean;
shouldLiveInThread: boolean;
threadId?: string;
} {
// A thread root is always shown in both timelines
if (event.isThreadRoot || roots?.has(event.getId())) {
return {
shouldLiveInRoom: true,
shouldLiveInThread: true,
threadId: event.getId(),
};
}
// A thread relation is always only shown in a thread
if (event.isThreadRelation) {
return this.threads.get(event.threadRootId);
} else if (event.isThreadRoot) {
return this.threads.get(event.getId());
} else {
const parentEvent = this.findEventById(event.getAssociatedId());
return this.findThreadForEvent(parentEvent);
return {
shouldLiveInRoom: false,
shouldLiveInThread: true,
threadId: event.relationEventId,
};
}
const parentEventId = event.getAssociatedId();
const parentEvent = this.findEventById(parentEventId) ?? events?.find(e => e.getId() === parentEventId);
// Treat relations and redactions as extensions of their parents so evaluate parentEvent instead
if (parentEvent && (event.isRelation() || event.isRedaction())) {
return this.eventShouldLiveIn(parentEvent, events, roots);
}
// Edge case where we know the event is a relation but don't have the parentEvent
if (roots?.has(event.relationEventId)) {
return {
shouldLiveInRoom: true,
shouldLiveInThread: true,
threadId: event.relationEventId,
};
}
// A reply directly to a thread response is shown as part of the thread only, this is to provide a better
// experience when communicating with users using clients without full threads support
if (parentEvent?.isThreadRelation) {
return {
shouldLiveInRoom: false,
shouldLiveInThread: true,
threadId: parentEvent.threadRootId,
};
}
// We've exhausted all scenarios, can safely assume that this event should live in the room timeline only
return {
shouldLiveInRoom: true,
shouldLiveInThread: false,
};
}
public findThreadForEvent(event?: MatrixEvent): Thread | null {
if (!event) return null;
const { threadId } = this.eventShouldLiveIn(event);
return threadId ? this.getThread(threadId) : null;
}
public async createThreadFetchRoot(
threadId: string,
events?: MatrixEvent[],
toStartOfTimeline?: boolean,
): Promise<Thread> {
let thread = this.getThread(threadId);
if (!thread) {
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);
}
} finally {
// 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);
}
}
}
return thread;
}
/**
@@ -1497,30 +1666,9 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
this.applyRedaction(event);
let thread = this.findThreadForEvent(event);
if (thread) {
thread.addEvent(event, toStartOfTimeline);
await thread.addEvent(event, toStartOfTimeline);
} else {
const events = [event];
let rootEvent = this.findEventById(event.threadRootId);
// If the rootEvent does not exist in the current sync, then look for
// it over the network
try {
let eventData;
if (event.threadRootId) {
eventData = await this.client.fetchRoomEvent(this.roomId, event.threadRootId);
}
if (!rootEvent) {
rootEvent = new MatrixEvent(eventData);
} else {
rootEvent.setUnsigned(eventData.unsigned);
}
} finally {
// 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);
}
thread = await this.createThreadFetchRoot(event.threadRootId, [event], toStartOfTimeline);
}
this.emit(ThreadEvent.Update, thread);
@@ -1544,12 +1692,12 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
room: this,
client: this.client,
});
// If we managed to create a thread and figure out its `id`
// then we can use it
// If we managed to create a thread and figure out its `id` then we can use it
if (thread.id) {
this.threads.set(thread.id, thread);
this.reEmitter.reEmit(thread, [
ThreadEvent.Update,
ThreadEvent.NewReply,
RoomEvent.Timeline,
RoomEvent.TimelineReset,
]);
@@ -1560,25 +1708,27 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
this.emit(ThreadEvent.New, thread, toStartOfTimeline);
this.threadsTimelineSets.forEach(timelineSet => {
if (thread.rootEvent) {
if (Thread.hasServerSideSupport) {
timelineSet.addLiveEvent(thread.rootEvent);
} else {
timelineSet.addEventToTimeline(
thread.rootEvent,
timelineSet.getLiveTimeline(),
toStartOfTimeline,
);
if (this.threadsReady) {
this.threadsTimelineSets.forEach(timelineSet => {
if (thread.rootEvent) {
if (Thread.hasServerSideSupport) {
timelineSet.addLiveEvent(thread.rootEvent);
} else {
timelineSet.addEventToTimeline(
thread.rootEvent,
timelineSet.getLiveTimeline(),
toStartOfTimeline,
);
}
}
}
});
});
}
return thread;
}
}
applyRedaction(event: MatrixEvent): void {
private applyRedaction(event: MatrixEvent): void {
if (event.isRedaction()) {
const redactId = event.event.redacts;
@@ -1805,12 +1955,11 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
* @param {module:models/event.MatrixEvent} event the relation event that needs to be aggregated.
*/
private aggregateNonLiveRelation(event: MatrixEvent): void {
const thread = this.findThreadForEvent(event);
if (thread) {
thread.timelineSet.aggregateRelations(event);
}
const { shouldLiveInRoom, threadId } = this.eventShouldLiveIn(event);
const thread = this.getThread(threadId);
thread?.timelineSet.aggregateRelations(event);
if (thread?.id === event.getAssociatedId() || !thread) {
if (shouldLiveInRoom) {
// TODO: We should consider whether this means it would be a better
// design to lift the relations handling up to the room instead.
for (let i = 0; i < this.timelineSets.length; i++) {
@@ -1866,12 +2015,11 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
// any, which is good, because we don't want to try decoding it again).
localEvent.handleRemoteEcho(remoteEvent.event);
const thread = this.findThreadForEvent(remoteEvent);
if (thread) {
thread.timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
}
const { shouldLiveInRoom, threadId } = this.eventShouldLiveIn(remoteEvent);
const thread = this.getThread(threadId);
thread?.timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
if (thread?.id === remoteEvent.getAssociatedId() || !thread) {
if (shouldLiveInRoom) {
for (let i = 0; i < this.timelineSets.length; i++) {
const timelineSet = this.timelineSets[i];
@@ -1937,11 +2085,11 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
// update the event id
event.replaceLocalEventId(newEventId);
const thread = this.findThreadForEvent(event);
if (thread) {
thread.timelineSet.replaceEventId(oldEventId, newEventId);
}
if (thread?.id === event.getAssociatedId() || !thread) {
const { shouldLiveInRoom, threadId } = this.eventShouldLiveIn(event);
const thread = this.getThread(threadId);
thread?.timelineSet.replaceEventId(oldEventId, newEventId);
if (shouldLiveInRoom) {
// if the event was already in the timeline (which will be the case if
// opts.pendingEventOrdering==chronological), we need to update the
// timeline map.
@@ -1952,12 +2100,10 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
} else if (newStatus == EventStatus.CANCELLED) {
// remove it from the pending event list, or the timeline.
if (this.pendingEventList) {
const idx = this.pendingEventList.findIndex(ev => ev.getId() === oldEventId);
if (idx !== -1) {
const [removedEvent] = this.pendingEventList.splice(idx, 1);
if (removedEvent.isRedaction()) {
this.revertRedactionLocalEcho(removedEvent);
}
const removedEvent = this.getPendingEvent(oldEventId);
this.removePendingEvent(oldEventId);
if (removedEvent.isRedaction()) {
this.revertRedactionLocalEcho(removedEvent);
}
}
this.removeEvent(oldEventId);
@@ -2003,13 +2149,12 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
* @throws If <code>duplicateStrategy</code> is not falsey, 'replace' or 'ignore'.
*/
public addLiveEvents(events: MatrixEvent[], duplicateStrategy?: DuplicateStrategy, fromCache = false): void {
let i;
if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
}
// sanity check that the live timeline is still live
for (i = 0; i < this.timelineSets.length; i++) {
for (let i = 0; i < this.timelineSets.length; i++) {
const liveTimeline = this.timelineSets[i].getLiveTimeline();
if (liveTimeline.getPaginationToken(EventTimeline.FORWARDS)) {
throw new Error(
@@ -2018,21 +2163,13 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
);
}
if (liveTimeline.getNeighbouringTimeline(EventTimeline.FORWARDS)) {
throw new Error(
"live timeline " + i + " is no longer live - " +
"it has a neighbouring timeline",
);
throw new Error(`live timeline ${i} is no longer live - it has a neighbouring timeline`);
}
}
for (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".
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);
const thread = this.findThreadForEvent(events[i]);
if (thread) {
thread.addEvent(events[i], true);
}
}
}
@@ -2425,7 +2562,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
/**
* Returns the type of the room from the `m.room.create` event content or undefined if none is set
* @returns {?string} the type of the room. Currently only RoomType.Space is known.
* @returns {?string} the type of the room.
*/
public getType(): RoomType | string | undefined {
const createEvent = this.currentState.getStateEvents(EventType.RoomCreate, "");
@@ -2447,6 +2584,22 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
return this.getType() === RoomType.Space;
}
/**
* Returns whether the room is a call-room as defined by MSC3417.
* @returns {boolean} true if the room's type is RoomType.UnstableCall
*/
public isCallRoom(): boolean {
return this.getType() === RoomType.UnstableCall;
}
/**
* Returns whether the room is a video room.
* @returns {boolean} true if the room's type is RoomType.ElementVideo
*/
public isElementVideoRoom(): boolean {
return this.getType() === RoomType.ElementVideo;
}
/**
* This is an internal method. Calculates the name of the room from the current
* room state.
+9 -4
View File
@@ -33,14 +33,19 @@ export class SearchResult {
public static fromJson(jsonObj: ISearchResult, eventMapper: EventMapper): SearchResult {
const jsonContext = jsonObj.context || {} as IResultContext;
const eventsBefore = jsonContext.events_before || [];
const eventsAfter = jsonContext.events_after || [];
let eventsBefore = (jsonContext.events_before || []).map(eventMapper);
let eventsAfter = (jsonContext.events_after || []).map(eventMapper);
const context = new EventContext(eventMapper(jsonObj.result));
// Filter out any contextual events which do not correspond to the same timeline (thread or room)
const threadRootId = context.ourEvent.threadRootId;
eventsBefore = eventsBefore.filter(e => e.threadRootId === threadRootId);
eventsAfter = eventsAfter.filter(e => e.threadRootId === threadRootId);
context.setPaginateToken(jsonContext.start, true);
context.addEvents(eventsBefore.map(eventMapper), true);
context.addEvents(eventsAfter.map(eventMapper), false);
context.addEvents(eventsBefore, true);
context.addEvents(eventsAfter, false);
context.setPaginateToken(jsonContext.end, false);
return new SearchResult(jsonObj.rank, context);
+36 -14
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixClient, RoomEvent } from "../matrix";
import { MatrixClient, RelationType, RoomEvent } from "../matrix";
import { TypedReEmitter } from "../ReEmitter";
import { IRelationsRequestOpts } from "../@types/requests";
import { IThreadBundledRelationship, MatrixEvent } from "./event";
@@ -24,6 +24,7 @@ import { Room } from './room';
import { TypedEventEmitter } from "./typed-event-emitter";
import { RoomState } from "./room-state";
import { ServerControlledNamespacedValue } from "../NamespacedValue";
import { logger } from "../logger";
export enum ThreadEvent {
New = "Thread.new",
@@ -93,14 +94,9 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
RoomEvent.TimelineReset,
]);
// If we weren't able to find the root event, it's probably missing
// 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
if (!rootEvent) {
this.id = opts?.initialEvents
?.find(event => event.isThreadRelation)?.relationEventId;
} else {
this.id = rootEvent.getId();
}
this.id = rootEvent?.getId() ?? opts?.initialEvents?.find(event => event.isThreadRelation)?.relationEventId;
this.initialiseThread(this.rootEvent);
opts?.initialEvents?.forEach(event => this.addEvent(event, false));
@@ -169,12 +165,12 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
this.addEventToTimeline(event, toStartOfTimeline);
await this.client.decryptEventIfNeeded(event, {});
}
if (Thread.hasServerSideSupport && this.initialEventsFetched) {
if (event.localTimestamp > this.lastReply().localTimestamp) {
this.addEventToTimeline(event, false);
}
} else if (!toStartOfTimeline &&
this.initialEventsFetched &&
event.localTimestamp > this.lastReply().localTimestamp
) {
await this.fetchEditsWhereNeeded(event);
this.addEventToTimeline(event, false);
}
if (!this._currentUserParticipated && event.getSender() === this.client.getUserId()) {
@@ -221,10 +217,28 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
const event = new MatrixEvent(bundledRelationship.latest_event);
this.setEventMetadata(event);
event.setThread(this);
this.lastEvent = event;
this.fetchEditsWhereNeeded(event);
}
}
// XXX: Workaround for https://github.com/matrix-org/matrix-spec-proposals/pull/2676/files#r827240084
private async fetchEditsWhereNeeded(...events: MatrixEvent[]): Promise<unknown> {
return Promise.all(events.filter(e => e.isEncrypted()).map((event: MatrixEvent) => {
return this.client.relations(this.roomId, event.getId(), RelationType.Replace, event.getType(), {
limit: 1,
}).then(relations => {
if (relations.events.length) {
event.makeReplaced(relations.events[0]);
}
}).catch(e => {
logger.error("Failed to load edits for encrypted thread event", e);
});
}));
}
public async fetchInitialEvents(): Promise<{
originalEvent: MatrixEvent;
events: MatrixEvent[];
@@ -235,6 +249,7 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
this.initialEventsFetched = true;
return null;
}
try {
const response = await this.fetchEvents();
this.initialEventsFetched = true;
@@ -253,6 +268,11 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
* Finds an event by ID in the current thread
*/
public findEventById(eventId: string) {
// Check the lastEvent as it may have been created based on a bundled relationship and not in a timeline
if (this.lastEvent?.getId() === eventId) {
return this.lastEvent;
}
return this.timelineSet.findEventById(eventId);
}
@@ -329,6 +349,8 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
events = [...events, originalEvent];
}
await this.fetchEditsWhereNeeded(...events);
await Promise.all(events.map(event => {
this.setEventMetadata(event);
return this.client.decryptEventIfNeeded(event);
+1 -25
View File
@@ -15,19 +15,17 @@ limitations under the License.
*/
import { EventType } from "../@types/event";
import { Group } from "../models/group";
import { Room } from "../models/room";
import { User } from "../models/user";
import { IEvent, MatrixEvent } from "../models/event";
import { Filter } from "../filter";
import { RoomSummary } from "../models/room-summary";
import { IMinimalEvent, IGroups, IRooms, ISyncResponse } from "../sync-accumulator";
import { IMinimalEvent, IRooms, ISyncResponse } from "../sync-accumulator";
import { IStartClientOpts } from "../client";
export interface ISavedSync {
nextBatch: string;
roomsData: IRooms;
groupsData: IGroups;
accountData: IMinimalEvent[];
}
@@ -53,28 +51,6 @@ export interface IStore {
*/
setSyncToken(token: string): void;
/**
* No-op.
* @param {Group} group
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
storeGroup(group: Group): void;
/**
* No-op.
* @param {string} groupId
* @return {null}
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
getGroup(groupId: string): Group | null;
/**
* No-op.
* @return {Array} An empty array.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
getGroups(): Group[];
/**
* No-op.
* @param {Room} room
+1 -5
View File
@@ -215,7 +215,6 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
this.syncAccumulator.accumulate({
next_batch: syncData.nextBatch,
rooms: syncData.roomsData,
groups: syncData.groupsData,
account_data: {
events: accountData,
},
@@ -405,7 +404,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
await Promise.all([
this.persistUserPresenceEvents(userTuples),
this.persistAccountData(syncData.accountData),
this.persistSyncData(syncData.nextBatch, syncData.roomsData, syncData.groupsData),
this.persistSyncData(syncData.nextBatch, syncData.roomsData),
]);
}
@@ -413,13 +412,11 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
* Persist rooms /sync data along with the next batch token.
* @param {string} nextBatch The next_batch /sync value.
* @param {Object} roomsData The 'rooms' /sync data from a SyncAccumulator
* @param {Object} groupsData The 'groups' /sync data from a SyncAccumulator
* @return {Promise} Resolves if the data was persisted.
*/
private persistSyncData(
nextBatch: string,
roomsData: ISyncResponse["rooms"],
groupsData: ISyncResponse["groups"],
): Promise<void> {
logger.log("Persisting sync data up to", nextBatch);
return utils.promiseTry<void>(() => {
@@ -429,7 +426,6 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
clobber: "-", // constant key so will always clobber
nextBatch,
roomsData,
groupsData,
}); // put == UPSERT
return txnAsPromise(txn).then();
});
-30
View File
@@ -20,7 +20,6 @@ limitations under the License.
*/
import { EventType } from "../@types/event";
import { Group } from "../models/group";
import { Room } from "../models/room";
import { User } from "../models/user";
import { IEvent, MatrixEvent } from "../models/event";
@@ -53,7 +52,6 @@ export interface IOpts {
*/
export class MemoryStore implements IStore {
private rooms: Record<string, Room> = {}; // roomId: Room
private groups: Record<string, Group> = {}; // groupId: Group
private users: Record<string, User> = {}; // userId: User
private syncToken: string = null;
// userId: {
@@ -90,34 +88,6 @@ export class MemoryStore implements IStore {
this.syncToken = token;
}
/**
* Store the given room.
* @param {Group} group The group to be stored
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public storeGroup(group: Group) {
this.groups[group.groupId] = group;
}
/**
* Retrieve a group by its group ID.
* @param {string} groupId The group ID.
* @return {Group} The group or null.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroup(groupId: string): Group | null {
return this.groups[groupId] || null;
}
/**
* Retrieve all known groups.
* @return {Group[]} A list of groups, which may be empty.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroups(): Group[] {
return Object.values(this.groups);
}
/**
* Store the given room.
* @param {Room} room The room to be stored. All properties must be stored.
-27
View File
@@ -20,7 +20,6 @@ limitations under the License.
*/
import { EventType } from "../@types/event";
import { Group } from "../models/group";
import { Room } from "../models/room";
import { User } from "../models/user";
import { IEvent, MatrixEvent } from "../models/event";
@@ -58,32 +57,6 @@ export class StubStore implements IStore {
this.fromToken = token;
}
/**
* No-op.
* @param {Group} group
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public storeGroup(group: Group) {}
/**
* No-op.
* @param {string} groupId
* @return {null}
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroup(groupId: string): Group | null {
return null;
}
/**
* No-op.
* @return {Array} An empty array.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroups(): Group[] {
return [];
}
/**
* No-op.
* @param {Room} room
-50
View File
@@ -128,12 +128,6 @@ interface IDeviceLists {
left: string[];
}
export interface IGroups {
[Category.Join]: object;
[Category.Invite]: object;
[Category.Leave]: object;
}
export interface ISyncResponse {
next_batch: string;
rooms: IRooms;
@@ -142,8 +136,6 @@ export interface ISyncResponse {
to_device?: IToDevice;
device_lists?: IDeviceLists;
device_one_time_keys_count?: Record<string, number>;
groups: IGroups; // unspecced
}
/* eslint-enable camelcase */
@@ -174,7 +166,6 @@ export interface ISyncData {
nextBatch: string;
accountData: IMinimalEvent[];
roomsData: IRooms;
groupsData: IGroups;
}
/**
@@ -197,13 +188,6 @@ export class SyncAccumulator {
// streaming from without losing events.
private nextBatch: string = null;
// { ('invite'|'join'|'leave'): $groupId: { ... sync 'group' data } }
private groups: Record<Category, object> = {
invite: {},
join: {},
leave: {},
};
/**
* @param {Object} opts
* @param {Number=} opts.maxTimelineEntries The ideal maximum number of
@@ -219,7 +203,6 @@ export class SyncAccumulator {
public accumulate(syncResponse: ISyncResponse, fromDatabase = false): void {
this.accumulateRooms(syncResponse, fromDatabase);
this.accumulateGroups(syncResponse);
this.accumulateAccountData(syncResponse);
this.nextBatch = syncResponse.next_batch;
}
@@ -505,38 +488,6 @@ export class SyncAccumulator {
}
}
/**
* Accumulate incremental /sync group data.
* @param {Object} syncResponse the complete /sync JSON
*/
private accumulateGroups(syncResponse: ISyncResponse): void {
if (!syncResponse.groups) {
return;
}
if (syncResponse.groups.invite) {
Object.keys(syncResponse.groups.invite).forEach((groupId) => {
this.accumulateGroup(groupId, Category.Invite, syncResponse.groups.invite[groupId]);
});
}
if (syncResponse.groups.join) {
Object.keys(syncResponse.groups.join).forEach((groupId) => {
this.accumulateGroup(groupId, Category.Join, syncResponse.groups.join[groupId]);
});
}
if (syncResponse.groups.leave) {
Object.keys(syncResponse.groups.leave).forEach((groupId) => {
this.accumulateGroup(groupId, Category.Leave, syncResponse.groups.leave[groupId]);
});
}
}
private accumulateGroup(groupId: string, category: Category, data: object): void {
for (const cat of [Category.Invite, Category.Leave, Category.Join]) {
delete this.groups[cat][groupId];
}
this.groups[category][groupId] = data;
}
/**
* Return everything under the 'rooms' key from a /sync response which
* represents all room data that should be stored. This should be paired
@@ -694,7 +645,6 @@ export class SyncAccumulator {
return {
nextBatch: this.nextBatch,
roomsData: data,
groupsData: this.groups,
accountData: accData,
};
}
+26 -113
View File
@@ -25,7 +25,6 @@ limitations under the License.
import { User, UserEvent } from "./models/user";
import { NotificationCountType, Room, RoomEvent } from "./models/room";
import { Group } from "./models/group";
import * as utils from "./utils";
import { IDeferred } from "./utils";
import { Filter } from "./filter";
@@ -35,7 +34,6 @@ import { logger } from './logger';
import { InvalidStoreError } from './errors';
import { ClientEvent, IStoredClientOpts, MatrixClient, PendingEventOrdering } from "./client";
import {
Category,
IEphemeral,
IInvitedRoom,
IInviteState,
@@ -213,21 +211,6 @@ export class SyncApi {
return room;
}
/**
* @param {string} groupId
* @return {Group}
*/
public createGroup(groupId: string): Group {
const client = this.client;
const group = new Group(groupId);
client.reEmitter.reEmit(group, [
ClientEvent.GroupProfile,
ClientEvent.GroupMyMembership,
]);
client.store.storeGroup(group);
return group;
}
/**
* @param {Room} room
* @private
@@ -244,6 +227,7 @@ export class SyncApi {
RoomStateEvent.Update,
BeaconEvent.New,
BeaconEvent.Update,
BeaconEvent.Destroy,
BeaconEvent.LivenessChange,
]);
@@ -293,15 +277,13 @@ export class SyncApi {
return client.http.authedRequest<any>( // TODO types
undefined, Method.Get, "/sync", qps as any, undefined, localTimeoutMs,
);
}).then((data) => {
}).then(async (data) => {
let leaveRooms = [];
if (data.rooms?.leave) {
leaveRooms = this.mapSyncResponseToRoomArray(data.rooms.leave);
}
const rooms = [];
leaveRooms.forEach(async (leaveObj) => {
return Promise.all(leaveRooms.map(async (leaveObj) => {
const room = leaveObj.room;
rooms.push(room);
if (!leaveObj.isBrandNewRoom) {
// the intention behind syncLeftRooms is to add in rooms which were
// *omitted* from the initial /sync. Rooms the user were joined to
@@ -315,25 +297,22 @@ export class SyncApi {
}
leaveObj.timeline = leaveObj.timeline || {};
const events = this.mapSyncEventsFormat(leaveObj.timeline, room);
const [timelineEvents, threadedEvents] = this.client.partitionThreadedEvents(events);
const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room);
// set the back-pagination token. Do this *before* adding any
// events so that clients can start back-paginating.
room.getLiveTimeline().setPaginationToken(leaveObj.timeline.prev_batch,
EventTimeline.BACKWARDS);
room.getLiveTimeline().setPaginationToken(leaveObj.timeline.prev_batch, EventTimeline.BACKWARDS);
this.processRoomEvents(room, stateEvents, timelineEvents);
await this.processThreadEvents(room, threadedEvents, false);
await this.processRoomEvents(room, stateEvents, events);
room.recalculate();
client.store.storeRoom(room);
client.emit(ClientEvent.Room, room);
this.processEventsForNotifs(room, events);
});
return rooms;
return room;
}));
});
}
@@ -768,7 +747,6 @@ export class SyncApi {
const data: ISyncResponse = {
next_batch: nextSyncToken,
rooms: savedSync.roomsData,
groups: savedSync.groupsData,
account_data: {
events: savedSync.accountData,
},
@@ -777,7 +755,7 @@ export class SyncApi {
try {
await this.processSyncResponse(syncEventData, data);
} catch (e) {
logger.error("Error processing cached sync", e.stack || e);
logger.error("Error processing cached sync", e);
}
// Don't emit a prepared if we've bailed because the store is invalid:
@@ -852,7 +830,7 @@ export class SyncApi {
} catch (e) {
// log the exception with stack if we have it, else fall back
// to the plain description
logger.error("Caught /sync error", e.stack || e);
logger.error("Caught /sync error", e);
// Emit the exception for client handling
this.client.emit(ClientEvent.SyncUnexpectedError, e);
@@ -1055,20 +1033,7 @@ export class SyncApi {
// timeline: { events: [], prev_batch: $token }
// }
// }
// },
// groups: {
// invite: {
// $groupId: {
// inviter: $inviter,
// profile: {
// avatar_url: $avatarUrl,
// name: $groupName,
// },
// },
// },
// join: {},
// leave: {},
// },
// }
// }
// TODO-arch:
@@ -1118,9 +1083,7 @@ export class SyncApi {
}
// handle to-device events
if (data.to_device && Array.isArray(data.to_device.events) &&
data.to_device.events.length > 0
) {
if (Array.isArray(data.to_device?.events) && data.to_device.events.length > 0) {
const cancelledKeyVerificationTxns = [];
data.to_device.events
.map(client.getEventMapper())
@@ -1172,20 +1135,6 @@ export class SyncApi {
this.catchingUp = false;
}
if (data.groups) {
if (data.groups.invite) {
this.processGroupSyncEntry(data.groups.invite, Category.Invite);
}
if (data.groups.join) {
this.processGroupSyncEntry(data.groups.join, Category.Join);
}
if (data.groups.leave) {
this.processGroupSyncEntry(data.groups.leave, Category.Leave);
}
}
// the returned json structure is a bit crap, so make it into a
// nicer form (array) after applying sanity to make sure we don't fail
// on missing keys (on the off chance)
@@ -1208,11 +1157,11 @@ export class SyncApi {
this.notifEvents = [];
// Handle invites
inviteRooms.forEach((inviteObj) => {
await utils.promiseMapSeries(inviteRooms, async (inviteObj) => {
const room = inviteObj.room;
const stateEvents = this.mapSyncEventsFormat(inviteObj.invite_state, room);
this.processRoomEvents(room, stateEvents);
await this.processRoomEvents(room, stateEvents);
if (inviteObj.isBrandNewRoom) {
room.recalculate();
client.store.storeRoom(room);
@@ -1319,10 +1268,7 @@ export class SyncApi {
}
}
const [timelineEvents, threadedEvents] = this.client.partitionThreadedEvents(events);
this.processRoomEvents(room, stateEvents, timelineEvents, syncEventData.fromCache);
await this.processThreadEvents(room, threadedEvents, false);
await this.processRoomEvents(room, stateEvents, events, syncEventData.fromCache);
// set summary after processing events,
// because it will trigger a name calculation
@@ -1363,8 +1309,7 @@ export class SyncApi {
};
await utils.promiseMapSeries(stateEvents, processRoomEvent);
await utils.promiseMapSeries(timelineEvents, processRoomEvent);
await utils.promiseMapSeries(threadedEvents, processRoomEvent);
await utils.promiseMapSeries(events, processRoomEvent);
ephemeralEvents.forEach(function(e) {
client.emit(ClientEvent.Event, e);
});
@@ -1381,16 +1326,13 @@ export class SyncApi {
});
// Handle leaves (e.g. kicked rooms)
leaveRooms.forEach(async (leaveObj) => {
await utils.promiseMapSeries(leaveRooms, async (leaveObj) => {
const room = leaveObj.room;
const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room);
const events = this.mapSyncEventsFormat(leaveObj.timeline, room);
const accountDataEvents = this.mapSyncEventsFormat(leaveObj.account_data);
const [timelineEvents, threadedEvents] = this.client.partitionThreadedEvents(events);
this.processRoomEvents(room, stateEvents, timelineEvents);
await this.processThreadEvents(room, threadedEvents, false);
await this.processRoomEvents(room, stateEvents, events);
room.addAccountData(accountDataEvents);
room.recalculate();
@@ -1404,10 +1346,7 @@ export class SyncApi {
stateEvents.forEach(function(e) {
client.emit(ClientEvent.Event, e);
});
timelineEvents.forEach(function(e) {
client.emit(ClientEvent.Event, e);
});
threadedEvents.forEach(function(e) {
events.forEach(function(e) {
client.emit(ClientEvent.Event, e);
});
accountDataEvents.forEach(function(e) {
@@ -1542,35 +1481,6 @@ export class SyncApi {
});
}
/**
* @param {Object} groupsSection Groups section object, eg. response.groups.invite
* @param {string} sectionName Which section this is ('invite', 'join' or 'leave')
*/
private processGroupSyncEntry(groupsSection: object, sectionName: Category) {
// Processes entries from 'groups' section of the sync stream
for (const groupId of Object.keys(groupsSection)) {
const groupInfo = groupsSection[groupId];
let group = this.client.store.getGroup(groupId);
const isBrandNew = group === null;
if (group === null) {
group = this.createGroup(groupId);
}
if (groupInfo.profile) {
group.setProfile(
groupInfo.profile.name, groupInfo.profile.avatar_url,
);
}
if (groupInfo.inviter) {
group.setInviter({ userId: groupInfo.inviter });
}
group.setMyMembership(sectionName);
if (isBrandNew) {
// Now we've filled in all the fields, emit the Group event
this.client.emit(ClientEvent.Group, group);
}
}
}
/**
* @param {Object} obj
* @return {Object[]}
@@ -1666,16 +1576,16 @@ export class SyncApi {
* @param {Room} room
* @param {MatrixEvent[]} stateEventList A list of state events. This is the state
* at the *START* of the timeline list if it is supplied.
* @param {MatrixEvent[]} [timelineEventList] A list of timeline events. Lower index
* @param {MatrixEvent[]} [timelineEventList] A list of timeline events, including threaded. Lower index
* @param {boolean} fromCache whether the sync response came from cache
* is earlier in time. Higher index is later.
*/
private processRoomEvents(
private async processRoomEvents(
room: Room,
stateEventList: MatrixEvent[],
timelineEventList?: MatrixEvent[],
fromCache = false,
): void {
): Promise<void> {
// If there are no events in the timeline yet, initialise it with
// the given state events
const liveTimeline = room.getLiveTimeline();
@@ -1725,11 +1635,14 @@ export class SyncApi {
room.oldState.setStateEvents(stateEventList || []);
room.currentState.setStateEvents(stateEventList || []);
}
// execute the timeline events. This will continue to diverge the current state
// Execute the timeline events. This will continue to diverge the current state
// 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.
room.addLiveEvents(timelineEventList || [], null, fromCache);
const [mainTimelineEvents, threadedEvents] = this.client.partitionThreadedEvents(room, timelineEventList || []);
room.addLiveEvents(mainTimelineEvents, null, fromCache);
await this.processThreadEvents(room, threadedEvents, false);
}
/**
+9 -18
View File
@@ -99,11 +99,11 @@ export class TimelineWindow {
*
* @return {Promise}
*/
public load(initialEventId?: string, initialWindowSize = 20): Promise<any> {
public load(initialEventId?: string, initialWindowSize = 20): Promise<void> {
// given an EventTimeline, find the event we were looking for, and initialise our
// fields so that the event in question is in the middle of the window.
const initFields = (timeline: EventTimeline) => {
let eventIndex;
let eventIndex: number;
const events = timeline.getEvents();
@@ -111,40 +111,31 @@ export class TimelineWindow {
// we were looking for the live timeline: initialise to the end
eventIndex = events.length;
} else {
for (let i = 0; i < events.length; i++) {
if (events[i].getId() == initialEventId) {
eventIndex = i;
break;
}
}
eventIndex = events.findIndex(e => e.getId() === initialEventId);
if (eventIndex === undefined) {
if (eventIndex < 0) {
throw new Error("getEventTimeline result didn't include requested event");
}
}
const endIndex = Math.min(events.length,
eventIndex + Math.ceil(initialWindowSize / 2));
const endIndex = Math.min(events.length, eventIndex + Math.ceil(initialWindowSize / 2));
const startIndex = Math.max(0, endIndex - initialWindowSize);
this.start = new TimelineIndex(timeline, startIndex - timeline.getBaseIndex());
this.end = new TimelineIndex(timeline, endIndex - timeline.getBaseIndex());
this.eventCount = endIndex - startIndex;
};
// We avoid delaying the resolution of the promise by a reactor tick if
// we already have the data we need, which is important to keep room-switching
// feeling snappy.
//
// We avoid delaying the resolution of the promise by a reactor tick if we already have the data we need,
// which is important to keep room-switching feeling snappy.
if (initialEventId) {
const timeline = this.timelineSet.getTimelineForEvent(initialEventId);
if (timeline) {
// hot-path optimization to save a reactor tick by replicating the sync check getTimelineForEvent does.
initFields(timeline);
return Promise.resolve(timeline);
return Promise.resolve();
}
const prom = this.client.getEventTimeline(this.timelineSet, initialEventId);
return prom.then(initFields);
return this.client.getEventTimeline(this.timelineSet, initialEventId).then(initFields);
} else {
const tl = this.timelineSet.getLiveTimeline();
initFields(tl);