From 84eaae9fa908f9cfab93a4bfc88267bbe9cefeb4 Mon Sep 17 00:00:00 2001 From: Midgard Date: Sun, 7 Feb 2021 21:10:36 +0100 Subject: [PATCH] Add attachments and author names --- assets/main.css | 8 ++++- js/ajax.js | 8 ++++- js/controller.js | 2 +- js/localstorage_credentials.js | 2 +- js/main.js | 19 +++++++++++- js/mm_client.js | 55 +++++++++++++++++++++++++++++----- js/util.js | 10 +++++++ js/view/view.js | 49 +++++++++++++++++++++++++----- 8 files changed, 133 insertions(+), 20 deletions(-) diff --git a/assets/main.css b/assets/main.css index f00c603..3669db7 100644 --- a/assets/main.css +++ b/assets/main.css @@ -214,7 +214,10 @@ ul#server_selection_list { .post .author { grid-row: 1; grid-column: 1; - color: #888; + font-size: 80%; +} +.post.same_author .author { + display: none; } .post .create_at { @@ -223,6 +226,9 @@ ul#server_selection_list { grid-column: 2; color: #888; } +.post.same_author .create_at { + display: none; +} .post .message { grid-row: 2; diff --git a/js/ajax.js b/js/ajax.js index 8f83977..1278319 100644 --- a/js/ajax.js +++ b/js/ajax.js @@ -60,6 +60,9 @@ function xhrParseJsonResponse(xhr) { } function withParams(url, queryParams) { + console.debug(url, queryParams); + if (!queryParams) return url; + let sep = "?"; for (let paramName of Object.getOwnPropertyNames(queryParams)) { const paramValue = queryParams[paramName]; @@ -70,6 +73,9 @@ function withParams(url, queryParams) { } sep = "&"; } + + console.debug(url); + return url; } function getJson(url, options={}) { @@ -80,7 +86,7 @@ function getJson(url, options={}) { const urlWithParams = withParams(url, options.queryParams); 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(); }).then(xhrParseJsonResponse); } diff --git a/js/controller.js b/js/controller.js index 8d2fd61..626bbad 100644 --- a/js/controller.js +++ b/js/controller.js @@ -128,7 +128,7 @@ function switchToChannel(client, team, channel) { client.channelPosts(channel.id) .then(response => { console.info(`Got channel contents of ${channel.id} (${channel.name})`); - populateChannelContents(response); + populateChannelContents(client, response); }) .catch(error => { console.error(error); diff --git a/js/localstorage_credentials.js b/js/localstorage_credentials.js index 911fd6e..e0ed4d0 100644 --- a/js/localstorage_credentials.js +++ b/js/localstorage_credentials.js @@ -10,7 +10,7 @@ function key_for(endpoint) { return { getServers() { 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 matches = key.match(RE_SERVER_ITEM); if (matches) { diff --git a/js/main.js b/js/main.js index 9a73b48..d3973a0 100644 --- a/js/main.js +++ b/js/main.js @@ -6,5 +6,22 @@ byId("login_no_button").addEventListener("click", e => { e.stopPropagation(); e. updateComposeHeight(); checkScrolledToBottom(); + 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(); +}); diff --git a/js/mm_client.js b/js/mm_client.js index 41f2741..a85b76c 100644 --- a/js/mm_client.js +++ b/js/mm_client.js @@ -10,6 +10,10 @@ class MattermostApi { return this._endpoint; } + get endpoint() { + return this._endpoint; + } + async get(path, token, queryParams) { const headers = token ? {"Authorization": `Bearer ${token}`} : {}; 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"}`); } - async authenticatedGet(url, queryParams) { + async authedGet(url, queryParams) { assert(this.token, "logged in"); const response = await this.api.get(url, this.token, queryParams); 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() { if (!this.token) { return false; @@ -62,7 +92,7 @@ class MattermostClient { } async logIn(login_id, password) { - if (this.token && await this.tokenWorks()) { + if (await this.loggedIn()) { throw Error("Already logged in on this server"); } @@ -81,7 +111,7 @@ class MattermostClient { const response = await this.api.post("/users/logout", this.token); // 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"); } @@ -92,24 +122,33 @@ class MattermostClient { user(user_id) { assertIsMattermostId(user_id); - return this.authenticatedGet(`/users/${user_id}`); + return this.authedGet(`/users/${user_id}`); } - userMe() { return this.authenticatedGet("/users/me"); } - myTeams() { return this.authenticatedGet("/users/me/teams"); } + userMe() { return this.authedGet("/users/me"); } + myTeams() { return this.authedGet("/users/me/teams"); } myChannels(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) { assertIsMattermostId(channel_id); assertIsNullOrMattermostId(beforePost); assertIsNullOrMattermostId(afterPost); - return this.authenticatedGet(`/channels/${channel_id}/posts`, { + return this.authedGet(`/channels/${channel_id}/posts`, { 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}; diff --git a/js/util.js b/js/util.js index 02284cd..e0f18b8 100644 --- a/js/util.js +++ b/js/util.js @@ -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 {} /** * Throw an AssertionError if the first argument is not true diff --git a/js/view/view.js b/js/view/view.js index f263ff6..9600084 100644 --- a/js/view/view.js +++ b/js/view/view.js @@ -24,12 +24,10 @@ function populateChannelList() { const client = createClient(endpoint); const teams = await client.myTeams(); - console.log(teams); for (let team of teams) { let nodes = []; const channels = await client.myChannels(team.id); - console.log(channels); for (let channel of channels) { const li = document.createElement("li"); const a = document.createElement("a"); @@ -58,9 +56,10 @@ function populateChannelList() { } -function populateChannelContents(contents) { +function populateChannelContents(client, contents) { byId("channel_contents").innerHTML = ""; + let lastAuthor = null; let nodes = []; for (let id of contents.order) { const post = contents.posts[id]; @@ -77,22 +76,59 @@ function populateChannelContents(contents) { createAtDiv.innerText = createAt.toLocaleString("nl-BE"); 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 = `Auteur: ${post.user_id}`; + 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.unshift(postDiv); } 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 scrolledTo = el.clientHeight + el.scrollTop; - const scrollHeight = el.scrollHeight - const atBottom = scrolledTo >= scrollHeight; + const atBottom = scrolledTo >= el.scrollHeight; el.className = atBottom ? "" : "not-at-bottom"; } byId("channel_contents_wrapper").addEventListener("scroll", checkScrolledToBottom);