diff --git a/index.html b/index.html
index 1883871..f32160b 100644
--- a/index.html
+++ b/index.html
@@ -63,6 +63,7 @@
+
diff --git a/js/controller.js b/js/controller.js
index 8103d7c..5e7dbb1 100644
--- a/js/controller.js
+++ b/js/controller.js
@@ -1,9 +1,5 @@
"use strict";
-function createClient(endpoint) {
- const api = new mm_client.MattermostApi(normalizedEndpoint(endpoint));
- return new mm_client.MattermostClient(api, localstorage_credentials);
-}
function buttonDisable(element, text) {
if (!element.dataset.originalText) {
@@ -18,7 +14,7 @@ function buttonEnable(element) {
}
function logIn() {
- const client = createClient(byId("login_server").value);
+ const client = mm_client.get(normalizedEndpoint(byId("login_server").value));
buttonDisable(byId("login_button"), "Logging in...");
@@ -39,7 +35,7 @@ function logIn() {
}
function logOut(endpoint, button) {
- const client = createClient(endpoint);
+ const client = mm_client.get(endpoint);
buttonDisable(button, "Logging out...");
@@ -56,6 +52,8 @@ function logOut(endpoint, button) {
button.parentElement.appendChild(span);
buttonEnable(button);
});
+
+ mm_client.drop(endpoint);
}
function channelNameElements(team, channel) {
@@ -73,7 +71,7 @@ function channelNameElements(team, channel) {
title = `${channel.name} in team ${team.name} (private)`;
break;
case "D": // Direct message
- return undefined;
+ return undefined; // XXX Because they clutter the list
teamName = "";
channelName = `👤 ...`;
title = `Direct message`;
@@ -129,7 +127,7 @@ function switchToChannel(client, team, channel) {
.then(response => {
console.info(`Got channel contents of ${channel.id} (${channel.name})`);
response.order.reverse();
- populateChannelContents(client, response);
+ populateChannelContents(client, channel, response);
})
.catch(error => {
console.error(error);
diff --git a/js/main.js b/js/main.js
index d3973a0..3452cf8 100644
--- a/js/main.js
+++ b/js/main.js
@@ -9,19 +9,9 @@ checkScrolledToBottom();
populateServerSelectionList();
-function fetchUsernames(endpoint) {
- console.debug(endpoint);
- const client = createClient(endpoint).users();
- return client;
-}
+localstorage_credentials.getServers()
+ .map(server => server.endpoint)
+ .map(mm_client.get)
+ .forEach(client => client.getUsers());
-let users = {};
-Promise.all(localstorage_credentials.getServers().map(server => server.endpoint).map(fetchUsernames)).then(server_user_data => {
- for (let user_data of server_user_data) {
- for (let id of Object.getOwnPropertyNames(user_data)) {
- const user = user_data[id];
- users[user.id] = user;
- }
- }
- populateChannelList();
-});
+populateChannelList();
diff --git a/js/mm_client.js b/js/mm_client.js
index a85b76c..81fe9c1 100644
--- a/js/mm_client.js
+++ b/js/mm_client.js
@@ -1,22 +1,19 @@
const mm_client = (function() { "use strict";
-class MattermostApi {
- constructor(endpoint) {
- this._endpoint = endpoint;
- }
+class MattermostClient {
+ constructor (endpoint, credentials_provider) {
+ this.endpoint = endpoint;
- get id() {
- return this._endpoint;
- }
-
- get endpoint() {
- return this._endpoint;
+ this.credentials = credentials_provider;
+ 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"}`);
}
async get(path, token, queryParams) {
const headers = token ? {"Authorization": `Bearer ${token}`} : {};
- const response = await ajax.getJson(`${this._endpoint}${path}`, {headers, queryParams});
+ const response = await ajax.getJson(`${this.endpoint}${path}`, {headers, queryParams});
if (!response.ok) {
throw response;
}
@@ -25,27 +22,16 @@ class MattermostApi {
async post(path, token, data) {
const headers = token ? {"Authorization": `Bearer ${token}`} : {};
- const response = await ajax.postJson(`${this._endpoint}${path}`, data, {headers});
+ const response = await ajax.postJson(`${this.endpoint}${path}`, data, {headers});
if (!response.ok) {
throw response;
}
return response;
}
-}
-
-
-class MattermostClient {
- constructor (api, credentials_provider) {
- this.api = api;
- this.credentials = credentials_provider;
- const creds = this.credentials.get(this.api.id);
- this.token = creds ? creds.token : null;
- console.info(`Created MattermostClient for ${this.api.id}, ${this.token ? "found token" : "did not find token"}`);
- }
async authedGet(url, queryParams) {
assert(this.token, "logged in");
- const response = await this.api.get(url, this.token, queryParams);
+ const response = await this.get(url, this.token, queryParams);
return response.responseJson;
}
@@ -64,8 +50,7 @@ class MattermostClient {
throw new Error("Requesting more than 100 pages, something looks wrong");
}
- const response = await this.api.get(url, this.token, params);
- console.log(response);
+ const response = await this.get(url, this.token, params);
data = data.concat(response.responseJson);
loadMore = response.responseJson.length > 0;
@@ -96,26 +81,27 @@ class MattermostClient {
throw Error("Already logged in on this server");
}
- const response = await this.api.post("/users/login", undefined, {login_id, password});
+ const response = await this.post("/users/login", undefined, {login_id, password});
const token = response.getResponseHeader("Token");
if (!token) {
throw Error("No Token header in response to log in request");
}
- this.credentials.store(this.api.id, login_id, token);
+ this.credentials.store(this.endpoint, login_id, token);
this.token = token;
+ let _ = this.getUsers();
return response.responseJson;
}
async logOut() {
assert(this.token, "logged in");
- const response = await this.api.post("/users/logout", this.token);
+ const response = await this.post("/users/logout", this.token);
// Verify that the token is now invalidated
if (await this.loggedIn()) {
throw new Error("Failed to log out: token still works after trying to log out");
}
- this.credentials.clear(this.api.id);
+ this.credentials.clear(this.endpoint);
this.token = null;
return response.responseJson;
}
@@ -141,8 +127,18 @@ class MattermostClient {
});
}
- users() {
- return this.authedPaginatedGet("/users", {});
+ getUsers() {
+ if (!this.users) {
+ this.users = this.authedPaginatedGet("/users", {}).then(users => {
+ const newUsers = Object.create(null);
+ for (let user_data of users) {
+ const user = user_data;
+ newUsers[user.id] = user;
+ }
+ return newUsers;
+ });
+ }
+ return this.users;
}
async filePublicLink(file_id) {
@@ -151,6 +147,24 @@ class MattermostClient {
}
}
-return {MattermostApi, MattermostClient};
+
+let clients = Object.create(null);
+
+function get(endpoint) {
+ if (!clients[endpoint]) {
+ clients[endpoint] = new MattermostClient(endpoint, localstorage_credentials);
+ }
+ return clients[endpoint];
+}
+
+function getMultiple(endpoints) {
+ return endpoints.map(get);
+}
+
+function drop(endpoint) {
+ delete clients[endpoint];
+}
+
+return {get, getMultiple, drop};
})();
diff --git a/js/pubsub.js b/js/pubsub.js
new file mode 100644
index 0000000..95a7058
--- /dev/null
+++ b/js/pubsub.js
@@ -0,0 +1,44 @@
+const pubsub = (function() { "use strict";
+
+const topics = [
+ "MESSAGES_NEW",
+ "MESSAGES_CHANGED",
+ "CHANNELS_NEW",
+ "CHANNELS_CHANGED",
+ "USERS_NEW",
+ "USERS_CHANGED",
+ "CHANNEL_MEMBERS_NEW",
+ "CHANNEL_MEMBERS_REMOVED",
+];
+
+let subscribers = Object.create(null);
+for (let topic of topics) {
+ subscribers[topic] = [];
+}
+
+
+function subscribe(topic, callback) {
+ assert(subscribers[topic], `Pubsub topic '${topic}' exists`);
+
+ subscribers[topic].push(callback);
+}
+
+
+function publish(topic, payload) {
+ assert(subscribers[topic], `Pubsub topic '${topic}' exists`);
+
+ const amountSubs = subscribers[topic].length;
+ if (amountSubs == 0) {
+ console.warn(`Pubsub event with topic '${topic}' without any subscribers`);
+ } else {
+ console.debug(`Pubsub event with topic '${topic}' with ${amountSubs} subscribers`);
+ }
+ for (let subscriber of subscribers[topic]) {
+ subscriber(payload);
+ }
+}
+
+
+return {subscribe, publish};
+
+})();
diff --git a/js/view/view.js b/js/view/view.js
index 4df7255..6191e59 100644
--- a/js/view/view.js
+++ b/js/view/view.js
@@ -20,9 +20,7 @@ function populateServerSelectionList() {
}
function populateChannelList() {
- async function addChannelItems(endpoint) {
- const client = createClient(endpoint);
-
+ async function addChannelItems(client) {
const teams = await client.myTeams();
for (let team of teams) {
@@ -47,92 +45,118 @@ function populateChannelList() {
}
}
- const servers = localstorage_credentials.getServers();
-
byId("channel_list").innerHTML = "";
- for (let server of servers) {
- addChannelItems(server.endpoint);
+ const endpoints = localstorage_credentials.getServers().map(server => server.endpoint);
+ for (let client of mm_client.getMultiple(endpoints)) {
+ addChannelItems(client);
}
}
-function populateChannelContents(client, contents) {
+async function createMessageElement(client, post, lastTime, lastAuthor) {
+ const users = await client.getUsers();
+ const isThreadReply = !!post.parent_id;
+
+ const messageDiv = document.createElement("div");
+ messageDiv.className = "message";
+ messageDiv.innerText = post.message;
+
+ const createAt = new Date(post.create_at);
+ const createAtDiv = document.createElement("time");
+ createAtDiv.className = "create_at";
+ const sim = dateSimilarity(lastTime, createAt);
+ lastTime = createAt;
+ let createAtText = "";
+ if (sim < DATE_SIMILARITY.date) createAtText += formatDdddMmYy(createAt);
+ if (sim < DATE_SIMILARITY.minutes) createAtText += " " + formatHhMm(createAt);
+ createAtDiv.title = createAt.toString();
+ createAtDiv.innerText = createAtText;
+ createAtDiv.dateTime = createAt.toISOString();
+
+ const authorName = users[post.user_id] ? users[post.user_id].username : post.user_id;
+ const authorDiv = document.createElement("div");
+ authorDiv.className = "author";
+ authorDiv.innerText = authorName;
+
+ const postDiv = document.createElement("div");
+ postDiv.className = "post";
+ if (lastAuthor === post.user_id) {
+ postDiv.className += " same_author";
+ }
+ lastAuthor = post.user_id;
+
+ postDiv.dataset["id"] = post.id;
+ postDiv.appendChild(authorDiv);
+ postDiv.appendChild(createAtDiv);
+ postDiv.appendChild(messageDiv);
+
+ if ((post.metadata.files || []).length > 0) {
+ const attachmentsUl = document.createElement("ul");
+ attachmentsUl.className = "attachments";
+ for (let file of post.metadata.files || []) {
+ const attachmentLi = document.createElement("li");
+ attachmentLi.dataset["id"] = file.id;
+
+ const attachmentA = document.createElement("a");
+ client.filePublicLink(file.id).then(link => attachmentA.href = link);
+ attachmentA.target = "_blank";
+ attachmentA.innerText = file.name;
+
+ if (file.mini_preview) {
+ const attachmentImg = document.createElement("img");
+ attachmentImg.src = `data:image/jpeg;base64,${file.mini_preview}`;
+ attachmentA.appendChild(attachmentImg);
+ }
+
+ attachmentLi.appendChild(attachmentA);
+ attachmentsUl.appendChild(attachmentLi);
+ }
+ postDiv.appendChild(attachmentsUl);
+ }
+
+ return {postDiv, lastTime, lastAuthor};
+}
+
+
+async function populateChannelContents(client, channel, contents) {
byId("channel_contents").innerHTML = "";
- let lastAuthor = null;
- let lastTime = null;
+ let result = {lastAuthor: null, lastTime: null, postDiv: null};
let nodes = [];
for (let id of contents.order) {
- const post = contents.posts[id];
-
- const isThreadReply = !!post.parent_id;
-
- const messageDiv = document.createElement("div");
- messageDiv.className = "message";
- messageDiv.innerText = post.message;
-
- const createAt = new Date(post.create_at);
- const createAtDiv = document.createElement("time");
- createAtDiv.className = "create_at";
- const sim = dateSimilarity(lastTime, createAt);
- lastTime = createAt;
- let createAtText = "";
- if (sim < DATE_SIMILARITY.date) createAtText += formatDdddMmYy(createAt);
- if (sim < DATE_SIMILARITY.minutes) createAtText += " " + formatHhMm(createAt);
- createAtDiv.title = createAt.toString();
- createAtDiv.innerText = createAtText;
- createAtDiv.dateTime = createAt.toISOString();
-
- const authorName = users[post.user_id] ? users[post.user_id].username : post.user_id;
- const authorDiv = document.createElement("div");
- authorDiv.className = "author";
- authorDiv.innerText = authorName;
-
- const postDiv = document.createElement("div");
- postDiv.className = "post";
- if (lastAuthor === post.user_id) {
- postDiv.className += " same_author";
- }
- lastAuthor = post.user_id;
-
- postDiv.dataset["id"] = id;
- postDiv.appendChild(authorDiv);
- postDiv.appendChild(createAtDiv);
- postDiv.appendChild(messageDiv);
-
- if ((post.metadata.files || []).length > 0) {
- const attachmentsUl = document.createElement("ul");
- attachmentsUl.className = "attachments";
- for (let file of post.metadata.files || []) {
- const attachmentLi = document.createElement("li");
- attachmentLi.dataset["id"] = file.id;
-
- const attachmentA = document.createElement("a");
- client.filePublicLink(file.id).then(link => attachmentA.href = link);
- attachmentA.target = "_blank";
- attachmentA.innerText = file.name;
-
- if (file.mini_preview) {
- const attachmentImg = document.createElement("img");
- attachmentImg.src = `data:image/jpeg;base64,${file.mini_preview}`;
- attachmentA.appendChild(attachmentImg);
- }
-
- attachmentLi.appendChild(attachmentA);
- attachmentsUl.appendChild(attachmentLi);
- }
- postDiv.appendChild(attachmentsUl);
- }
-
- nodes.push(postDiv);
+ result = await createMessageElement(client, contents.posts[id], result.lastTime, result.lastAuthor);
+ nodes.push(result.postDiv);
}
+ byId("channel_contents").dataset["server"] = client.endpoint;
+ byId("channel_contents").dataset["id"] = channel.id;
+ byId("channel_contents").dataset["lastTime"] = result.lastTime.getTime();
+ byId("channel_contents").dataset["lastAuthor"] = result.lastAuthor;
byId("channel_contents").append(...nodes);
scrollToBottom();
}
+async function addMessage(client, post) {
+ const shouldScroll = isScrolledToBottom();
+
+ const result = await createMessageElement(
+ client, post,
+ new Date(1 * (byId("channel_contents").dataset["lastTime"])),
+ byId("channel_contents").dataset["lastAuthor"]
+ );
+ console.log(result);
+ byId("channel_contents").dataset["lastTime"] = result.lastTime.getTime();
+ byId("channel_contents").dataset["lastAuthor"] = result.lastAuthor;
+ byId("channel_contents").appendChild(result.postDiv);
+
+ if (shouldScroll) {
+ scrollToBottom();
+ }
+}
+
+
function scrollToBottom() {
const el = byId("channel_contents_wrapper");
el.scrollTop = el.scrollHeight;
@@ -146,11 +170,23 @@ function updateComposeHeight() {
}
byId("compose").addEventListener("input", updateComposeHeight);
-function checkScrolledToBottom() {
+function isScrolledToBottom() {
const el = byId("channel_contents_wrapper");
const scrolledTo = el.clientHeight + el.scrollTop;
- const atBottom = scrolledTo >= el.scrollHeight;
- el.className = atBottom ? "" : "not-at-bottom";
+ return scrolledTo >= el.scrollHeight;
+}
+function checkScrolledToBottom() {
+ byId("channel_contents_wrapper").className = isScrolledToBottom() ? "" : "not-at-bottom";
}
byId("channel_contents_wrapper").addEventListener("scroll", checkScrolledToBottom);
+
+
+pubsub.subscribe("MESSAGES_NEW", post => {
+ if (
+ post.endpoint === byId("channel_contents").dataset["server"] &&
+ post.channel_id === byId("channel_contents").dataset["id"]
+ ) {
+ addMessage(mm_client.get(post.endpoint), post);
+ }
+});