thelounge/client/components/MessageList.vue

327 lines
7.7 KiB
Vue
Raw Normal View History

<template>
2019-07-17 09:33:59 +00:00
<div ref="chat" class="chat" tabindex="-1">
<div :class="['show-more', {show: channel.moreHistoryAvailable}]">
<button
ref="loadMoreButton"
:disabled="channel.historyLoading || !$store.state.isConnected"
class="btn"
@click="onShowMoreClick"
>
<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"
@copy="onCopy"
>
<template v-for="(message, id) in condensedMessages">
<DateMarker
v-if="shouldDisplayDateMarker(message, id)"
:key="message.id + '-date'"
:message="message"
/>
<div
v-if="shouldDisplayUnreadMarker(message.id)"
:key="message.id + '-unread'"
class="unread-marker"
>
2018-07-29 17:57:14 +00:00
<span class="unread-marker-text" />
</div>
<MessageCondensed
v-if="message.type === 'condensed'"
:key="message.messages[0].id"
2018-07-19 17:44:24 +00:00
:network="network"
2018-07-29 17:57:14 +00:00
:keep-scroll-position="keepScrollPosition"
:messages="message.messages"
/>
<Message
v-else
2018-07-29 17:57:14 +00:00
:key="message.id"
2019-11-09 22:21:34 +00:00
:channel="channel"
2018-07-19 17:44:24 +00:00
:network="network"
:message="message"
:keep-scroll-position="keepScrollPosition"
@linkPreviewToggle="onLinkPreviewToggle"
/>
</template>
</div>
</div>
</template>
<script>
require("intersection-observer");
2019-11-16 17:24:03 +00:00
import constants from "../js/constants";
import clipboard from "../js/clipboard";
import socket from "../js/socket";
import Message from "./Message.vue";
2018-07-09 12:14:44 +00:00
import MessageCondensed from "./MessageCondensed.vue";
import DateMarker from "./DateMarker.vue";
export default {
name: "MessageList",
components: {
Message,
2018-07-09 12:14:44 +00:00
MessageCondensed,
DateMarker,
},
props: {
2018-07-19 17:44:24 +00:00
network: Object,
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;
}
// If actions are hidden, just return a message list with them excluded
if (this.$store.state.settings.statusMessages === "hidden") {
2019-07-17 09:33:59 +00:00
return this.channel.messages.filter(
(message) => !constants.condensedTypes.includes(message.type)
);
}
// If actions are not condensed, just return raw message list
if (this.$store.state.settings.statusMessages !== "condensed") {
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);
// Set id of the condensed container to last message id,
// which is required for the unread marker to work correctly
lastCondensedContainer.id = message.id;
// 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;
},
},
watch: {
"channel.id"() {
this.channel.scrolledToBottom = true;
// 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);
}
},
"channel.messages"() {
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();
});
},
},
created() {
this.$nextTick(() => {
if (!this.$refs.chat) {
return;
}
if (window.IntersectionObserver) {
this.historyObserver = new window.IntersectionObserver(this.onLoadButtonObserved, {
root: this.$refs.chat,
});
}
this.jumpToBottom();
});
},
mounted() {
this.$refs.chat.addEventListener("scroll", this.handleScroll, {passive: true});
2018-07-13 19:53:30 +00:00
this.$root.$on("resize", this.handleResize);
this.$nextTick(() => {
if (this.historyObserver) {
this.historyObserver.observe(this.$refs.loadMoreButton);
}
});
},
beforeUpdate() {
this.unreadMarkerShown = false;
},
beforeDestroy() {
this.$root.$off("resize", this.handleResize);
this.$refs.chat.removeEventListener("scroll", this.handleScroll);
},
destroyed() {
if (this.historyObserver) {
this.historyObserver.disconnect();
}
},
methods: {
2018-07-11 09:27:44 +00:00
shouldDisplayDateMarker(message, id) {
const previousMessage = this.condensedMessages[id - 1];
2018-07-11 09:27:44 +00:00
if (!previousMessage) {
return true;
}
2019-07-17 09:33:59 +00:00
return new Date(previousMessage.time).getDay() !== new Date(message.time).getDay();
},
2018-07-11 09:27:44 +00:00
shouldDisplayUnreadMarker(id) {
if (!this.unreadMarkerShown && id > this.channel.firstUnread) {
this.unreadMarkerShown = true;
return true;
2018-07-11 09:27:44 +00:00
}
return false;
},
2018-07-09 18:37:52 +00:00
onCopy() {
clipboard(this.$el);
},
onLinkPreviewToggle(preview, message) {
2018-07-25 11:04:10 +00:00
this.keepScrollPosition();
// Tell the server we're toggling so it remembers at page reload
socket.emit("msg:preview:toggle", {
target: this.channel.id,
msgId: message.id,
link: preview.link,
shown: preview.shown,
});
},
onShowMoreClick() {
if (!this.$store.state.isConnected) {
return;
}
let lastMessage = -1;
// Find the id of first message that isn't showInActive
// If showInActive is set, this message is actually in another channel
for (const message of this.channel.messages) {
if (!message.showInActive) {
lastMessage = message.id;
break;
}
}
this.channel.historyLoading = true;
socket.emit("more", {
target: this.channel.id,
lastId: lastMessage,
});
},
onLoadButtonObserved(entries) {
entries.forEach((entry) => {
if (!entry.isIntersecting) {
return;
}
this.onShowMoreClick();
});
},
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;
}
if (!this.channel.scrolledToBottom) {
if (this.channel.historyLoading) {
const heightOld = el.scrollHeight - el.scrollTop;
this.isWaitingForNextTick = true;
this.$nextTick(() => {
this.isWaitingForNextTick = false;
this.skipNextScrollEvent = true;
el.scrollTop = el.scrollHeight - heightOld;
});
}
return;
}
this.isWaitingForNextTick = true;
this.$nextTick(() => {
this.isWaitingForNextTick = false;
this.jumpToBottom();
});
},
handleScroll() {
// Setting scrollTop also triggers scroll event
// We don't want to perform calculations for that
if (this.skipNextScrollEvent) {
this.skipNextScrollEvent = false;
return;
}
const el = this.$refs.chat;
if (!el) {
return;
}
this.channel.scrolledToBottom = el.scrollHeight - el.scrollTop - el.offsetHeight <= 30;
},
handleResize() {
// Keep message list scrolled to bottom on resize
if (this.channel.scrolledToBottom) {
this.jumpToBottom();
}
},
jumpToBottom() {
this.skipNextScrollEvent = true;
this.channel.scrolledToBottom = true;
const el = this.$refs.chat;
el.scrollTop = el.scrollHeight;
},
},
};
</script>