Add attachments and author names
This commit is contained in:
parent
c0fbb51ddf
commit
84eaae9fa9
8 changed files with 133 additions and 20 deletions
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
17
js/main.js
17
js/main.js
|
@ -6,5 +6,22 @@ byId("login_no_button").addEventListener("click", e => { e.stopPropagation(); e.
|
||||||
|
|
||||||
updateComposeHeight();
|
updateComposeHeight();
|
||||||
checkScrolledToBottom();
|
checkScrolledToBottom();
|
||||||
|
|
||||||
populateServerSelectionList();
|
populateServerSelectionList();
|
||||||
|
|
||||||
|
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();
|
populateChannelList();
|
||||||
|
});
|
||||||
|
|
|
@ -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};
|
||||||
|
|
10
js/util.js
10
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 {}
|
class AssertionError extends Error {}
|
||||||
/**
|
/**
|
||||||
* Throw an AssertionError if the first argument is not true
|
* Throw an AssertionError if the first argument is not true
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in a new issue