UNPKG

@river-build/sdk

Version:

For more details, visit the following resources:

246 lines 10.1 kB
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