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();