Make history loading work

This commit is contained in:
Pavel Djundik 2018-07-08 16:42:54 +03:00 committed by Pavel Djundik
parent 25840dfef4
commit 0e930c9356
14 changed files with 131 additions and 155 deletions

View File

@ -59,7 +59,10 @@
</aside> </aside>
<div id="sidebar-overlay"/> <div id="sidebar-overlay"/>
<article id="windows"> <article id="windows">
<Chat v-if="activeChannel" :network="activeChannel.network" :channel="activeChannel.channel"/> <Chat
v-if="activeChannel"
:network="activeChannel.network"
:channel="activeChannel.channel"/>
<div <div
id="sign-in" id="sign-in"
class="window" class="window"

View File

@ -1,6 +1,10 @@
<template> <template>
<div id="chat-container" class="window"> <div
<div id="chat"> id="chat-container"
class="window">
<div
id="chat"
ref="chat">
<div <div
:id="'chan-' + channel.id" :id="'chan-' + channel.id"
:class="[channel.type, 'chan', 'active']" :class="[channel.type, 'chan', 'active']"
@ -35,11 +39,17 @@
<div class="chat"> <div class="chat">
<div <div
v-if="channel.messages.length > 0" v-if="channel.messages.length > 0"
class="show-more show"> ref="loadMoreButton"
:disabled="channel.historyLoading"
class="show-more show"
@click="onShowMoreClick"
>
<button <button
:data-id="channel.id" v-if="channel.historyLoading"
class="btn" class="btn">Loading</button>
data-alt-text="Loading…">Show older messages</button> <button
v-else
class="btn">Show older messages</button>
</div> </div>
<div <div
class="messages" class="messages"
@ -86,8 +96,14 @@
tabindex="-1"> tabindex="-1">
</div> </div>
<div class="names"> <div class="names">
<div v-for="(users, mode) in groupedUsers" :key="mode" :class="['user-mode', getModeClass(mode)]"> <div
<Username v-for="user in users" :key="user.nick" :user="user"/> v-for="(users, mode) in groupedUsers"
:key="mode"
:class="['user-mode', getModeClass(mode)]">
<Username
v-for="user in users"
:key="user.nick"
:user="user"/>
</div> </div>
</div> </div>
</aside> </aside>
@ -95,34 +111,18 @@
</div> </div>
</div> </div>
<div id="connection-error"/> <div id="connection-error"/>
<form <ChatInput
id="form" :network="network"
method="post" :channel="channel"/>
action="">
<span id="nick">{{network.nick}}</span>
<textarea
id="input"
class="mousetrap"
v-model="channel.pendingMessage"
:placeholder="getInputPlaceholder(channel)"
:aria-label="getInputPlaceholder(channel)"
/>
<span
id="submit-tooltip"
class="tooltipped tooltipped-w tooltipped-no-touch"
aria-label="Send message">
<button
id="submit"
type="submit"
aria-label="Send message"/>
</span>
</form>
</div> </div>
</template> </template>
<script> <script>
require("intersection-observer");
const socket = require("../js/socket");
import Message from "./Message.vue"; import Message from "./Message.vue";
import Username from "./Username.vue"; import Username from "./Username.vue";
import ChatInput from "./ChatInput.vue";
const modes = { const modes = {
"~": "owner", "~": "owner",
@ -139,6 +139,7 @@ export default {
components: { components: {
Message, Message,
Username, Username,
ChatInput,
}, },
props: { props: {
network: Object, network: Object,
@ -159,6 +160,23 @@ export default {
return groups; return groups;
}, },
}, },
created() {
if (window.IntersectionObserver) {
this.historyObserver = new window.IntersectionObserver(loadMoreHistory, {
root: this.$refs.chat,
});
}
},
mounted() {
if (this.historyObserver) {
this.historyObserver.observe(this.$refs.loadMoreButton);
}
},
destroyed() {
if (this.historyObserver) {
this.historyObserver.unobserve(this.$refs.loadMoreButton);
}
},
methods: { methods: {
shouldDisplayDateMarker(id) { shouldDisplayDateMarker(id) {
const previousTime = this.channel.messages[id - 1]; const previousTime = this.channel.messages[id - 1];
@ -180,16 +198,30 @@ export default {
return true; return true;
}, },
getInputPlaceholder(channel) {
if (channel.type === "channel" || channel.type === "query") {
return `Write to ${channel.name}`;
}
return "";
},
getModeClass(mode) { getModeClass(mode) {
return modes[mode]; return modes[mode];
}, },
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,
});
},
}, },
}; };
function loadMoreHistory(entries) {
entries.forEach((entry) => {
if (!entry.isIntersecting) {
return;
}
entry.target.click();
});
}
</script> </script>

View File

@ -0,0 +1,43 @@
<template>
<form
id="form"
method="post"
action="">
<span id="nick">{{ network.nick }}</span>
<textarea
id="input"
v-model="channel.pendingMessage"
:placeholder="getInputPlaceholder(channel)"
:aria-label="getInputPlaceholder(channel)"
class="mousetrap"
/>
<span
id="submit-tooltip"
class="tooltipped tooltipped-w tooltipped-no-touch"
aria-label="Send message">
<button
id="submit"
type="submit"
aria-label="Send message"/>
</span>
</form>
</template>
<script>
export default {
name: "ChatInput",
props: {
network: Object,
channel: Object,
},
methods: {
getInputPlaceholder(channel) {
if (channel.type === "channel" || channel.type === "query") {
return `Write to ${channel.name}`;
}
return "";
},
},
};
</script>

View File

@ -1,12 +0,0 @@
"use strict";
let diff;
module.exports = function(a, opt) {
if (a !== diff) {
diff = a;
return opt.fn(this);
}
return opt.inverse(this);
};

View File

@ -9,7 +9,6 @@ require("./libs/jquery/stickyscroll");
const slideoutMenu = require("./slideout"); const slideoutMenu = require("./slideout");
const templates = require("../views"); const templates = require("../views");
const socket = require("./socket"); const socket = require("./socket");
const render = require("./render");
require("./socket-events"); require("./socket-events");
const storage = require("./localStorage"); const storage = require("./localStorage");
const utils = require("./utils"); const utils = require("./utils");
@ -232,7 +231,7 @@ $(function() {
const type = chan.data("type"); const type = chan.data("type");
if (self.hasClass("chan")) { if (self.hasClass("chan")) {
$("#chat-container").addClass("active"); vueApp.$nextTick(() => $("#chat-container").addClass("active"));
} }
const chanChat = chan.find(".chat"); const chanChat = chan.find(".chat");

View File

@ -8,23 +8,11 @@ const utils = require("./utils");
const constants = require("./constants"); const constants = require("./constants");
const condensed = require("./condensed"); const condensed = require("./condensed");
const JoinChannel = require("./join-channel"); const JoinChannel = require("./join-channel");
const helpers_parse = require("./libs/handlebars/parse");
const Userlist = require("./userlist");
const storage = require("./localStorage"); const storage = require("./localStorage");
const {vueApp} = require("./vue"); const {vueApp} = require("./vue");
const chat = $("#chat");
const sidebar = $("#sidebar"); const sidebar = $("#sidebar");
require("intersection-observer");
const historyObserver = window.IntersectionObserver ?
new window.IntersectionObserver(loadMoreHistory, {
root: chat.get(0),
}) : null;
module.exports = { module.exports = {
renderChannel,
renderNetworks, renderNetworks,
trimMessageInChannel, trimMessageInChannel,
}; };
@ -124,24 +112,6 @@ function buildChatMessage(msg) {
return renderedMessage; return renderedMessage;
} }
function renderChannel(data) {
renderChannelMessages(data);
if (data.type === "channel") {
//const users = renderChannelUsers(data);
//Userlist.handleKeybinds(users.find(".search"));
}
if (historyObserver) {
//historyObserver.observe(chat.find("#chan-" + data.id + " .show-more").get(0));
}
}
function renderChannelMessages(data) {
const channel = chat.find("#chan-" + data.id + " .messages");
}
function renderNetworks(data, singleNetwork) { function renderNetworks(data, singleNetwork) {
const collapsed = new Set(JSON.parse(storage.get("thelounge.networks.collapsed"))); const collapsed = new Set(JSON.parse(storage.get("thelounge.networks.collapsed")));
@ -187,8 +157,6 @@ function renderNetworks(data, singleNetwork) {
if (newChannels.length > 0) { if (newChannels.length > 0) {
newChannels.forEach((channel) => { newChannels.forEach((channel) => {
renderChannel(channel);
if (channel.type === "channel") { if (channel.type === "channel") {
channel.usersOutdated = true; channel.usersOutdated = true;
} }
@ -227,22 +195,6 @@ function trimMessageInChannel(channel, messageLimit) {
}); });
} }
function loadMoreHistory(entries) {
entries.forEach((entry) => {
if (!entry.isIntersecting) {
return;
}
const target = $(entry.target).find("button");
if (target.prop("disabled")) {
return;
}
target.trigger("click");
});
}
sidebar.on("click", "button.collapse-network", (e) => collapseNetwork($(e.target))); sidebar.on("click", "button.collapse-network", (e) => collapseNetwork($(e.target)));
function collapseNetwork(target) { function collapseNetwork(target) {

View File

@ -9,7 +9,7 @@ const slideoutMenu = require("../slideout");
const sidebar = $("#sidebar"); const sidebar = $("#sidebar");
const storage = require("../localStorage"); const storage = require("../localStorage");
const utils = require("../utils"); const utils = require("../utils");
const {Vue, vueApp} = require("../vue"); const {vueApp} = require("../vue");
socket.on("init", function(data) { socket.on("init", function(data) {
$("#loading-page-message, #connection-error").text("Rendering…"); $("#loading-page-message, #connection-error").text("Rendering…");
@ -24,7 +24,7 @@ socket.on("init", function(data) {
vueApp.networks = data.networks; vueApp.networks = data.networks;
if (data.networks.length > 0) { if (data.networks.length > 0) {
Vue.nextTick(() => render.renderNetworks(data)); vueApp.$nextTick(() => render.renderNetworks(data));
} }
$("#connection-error").removeClass("shown"); $("#connection-error").removeClass("shown");
@ -66,7 +66,7 @@ socket.on("init", function(data) {
} }
} }
Vue.nextTick(() => openCorrectChannel(previousActive, data.active)); vueApp.$nextTick(() => openCorrectChannel(previousActive, data.active));
}); });
function openCorrectChannel(clientActive, serverActive) { function openCorrectChannel(clientActive, serverActive) {

View File

@ -2,23 +2,19 @@
const $ = require("jquery"); const $ = require("jquery");
const socket = require("../socket"); const socket = require("../socket");
const render = require("../render");
const templates = require("../../views");
const sidebar = $("#sidebar"); const sidebar = $("#sidebar");
const {Vue, vueApp} = require("../vue"); const {vueApp} = require("../vue");
socket.on("join", function(data) { socket.on("join", function(data) {
vueApp.networks.find((n) => n.uuid === data.network) vueApp.networks.find((n) => n.uuid === data.network)
.channels.splice(data.index || -1, 0, data.chan); .channels.splice(data.index || -1, 0, data.chan);
Vue.nextTick(() => render.renderChannel(data.chan));
// Queries do not automatically focus, unless the user did a whois // Queries do not automatically focus, unless the user did a whois
if (data.chan.type === "query" && !data.shouldOpen) { if (data.chan.type === "query" && !data.shouldOpen) {
return; return;
} }
Vue.nextTick(() => { vueApp.$nextTick(() => {
sidebar.find(".chan") sidebar.find(".chan")
.sort(function(a, b) { .sort(function(a, b) {
return $(a).data("id") - $(b).data("id"); return $(a).data("id") - $(b).data("id");

View File

@ -2,13 +2,11 @@
const $ = require("jquery"); const $ = require("jquery");
const socket = require("../socket"); const socket = require("../socket");
const render = require("../render");
const condensed = require("../condensed"); const condensed = require("../condensed");
const chat = $("#chat"); const {vueApp, findChannel} = require("../vue");
const {Vue, vueApp, findChannel} = require("../vue");
socket.on("more", function(data) { socket.on("more", function(data) {
let chan = chat.find("#chan-" + data.chan); let chan = $("#chat #chan-" + data.chan);
const type = chan.data("type"); const type = chan.data("type");
chan = chan.find(".messages"); chan = chan.find(".messages");
@ -29,8 +27,9 @@ socket.on("more", function(data) {
} }
channel.channel.messages.unshift(...data.messages); channel.channel.messages.unshift(...data.messages);
channel.channel.historyLoading = false;
Vue.nextTick(() => { vueApp.$nextTick(() => {
// restore scroll position // restore scroll position
const position = chan.height() - heightOld; const position = chan.height() - heightOld;
scrollable.finish().scrollTop(position); scrollable.finish().scrollTop(position);
@ -40,11 +39,6 @@ socket.on("more", function(data) {
scrollable.find(".show-more").removeClass("show"); scrollable.find(".show-more").removeClass("show");
} }
// Swap button text back from its alternative label
const showMoreBtn = scrollable.find(".show-more button");
swapText(showMoreBtn);
showMoreBtn.prop("disabled", false);
return; return;
// Join duplicate condensed messages together // Join duplicate condensed messages together
@ -62,28 +56,3 @@ socket.on("more", function(data) {
condensedDuplicate.remove(); condensedDuplicate.remove();
} }
}); });
chat.on("click", ".show-more button", function() {
const self = $(this);
const lastMessage = self.closest(".chat").find(".msg:not(.condensed)").first();
let lastMessageId = -1;
if (lastMessage.length > 0) {
lastMessageId = parseInt(lastMessage.prop("id").replace("msg-", ""), 10);
}
// Swap button text with its alternative label
swapText(self);
self.prop("disabled", true);
socket.emit("more", {
target: self.data("id"),
lastId: lastMessageId,
});
});
// Given a button, swap its text with the content of `data-alt-text`
function swapText(btn) {
const altText = btn.data("alt-text");
btn.data("alt-text", btn.text()).text(altText);
}

View File

@ -6,12 +6,12 @@ const render = require("../render");
const templates = require("../../views"); const templates = require("../../views");
const sidebar = $("#sidebar"); const sidebar = $("#sidebar");
const utils = require("../utils"); const utils = require("../utils");
const {Vue, vueApp} = require("../vue"); const {vueApp} = require("../vue");
socket.on("network", function(data) { socket.on("network", function(data) {
vueApp.networks.push(data.networks[0]); vueApp.networks.push(data.networks[0]);
Vue.nextTick(() => { vueApp.$nextTick(() => {
render.renderNetworks(data, true); render.renderNetworks(data, true);
sidebar.find(".chan") sidebar.find(".chan")

View File

@ -3,12 +3,12 @@
const $ = require("jquery"); const $ = require("jquery");
const socket = require("../socket"); const socket = require("../socket");
const sidebar = $("#sidebar"); const sidebar = $("#sidebar");
const {Vue, vueApp} = require("../vue"); const {vueApp} = require("../vue");
socket.on("quit", function(data) { socket.on("quit", function(data) {
vueApp.networks.splice(vueApp.networks.findIndex((n) => n.uuid === data.network), 1); vueApp.networks.splice(vueApp.networks.findIndex((n) => n.uuid === data.network), 1);
Vue.nextTick(() => { vueApp.$nextTick(() => {
const chan = sidebar.find(".chan"); const chan = sidebar.find(".chan");
if (chan.length === 0) { if (chan.length === 0) {

View File

@ -2,7 +2,6 @@
const $ = require("jquery"); const $ = require("jquery");
const io = require("socket.io-client"); const io = require("socket.io-client");
const utils = require("./utils");
const socket = io({ const socket = io({
transports: $(document.body).data("transports"), transports: $(document.body).data("transports"),
@ -51,6 +50,7 @@ function handleDisconnect(data) {
// If the server shuts down, socket.io skips reconnection // If the server shuts down, socket.io skips reconnection
// and we have to manually call connect to start the process // and we have to manually call connect to start the process
if (socket.io.skipReconnect) { if (socket.io.skipReconnect) {
const utils = require("./utils");
utils.requestIdleCallback(() => socket.connect(), 2000); utils.requestIdleCallback(() => socket.connect(), 2000);
} }
} }

View File

@ -45,7 +45,6 @@ function findChannel(id) {
} }
module.exports = { module.exports = {
Vue,
vueApp, vueApp,
findChannel, findChannel,
}; };

View File

@ -1,5 +0,0 @@
<div class="date-marker-container tooltipped tooltipped-s" data-time="{{time}}" aria-label="{{localedate time}}">
<div class="date-marker">
<span class="date-marker-text" data-label="{{friendlydate time}}"></span>
</div>
</div>