Add chat webapp layout

This commit is contained in:
Midgard 2020-03-29 18:54:49 +02:00
parent 0d1771cbca
commit ee96bac928
Signed by: midgard
GPG key ID: 511C112F1331BBB4
3 changed files with 281 additions and 58 deletions

View file

@ -1,23 +1,46 @@
html, body {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
}
body { body {
font-family: sans-serif; font-family: sans-serif;
background-color: #eee; background-color: #eee;
color: #222; color: #222;
max-width: 768px;
margin: 0 auto;
line-height: 1.5; line-height: 1.5;
display: flex;
} }
input, button { a button {
color: #222;
}
* {
box-sizing: border-box;
}
input, button, textarea {
font: inherit; font: inherit;
color: inherit; color: inherit;
vertical-align: middle;
margin: 0;
} }
button { button {
padding: 0 0.5em;
cursor: pointer; cursor: pointer;
font-size: 90%;
font-variant: all-small-caps;
background-color: #fff;
border: 1px solid #aaa;
} }
h1 { h1 {
text-align: center; text-align: center;
line-height: 1.2; line-height: 1.2;
margin: 0;
} }
h1 img { h1 img {
width: 1em; width: 1em;
@ -25,6 +48,117 @@ h1 img {
vertical-align: middle; vertical-align: middle;
} }
.sidebar, .main-area {
height: 100%;
overflow-y: hidden;
display: grid;
}
.sidebar {
width: 300px;
overflow: hidden;
grid-template-rows: auto 1fr;
border-right: 1px solid #aaa;
}
.main-area {
flex-grow: 1;
grid-template-rows: auto 1fr auto;
position: relative;
}
.sidebar-head {
background-color: #e5e5e5;
padding: 0.5em;
border-bottom: 1px solid #aaa;
}
#channel_list {
padding: 0;
height: 100%;
overflow-x: hidden;
overflow-y: scroll;
}
#channel_list a {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
width: 300px;
display: block;
padding: 2px 10px;
color: #333;
text-decoration: none;
}
#channel_list a:hover {
background-color: #e5e5e5;
}
#channel_list .active a {
background-color: #ddd;
}
#channel_list :first-child a {
padding-top: 5px;
}
#channel_list :last-child a {
padding-bottom: 5px;
}
#channel_list .team-name {
color: #888;
}
#channel_list .separator {
color: #888;
margin: 0 1px;
}
ul#channel_list, ul#server_selection_list {
list-style: none;
margin: 0;
}
ul#server_selection_list {
padding-left: 0;
font-size: 90%;
}
.centered {
width: 100%;
max-width: 700px;
margin: 0 auto;
position: relative;
}
#channel_header {
box-sizing: content-box;
min-height: 1em;
padding: 10px;
border-bottom: 1px solid #aaa;
background-color: #e5e5e5;
}
#channel_header .team-name {
color: #888;
}
#channel_header .separator {
color: #888;
margin: 0 3px;
}
.channel-contents-wrapper {
overflow-x: hidden;
overflow-y: scroll;
margin-left: 12px;
height: 100%;
}
#channel_contents {
text-rendering: optimizeLegibility;
width: 100%;
height: 100%;
padding: 0 10px;
}
.compose-wrapper {
padding: 0 5px 8px;
}
#compose {
width: 100%;
resize: none;
overflow: hidden;
}
#login { #login {
display: none; display: none;
text-align: center; text-align: center;
@ -49,6 +183,12 @@ h1 img {
grid-template-rows: auto auto; grid-template-rows: auto auto;
grid-template-columns: auto auto; grid-template-columns: auto auto;
} }
.post:first-child {
margin-top: 8px;
}
.post:last-child {
margin-bottom: 8px;
}
.post .author { .post .author {
grid-row: 1; grid-row: 1;

View file

@ -11,39 +11,63 @@
<p>This application cannot work without JavaScript, unfortunately. In order to proceed, please enable it for this website.</p> <p>This application cannot work without JavaScript, unfortunately. In order to proceed, please enable it for this website.</p>
<p>Feathermost is an alternative webclient for the Mattermost chat platform. It is not possible to provide a JavaScript-less experience without having to trust us with your user data.</p> <p>Feathermost is an alternative webclient for the Mattermost chat platform. It is not possible to provide a JavaScript-less experience without having to trust us with your user data.</p>
</div> </div>
<script type="text/javascript">document.body.innerHTML = "";</script> <script type="text/javascript">
document.body.innerHTML = "";
document.body.className = "yesscript";
</script>
<h1><img src="/assets/feathermost.svg" alt=""/> Feathermost</h1> <div class="sidebar">
<div class="sidebar-head">
<h1><img src="/assets/feathermost.svg" alt=""/> Feathermost</h1>
<div id="server_selection"> <div id="server_selection">
<ul id="server_selection_list"></ul> <ul id="server_selection_list"></ul>
<a href="#login"><button id="server_selection_add">Add a server</button></a> <a href="#login"><button id="server_selection_add">Add a server</button></a>
</div>
<div id="login">
<h2>Add a server</h2>
<table>
<tr id="login_server_row">
<th>Server</th>
<td><input type="text" id="login_server" value="http://localhost:8080" required/></td>
</tr><tr>
<th>Email or username</th>
<td><input type="text" id="login_login_id" required/></td>
</tr><tr>
<th>Password</th>
<td><input type="password" id="login_password" required/></td>
</tr>
</table>
<button id="login_button">Log in</button>
<a href="#"><button id="server_selection_add">Cancel</button></a>
<div id="login_message"></div>
</div>
</div>
<ul id="channel_list"></ul>
</div> </div>
<div id="login"> <div class="main-area">
<h2>Add a server</h2> <div id="channel_header"></div>
<table>
<tr id="login_server_row">
<th>Server</th>
<td><input type="text" id="login_server" value="http://localhost:8080" required/></td>
</tr><tr>
<th>Email or username</th>
<td><input type="text" id="login_login_id" required/></td>
</tr><tr>
<th>Password</th>
<td><input type="password" id="login_password" required/></td>
</tr>
</table>
<button id="login_button">Log in</button> <div class="channel-contents-wrapper">
<a href="#"><button id="server_selection_add">Cancel</button></a> <div class="centered" id="channel_contents">
<div id="login_message"></div>
<div style="text-align: center; width: 100%; height: 100%; display: flex;
align-items: center; justify-content: center; color: #aaa">
<div>← Select a channel in the sidebar to read it</div>
</div>
</div>
</div>
<div class="centered compose-wrapper">
<textarea id="compose" disabled="disabled" oninput="" rows="1"></textarea>
</div>
</div> </div>
<ul id="channel_list"></ul>
<div id="channel_contents"></div>
<script type="text/javascript" src="/ajax.js"></script> <script type="text/javascript" src="/ajax.js"></script>
<script type="text/javascript" src="/main.js"></script> <script type="text/javascript" src="/main.js"></script>
</body> </body>

117
main.js
View file

@ -222,25 +222,15 @@ function populateChannelList() {
const li = document.createElement("li"); const li = document.createElement("li");
const a = document.createElement("a"); const a = document.createElement("a");
a.href = "javascript:void(0)"; a.href = "javascript:void(0)";
switch (channel.type) { const titleAndElements = channelNameElements(team, channel);
case "O": // Public channel if (!titleAndElements) continue;
a.innerText = `${team.name}/${channel.name}`; a.title = titleAndElements[0];
break; a.append(...titleAndElements[1]);
case "P": // Private channel
a.innerText = `🔒 ${team.name}/${channel.name}`; a.addEventListener("click", () => switchToChannel(client, team, channel));
break;
case "D": // Direct message
a.innerText = `👤 ...`;
break;
case "G": // Group chat
a.innerText = `👥 ${channel.display_name}`;
break;
default: // Unsupported
a.innerText = `${channel.type} ${team.name}/${channel.name}`;
break;
}
a.addEventListener("click", () => switchToChannel(client, team.id, channel.id));
li.appendChild(a); li.appendChild(a);
li.dataset["id"] = channel.id;
nodes.push(li); nodes.push(li);
} }
byId("channel_list").append(...nodes); byId("channel_list").append(...nodes);
@ -281,7 +271,7 @@ function populateChannelContents(contents) {
const postDiv = document.createElement("div"); const postDiv = document.createElement("div");
postDiv.className = "post"; postDiv.className = "post";
postDiv.dataset["postid"] = id; postDiv.dataset["id"] = id;
postDiv.appendChild(authorDiv); postDiv.appendChild(authorDiv);
postDiv.appendChild(createAtDiv); postDiv.appendChild(createAtDiv);
postDiv.appendChild(messageDiv); postDiv.appendChild(messageDiv);
@ -333,20 +323,89 @@ function logOut(endpoint, button) {
}); });
} }
function switchToChannel(client, team_id, channel_id) { function channelNameElements(team, channel) {
byId("channel_contents").innerText = "Loading…"; let icon = "";
window.location = "#channel_contents"; let teamName = team.name;
let channelName = channel.name;
let title = "";
client.channelPosts(channel_id) switch (channel.type) {
case "O": // Public channel
title = `${channel.name} in team ${team.name} (public)`;
break;
case "P": // Private channel
icon = "🔒";
title = `${channel.name} in team ${team.name} (private)`;
break;
case "D": // Direct message
return undefined;
teamName = "";
channelName = `👤 ...`;
title = `Direct message`;
break;
case "G": // Group chat
teamName = "";
channelName = `👥 ${channel.display_name}`;
title = `Group chat with ${channel.display_name}`;
break;
default: // Unsupported
icon = channel.type;
title = `${channel.name} in team ${team.name} (type ${channel.type})`;
break;
}
let elements = [];
if (teamName) {
const teamElement = document.createElement("span");
teamElement.className = "team-name";
teamElement.innerText = teamName;
elements.push(teamElement);
const separatorElement = document.createElement("span");
separatorElement.className = "separator";
separatorElement.innerText = "/";
elements.push(separatorElement);
}
const channelElement = document.createElement("span");
channelElement.className = "channel-name";
channelElement.innerText = channelName;
elements.push(channelElement);
return [title, elements];
}
function switchToChannel(client, team, channel) {
for (let el of byId("channel_list").childNodes) {
if (el.dataset["id"] == channel.id) {
el.className = "active";
} else {
el.className = "";
}
}
const [title, elements] = channelNameElements(team, channel);
byId("channel_header").innerHTML = "";
byId("channel_header").append(...elements);
byId("channel_contents").innerText = "Loading…";
client.channelPosts(channel.id)
.then(response => { .then(response => {
console.info(`Got channel contents of ${channel_id}`); console.info(`Got channel contents of ${channel.id} (${channel.name})`);
populateChannelContents(response); populateChannelContents(response);
window.location = "#channel_contents"; })
//}) .catch(error => {
//.catch(error => { console.error(error);
//console.error(error); byId("channel_contents").innerText = `Failed to get channel contents:\n${error.message}`;
//byId("channel_contents").innerText = `Failed to get channel contents:\n${error.message}`;
}); });
} }
function updateComposeHeight() {
byId("compose").style.height = "";
byId("compose").style.height = (byId("compose").scrollHeight + 1) + "px";
}
byId("compose").addEventListener("input", updateComposeHeight);
updateComposeHeight();
byId("login_button").addEventListener("click", logIn); byId("login_button").addEventListener("click", logIn);