Move history logic to MessageList, fix previews not keeping scroll
This commit is contained in:
parent
9926157683
commit
bb0450cb31
@ -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>
|
||||||
|
@ -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() {
|
||||||
|
@ -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;
|
||||||
|
@ -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() {
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
|
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user