Add pubsub for new message, no websocket though yet
This commit is contained in:
parent
1419b44acf
commit
de201dca56
6 changed files with 214 additions and 131 deletions
|
@ -63,6 +63,7 @@
|
|||
|
||||
<script type="text/javascript" src="/js/ajax.js"></script>
|
||||
<script type="text/javascript" src="/js/util.js"></script>
|
||||
<script type="text/javascript" src="/js/pubsub.js"></script>
|
||||
<script type="text/javascript" src="/js/localstorage_credentials.js"></script>
|
||||
<script type="text/javascript" src="/js/mm_client.js"></script>
|
||||
<script type="text/javascript" src="/js/view/view.js"></script>
|
||||
|
|
|
@ -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);
|
||||
|
|
20
js/main.js
20
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();
|
||||
|
|
|
@ -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};
|
||||
|
||||
})();
|
||||
|
|
44
js/pubsub.js
Normal file
44
js/pubsub.js
Normal file
|
@ -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};
|
||||
|
||||
})();
|
186
js/view/view.js
186
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);
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue