619 lines
16 KiB
HTML
619 lines
16 KiB
HTML
<script>
|
|
document.addEventListener("topics_updated", ({ detail: { topics } }) => {
|
|
const topic_list = document.getElementById("topic-list");
|
|
topic_list.innerHTML = "";
|
|
for (const topic of topics) {
|
|
topic_list.insertAdjacentHTML(
|
|
"beforeend",
|
|
`<li id="topic-selector-${topic.id}" class="topic" data-topic-selector-for="${topic.id}"><a href="#/topic/${topic.id}/chat">${topic.name}</a></li>`,
|
|
);
|
|
}
|
|
});
|
|
|
|
function update_topic_indicators(event) {
|
|
document
|
|
.querySelectorAll("[data-topic-selector-for]")
|
|
.forEach((element) => element.classList.remove("active"));
|
|
|
|
const new_topic_id = event?.detail?.topic_id ?? document.body.dataset.topic;
|
|
|
|
if (!new_topic_id) {
|
|
return;
|
|
}
|
|
|
|
document
|
|
.querySelectorAll(`[data-topic-selector-for="${new_topic_id}"]`)
|
|
.forEach((element) => element.classList.add("active"));
|
|
}
|
|
|
|
document.addEventListener("topics_updated", update_topic_indicators);
|
|
document.addEventListener("topic_changed", update_topic_indicators);
|
|
document.addEventListener("user_logged_in", update_topic_indicators);
|
|
|
|
function clear_invite_popup() {
|
|
document.body.querySelectorAll(".invitepopover").forEach((element) => element.remove());
|
|
}
|
|
|
|
function generate_invite(click_event) {
|
|
click_event.preventDefault();
|
|
|
|
const button = click_event.target;
|
|
|
|
clear_invite_popup();
|
|
const invite_div = document.createElement("div");
|
|
invite_div.classList.add("invitepopover");
|
|
invite_div.innerHTML = `
|
|
<div class="icon close" onclick="clear_invite_popup()"></div>
|
|
<form>
|
|
<input name="code" type="text" placeholder="Custom code (optional)">
|
|
<button onclick="create_invite(event);">Generate</button>
|
|
</form>`;
|
|
|
|
document.body.appendChild(invite_div);
|
|
invite_div.style.left = button.getBoundingClientRect().left + "px";
|
|
invite_div.style.top = button.getBoundingClientRect().top + "px";
|
|
}
|
|
|
|
async function create_invite(click_event) {
|
|
click_event.preventDefault();
|
|
|
|
const button = click_event.target;
|
|
const invite_popover = document.body.querySelector(".invitepopover");
|
|
if (!invite_popover) {
|
|
alert("Unknown error, try again.");
|
|
return;
|
|
}
|
|
|
|
const user = document.body.dataset.user && JSON.parse(document.body.dataset.user);
|
|
if (!user) {
|
|
alert("You must be logged in.");
|
|
return;
|
|
}
|
|
|
|
const form = button.closest("form");
|
|
const code_input = form.querySelector('[name="code"]');
|
|
|
|
const invite_code_response = await api.fetch(`/api/users/${user.id}/invites`, {
|
|
method: "POST",
|
|
json: {
|
|
code: code_input.value,
|
|
},
|
|
});
|
|
|
|
if (!invite_code_response.ok) {
|
|
const error = await invite_code_response.json();
|
|
return alert(error?.error?.message ?? error?.errors?.[0]?.message ?? "Unknown error.");
|
|
}
|
|
|
|
const invite_code = await invite_code_response.json();
|
|
|
|
console.dir({
|
|
invite_code,
|
|
});
|
|
|
|
invite_popover.innerHTML = `
|
|
<div>
|
|
<div class="icon close" onclick="clear_invite_popup()"></div>
|
|
<div class="share-option">
|
|
<span class="name">Code</span>
|
|
<input readonly type="text" name="code" value="${invite_code.code}" />
|
|
<button onclick="navigator.clipboard.writeText('${invite_code.code}');" />Copy</button>
|
|
</div>
|
|
<div class="share-option">
|
|
<span class="name">Link</span>
|
|
<input readonly type="text" name="code" value="${window.location.protocol + "//" + window.location.host + "/?invite_code=" + encodeURIComponent(invite_code.code)}" />
|
|
<button onclick="navigator.clipboard.writeText('${window.location.protocol + "//" + window.location.host + "/?invite_code=" + encodeURIComponent(invite_code.code)}');" />Copy</button>
|
|
</div>
|
|
<button onclick="( () => document.querySelectorAll( '.invitepopover' ).forEach( (element) => element.remove() ) )()">Done</button>
|
|
</div>`;
|
|
}
|
|
</script>
|
|
|
|
<style type="text/css">
|
|
main {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
display: grid;
|
|
grid-template-columns: auto 1fr;
|
|
}
|
|
@media screen and (max-width: 1200px) {
|
|
main {
|
|
grid-template-columns: auto;
|
|
}
|
|
}
|
|
|
|
#sidebar {
|
|
z-index: 100;
|
|
background: var(--bg);
|
|
position: relative;
|
|
width: auto;
|
|
left: 0;
|
|
max-width: 32rem;
|
|
padding: 0.5rem;
|
|
transition: all ease-in-out 0.33s;
|
|
border-right: 1px solid var(--border-subtle);
|
|
}
|
|
|
|
#sidebar #sidebar-toggle,
|
|
#sidebar #sidebar-toggle-icon {
|
|
opacity: 0;
|
|
display: none;
|
|
}
|
|
|
|
@media screen and (max-width: 1200px) {
|
|
#sidebar {
|
|
position: absolute;
|
|
top: 0;
|
|
bottom: 0;
|
|
transform: translateX(-100%);
|
|
}
|
|
|
|
#sidebar #sidebar-toggle-icon {
|
|
opacity: 1;
|
|
display: block;
|
|
position: absolute;
|
|
top: 0.1rem;
|
|
right: -2rem;
|
|
cursor: pointer;
|
|
transition: all ease-in-out 0.33s;
|
|
background: rgba(128, 128, 128, 0.5);
|
|
border-radius: 0 1rem 1rem 0;
|
|
padding: 0.25rem;
|
|
}
|
|
|
|
#sidebar .icon {
|
|
transition: all ease-in-out 0.15s;
|
|
}
|
|
|
|
#sidebar:has(#sidebar-toggle:checked) {
|
|
transform: translateX(0);
|
|
}
|
|
|
|
#sidebar:has(#sidebar-toggle:checked) #sidebar-toggle-icon {
|
|
right: 0;
|
|
rotate: 180deg;
|
|
}
|
|
}
|
|
|
|
#sidebar .title {
|
|
text-transform: uppercase;
|
|
font-size: small;
|
|
font-weight: bold;
|
|
line-height: 2rem;
|
|
}
|
|
|
|
#sidebar #topic-creation-container {
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
#sidebar #topic-creation-container #toggle-topic-creation-form-button {
|
|
transform: scale(0.8);
|
|
}
|
|
|
|
#sidebar .topic-list {
|
|
list-style-type: none;
|
|
margin-left: 1rem;
|
|
}
|
|
|
|
#sidebar .topic-list > li.topic a:before {
|
|
position: absolute;
|
|
left: -1.75rem;
|
|
top: 0;
|
|
font-weight: bold;
|
|
font-size: x-large;
|
|
content: "#";
|
|
color: var(--text);
|
|
}
|
|
|
|
#sidebar .topic-list > li.topic a {
|
|
position: relative;
|
|
display: block;
|
|
width: 100%;
|
|
min-height: 1.5rem;
|
|
line-height: 1.5rem;
|
|
font-weight: bold;
|
|
font-size: large;
|
|
margin-left: 1.75rem;
|
|
text-decoration: none;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
#sidebar .topic-list > li.topic.active a {
|
|
color: var(--accent);
|
|
}
|
|
|
|
.invitepopover {
|
|
position: fixed;
|
|
z-index: 1000;
|
|
background: var(--bg);
|
|
margin: 1rem;
|
|
border: 1px solid var(--border-normal);
|
|
border-radius: var(--border-radius);
|
|
padding: 1rem;
|
|
max-width: 90%;
|
|
}
|
|
|
|
.invitepopover .share-option .name {
|
|
text-transform: uppercase;
|
|
min-width: 4rem;
|
|
display: inline-block;
|
|
}
|
|
|
|
.invitepopover .icon.close {
|
|
cursor: pointer;
|
|
margin-right: -0.25rem;
|
|
margin-top: -0.25rem;
|
|
margin-bottom: 0.75rem;
|
|
margin-left: auto;
|
|
}
|
|
|
|
.invitepopover .share-option input {
|
|
padding: 0.75rem;
|
|
margin: 0 1rem 1rem 0;
|
|
background: none;
|
|
color: var(--text);
|
|
border: 1px solid var(--border-highlight);
|
|
border-radius: var(--border-radius);
|
|
box-shadow: none;
|
|
font-size: large;
|
|
width: 100%;
|
|
max-width: 16rem;
|
|
}
|
|
|
|
.invitepopover .share-option button {
|
|
padding: 1rem;
|
|
text-transform: uppercase;
|
|
border: 1px solid var(--border-subtle);
|
|
border-radius: var(--border-radius);
|
|
margin-left: -0.75rem;
|
|
}
|
|
|
|
@media screen and (max-width: 1200px) {
|
|
.invitepopover {
|
|
margin: 0;
|
|
padding: 0.5rem;
|
|
}
|
|
|
|
.invitepopover .share-option .name {
|
|
display: block;
|
|
text-align: center;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.invitepopover .share-option button {
|
|
display: block;
|
|
margin: 0 auto 1rem;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<div id="sidebar">
|
|
<input type="checkbox" id="sidebar-toggle" />
|
|
<label id="sidebar-toggle-icon" for="sidebar-toggle">
|
|
<div class="icon right"></div>
|
|
</label>
|
|
|
|
<script>
|
|
const DEFAULT_AVATAR_URL = `//${window.location.host}/images/default_avatar.gif`;
|
|
|
|
new MutationObserver((mutations, observer) => {
|
|
mutations.forEach((mutation) => {
|
|
const user = document.body.dataset.user
|
|
? JSON.parse(document.body.dataset.user)
|
|
: null;
|
|
|
|
const user_bound_elements = document.querySelectorAll("[data-bind-to-user-field]");
|
|
for (const user_bound_element of user_bound_elements) {
|
|
const key =
|
|
user_bound_element.dataset
|
|
.bindToUserField; /* I hate that it converts the name */
|
|
const key_elements = key.split(".");
|
|
|
|
let value = undefined;
|
|
if (user) {
|
|
let current = user;
|
|
for (const key_element of key_elements) {
|
|
current = current[key_element];
|
|
if (!current) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
value = current;
|
|
}
|
|
|
|
const target =
|
|
typeof user_bound_element.dataset.userFieldTarget === "string" &&
|
|
user_bound_element.dataset.userFieldTarget.length > 0
|
|
? user_bound_element.dataset.userFieldTarget
|
|
: "innerHTML";
|
|
|
|
const default_value =
|
|
typeof user_bound_element.dataset.userFieldDefault === "string" &&
|
|
user_bound_element.dataset.userFieldDefault.length > 0
|
|
? user_bound_element.dataset.userFieldDefault
|
|
: "";
|
|
|
|
user_bound_element[target] = value ?? default_value;
|
|
}
|
|
|
|
const primary_color_setting = user?.meta?.primary_color;
|
|
if (primary_color_setting) {
|
|
const root = document.querySelector(":root");
|
|
root.style.setProperty("--base-color", primary_color_setting);
|
|
}
|
|
});
|
|
}).observe(document.body, {
|
|
attributes: true,
|
|
attributeFilter: ["data-user"],
|
|
});
|
|
</script>
|
|
<style type="text/css">
|
|
.profile-container {
|
|
max-width: 1024px;
|
|
padding: 1rem;
|
|
padding-bottom: 0;
|
|
}
|
|
|
|
.profile-container .avatar-container {
|
|
position: relative;
|
|
width: 100%;
|
|
max-width: 200px;
|
|
}
|
|
|
|
.profile-container .avatar-container #user-avatar {
|
|
width: 100%;
|
|
height: 100%;
|
|
max-width: 100%;
|
|
max-height: 100%;
|
|
}
|
|
|
|
.profile-container .avatar-container input[type="file"] {
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
left: 0;
|
|
opacity: 0;
|
|
cursor: pointer;
|
|
}
|
|
</style>
|
|
<form class="profile-container">
|
|
<script>
|
|
const profile_form = document.currentScript.closest("form");
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
const inputs = profile_form.querySelectorAll("input");
|
|
|
|
async function update_from_input(input) {
|
|
delete input.__debounce_timeout;
|
|
|
|
if (!document.body.dataset.user) {
|
|
return;
|
|
}
|
|
|
|
const user = JSON.parse(document.body.dataset.user);
|
|
|
|
const updated_user = { ...user };
|
|
|
|
switch (input.name) {
|
|
case "meta.avatar":
|
|
const avatar = input.files[0];
|
|
|
|
if (!avatar || !avatar.type || !avatar.type.includes("image")) {
|
|
return alert(
|
|
"You must select a valid image to upload as your avatar.",
|
|
);
|
|
}
|
|
|
|
// TODO: actually enforce this on the upload in serverus somehow
|
|
if (avatar.size > 512_000) {
|
|
return alert("512K is the largest allowed avatar size.");
|
|
}
|
|
|
|
const avatar_upload_body = new FormData();
|
|
avatar_upload_body.append(
|
|
"file",
|
|
avatar,
|
|
encodeURIComponent(avatar.name),
|
|
);
|
|
|
|
const avatar_path = `/files/users/${user.id}/avatars/${encodeURIComponent(avatar.name)}`;
|
|
|
|
const avatar_upload_response = await api.fetch(avatar_path, {
|
|
method: "PUT",
|
|
body: avatar_upload_body,
|
|
});
|
|
|
|
if (!avatar_upload_response.ok) {
|
|
const error = await avatar_upload_response.json();
|
|
return alert(error?.error?.message ?? "Unknown error.");
|
|
}
|
|
|
|
updated_user.meta = updated_user.meta ?? {};
|
|
updated_user.meta.avatar = `//${window.location.host}${avatar_path}`;
|
|
break;
|
|
default:
|
|
const elements = input.name.split(".");
|
|
let current = updated_user;
|
|
for (const element of elements.slice(0, elements.length - 1)) {
|
|
current[element] = current[element] ?? {};
|
|
current = current[element];
|
|
}
|
|
|
|
current[elements.slice(elements.length - 1).shift()] =
|
|
input.value.trim();
|
|
|
|
break;
|
|
}
|
|
|
|
const saved_user_response = await api.fetch(`/api/users/${user.id}`, {
|
|
method: "PUT",
|
|
json: updated_user,
|
|
});
|
|
|
|
if (!saved_user_response.ok) {
|
|
const error = await avatar_upload_response.json();
|
|
return alert(error?.error?.message ?? "Unknown error.");
|
|
}
|
|
|
|
const saved_user = await saved_user_response.json();
|
|
|
|
document.body.dataset.user = JSON.stringify(saved_user);
|
|
document.body.dataset.perms = saved_user.permissions.join(":");
|
|
}
|
|
|
|
for (const input of inputs) {
|
|
function on_updated(event) {
|
|
if (input.__debounce_timeout) {
|
|
clearTimeout(input.__debounce_timeout);
|
|
}
|
|
|
|
input.__debounce_timeout = setTimeout(() => {
|
|
update_from_input(input);
|
|
}, 250);
|
|
}
|
|
|
|
input.addEventListener("input", on_updated);
|
|
input.addEventListener("change", on_updated);
|
|
}
|
|
});
|
|
</script>
|
|
<div class="avatar-container">
|
|
<img
|
|
id="user-avatar"
|
|
src="/images/default_avatar.gif"
|
|
alt="User Avatar"
|
|
data-bind-to-user-field="meta.avatar"
|
|
data-user-field-target="src"
|
|
data-user-field-default="/images/default_avatar.gif"
|
|
/>
|
|
<input type="file" accept="image/*" name="meta.avatar" />
|
|
</div>
|
|
|
|
<details class="additional-profile">
|
|
<summary>
|
|
<div class="username-container">
|
|
<span class="username" data-bind-to-user-field="username"></span>
|
|
</div>
|
|
</summary>
|
|
|
|
<div class="notifications-settings-container">
|
|
<button class="mockup" onclick="NOTIFICATIONS.request_permission()">
|
|
Enable Notifications
|
|
</button>
|
|
</div>
|
|
|
|
<div class="color-settings-container">
|
|
<input
|
|
type="text"
|
|
id="user-color-setting-primary"
|
|
name="meta.primary_color"
|
|
value=""
|
|
data-bind-to-user-field="meta.primary_color"
|
|
data-user-field-target="value"
|
|
data-user-field-default=""
|
|
/>
|
|
<label class="placeholder" for="user-color-setting-primary">Primary Color</label>
|
|
</div>
|
|
</details>
|
|
</form>
|
|
|
|
<button
|
|
style="text-transform: uppercase; width: 100%; padding: 1.1rem 0"
|
|
onclick="generate_invite(event)"
|
|
>
|
|
Invite Another Human
|
|
</button>
|
|
|
|
<form
|
|
data-smart="true"
|
|
data-method="DELETE"
|
|
action="/api/auth"
|
|
style="position: absolute; left: 1rem; right: 1rem; bottom: 1rem"
|
|
>
|
|
<script>
|
|
{
|
|
const form = document.currentScript.closest("form");
|
|
form.on_reply = (response) => {
|
|
if (!response.deleted) {
|
|
alert("error logging out? please reload.");
|
|
return;
|
|
}
|
|
|
|
delete document.body.dataset.user;
|
|
delete document.body.dataset.perms;
|
|
window.location = "/";
|
|
|
|
document.dispatchEvent(new CustomEvent("user_logged_out", { detail: {} }));
|
|
};
|
|
}
|
|
</script>
|
|
<button class="primary">Log Out</button>
|
|
</form>
|
|
|
|
<div class="topics-container">
|
|
<div style="margin-bottom: 1rem">
|
|
<span class="title">topics</span>
|
|
</div>
|
|
<ul id="topic-list" class="topic-list"></ul>
|
|
|
|
<div id="topic-creation-container" data-requires-permission="topics.create">
|
|
<button
|
|
id="toggle-topic-creation-form-button"
|
|
onclick="((event) => {
|
|
event.preventDefault();
|
|
const topic_create_form = document.getElementById( 'topic-create' );
|
|
topic_create_form.style[ 'height' ] = topic_create_form.style[ 'height' ] === '5rem' ? '0' : '5rem';
|
|
})(event)"
|
|
>
|
|
<div class="icon plus"></div>
|
|
</button>
|
|
<form
|
|
id="topic-create"
|
|
data-smart="true"
|
|
action="/api/topics"
|
|
method="POST"
|
|
style="
|
|
margin-top: 1rem;
|
|
width: 100%;
|
|
overflow: hidden;
|
|
height: 0;
|
|
overflow: hidden;
|
|
transition: all 0.5s;
|
|
"
|
|
>
|
|
<input
|
|
id="new-topic-name-input"
|
|
type="text"
|
|
name="name"
|
|
value=""
|
|
placeholder="new topic"
|
|
/>
|
|
|
|
<input type="submit" hidden />
|
|
<script>
|
|
{
|
|
const form = document.currentScript.closest("form");
|
|
const topic_create_form = document.getElementById("topic-create");
|
|
const new_topic_name_input =
|
|
document.getElementById("new-topic-name-input");
|
|
|
|
form.on_reply = (new_topic) => {
|
|
const topic_list = document.getElementById("topic-list");
|
|
topic_list.insertAdjacentHTML(
|
|
"beforeend",
|
|
`<li id="topic-selector-${new_topic.id}" class="topic"><a href="#/topic/${new_topic.id}">${new_topic.name}</a></li>`,
|
|
);
|
|
|
|
new_topic_name_input.value = "";
|
|
window.location.hash = `/topic/${new_topic.id}`;
|
|
topic_create_form.style["height"] = "0";
|
|
};
|
|
}
|
|
</script>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|