Add attachments and author names

This commit is contained in:
Midgard 2021-02-07 21:10:36 +01:00
parent c0fbb51ddf
commit 84eaae9fa9
Signed by: midgard
GPG key ID: 511C112F1331BBB4
8 changed files with 133 additions and 20 deletions

View file

@ -214,7 +214,10 @@ ul#server_selection_list {
.post .author { .post .author {
grid-row: 1; grid-row: 1;
grid-column: 1; grid-column: 1;
color: #888; font-size: 80%;
}
.post.same_author .author {
display: none;
} }
.post .create_at { .post .create_at {
@ -223,6 +226,9 @@ ul#server_selection_list {
grid-column: 2; grid-column: 2;
color: #888; color: #888;
} }
.post.same_author .create_at {
display: none;
}
.post .message { .post .message {
grid-row: 2; grid-row: 2;

View file

@ -60,6 +60,9 @@ function xhrParseJsonResponse(xhr) {
} }
function withParams(url, queryParams) { function withParams(url, queryParams) {
console.debug(url, queryParams);
if (!queryParams) return url;
let sep = "?"; let sep = "?";
for (let paramName of Object.getOwnPropertyNames(queryParams)) { for (let paramName of Object.getOwnPropertyNames(queryParams)) {
const paramValue = queryParams[paramName]; const paramValue = queryParams[paramName];
@ -70,6 +73,9 @@ function withParams(url, queryParams) {
} }
sep = "&"; sep = "&";
} }
console.debug(url);
return url;
} }
function getJson(url, options={}) { function getJson(url, options={}) {
@ -80,7 +86,7 @@ function getJson(url, options={}) {
const urlWithParams = withParams(url, options.queryParams); const urlWithParams = withParams(url, options.queryParams);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let xhr = xhrInitForPromise(resolve, reject, url, "GET", options.headers); let xhr = xhrInitForPromise(resolve, reject, urlWithParams, "GET", options.headers);
xhr.send(); xhr.send();
}).then(xhrParseJsonResponse); }).then(xhrParseJsonResponse);
} }

View file

@ -128,7 +128,7 @@ function switchToChannel(client, team, channel) {
client.channelPosts(channel.id) client.channelPosts(channel.id)
.then(response => { .then(response => {
console.info(`Got channel contents of ${channel.id} (${channel.name})`); console.info(`Got channel contents of ${channel.id} (${channel.name})`);
populateChannelContents(response); populateChannelContents(client, response);
}) })
.catch(error => { .catch(error => {
console.error(error); console.error(error);

View file

@ -10,7 +10,7 @@ function key_for(endpoint) {
return { return {
getServers() { getServers() {
let servers = []; let servers = [];
for (var i = 0; i < window.localStorage.length; i++) { for (let i = 0; i < window.localStorage.length; i++) {
const key = window.localStorage.key(i); const key = window.localStorage.key(i);
const matches = key.match(RE_SERVER_ITEM); const matches = key.match(RE_SERVER_ITEM);
if (matches) { if (matches) {

View file

@ -6,5 +6,22 @@ byId("login_no_button").addEventListener("click", e => { e.stopPropagation(); e.
updateComposeHeight(); updateComposeHeight();
checkScrolledToBottom(); checkScrolledToBottom();
populateServerSelectionList(); populateServerSelectionList();
populateChannelList();
function fetchUsernames(endpoint) {
console.debug(endpoint);
const client = createClient(endpoint).users();
return client;
}
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();
});

View file

@ -10,6 +10,10 @@ class MattermostApi {
return this._endpoint; return this._endpoint;
} }
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});
@ -39,12 +43,38 @@ class MattermostClient {
console.info(`Created MattermostClient for ${this.api.id}, ${this.token ? "found token" : "did not find token"}`); console.info(`Created MattermostClient for ${this.api.id}, ${this.token ? "found token" : "did not find token"}`);
} }
async authenticatedGet(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.api.get(url, this.token, queryParams);
return response.responseJson; return response.responseJson;
} }
async authedPaginatedGet(url, queryParams, perPage=200) {
assert(this.token, "logged in");
let data = [];
let params = {page: 0, per_page: perPage};
if (queryParams) {
extend(params, queryParams);
}
let loadMore = true;
while (loadMore) {
if (params.page > 100) {
throw new Error("Requesting more than 100 pages, something looks wrong");
}
const response = await this.api.get(url, this.token, params);
console.log(response);
data = data.concat(response.responseJson);
loadMore = response.responseJson.length > 0;
params.page++;
}
return data;
}
async loggedIn() { async loggedIn() {
if (!this.token) { if (!this.token) {
return false; return false;
@ -62,7 +92,7 @@ class MattermostClient {
} }
async logIn(login_id, password) { async logIn(login_id, password) {
if (this.token && await this.tokenWorks()) { if (await this.loggedIn()) {
throw Error("Already logged in on this server"); throw Error("Already logged in on this server");
} }
@ -81,7 +111,7 @@ class MattermostClient {
const response = await this.api.post("/users/logout", this.token); const response = await this.api.post("/users/logout", this.token);
// Verify that the token is now invalidated // Verify that the token is now invalidated
if (await 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");
} }
@ -92,24 +122,33 @@ class MattermostClient {
user(user_id) { user(user_id) {
assertIsMattermostId(user_id); assertIsMattermostId(user_id);
return this.authenticatedGet(`/users/${user_id}`); return this.authedGet(`/users/${user_id}`);
} }
userMe() { return this.authenticatedGet("/users/me"); } userMe() { return this.authedGet("/users/me"); }
myTeams() { return this.authenticatedGet("/users/me/teams"); } myTeams() { return this.authedGet("/users/me/teams"); }
myChannels(team_id) { myChannels(team_id) {
assertIsMattermostId(team_id); assertIsMattermostId(team_id);
return this.authenticatedGet(`/users/me/teams/${team_id}/channels`); return this.authedGet(`/users/me/teams/${team_id}/channels`);
} }
channelPosts(channel_id, beforePost=null, afterPost=null, since=null) { channelPosts(channel_id, beforePost=null, afterPost=null, since=null) {
assertIsMattermostId(channel_id); assertIsMattermostId(channel_id);
assertIsNullOrMattermostId(beforePost); assertIsNullOrMattermostId(beforePost);
assertIsNullOrMattermostId(afterPost); assertIsNullOrMattermostId(afterPost);
return this.authenticatedGet(`/channels/${channel_id}/posts`, { return this.authedGet(`/channels/${channel_id}/posts`, {
before: beforePost, after: afterPost, since before: beforePost, after: afterPost, since
}); });
} }
users() {
return this.authedPaginatedGet("/users", {});
}
async filePublicLink(file_id) {
const response = await this.authedGet(`/files/${file_id}/link`, {});
return response.link;
}
} }
return {MattermostApi, MattermostClient}; return {MattermostApi, MattermostClient};

View file

@ -45,6 +45,16 @@ function thisToArg(f) {
} }
/**
* Extend an object in-place with own properties of a second one
*/
function extend(obj1, obj2) {
for (let key in Object.getOwnPropertyNames(obj1)) {
obj1[key] = obj2[key];
}
}
class AssertionError extends Error {} class AssertionError extends Error {}
/** /**
* Throw an AssertionError if the first argument is not true * Throw an AssertionError if the first argument is not true

View file

@ -24,12 +24,10 @@ function populateChannelList() {
const client = createClient(endpoint); const client = createClient(endpoint);
const teams = await client.myTeams(); const teams = await client.myTeams();
console.log(teams);
for (let team of teams) { for (let team of teams) {
let nodes = []; let nodes = [];
const channels = await client.myChannels(team.id); const channels = await client.myChannels(team.id);
console.log(channels);
for (let channel of channels) { for (let channel of channels) {
const li = document.createElement("li"); const li = document.createElement("li");
const a = document.createElement("a"); const a = document.createElement("a");
@ -58,9 +56,10 @@ function populateChannelList() {
} }
function populateChannelContents(contents) { function populateChannelContents(client, contents) {
byId("channel_contents").innerHTML = ""; byId("channel_contents").innerHTML = "";
let lastAuthor = null;
let nodes = []; let nodes = [];
for (let id of contents.order) { for (let id of contents.order) {
const post = contents.posts[id]; const post = contents.posts[id];
@ -77,22 +76,59 @@ function populateChannelContents(contents) {
createAtDiv.innerText = createAt.toLocaleString("nl-BE"); createAtDiv.innerText = createAt.toLocaleString("nl-BE");
createAtDiv.dateTime = createAt.toISOString(); createAtDiv.dateTime = createAt.toISOString();
const authorName = users[post.user_id] ? users[post.user_id].username : post.user_id;
const authorDiv = document.createElement("div"); const authorDiv = document.createElement("div");
authorDiv.className = "author"; authorDiv.className = "author";
authorDiv.innerText = `Auteur: ${post.user_id}`; authorDiv.innerText = `@${authorName}`;
const postDiv = document.createElement("div"); const postDiv = document.createElement("div");
postDiv.className = "post"; postDiv.className = "post";
if (lastAuthor === post.user_id) {
postDiv.className += " same_author";
}
lastAuthor = post.user_id;
postDiv.dataset["id"] = id; postDiv.dataset["id"] = id;
postDiv.appendChild(authorDiv); postDiv.appendChild(authorDiv);
postDiv.appendChild(createAtDiv); postDiv.appendChild(createAtDiv);
postDiv.appendChild(messageDiv); 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.unshift(postDiv); nodes.unshift(postDiv);
} }
byId("channel_contents").append(...nodes); byId("channel_contents").append(...nodes);
checkScrolledToBottom(); scrollToBottom();
}
function scrollToBottom() {
const el = byId("channel_contents_wrapper");
el.scrollTop = el.scrollHeight;
el.className = "";
} }
@ -106,8 +142,7 @@ function checkScrolledToBottom() {
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 scrollHeight = el.scrollHeight const atBottom = scrolledTo >= el.scrollHeight;
const atBottom = scrolledTo >= scrollHeight;
el.className = atBottom ? "" : "not-at-bottom"; el.className = atBottom ? "" : "not-at-bottom";
} }
byId("channel_contents_wrapper").addEventListener("scroll", checkScrolledToBottom); byId("channel_contents_wrapper").addEventListener("scroll", checkScrolledToBottom);