Lots of fixes to "media threads"

This commit is contained in:
N Pex 2023-11-06 15:28:26 +00:00
parent fe081edc62
commit 8bcceafcff
23 changed files with 867 additions and 333 deletions

View file

@ -32,7 +32,7 @@
:attachments="currentFileInputs"
/>
<div v-if="!useVoiceMode && !useFileModeNonAdmin" class="chat-content flex-grow-1 flex-shrink-1" ref="chatContainer"
<div v-if="!useVoiceMode && !useFileModeNonAdmin" :class="{'chat-content': true, 'flex-grow-1': true, 'flex-shrink-1': true, 'invisible': !initialLoadDone}" ref="chatContainer"
v-on:scroll="onScroll" @click="closeContextMenusIfOpen">
<div ref="messageOperationsStrut" class="message-operations-strut">
<message-operations ref="messageOperations" :style="opStyle" :emojis="recentEmojis" v-on:close="
@ -42,8 +42,8 @@
v-on:addreply="addReply(selectedEvent)" v-on:edit="edit(selectedEvent)" v-on:redact="redact(selectedEvent)"
v-on:download="download(selectedEvent)" v-on:more="
isEmojiQuickReaction= true
showMoreMessageOperations($event)
" :originalEvent="selectedEvent" />
showMoreMessageOperations({event: selectedEvent, anchor: $event.anchor})
" :originalEvent="selectedEvent" :timelineSet="timelineSet" />
</div>
<div ref="avatarOperationsStrut" class="avatar-operations-strut">
@ -87,6 +87,7 @@
isEmojiQuickReaction = true
showMoreMessageOperations({event: event, anchor: $event.anchor})
"
v-on:layout-change="onLayoutChange"
/>
<!-- <div v-if="debugging" style="user-select:text">EventID: {{ event.getId() }}</div> -->
<!-- <div v-if="debugging" style="user-select:text">Event: {{ JSON.stringify(event) }}</div> -->
@ -114,6 +115,7 @@
<div v-if="replyToContentType === 'm.text'" class="reply-text" :title="replyToEvent.getContent().body">
{{ replyToEvent.getContent().body | latestReply }}
</div>
<div v-if="replyToContentType === 'm.thread'">{{ replyToThreadMessage }}</div>
<div v-if="replyToContentType === 'm.image'">{{ $t("message.reply_image") }}</div>
<div v-if="replyToContentType === 'm.audio'">{{ $t("message.reply_audio_message") }}</div>
<div v-if="replyToContentType === 'm.video'">{{ $t("message.reply_video") }}</div>
@ -533,7 +535,7 @@ export default {
if (contentArr[0] === "") {
contentArr.shift();
}
return contentArr[0].replace(/^> (<.*> )?/g, "");
return (contentArr && contentArr.length > 0) ? contentArr[0].replace(/^> (<.*> )?/g, "") : "";
},
},
@ -792,6 +794,18 @@ export default {
}
}
return "";
},
/**
* If we are replying to a (media) thread, this is the hint we show when replying.
*/
replyToThreadMessage() {
if (this.replyToEvent && this.timelineSet) {
return this.$t("message.sent_media", {count: this.timelineSet.relations
.getAllChildEventsForEvent(this.replyToEvent.getId())
.filter((e) => util.downloadableTypes().includes(e.getContent().msgtype)).length});
}
return "";
}
},
@ -800,6 +814,8 @@ export default {
immediate: true,
handler(value, oldValue) {
if (value && !oldValue) {
this.events.filter(event => (event.threadRootId && !event.parentThread)).forEach(event => this.setParentThread(event));
this.events.filter(event => (event.replyEventId && !event.replyEvent)).forEach(event => this.setReplyToEvent(event));
console.log("Loading finished!");
}
}
@ -842,7 +858,7 @@ export default {
}
});
} else {
this.initialLoadDone = true;
this.setInitialLoadDone();
return; // no room
}
},
@ -889,6 +905,15 @@ export default {
},
methods: {
/**
* Set initialLoadDone to 'true'. First process all events, setting threadParent and replyEvent if needed.
*/
setInitialLoadDone() {
this.events.filter(event => (event.threadRootId && !event.parentThread)).forEach(event => this.setParentThread(event));
this.events.filter(event => (event.replyEventId && !event.replyEvent)).forEach(event => this.setReplyToEvent(event));
this.initialLoadDone = true;
console.log("Loading finished!");
},
windowNotificationPermission,
onNotificationDialog() {
if(this.windowNotificationPermission() === 'denied') {
@ -943,9 +968,23 @@ export default {
console.log("ERROR " + err);
})
.finally(() => {
self.initialLoadDone = true;
if (initialEventId && !this.showCreatedRoomWelcomeHeader) {
self.scrollToEvent(initialEventId);
// const [timelineEvents, threadedEvents, unknownRelations] =
// this.room.partitionThreadedEvents(self.events);
// this.$matrix.matrixClient.processAggregatedTimelineEvents(this.room, timelineEvents);
// //room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline());
// this.$matrix.matrixClient.processThreadEvents(this.room, threadedEvents, true);
// unknownRelations.forEach((event) => this.room.relations.aggregateChildEvent(event));
this.setInitialLoadDone();
if (initialEventId && !this.showCreatedRoomWelcomeHeader) {
const event = this.room.findEventById(initialEventId);
this.$nextTick(() => {
if (event && event.parentThread) {
self.scrollToEvent(event.parentThread.getId());
} else {
self.scrollToEvent(initialEventId);
}
});
} else if (this.showCreatedRoomWelcomeHeader || this.showDirectChatWelcomeHeader) {
self.onScroll();
}
@ -960,7 +999,7 @@ export default {
} else {
// Error. Done loading.
this.events = this.timelineWindow.getEvents();
this.initialLoadDone = true;
this.setInitialLoadDone();
}
})
.finally(() => {
@ -1094,12 +1133,83 @@ export default {
this.restartRRTimer();
},
setParentThread(event) {
const parentEvent = this.timelineSet.findEventById(event.threadRootId) || this.room.findEventById(event.threadRootId);
if (parentEvent) {
Vue.set(parentEvent, "isMxThread", true);
Vue.set(event, "parentThread", parentEvent);
} else {
// Try to load from server.
this.$matrix.matrixClient.getEventTimeline(this.timelineSet, event.threadRootId).then((tl) => {
if (tl) {
const parentEvent = tl.getEvents().find((e) => e.getId() === event.threadRootId);
if (parentEvent) {
this.events = this.timelineWindow.getEvents();
const fn = () => {
Vue.set(parentEvent, "isMxThread", true);
Vue.set(event, "parentThread", parentEvent);
};
if (this.initialLoadDone) {
const sel = "[eventId=\"" + parentEvent.getId() + "\"]";
const element = document.querySelector(sel);
if (element) {
this.onLayoutChange(fn, element);
} else {
fn();
}
} else {
fn();
}
}
}
});
}
},
setReplyToEvent(event) {
const parentEvent = this.timelineSet.findEventById(event.replyEventId) || this.room.findEventById(event.replyEventId);
if (parentEvent) {
Vue.set(event, "replyEvent", parentEvent);
} else {
// Try to load from server.
this.$matrix.matrixClient.getEventTimeline(this.timelineSet, event.replyEventId)
.then((tl) => {
if (tl) {
const parentEvent = tl.getEvents().find((e) => e.getId() === event.replyEventId);
if (parentEvent) {
this.events = this.timelineWindow.getEvents();
const fn = () => {Vue.set(event, "replyEvent", parentEvent);};
if (this.initialLoadDone) {
const sel = "[eventId=\"" + parentEvent.getId() + "\"]";
const element = document.querySelector(sel);
if (element) {
this.onLayoutChange(fn, element);
} else {
fn();
}
} else {
fn();
}
}
}
}).catch(e => console.error(e));
}
},
onEvent(event) {
//console.log("OnEvent", JSON.stringify(event));
if (event.getRoomId() !== this.roomId) {
return; // Not for this room
}
if (this.initialLoadDone && event.threadRootId && !event.parentThread) {
this.setParentThread(event);
}
if (this.initialLoadDone && event.replyEventId && !event.replyEvent) {
this.setReplyToEvent(event);
}
const loadingDone = this.initialLoadDone;
this.$matrix.matrixClient.decryptEventIfNeeded(event, {});
@ -1107,7 +1217,7 @@ export default {
this.paginateBackIfNeeded();
}
if (loadingDone && event.forwardLooking && !event.isRelation()) {
if (loadingDone && event.forwardLooking && (!event.isRelation() || event.isMxThread || event.threadRootId || event.parentThread )) {
// If we are at bottom, scroll to see new events...
var scrollToSeeNew = event.getSender() == this.$matrix.currentUserId; // When we sent, scroll
const container = this.chatContainer;
@ -1315,6 +1425,28 @@ export default {
this.cancelSendAttachment();
},
/**
* Called by message components that need to change their layout. This will avoid "jumping" in the UI, because
* we remember scroll position, apply the layout change, then restore the scroll.
* NOTE: we use "parentElement" below, because it is expected to be called with "element" set to the message component
* and the message component in turn being wrapped by a "message-wrapper" element (see html above).
* @param {} action A function that performs desired layout changes.
* @param {*} element Root element for the chat message.
*/
onLayoutChange(action, element) {
if (!element || !element.parentElemen || this.useVoiceMode || this.useFileModeNonAdmin) {
action();
return
}
const container = this.chatContainer;
this.scrollPosition.prepareFor(element.parentElement.offsetTop >= container.scrollTop ? "down" : "up");
action();
this.$nextTick(() => {
// restore scroll position!
this.scrollPosition.restore();
});
},
handleScrolledToTop() {
if (
this.timelineWindow &&
@ -1379,9 +1511,18 @@ export default {
const container = this.chatContainer;
const ref = this.$refs[eventId];
if (container && ref) {
const targetY = container.clientHeight / 2;
const sourceY = ref[0].offsetTop;
container.scrollTo(0, sourceY - targetY);
const parent = container.getBoundingClientRect();
const item = ref[0].getBoundingClientRect();
let offsetY = (parent.bottom - parent.top) / 2;
if (ref[0].clientHeight > offsetY) {
offsetY = Math.max(0, (parent.bottom - parent.top) - ref[0].clientHeight);
}
const targetY = parent.top + offsetY;
const currentY = item.top;
const y = container.scrollTop + (currentY - targetY);
this.$nextTick(() => {
container.scrollTo(0, y);
});
}
},
@ -1433,7 +1574,11 @@ export default {
addReply(event) {
this.replyToEvent = event;
this.$refs.messageInput.focus();
this.replyToContentType = event.getContent().msgtype || 'm.poll';
if (event.parentThread || event.isThreadRoot || event.isMxThread) {
this.replyToContentType = 'm.thread';
} else {
this.replyToContentType = event.getContent().msgtype || 'm.poll';
}
this.setReplyToImage(event);
},
@ -1455,7 +1600,12 @@ export default {
},
download(event) {
util.download(this.$matrix.matrixClient, event);
if ((event.isThreadRoot || event.isMxThread) && this.timelineSet) {
const children = this.timelineSet.relations.getAllChildEventsForEvent(event.getId()).filter(e => util.downloadableTypes().includes(e.getContent().msgtype));
children.forEach(child => util.download(this.$matrix.matrixClient, child));
} else {
util.download(this.$matrix.matrixClient, event);
}
},
cancelEditReply() {