diff --git a/client/css/style.css b/client/css/style.css
index e517fe36..13b70968 100644
--- a/client/css/style.css
+++ b/client/css/style.css
@@ -194,6 +194,7 @@ kbd {
#chat .title:before,
#footer .icon,
#chat .count:before,
+#settings .extra-help,
#settings #play:before,
#form #submit:before,
#chat .invite .from:before,
@@ -317,6 +318,10 @@ kbd {
line-height: 50px;
}
+#settings .extra-help:before {
+ content: "\f059"; /* http://fontawesome.io/icon/question-circle/ */
+}
+
#settings #play:before {
content: "\f028"; /* http://fontawesome.io/icon/volume-up/ */
margin-right: 9px;
@@ -801,6 +806,31 @@ kbd {
display: block;
}
+#chat .condensed {
+ flex-wrap: wrap;
+}
+
+#chat .condensed .content {
+ flex: 1;
+}
+
+#chat .condensed-text {
+ cursor: pointer;
+ transition: opacity .2s;
+}
+
+#chat .condensed-text:hover {
+ opacity: .6;
+}
+
+#chat .condensed-text .toggle-button:hover {
+ opacity: 1;
+}
+
+#chat .condensed.closed .msg {
+ display: none;
+}
+
#windows .header .topic,
.messages .msg,
.sidebar {
@@ -1092,15 +1122,17 @@ kbd {
color: #555;
}
-#chat.hide-join .join,
-#chat.hide-mode .mode,
-#chat.hide-motd .motd,
-#chat.hide-nick .nick,
-#chat.hide-part .part,
-#chat.hide-quit .quit {
+#chat.hide-status-messages .join,
+#chat.hide-status-messages .mode,
+#chat.hide-status-messages .nick,
+#chat.hide-status-messages .part,
+#chat.hide-status-messages .quit,
+#chat.hide-status-messages .condensed,
+#chat.hide-motd .motd {
display: none !important;
}
+#chat .condensed .content,
#chat .join .content,
#chat .kick .content,
#chat .mode .content,
@@ -1138,19 +1170,14 @@ kbd {
#chat .toggle-button {
display: inline-block;
- color: #666;
- transition: color .2s, transform .2s;
+ transition: opacity .2s, transform .2s;
}
-#chat .toggle-button.opened {
+#chat .toggle-button.opened, /* Thumbnail toggle */
+#chat .msg.condensed:not(.closed) .toggle-button { /* Expanded status message toggle */
transform: rotate(90deg);
}
-#chat .toggle-button:hover {
- /* transform and opacity together glitch, so need to use RGBA transition */
- color: rgba(102, 102, 102, .8); /* #666 x .8 opacity */
-}
-
#chat .toggle-content {
background: #f5f5f5;
border-radius: 2px;
@@ -1361,18 +1388,26 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
#settings .opt {
display: block;
- padding: 5px 0 10px 1px;
+ padding: 5px 0 5px 1px;
}
#settings .opt input {
- float: left;
- margin: 4px 10px 0 0;
+ margin-right: 6px;
}
+#settings .extra-help,
#settings #play {
color: #7f8c8d;
}
+#settings .extra-help {
+ cursor: help;
+}
+
+#settings h2 .extra-help {
+ font-size: .8em;
+}
+
#settings #play {
font-size: 14px;
transition: opacity .2s;
@@ -1694,6 +1729,16 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
animation-delay: .4s;
}
+.tooltipped-no-delay:hover:before,
+.tooltipped-no-delay:hover:after,
+.tooltipped-no-delay:active:before,
+.tooltipped-no-delay:active:after,
+.tooltipped-no-delay:focus:before,
+.tooltipped-no-delay:focus:after {
+ -webkit-animation-delay: 0s;
+ animation-delay: 0s;
+}
+
.tooltipped-s:after,
.tooltipped-se:after,
.tooltipped-sw:after {
@@ -2130,3 +2175,7 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
#chat table.channel-list .topic {
white-space: pre-wrap;
}
+
+.hide-text {
+ color: transparent !important;
+}
diff --git a/client/index.html b/client/index.html
index d8d39618..09baadb5 100644
--- a/client/index.html
+++ b/client/index.html
@@ -216,48 +216,40 @@
Messages
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ Status messages
+
+
+
+
+
+
+
+
+
+
Visual Aids
diff --git a/client/js/condensed.js b/client/js/condensed.js
new file mode 100644
index 00000000..a7119d1f
--- /dev/null
+++ b/client/js/condensed.js
@@ -0,0 +1,55 @@
+"use strict";
+
+const constants = require("./constants");
+const templates = require("../views");
+
+module.exports = {
+ updateText
+};
+
+function updateText(condensed, addedTypes) {
+ const obj = {};
+
+ constants.condensedTypes.forEach((type) => {
+ obj[type] = condensed.data(type) || 0;
+ });
+
+ addedTypes.forEach((type) => {
+ obj[type]++;
+ condensed.data(type, obj[type]);
+ });
+
+ const strings = [];
+ constants.condensedTypes.forEach((type) => {
+ if (obj[type]) {
+ switch (type) {
+ case "join":
+ strings.push(obj[type] + (obj[type] > 1 ? " users have joined the channel" : " user has joined the channel"));
+ break;
+ case "part":
+ strings.push(obj[type] + (obj[type] > 1 ? " users have left the channel" : " user has left the channel"));
+ break;
+ case "quit":
+ strings.push(obj[type] + (obj[type] > 1 ? " users have quit" : " user has quit"));
+ break;
+ case "nick":
+ strings.push(obj[type] + (obj[type] > 1 ? " users have changed nick" : " user has changed nick"));
+ break;
+ case "kick":
+ strings.push(obj[type] + (obj[type] > 1 ? " users were kicked" : " user was kicked"));
+ break;
+ case "mode":
+ strings.push(obj[type] + (obj[type] > 1 ? " modes were set" : " mode was set"));
+ break;
+ }
+ }
+ });
+
+ let text = strings.pop();
+ if (strings.length) {
+ text = strings.join(", ") + ", and " + text;
+ }
+
+ condensed.find(".condensed-text")
+ .html(text + templates.msg_condensed_toggle());
+}
diff --git a/client/js/constants.js b/client/js/constants.js
index f95fbd00..878f0276 100644
--- a/client/js/constants.js
+++ b/client/js/constants.js
@@ -56,6 +56,32 @@ const commands = [
"/whois"
];
+const actionTypes = [
+ "ban_list",
+ "invite",
+ "join",
+ "mode",
+ "kick",
+ "nick",
+ "part",
+ "quit",
+ "topic",
+ "topic_set_by",
+ "action",
+ "whois",
+ "ctcp",
+ "channel_list",
+];
+
+const condensedTypes = [
+ "join",
+ "part",
+ "quit",
+ "nick",
+ "kick",
+ "mode",
+];
+
const timeFormats = {
msgDefault: "HH:mm",
msgWithSeconds: "HH:mm:ss"
@@ -63,6 +89,8 @@ const timeFormats = {
module.exports = {
colorCodeMap: colorCodeMap,
- timeFormats: timeFormats,
- commands: commands
+ commands: commands,
+ condensedTypes: condensedTypes,
+ actionTypes: actionTypes,
+ timeFormats: timeFormats
};
diff --git a/client/js/lounge.js b/client/js/lounge.js
index 3322d10e..23e8dd5e 100644
--- a/client/js/lounge.js
+++ b/client/js/lounge.js
@@ -439,6 +439,10 @@ $(function() {
}
});
+ chat.on("click", ".condensed-text", function() {
+ $(this).closest(".msg.condensed").toggleClass("closed");
+ });
+
chat.on("click", ".user", function() {
var name = $(this).data("name");
var chan = findCurrentNetworkChan(name);
@@ -707,6 +711,9 @@ $(function() {
chat.on("click", ".show-more-button", function() {
var self = $(this);
var lastMessage = self.parent().next(".messages").children(".msg").first();
+ if (lastMessage.is(".condensed")) {
+ lastMessage = lastMessage.children(".msg").first();
+ }
var lastMessageId = parseInt(lastMessage[0].id.replace("msg-", ""), 10);
self
diff --git a/client/js/options.js b/client/js/options.js
index cd38b37f..4e0d83a6 100644
--- a/client/js/options.js
+++ b/client/js/options.js
@@ -9,25 +9,29 @@ const tz = require("./libs/handlebars/tz");
const windows = $("#windows");
const chat = $("#chat");
-const options = $.extend({
+// Default options
+const options = {
+ autocomplete: true,
coloredNicks: true,
desktopNotifications: false,
- join: true,
+ highlights: [],
links: true,
- mode: true,
motd: true,
- nick: true,
notification: true,
notifyAllMessages: false,
- part: true,
- quit: true,
showSeconds: false,
+ statusMessages: "condensed",
theme: $("#theme").attr("href").replace(/^themes\/(.*).css$/, "$1"), // Extracts default theme name, set on the server configuration
thumbnails: true,
userStyles: userStyles.text(),
- highlights: [],
- autocomplete: true
-}, JSON.parse(storage.get("settings")));
+};
+const userOptions = JSON.parse(storage.get("settings")) || {};
+
+for (const key in options) {
+ if (userOptions[key]) {
+ options[key] = userOptions[key];
+ }
+}
module.exports = options;
@@ -43,6 +47,9 @@ for (var i in options) {
settings.find("#user-specified-css-input").val(options[i]);
} else if (i === "highlights") {
settings.find("input[name=" + i + "]").val(options[i]);
+ } else if (i === "statusMessages") {
+ settings.find(`input[name=${i}][value=${options[i]}]`)
+ .prop("checked", true);
} else if (i === "theme") {
$("#theme").attr("href", "themes/" + options[i] + ".css");
settings.find("select[name=" + i + "]").val(options[i]);
@@ -58,6 +65,10 @@ settings.on("change", "input, select, textarea", function() {
if (type === "password") {
return;
+ } else if (type === "radio") {
+ if (self.prop("checked")) {
+ options[name] = self.val();
+ }
} else if (type === "checkbox") {
options[name] = self.prop("checked");
} else {
@@ -66,16 +77,10 @@ settings.on("change", "input, select, textarea", function() {
storage.set("settings", JSON.stringify(options));
- if ([
- "join",
- "mode",
- "motd",
- "nick",
- "part",
- "quit",
- "notifyAllMessages",
- ].indexOf(name) !== -1) {
+ if (name === "motd") {
chat.toggleClass("hide-" + name, !self.prop("checked"));
+ } else if (name === "statusMessages") {
+ chat.toggleClass("hide-status-messages", options[name] === "hidden");
} else if (name === "coloredNicks") {
chat.toggleClass("colored-nicks", self.prop("checked"));
} else if (name === "theme") {
diff --git a/client/js/render.js b/client/js/render.js
index e3cca373..246e62b2 100644
--- a/client/js/render.js
+++ b/client/js/render.js
@@ -6,11 +6,14 @@ const options = require("./options");
const renderPreview = require("./renderPreview");
const utils = require("./utils");
const sorting = require("./sorting");
+const constants = require("./constants");
+const condense = require("./condensed");
const chat = $("#chat");
const sidebar = $("#sidebar");
module.exports = {
+ appendMessage,
buildChannelMessages,
buildChatMessage,
renderChannel,
@@ -19,16 +22,39 @@ module.exports = {
renderNetworks,
};
-function buildChannelMessages(channel, messages) {
- return messages.reduce(function(docFragment, message) {
- docFragment.append(buildChatMessage({
- chan: channel,
+function buildChannelMessages(data) {
+ return data.messages.reduce(function(docFragment, message) {
+ appendMessage(docFragment, data.id, data.type, message.type, buildChatMessage({
+ chan: data.id,
msg: message
}));
return docFragment;
}, $(document.createDocumentFragment()));
}
+function appendMessage(container, chan, chanType, messageType, msg) {
+ if (constants.condensedTypes.indexOf(messageType) !== -1 && chanType !== "lobby") {
+ var condensedTypesClasses = "." + constants.condensedTypes.join(", .");
+ var lastChild = container.children("div.msg").last();
+ var lastDate = (new Date(lastChild.attr("data-time"))).toDateString();
+ var msgDate = (new Date(msg.attr("data-time"))).toDateString();
+ if (lastChild && $(lastChild).hasClass("condensed") && !$(msg).hasClass("message") && lastDate === msgDate) {
+ lastChild.append(msg);
+ condense.updateText(lastChild, [messageType]);
+ } else if (lastChild && $(lastChild).is(condensedTypesClasses) && options.statusMessages === "condensed") {
+ var condensed = buildChatMessage({msg: {type: "condensed", time: msg.attr("data-time"), previews: []}, chan: chan});
+ condensed.append(lastChild);
+ condensed.append(msg);
+ container.append(condensed);
+ condense.updateText(condensed, [messageType, lastChild.attr("data-type")]);
+ } else {
+ container.append(msg);
+ }
+ } else {
+ container.append(msg);
+ }
+}
+
function buildChatMessage(data) {
const type = data.msg.type;
let target = "#chan-" + data.chan;
@@ -45,25 +71,13 @@ function buildChatMessage(data) {
data.msg.highlight = true;
}
- if ([
- "invite",
- "join",
- "mode",
- "kick",
- "nick",
- "part",
- "quit",
- "topic",
- "topic_set_by",
- "action",
- "whois",
- "ctcp",
- "channel_list",
- "ban_list",
- ].indexOf(type) !== -1) {
+ if (constants.actionTypes.indexOf(type) !== -1) {
+ data.msg.template = "actions/" + type;
template = "msg_action";
} else if (type === "unhandled") {
template = "msg_unhandled";
+ } else if (type === "condensed") {
+ template = "msg_condensed";
}
const msg = $(templates[template](data.msg));
@@ -100,7 +114,7 @@ function renderChannel(data) {
}
function renderChannelMessages(data) {
- const documentFragment = buildChannelMessages(data.id, data.messages);
+ const documentFragment = buildChannelMessages(data);
const channel = chat.find("#chan-" + data.id + " .messages").append(documentFragment);
if (data.firstUnread > 0) {
@@ -109,6 +123,8 @@ function renderChannelMessages(data) {
// TODO: If the message is far off in the history, we still need to append the marker into DOM
if (!first.length) {
channel.prepend(templates.unread_marker());
+ } else if (first.parent().hasClass("condensed")) {
+ first.parent().before(templates.unread_marker());
} else {
first.before(templates.unread_marker());
}
@@ -129,6 +145,10 @@ function renderChannelMessages(data) {
}
if (lastDate.toDateString() !== msgDate.toDateString()) {
+ var parent = msg.parent();
+ if (parent.hasClass("condensed")) {
+ msg.insertAfter(parent);
+ }
msg.before(templates.date_marker({msgDate: msgDate}));
}
diff --git a/client/js/renderPreview.js b/client/js/renderPreview.js
index 3f09ff44..14edb400 100644
--- a/client/js/renderPreview.js
+++ b/client/js/renderPreview.js
@@ -47,7 +47,7 @@ function renderPreview(preview, msg) {
container.trigger("keepToBottom");
}
-$("#chat").on("click", ".toggle-button", function() {
+$("#chat").on("click", ".text .toggle-button", function() {
const self = $(this);
const container = self.closest(".chat");
const content = self.closest(".content")
diff --git a/client/js/socket-events/more.js b/client/js/socket-events/more.js
index b8029934..8640a883 100644
--- a/client/js/socket-events/more.js
+++ b/client/js/socket-events/more.js
@@ -7,7 +7,7 @@ const chat = $("#chat");
const templates = require("../../views");
socket.on("more", function(data) {
- const documentFragment = render.buildChannelMessages(data.chan, data.messages);
+ const documentFragment = render.buildChannelMessages(data);
const chan = chat
.find("#chan-" + data.chan)
.find(".messages");
@@ -24,6 +24,8 @@ socket.on("more", function(data) {
} else if (children.eq(1).hasClass("date-marker-container")) {
// The unread-marker could be at index 0, which will cause the date-marker to become "stuck"
children.eq(1).remove();
+ } else if (children.eq(0).hasClass("condensed") && children.eq(0).children(".date-marker-container").eq(0).hasClass("date-marker-container")) {
+ children.eq(0).children(".date-marker-container").eq(0).remove();
}
// Add the older messages
@@ -52,6 +54,10 @@ socket.on("more", function(data) {
}
if (lastDate.toDateString() !== msgDate.toDateString()) {
+ var parent = msg.parent();
+ if (parent.hasClass("condensed")) {
+ msg.insertAfter(parent);
+ }
msg.before(templates.date_marker({msgDate: msgDate}));
}
diff --git a/client/js/socket-events/msg.js b/client/js/socket-events/msg.js
index 15764848..44bf8081 100644
--- a/client/js/socket-events/msg.js
+++ b/client/js/socket-events/msg.js
@@ -8,8 +8,9 @@ const templates = require("../../views");
socket.on("msg", function(data) {
const msg = render.buildChatMessage(data);
- const target = data.chan;
- const channel = chat.find("#chan-" + target);
+ const targetId = data.chan;
+ const target = "#chan-" + targetId;
+ const channel = chat.find(target);
const container = channel.find(".messages");
const activeChannelId = chat.find(".chan.active").data("id");
@@ -19,7 +20,7 @@ socket.on("msg", function(data) {
}
// Check if date changed
- const prevMsg = $(container.find(".msg")).last();
+ let prevMsg = $(container.find(".msg")).last();
const prevMsgTime = new Date(prevMsg.attr("data-time"));
const msgTime = new Date(msg.attr("data-time"));
@@ -29,17 +30,26 @@ socket.on("msg", function(data) {
}
if (prevMsgTime.toDateString() !== msgTime.toDateString()) {
+ var parent = prevMsg.parent();
+ if (parent.hasClass("condensed")) {
+ prevMsg = parent;
+ }
prevMsg.after(templates.date_marker({msgDate: msgTime}));
}
// Add message to the container
- container
- .append(msg)
- .trigger("msg", [
- "#chan-" + target,
- data
- ])
- .trigger("keepToBottom");
+ render.appendMessage(
+ container,
+ data.chan,
+ $(target).attr("data-type"),
+ data.msg.type,
+ msg
+ );
+
+ container.trigger("msg", [
+ target,
+ data
+ ]).trigger("keepToBottom");
var lastVisible = container.find("div:visible").last();
if (data.msg.self
@@ -52,7 +62,7 @@ socket.on("msg", function(data) {
}
// Message arrived in a non active channel, trim it to 100 messages
- if (activeChannelId !== target && container.find(".msg").slice(0, -100).remove().length) {
+ if (activeChannelId !== targetId && container.find(".msg").slice(0, -100).remove().length) {
channel.find(".show-more").addClass("show");
// Remove date-seperators that would otherwise
diff --git a/client/themes/morning.css b/client/themes/morning.css
index 17e68bcd..3a5e7677 100644
--- a/client/themes/morning.css
+++ b/client/themes/morning.css
@@ -101,10 +101,6 @@ body {
color: #428bca;
}
-#chat button:hover {
- opacity: 1;
-}
-
/* Increase contrast of some IRC colors */
.irc-fg2 { color: #0074d9; }
.irc-fg5 { color: #e969a7; }
@@ -208,17 +204,9 @@ body {
}
/* Embeds */
-#chat .toggle-content,
-#chat .toggle-button {
- color: #f3f3f3;
-}
-
-#chat .toggle-button:hover {
- color: rgba(243, 243, 243, .8);
-}
-
#chat .toggle-content {
background: #242a33;
+ color: #f3f3f3;
}
#chat .toggle-content .body {
diff --git a/client/themes/zenburn.css b/client/themes/zenburn.css
index 1465a3b3..5e73ed96 100644
--- a/client/themes/zenburn.css
+++ b/client/themes/zenburn.css
@@ -127,10 +127,6 @@ body {
color: #8c8cbc;
}
-#chat button:hover {
- opacity: 1;
-}
-
/* Increase contrast of some IRC colors */
.irc-fg2 { color: #1b94ff; }
.irc-fg5 { color: #e969a7; }
@@ -235,17 +231,9 @@ body {
/* Previews */
-#chat .toggle-content,
-#chat .toggle-button {
- color: #dcdccc;
-}
-
-#chat .toggle-button:hover {
- color: rgba(220, 220, 204, .8);
-}
-
#chat .toggle-content {
background: #333;
+ color: #dcdccc;
}
#chat .toggle-content .body {
diff --git a/client/views/index.js b/client/views/index.js
index 5c9da5dd..aae3c78b 100644
--- a/client/views/index.js
+++ b/client/views/index.js
@@ -25,6 +25,8 @@ module.exports = {
date_marker: require("./date-marker.tpl"),
msg: require("./msg.tpl"),
msg_action: require("./msg_action.tpl"),
+ msg_condensed_toggle: require("./msg_condensed_toggle.tpl"),
+ msg_condensed: require("./msg_condensed.tpl"),
msg_preview: require("./msg_preview.tpl"),
msg_preview_toggle: require("./msg_preview_toggle.tpl"),
msg_unhandled: require("./msg_unhandled.tpl"),
diff --git a/client/views/msg_action.tpl b/client/views/msg_action.tpl
index c3d5c772..42eb385f 100644
--- a/client/views/msg_action.tpl
+++ b/client/views/msg_action.tpl
@@ -1,4 +1,5 @@
-
+
{{tz time}}
diff --git a/client/views/msg_condensed.tpl b/client/views/msg_condensed.tpl
new file mode 100644
index 00000000..914ef27c
--- /dev/null
+++ b/client/views/msg_condensed.tpl
@@ -0,0 +1,7 @@
+
+ {{tz time}}
+
+
+
+
+
diff --git a/client/views/msg_condensed_toggle.tpl b/client/views/msg_condensed_toggle.tpl
new file mode 100644
index 00000000..2c740773
--- /dev/null
+++ b/client/views/msg_condensed_toggle.tpl
@@ -0,0 +1 @@
+