2021-07-28 22:15:52 +09:00
|
|
|
import EventEmitter from 'events';
|
|
|
|
import initMatrix from '../initMatrix';
|
|
|
|
import cons from './cons';
|
|
|
|
|
2021-11-18 17:02:12 +09:00
|
|
|
function isEdited(mEvent) {
|
|
|
|
return mEvent.getRelation()?.rel_type === 'm.replace';
|
|
|
|
}
|
|
|
|
|
|
|
|
function isReaction(mEvent) {
|
|
|
|
return mEvent.getType() === 'm.reaction';
|
|
|
|
}
|
|
|
|
|
|
|
|
function getRelateToId(mEvent) {
|
|
|
|
const relation = mEvent.getRelation();
|
|
|
|
return relation && relation.event_id;
|
|
|
|
}
|
|
|
|
|
|
|
|
function addToMap(myMap, mEvent) {
|
|
|
|
const relateToId = getRelateToId(mEvent);
|
|
|
|
if (relateToId === null) return null;
|
|
|
|
|
|
|
|
if (typeof myMap.get(relateToId) === 'undefined') myMap.set(relateToId, []);
|
|
|
|
myMap.get(relateToId).push(mEvent);
|
|
|
|
return mEvent;
|
|
|
|
}
|
|
|
|
|
2021-12-03 22:02:10 +09:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-08 00:34:07 +09:00
|
|
|
function isTimelineLinked(tm1, tm2) {
|
|
|
|
let tm = getFirstLinkedTimeline(tm1);
|
|
|
|
while (tm) {
|
|
|
|
if (tm === tm2) return true;
|
|
|
|
tm = tm.nextTimeline;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-07-28 22:15:52 +09:00
|
|
|
class RoomTimeline extends EventEmitter {
|
2021-12-08 17:19:47 +09:00
|
|
|
constructor(roomId, notifications) {
|
2021-07-28 22:15:52 +09:00
|
|
|
super();
|
2021-12-03 22:02:10 +09:00
|
|
|
// These are local timelines
|
|
|
|
this.timeline = [];
|
|
|
|
this.editedTimeline = new Map();
|
|
|
|
this.reactionTimeline = new Map();
|
|
|
|
this.typingMembers = new Set();
|
|
|
|
|
2021-07-28 22:15:52 +09:00
|
|
|
this.matrixClient = initMatrix.matrixClient;
|
|
|
|
this.roomId = roomId;
|
|
|
|
this.room = this.matrixClient.getRoom(roomId);
|
2021-12-08 17:19:47 +09:00
|
|
|
this.notifications = notifications;
|
2021-11-18 17:02:12 +09:00
|
|
|
|
2021-12-03 22:02:10 +09:00
|
|
|
this.liveTimeline = this.room.getLiveTimeline();
|
|
|
|
this.activeTimeline = this.liveTimeline;
|
2021-11-18 17:02:12 +09:00
|
|
|
|
2021-07-28 22:15:52 +09:00
|
|
|
this.isOngoingPagination = false;
|
|
|
|
this.ongoingDecryptionCount = 0;
|
2021-12-03 22:02:10 +09:00
|
|
|
this.initialized = false;
|
|
|
|
|
|
|
|
// 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 = [];
|
2021-12-04 18:55:14 +09:00
|
|
|
|
|
|
|
// TODO: don't clear these timeline cause there data can be used in other timeline
|
2021-12-08 00:34:07 +09:00
|
|
|
this.reactionTimeline.clear();
|
|
|
|
this.editedTimeline.clear();
|
2021-12-03 22:02:10 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
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 }));
|
2021-07-28 22:15:52 +09:00
|
|
|
|
2021-12-03 22:02:10 +09:00
|
|
|
return Promise.allSettled(decryptionPromises);
|
|
|
|
}
|
|
|
|
|
2021-12-08 00:34:07 +09:00
|
|
|
markAllAsRead() {
|
2021-12-03 22:02:10 +09:00
|
|
|
const readEventId = this.getReadUpToEventId();
|
2021-12-08 17:19:47 +09:00
|
|
|
this.notifications.deleteNoti(this.roomId);
|
2021-12-03 22:02:10 +09:00
|
|
|
if (this.timeline.length === 0) return;
|
|
|
|
const latestEvent = this.timeline[this.timeline.length - 1];
|
|
|
|
if (readEventId === latestEvent.getId()) return;
|
|
|
|
this.matrixClient.sendReadReceipt(latestEvent);
|
2021-12-08 00:34:07 +09:00
|
|
|
this.emit(cons.events.roomTimeline.MARKED_AS_READ, latestEvent);
|
2021-12-03 22:02:10 +09:00
|
|
|
}
|
|
|
|
|
2021-12-08 00:34:07 +09:00
|
|
|
hasEventInTimeline(eventId, timeline = this.activeTimeline) {
|
2021-12-03 22:02:10 +09:00
|
|
|
const timelineSet = this.getUnfilteredTimelineSet();
|
2021-12-08 00:34:07 +09:00
|
|
|
const eventTimeline = timelineSet.getTimelineForEvent(eventId);
|
|
|
|
if (!eventTimeline) return false;
|
|
|
|
return isTimelineLinked(eventTimeline, timeline);
|
2021-12-03 22:02:10 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
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)];
|
|
|
|
}
|
|
|
|
|
2021-12-08 00:34:07 +09:00
|
|
|
getUnreadEventIndex(readUpToEventId) {
|
|
|
|
if (!this.hasEventInTimeline(readUpToEventId)) return -1;
|
|
|
|
|
|
|
|
const readUpToEvent = this.findEventByIdInTimelineSet(readUpToEventId);
|
|
|
|
if (!readUpToEvent) return -1;
|
|
|
|
const rTs = readUpToEvent.getTs();
|
|
|
|
|
|
|
|
const tLength = this.timeline.length;
|
|
|
|
|
|
|
|
for (let i = 0; i < tLength; i += 1) {
|
|
|
|
const mEvent = this.timeline[i];
|
|
|
|
if (mEvent.getTs() > rTs) return i;
|
|
|
|
}
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2021-12-03 22:02:10 +09:00
|
|
|
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;
|
2021-12-08 00:34:07 +09:00
|
|
|
return this.timeline.splice(i, 1)[0];
|
2021-12-03 22:02:10 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
_listenEvents() {
|
|
|
|
this._listenRoomTimeline = (event, room, toStartOfTimeline, removed, data) => {
|
2021-07-28 22:15:52 +09:00
|
|
|
if (room.roomId !== this.roomId) return;
|
2021-12-03 22:02:10 +09:00
|
|
|
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;
|
2021-07-28 22:15:52 +09:00
|
|
|
|
|
|
|
if (event.isEncrypted()) {
|
2021-12-03 22:02:10 +09:00
|
|
|
// We will add this event after it is being decrypted.
|
2021-07-28 22:15:52 +09:00
|
|
|
this.ongoingDecryptionCount += 1;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-12-03 22:02:10 +09:00
|
|
|
// FIXME: An unencrypted plain event can come
|
|
|
|
// while previous event is still decrypting
|
|
|
|
// and has not been added to timeline
|
|
|
|
// causing unordered timeline view.
|
2021-07-28 22:15:52 +09:00
|
|
|
|
2021-11-18 17:02:12 +09:00
|
|
|
this.addToTimeline(event);
|
2021-12-03 22:02:10 +09:00
|
|
|
this.emit(cons.events.roomTimeline.EVENT, event);
|
2021-08-12 13:12:12 +09:00
|
|
|
};
|
|
|
|
|
2021-07-28 22:15:52 +09:00
|
|
|
this._listenDecryptEvent = (event) => {
|
|
|
|
if (event.getRoomId() !== this.roomId) return;
|
2021-12-03 22:02:10 +09:00
|
|
|
if (this.isOngoingPagination) return;
|
|
|
|
|
|
|
|
// Not a live event.
|
|
|
|
// so we don't need to process it here
|
|
|
|
if (this.ongoingDecryptionCount === 0) return;
|
2021-07-28 22:15:52 +09:00
|
|
|
|
2021-11-18 17:02:12 +09:00
|
|
|
if (this.ongoingDecryptionCount > 0) {
|
|
|
|
this.ongoingDecryptionCount -= 1;
|
|
|
|
}
|
2021-11-19 13:30:07 +09:00
|
|
|
this.addToTimeline(event);
|
2021-12-03 22:02:10 +09:00
|
|
|
this.emit(cons.events.roomTimeline.EVENT, event);
|
2021-11-18 17:02:12 +09:00
|
|
|
};
|
|
|
|
|
2021-12-08 00:34:07 +09:00
|
|
|
this._listenRedaction = (mEvent, room) => {
|
2021-11-18 17:02:12 +09:00
|
|
|
if (room.roomId !== this.roomId) return;
|
2021-12-08 00:34:07 +09:00
|
|
|
const rEvent = this.deleteFromTimeline(mEvent.event.redacts);
|
|
|
|
this.editedTimeline.delete(mEvent.event.redacts);
|
|
|
|
this.reactionTimeline.delete(mEvent.event.redacts);
|
|
|
|
this.emit(cons.events.roomTimeline.EVENT_REDACTED, rEvent, mEvent);
|
2021-07-28 22:15:52 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
this._listenTypingEvent = (event, member) => {
|
|
|
|
if (member.roomId !== this.roomId) return;
|
|
|
|
|
|
|
|
const isTyping = member.typing;
|
|
|
|
if (isTyping) this.typingMembers.add(member.userId);
|
|
|
|
else this.typingMembers.delete(member.userId);
|
|
|
|
this.emit(cons.events.roomTimeline.TYPING_MEMBERS_UPDATED, new Set([...this.typingMembers]));
|
|
|
|
};
|
|
|
|
this._listenReciptEvent = (event, room) => {
|
2021-12-03 22:02:10 +09:00
|
|
|
// we only process receipt for latest message here.
|
2021-07-28 22:15:52 +09:00
|
|
|
if (room.roomId !== this.roomId) return;
|
|
|
|
const receiptContent = event.getContent();
|
2021-12-03 22:02:10 +09:00
|
|
|
|
|
|
|
const mEvents = this.liveTimeline.getEvents();
|
|
|
|
const lastMEvent = mEvents[mEvents.length - 1];
|
|
|
|
const lastEventId = lastMEvent.getId();
|
2021-07-28 22:15:52 +09:00
|
|
|
const lastEventRecipt = receiptContent[lastEventId];
|
2021-12-03 22:02:10 +09:00
|
|
|
|
2021-07-28 22:15:52 +09:00
|
|
|
if (typeof lastEventRecipt === 'undefined') return;
|
|
|
|
if (lastEventRecipt['m.read']) {
|
2021-12-03 22:02:10 +09:00
|
|
|
this.emit(cons.events.roomTimeline.LIVE_RECEIPT);
|
2021-07-28 22:15:52 +09:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
this.matrixClient.on('Room.timeline', this._listenRoomTimeline);
|
2021-08-12 13:12:12 +09:00
|
|
|
this.matrixClient.on('Room.redaction', this._listenRedaction);
|
2021-07-28 22:15:52 +09:00
|
|
|
this.matrixClient.on('Event.decrypted', this._listenDecryptEvent);
|
|
|
|
this.matrixClient.on('RoomMember.typing', this._listenTypingEvent);
|
|
|
|
this.matrixClient.on('Room.receipt', this._listenReciptEvent);
|
|
|
|
}
|
|
|
|
|
|
|
|
removeInternalListeners() {
|
2021-12-03 22:02:10 +09:00
|
|
|
if (!this.initialized) return;
|
2021-07-28 22:15:52 +09:00
|
|
|
this.matrixClient.removeListener('Room.timeline', this._listenRoomTimeline);
|
2021-08-12 13:12:12 +09:00
|
|
|
this.matrixClient.removeListener('Room.redaction', this._listenRedaction);
|
2021-07-28 22:15:52 +09:00
|
|
|
this.matrixClient.removeListener('Event.decrypted', this._listenDecryptEvent);
|
|
|
|
this.matrixClient.removeListener('RoomMember.typing', this._listenTypingEvent);
|
|
|
|
this.matrixClient.removeListener('Room.receipt', this._listenReciptEvent);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default RoomTimeline;
|