mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-05 23:10:28 +03:00
Signed-off-by: Ajay Bura <ajbura@gmail.com>
This commit is contained in:
parent
0c0a978886
commit
38cbb87a62
21 changed files with 948 additions and 507 deletions
|
|
@ -24,52 +24,289 @@ function addToMap(myMap, mEvent) {
|
|||
return mEvent;
|
||||
}
|
||||
|
||||
function getFirstLinkedTimeline(timeline) {
|
||||
let tm = timeline;
|
||||
while (tm.prevTimeline) {
|
||||
tm = tm.prevTimeline;
|
||||
}
|
||||
return tm;
|
||||
}
|
||||
function getLastLinkedTimeline(timeline) {
|
||||
let tm = timeline;
|
||||
while (tm.nextTimeline) {
|
||||
tm = tm.nextTimeline;
|
||||
}
|
||||
return tm;
|
||||
}
|
||||
|
||||
function iterateLinkedTimelines(timeline, backwards, callback) {
|
||||
let tm = timeline;
|
||||
while (tm) {
|
||||
callback(tm);
|
||||
if (backwards) tm = tm.prevTimeline;
|
||||
else tm = tm.nextTimeline;
|
||||
}
|
||||
}
|
||||
|
||||
class RoomTimeline extends EventEmitter {
|
||||
constructor(roomId) {
|
||||
super();
|
||||
// These are local timelines
|
||||
this.timeline = [];
|
||||
this.editedTimeline = new Map();
|
||||
this.reactionTimeline = new Map();
|
||||
this.typingMembers = new Set();
|
||||
|
||||
this.matrixClient = initMatrix.matrixClient;
|
||||
this.roomId = roomId;
|
||||
this.room = this.matrixClient.getRoom(roomId);
|
||||
|
||||
this.timeline = new Map();
|
||||
this.editedTimeline = new Map();
|
||||
this.reactionTimeline = new Map();
|
||||
this.liveTimeline = this.room.getLiveTimeline();
|
||||
this.activeTimeline = this.liveTimeline;
|
||||
|
||||
this.isOngoingPagination = false;
|
||||
this.ongoingDecryptionCount = 0;
|
||||
this.typingMembers = new Set();
|
||||
this.initialized = false;
|
||||
|
||||
this._listenRoomTimeline = (event, room) => {
|
||||
// TODO: remove below line
|
||||
window.selectedRoom = this;
|
||||
}
|
||||
|
||||
isServingLiveTimeline() {
|
||||
return getLastLinkedTimeline(this.activeTimeline) === this.liveTimeline;
|
||||
}
|
||||
|
||||
canPaginateBackward() {
|
||||
const tm = getFirstLinkedTimeline(this.activeTimeline);
|
||||
return tm.getPaginationToken('b') !== null;
|
||||
}
|
||||
|
||||
canPaginateForward() {
|
||||
return !this.isServingLiveTimeline();
|
||||
}
|
||||
|
||||
isEncrypted() {
|
||||
return this.matrixClient.isRoomEncrypted(this.roomId);
|
||||
}
|
||||
|
||||
clearLocalTimelines() {
|
||||
this.timeline = [];
|
||||
this.reactionTimeline.clear();
|
||||
this.editedTimeline.clear();
|
||||
}
|
||||
|
||||
addToTimeline(mEvent) {
|
||||
if (mEvent.isRedacted()) return;
|
||||
if (isReaction(mEvent)) {
|
||||
addToMap(this.reactionTimeline, mEvent);
|
||||
return;
|
||||
}
|
||||
if (!cons.supportEventTypes.includes(mEvent.getType())) return;
|
||||
if (isEdited(mEvent)) {
|
||||
addToMap(this.editedTimeline, mEvent);
|
||||
return;
|
||||
}
|
||||
this.timeline.push(mEvent);
|
||||
}
|
||||
|
||||
_populateAllLinkedEvents(timeline) {
|
||||
const firstTimeline = getFirstLinkedTimeline(timeline);
|
||||
iterateLinkedTimelines(firstTimeline, false, (tm) => {
|
||||
tm.getEvents().forEach((mEvent) => this.addToTimeline(mEvent));
|
||||
});
|
||||
}
|
||||
|
||||
_populateTimelines() {
|
||||
this.clearLocalTimelines();
|
||||
this._populateAllLinkedEvents(this.activeTimeline);
|
||||
}
|
||||
|
||||
async _reset(eventId) {
|
||||
if (this.isEncrypted()) await this.decryptAllEventsOfTimeline(this.activeTimeline);
|
||||
this._populateTimelines();
|
||||
if (!this.initialized) {
|
||||
this.initialized = true;
|
||||
this._listenEvents();
|
||||
}
|
||||
this.emit(cons.events.roomTimeline.READY, eventId ?? null);
|
||||
}
|
||||
|
||||
async loadLiveTimeline() {
|
||||
this.activeTimeline = this.liveTimeline;
|
||||
await this._reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
async loadEventTimeline(eventId) {
|
||||
// we use first unfiltered EventTimelineSet for room pagination.
|
||||
const timelineSet = this.getUnfilteredTimelineSet();
|
||||
try {
|
||||
const eventTimeline = await this.matrixClient.getEventTimeline(timelineSet, eventId);
|
||||
this.activeTimeline = eventTimeline;
|
||||
await this._reset(eventId);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async paginateTimeline(backwards = false, limit = 30) {
|
||||
if (this.initialized === false) return false;
|
||||
if (this.isOngoingPagination) return false;
|
||||
|
||||
this.isOngoingPagination = true;
|
||||
|
||||
const timelineToPaginate = backwards
|
||||
? getFirstLinkedTimeline(this.activeTimeline)
|
||||
: getLastLinkedTimeline(this.activeTimeline);
|
||||
|
||||
if (timelineToPaginate.getPaginationToken(backwards ? 'b' : 'f') === null) {
|
||||
this.isOngoingPagination = false;
|
||||
this.emit(cons.events.roomTimeline.PAGINATED, backwards, 0, false);
|
||||
return false;
|
||||
}
|
||||
|
||||
const oldSize = this.timeline.length;
|
||||
try {
|
||||
const canPaginateMore = await this.matrixClient
|
||||
.paginateEventTimeline(timelineToPaginate, { backwards, limit });
|
||||
|
||||
if (this.isEncrypted()) await this.decryptAllEventsOfTimeline(this.activeTimeline);
|
||||
this._populateTimelines();
|
||||
|
||||
const loaded = this.timeline.length - oldSize;
|
||||
this.isOngoingPagination = false;
|
||||
this.emit(cons.events.roomTimeline.PAGINATED, backwards, loaded, canPaginateMore);
|
||||
return true;
|
||||
} catch {
|
||||
this.isOngoingPagination = false;
|
||||
this.emit(cons.events.roomTimeline.PAGINATED, backwards, 0, true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
decryptAllEventsOfTimeline(eventTimeline) {
|
||||
const decryptionPromises = eventTimeline
|
||||
.getEvents()
|
||||
.filter((event) => event.isEncrypted() && !event.clearEvent)
|
||||
.reverse()
|
||||
.map((event) => event.attemptDecryption(this.matrixClient.crypto, { isRetry: true }));
|
||||
|
||||
return Promise.allSettled(decryptionPromises);
|
||||
}
|
||||
|
||||
markAsRead() {
|
||||
const readEventId = this.getReadUpToEventId();
|
||||
if (this.timeline.length === 0) return;
|
||||
const latestEvent = this.timeline[this.timeline.length - 1];
|
||||
if (readEventId === latestEvent.getId()) return;
|
||||
this.matrixClient.sendReadReceipt(latestEvent);
|
||||
}
|
||||
|
||||
hasEventInLiveTimeline(eventId) {
|
||||
const timelineSet = this.getUnfilteredTimelineSet();
|
||||
return timelineSet.getTimelineForEvent(eventId) === this.liveTimeline;
|
||||
}
|
||||
|
||||
hasEventInActiveTimeline(eventId) {
|
||||
const timelineSet = this.getUnfilteredTimelineSet();
|
||||
return timelineSet.getTimelineForEvent(eventId) === this.activeTimeline;
|
||||
}
|
||||
|
||||
getUnfilteredTimelineSet() {
|
||||
return this.room.getUnfilteredTimelineSet();
|
||||
}
|
||||
|
||||
getLiveReaders() {
|
||||
const lastEvent = this.timeline[this.timeline.length - 1];
|
||||
const liveEvents = this.liveTimeline.getEvents();
|
||||
const lastLiveEvent = liveEvents[liveEvents.length - 1];
|
||||
|
||||
let readers = [];
|
||||
if (lastEvent) readers = this.room.getUsersReadUpTo(lastEvent);
|
||||
if (lastLiveEvent !== lastEvent) {
|
||||
readers.splice(readers.length, 0, ...this.room.getUsersReadUpTo(lastLiveEvent));
|
||||
}
|
||||
return [...new Set(readers)];
|
||||
}
|
||||
|
||||
getEventReaders(eventId) {
|
||||
const readers = [];
|
||||
let eventIndex = this.getEventIndex(eventId);
|
||||
if (eventIndex < 0) return this.getLiveReaders();
|
||||
for (; eventIndex < this.timeline.length; eventIndex += 1) {
|
||||
readers.splice(readers.length, 0, ...this.room.getUsersReadUpTo(this.timeline[eventIndex]));
|
||||
}
|
||||
return [...new Set(readers)];
|
||||
}
|
||||
|
||||
getReadUpToEventId() {
|
||||
return this.room.getEventReadUpTo(this.matrixClient.getUserId());
|
||||
}
|
||||
|
||||
getEventIndex(eventId) {
|
||||
return this.timeline.findIndex((mEvent) => mEvent.getId() === eventId);
|
||||
}
|
||||
|
||||
findEventByIdInTimelineSet(eventId, eventTimelineSet = this.getUnfilteredTimelineSet()) {
|
||||
return eventTimelineSet.findEventById(eventId);
|
||||
}
|
||||
|
||||
findEventById(eventId) {
|
||||
return this.timeline[this.getEventIndex(eventId)] ?? null;
|
||||
}
|
||||
|
||||
deleteFromTimeline(eventId) {
|
||||
const i = this.getEventIndex(eventId);
|
||||
if (i === -1) return undefined;
|
||||
return this.timeline.splice(i, 1);
|
||||
}
|
||||
|
||||
_listenEvents() {
|
||||
this._listenRoomTimeline = (event, room, toStartOfTimeline, removed, data) => {
|
||||
if (room.roomId !== this.roomId) return;
|
||||
if (this.isOngoingPagination) return;
|
||||
|
||||
// User is currently viewing the old events probably
|
||||
// no need to add this event and emit changes.
|
||||
if (this.isServingLiveTimeline() === false) return;
|
||||
|
||||
// We only process live events here
|
||||
if (!data.liveEvent) return;
|
||||
|
||||
if (event.isEncrypted()) {
|
||||
// We will add this event after it is being decrypted.
|
||||
this.ongoingDecryptionCount += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.ongoingDecryptionCount !== 0) return;
|
||||
if (this.isOngoingPagination) return;
|
||||
// FIXME: An unencrypted plain event can come
|
||||
// while previous event is still decrypting
|
||||
// and has not been added to timeline
|
||||
// causing unordered timeline view.
|
||||
|
||||
this.addToTimeline(event);
|
||||
this.emit(cons.events.roomTimeline.EVENT);
|
||||
this.emit(cons.events.roomTimeline.EVENT, event);
|
||||
};
|
||||
|
||||
this._listenDecryptEvent = (event) => {
|
||||
if (event.getRoomId() !== this.roomId) return;
|
||||
if (this.isOngoingPagination) return;
|
||||
|
||||
// Not a live event.
|
||||
// so we don't need to process it here
|
||||
if (this.ongoingDecryptionCount === 0) return;
|
||||
|
||||
if (this.ongoingDecryptionCount > 0) {
|
||||
this.ongoingDecryptionCount -= 1;
|
||||
}
|
||||
if (this.ongoingDecryptionCount > 0) return;
|
||||
|
||||
if (this.isOngoingPagination) return;
|
||||
this.addToTimeline(event);
|
||||
this.emit(cons.events.roomTimeline.EVENT);
|
||||
this.emit(cons.events.roomTimeline.EVENT, event);
|
||||
};
|
||||
|
||||
this._listenRedaction = (event, room) => {
|
||||
if (room.roomId !== this.roomId) return;
|
||||
this.timeline.delete(event.getId());
|
||||
this.deleteFromTimeline(event.getId());
|
||||
this.editedTimeline.delete(event.getId());
|
||||
this.reactionTimeline.delete(event.getId());
|
||||
this.emit(cons.events.roomTimeline.EVENT);
|
||||
|
|
@ -84,15 +321,18 @@ class RoomTimeline extends EventEmitter {
|
|||
this.emit(cons.events.roomTimeline.TYPING_MEMBERS_UPDATED, new Set([...this.typingMembers]));
|
||||
};
|
||||
this._listenReciptEvent = (event, room) => {
|
||||
// we only process receipt for latest message here.
|
||||
if (room.roomId !== this.roomId) return;
|
||||
const receiptContent = event.getContent();
|
||||
if (this.timeline.length === 0) return;
|
||||
const tmlLastEvent = room.timeline[room.timeline.length - 1];
|
||||
const lastEventId = tmlLastEvent.getId();
|
||||
|
||||
const mEvents = this.liveTimeline.getEvents();
|
||||
const lastMEvent = mEvents[mEvents.length - 1];
|
||||
const lastEventId = lastMEvent.getId();
|
||||
const lastEventRecipt = receiptContent[lastEventId];
|
||||
|
||||
if (typeof lastEventRecipt === 'undefined') return;
|
||||
if (lastEventRecipt['m.read']) {
|
||||
this.emit(cons.events.roomTimeline.READ_RECEIPT);
|
||||
this.emit(cons.events.roomTimeline.LIVE_RECEIPT);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -101,62 +341,10 @@ class RoomTimeline extends EventEmitter {
|
|||
this.matrixClient.on('Event.decrypted', this._listenDecryptEvent);
|
||||
this.matrixClient.on('RoomMember.typing', this._listenTypingEvent);
|
||||
this.matrixClient.on('Room.receipt', this._listenReciptEvent);
|
||||
|
||||
// TODO: remove below line when release
|
||||
window.selectedRoom = this;
|
||||
|
||||
if (this.isEncryptedRoom()) this.room.decryptAllEvents();
|
||||
this._populateTimelines();
|
||||
}
|
||||
|
||||
isEncryptedRoom() {
|
||||
return this.matrixClient.isRoomEncrypted(this.roomId);
|
||||
}
|
||||
|
||||
addToTimeline(mEvent) {
|
||||
if (isReaction(mEvent)) {
|
||||
addToMap(this.reactionTimeline, mEvent);
|
||||
return;
|
||||
}
|
||||
if (!cons.supportEventTypes.includes(mEvent.getType())) return;
|
||||
if (isEdited(mEvent)) {
|
||||
addToMap(this.editedTimeline, mEvent);
|
||||
return;
|
||||
}
|
||||
this.timeline.set(mEvent.getId(), mEvent);
|
||||
}
|
||||
|
||||
_populateTimelines() {
|
||||
this.timeline.clear();
|
||||
this.reactionTimeline.clear();
|
||||
this.editedTimeline.clear();
|
||||
this.room.timeline.forEach((mEvent) => this.addToTimeline(mEvent));
|
||||
}
|
||||
|
||||
paginateBack() {
|
||||
if (this.isOngoingPagination) return;
|
||||
this.isOngoingPagination = true;
|
||||
|
||||
const oldSize = this.timeline.size;
|
||||
const MSG_LIMIT = 30;
|
||||
this.matrixClient.scrollback(this.room, MSG_LIMIT).then(async (room) => {
|
||||
if (room.oldState.paginationToken === null) {
|
||||
// We have reached start of the timeline
|
||||
this.isOngoingPagination = false;
|
||||
if (this.isEncryptedRoom()) await this.room.decryptAllEvents();
|
||||
this.emit(cons.events.roomTimeline.PAGINATED, false, 0);
|
||||
return;
|
||||
}
|
||||
this._populateTimelines();
|
||||
const loaded = this.timeline.size - oldSize;
|
||||
|
||||
if (this.isEncryptedRoom()) await this.room.decryptAllEvents();
|
||||
this.isOngoingPagination = false;
|
||||
this.emit(cons.events.roomTimeline.PAGINATED, true, loaded);
|
||||
});
|
||||
}
|
||||
|
||||
removeInternalListeners() {
|
||||
if (!this.initialized) return;
|
||||
this.matrixClient.removeListener('Room.timeline', this._listenRoomTimeline);
|
||||
this.matrixClient.removeListener('Room.redaction', this._listenRedaction);
|
||||
this.matrixClient.removeListener('Event.decrypted', this._listenDecryptEvent);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue