Add chat webapp layout
This commit is contained in:
parent
0d1771cbca
commit
ee96bac928
3 changed files with 281 additions and 58 deletions
146
assets/main.css
146
assets/main.css
|
@ -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;
|
||||||
|
|
28
index.html
28
index.html
|
@ -11,8 +11,13 @@
|
||||||
<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>
|
||||||
|
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="sidebar-head">
|
||||||
<h1><img src="/assets/feathermost.svg" alt=""/> Feathermost</h1>
|
<h1><img src="/assets/feathermost.svg" alt=""/> Feathermost</h1>
|
||||||
|
|
||||||
<div id="server_selection">
|
<div id="server_selection">
|
||||||
|
@ -39,10 +44,29 @@
|
||||||
<a href="#"><button id="server_selection_add">Cancel</button></a>
|
<a href="#"><button id="server_selection_add">Cancel</button></a>
|
||||||
<div id="login_message"></div>
|
<div id="login_message"></div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ul id="channel_list"></ul>
|
<ul id="channel_list"></ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="channel_contents"></div>
|
<div class="main-area">
|
||||||
|
<div id="channel_header"></div>
|
||||||
|
|
||||||
|
<div class="channel-contents-wrapper">
|
||||||
|
<div class="centered" id="channel_contents">
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
<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>
|
||||||
|
|
117
main.js
117
main.js
|
@ -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);
|
||||||
|
|
Loading…
Reference in a new issue