diff --git a/index.html b/index.html index 2dab188..8a3fc4a 100644 --- a/index.html +++ b/index.html @@ -63,6 +63,7 @@ + diff --git a/js/controller.js b/js/controller.js index 761d930..bdd9f00 100644 --- a/js/controller.js +++ b/js/controller.js @@ -62,33 +62,32 @@ function logOut(endpoint, button) { } function channelNameElements(team, channel) { + const teamName = team ? team.name : ""; + const inTeam = teamName ? " in team " + teamName : ""; let icon = ""; - let teamName = team.name; let channelName = channel.name; let title = ""; switch (channel.type) { case "O": // Public channel - title = `${channel.name} in team ${team.name} (public)`; + title = `${channel.name}${inTeam} (public)`; break; case "P": // Private channel icon = "🔒"; - title = `${channel.name} in team ${team.name} (private)`; + title = `${channel.name}${inTeam} (private)`; break; case "D": // Direct message return undefined; // XXX Because they clutter the list - teamName = ""; channelName = `👤 ...`; title = `Direct message`; break; case "G": // Group chat - teamName = ""; channelName = `👥 ${channel.display_name}`; title = `Group chat with ${channel.display_name}`; break; default: // Unsupported icon = channel.type; - title = `${channel.name} in team ${team.name} (type ${channel.type})`; + title = `${channel.name}${inTeam} (type ${channel.type})`; break; } @@ -210,16 +209,19 @@ function checkKeyPress(event) { } pubsub.subscribe("MESSAGES_NEW", post => { - if (!window.hasFocus) { - return; - } - const curChan = currentChannel(); - if (post.endpoint === curChan.endpoint && post.channel_id === curChan.channel_id) { - mm_client.getOrCreate(post.endpoint).markChannelRead(post.channel_id); + const client = mm_client.getOrCreate(post.endpoint); + if (window.hasFocus && post.endpoint === curChan.endpoint && post.channel_id === curChan.channel_id) { + client.markChannelRead(post.channel_id); + } else { + client.channelStore.increaseUnread(post); } }); +pubsub.subscribe("CHANNEL_READ", ({endpoint, channel_id}) => { + mm_client.getOrCreate(endpoint).channelStore.setUnread(channel_id, 0, 0); +}); + pubsub.subscribe("WINDOW_FOCUSED", () => { const curChan = currentChannel(); if (!curChan.channel_id) return; diff --git a/js/mm_client.js b/js/mm_client.js index 3d68377..6fa8c64 100644 --- a/js/mm_client.js +++ b/js/mm_client.js @@ -9,6 +9,8 @@ class MattermostClient { const creds = this.credentials.get(this.endpoint); this.token = creds ? creds.token : null; console.info(`Created MattermostClient for ${this.endpoint}, ${this.token ? "found token" : "did not find token"}`); + + this.channelStore = new store.ChannelStore(this); } async get(path, queryParams) { @@ -56,11 +58,16 @@ class MattermostClient { start() { assert(this.token); - let _ = this.getUsers(); - return this.userMe().then(data => { - this.me = data; - this.websocket(); - }); + const users = this.getUsers(); + const me = this.userMe().then(data => {this.me = data;}); + const channels = this.channelStore.get(); + + return Promise.all([ + users, + me, + channels + ]) + .then(() => {this.websocket();}); } async loggedIn() { diff --git a/js/pubsub.js b/js/pubsub.js index 79ae073..1241fee 100644 --- a/js/pubsub.js +++ b/js/pubsub.js @@ -3,8 +3,9 @@ const pubsub = (function() { "use strict"; const topics = [ "MESSAGES_NEW", // {endpoint, channel_id, create_at, user_id, message} "MESSAGES_CHANGED", - "CHANNELS_NEW", - "CHANNELS_CHANGED", + "CHANNELS_RELOADED", // + "CHANNEL_UPDATED", // {endpoint, channel} + "CHANNEL_UNREAD_UPDATED", // {endpoint, channel_id, unread, mentions} "USERS_NEW", "USERS_CHANGED", "CHANNEL_MEMBERS_NEW", diff --git a/js/store.js b/js/store.js new file mode 100644 index 0000000..0a28b2e --- /dev/null +++ b/js/store.js @@ -0,0 +1,106 @@ +const store = (function() { "use strict"; + +class ChannelStore { + constructor(client) { + this.client = client; + this._channels = null; + this._unread = null; + this._promise = null; + this._fetching = false; + } + + fetch() { + if (this._fetching) { + return; + } + this._fetching = true; + this._channels = null; + this._unread = null; + + const teams = this.client.myTeams(); + + const channels = teams + .then(teams => Promise.all(teams.map(team => this.client.myChannels(team.id)))) + .then(teams => teams.map(channels => arrayToHashmap(channels, "id"))) + .then(flattenHashmaps); + + const unread = teams + .then(teams => Promise.all(teams.map(team => this.client.getUnread(team.id)))) + .then(flattenArrays); + + function processUnread(channels, unread) { + // In the Mattermost API, not the number of *unread* messages but the number of *read* + // messages is returned, so we have to subtract that from the number of total messages. + let result = Object.create(null); + for (let x of unread) { + if (!(x["channel_id"] in channels)) continue; + let object = Object.create(null); + object.unread = channels[x["channel_id"]]["total_msg_count"] - x["msg_count"]; + object.mentions = x["mention_count"]; + result[x["channel_id"]] = object; + } + return result; + } + + this._promise = Promise.all([teams, channels, unread]) + .then(([teams, channels, unread]) => { + this._teams = arrayToHashmap(teams, "id"); + this._channels = channels; + this._unread = processUnread(channels, unread); + this._fetching = false; + + pubsub.publish("CHANNELS_RELOADED"); + }); + } + + async get() { + if (this._promise === null) { + this.fetch(); + } + await this._promise; + return {teams: this._teams, channels: this._channels, unread: this._unread}; + } + + async updateChannel(channel) { + if (this._promise === null) { + // Data does not exist yet and is not being requested + return; + } + await this._promise; + this._channels[channel["id"]] = channel; + } + + async increaseUnread(post) { + if (this._promise === null) { + // Data does not exist yet and is not being requested + return; + } + await this._promise; + + // TODO Check if post is mention and increase mentions counter if it is + this._unread[post["channel_id"]].unread += 1; + + const unread = this._unread[post["channel_id"]].unread; + const mentions = this._unread[post["channel_id"]].mentions; + + pubsub.publish("CHANNEL_UNREAD_UPDATED", {endpoint: this.client.endpoint, channel_id: post["channel_id"], unread, mentions}); + } + + async setUnread(channel_id, unread, mentions) { + if (this._promise === null) { + // Data does not exist yet and is not being requested + return; + } + await this._promise; + + this._unread[channel_id].unread = unread; + this._unread[channel_id].mentions = mentions; + + pubsub.publish("CHANNEL_UNREAD_UPDATED", {endpoint: this.client.endpoint, channel_id, unread, mentions}); + } +} + + +return {ChannelStore}; + +})(); diff --git a/js/util.js b/js/util.js index 2c7aaf0..1059127 100644 --- a/js/util.js +++ b/js/util.js @@ -71,6 +71,23 @@ function arrayToHashmap(array, key) { return result; } +function flattenHashmaps(hashmaps) { + let result = Object.create(null); + for (let x of hashmaps) { + Object.assign(result, x); + } + return result; +} + +function flattenArrays(arrays) { + let result = []; + for (let x of arrays) { + Object.assign(result, x); + Array.prototype.push.apply(result, x) + } + return result; +} + function startsWith(string, start) { return string.slice(0, start.length) == start; diff --git a/js/view/sidebar.js b/js/view/sidebar.js index 9f52ec6..fea0ccc 100644 --- a/js/view/sidebar.js +++ b/js/view/sidebar.js @@ -21,49 +21,42 @@ function populateServerSelectionList() { function populateChannelList() { async function addChannelItems(client) { - const teams = await client.myTeams(); + const {teams, channels, unread} = await client.channelStore.get(); - for (let team of teams) { - let nodes = []; - const [channels, unreadsList] = await Promise.all([ - client.myChannels(team.id), - client.getUnread(team.id) - ]); + let nodes = []; - const unreads = arrayToHashmap(unreadsList, "channel_id"); + for (let channel_id in channels) { + const channel = channels[channel_id]; + const team = teams[channel["team_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 chanUnread = unread[channel.id].unread; + const chanMentions = unread[channel.id].mentions; - const li = document.createElement("li"); - const a = document.createElement("a"); - a.href = "javascript:void(0)"; - const titleAndElements = channelNameElements(team, channel); - if (!titleAndElements) continue; - a.title = titleAndElements[0]; - a.append(...titleAndElements[1]); + const li = document.createElement("li"); + const a = document.createElement("a"); + a.href = "javascript:void(0)"; + const titleAndElements = channelNameElements(team, channel); + if (!titleAndElements) continue; + 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.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)); + 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; + li.appendChild(a); + li.dataset["id"] = channel.id; + li.dataset["server"] = client.endpoint; - if (chanUnreads > 0) li.className = "unread"; - nodes.push(li); - } - byId("channel_list").append(...nodes); + if (chanUnread > 0) li.className = "unread"; + nodes.push(li); } + byId("channel_list").append(...nodes); } byId("channel_list").innerHTML = ""; @@ -73,18 +66,7 @@ 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; +function updateUnreadCount(el, unread, mentions) { let msgCountGem = el.querySelector('.msg_count_gem'); if (mentions > 0) { msgCountGem.style.display = "inline-block"; @@ -92,29 +74,21 @@ function setUnreadCount(el, unreads, mentions) { } else { msgCountGem.style.display = "none"; } - if (unreads > 0) { + if (unread > 0) { addClass(el, "unread"); } else { removeClass(el, "unread"); } } - -pubsub.subscribe("MESSAGES_NEW", post => { - 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 => { +pubsub.subscribe("CHANNEL_UNREAD_UPDATED", ({endpoint, channel_id, unread, mentions}) => { for (let el of byId("channel_list").childNodes) { - if (el.dataset["server"] == channel.endpoint && el.dataset["id"] == channel.channel_id) { - setUnreadCount(el, 0, 0); + if (el.dataset["server"] == endpoint && el.dataset["id"] == channel_id) { + updateUnreadCount(el, unread, mentions); } } }); + +pubsub.subscribe("CHANNELS_RELOADED", () => { + populateChannelList(); +});