diff --git a/public/css/main.css b/public/css/main.css index d1e6dab..8017350 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -131,6 +131,21 @@ body { cursor: pointer; font-weight: 600; } +.ircclient #chat .ircwrapper .toolbar .tabby .tab #unread { + display: inline-block; + min-width: 22px; + height: 22px; + background-color: #bf0000; + border: 1px solid #b00; + box-shadow: inset 2px 2px 3px #f00; + text-align: center; + font-size: 10px; + margin: 0 5px; + border-radius: 100%; +} +.ircclient #chat .ircwrapper .toolbar .tabby .tab #unread.none { + display: none; +} .ircclient #chat .ircwrapper .toolbar .tabby .tab #close { display: inline-block; width: 12px; @@ -174,6 +189,9 @@ body { bottom: 0; left: 0; right: 0; + overflow-y: auto; + white-space: pre-wrap; + word-wrap: break-word; } .ircclient #chat .ircwrapper .chatarea .nicklist { width: 280px; @@ -188,6 +206,9 @@ body { .ircclient #chat .ircwrapper .chatarea.vnicks .nicklist { display: block; } +.ircclient #chat .ircwrapper .chatarea.vnicks .nicklist.forceopen { + display: block !important; +} .ircclient #chat .ircwrapper .chatarea.vnicks .letterbox { right: 301px; } @@ -234,3 +255,25 @@ body { border-left: 0; box-shadow: inset 4px 4px 8px #d8d8d8; } +.message.type_simple .timestamp { + margin-right: 5px; + color: #696969; +} +.message.type_simple .timestamp:before { + color: #607d8b; + content: "["; +} +.message.type_simple .timestamp:after { + color: #607d8b; + content: "]"; +} +.message.type_simple .sender { + margin-right: 5px; + color: #3f51b5; +} +.message.type_simple .sender:before { + content: "<"; +} +.message.type_simple .sender:after { + content: ">"; +} diff --git a/public/js/main.js b/public/js/main.js index 1f608a2..2afaf23 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -1,9 +1,22 @@ window.irc = { socketUp: false, primaryFrame: null, - timestamps: true + timestamps: true, + timestampFormat: "HH:mm:ss", + supportedPrefixes: "@%+", + modeTranslation: { + "o": "@", + "h": "%", + "v": "+" + } }; +/*********************\ +|** **| +|** UTILITIES **| +|** **| +\*********************/ + window.validators = {}; window.validators.iporhost = function(str) { @@ -35,6 +48,85 @@ function remove_str(arr, str) { return arr; }; +Date.prototype.format = function (format, utc){ + var date = this; + var MMMM = ["\x00", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; + var MMM = ["\x01", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + var dddd = ["\x02", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; + var ddd = ["\x03", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + function ii(i, len) { var s = i + ""; len = len || 2; while (s.length < len) s = "0" + s; return s; } + + var y = utc ? date.getUTCFullYear() : date.getFullYear(); + format = format.replace(/(^|[^\\])yyyy+/g, "$1" + y); + format = format.replace(/(^|[^\\])yy/g, "$1" + y.toString().substr(2, 2)); + format = format.replace(/(^|[^\\])y/g, "$1" + y); + + var M = (utc ? date.getUTCMonth() : date.getMonth()) + 1; + format = format.replace(/(^|[^\\])MMMM+/g, "$1" + MMMM[0]); + format = format.replace(/(^|[^\\])MMM/g, "$1" + MMM[0]); + format = format.replace(/(^|[^\\])MM/g, "$1" + ii(M)); + format = format.replace(/(^|[^\\])M/g, "$1" + M); + + var d = utc ? date.getUTCDate() : date.getDate(); + format = format.replace(/(^|[^\\])dddd+/g, "$1" + dddd[0]); + format = format.replace(/(^|[^\\])ddd/g, "$1" + ddd[0]); + format = format.replace(/(^|[^\\])dd/g, "$1" + ii(d)); + format = format.replace(/(^|[^\\])d/g, "$1" + d); + + var H = utc ? date.getUTCHours() : date.getHours(); + format = format.replace(/(^|[^\\])HH+/g, "$1" + ii(H)); + format = format.replace(/(^|[^\\])H/g, "$1" + H); + + var h = H > 12 ? H - 12 : H == 0 ? 12 : H; + format = format.replace(/(^|[^\\])hh+/g, "$1" + ii(h)); + format = format.replace(/(^|[^\\])h/g, "$1" + h); + + var m = utc ? date.getUTCMinutes() : date.getMinutes(); + format = format.replace(/(^|[^\\])mm+/g, "$1" + ii(m)); + format = format.replace(/(^|[^\\])m/g, "$1" + m); + + var s = utc ? date.getUTCSeconds() : date.getSeconds(); + format = format.replace(/(^|[^\\])ss+/g, "$1" + ii(s)); + format = format.replace(/(^|[^\\])s/g, "$1" + s); + + var f = utc ? date.getUTCMilliseconds() : date.getMilliseconds(); + format = format.replace(/(^|[^\\])fff+/g, "$1" + ii(f, 3)); + f = Math.round(f / 10); + format = format.replace(/(^|[^\\])ff/g, "$1" + ii(f)); + f = Math.round(f / 10); + format = format.replace(/(^|[^\\])f/g, "$1" + f); + + var T = H < 12 ? "AM" : "PM"; + format = format.replace(/(^|[^\\])TT+/g, "$1" + T); + format = format.replace(/(^|[^\\])T/g, "$1" + T.charAt(0)); + + var t = T.toLowerCase(); + format = format.replace(/(^|[^\\])tt+/g, "$1" + t); + format = format.replace(/(^|[^\\])t/g, "$1" + t.charAt(0)); + + var tz = -date.getTimezoneOffset(); + var K = utc || !tz ? "Z" : tz > 0 ? "+" : "-"; + if (!utc) + { + tz = Math.abs(tz); + var tzHrs = Math.floor(tz / 60); + var tzMin = tz % 60; + K += ii(tzHrs) + ":" + ii(tzMin); + } + format = format.replace(/(^|[^\\])K/g, "$1" + K); + + var day = (utc ? date.getUTCDay() : date.getDay()) + 1; + format = format.replace(new RegExp(dddd[0], "g"), dddd[day]); + format = format.replace(new RegExp(ddd[0], "g"), ddd[day]); + + format = format.replace(new RegExp(MMMM[0], "g"), MMMM[M]); + format = format.replace(new RegExp(MMM[0], "g"), MMM[M]); + + format = format.replace(/\\(.)/g, "$1"); + + return format; +}; + function removeClass(element, cl) { let classList = element.className.split(" "); @@ -48,14 +140,127 @@ function addClass(element, cl) { element.className = classList.join(" "); } +/*********************\ +|** **| +|** CLASSES **| +|** **| +\*********************/ + class Nicklist { - constructor(buffer) { + constructor(buffer, frame) { this.buffer = buffer; + this.frame = frame; this.nicks = []; } - render(frame) { + sort() { + this.nicks.sort(function (a,b) { + let rex = new RegExp('^['+irc.supportedPrefixes+']'); + let nicks = [a.prefix.replace(rex,'').toLowerCase(), b.prefix.replace(rex,'').toLowerCase()]; + let prefix = []; + if (rex.test(a.prefix)) prefix.push(irc.supportedPrefixes.indexOf(a.prefix[0])); + else prefix.push(irc.supportedPrefixes.length+1); + if (rex.test(b.prefix)) prefix.push(irc.supportedPrefixes.indexOf(b.prefix[0])); + else prefix.push(irc.supportedPrefixes.length+1); + if (prefix[0] < prefix[1]) return -1; + if (prefix[0] > prefix[1]) return 1; + if (nicks[0] > nicks[1]) return 1; + if (nicks[0] < nicks[1]) return -1; + return 0; + }); + return this.nicks; + } + appendToList(nick) { + if(!this.buffer.active) return; + + let str = document.createElement("div"); + str.className = "nick"; + str.setAttribute('id', 'nick-'+nick.nickname); + let construct = ""+nick.prefix+""+nick.nickname+""; + str.innerHTML = construct; + this.frame.appendChild(str); + } + + render() { + this.frame.innerHTML = ""; + for(let n in this.nicks) { + let nick = this.nicks[n]; + this.appendToList(nick); + } + } + + nickAdd(nickname) { + let newbie = { nickname: nickname, prefix: "", modes: [] } + this.nicks.push(newbie); + this.render(); + } + + nickRemove(nickname) { + let nickIndex = this.getNickIndex(nickname); + + if(nickIndex != null) + this.nicks.splice(nickIndex, 1); + else + return; + + this.render(); + } + + nickChange(oldNickname, newNickname) { + let nickIndex = this.getNickIndex(oldNickname); + + if(nickIndex != null) + this.nicks[nickIndex].nickname = newNickname; + else + return; + + this.render(); + } + + getNickIndex(nickname) { + let result = null; + + for(let n in this.nicks) + if(this.nicks[n].nickname == nickname) result = n; + + return result; + } + + modeAdded(nickname, newMode) { + let nickIndex = this.getNickIndex(nickname); + let nick = null; + + if(nickIndex != null) + nick = this.nicks[nickIndex]; + else + return; + + for(let mode in irc.modeTranslation) { + let prefix = irc.modeTranslation[m]; + if(newMode == mode) + this.nicks[nickIndex].prefix = prefix; + } + + this.render(); + } + + modeRemoved(nickname, oldMode) { + let nickIndex = this.getNickIndex(nickname); + let nick = null; + + if(nickIndex != null) + nick = this.nicks[nickIndex]; + else + return; + + for(let mode in irc.modeTranslation) { + let prefix = irc.modeTranslation[m]; + if(newMode == mode) + this.nicks[nickIndex].prefix = ""; + } + + this.render(); } } @@ -94,10 +299,12 @@ class Tab { setUnreadCount(count) { if(this.element) { let counter = this.element.querySelector('#unread'); - if(count == 0) - counter.innerHTML = ""; - else + if(count == 0) { + counter.className = "none"; + } else { counter.innerHTML = count; + counter.className = ""; + } } } @@ -109,7 +316,7 @@ class Tab { class Buffer { constructor(servername, buffername, tabname, type) { this.messages = []; - this.nicklist = []; + this.nicklist = null; this.topic = null; this.input = ""; this.lastscroll = 0; @@ -126,8 +333,10 @@ class Buffer { this.tab = new Tab(this); this.tab.renderTab(irc.primaryFrame.querySelector('.tabby')); - if(type == "channel") + if(type == "channel") { this.nicklistDisplayed = true; + this.nicklist = new Nicklist(this); + } this.frame = irc.primaryFrame.querySelector('#chat'); this.letterbox = this.frame.querySelector('.letterbox'); @@ -157,6 +366,7 @@ class Buffer { } this.renderMessages(); + this.letterbox.scrollTop = this.lastscroll; } renderMessages() { @@ -172,11 +382,11 @@ class Buffer { appendMessage(meta) { let mesgConstr = document.createElement('div'); - mesgConstr.className = "message m_"+meta.type; + mesgConstr.className = "message type_simple m_"+meta.type; let construct = ""; if(irc.timestamps) - construct += ""+meta.time+""; + construct += ""+meta.time.format(irc.timestampFormat)+""; if(meta.sender != null) construct += ""+meta.sender+""; @@ -187,10 +397,14 @@ class Buffer { mesgConstr.innerHTML = construct; this.letterbox.appendChild(mesgConstr); + + let lFactor = this.letterbox.offsetHeight + this.letterbox.scrollTop + if(lFactor > this.letterbox.scrollHeight - 100) + this.letterbox.scrollTop = this.letterbox.scrollHeight; } addMessage(message, sender, type) { - let mesg = {message: message, sender: sender, type: type, time: Date.now()} + let mesg = {message: message, sender: sender, type: type, time: new Date()} this.messages.push(mesg); if(this.active) @@ -201,6 +415,12 @@ class Buffer { this.tab.setUnreadCount(this.unreadCount); } + switchOff() { + this.tab.setActive(false); + this.lastscroll = this.letterbox.scrollTop; + this.active = false; + } + close() { } @@ -310,10 +530,7 @@ class IRCChatWindow { this.buffers = []; this.frame.style.display = "none"; this.firstServer = true; - this.currentBuffer = null; - - irc.switchBuffer = this.render; } getBufferByName(buffername) { @@ -392,25 +609,35 @@ class IRCChatWindow { this.render(buf); } + closeBuffer(buffer) { + // todo: close + } + messageBuffer(name, server, message) { let buf = this.getBufferByNameServer(server, name); + if(buf == null) - return; + buf = this.createBuffer(server, name, "message", false); + buf.addMessage(message.message, message.from, message.type); } render(buffer) { let activeNow = this.getActiveBuffer(); - if(activeNow) { - activeNow.tab.setActive(false); - activeNow.active = false; - } + if(activeNow) + activeNow.switchOff(); buffer.render(); } } +/**************************\ +|** **| +|** INITIALIZATION **| +|** **| +\**************************/ + window.onload = function() { irc.socket = io.connect('http://localhost:8080'); diff --git a/public/main.styl b/public/main.styl index 182bda8..ee394e5 100644 --- a/public/main.styl +++ b/public/main.styl @@ -120,6 +120,19 @@ body color: #fff; cursor: pointer; font-weight: 600; + #unread + display: inline-block; + min-width: 22px; + height: 22px; + background-color: #bf0000; + border: 1px solid #b00; + box-shadow: inset 2px 2px 3px #f00; + text-align: center; + font-size: 10px; + margin: 0 5px; + border-radius: 100%; + &.none + display: none; #close display: inline-block; width: 12px; @@ -157,6 +170,9 @@ body bottom: 0; left: 0; right: 0; + overflow-y: auto; + white-space: pre-wrap; + word-wrap: break-word; .nicklist width: 280px; position: absolute; @@ -169,6 +185,8 @@ body &.vnicks .nicklist display: block; + &.forceopen + display: block !important; .letterbox right: 301px; .topicbar @@ -209,3 +227,21 @@ body border-left: 0; box-shadow: inset 4px 4px 8px #d8d8d8; + +.message.type_simple + .timestamp + margin-right: 5px; + color: #696969; + &:before + color: #607D8B; + content: "["; + &:after + color: #607D8B; + content: "]"; + .sender + margin-right: 5px; + color: #3F51B5; + &:before + content: "<"; + &:after + content: ">"; diff --git a/teemant.js b/teemant.js index e882911..78f7b33 100755 --- a/teemant.js +++ b/teemant.js @@ -48,7 +48,7 @@ io.sockets.on('connection', function (socket) { // Spam the client with messages (for testing) setInterval(function() { socket.emit('act_client', {type: 'server_message', messageType: "privmsg", server: connectiondata.server, to: "#channel", from: "horse", message: "I like ponies"}); - }, 4000); + }, 1000); }, 4000); }); });