Handle reverse ordering of events

This commit is contained in:
N-Pex 2024-09-17 09:43:53 +02:00
parent 1178d4bb07
commit 14895357a3
2 changed files with 76 additions and 32 deletions

View file

@ -341,8 +341,12 @@ body {
.scroll-to-end { .scroll-to-end {
position: absolute; position: absolute;
top: -64px; bottom: 20px;
right: 16px; right: 16px;
&.reversed {
top: 120px;
transform: rotate(180deg);
}
} }
.op-button { .op-button {

View file

@ -16,8 +16,8 @@
:readMarker="readMarker" :readMarker="readMarker"
:recordingMembers="typingMembers" :recordingMembers="typingMembers"
v-on:start-recording="setShowRecorder()" v-on:start-recording="setShowRecorder()"
v-on:loadnext="handleScrolledToBottom(false)" v-on:loadnext="handleScrolledToLatest(false)"
v-on:loadprevious="handleScrolledToTop()" v-on:loadprevious="handleScrolledToOldest()"
v-on:mark-read="sendRR" v-on:mark-read="sendRR"
v-on:sendclap="sendClapReactionAtTime" v-on:sendclap="sendClapReactionAtTime"
/> />
@ -58,7 +58,7 @@
<div v-for="(event, index) in filteredEvents" :key="event.getId()" :eventId="event.getId()"> <div v-for="(event, index) in filteredEvents" :key="event.getId()" :eventId="event.getId()">
<!-- DAY Marker, shown for every new day in the timeline --> <!-- DAY Marker, shown for every new day in the timeline -->
<div v-if="showDayMarkerBeforeEvent(event) && !!componentForEvent(event, isForExport = false)" class="day-marker"><div class="line"></div><div class="text">{{ dayForEvent(event) }}</div><div class="line"></div></div> <div v-if="!reverseOrder && showDayMarkerBeforeEvent(event) && !!componentForEvent(event, isForExport = false)" class="day-marker"><div class="line"></div><div class="text">{{ dayForEvent(event) }}</div><div class="line"></div></div>
<div v-if="!event.isRelation() && !event.isRedaction()" :ref="event.getId()"> <div v-if="!event.isRelation() && !event.isRedaction()" :ref="event.getId()">
<MessageErrorHandler> <MessageErrorHandler>
@ -67,6 +67,9 @@
touchStart(e, event); touchStart(e, event);
} }
" v-on:touchend="touchEnd" v-on:touchcancel="touchCancel" v-on:touchmove="touchMove"> " v-on:touchend="touchEnd" v-on:touchcancel="touchCancel" v-on:touchmove="touchMove">
<div v-if="reverseOrder && event.getId() == readMarker && index > 0" class="read-marker"><div class="line"></div><div class="text">{{ $t('message.unread_messages') }}</div><div class="line"></div></div>
<!-- Note: For threaded media messages, IF there is only one item we show that media item as a single component. <!-- Note: For threaded media messages, IF there is only one item we show that media item as a single component.
We might therefore get calls to v-on:context-menu that has the event set to that single media item, not the top level thread event We might therefore get calls to v-on:context-menu that has the event set to that single media item, not the top level thread event
that is really displayed in the flow. Therefore, we rewrite these events with "{event: event, anchor: $event.anchor}", that is really displayed in the flow. Therefore, we rewrite these events with "{event: event, anchor: $event.anchor}",
@ -89,24 +92,30 @@
/> />
<!-- <div v-if="debugging" style="user-select:text">EventID: {{ event.getId() }}</div> --> <!-- <div v-if="debugging" style="user-select:text">EventID: {{ event.getId() }}</div> -->
<!-- <div v-if="debugging" style="user-select:text">Event: {{ JSON.stringify(event) }}<br /><br /></div> --> <!-- <div v-if="debugging" style="user-select:text">Event: {{ JSON.stringify(event) }}<br /><br /></div> -->
<div v-if="event.getId() == readMarker && index < filteredEvents.length - 1" class="read-marker"><div class="line"></div><div class="text">{{ $t('message.unread_messages') }}</div><div class="line"></div></div> <div v-if="!reverseOrder && event.getId() == readMarker && index < filteredEvents.length - 1" class="read-marker"><div class="line"></div><div class="text">{{ $t('message.unread_messages') }}</div><div class="line"></div></div>
</div> </div>
</MessageErrorHandler> </MessageErrorHandler>
</div> </div>
<!-- Day marker when reverseOrder is set -->
<div v-if="reverseOrder && showDayMarkerBeforeEvent(event) && !!componentForEvent(event, isForExport = false)" class="day-marker"><div class="line"></div><div class="text">{{ dayForEvent(event) }}</div><div class="line"></div></div>
</div> </div>
<NoHistoryRoomWelcomeHeader v-if="showNoHistoryRoomWelcomeHeader" /> <NoHistoryRoomWelcomeHeader v-if="showNoHistoryRoomWelcomeHeader" />
<!-- "Scroll to end"-button -->
<v-btn v-if="!useVoiceMode" :class="{'scroll-to-end': true, 'reversed': reverseOrder}" v-show="showScrollToEnd" fab x-small elevation="0" color="black"
@click.stop="scrollToEndOfTimeline">
<v-icon color="white">arrow_downward</v-icon>
</v-btn>
</div> </div>
<!-- Input area --> <!-- Input area -->
<v-container v-if="!useVoiceMode && !useFileModeNonAdmin && room" fluid :class="['input-area-outer', replyToEvent ? 'reply-to' : '']"> <v-container v-if="!useVoiceMode && !useFileModeNonAdmin && room" fluid :class="['input-area-outer', replyToEvent ? 'reply-to' : '']">
<div :class="[replyToEvent ? 'iput-area-inner-box' : '']"> <div :class="[replyToEvent ? 'iput-area-inner-box' : '']">
<!-- "Scroll to end"-button -->
<v-btn v-if="!useVoiceMode" class="scroll-to-end" v-show="showScrollToEnd" fab x-small elevation="0" color="black"
@click.stop="scrollToEndOfTimeline">
<v-icon color="white">arrow_downward</v-icon>
</v-btn>
<v-row class="ma-0 pa-0"> <v-row class="ma-0 pa-0">
<div v-if="replyToEvent" class="row"> <div v-if="replyToEvent" class="row">
<div class="col"> <div class="col">
@ -512,7 +521,8 @@ export default {
heartPosition: { heartPosition: {
top: 0, top: 0,
left: 0 left: 0
} },
reverseOrder: false
}; };
}, },
@ -692,12 +702,12 @@ export default {
(!e.getPrevContent() || e.getPrevContent().membership != "join") && (!e.getPrevContent() || e.getPrevContent().membership != "join") &&
e.getStateKey() == this.$matrix.currentUserId) { e.getStateKey() == this.$matrix.currentUserId) {
// Our own join event. // Our own join event.
return this.events.slice(idx + 1); return this.reverseOrder ? this.events.slice(idx + 1).toReversed() : this.events.slice(idx + 1);
} }
} }
} }
} }
return this.events; return this.reverseOrder ? this.events.toReversed() : this.events;
}, },
roomCreatedByUsRecently() { roomCreatedByUsRecently() {
@ -1080,7 +1090,7 @@ export default {
}); });
} else { } else {
// Can't paginate, just scroll to bottom of window! // Can't paginate, just scroll to bottom of window!
this.smoothScrollToEnd(); this.smoothScrollToLatest();
} }
}, },
@ -1150,7 +1160,7 @@ export default {
this.$nextTick(() => { this.$nextTick(() => {
const container = this.chatContainer; const container = this.chatContainer;
if (container && container.scrollHeight <= container.clientHeight) { if (container && container.scrollHeight <= container.clientHeight) {
this.handleScrolledToTop(); this.handleScrolledToOldest();
} }
}); });
}, },
@ -1162,15 +1172,23 @@ export default {
const bufferHeight = container.clientHeight * WINDOW_BUFFER_SIZE; const bufferHeight = container.clientHeight * WINDOW_BUFFER_SIZE;
if (container.scrollTop <= bufferHeight) { if (container.scrollTop <= bufferHeight) {
// Scrolled to top // Scrolled to top
this.handleScrolledToTop(); if (this.reverseOrder) {
this.handleScrolledToLatest(false);
} else {
this.handleScrolledToOldest();
}
} else if (container.scrollHeight - container.scrollTop.toFixed(0) - container.clientHeight <= bufferHeight) { } else if (container.scrollHeight - container.scrollTop.toFixed(0) - container.clientHeight <= bufferHeight) {
this.handleScrolledToBottom(false); if (this.reverseOrder) {
this.handleScrolledToOldest();
} else {
this.handleScrolledToLatest(false);
}
} }
this.showScrollToEnd = this.showScrollToEnd =
container.scrollHeight === container.clientHeight container.scrollHeight === container.clientHeight
? false ? false
: container.scrollHeight - container.scrollTop.toFixed(0) > container.clientHeight || : (this.reverseOrder ? (container.scrollTop.toFixed(0) > 0) : (container.scrollHeight - container.scrollTop.toFixed(0) > container.clientHeight)) ||
(this.timelineWindow && this.timelineWindow.canPaginate(EventTimeline.FORWARDS)); (this.timelineWindow && this.timelineWindow.canPaginate(EventTimeline.FORWARDS));
this.restartRRTimer(); this.restartRRTimer();
@ -1265,11 +1283,14 @@ export default {
var scrollToSeeNew = event.getSender() == this.$matrix.currentUserId; // When we sent, scroll var scrollToSeeNew = event.getSender() == this.$matrix.currentUserId; // When we sent, scroll
const container = this.chatContainer; const container = this.chatContainer;
if (container) { if (container) {
if (container.scrollHeight - container.scrollTop.toFixed(0) == container.clientHeight) { if (this.reverseOrder && container.scrollTop.toFixed(0) == 0) {
scrollToSeeNew = true;
}
else if (!this.reverseOrder && container.scrollHeight - container.scrollTop.toFixed(0) == container.clientHeight) {
scrollToSeeNew = true; scrollToSeeNew = true;
} }
} }
this.handleScrolledToBottom(scrollToSeeNew); this.handleScrolledToLatest(scrollToSeeNew);
// If kick or ban event, redirect to "goodbye"... // If kick or ban event, redirect to "goodbye"...
if (event.getType() === "m.room.member" && if (event.getType() === "m.room.member" &&
@ -1494,7 +1515,7 @@ export default {
}); });
}, },
handleScrolledToTop() { handleScrolledToOldest() {
if ( if (
this.timelineWindow && this.timelineWindow &&
this.timelineWindow.canPaginate(EventTimeline.BACKWARDS) && this.timelineWindow.canPaginate(EventTimeline.BACKWARDS) &&
@ -1505,7 +1526,7 @@ export default {
.paginate(EventTimeline.BACKWARDS, 10, true) .paginate(EventTimeline.BACKWARDS, 10, true)
.then((success) => { .then((success) => {
if (success && this.scrollPosition) { if (success && this.scrollPosition) {
this.scrollPosition.prepareFor("up"); this.scrollPosition.prepareFor(this.reverseOrder ? "down" : "up");
this.setEvents(this.timelineWindow.getEvents()); this.setEvents(this.timelineWindow.getEvents());
this.$nextTick(() => { this.$nextTick(() => {
// restore scroll position! // restore scroll position!
@ -1520,7 +1541,7 @@ export default {
} }
}, },
handleScrolledToBottom(scrollToEnd) { handleScrolledToLatest(smoothScrollToLatest) {
if ( if (
this.timelineWindow && this.timelineWindow &&
this.timelineWindow.canPaginate(EventTimeline.FORWARDS) && this.timelineWindow.canPaginate(EventTimeline.FORWARDS) &&
@ -1533,13 +1554,13 @@ export default {
if (success) { if (success) {
this.setEvents(this.timelineWindow.getEvents()); this.setEvents(this.timelineWindow.getEvents());
if (!this.useVoiceMode && this.scrollPosition) { if (!this.useVoiceMode && this.scrollPosition) {
this.scrollPosition.prepareFor("down"); this.scrollPosition.prepareFor(this.reverseOrder ? "up" : "down");
this.$nextTick(() => { this.$nextTick(() => {
// restore scroll position! // restore scroll position!
console.log("Restore scroll!"); console.log("Restore scroll!");
this.scrollPosition.restore(); this.scrollPosition.restore();
if (scrollToEnd) { if (smoothScrollToLatest) {
this.smoothScrollToEnd(); this.smoothScrollToLatest();
} }
}); });
} }
@ -1555,6 +1576,7 @@ export default {
* Scroll so that the given event is at the middle of the chat view (if more events) or else at the bottom. * Scroll so that the given event is at the middle of the chat view (if more events) or else at the bottom.
*/ */
scrollToEvent(eventId) { scrollToEvent(eventId) {
console.log("Scroll to event", eventId);
const container = this.chatContainer; const container = this.chatContainer;
const ref = this.$refs[eventId]; const ref = this.$refs[eventId];
if (container && ref) { if (container && ref) {
@ -1562,7 +1584,7 @@ export default {
const item = ref[0].getBoundingClientRect(); const item = ref[0].getBoundingClientRect();
let offsetY = (parent.bottom - parent.top) / 2; let offsetY = (parent.bottom - parent.top) / 2;
if (ref[0].clientHeight > offsetY) { if (ref[0].clientHeight > offsetY) {
offsetY = Math.max(0, (parent.bottom - parent.top) - ref[0].clientHeight); offsetY = this.reverseOrder ? 0 : Math.max(0, (parent.bottom - parent.top) - ref[0].clientHeight);
} }
const targetY = parent.top + offsetY; const targetY = parent.top + offsetY;
const currentY = item.top; const currentY = item.top;
@ -1573,10 +1595,21 @@ export default {
} }
}, },
smoothScrollToEnd() { smoothScrollToLatest() {
this.$nextTick(function () { this.$nextTick(function () {
const container = this.chatContainer; const container = this.chatContainer;
if (container && container.children.length > 0) { if (container && container.children.length > 0) {
if (this.reverseOrder) {
const firstChild = container.children[0];
console.log("Scroll into view", firstChild);
window.requestAnimationFrame(() => {
firstChild.scrollIntoView({
behavior: "smooth",
block: "end",
inline: "nearest",
});
});
} else {
const lastChild = container.children[container.children.length - 1]; const lastChild = container.children[container.children.length - 1];
console.log("Scroll into view", lastChild); console.log("Scroll into view", lastChild);
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
@ -1587,6 +1620,7 @@ export default {
}); });
}); });
} }
}
}); });
}, },
@ -1809,10 +1843,16 @@ export default {
const elFirst = util.getFirstVisibleElement(container, (item) => item.hasAttribute("eventId")); const elFirst = util.getFirstVisibleElement(container, (item) => item.hasAttribute("eventId"));
const elLast = util.getLastVisibleElement(container, (item) => item.hasAttribute("eventId")); const elLast = util.getLastVisibleElement(container, (item) => item.hasAttribute("eventId"));
if (elFirst && elLast) { if (elFirst && elLast) {
if (this.reverseOrder) {
// For reverse order, the "first visible" is actually later in time, so swap them
eventIdFirst = elLast.getAttribute("eventId");
eventIdLast = elFirst.getAttribute("eventId");
} else {
eventIdFirst = elFirst.getAttribute("eventId"); eventIdFirst = elFirst.getAttribute("eventId");
eventIdLast = elLast.getAttribute("eventId"); eventIdLast = elLast.getAttribute("eventId");
} }
} }
}
if (eventIdFirst && eventIdLast) { if (eventIdFirst && eventIdLast) {
this.rrTimer = setTimeout(() => { this.rrTimerElapsed(eventIdFirst, eventIdLast) }, READ_RECEIPT_TIMEOUT); this.rrTimer = setTimeout(() => { this.rrTimerElapsed(eventIdFirst, eventIdLast) }, READ_RECEIPT_TIMEOUT);
} }