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/ajax.js"></script>
|
||||||
<script type="text/javascript" src="/js/util.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/localstorage_credentials.js"></script>
|
||||||
<script type="text/javascript" src="/js/mm_client.js"></script>
|
<script type="text/javascript" src="/js/mm_client.js"></script>
|
||||||
<script type="text/javascript" src="/js/view/view.js"></script>
|
<script type="text/javascript" src="/js/view/view.js"></script>
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
"use strict";
|
"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) {
|
function buttonDisable(element, text) {
|
||||||
if (!element.dataset.originalText) {
|
if (!element.dataset.originalText) {
|
||||||
|
@ -18,7 +14,7 @@ function buttonEnable(element) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function logIn() {
|
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...");
|
buttonDisable(byId("login_button"), "Logging in...");
|
||||||
|
|
||||||
|
@ -39,7 +35,7 @@ function logIn() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function logOut(endpoint, button) {
|
function logOut(endpoint, button) {
|
||||||
const client = createClient(endpoint);
|
const client = mm_client.get(endpoint);
|
||||||
|
|
||||||
buttonDisable(button, "Logging out...");
|
buttonDisable(button, "Logging out...");
|
||||||
|
|
||||||
|
@ -56,6 +52,8 @@ function logOut(endpoint, button) {
|
||||||
button.parentElement.appendChild(span);
|
button.parentElement.appendChild(span);
|
||||||
buttonEnable(button);
|
buttonEnable(button);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
mm_client.drop(endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
function channelNameElements(team, channel) {
|
function channelNameElements(team, channel) {
|
||||||
|
@ -73,7 +71,7 @@ function channelNameElements(team, channel) {
|
||||||
title = `${channel.name} in team ${team.name} (private)`;
|
title = `${channel.name} in team ${team.name} (private)`;
|
||||||
break;
|
break;
|
||||||
case "D": // Direct message
|
case "D": // Direct message
|
||||||
return undefined;
|
return undefined; // XXX Because they clutter the list
|
||||||
teamName = "";
|
teamName = "";
|
||||||
channelName = `👤 ...`;
|
channelName = `👤 ...`;
|
||||||
title = `Direct message`;
|
title = `Direct message`;
|
||||||
|
@ -129,7 +127,7 @@ function switchToChannel(client, team, channel) {
|
||||||
.then(response => {
|
.then(response => {
|
||||||
console.info(`Got channel contents of ${channel.id} (${channel.name})`);
|
console.info(`Got channel contents of ${channel.id} (${channel.name})`);
|
||||||
response.order.reverse();
|
response.order.reverse();
|
||||||
populateChannelContents(client, response);
|
populateChannelContents(client, channel, response);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
20
js/main.js
20
js/main.js
|
@ -9,19 +9,9 @@ checkScrolledToBottom();
|
||||||
|
|
||||||
populateServerSelectionList();
|
populateServerSelectionList();
|
||||||
|
|
||||||
function fetchUsernames(endpoint) {
|
localstorage_credentials.getServers()
|
||||||
console.debug(endpoint);
|
.map(server => server.endpoint)
|
||||||
const client = createClient(endpoint).users();
|
.map(mm_client.get)
|
||||||
return client;
|
.forEach(client => client.getUsers());
|
||||||
}
|
|
||||||
|
|
||||||
let users = {};
|
populateChannelList();
|
||||||
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();
|
|
||||||
});
|
|
||||||
|
|
|
@ -1,22 +1,19 @@
|
||||||
const mm_client = (function() { "use strict";
|
const mm_client = (function() { "use strict";
|
||||||
|
|
||||||
|
|
||||||
class MattermostApi {
|
class MattermostClient {
|
||||||
constructor(endpoint) {
|
constructor (endpoint, credentials_provider) {
|
||||||
this._endpoint = endpoint;
|
this.endpoint = endpoint;
|
||||||
}
|
|
||||||
|
|
||||||
get id() {
|
this.credentials = credentials_provider;
|
||||||
return this._endpoint;
|
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"}`);
|
||||||
get endpoint() {
|
|
||||||
return this._endpoint;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(path, token, queryParams) {
|
async get(path, token, queryParams) {
|
||||||
const headers = token ? {"Authorization": `Bearer ${token}`} : {};
|
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) {
|
if (!response.ok) {
|
||||||
throw response;
|
throw response;
|
||||||
}
|
}
|
||||||
|
@ -25,27 +22,16 @@ class MattermostApi {
|
||||||
|
|
||||||
async post(path, token, data) {
|
async post(path, token, data) {
|
||||||
const headers = token ? {"Authorization": `Bearer ${token}`} : {};
|
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) {
|
if (!response.ok) {
|
||||||
throw response;
|
throw response;
|
||||||
}
|
}
|
||||||
return 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) {
|
async authedGet(url, queryParams) {
|
||||||
assert(this.token, "logged in");
|
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;
|
return response.responseJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,8 +50,7 @@ class MattermostClient {
|
||||||
throw new Error("Requesting more than 100 pages, something looks wrong");
|
throw new Error("Requesting more than 100 pages, something looks wrong");
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await this.api.get(url, this.token, params);
|
const response = await this.get(url, this.token, params);
|
||||||
console.log(response);
|
|
||||||
data = data.concat(response.responseJson);
|
data = data.concat(response.responseJson);
|
||||||
|
|
||||||
loadMore = response.responseJson.length > 0;
|
loadMore = response.responseJson.length > 0;
|
||||||
|
@ -96,26 +81,27 @@ class MattermostClient {
|
||||||
throw Error("Already logged in on this server");
|
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");
|
const token = response.getResponseHeader("Token");
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw Error("No Token header in response to log in request");
|
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;
|
this.token = token;
|
||||||
|
let _ = this.getUsers();
|
||||||
return response.responseJson;
|
return response.responseJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
async logOut() {
|
async logOut() {
|
||||||
assert(this.token, "logged in");
|
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
|
// Verify that the token is now invalidated
|
||||||
if (await this.loggedIn()) {
|
if (await this.loggedIn()) {
|
||||||
throw new Error("Failed to log out: token still works after trying to log out");
|
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;
|
this.token = null;
|
||||||
return response.responseJson;
|
return response.responseJson;
|
||||||
}
|
}
|
||||||
|
@ -141,8 +127,18 @@ class MattermostClient {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
users() {
|
getUsers() {
|
||||||
return this.authedPaginatedGet("/users", {});
|
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) {
|
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};
|
||||||
|
|
||||||
|
})();
|
|
@ -20,9 +20,7 @@ function populateServerSelectionList() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function populateChannelList() {
|
function populateChannelList() {
|
||||||
async function addChannelItems(endpoint) {
|
async function addChannelItems(client) {
|
||||||
const client = createClient(endpoint);
|
|
||||||
|
|
||||||
const teams = await client.myTeams();
|
const teams = await client.myTeams();
|
||||||
|
|
||||||
for (let team of teams) {
|
for (let team of teams) {
|
||||||
|
@ -47,25 +45,16 @@ function populateChannelList() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const servers = localstorage_credentials.getServers();
|
|
||||||
|
|
||||||
byId("channel_list").innerHTML = "";
|
byId("channel_list").innerHTML = "";
|
||||||
for (let server of servers) {
|
const endpoints = localstorage_credentials.getServers().map(server => server.endpoint);
|
||||||
addChannelItems(server.endpoint);
|
for (let client of mm_client.getMultiple(endpoints)) {
|
||||||
|
addChannelItems(client);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function populateChannelContents(client, contents) {
|
async function createMessageElement(client, post, lastTime, lastAuthor) {
|
||||||
byId("channel_contents").innerHTML = "";
|
const users = await client.getUsers();
|
||||||
|
|
||||||
let lastAuthor = null;
|
|
||||||
let lastTime = null;
|
|
||||||
|
|
||||||
let nodes = [];
|
|
||||||
for (let id of contents.order) {
|
|
||||||
const post = contents.posts[id];
|
|
||||||
|
|
||||||
const isThreadReply = !!post.parent_id;
|
const isThreadReply = !!post.parent_id;
|
||||||
|
|
||||||
const messageDiv = document.createElement("div");
|
const messageDiv = document.createElement("div");
|
||||||
|
@ -96,7 +85,7 @@ function populateChannelContents(client, contents) {
|
||||||
}
|
}
|
||||||
lastAuthor = post.user_id;
|
lastAuthor = post.user_id;
|
||||||
|
|
||||||
postDiv.dataset["id"] = id;
|
postDiv.dataset["id"] = post.id;
|
||||||
postDiv.appendChild(authorDiv);
|
postDiv.appendChild(authorDiv);
|
||||||
postDiv.appendChild(createAtDiv);
|
postDiv.appendChild(createAtDiv);
|
||||||
postDiv.appendChild(messageDiv);
|
postDiv.appendChild(messageDiv);
|
||||||
|
@ -125,14 +114,49 @@ function populateChannelContents(client, contents) {
|
||||||
postDiv.appendChild(attachmentsUl);
|
postDiv.appendChild(attachmentsUl);
|
||||||
}
|
}
|
||||||
|
|
||||||
nodes.push(postDiv);
|
return {postDiv, lastTime, lastAuthor};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function populateChannelContents(client, channel, contents) {
|
||||||
|
byId("channel_contents").innerHTML = "";
|
||||||
|
|
||||||
|
let result = {lastAuthor: null, lastTime: null, postDiv: null};
|
||||||
|
|
||||||
|
let nodes = [];
|
||||||
|
for (let id of contents.order) {
|
||||||
|
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);
|
byId("channel_contents").append(...nodes);
|
||||||
scrollToBottom();
|
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() {
|
function scrollToBottom() {
|
||||||
const el = byId("channel_contents_wrapper");
|
const el = byId("channel_contents_wrapper");
|
||||||
el.scrollTop = el.scrollHeight;
|
el.scrollTop = el.scrollHeight;
|
||||||
|
@ -146,11 +170,23 @@ function updateComposeHeight() {
|
||||||
}
|
}
|
||||||
byId("compose").addEventListener("input", updateComposeHeight);
|
byId("compose").addEventListener("input", updateComposeHeight);
|
||||||
|
|
||||||
function checkScrolledToBottom() {
|
function isScrolledToBottom() {
|
||||||
const el = byId("channel_contents_wrapper");
|
const el = byId("channel_contents_wrapper");
|
||||||
|
|
||||||
const scrolledTo = el.clientHeight + el.scrollTop;
|
const scrolledTo = el.clientHeight + el.scrollTop;
|
||||||
const atBottom = scrolledTo >= el.scrollHeight;
|
return scrolledTo >= el.scrollHeight;
|
||||||
el.className = atBottom ? "" : "not-at-bottom";
|
}
|
||||||
|
function checkScrolledToBottom() {
|
||||||
|
byId("channel_contents_wrapper").className = isScrolledToBottom() ? "" : "not-at-bottom";
|
||||||
}
|
}
|
||||||
byId("channel_contents_wrapper").addEventListener("scroll", checkScrolledToBottom);
|
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