import EventEmitter from 'events'; import initMatrix from '../initMatrix'; import cons from './cons'; class RoomTimeline extends EventEmitter { constructor(roomId) { super(); this.matrixClient = initMatrix.matrixClient; this.roomId = roomId; this.room = this.matrixClient.getRoom(roomId); this.timeline = this.room.timeline; this.editedTimeline = this.getEditedTimeline(); this.reactionTimeline = this.getReactionTimeline(); this.isOngoingPagination = false; this.ongoingDecryptionCount = 0; this.typingMembers = new Set(); this._listenRoomTimeline = (event, room) => { if (room.roomId !== this.roomId) return; if (event.isEncrypted()) { this.ongoingDecryptionCount += 1; return; } this.timeline = this.room.timeline; if (this.isEdited(event)) { this.addToMap(this.editedTimeline, event); } if (this.isReaction(event)) { this.addToMap(this.reactionTimeline, event); } if (this.ongoingDecryptionCount !== 0) return; this.emit(cons.events.roomTimeline.EVENT); }; this._listenDecryptEvent = (event) => { if (event.getRoomId() !== this.roomId) return; if (this.ongoingDecryptionCount > 0) this.ongoingDecryptionCount -= 1; this.timeline = this.room.timeline; if (this.ongoingDecryptionCount !== 0) return; this.emit(cons.events.roomTimeline.EVENT); }; 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) => { if (room.roomId !== this.roomId) return; const receiptContent = event.getContent(); if (this.timeline.length === 0) return; const tmlLastEvent = this.timeline[this.timeline.length - 1]; const lastEventId = tmlLastEvent.getId(); const lastEventRecipt = receiptContent[lastEventId]; if (typeof lastEventRecipt === 'undefined') return; if (lastEventRecipt['m.read']) { this.emit(cons.events.roomTimeline.READ_RECEIPT); } }; this.matrixClient.on('Room.timeline', this._listenRoomTimeline); 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(); } isEncryptedRoom() { return this.matrixClient.isRoomEncrypted(this.roomId); } // eslint-disable-next-line class-methods-use-this isEdited(mEvent) { return mEvent.getRelation()?.rel_type === 'm.replace'; } // eslint-disable-next-line class-methods-use-this getRelateToId(mEvent) { const relation = mEvent.getRelation(); return relation && relation.event_id; } addToMap(myMap, mEvent) { const relateToId = this.getRelateToId(mEvent); if (relateToId === null) return null; if (typeof myMap.get(relateToId) === 'undefined') myMap.set(relateToId, []); myMap.get(relateToId).push(mEvent); return mEvent; } getEditedTimeline() { const mReplace = new Map(); this.timeline.forEach((mEvent) => { if (this.isEdited(mEvent)) { this.addToMap(mReplace, mEvent); } }); return mReplace; } // eslint-disable-next-line class-methods-use-this isReaction(mEvent) { return mEvent.getType() === 'm.reaction'; } getReactionTimeline() { const mReaction = new Map(); this.timeline.forEach((mEvent) => { if (this.isReaction(mEvent)) { this.addToMap(mReaction, mEvent); } }); return mReaction; } paginateBack() { if (this.isOngoingPagination) return; this.isOngoingPagination = true; 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); return; } this.editedTimeline = this.getEditedTimeline(); this.reactionTimeline = this.getReactionTimeline(); this.isOngoingPagination = false; if (this.isEncryptedRoom()) await this.room.decryptAllEvents(); this.emit(cons.events.roomTimeline.PAGINATED, true); }); } removeInternalListeners() { this.matrixClient.removeListener('Room.timeline', this._listenRoomTimeline); this.matrixClient.removeListener('Event.decrypted', this._listenDecryptEvent); this.matrixClient.removeListener('RoomMember.typing', this._listenTypingEvent); this.matrixClient.removeListener('Room.receipt', this._listenReciptEvent); } } export default RoomTimeline;