Single chat container (with buffered input), user list in vue
This commit is contained in:
parent
e931866aeb
commit
25840dfef4
@ -59,38 +59,7 @@
|
||||
</aside>
|
||||
<div id="sidebar-overlay"/>
|
||||
<article id="windows">
|
||||
<div
|
||||
id="chat-container"
|
||||
class="window">
|
||||
<div id="chat">
|
||||
<!--Chat v-if="activeChannel" :channel="activeChannel.channel"/-->
|
||||
<template v-for="network in networks">
|
||||
<Chat
|
||||
v-for="channel in network.channels"
|
||||
:key="channel.id"
|
||||
:channel="channel"/>
|
||||
</template>
|
||||
</div>
|
||||
<div id="connection-error"/>
|
||||
<form
|
||||
id="form"
|
||||
method="post"
|
||||
action="">
|
||||
<span id="nick"/>
|
||||
<textarea
|
||||
id="input"
|
||||
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>
|
||||
</div>
|
||||
<Chat v-if="activeChannel" :network="activeChannel.network" :channel="activeChannel.channel"/>
|
||||
<div
|
||||
id="sign-in"
|
||||
class="window"
|
||||
|
@ -1,7 +1,9 @@
|
||||
<template>
|
||||
<div id="chat-container" class="window">
|
||||
<div id="chat">
|
||||
<div
|
||||
:id="'chan-' + channel.id"
|
||||
:class="[channel.type, 'chan']"
|
||||
:class="[channel.type, 'chan', 'active']"
|
||||
:data-id="channel.id"
|
||||
:data-type="channel.type"
|
||||
:aria-label="channel.name"
|
||||
@ -33,7 +35,7 @@
|
||||
<div class="chat">
|
||||
<div
|
||||
v-if="channel.messages.length > 0"
|
||||
class="show-more">
|
||||
class="show-more show">
|
||||
<button
|
||||
:data-id="channel.id"
|
||||
class="btn"
|
||||
@ -59,7 +61,13 @@
|
||||
class="date-marker-text"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="shouldDisplayUnreadMarker(message.id)"
|
||||
:key="message.id + '-unread'"
|
||||
class="unread-marker"
|
||||
>
|
||||
<span class="unread-marker-text"/>
|
||||
</div>
|
||||
<Message
|
||||
:message="message"
|
||||
:key="message.id"/>
|
||||
@ -71,29 +79,86 @@
|
||||
class="userlist">
|
||||
<div class="count">
|
||||
<input
|
||||
:placeholder="channel.users.length + ' user' + (channel.users.length === 1 ? '' : 's')"
|
||||
type="search"
|
||||
class="search"
|
||||
aria-label="Search among the user list"
|
||||
tabindex="-1">
|
||||
</div>
|
||||
<div class="names names-filtered"/>
|
||||
<div class="names names-original"/>
|
||||
<div class="names">
|
||||
<div 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>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="connection-error"/>
|
||||
<form
|
||||
id="form"
|
||||
method="post"
|
||||
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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Message from "./Message.vue";
|
||||
import Username from "./Username.vue";
|
||||
|
||||
const modes = {
|
||||
"~": "owner",
|
||||
"&": "admin",
|
||||
"!": "admin",
|
||||
"@": "op",
|
||||
"%": "half-op",
|
||||
"+": "voice",
|
||||
"": "normal",
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "Chat",
|
||||
components: {
|
||||
Message,
|
||||
Username,
|
||||
},
|
||||
props: {
|
||||
network: Object,
|
||||
channel: Object,
|
||||
},
|
||||
computed: {
|
||||
groupedUsers() {
|
||||
const groups = {};
|
||||
|
||||
for (const user of this.channel.users) {
|
||||
if (!groups[user.mode]) {
|
||||
groups[user.mode] = [user];
|
||||
} else {
|
||||
groups[user.mode].push(user);
|
||||
}
|
||||
}
|
||||
|
||||
return groups;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
shouldDisplayDateMarker(id) {
|
||||
const previousTime = this.channel.messages[id - 1];
|
||||
@ -106,6 +171,25 @@ export default {
|
||||
|
||||
return (new Date(previousTime.time)).getDay() !== (new Date(currentTime.time)).getDay();
|
||||
},
|
||||
shouldDisplayUnreadMarker(msgId) {
|
||||
if (this.channel.firstUnread < msgId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.channel.firstUnread = 0;
|
||||
|
||||
return true;
|
||||
},
|
||||
getInputPlaceholder(channel) {
|
||||
if (channel.type === "channel" || channel.type === "query") {
|
||||
return `Write to ${channel.name}`;
|
||||
}
|
||||
|
||||
return "";
|
||||
},
|
||||
getModeClass(mode) {
|
||||
return modes[mode];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -1,15 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const modes = {
|
||||
"~": "owner",
|
||||
"&": "admin",
|
||||
"!": "admin",
|
||||
"@": "op",
|
||||
"%": "half-op",
|
||||
"+": "voice",
|
||||
"": "normal",
|
||||
};
|
||||
|
||||
module.exports = function(mode) {
|
||||
return modes[mode];
|
||||
};
|
@ -164,6 +164,7 @@ $(function() {
|
||||
// sidebar specifically. Needs to be done better when window management gets
|
||||
// refactored.
|
||||
const inSidebar = self.parents("#sidebar, #footer").length > 0;
|
||||
let channel;
|
||||
|
||||
if (inSidebar) {
|
||||
chat.data(
|
||||
@ -175,7 +176,7 @@ $(function() {
|
||||
self.data("id")
|
||||
);
|
||||
|
||||
const channel = findChannel(self.data("id"));
|
||||
channel = findChannel(self.data("id"));
|
||||
|
||||
vueApp.activeChannel = channel;
|
||||
|
||||
@ -207,11 +208,9 @@ $(function() {
|
||||
const lastActive = $("#windows > .active");
|
||||
|
||||
lastActive
|
||||
.removeClass("active")
|
||||
.find(".chat")
|
||||
.unsticky();
|
||||
.removeClass("active");
|
||||
|
||||
const lastActiveChan = lastActive.find(".chan.active");
|
||||
/*const lastActiveChan = lastActive.find(".chan.active");
|
||||
|
||||
if (lastActiveChan.length > 0) {
|
||||
lastActiveChan
|
||||
@ -221,7 +220,7 @@ $(function() {
|
||||
.appendTo(lastActiveChan.find(".messages"));
|
||||
|
||||
render.trimMessageInChannel(lastActiveChan, 100);
|
||||
}
|
||||
}*/
|
||||
|
||||
const chan = $(target)
|
||||
.addClass("active")
|
||||
@ -231,19 +230,9 @@ $(function() {
|
||||
utils.updateTitle();
|
||||
|
||||
const type = chan.data("type");
|
||||
let placeholder = "";
|
||||
|
||||
if (type === "channel" || type === "query") {
|
||||
placeholder = `Write to ${chan.attr("aria-label")}`;
|
||||
}
|
||||
|
||||
input
|
||||
.prop("placeholder", placeholder)
|
||||
.attr("aria-label", placeholder);
|
||||
|
||||
if (self.hasClass("chan")) {
|
||||
$("#chat-container").addClass("active");
|
||||
$("#nick").text(self.closest(".network").attr("data-nick"));
|
||||
}
|
||||
|
||||
const chanChat = chan.find(".chat");
|
||||
@ -257,9 +246,12 @@ $(function() {
|
||||
input.trigger("ontouchstart" in window ? "blur" : "focus");
|
||||
}
|
||||
|
||||
if (chan.data("needsNamesRefresh") === true) {
|
||||
chan.data("needsNamesRefresh", false);
|
||||
socket.emit("names", {target: self.data("id")});
|
||||
if (channel && channel.channel.usersOutdated) {
|
||||
channel.channel.usersOutdated = false;
|
||||
|
||||
socket.emit("names", {
|
||||
target: channel.channel.id,
|
||||
});
|
||||
}
|
||||
|
||||
// Pushes states to history web API when clicking elements with a data-target attribute.
|
||||
|
@ -24,26 +24,18 @@ const historyObserver = window.IntersectionObserver ?
|
||||
}) : null;
|
||||
|
||||
module.exports = {
|
||||
appendMessage,
|
||||
buildChannelMessages,
|
||||
renderChannel,
|
||||
renderChannelUsers,
|
||||
renderNetworks,
|
||||
trimMessageInChannel,
|
||||
};
|
||||
|
||||
function buildChannelMessages(container, chanId, chanType, messages) {
|
||||
return messages.reduce((docFragment, message) => {
|
||||
appendMessage(docFragment, chanId, chanType, message);
|
||||
return docFragment;
|
||||
}, container);
|
||||
}
|
||||
|
||||
function appendMessage(container, chanId, chanType, msg) {
|
||||
if (utils.lastMessageId < msg.id) {
|
||||
utils.lastMessageId = msg.id;
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
let lastChild = container.children(".msg, .date-marker-container").last();
|
||||
const renderedMessage = buildChatMessage(msg);
|
||||
|
||||
@ -136,80 +128,18 @@ function renderChannel(data) {
|
||||
renderChannelMessages(data);
|
||||
|
||||
if (data.type === "channel") {
|
||||
const users = renderChannelUsers(data);
|
||||
//const users = renderChannelUsers(data);
|
||||
|
||||
Userlist.handleKeybinds(users.find(".search"));
|
||||
//Userlist.handleKeybinds(users.find(".search"));
|
||||
}
|
||||
|
||||
if (historyObserver) {
|
||||
historyObserver.observe(chat.find("#chan-" + data.id + " .show-more").get(0));
|
||||
//historyObserver.observe(chat.find("#chan-" + data.id + " .show-more").get(0));
|
||||
}
|
||||
}
|
||||
|
||||
function renderChannelMessages(data) {
|
||||
const documentFragment = buildChannelMessages($(document.createDocumentFragment()), data.id, data.type, data.messages);
|
||||
const channel = chat.find("#chan-" + data.id + " .messages");
|
||||
|
||||
renderUnreadMarker($(templates.unread_marker()), data.firstUnread, channel);
|
||||
}
|
||||
|
||||
function renderUnreadMarker(template, firstUnread, channel) {
|
||||
if (firstUnread > 0) {
|
||||
let first = channel.find("#msg-" + firstUnread);
|
||||
|
||||
if (!first.length) {
|
||||
template.data("unread-id", firstUnread);
|
||||
channel.prepend(template);
|
||||
} else {
|
||||
const parent = first.parent();
|
||||
|
||||
if (parent.hasClass("condensed")) {
|
||||
first = parent;
|
||||
}
|
||||
|
||||
first.before(template);
|
||||
}
|
||||
} else {
|
||||
channel.append(template);
|
||||
}
|
||||
}
|
||||
|
||||
function renderChannelUsers(data) {
|
||||
const users = chat.find("#chan-" + data.id).find(".userlist");
|
||||
const nicks = data.users
|
||||
.concat() // Make a copy of the user list, sort is applied in-place
|
||||
.sort((a, b) => b.lastMessage - a.lastMessage)
|
||||
.map((a) => a.nick);
|
||||
|
||||
// Before re-rendering the list of names, there might have been an entry
|
||||
// marked as active (i.e. that was highlighted by keyboard navigation).
|
||||
// It is `undefined` if there was none.
|
||||
const previouslyActive = users.find(".active");
|
||||
|
||||
const search = users
|
||||
.find(".search")
|
||||
.prop("placeholder", nicks.length + " " + (nicks.length === 1 ? "user" : "users"));
|
||||
|
||||
users
|
||||
.data("nicks", nicks)
|
||||
.find(".names-original")
|
||||
.html(templates.user(data));
|
||||
|
||||
// Refresh user search
|
||||
if (search.val().length) {
|
||||
search.trigger("input");
|
||||
}
|
||||
|
||||
// If a nick was highlighted before re-rendering the lists, re-highlight it in
|
||||
// the newly-rendered list.
|
||||
if (previouslyActive.length > 0) {
|
||||
// We need to un-highlight everything first because triggering `input` with
|
||||
// a value highlights the first entry.
|
||||
users.find(".user").removeClass("active");
|
||||
users.find(`.user[data-name="${previouslyActive.attr("data-name")}"]`).addClass("active");
|
||||
}
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
function renderNetworks(data, singleNetwork) {
|
||||
@ -234,20 +164,12 @@ function renderNetworks(data, singleNetwork) {
|
||||
const chan = $("#chan-" + channel.id);
|
||||
|
||||
if (chan.length > 0) {
|
||||
if (chan.data("type") === "channel") {
|
||||
chan
|
||||
.data("needsNamesRefresh", true)
|
||||
.find(".header .topic")
|
||||
.html(helpers_parse(channel.topic))
|
||||
.prop("title", channel.topic);
|
||||
if (channel.type === "channel") {
|
||||
channel.usersOutdated = true;
|
||||
}
|
||||
|
||||
if (channel.messages.length > 0) {
|
||||
const container = chan.find(".messages");
|
||||
buildChannelMessages(container, channel.id, channel.type, channel.messages);
|
||||
|
||||
const unreadMarker = container.find(".unread-marker").data("unread-id", 0);
|
||||
renderUnreadMarker(unreadMarker, channel.firstUnread, container);
|
||||
|
||||
if (container.find(".msg").length >= 100) {
|
||||
container.find(".show-more").addClass("show");
|
||||
@ -268,7 +190,7 @@ function renderNetworks(data, singleNetwork) {
|
||||
renderChannel(channel);
|
||||
|
||||
if (channel.type === "channel") {
|
||||
chat.find("#chan-" + channel.id).data("needsNamesRefresh", true);
|
||||
channel.usersOutdated = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ const socket = require("../socket");
|
||||
const render = require("../render");
|
||||
const condensed = require("../condensed");
|
||||
const chat = $("#chat");
|
||||
const {Vue, vueApp, findChannel} = require("../vue");
|
||||
|
||||
socket.on("more", function(data) {
|
||||
let chan = chat.find("#chan-" + data.chan);
|
||||
@ -21,51 +22,30 @@ socket.on("more", function(data) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the date marker at the top if date does not change
|
||||
const children = $(chan).children();
|
||||
const channel = findChannel(data.chan);
|
||||
|
||||
// Check the top-most element and the one after because
|
||||
// unread and date markers may switch positions
|
||||
for (let i = 0; i <= 1; i++) {
|
||||
const marker = children.eq(i);
|
||||
|
||||
if (marker.hasClass("date-marker-container")) {
|
||||
const msgTime = new Date(data.messages[data.messages.length - 1].time);
|
||||
const prevMsgTime = new Date(marker.data("time"));
|
||||
|
||||
if (prevMsgTime.toDateString() === msgTime.toDateString()) {
|
||||
marker.remove();
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
channel.channel.messages.unshift(...data.messages);
|
||||
|
||||
Vue.nextTick(() => {
|
||||
// restore scroll position
|
||||
const position = chan.height() - heightOld;
|
||||
scrollable.finish().scrollTop(position);
|
||||
});
|
||||
|
||||
if (data.messages.length !== 100) {
|
||||
scrollable.find(".show-more").removeClass("show");
|
||||
}
|
||||
|
||||
// Add the older messages
|
||||
const documentFragment = render.buildChannelMessages($(document.createDocumentFragment()), data.chan, type, data.messages);
|
||||
chan.prepend(documentFragment);
|
||||
// Swap button text back from its alternative label
|
||||
const showMoreBtn = scrollable.find(".show-more button");
|
||||
swapText(showMoreBtn);
|
||||
showMoreBtn.prop("disabled", false);
|
||||
|
||||
// Move unread marker to correct spot if needed
|
||||
const unreadMarker = chan.find(".unread-marker");
|
||||
const firstUnread = unreadMarker.data("unread-id");
|
||||
|
||||
if (firstUnread > 0) {
|
||||
let first = chan.find("#msg-" + firstUnread);
|
||||
|
||||
if (!first.length) {
|
||||
chan.prepend(unreadMarker);
|
||||
} else {
|
||||
const parent = first.parent();
|
||||
|
||||
if (parent.hasClass("condensed")) {
|
||||
first = parent;
|
||||
}
|
||||
|
||||
unreadMarker.data("unread-id", 0);
|
||||
|
||||
first.before(unreadMarker);
|
||||
}
|
||||
}
|
||||
return;
|
||||
|
||||
// Join duplicate condensed messages together
|
||||
const condensedDuplicate = chan.find(".msg.condensed + .msg.condensed");
|
||||
@ -81,25 +61,6 @@ socket.on("more", function(data) {
|
||||
|
||||
condensedDuplicate.remove();
|
||||
}
|
||||
|
||||
// restore scroll position
|
||||
const position = chan.height() - heightOld;
|
||||
scrollable.finish().scrollTop(position);
|
||||
|
||||
// We have to do this hack due to smooth scrolling in browsers,
|
||||
// as scrollTop does not apply correctly
|
||||
if (window.requestAnimationFrame) {
|
||||
window.requestAnimationFrame(() => scrollable.scrollTop(position));
|
||||
}
|
||||
|
||||
if (data.messages.length !== 100) {
|
||||
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);
|
||||
});
|
||||
|
||||
chat.on("click", ".show-more button", function() {
|
||||
|
@ -60,13 +60,7 @@ function processReceivedMessage(data) {
|
||||
$(container).empty();
|
||||
}
|
||||
|
||||
// Add message to the container
|
||||
render.appendMessage(
|
||||
container,
|
||||
targetId,
|
||||
channelContainer.data("type"),
|
||||
data.msg
|
||||
);
|
||||
channel.channel.messages.push(data.msg);
|
||||
|
||||
if (activeChannelId === targetId) {
|
||||
scrollContainer.trigger("keepToBottom");
|
||||
|
@ -2,5 +2,12 @@
|
||||
|
||||
const socket = require("../socket");
|
||||
const render = require("../render");
|
||||
const {vueApp, findChannel} = require("../vue");
|
||||
|
||||
socket.on("names", render.renderChannelUsers);
|
||||
socket.on("names", function(data) {
|
||||
const channel = findChannel(data.id);
|
||||
|
||||
if (channel) {
|
||||
channel.channel.users = data.users;
|
||||
}
|
||||
});
|
||||
|
@ -1,14 +1,12 @@
|
||||
"use strict";
|
||||
|
||||
const $ = require("jquery");
|
||||
const socket = require("../socket");
|
||||
const {vueApp} = require("../vue");
|
||||
|
||||
socket.on("nick", function(data) {
|
||||
const id = data.network;
|
||||
const nick = data.nick;
|
||||
const network = $(`#sidebar .network[data-uuid="${id}"]`).attr("data-nick", nick);
|
||||
const network = vueApp.networks.find((n) => n.uuid === data.network);
|
||||
|
||||
if (network.find(".active").length) {
|
||||
$("#nick").text(nick);
|
||||
if (network) {
|
||||
network.nick = data.nick;
|
||||
}
|
||||
});
|
||||
|
@ -2,16 +2,18 @@
|
||||
|
||||
const $ = require("jquery");
|
||||
const socket = require("../socket");
|
||||
const chat = $("#chat");
|
||||
const {vueApp, findChannel} = require("../vue");
|
||||
|
||||
socket.on("users", function(data) {
|
||||
const chan = chat.find("#chan-" + data.chan);
|
||||
|
||||
if (chan.hasClass("active")) {
|
||||
socket.emit("names", {
|
||||
if (vueApp.activeChannel && vueApp.activeChannel.channel.id === data.chan) {
|
||||
return socket.emit("names", {
|
||||
target: data.chan,
|
||||
});
|
||||
} else {
|
||||
chan.data("needsNamesRefresh", true);
|
||||
}
|
||||
|
||||
const channel = findChannel(data.chan);
|
||||
|
||||
if (channel) {
|
||||
channel.channel.usersOutdated = true;
|
||||
}
|
||||
});
|
||||
|
@ -1,3 +0,0 @@
|
||||
<div class="unread-marker">
|
||||
<span class="unread-marker-text"></span>
|
||||
</div>
|
Loading…
Reference in New Issue
Block a user