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 {
|
||||
font-family: sans-serif;
|
||||
background-color: #eee;
|
||||
color: #222;
|
||||
max-width: 768px;
|
||||
margin: 0 auto;
|
||||
line-height: 1.5;
|
||||
|
||||
display: flex;
|
||||
}
|
||||
|
||||
input, button {
|
||||
a button {
|
||||
color: #222;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
input, button, textarea {
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
vertical-align: middle;
|
||||
margin: 0;
|
||||
}
|
||||
button {
|
||||
padding: 0 0.5em;
|
||||
cursor: pointer;
|
||||
font-size: 90%;
|
||||
font-variant: all-small-caps;
|
||||
background-color: #fff;
|
||||
border: 1px solid #aaa;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
}
|
||||
h1 img {
|
||||
width: 1em;
|
||||
|
@ -25,6 +48,117 @@ h1 img {
|
|||
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 {
|
||||
display: none;
|
||||
text-align: center;
|
||||
|
@ -49,6 +183,12 @@ h1 img {
|
|||
grid-template-rows: auto auto;
|
||||
grid-template-columns: auto auto;
|
||||
}
|
||||
.post:first-child {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.post:last-child {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.post .author {
|
||||
grid-row: 1;
|
||||
|
|
76
index.html
76
index.html
|
@ -11,39 +11,63 @@
|
|||
<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>
|
||||
</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">
|
||||
<ul id="server_selection_list"></ul>
|
||||
<a href="#login"><button id="server_selection_add">Add a server</button></a>
|
||||
<div id="server_selection">
|
||||
<ul id="server_selection_list"></ul>
|
||||
<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 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>
|
||||
<div class="main-area">
|
||||
<div id="channel_header"></div>
|
||||
|
||||
<button id="login_button">Log in</button>
|
||||
<a href="#"><button id="server_selection_add">Cancel</button></a>
|
||||
<div id="login_message"></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>
|
||||
|
||||
<ul id="channel_list"></ul>
|
||||
|
||||
<div id="channel_contents"></div>
|
||||
|
||||
<script type="text/javascript" src="/ajax.js"></script>
|
||||
<script type="text/javascript" src="/main.js"></script>
|
||||
</body>
|
||||
|
|
117
main.js
117
main.js
|
@ -222,25 +222,15 @@ function populateChannelList() {
|
|||
const li = document.createElement("li");
|
||||
const a = document.createElement("a");
|
||||
a.href = "javascript:void(0)";
|
||||
switch (channel.type) {
|
||||
case "O": // Public channel
|
||||
a.innerText = `${team.name}/${channel.name}`;
|
||||
break;
|
||||
case "P": // Private channel
|
||||
a.innerText = `🔒 ${team.name}/${channel.name}`;
|
||||
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));
|
||||
const titleAndElements = channelNameElements(team, channel);
|
||||
if (!titleAndElements) continue;
|
||||
a.title = titleAndElements[0];
|
||||
a.append(...titleAndElements[1]);
|
||||
|
||||
a.addEventListener("click", () => switchToChannel(client, team, channel));
|
||||
|
||||
li.appendChild(a);
|
||||
li.dataset["id"] = channel.id;
|
||||
nodes.push(li);
|
||||
}
|
||||
byId("channel_list").append(...nodes);
|
||||
|
@ -281,7 +271,7 @@ function populateChannelContents(contents) {
|
|||
|
||||
const postDiv = document.createElement("div");
|
||||
postDiv.className = "post";
|
||||
postDiv.dataset["postid"] = id;
|
||||
postDiv.dataset["id"] = id;
|
||||
postDiv.appendChild(authorDiv);
|
||||
postDiv.appendChild(createAtDiv);
|
||||
postDiv.appendChild(messageDiv);
|
||||
|
@ -333,20 +323,89 @@ function logOut(endpoint, button) {
|
|||
});
|
||||
}
|
||||
|
||||
function switchToChannel(client, team_id, channel_id) {
|
||||
byId("channel_contents").innerText = "Loading…";
|
||||
window.location = "#channel_contents";
|
||||
function channelNameElements(team, channel) {
|
||||
let icon = "";
|
||||
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 => {
|
||||
console.info(`Got channel contents of ${channel_id}`);
|
||||
console.info(`Got channel contents of ${channel.id} (${channel.name})`);
|
||||
populateChannelContents(response);
|
||||
window.location = "#channel_contents";
|
||||
//})
|
||||
//.catch(error => {
|
||||
//console.error(error);
|
||||
//byId("channel_contents").innerText = `Failed to get channel contents:\n${error.message}`;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
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);
|
||||
|
|
Loading…
Reference in a new issue