forked from andyburke/autonomous.contact
feature: watches on the backend, need frontend implementation for
notifications and unread indicators
This commit is contained in:
parent
7046bb0389
commit
6293374bb7
28 changed files with 1405 additions and 608 deletions
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
document.addEventListener("topics_updated", ({ detail: { topics } }) => {
|
||||
APP.on("topics_updated", ({ topics }) => {
|
||||
const topic_list = document.getElementById("topic-list");
|
||||
topic_list.innerHTML = "";
|
||||
for (const topic of topics.sort((lhs, rhs) => lhs.name.localeCompare(rhs.name))) {
|
||||
|
|
@ -16,19 +16,22 @@
|
|||
.forEach((element) => element.classList.remove("active"));
|
||||
|
||||
const new_topic_id = event?.detail?.topic_id ?? document.body.dataset.topic;
|
||||
|
||||
if (!new_topic_id) {
|
||||
return;
|
||||
if (new_topic_id) {
|
||||
document
|
||||
.querySelectorAll(`[data-topic-selector-for="${new_topic_id}"]`)
|
||||
.forEach((element) => element.classList.add("active"));
|
||||
}
|
||||
|
||||
document
|
||||
.querySelectorAll(`[data-topic-selector-for="${new_topic_id}"]`)
|
||||
.forEach((element) => element.classList.add("active"));
|
||||
for ( const watch of APP.user_watches ) {
|
||||
// find the topic indicator for this watch
|
||||
// if there is new stuff - TODO implement a HEAD for getting latest event id?
|
||||
// add a class of 'new-content'
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("topics_updated", update_topic_indicators);
|
||||
document.addEventListener("topic_changed", update_topic_indicators);
|
||||
document.addEventListener("user_logged_in", update_topic_indicators);
|
||||
APP.on("topics_updated", update_topic_indicators);
|
||||
APP.on("topic_changed", update_topic_indicators);
|
||||
APP.on("user_logged_in", update_topic_indicators);
|
||||
|
||||
function clear_invite_popup() {
|
||||
document.body.querySelectorAll(".invitepopover").forEach((element) => element.remove());
|
||||
|
|
@ -64,7 +67,7 @@
|
|||
return;
|
||||
}
|
||||
|
||||
const user = document.body.dataset.user && JSON.parse(document.body.dataset.user);
|
||||
const user = APP.user;
|
||||
if (!user) {
|
||||
alert("You must be logged in.");
|
||||
return;
|
||||
|
|
@ -103,22 +106,53 @@
|
|||
<button onclick="( () => document.querySelectorAll( '.invitepopover' ).forEach( (element) => element.remove() ) )()">Done</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
document.addEventListener(
|
||||
"contextmenu",
|
||||
(event) => {
|
||||
if (!event.target?.closest("#sidebar")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const topic_selector = event.target.closest("li.topic");
|
||||
if (!topic_selector) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const context_menu = document.getElementById("sidebar-context-menu");
|
||||
context_menu.dataset.prepare = true;
|
||||
|
||||
const position = get_best_coords_for_popup({
|
||||
target_element: topic_selector,
|
||||
popup: {
|
||||
width: context_menu.getBoundingClientRect().width,
|
||||
height: context_menu.getBoundingClientRect().height,
|
||||
},
|
||||
offset: {
|
||||
x: 4,
|
||||
y: 4,
|
||||
},
|
||||
});
|
||||
|
||||
context_menu.style.left = position.x + "px";
|
||||
context_menu.style.top = position.y + "px";
|
||||
context_menu.dataset.show = true;
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
document.addEventListener("click", (event) => {
|
||||
if (!event.target?.closest("#sidebar-context-menu")) {
|
||||
const context_menu = document.getElementById("sidebar-context-menu");
|
||||
delete context_menu.dataset.show;
|
||||
delete context_menu.dataset.prepare;
|
||||
}
|
||||
});
|
||||
</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);
|
||||
|
|
@ -126,15 +160,86 @@
|
|||
width: auto;
|
||||
left: 0;
|
||||
max-width: 32rem;
|
||||
padding: 0.5rem;
|
||||
padding-left: 6rem;
|
||||
transition: all ease-in-out 0.33s;
|
||||
border-right: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
#sidebar #sidebar-context-menu {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
min-width: 220px;
|
||||
overflow: hidden;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid var(--border-highlight);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow:
|
||||
0 0 25px hsla(var(--accent), 100%, 70%, 0.4),
|
||||
inset 0 0 10px hsla(var(--accent), 100%, 60%, 0.2);
|
||||
backdrop-filter: blur(var(--blur-radius));
|
||||
}
|
||||
|
||||
#sidebar #sidebar-context-menu[data-prepare] {
|
||||
display: block;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
#sidebar #sidebar-context-menu[data-show] {
|
||||
display: block;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#sidebar #sidebar-context-menu button {
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text);
|
||||
letter-spacing: 0.5px;
|
||||
transition:
|
||||
background 0.25s,
|
||||
color 0.25s;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
#sidebar #sidebar-context-menu button:last-of-type {
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
#sidebar #sidebar-context-menu button::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 3px;
|
||||
height: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: hsla(var(--accent), 100%, 75%, 0.8);
|
||||
transition: height 0.25s;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
#sidebar #sidebar-context-menu button:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
#sidebar #sidebar-context-menu button:hover::before {
|
||||
height: 70%;
|
||||
}
|
||||
|
||||
#sidebar #sidebar-toggle,
|
||||
#sidebar #sidebar-toggle-icon {
|
||||
opacity: 0;
|
||||
display: none;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
|
|
@ -285,324 +390,437 @@
|
|||
</style>
|
||||
|
||||
<div id="sidebar">
|
||||
<div id="sidebar-context-menu">
|
||||
<form
|
||||
data-smart="true"
|
||||
data-method="POST"
|
||||
action="/api/users/${ APP.user?.id }/watches"
|
||||
>
|
||||
<input type="hidden" name="target" value="" />
|
||||
|
||||
<button data-sidebar-context-menu-item="true">👁️🗨️ Watch</button>
|
||||
<button data-sidebar-context-menu-item="true">🕳️ Hide</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<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`;
|
||||
async function update_servers_list() {
|
||||
const template = document.getElementById( 'server-list-entry-template');
|
||||
|
||||
new MutationObserver((mutations, observer) => {
|
||||
mutations.forEach((mutation) => {
|
||||
const user = document.body.dataset.user
|
||||
? JSON.parse(document.body.dataset.user)
|
||||
: null;
|
||||
try {
|
||||
const server = APP.server;
|
||||
const entry = eval("`" + template.innerHTML.trim() + "`");
|
||||
document.getElementById('this-server-container').innerHTML = entry;
|
||||
}
|
||||
catch( error ) {
|
||||
console.error( error );
|
||||
}
|
||||
|
||||
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(".");
|
||||
try {
|
||||
document.getElementById( 'suggested-servers-container').innerHTML = '';
|
||||
|
||||
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;
|
||||
for ( const server of APP.suggested_servers ) {
|
||||
const entry = eval( "`" + template.innerHTML.trim() + "`");
|
||||
document.getElementById( 'suggested-servers-container').insertAdjacentHTML( 'beforeend', entry );
|
||||
}
|
||||
}
|
||||
catch( error ) {
|
||||
console.error( error );
|
||||
}
|
||||
|
||||
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);
|
||||
try {
|
||||
document.getElementById( 'user-servers-container').innerHTML = '';
|
||||
|
||||
for ( const server of APP.user_servers ) {
|
||||
const entry = eval( "`" + template.innerHTML.trim() + "`");
|
||||
document.getElementById( 'user-servers-container').insertAdjacentHTML( 'beforeend', entry );
|
||||
}
|
||||
});
|
||||
}).observe(document.body, {
|
||||
attributes: true,
|
||||
attributeFilter: ["data-user"],
|
||||
});
|
||||
}
|
||||
catch( error ) {
|
||||
console.error( error );
|
||||
}
|
||||
}
|
||||
|
||||
APP.on( 'load', update_servers_list );
|
||||
</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 input[type="file"] {
|
||||
<style>
|
||||
#server-list-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
bottom: 0;
|
||||
padding: 0.75rem;
|
||||
width: 6rem;
|
||||
border-right: 1px solid var(--border-subtle);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.server-list-entry {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
margin: 0.5rem auto;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.server-list-entry a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.server-list-entry img {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
margin: 0 auto 0.75rem;
|
||||
padding: 0.2rem;
|
||||
border-radius: var(--border-radius);
|
||||
object-fit: scale-down;
|
||||
align-content: center;
|
||||
border: 1px solid var(--border-subtle);
|
||||
background: var(--icon-background);
|
||||
}
|
||||
|
||||
.server-list-entry .server-name {
|
||||
font-size: x-small;
|
||||
word-wrap: break-word;
|
||||
font-weight: bold;
|
||||
max-height: 3rem;
|
||||
}
|
||||
</style>
|
||||
<form class="profile-container">
|
||||
<div id="server-list-container">
|
||||
<template id="server-list-entry-template">
|
||||
<div class="server-list-entry">
|
||||
<a href="${ server.url }">
|
||||
<img class="server-icon" src="${ server.icon ?? ( server.url + '/favicon.ico' ) }" alt="${ server.name ?? server.url } icon" style="${ server.icon_background ? `--icon-background: ${ server.icon_background };` : '' }" />
|
||||
<div class="server-name">${ server.name ?? server.url }</div>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div id="this-server-container"></div>
|
||||
<div id="suggested-servers-container"></div>
|
||||
<div id="user-servers-container"></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#server-info {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
<div id="server-info">
|
||||
<script>
|
||||
const profile_form = document.currentScript.closest("form");
|
||||
const DEFAULT_AVATAR_URL = `//${window.location.host}/images/default_avatar.gif`;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const inputs = profile_form.querySelectorAll("input");
|
||||
new MutationObserver((mutations, observer) => {
|
||||
mutations.forEach((mutation) => {
|
||||
const user = APP.user;
|
||||
|
||||
async function update_from_input(input) {
|
||||
delete input.__debounce_timeout;
|
||||
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(".");
|
||||
|
||||
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.",
|
||||
);
|
||||
let value = undefined;
|
||||
if (user) {
|
||||
let current = user;
|
||||
for (const key_element of key_elements) {
|
||||
current = current[key_element];
|
||||
if (!current) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
value = current;
|
||||
}
|
||||
|
||||
input.__debounce_timeout = setTimeout(() => {
|
||||
update_from_input(input);
|
||||
}, 250);
|
||||
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;
|
||||
}
|
||||
|
||||
input.addEventListener("input", on_updated);
|
||||
input.addEventListener("change", on_updated);
|
||||
}
|
||||
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>
|
||||
<div class="avatar-container xx-large">
|
||||
<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: {} }));
|
||||
};
|
||||
<style type="text/css">
|
||||
.profile-container {
|
||||
max-width: 1024px;
|
||||
padding: 1rem;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
</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>
|
||||
.profile-container .avatar-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
<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"
|
||||
/>
|
||||
.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");
|
||||
|
||||
<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");
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const inputs = profile_form.querySelectorAll("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>`,
|
||||
);
|
||||
async function update_from_input(input) {
|
||||
delete input.__debounce_timeout;
|
||||
|
||||
new_topic_name_input.value = "";
|
||||
window.location.hash = `/topic/${new_topic.id}/chat`;
|
||||
topic_create_form.style["height"] = "0";
|
||||
};
|
||||
if (!document.body.dataset.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
const user = APP.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();
|
||||
APP.update_user( saved_user );
|
||||
}
|
||||
</script>
|
||||
</form>
|
||||
|
||||
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 xx-large">
|
||||
<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;
|
||||
}
|
||||
|
||||
APP.logout();
|
||||
};
|
||||
}
|
||||
</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}/chat`;
|
||||
topic_create_form.style["height"] = "0";
|
||||
};
|
||||
}
|
||||
</script>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue