Read marker, receipts and load messages up to unread

This commit is contained in:
N-Pex 2021-01-14 16:17:05 +01:00
parent 7165b5af8c
commit 0671a9ec40
2 changed files with 148 additions and 22 deletions

View file

@ -28,7 +28,7 @@
@notify="handleChatContainerResize"
/>
<div v-for="(event,index) in events" :key="event.getId()">
<div v-for="(event,index) in events" :key="event.getId()" :eventId="event.getId()">
<div
v-if="
!event.isRelation() && !event.isRedacted() && !event.isRedaction()
@ -62,6 +62,8 @@
v-on:send-quick-reaction="sendQuickReaction"
v-on:context-menu="showContextMenuForEvent($event)"
/>
<div>EventID: {{ event.getId() }}</div>
<div v-if="event.getId() == readMarker">------- READ MARKER -------</div>
</div>
</div>
</div>
@ -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();
});
}
}
}
}
},
};

View file

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