@river-build/sdk
Version:
For more details, visit the following resources:
246 lines • 10.1 kB
JavaScript
import { getEditsId, getRedactsId, makeRedactionEvent, toEvent, toReplacedMessageEvent, } from './models/timelineEvent';
import { TimelineEvents } from './models/timelineEvents';
import { Reactions } from './models/reactions';
import { PendingReplacedEvents } from './models/pendingReplacedEvents';
import { ReplacedEvents } from './models/replacedEvents';
import { ThreadStats } from './models/threadStats';
import { Threads } from './models/threads';
export class MessageTimeline {
streamId;
userId;
riverConnection;
events = new TimelineEvents();
replacedEvents = new ReplacedEvents();
pendingReplacedEvents = new PendingReplacedEvents();
threadsStats = new ThreadStats();
threads = new Threads();
reactions = new Reactions();
// TODO: figure out a better way to do online check
// lastestEventByUser = new TimelineEvents()
// TODO: we probably wont need this for a while
filterFn = (_event, _kind) => {
return true;
};
constructor(streamId, userId, riverConnection) {
this.streamId = streamId;
this.userId = userId;
this.riverConnection = riverConnection;
//
}
initialize(stream) {
this.reset();
stream.off('streamUpdated', this.onStreamUpdated);
stream.off('streamLocalEventUpdated', this.onStreamLocalEventUpdated);
stream.on('streamUpdated', this.onStreamUpdated);
stream.on('streamLocalEventUpdated', this.onStreamLocalEventUpdated);
const events = stream.view.timeline
.map((event) => toEvent(event, this.userId))
.filter((event) => this.filterFn(event, stream.view.contentKind));
this.appendEvents(events, this.userId);
}
async scrollback() {
return this.riverConnection.callWithStream(this.streamId, async (client) => {
return client.scrollback(this.streamId).then(({ terminus, firstEvent }) => ({
terminus,
firstEvent: firstEvent ? toEvent(firstEvent, this.userId) : undefined,
}));
});
}
reset() {
this.events.reset();
this.threads.reset();
this.threadsStats.reset();
this.reactions.reset();
this.pendingReplacedEvents.reset();
this.replacedEvents.reset();
}
onStreamUpdated = (_streamId, kind, change) => {
const { prepended, appended, updated, confirmed } = change;
if (prepended) {
const events = prepended
.map((event) => toEvent(event, this.userId))
.filter((event) => this.filterFn(event, kind));
this.prependEvents(events, this.userId);
}
if (appended) {
const events = appended
.map((event) => toEvent(event, this.userId))
.filter((event) => this.filterFn(event, kind));
this.appendEvents(events, this.userId);
}
if (updated) {
const events = updated
.map((event) => toEvent(event, this.userId))
.filter((event) => this.filterFn(event, kind));
this.updateEvents(events, this.userId);
}
if (confirmed) {
const confirmations = confirmed.map((event) => ({
eventId: event.hashStr,
confirmedInBlockNum: event.miniblockNum,
confirmedEventNum: event.confirmedEventNum,
}));
this.confirmEvents(confirmations);
}
};
onStreamLocalEventUpdated = (_streamId, kind, localEventId, localEvent) => {
const event = toEvent(localEvent, this.userId);
if (this.filterFn(event, kind)) {
this.updateEvent(event, localEventId);
}
};
prependEvents(events, userId) {
for (const event of events.reverse()) {
const editsEventId = getEditsId(event.content);
const redactsEventId = getRedactsId(event.content);
if (redactsEventId) {
const redactedEvent = makeRedactionEvent(event);
this.prependEvent(userId, event);
this.replaceEvent(userId, redactsEventId, redactedEvent);
}
else if (editsEventId) {
this.replaceEvent(userId, editsEventId, event);
}
else {
this.prependEvent(userId, event);
}
}
}
prependEvent = (_userId, inTimelineEvent) => {
const pendingReplace = this.pendingReplacedEvents.get(inTimelineEvent.eventId);
const timelineEvent = pendingReplace
? toReplacedMessageEvent(inTimelineEvent, pendingReplace)
: inTimelineEvent;
this.events.prepend(timelineEvent);
this.reactions.addEvent(timelineEvent);
this.threads.add(timelineEvent);
this.threadsStats.add(this.userId, timelineEvent, this.events.value);
};
appendEvent(_userId, event) {
this.events.append(event);
this.threads.add(event);
this.threadsStats.add(this.userId, event, this.events.value);
this.reactions.addEvent(event);
}
replaceEvent(_userId, replacedEventId, event) {
const eventIndex = this.events.value.findIndex((e) => e.eventId === replacedEventId ||
(e.localEventId && e.localEventId === event.localEventId));
if (eventIndex === -1) {
// if we didn't find an event to replace..
const pendingReplace = this.pendingReplacedEvents.get(replacedEventId);
if (pendingReplace?.latestEventNum &&
event?.latestEventNum &&
pendingReplace.latestEventNum > event.latestEventNum) {
// if we already have a replacement here, leave it, because we sync backwards, we assume the first one is the correct one
return;
}
else {
// otherwise add it to the pending list
this.pendingReplacedEvents.add(replacedEventId, event);
return;
}
}
const oldEvent = this.events.value[eventIndex];
if (event?.latestEventNum &&
oldEvent?.latestEventNum &&
event.latestEventNum < oldEvent.latestEventNum) {
return;
}
const newEvent = toReplacedMessageEvent(oldEvent, event);
this.events.replace(event, eventIndex, this.events.value);
this.replacedEvents.add(event.eventId, oldEvent, newEvent);
this.reactions.removeEvent(oldEvent);
this.reactions.addEvent(newEvent);
this.threadsStats.remove(oldEvent);
this.threadsStats.add(this.userId, newEvent, this.events.value);
const threadTimeline = newEvent.threadParentId
? this.threads.get(newEvent.threadParentId)
: undefined;
const threadEventIndex = threadTimeline?.findIndex((e) => e.eventId === replacedEventId ||
(e.localEventId && e.localEventId === newEvent.localEventId)) ?? -1;
if (threadEventIndex !== -1) {
this.threads.replace(newEvent, threadEventIndex);
}
else {
this.threads.add(newEvent);
}
}
appendEvents(events, _userId) {
for (const event of events) {
this.processEvent(event);
}
}
updateEvents(events, _userId) {
for (const event of events) {
this.processEvent(event, event.eventId);
}
}
updateEvent(event, updatingEventId) {
this.processEvent(event, updatingEventId);
}
confirmEvents(confirmations) {
for (const confirmation of confirmations) {
this.confirmEvent(confirmation);
}
}
// Similar to replaceEvent, but we dont only swap out the confirmedInBlockNum and confirmedEventNum
confirmEvent(confirmation) {
const eventIndex = this.events.value.findIndex((e) => e.eventId === confirmation.eventId);
if (eventIndex === -1) {
return;
}
const oldEvent = this.events.value[eventIndex];
const newEvent = {
...oldEvent,
confirmedEventNum: confirmation.confirmedEventNum,
confirmedInBlockNum: confirmation.confirmedInBlockNum,
};
this.events.replace(newEvent, eventIndex, this.events.value);
this.replacedEvents.add(newEvent.eventId, oldEvent, newEvent);
// TODO: why we dont change reactions here?
}
// handle local pending events, redact and edits
processEvent(event, updatingEventId) {
const editsEventId = getEditsId(event.content);
const redactsEventId = getRedactsId(event.content);
if (redactsEventId) {
const redactedEvent = makeRedactionEvent(event);
this.replaceEvent(this.userId, redactsEventId, redactedEvent);
if (updatingEventId) {
// replace the formerly encrypted event
this.replaceEvent(this.userId, updatingEventId, event);
}
else {
this.appendEvent(this.userId, event);
}
}
else if (editsEventId) {
if (updatingEventId) {
// remove the formerly encrypted event
this.removeEvent(updatingEventId);
}
this.replaceEvent(this.userId, editsEventId, event);
}
else {
if (updatingEventId) {
// replace the formerly encrypted event
this.replaceEvent(this.userId, updatingEventId, event);
}
else {
this.appendEvent(this.userId, event);
}
}
}
removeEvent(eventId) {
const eventIndex = this.events.value.findIndex((e) => e.eventId == eventId);
if ((eventIndex ?? -1) < 0) {
return;
}
const event = this.events.value[eventIndex];
this.events.removeByIndex(eventIndex);
this.reactions.removeEvent(event);
this.threadsStats.remove(event);
this.threads.remove(event);
}
}
//# sourceMappingURL=timeline.js.map