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" @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 <div
v-if=" v-if="
!event.isRelation() && !event.isRedacted() && !event.isRedaction() !event.isRelation() && !event.isRedacted() && !event.isRedaction()
@ -62,6 +62,8 @@
v-on:send-quick-reaction="sendQuickReaction" v-on:send-quick-reaction="sendQuickReaction"
v-on:context-menu="showContextMenuForEvent($event)" v-on:context-menu="showContextMenuForEvent($event)"
/> />
<div>EventID: {{ event.getId() }}</div>
<div v-if="event.getId() == readMarker">------- READ MARKER -------</div>
</div> </div>
</div> </div>
</div> </div>
@ -197,6 +199,8 @@ import util from "../plugins/utils";
import MessageOperations from "./messages/MessageOperations.vue"; import MessageOperations from "./messages/MessageOperations.vue";
import ChatHeader from "./ChatHeader"; 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 // from https://kirbysayshi.com/2013/08/19/maintaining-scroll-position-knockoutjs-list.html
function ScrollPosition(node) { function ScrollPosition(node) {
this.node = node; this.node = node;
@ -263,6 +267,7 @@ export default {
replyToEvent: null, replyToEvent: null,
showContextMenu: false, showContextMenu: false,
showContextMenuAnchor: null, showContextMenuAnchor: null,
initialLoadDone: false,
/** /**
* Current chat container size. We need to keep track of this so that if and when * 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 */ /** Shows a dialog with info about an operation being disallowed for guests */
showNotAllowedForGuests: false, showNotAllowedForGuests: false,
/** A timer for read receipts. */
rrTimer: null,
/** Timestamp of last send Read Receipt */
lastRRTimestamp: null,
}; };
}, },
@ -299,6 +310,16 @@ export default {
roomId() { roomId() {
return this.$matrix.currentRoomId; 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() { attachButtonDisabled() {
return this.editedEvent != null || this.replyToEvent != null || this.currentInput.length > 0; return this.editedEvent != null || this.replyToEvent != null || this.currentInput.length > 0;
}, },
@ -347,6 +368,7 @@ export default {
this.events = []; this.events = [];
this.timelineWindow = null; this.timelineWindow = null;
this.typingMembers = []; this.typingMembers = [];
this.initialLoadDone = false;
if (!room) { if (!room) {
// Public room? // Public room?
@ -363,35 +385,53 @@ export default {
} else { } else {
this.onRoomNotJoined(); 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: { methods: {
onRoomJoined() { onRoomJoined() {
var initialEventId = this.readMarker;
console.log("Read up to " + initialEventId);
this.timelineWindow = new TimelineWindow( this.timelineWindow = new TimelineWindow(
this.$matrix.matrixClient, this.$matrix.matrixClient,
this.room.getUnfilteredTimelineSet(), this.room.getUnfilteredTimelineSet(),
{} {}
); );
this.timelineWindow.load(null, 20).then(() => { this.timelineWindow.load(initialEventId, 20).then(() => {
this.events = this.timelineWindow.getEvents(); 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.handleScrolledToBottom(false);
} }
this.restartRRTimer();
}, },
onEvent(event) { onEvent(event) {
console.log("OnEvent", JSON.stringify(event)); console.log("OnEvent", JSON.stringify(event));
if (event.getRoomId() !== this.roomId) { if (event.getRoomId() !== this.roomId) {
return; // Not for this room return; // Not for this room
} }
this.paginateBackIfNeeded();
if (this.initialLoadDone) {
this.paginateBackIfNeeded();
}
// If we are at bottom, scroll to see new events... // If we are at bottom, scroll to see new events...
const container = this.$refs.chatContainer; const container = this.$refs.chatContainer;
@ -538,7 +582,7 @@ export default {
) { ) {
scrollToSeeNew = true; scrollToSeeNew = true;
} }
if (event.forwardLooking && !event.isRelation()) { if (this.initialLoadDone && event.forwardLooking && !event.isRelation()) {
this.handleScrolledToBottom(scrollToSeeNew); 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() { smoothScrollToEnd() {
this.$nextTick(function () { this.$nextTick(function () {
const container = this.$refs.chatContainer; const container = this.$refs.chatContainer;
@ -796,6 +853,50 @@ export default {
this.showContextMenu = false; this.showContextMenu = false;
e.preventDefault(); 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) // 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; return "> " + (index == 0 ? ("<" + replyToEvent.getSender() + "> ") : "") + item;
}).join('\n'); }).join('\n');
content.body = prefix + "\n\n" + content.body; content.body = prefix + "\n\n" + content.body;
@ -340,6 +340,31 @@ class Util {
} }
return null; 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(); export default new Util();