diff --git a/src/components/Chat.vue b/src/components/Chat.vue index fbfad7d..9c3a650 100644 --- a/src/components/Chat.vue +++ b/src/components/Chat.vue @@ -28,7 +28,7 @@ @notify="handleChatContainerResize" /> -
+
+
EventID: {{ event.getId() }}
+
------- READ MARKER -------
@@ -197,6 +199,8 @@ import util from "../plugins/utils"; import MessageOperations from "./messages/MessageOperations.vue"; import ChatHeader from "./ChatHeader"; +const READ_RECEIPT_TIMEOUT = 5000; /* How long a message must have been visible before the read marker is updated */ + // from https://kirbysayshi.com/2013/08/19/maintaining-scroll-position-knockoutjs-list.html function ScrollPosition(node) { this.node = node; @@ -263,6 +267,7 @@ export default { replyToEvent: null, showContextMenu: false, showContextMenuAnchor: null, + initialLoadDone: false, /** * Current chat container size. We need to keep track of this so that if and when @@ -273,6 +278,12 @@ export default { /** Shows a dialog with info about an operation being disallowed for guests */ showNotAllowedForGuests: false, + + /** A timer for read receipts. */ + rrTimer: null, + + /** Timestamp of last send Read Receipt */ + lastRRTimestamp: null, }; }, @@ -299,6 +310,16 @@ export default { roomId() { return this.$matrix.currentRoomId; }, + readMarker() { + return this.fullyReadMarker || this.room.getEventReadUpTo(this.$matrix.currentUserId, false); + }, + fullyReadMarker() { + const readEvent = this.room.getAccountData('m.fully_read'); + if (readEvent) { + return readEvent.getContent().event_id; + } + return null; + }, attachButtonDisabled() { return this.editedEvent != null || this.replyToEvent != null || this.currentInput.length > 0; }, @@ -347,7 +368,8 @@ export default { this.events = []; this.timelineWindow = null; this.typingMembers = []; - + this.initialLoadDone = false; + if (!room) { // Public room? if (this.roomId && this.roomId.startsWith('#')) { @@ -363,35 +385,53 @@ export default { } else { this.onRoomNotJoined(); } - - // this.$matrix - // .getPublicRoomInfo(this.roomId) - // .then((room) => { - // console.log("Found room:", room); - // this.roomName = room.name; - // this.roomAvatar = room.avatar; - // this.waitingForInfo = false; - // }) - // .catch((err) => { - // console.log("Could not find room info", err); - // this.waitingForInfo = false; - // }); - // }, } }, }, methods: { onRoomJoined() { + + var initialEventId = this.readMarker; + console.log("Read up to " + initialEventId); + this.timelineWindow = new TimelineWindow( this.$matrix.matrixClient, this.room.getUnfilteredTimelineSet(), {} ); - this.timelineWindow.load(null, 20).then(() => { + this.timelineWindow.load(initialEventId, 20).then(() => { this.events = this.timelineWindow.getEvents(); - this.$nextTick(() => { - this.paginateBackIfNeeded(); + + const getMoreIfNeeded = function _getMoreIfNeeded() { + const container = this.$refs.chatContainer; + if (container.scrollHeight <= container.clientHeight && + this.timelineWindow && + this.timelineWindow.canPaginate(EventTimeline.BACKWARDS)) { + return this.timelineWindow.paginate(EventTimeline.BACKWARDS, 10, true) + .then(success => { + if (success) { + this.events = this.timelineWindow.getEvents(); + return _getMoreIfNeeded.call(this); + } else { + return Promise.reject("Failed to paginate"); + } + }); + } else { + return Promise.resolve("Done"); + } + }.bind(this); + + getMoreIfNeeded() + .catch(err => { + console.log("ERROR " + err); + }) + .finally(() => { + this.initialLoadDone = true; + if (initialEventId) { + this.scrollToEvent(initialEventId); + } + this.restartRRTimer(); }); }); }, @@ -521,13 +561,17 @@ export default { ) { this.handleScrolledToBottom(false); } + this.restartRRTimer(); }, onEvent(event) { console.log("OnEvent", JSON.stringify(event)); if (event.getRoomId() !== this.roomId) { return; // Not for this room } - this.paginateBackIfNeeded(); + + if (this.initialLoadDone) { + this.paginateBackIfNeeded(); + } // If we are at bottom, scroll to see new events... const container = this.$refs.chatContainer; @@ -538,7 +582,7 @@ export default { ) { scrollToSeeNew = true; } - if (event.forwardLooking && !event.isRelation()) { + if (this.initialLoadDone && event.forwardLooking && !event.isRelation()) { this.handleScrolledToBottom(scrollToSeeNew); } }, @@ -698,6 +742,19 @@ export default { } }, + /** + * Scroll so that the given event is at the middle of the chat view (if more events) or else at the bottom. + */ + scrollToEvent(eventId) { + const container = this.$refs.chatContainer; + const ref = this.$refs[eventId]; + if (container && ref) { + const targetY = container.clientHeight / 2; + const sourceY = ref[0].offsetTop; + container.scrollTo(0, sourceY - targetY); + } + }, + smoothScrollToEnd() { this.$nextTick(function () { const container = this.$refs.chatContainer; @@ -796,6 +853,50 @@ export default { this.showContextMenu = false; e.preventDefault(); } + }, + + /** + * Start/restart the timer to Read Receipts. + */ + restartRRTimer() { + if (this.rrTimer) { + clearInterval(this.rrTimer); + this.rrTimer = null; + } + this.rrTimer = setInterval(this.rrTimerElapsed, READ_RECEIPT_TIMEOUT); + }, + + rrTimerElapsed() { + const container = this.$refs.chatContainer; + const el = util.getLastVisibleElement(container); + if (el) { + const eventId = el.getAttribute('eventId'); + if (eventId && this.room) { + const event = this.room.findEventById(eventId); + if (event && event.getTs() > this.lastRRTimestamp) { + + // Disable timer while we are sending + clearInterval(this.rrTimer); + this.rrTimer = null; + + // Send read receipt + this.$matrix.matrixClient.sendReadReceipt(event) + .then(() => { + this.$matrix.matrixClient.setRoomReadMarkers(this.room.roomId, eventId) + }) + .then(() => { + console.log("RR sent for event: " + eventId); + this.lastRRTimestamp = event.getTs(); + }) + .catch(err => { + console.log("Failed to update read marker: ", err); + }) + .finally(() => { + this.restartRRTimer(); + }); + } + } + } } }, }; diff --git a/src/plugins/utils.js b/src/plugins/utils.js index c4946be..d27d98f 100644 --- a/src/plugins/utils.js +++ b/src/plugins/utils.js @@ -132,7 +132,7 @@ class Util { } // Prefix the content with reply info (seems to be a legacy thing) - const prefix = replyToEvent.getContent().body.split('\n').map((item,index) => { + const prefix = replyToEvent.getContent().body.split('\n').map((item, index) => { return "> " + (index == 0 ? ("<" + replyToEvent.getSender() + "> ") : "") + item; }).join('\n'); content.body = prefix + "\n\n" + content.body; @@ -340,6 +340,31 @@ class Util { } return null; } + + getLastVisibleElement(parentNode) { + const y = parentNode.scrollTop + parentNode.clientHeight; + + let start = 0; + let end = parentNode.children.length - 1; + + while (start <= end) { + let middle = Math.floor((start + end) / 2); + const yMiddleTop = parentNode.children[middle].offsetTop; + const yMiddleBottom = yMiddleTop + parentNode.children[middle].clientHeight; + if (yMiddleTop <= y && yMiddleBottom >= y) { + // found the key + return parentNode.children[middle]; + } else if (yMiddleBottom < y) { + // continue searching to the right + start = middle + 1; + } else { + // search searching to the left + end = middle - 1; + } + } + // key wasn't found + return null; + } } export default new Util();