cinny/src/client/state/RoomTimeline.js

386 lines
12 KiB
JavaScript
Raw Normal View History

2021-07-28 22:15:52 +09:00
import EventEmitter from 'events';
import initMatrix from '../initMatrix';
import cons from './cons';
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;
const mEventId = mEvent.getId();
if (typeof myMap.get(relateToId) === 'undefined') myMap.set(relateToId, []);
const mEvents = myMap.get(relateToId);
if (mEvents.find((ev) => ev.getId() === mEventId)) return mEvent;
mEvents.push(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;
}
}
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 {
constructor(roomId, notifications) {
2021-07-28 22:15:52 +09:00
super();
// 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);
this.notifications = notifications;
this.liveTimeline = this.room.getLiveTimeline();
this.activeTimeline = this.liveTimeline;
2021-07-28 22:15:52 +09:00
this.isOngoingPagination = false;
this.ongoingDecryptionCount = 0;
this.initialized = false;
// TODO: remove below line
window.selectedRoom = this;
}
isServingLiveTimeline() {
return getLastLinkedTimeline(this.activeTimeline) === this.liveTimeline;
}
canPaginateBackward() {
if (this.timeline[0]?.getType() === 'm.room.create') return false;
const tm = getFirstLinkedTimeline(this.activeTimeline);
return tm.getPaginationToken('b') !== null;
}
canPaginateForward() {
return !this.isServingLiveTimeline();
}
isEncrypted() {
return this.matrixClient.isRoomEncrypted(this.roomId);
}
clearLocalTimelines() {
this.timeline = [];
}
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.emit(cons.events.roomTimeline.PAGINATED, backwards, 0);
this.isOngoingPagination = false;
return false;
}
const oldSize = this.timeline.length;
try {
await this.matrixClient.paginateEventTimeline(timelineToPaginate, { backwards, limit });
if (this.isEncrypted()) await this.decryptAllEventsOfTimeline(this.activeTimeline);
this._populateTimelines();
const loaded = this.timeline.length - oldSize;
this.emit(cons.events.roomTimeline.PAGINATED, backwards, loaded);
this.isOngoingPagination = false;
return true;
} catch {
this.emit(cons.events.roomTimeline.PAGINATED, backwards, 0);
this.isOngoingPagination = false;
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
return Promise.allSettled(decryptionPromises);
}
markAllAsRead() {
const readEventId = this.getReadUpToEventId();
this.notifications.deleteNoti(this.roomId);
if (this.timeline.length === 0) return;
const latestEvent = this.timeline[this.timeline.length - 1];
if (readEventId === latestEvent.getId()) return;
this.matrixClient.sendReadReceipt(latestEvent);
this.emit(cons.events.roomTimeline.MARKED_AS_READ, latestEvent);
}
hasEventInTimeline(eventId, timeline = this.activeTimeline) {
const timelineSet = this.getUnfilteredTimelineSet();
const eventTimeline = timelineSet.getTimelineForEvent(eventId);
if (!eventTimeline) return false;
return isTimelineLinked(eventTimeline, timeline);
}
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)];
}
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;
}
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)[0];
}
_listenEvents() {
this._listenRoomTimeline = (event, room, toStartOfTimeline, removed, data) => {
2021-07-28 22:15:52 +09:00
if (room.roomId !== this.roomId) return;
if (this.isOngoingPagination) return;
// User is currently viewing the old events probably
// no need to add new event and emit changes.
// only add reactions and edited messages
if (this.isServingLiveTimeline() === false) {
if (!isReaction(event) && !isEdited(event)) return;
}
// We only process live events here
if (!data.liveEvent) return;
2021-07-28 22:15:52 +09:00
if (event.isEncrypted()) {
// We will add this event after it is being decrypted.
2021-07-28 22:15:52 +09:00
this.ongoingDecryptionCount += 1;
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.
2021-07-28 22:15:52 +09:00
this.addToTimeline(event);
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;
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
if (this.ongoingDecryptionCount > 0) {
this.ongoingDecryptionCount -= 1;
}
this.addToTimeline(event);
this.emit(cons.events.roomTimeline.EVENT, event);
};
this._listenRedaction = (mEvent, room) => {
if (room.roomId !== this.roomId) return;
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) => {
// 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();
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-07-28 22:15:52 +09:00
if (typeof lastEventRecipt === 'undefined') return;
if (lastEventRecipt['m.read']) {
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() {
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;