Move history logic to MessageList, fix previews not keeping scroll

This commit is contained in:
Pavel Djundik 2018-07-13 13:43:11 +03:00 committed by Pavel Djundik
parent 9926157683
commit bb0450cb31
7 changed files with 181 additions and 199 deletions

View File

@ -54,25 +54,7 @@
<div <div
v-else v-else
class="chat-content"> class="chat-content">
<div <MessageList :channel="channel"/>
ref="chat"
class="chat"
>
<div :class="['show-more', { show: channel.moreHistoryAvailable }]">
<button
ref="loadMoreButton"
:disabled="channel.historyLoading || !$root.connected"
class="btn"
@click="onShowMoreClick"
>
<span v-if="channel.historyLoading">Loading</span>
<span v-else>Show older messages</span>
</button>
</div>
<MessageList
:channel="channel"
@keepScrollPosition="keepScrollPosition"/>
</div>
<ChatUserList <ChatUserList
v-if="channel.type === 'channel'" v-if="channel.type === 'channel'"
:channel="channel"/> :channel="channel"/>
@ -87,8 +69,6 @@
</template> </template>
<script> <script>
require("intersection-observer");
const socket = require("../js/socket");
import ParsedMessage from "./ParsedMessage.vue"; import ParsedMessage from "./ParsedMessage.vue";
import MessageList from "./MessageList.vue"; import MessageList from "./MessageList.vue";
import ChatInput from "./ChatInput.vue"; import ChatInput from "./ChatInput.vue";
@ -119,92 +99,5 @@ export default {
} }
}, },
}, },
watch: {
"channel.messages"() {
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.$refs.chat.scrollTop = this.$refs.chat.scrollHeight;
});
},
mounted() {
this.$nextTick(() => {
if (this.historyObserver) {
this.historyObserver.observe(this.$refs.loadMoreButton);
}
});
},
destroyed() {
if (this.historyObserver) {
this.historyObserver.disconnect();
}
},
methods: {
onShowMoreClick() {
let lastMessage = this.channel.messages[0];
lastMessage = lastMessage ? lastMessage.id : -1;
this.$set(this.channel, "historyLoading", true);
socket.emit("more", {
target: this.channel.id,
lastId: lastMessage,
});
},
onLoadButtonObserved(entries) {
entries.forEach((entry) => {
if (!entry.isIntersecting) {
return;
}
entry.target.click();
});
},
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 (el.scrollHeight - el.scrollTop - el.offsetHeight > 30) {
if (this.channel.historyLoading) {
const heightOld = el.scrollHeight - el.scrollTop;
this.isWaitingForNextTick = true;
this.$nextTick(() => {
this.isWaitingForNextTick = false;
el.scrollTop = el.scrollHeight - heightOld;
});
}
return;
}
this.isWaitingForNextTick = true;
this.$nextTick(() => {
this.isWaitingForNextTick = false;
el.scrollTop = el.scrollHeight;
});
},
},
}; };
</script> </script>

View File

@ -67,20 +67,18 @@ export default {
channel: Object, channel: Object,
}, },
watch: { watch: {
"channel.pendingMessage": { "channel.pendingMessage"() {
handler: function() { const style = window.getComputedStyle(this.$refs.input);
const style = window.getComputedStyle(this.$refs.input); const lineHeight = parseFloat(style.lineHeight, 10) || 1;
const lineHeight = parseFloat(style.lineHeight, 10) || 1;
// Start by resetting height before computing as scrollHeight does not // Start by resetting height before computing as scrollHeight does not
// decrease when deleting characters // decrease when deleting characters
resetInputHeight(this); resetInputHeight(this);
// Use scrollHeight to calculate how many lines there are in input, and ceil the value // Use scrollHeight to calculate how many lines there are in input, and ceil the value
// because some browsers tend to incorrently round the values when using high density // because some browsers tend to incorrently round the values when using high density
// displays or using page zoom feature // displays or using page zoom feature
this.$refs.input.style.height = Math.ceil(this.$refs.input.scrollHeight / lineHeight) * lineHeight + "px"; this.$refs.input.style.height = Math.ceil(this.$refs.input.scrollHeight / lineHeight) * lineHeight + "px";
},
}, },
}, },
mounted() { mounted() {

View File

@ -125,6 +125,7 @@ export default {
name: "LinkPreview", name: "LinkPreview",
props: { props: {
link: Object, link: Object,
keepScrollPosition: Function,
}, },
data() { data() {
return { return {
@ -137,29 +138,36 @@ export default {
return this.isContentShown ? "Less" : "More"; return this.isContentShown ? "Less" : "More";
}, },
}, },
watch: {
"link.type"() {
this.onPreviewUpdate();
},
},
mounted() { mounted() {
// Don't display previews while they are loading on the server // Don't display previews while they are loading on the server
if (this.link.type === "loading") { if (this.link.type === "loading") {
return; return;
} }
// Error don't have any media to render this.onPreviewUpdate();
if (this.link.type === "error") {
this.onPreviewReady();
}
// If link doesn't have a thumbnail, render it
if (this.link.type === "link" && !this.link.thumb) {
this.onPreviewReady();
}
}, },
methods: { methods: {
onPreviewUpdate() {
// Error don't have any media to render
if (this.link.type === "error") {
this.onPreviewReady();
}
// If link doesn't have a thumbnail, render it
if (this.link.type === "link" && !this.link.thumb) {
this.onPreviewReady();
}
},
onPreviewReady() { onPreviewReady() {
const options = require("../js/options"); const options = require("../js/options");
this.$set(this.link, "canDisplay", this.link.type !== "loading" && options.shouldOpenMessagePreview(this.link.type)); this.$set(this.link, "canDisplay", this.link.type !== "loading" && options.shouldOpenMessagePreview(this.link.type));
// parent 1 - message - parent 2 - messagelist this.keepScrollPosition();
this.$parent.$parent.$emit("keepScrollPosition");
if (this.link.type !== "link") { if (this.link.type !== "link") {
return; return;

View File

@ -21,6 +21,17 @@
:is="messageComponent" :is="messageComponent"
:message="message"/> :message="message"/>
</template> </template>
<template v-if="message.type === 'action'">
<span class="from"/>
<span class="content">
<span class="text"><Username :user="message.from"/> <ParsedMessage :message="message"/></span>
<LinkPreview
v-for="preview in message.previews"
:keep-scroll-position="keepScrollPosition"
:key="preview.link"
:link="preview"/>
</span>
</template>
<template v-else> <template v-else>
<span class="from"> <span class="from">
<template v-if="message.from && message.from.nick"> <template v-if="message.from && message.from.nick">
@ -29,9 +40,9 @@
</span> </span>
<span class="content"> <span class="content">
<span class="text"><ParsedMessage :message="message"/></span> <span class="text"><ParsedMessage :message="message"/></span>
<LinkPreview <LinkPreview
v-for="preview in message.previews" v-for="preview in message.previews"
:keep-scroll-position="keepScrollPosition"
:key="preview.link" :key="preview.link"
:link="preview"/> :link="preview"/>
</span> </span>
@ -54,6 +65,7 @@ export default {
components: MessageTypes, components: MessageTypes,
props: { props: {
message: Object, message: Object,
keepScrollPosition: Function,
}, },
computed: { computed: {
messageComponent() { messageComponent() {

View File

@ -1,47 +1,66 @@
<template> <template>
<div <div
class="messages" ref="chat"
role="log" class="chat"
aria-live="polite"
aria-relevant="additions"
@copy="onCopy"
> >
<template v-for="(message, id) in condensedMessages"> <div :class="['show-more', { show: channel.moreHistoryAvailable }]">
<div <button
v-if="shouldDisplayDateMarker(message, id)" ref="loadMoreButton"
:key="message.id + '-date'" :disabled="channel.historyLoading || !$root.connected"
:data-time="message.time" class="btn"
:aria-label="message.time | localedate" @click="onShowMoreClick"
class="date-marker-container tooltipped tooltipped-s"
> >
<div class="date-marker"> <span v-if="channel.historyLoading">Loading</span>
<span <span v-else>Show older messages</span>
:data-label="message.time | friendlydate" </button>
class="date-marker-text"/> </div>
<div
class="messages"
role="log"
aria-live="polite"
aria-relevant="additions"
@copy="onCopy"
>
<template v-for="(message, id) in condensedMessages">
<div
v-if="shouldDisplayDateMarker(message, id)"
:key="message.id + '-date'"
:data-time="message.time"
:aria-label="message.time | localedate"
class="date-marker-container tooltipped tooltipped-s"
>
<div class="date-marker">
<span
:data-label="message.time | friendlydate"
class="date-marker-text"/>
</div>
</div>
<div
v-if="shouldDisplayUnreadMarker(id)"
:key="message.id + '-unread'"
class="unread-marker"
>
<span class="unread-marker-text"/>
</div> </div>
</div>
<div
v-if="shouldDisplayUnreadMarker(id)"
:key="message.id + '-unread'"
class="unread-marker"
>
<span class="unread-marker-text"/>
</div>
<MessageCondensed <MessageCondensed
v-if="message.type === 'condensed'" v-if="message.type === 'condensed'"
:key="message.id" :key="message.id"
:messages="message.messages"/> :messages="message.messages"/>
<Message <Message
v-else v-else
:message="message" :message="message"
:key="message.id" :key="message.id"
@linkPreviewToggle="onLinkPreviewToggle"/> :keep-scroll-position="keepScrollPosition"
</template> @linkPreviewToggle="onLinkPreviewToggle"/>
</template>
</div>
</div> </div>
</template> </template>
<script> <script>
require("intersection-observer");
const constants = require("../js/constants"); const constants = require("../js/constants");
const clipboard = require("../js/clipboard"); const clipboard = require("../js/clipboard");
import socket from "../js/socket"; import socket from "../js/socket";
@ -94,6 +113,38 @@ export default {
return condensed; return condensed;
}, },
}, },
watch: {
"channel.messages"() {
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.$refs.chat.scrollTop = this.$refs.chat.scrollHeight;
});
},
mounted() {
this.$nextTick(() => {
if (this.historyObserver) {
this.historyObserver.observe(this.$refs.loadMoreButton);
}
});
},
destroyed() {
if (this.historyObserver) {
this.historyObserver.disconnect();
}
},
methods: { methods: {
shouldDisplayDateMarker(message, id) { shouldDisplayDateMarker(message, id) {
const previousMessage = this.condensedMessages[id - 1]; const previousMessage = this.condensedMessages[id - 1];
@ -127,6 +178,59 @@ export default {
shown: preview.shown, shown: preview.shown,
}); });
}, },
onShowMoreClick() {
let lastMessage = this.channel.messages[0];
lastMessage = lastMessage ? lastMessage.id : -1;
this.$set(this.channel, "historyLoading", true);
socket.emit("more", {
target: this.channel.id,
lastId: lastMessage,
});
},
onLoadButtonObserved(entries) {
entries.forEach((entry) => {
if (!entry.isIntersecting) {
return;
}
entry.target.click();
});
},
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 (el.scrollHeight - el.scrollTop - el.offsetHeight > 30) {
if (this.channel.historyLoading) {
const heightOld = el.scrollHeight - el.scrollTop;
this.isWaitingForNextTick = true;
this.$nextTick(() => {
this.isWaitingForNextTick = false;
el.scrollTop = el.scrollHeight - heightOld;
});
}
return;
}
this.isWaitingForNextTick = true;
this.$nextTick(() => {
this.isWaitingForNextTick = false;
el.scrollTop = el.scrollHeight;
});
},
}, },
}; };
</script> </script>

View File

@ -1,30 +0,0 @@
<template>
<span class="content">
<Username :user="message.from"/>
<span
ref="text"
class="text"><ParsedMessage :message="message"/></span>
<LinkPreview
v-for="preview in message.previews"
:key="preview.link"
:link="preview"/>
</span>
</template>
<script>
import ParsedMessage from "../ParsedMessage.vue";
import LinkPreview from "../LinkPreview.vue";
import Username from "../Username.vue";
export default {
name: "MessageTypeAction",
components: {
ParsedMessage,
LinkPreview,
Username,
},
props: {
message: Object,
},
};
</script>

View File

@ -1,7 +1,6 @@
"use strict"; "use strict";
const socket = require("../socket"); const socket = require("../socket");
const {shouldOpenMessagePreview} = require("../options");
const {vueApp, findChannel} = require("../vue"); const {vueApp, findChannel} = require("../vue");
socket.on("msg:preview", function(data) { socket.on("msg:preview", function(data) {
@ -15,8 +14,6 @@ socket.on("msg:preview", function(data) {
const previewIndex = message.previews.findIndex((m) => m.link === data.preview.link); const previewIndex = message.previews.findIndex((m) => m.link === data.preview.link);
if (previewIndex > -1) { if (previewIndex > -1) {
data.preview.canDisplay = shouldOpenMessagePreview(data.preview.type);
vueApp.$set(message.previews, previewIndex, data.preview); vueApp.$set(message.previews, previewIndex, data.preview);
} }
}); });