From 0cc0cb1c133762afff620b0644ee2e47b8cb4e45 Mon Sep 17 00:00:00 2001 From: Midgard Date: Wed, 8 Jun 2022 23:24:59 +0200 Subject: [PATCH] Add support for channel read status --- assets/main.css | 18 +++++++++++++ js/controller.js | 16 +++++++++-- js/mm_client.js | 11 ++++++++ js/pubsub.js | 3 ++- js/util.js | 17 ++++++++++++ js/view/sidebar.js | 67 +++++++++++++++++++++++++++++++++++++++++++--- 6 files changed, 125 insertions(+), 7 deletions(-) diff --git a/assets/main.css b/assets/main.css index b2167e8..f449903 100644 --- a/assets/main.css +++ b/assets/main.css @@ -78,6 +78,7 @@ h1 img { border-right: 1px solid #aaa; } #channel_list a { + position: relative; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; @@ -105,6 +106,23 @@ h1 img { color: #888; margin: 0 1px; } +#channel_list .unread { + font-weight: bold; +} +#channel_list a .msg_count_gem { + display: none; + position: absolute; + background-color: #444; + border-radius: 100%; + color: #ccc; + font-weight: normal; + font-size: 50%; + padding: 0 0.6em; + right: 0.5em; + top: 50%; + transform: translateY(-50%); + vertical-align: middle; +} ul#channel_list, ul#server_selection_list { list-style: none; diff --git a/js/controller.js b/js/controller.js index 01d516e..2896cf8 100644 --- a/js/controller.js +++ b/js/controller.js @@ -120,9 +120,9 @@ function channelNameElements(team, channel) { function switchToChannel(client, team, channel) { for (let el of byId("channel_list").childNodes) { if (el.dataset["server"] == client.endpoint && el.dataset["id"] == channel.id) { - el.className = "active"; + addClass(el, "active"); } else { - el.className = ""; + removeClass(el, "active"); } } @@ -132,6 +132,14 @@ function switchToChannel(client, team, channel) { console.info(`Got channel contents of ${channel.id} (${channel.name})`); response.order.reverse(); populateChannelContents(client, channel, response); + + markChannelAsRead(client, channel) + .then(_ => { + pubsub.publish("CHANNEL_READ", { + endpoint: client.endpoint, + channel_id: channel.id + }); + }); }) .catch(error => { console.error(error); @@ -156,6 +164,10 @@ function sendMessage(endpoint, channel_id, message) { //}); } +function markChannelAsRead(client, channel) { + return client.markChannelRead(channel.id); +} + function checkKeyPress(event) { // Battle tested for many years in several browsers if ((event.keyCode === event.DOM_VK_RETURN || event.keyCode === 13 || event.keyCode === 10 || event.key === "Enter" || event.keyIdentifier === "U+000A") && !event.shiftKey && !event.ctrlKey) { diff --git a/js/mm_client.js b/js/mm_client.js index adfb277..a0dda4e 100644 --- a/js/mm_client.js +++ b/js/mm_client.js @@ -137,6 +137,17 @@ class MattermostClient { }); } + getUnread(team_id) { + return this.get(`/users/me/teams/${team_id}/channels/members`); + } + + markChannelRead(channel_id) { + assertIsMattermostId(channel_id); + return this.post(`/channels/members/me/view`, { + channel_id: channel_id + }); + } + writePost(channel_id, message) { return this.post("/posts", { "channel_id": channel_id, diff --git a/js/pubsub.js b/js/pubsub.js index 95a7058..0e5102d 100644 --- a/js/pubsub.js +++ b/js/pubsub.js @@ -1,7 +1,7 @@ const pubsub = (function() { "use strict"; const topics = [ - "MESSAGES_NEW", + "MESSAGES_NEW", // {endpoint, channel_id, create_at, user_id, message} "MESSAGES_CHANGED", "CHANNELS_NEW", "CHANNELS_CHANGED", @@ -9,6 +9,7 @@ const topics = [ "USERS_CHANGED", "CHANNEL_MEMBERS_NEW", "CHANNEL_MEMBERS_REMOVED", + "CHANNEL_READ", // {endpoint, channel_id} ]; let subscribers = Object.create(null); diff --git a/js/util.js b/js/util.js index 31683dd..749d90a 100644 --- a/js/util.js +++ b/js/util.js @@ -35,6 +35,14 @@ function byId(id, nullOk=false) { } +function removeClass(el, className) { + el.className = el.className.split(" ").filter(x => x !== className).join(" "); +} +function addClass(el, className) { + el.className = el.className.split(" ").filter(x => x !== className).join(" ") + ` ${className}`; +} + + /** * Wrap a function so that it receives `this` as first argument */ @@ -55,6 +63,15 @@ function extend(obj1, obj2) { } +function arrayToHashmap(array, key) { + let result = Object.create(null); + for (let x of array) { + result[x[key]] = x; + } + return result; +} + + function padLeft(input, width, padding=" ") { text = input + ""; while (text.length < width) { diff --git a/js/view/sidebar.js b/js/view/sidebar.js index 9b2c80a..7c16282 100644 --- a/js/view/sidebar.js +++ b/js/view/sidebar.js @@ -25,8 +25,17 @@ function populateChannelList() { for (let team of teams) { let nodes = []; - const channels = await client.myChannels(team.id); + const [channels, unreadsList] = await Promise.all([ + client.myChannels(team.id), + client.getUnread(team.id) + ]); + + const unreads = arrayToHashmap(unreadsList, "channel_id"); + for (let channel of channels) { + const chanUnreads = channel["total_msg_count"] - unreads[channel.id]["msg_count"]; + const chanMentions = unreads[channel.id]["mention_count"]; + const li = document.createElement("li"); const a = document.createElement("a"); a.href = "javascript:void(0)"; @@ -35,11 +44,23 @@ function populateChannelList() { a.title = titleAndElements[0]; a.append(...titleAndElements[1]); + a.append(document.createTextNode(" ")); + const msgCountGem = document.createElement("span"); + msgCountGem.className = "msg_count_gem"; + msgCountGem.innerText = chanMentions; + if (chanMentions > 0) msgCountGem.style.display = "inline-block"; + a.append(msgCountGem); + a.addEventListener("click", () => switchToChannel(client, team, channel)); li.appendChild(a); li.dataset["id"] = channel.id; li.dataset["server"] = client.endpoint; + li.dataset["unreads"] = chanUnreads; + li.dataset["mentions"] = chanMentions; + + if (chanUnreads > 0) li.className = "unread"; + console.debug(`Channel ${channel.name} with ${chanUnreads} unreads`); nodes.push(li); } byId("channel_list").append(...nodes); @@ -53,10 +74,48 @@ function populateChannelList() { } } +function increaseUnreadCount(el, post) { + // TODO Check if post is mention and increase mentions counter if it is + setUnreadCount( + el, + el.dataset["unreads"] * 1 + 1, + el.dataset["mentions"] * 1 + ); +} + +function setUnreadCount(el, unreads, mentions) { + el.dataset["unreads"] = unreads; + el.dataset["mentions"] = mentions; + let msgCountGem = el.querySelector('.msg_count_gem'); + if (mentions > 0) { + msgCountGem.style.display = "inline-block"; + msgCountGem.innerText = mentions; + } else { + msgCountGem.style.display = "none"; + } + if (unreads > 0) { + addClass(el, "unread"); + } else { + removeClass(el, "unread"); + } +} + pubsub.subscribe("MESSAGES_NEW", post => { - const chan = currentChannel(); - if (!(post.endpoint === chan.endpoint && post.channel_id === chan.channel_id)) { - // TODO mark channel unread + const curChan = currentChannel(); + if (!(post.endpoint === curChan.endpoint && post.channel_id === curChan.channel_id)) { + for (let el of byId("channel_list").childNodes) { + if (el.dataset["server"] == post.endpoint && el.dataset["id"] == post.channel_id) { + increaseUnreadCount(el, post); + } + } + } +}); + +pubsub.subscribe("CHANNEL_READ", channel => { + for (let el of byId("channel_list").childNodes) { + if (el.dataset["server"] == channel.endpoint && el.dataset["id"] == channel.channel_id) { + setUnreadCount(el, 0, 0); + } } });