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
v-else
class="chat-content">
<div
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>
<MessageList :channel="channel"/>
<ChatUserList
v-if="channel.type === 'channel'"
:channel="channel"/>
@ -87,8 +69,6 @@
</template>
<script>
require("intersection-observer");
const socket = require("../js/socket");
import ParsedMessage from "./ParsedMessage.vue";
import MessageList from "./MessageList.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>

View file

@ -67,20 +67,18 @@ export default {
channel: Object,
},
watch: {
"channel.pendingMessage": {
handler: function() {
const style = window.getComputedStyle(this.$refs.input);
const lineHeight = parseFloat(style.lineHeight, 10) || 1;
"channel.pendingMessage"() {
const style = window.getComputedStyle(this.$refs.input);
const lineHeight = parseFloat(style.lineHeight, 10) || 1;
// Start by resetting height before computing as scrollHeight does not
// decrease when deleting characters
resetInputHeight(this);
// Start by resetting height before computing as scrollHeight does not
// decrease when deleting characters
resetInputHeight(this);
// 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
// displays or using page zoom feature
this.$refs.input.style.height = Math.ceil(this.$refs.input.scrollHeight / lineHeight) * lineHeight + "px";
},
// 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
// displays or using page zoom feature
this.$refs.input.style.height = Math.ceil(this.$refs.input.scrollHeight / lineHeight) * lineHeight + "px";
},
},
mounted() {

View file

@ -125,6 +125,7 @@ export default {
name: "LinkPreview",
props: {
link: Object,
keepScrollPosition: Function,
},
data() {
return {
@ -137,29 +138,36 @@ export default {
return this.isContentShown ? "Less" : "More";
},
},
watch: {
"link.type"() {
this.onPreviewUpdate();
},
},
mounted() {
// Don't display previews while they are loading on the server
if (this.link.type === "loading") {
return;
}
// 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();
}
this.onPreviewUpdate();
},
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() {
const options = require("../js/options");
this.$set(this.link, "canDisplay", this.link.type !== "loading" && options.shouldOpenMessagePreview(this.link.type));
// parent 1 - message - parent 2 - messagelist
this.$parent.$parent.$emit("keepScrollPosition");
this.keepScrollPosition();
if (this.link.type !== "link") {
return;

View file

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

View file

@ -1,47 +1,66 @@
<template>
<div
class="messages"
role="log"
aria-live="polite"
aria-relevant="additions"
@copy="onCopy"
ref="chat"
class="chat"
>
<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="['show-more', { show: channel.moreHistoryAvailable }]">
<button
ref="loadMoreButton"
:disabled="channel.historyLoading || !$root.connected"
class="btn"
@click="onShowMoreClick"
>
<div class="date-marker">
<span
:data-label="message.time | friendlydate"
class="date-marker-text"/>
<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">
<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
v-if="shouldDisplayUnreadMarker(id)"
:key="message.id + '-unread'"
class="unread-marker"
>
<span class="unread-marker-text"/>
</div>
<MessageCondensed
v-if="message.type === 'condensed'"
:key="message.id"
:messages="message.messages"/>
<Message
v-else
:message="message"
:key="message.id"
@linkPreviewToggle="onLinkPreviewToggle"/>
</template>
<MessageCondensed
v-if="message.type === 'condensed'"
:key="message.id"
:messages="message.messages"/>
<Message
v-else
:message="message"
:key="message.id"
:keep-scroll-position="keepScrollPosition"
@linkPreviewToggle="onLinkPreviewToggle"/>
</template>
</div>
</div>
</template>
<script>
require("intersection-observer");
const constants = require("../js/constants");
const clipboard = require("../js/clipboard");
import socket from "../js/socket";
@ -94,6 +113,38 @@ export default {
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: {
shouldDisplayDateMarker(message, id) {
const previousMessage = this.condensedMessages[id - 1];
@ -127,6 +178,59 @@ export default {
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>

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";
const socket = require("../socket");
const {shouldOpenMessagePreview} = require("../options");
const {vueApp, findChannel} = require("../vue");
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);
if (previewIndex > -1) {
data.preview.canDisplay = shouldOpenMessagePreview(data.preview.type);
vueApp.$set(message.previews, previewIndex, data.preview);
}
});