2018-07-08 17:22:01 +00:00
|
|
|
<template>
|
2019-07-17 09:33:59 +00:00
|
|
|
<div ref="chat" class="chat" tabindex="-1">
|
|
|
|
<div :class="['show-more', {show: channel.moreHistoryAvailable}]">
|
2018-07-13 10:43:11 +00:00
|
|
|
<button
|
|
|
|
ref="loadMoreButton"
|
2018-09-09 13:09:19 +00:00
|
|
|
:disabled="channel.historyLoading || !$root.isConnected"
|
2018-07-13 10:43:11 +00:00
|
|
|
class="btn"
|
2019-02-25 05:38:13 +00:00
|
|
|
@click="onShowMoreClick"
|
|
|
|
>
|
2018-07-13 10:43:11 +00:00
|
|
|
<span v-if="channel.historyLoading">Loading…</span>
|
|
|
|
<span v-else>Show older messages</span>
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
<div
|
|
|
|
class="messages"
|
|
|
|
role="log"
|
|
|
|
aria-live="polite"
|
|
|
|
aria-relevant="additions"
|
2019-02-25 05:38:13 +00:00
|
|
|
@copy="onCopy"
|
|
|
|
>
|
2018-07-13 10:43:11 +00:00
|
|
|
<template v-for="(message, id) in condensedMessages">
|
2018-09-12 17:14:45 +00:00
|
|
|
<DateMarker
|
2018-07-13 10:43:11 +00:00
|
|
|
v-if="shouldDisplayDateMarker(message, id)"
|
|
|
|
:key="message.id + '-date'"
|
2019-02-25 05:38:13 +00:00
|
|
|
:message="message"
|
|
|
|
/>
|
2018-07-13 10:43:11 +00:00
|
|
|
<div
|
2018-09-14 13:27:41 +00:00
|
|
|
v-if="shouldDisplayUnreadMarker(message.id)"
|
2018-07-13 10:43:11 +00:00
|
|
|
:key="message.id + '-unread'"
|
2019-02-25 05:38:13 +00:00
|
|
|
class="unread-marker"
|
|
|
|
>
|
2018-07-29 17:57:14 +00:00
|
|
|
<span class="unread-marker-text" />
|
2018-07-13 10:43:11 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
<MessageCondensed
|
|
|
|
v-if="message.type === 'condensed'"
|
|
|
|
:key="message.id"
|
2018-07-19 17:44:24 +00:00
|
|
|
:network="network"
|
2018-07-29 17:57:14 +00:00
|
|
|
:keep-scroll-position="keepScrollPosition"
|
2019-02-25 05:38:13 +00:00
|
|
|
:messages="message.messages"
|
|
|
|
/>
|
2018-07-13 10:43:11 +00:00
|
|
|
<Message
|
|
|
|
v-else
|
2018-07-29 17:57:14 +00:00
|
|
|
:key="message.id"
|
2018-07-19 17:44:24 +00:00
|
|
|
:network="network"
|
2018-07-13 10:43:11 +00:00
|
|
|
:message="message"
|
|
|
|
:keep-scroll-position="keepScrollPosition"
|
2019-02-25 05:38:13 +00:00
|
|
|
@linkPreviewToggle="onLinkPreviewToggle"
|
|
|
|
/>
|
2018-07-13 10:43:11 +00:00
|
|
|
</template>
|
|
|
|
</div>
|
2018-07-08 17:22:01 +00:00
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
2018-07-13 10:43:11 +00:00
|
|
|
require("intersection-observer");
|
2018-07-14 07:27:45 +00:00
|
|
|
|
2018-07-09 12:14:44 +00:00
|
|
|
const constants = require("../js/constants");
|
2018-07-09 18:37:52 +00:00
|
|
|
const clipboard = require("../js/clipboard");
|
2018-07-11 20:45:01 +00:00
|
|
|
import socket from "../js/socket";
|
2018-07-08 17:22:01 +00:00
|
|
|
import Message from "./Message.vue";
|
2018-07-09 12:14:44 +00:00
|
|
|
import MessageCondensed from "./MessageCondensed.vue";
|
2018-09-12 17:14:45 +00:00
|
|
|
import DateMarker from "./DateMarker.vue";
|
2018-07-08 17:22:01 +00:00
|
|
|
|
|
|
|
export default {
|
|
|
|
name: "MessageList",
|
|
|
|
components: {
|
|
|
|
Message,
|
2018-07-09 12:14:44 +00:00
|
|
|
MessageCondensed,
|
2018-09-12 17:14:45 +00:00
|
|
|
DateMarker,
|
2018-07-08 17:22:01 +00:00
|
|
|
},
|
|
|
|
props: {
|
2018-07-19 17:44:24 +00:00
|
|
|
network: Object,
|
2018-07-08 17:22:01 +00:00
|
|
|
channel: Object,
|
|
|
|
},
|
2018-07-09 12:14:44 +00:00
|
|
|
computed: {
|
2018-07-11 09:27:44 +00:00
|
|
|
condensedMessages() {
|
2018-07-09 12:14:44 +00:00
|
|
|
if (this.channel.type !== "channel") {
|
|
|
|
return this.channel.messages;
|
|
|
|
}
|
|
|
|
|
2018-07-18 11:16:21 +00:00
|
|
|
// If actions are hidden, just return a message list with them excluded
|
2018-09-13 09:16:27 +00:00
|
|
|
if (this.$root.settings.statusMessages === "hidden") {
|
2019-07-17 09:33:59 +00:00
|
|
|
return this.channel.messages.filter(
|
|
|
|
(message) => !constants.condensedTypes.includes(message.type)
|
|
|
|
);
|
2018-07-18 11:16:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// If actions are not condensed, just return raw message list
|
2018-09-13 09:16:27 +00:00
|
|
|
if (this.$root.settings.statusMessages !== "condensed") {
|
2018-07-18 11:16:21 +00:00
|
|
|
return this.channel.messages;
|
|
|
|
}
|
|
|
|
|
2018-07-09 12:14:44 +00:00
|
|
|
const condensed = [];
|
|
|
|
let lastCondensedContainer = null;
|
|
|
|
|
|
|
|
for (const message of this.channel.messages) {
|
|
|
|
// If this message is not condensable, or its an action affecting our user,
|
|
|
|
// then just append the message to container and be done with it
|
2019-07-17 09:33:59 +00:00
|
|
|
if (
|
|
|
|
message.self ||
|
|
|
|
message.highlight ||
|
|
|
|
!constants.condensedTypes.includes(message.type)
|
|
|
|
) {
|
2018-07-09 12:14:44 +00:00
|
|
|
lastCondensedContainer = null;
|
|
|
|
|
|
|
|
condensed.push(message);
|
|
|
|
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (lastCondensedContainer === null) {
|
|
|
|
lastCondensedContainer = {
|
2018-07-11 09:27:44 +00:00
|
|
|
time: message.time,
|
2018-07-09 12:14:44 +00:00
|
|
|
type: "condensed",
|
|
|
|
messages: [],
|
|
|
|
};
|
|
|
|
|
|
|
|
condensed.push(lastCondensedContainer);
|
|
|
|
}
|
|
|
|
|
|
|
|
lastCondensedContainer.messages.push(message);
|
2018-07-23 06:58:35 +00:00
|
|
|
|
|
|
|
// Set id of the condensed container to last message id,
|
|
|
|
// which is required for the unread marker to work correctly
|
|
|
|
lastCondensedContainer.id = message.id;
|
2018-07-23 09:27:52 +00:00
|
|
|
|
|
|
|
// If this message is the unread boundary, create a split condensed container
|
|
|
|
if (message.id === this.channel.firstUnread) {
|
|
|
|
lastCondensedContainer = null;
|
|
|
|
}
|
2018-07-09 12:14:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return condensed;
|
|
|
|
},
|
|
|
|
},
|
2018-07-13 10:43:11 +00:00
|
|
|
watch: {
|
2018-07-13 20:07:16 +00:00
|
|
|
"channel.id"() {
|
2018-07-18 18:31:51 +00:00
|
|
|
this.channel.scrolledToBottom = true;
|
2018-07-25 11:12:28 +00:00
|
|
|
|
|
|
|
// Re-add the intersection observer to trigger the check again on channel switch
|
|
|
|
// Otherwise if last channel had the button visible, switching to a new channel won't trigger the history
|
|
|
|
if (this.historyObserver) {
|
|
|
|
this.historyObserver.unobserve(this.$refs.loadMoreButton);
|
|
|
|
this.historyObserver.observe(this.$refs.loadMoreButton);
|
|
|
|
}
|
2018-07-13 20:07:16 +00:00
|
|
|
},
|
|
|
|
"channel.messages"() {
|
2018-07-13 10:43:11 +00:00
|
|
|
this.keepScrollPosition();
|
|
|
|
},
|
2018-07-16 16:43:25 +00:00
|
|
|
"channel.pendingMessage"() {
|
|
|
|
this.$nextTick(() => {
|
|
|
|
// Keep the scroll stuck when input gets resized while typing
|
|
|
|
this.keepScrollPosition();
|
|
|
|
});
|
|
|
|
},
|
2018-07-13 10:43:11 +00:00
|
|
|
},
|
|
|
|
created() {
|
|
|
|
this.$nextTick(() => {
|
|
|
|
if (!this.$refs.chat) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (window.IntersectionObserver) {
|
|
|
|
this.historyObserver = new window.IntersectionObserver(this.onLoadButtonObserved, {
|
|
|
|
root: this.$refs.chat,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-07-18 18:31:51 +00:00
|
|
|
this.jumpToBottom();
|
2018-07-13 10:43:11 +00:00
|
|
|
});
|
|
|
|
},
|
|
|
|
mounted() {
|
2018-09-21 15:36:21 +00:00
|
|
|
this.$refs.chat.addEventListener("scroll", this.handleScroll, {passive: true});
|
2018-07-13 19:53:30 +00:00
|
|
|
|
2018-08-31 18:37:39 +00:00
|
|
|
this.$root.$on("resize", this.handleResize);
|
2018-08-31 18:15:46 +00:00
|
|
|
|
2018-07-13 10:43:11 +00:00
|
|
|
this.$nextTick(() => {
|
|
|
|
if (this.historyObserver) {
|
|
|
|
this.historyObserver.observe(this.$refs.loadMoreButton);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
2018-09-14 13:27:41 +00:00
|
|
|
beforeUpdate() {
|
|
|
|
this.unreadMarkerShown = false;
|
|
|
|
},
|
2018-07-13 17:39:35 +00:00
|
|
|
beforeDestroy() {
|
2018-08-31 18:37:39 +00:00
|
|
|
this.$root.$off("resize", this.handleResize);
|
2018-09-21 15:36:21 +00:00
|
|
|
this.$refs.chat.removeEventListener("scroll", this.handleScroll);
|
2018-07-13 17:39:35 +00:00
|
|
|
},
|
2018-07-13 10:43:11 +00:00
|
|
|
destroyed() {
|
|
|
|
if (this.historyObserver) {
|
|
|
|
this.historyObserver.disconnect();
|
|
|
|
}
|
|
|
|
},
|
2018-07-08 17:22:01 +00:00
|
|
|
methods: {
|
2018-07-11 09:27:44 +00:00
|
|
|
shouldDisplayDateMarker(message, id) {
|
|
|
|
const previousMessage = this.condensedMessages[id - 1];
|
2018-07-08 17:22:01 +00:00
|
|
|
|
2018-07-11 09:27:44 +00:00
|
|
|
if (!previousMessage) {
|
2018-07-08 17:22:01 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2019-07-17 09:33:59 +00:00
|
|
|
return new Date(previousMessage.time).getDay() !== new Date(message.time).getDay();
|
2018-07-08 17:22:01 +00:00
|
|
|
},
|
2018-07-11 09:27:44 +00:00
|
|
|
shouldDisplayUnreadMarker(id) {
|
2018-09-14 13:27:41 +00:00
|
|
|
if (!this.unreadMarkerShown && id > this.channel.firstUnread) {
|
|
|
|
this.unreadMarkerShown = true;
|
|
|
|
return true;
|
2018-07-11 09:27:44 +00:00
|
|
|
}
|
|
|
|
|
2018-09-14 13:27:41 +00:00
|
|
|
return false;
|
2018-07-08 17:22:01 +00:00
|
|
|
},
|
2018-07-09 18:37:52 +00:00
|
|
|
onCopy() {
|
|
|
|
clipboard(this.$el);
|
|
|
|
},
|
2018-07-11 20:45:01 +00:00
|
|
|
onLinkPreviewToggle(preview, message) {
|
2018-07-25 11:04:10 +00:00
|
|
|
this.keepScrollPosition();
|
|
|
|
|
2018-07-11 20:45:01 +00:00
|
|
|
// Tell the server we're toggling so it remembers at page reload
|
|
|
|
// TODO Avoid sending many single events when using `/collapse` or `/expand`
|
|
|
|
// See https://github.com/thelounge/thelounge/issues/1377
|
|
|
|
socket.emit("msg:preview:toggle", {
|
|
|
|
target: this.channel.id,
|
|
|
|
msgId: message.id,
|
|
|
|
link: preview.link,
|
|
|
|
shown: preview.shown,
|
|
|
|
});
|
|
|
|
},
|
2018-07-13 10:43:11 +00:00
|
|
|
onShowMoreClick() {
|
|
|
|
let lastMessage = this.channel.messages[0];
|
|
|
|
lastMessage = lastMessage ? lastMessage.id : -1;
|
|
|
|
|
2018-07-18 18:40:09 +00:00
|
|
|
this.channel.historyLoading = true;
|
2018-07-13 10:43:11 +00:00
|
|
|
|
|
|
|
socket.emit("more", {
|
|
|
|
target: this.channel.id,
|
|
|
|
lastId: lastMessage,
|
|
|
|
});
|
|
|
|
},
|
|
|
|
onLoadButtonObserved(entries) {
|
|
|
|
entries.forEach((entry) => {
|
|
|
|
if (!entry.isIntersecting) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-07-25 11:12:28 +00:00
|
|
|
this.onShowMoreClick();
|
2018-07-13 10:43:11 +00:00
|
|
|
});
|
|
|
|
},
|
|
|
|
keepScrollPosition() {
|
|
|
|
// If we are already waiting for the next tick to force scroll position,
|
|
|
|
// we have no reason to perform more checks and set it again in the next tick
|
|
|
|
if (this.isWaitingForNextTick) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const el = this.$refs.chat;
|
|
|
|
|
|
|
|
if (!el) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-07-13 20:40:20 +00:00
|
|
|
if (!this.channel.scrolledToBottom) {
|
2018-07-13 10:43:11 +00:00
|
|
|
if (this.channel.historyLoading) {
|
|
|
|
const heightOld = el.scrollHeight - el.scrollTop;
|
|
|
|
|
|
|
|
this.isWaitingForNextTick = true;
|
|
|
|
this.$nextTick(() => {
|
|
|
|
this.isWaitingForNextTick = false;
|
2018-09-21 15:36:21 +00:00
|
|
|
this.skipNextScrollEvent = true;
|
2018-07-13 10:43:11 +00:00
|
|
|
el.scrollTop = el.scrollHeight - heightOld;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.isWaitingForNextTick = true;
|
|
|
|
this.$nextTick(() => {
|
|
|
|
this.isWaitingForNextTick = false;
|
2018-07-18 18:31:51 +00:00
|
|
|
this.jumpToBottom();
|
2018-07-13 10:43:11 +00:00
|
|
|
});
|
|
|
|
},
|
2018-07-13 17:39:35 +00:00
|
|
|
handleScroll() {
|
2018-09-21 15:36:21 +00:00
|
|
|
// Setting scrollTop also triggers scroll event
|
|
|
|
// We don't want to perform calculations for that
|
|
|
|
if (this.skipNextScrollEvent) {
|
|
|
|
this.skipNextScrollEvent = false;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-07-13 17:39:35 +00:00
|
|
|
const el = this.$refs.chat;
|
|
|
|
|
|
|
|
if (!el) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-07-13 20:40:20 +00:00
|
|
|
this.channel.scrolledToBottom = el.scrollHeight - el.scrollTop - el.offsetHeight <= 30;
|
2018-07-13 17:39:35 +00:00
|
|
|
},
|
|
|
|
handleResize() {
|
|
|
|
// Keep message list scrolled to bottom on resize
|
2018-07-18 18:31:51 +00:00
|
|
|
if (this.channel.scrolledToBottom) {
|
|
|
|
this.jumpToBottom();
|
2018-07-13 17:39:35 +00:00
|
|
|
}
|
|
|
|
},
|
2018-07-18 18:31:51 +00:00
|
|
|
jumpToBottom() {
|
2018-09-21 15:36:21 +00:00
|
|
|
this.skipNextScrollEvent = true;
|
|
|
|
this.channel.scrolledToBottom = true;
|
|
|
|
|
2018-07-18 18:31:51 +00:00
|
|
|
const el = this.$refs.chat;
|
|
|
|
el.scrollTop = el.scrollHeight;
|
|
|
|
},
|
2018-07-08 17:22:01 +00:00
|
|
|
},
|
|
|
|
};
|
|
|
|
</script>
|