-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathtimeline.ts
184 lines (170 loc) · 5.53 KB
/
timeline.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
import debounce from 'lodash/debounce';
import { Room, type MatrixEvent } from 'matrix-js-sdk';
import {
type CardMessageContent,
type CardFragmentContent,
type MatrixEvent as DiscreteMatrixEvent,
} from 'https://cardstack.com/base/matrix-event';
import { eventDebounceMs } from '../matrix-utils';
import {
type Context,
type Event,
addRoomEvent,
updateRoomEvent,
} from './index';
export function onReceipt(context: Context) {
return async (e: MatrixEvent) => {
let userId = context.client?.credentials.userId;
if (userId) {
let eventIds = Object.keys(e.getContent());
for (let eventId of eventIds) {
let receipt = e.getContent()[eventId]['m.read'][userId];
if (receipt) {
context.addEventReadReceipt(eventId, { readAt: receipt.ts });
}
}
}
};
}
export function onTimeline(context: Context) {
return (e: MatrixEvent) => {
context.timelineQueue.push({ event: e });
debouncedTimelineDrain(context);
};
}
export function onUpdateEventStatus(context: Context) {
return (e: MatrixEvent, _room: Room, maybeOldEventId?: unknown) => {
if (typeof maybeOldEventId !== 'string') {
return;
}
context.timelineQueue.push({ event: e, oldEventId: maybeOldEventId });
debouncedTimelineDrain(context);
};
}
const debouncedTimelineDrain = debounce((context: Context) => {
drainTimeline(context);
}, eventDebounceMs);
async function drainTimeline(context: Context) {
await context.flushTimeline;
let eventsDrained: () => void;
context.flushTimeline = new Promise((res) => (eventsDrained = res));
let events = [...context.timelineQueue];
context.timelineQueue = [];
for (let { event, oldEventId } of events) {
await context.client?.decryptEventIfNeeded(event);
await processDecryptedEvent(
context,
{
...event.event,
status: event.status,
content: event.getContent() || undefined,
error: event.error ?? undefined,
},
oldEventId,
);
}
eventsDrained!();
}
async function processDecryptedEvent(
context: Context,
event: Event,
oldEventId?: string,
) {
let { room_id: roomId } = event;
if (!roomId) {
throw new Error(
`bug: roomId is undefined for event ${JSON.stringify(event, null, 2)}`,
);
}
let room = context.client?.getRoom(roomId);
if (!room) {
throw new Error(
`bug: should never get here--matrix sdk returned a null room for ${roomId}`,
);
}
let userId = context.client?.getUserId();
if (!userId) {
throw new Error(
`bug: userId is required for event ${JSON.stringify(event, null, 2)}`,
);
}
// We might still receive events from the rooms that the user has left.
let member = room.getMember(userId);
if (!member || member.membership !== 'join') {
return;
}
let roomState = context.getRoom(roomId);
// patch in any missing room events--this will support dealing with local
// echoes, migrating older histories as well as handle any matrix syncing gaps
// that might occur
if (
roomState &&
event.type === 'm.room.message' &&
event.content?.msgtype === 'org.boxel.message' &&
event.content.data
) {
let data = (
typeof event.content.data === 'string'
? JSON.parse(event.content.data)
: event.content.data
) as CardMessageContent['data'];
if (
'attachedCardsEventIds' in data &&
Array.isArray(data.attachedCardsEventIds)
) {
for (let attachedCardEventId of data.attachedCardsEventIds) {
let currentFragmentId: string | undefined = attachedCardEventId;
do {
let fragmentEvent = roomState.events.find(
(e: DiscreteMatrixEvent) => e.event_id === currentFragmentId,
);
let fragmentData: CardFragmentContent['data'];
if (!fragmentEvent) {
fragmentEvent = (await context.client?.fetchRoomEvent(
roomId,
currentFragmentId ?? '',
)) as DiscreteMatrixEvent;
if (
fragmentEvent.type !== 'm.room.message' ||
fragmentEvent.content.msgtype !== 'org.boxel.cardFragment'
) {
throw new Error(
`Expected event ${currentFragmentId} to be 'org.boxel.card' but was ${JSON.stringify(
fragmentEvent,
)}`,
);
}
await addRoomEvent(context, { ...fragmentEvent, status: null });
fragmentData = (
typeof fragmentEvent.content.data === 'string'
? JSON.parse((fragmentEvent.content as any).data)
: fragmentEvent.content.data
) as CardFragmentContent['data'];
} else {
if (
fragmentEvent.type !== 'm.room.message' ||
fragmentEvent.content.msgtype !== 'org.boxel.cardFragment'
) {
throw new Error(
`Expected event to be 'org.boxel.cardFragment' but was ${JSON.stringify(
fragmentEvent,
)}`,
);
}
fragmentData = fragmentEvent.content.data;
}
currentFragmentId = fragmentData?.nextFragment; // using '?' so we can be kind to older event schemas
} while (currentFragmentId);
}
}
}
if (oldEventId) {
await updateRoomEvent(context, event, oldEventId);
} else {
await addRoomEvent(context, event);
}
if (room.oldState.paginationToken != null) {
// we need to scroll back to capture any room events fired before this one
await context.client?.scrollback(room);
}
}