3eca924a7c
This removes the leading hyphens from MOTD lines (under the condition they all do) and trims empty lines around the MOTD (but not inside).
357 lines
9.5 KiB
JavaScript
357 lines
9.5 KiB
JavaScript
"use strict";
|
|
|
|
const $ = require("jquery");
|
|
const templates = require("../views");
|
|
const options = require("./options");
|
|
const renderPreview = require("./renderPreview");
|
|
const utils = require("./utils");
|
|
const sorting = require("./sorting");
|
|
const constants = require("./constants");
|
|
const condensed = require("./condensed");
|
|
const JoinChannel = require("./join-channel");
|
|
const helpers_parse = require("./libs/handlebars/parse");
|
|
const Userlist = require("./userlist");
|
|
const storage = require("./localStorage");
|
|
|
|
const chat = $("#chat");
|
|
const sidebar = $("#sidebar");
|
|
|
|
require("intersection-observer");
|
|
|
|
const historyObserver = window.IntersectionObserver ?
|
|
new window.IntersectionObserver(loadMoreHistory, {
|
|
root: chat.get(0),
|
|
}) : 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;
|
|
}
|
|
|
|
let lastChild = container.children(".msg, .date-marker-container").last();
|
|
const renderedMessage = buildChatMessage(msg);
|
|
|
|
// Check if date changed
|
|
const msgTime = new Date(msg.time);
|
|
const prevMsgTime = new Date(lastChild.data("time"));
|
|
|
|
// Insert date marker if date changed compared to previous message
|
|
if (prevMsgTime.toDateString() !== msgTime.toDateString()) {
|
|
lastChild = $(templates.date_marker({time: msg.time}));
|
|
container.append(lastChild);
|
|
}
|
|
|
|
// If current window is not a channel or this message is not condensable,
|
|
// then just append the message to container and be done with it
|
|
if (msg.self || msg.highlight || constants.condensedTypes.indexOf(msg.type) === -1 || chanType !== "channel") {
|
|
container.append(renderedMessage);
|
|
return;
|
|
}
|
|
|
|
const obj = {};
|
|
obj[msg.type] = 1;
|
|
|
|
// If the previous message is already condensed,
|
|
// we just append to it and update text
|
|
if (lastChild.hasClass("condensed")) {
|
|
lastChild.append(renderedMessage);
|
|
condensed.updateText(lastChild, obj);
|
|
return;
|
|
}
|
|
|
|
// Always create a condensed container
|
|
const newCondensed = $(templates.msg_condensed({time: msg.time}));
|
|
|
|
condensed.updateText(newCondensed, obj);
|
|
newCondensed.append(renderedMessage);
|
|
container.append(newCondensed);
|
|
}
|
|
|
|
function buildChatMessage(msg) {
|
|
const type = msg.type;
|
|
let template = "msg";
|
|
|
|
// See if any of the custom highlight regexes match
|
|
if (!msg.highlight && !msg.self
|
|
&& options.highlightsRE
|
|
&& (type === "message" || type === "notice")
|
|
&& options.highlightsRE.exec(msg.text)) {
|
|
msg.highlight = true;
|
|
}
|
|
|
|
if (typeof templates.actions[type] !== "undefined") {
|
|
template = "msg_action";
|
|
} else if (type === "unhandled") {
|
|
template = "msg_unhandled";
|
|
}
|
|
|
|
// Make the MOTDs a little nicer if possible
|
|
if (msg.type === "motd") {
|
|
let lines = msg.text.split("\n");
|
|
|
|
// If all non-empty lines of the MOTD start with a hyphen (which is common
|
|
// across MOTDs), remove all the leading hyphens.
|
|
if (lines.every((line) => line === "" || line[0] === "-")) {
|
|
lines = lines.map((line) => line.substr(2));
|
|
}
|
|
|
|
// Remove empty lines around the MOTD (but not within it)
|
|
msg.text = lines.join("\n").trim();
|
|
}
|
|
|
|
const renderedMessage = $(templates[template](msg));
|
|
const content = renderedMessage.find(".content");
|
|
|
|
if (template === "msg_action") {
|
|
content.html(templates.actions[type](msg));
|
|
}
|
|
|
|
msg.previews.forEach((preview) => {
|
|
renderPreview(preview, 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 documentFragment = buildChannelMessages($(document.createDocumentFragment()), data.id, data.type, data.messages);
|
|
const channel = chat.find("#chan-" + data.id + " .messages").append(documentFragment);
|
|
|
|
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) {
|
|
const collapsed = new Set(JSON.parse(storage.get("thelounge.networks.collapsed")));
|
|
|
|
sidebar.find(".empty").hide();
|
|
sidebar.find(".networks").append(
|
|
templates.network({
|
|
networks: data.networks,
|
|
}).trim()
|
|
);
|
|
|
|
// Add keyboard handlers to the "Join a channel…" form inputs/button
|
|
JoinChannel.handleKeybinds(data.networks);
|
|
|
|
let newChannels;
|
|
const channels = $.map(data.networks, function(n) {
|
|
if (collapsed.has(n.uuid)) {
|
|
collapseNetwork($(`.network[data-uuid="${n.uuid}"] button.collapse-network`));
|
|
}
|
|
|
|
return n.channels;
|
|
});
|
|
|
|
if (!singleNetwork && utils.lastMessageId > -1) {
|
|
newChannels = [];
|
|
|
|
channels.forEach((channel) => {
|
|
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.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");
|
|
}
|
|
|
|
container.parent().trigger("keepToBottom");
|
|
}
|
|
} else {
|
|
newChannels.push(channel);
|
|
}
|
|
});
|
|
} else {
|
|
newChannels = channels;
|
|
}
|
|
|
|
if (newChannels.length > 0) {
|
|
chat.append(
|
|
templates.chat({
|
|
channels: newChannels,
|
|
})
|
|
);
|
|
|
|
newChannels.forEach((channel) => {
|
|
renderChannel(channel);
|
|
|
|
if (channel.type === "channel") {
|
|
chat.find("#chan-" + channel.id).data("needsNamesRefresh", true);
|
|
}
|
|
});
|
|
}
|
|
|
|
utils.confirmExit();
|
|
sorting();
|
|
|
|
if (sidebar.find(".highlight").length) {
|
|
utils.toggleNotificationMarkers(true);
|
|
}
|
|
}
|
|
|
|
function trimMessageInChannel(channel, messageLimit) {
|
|
const messages = channel.find(".messages .msg").slice(0, -messageLimit);
|
|
|
|
if (messages.length === 0) {
|
|
return;
|
|
}
|
|
|
|
messages.remove();
|
|
|
|
channel.find(".show-more").addClass("show");
|
|
|
|
// Remove date-separators that would otherwise be "stuck" at the top of the channel
|
|
channel.find(".date-marker-container").each(function() {
|
|
if ($(this).next().hasClass("date-marker-container")) {
|
|
$(this).remove();
|
|
}
|
|
});
|
|
}
|
|
|
|
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)));
|
|
|
|
function collapseNetwork(target) {
|
|
const collapseButton = target.closest(".collapse-network");
|
|
const networks = new Set(JSON.parse(storage.get("thelounge.networks.collapsed")));
|
|
const networkuuid = collapseButton.closest(".network").data("uuid");
|
|
|
|
if (collapseButton.closest(".network").find(".active").length > 0) {
|
|
collapseButton.closest(".lobby").trigger("click", {
|
|
keepSidebarOpen: true,
|
|
});
|
|
}
|
|
|
|
collapseButton.closest(".network").toggleClass("collapsed");
|
|
|
|
if (collapseButton.attr("aria-expanded") === "true") {
|
|
collapseButton.attr("aria-expanded", false);
|
|
collapseButton.attr("aria-label", "Expand");
|
|
networks.add(networkuuid);
|
|
} else {
|
|
collapseButton.attr("aria-expanded", true);
|
|
collapseButton.attr("aria-label", "Collapse");
|
|
networks.delete(networkuuid);
|
|
}
|
|
|
|
storage.set("thelounge.networks.collapsed", JSON.stringify([...networks]));
|
|
return false;
|
|
}
|