feature: forum posts working

This commit is contained in:
Andy Burke 2025-09-16 19:10:26 -07:00
parent 7e4ab72fe6
commit 778d1b3aef
13 changed files with 352 additions and 77 deletions

View file

@ -981,6 +981,18 @@ body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"]
left: -6px;
}
/* ICON - MINUS */
.icon.minus {
box-sizing: border-box;
position: relative;
display: block;
transform: scale(var(--icon-scale, 1));
width: 16px;
height: 2px;
background: currentColor;
border-radius: 10px;
}
/* ICON - MORE */
.icon.more {
box-sizing: border-box;

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8 KiB

View file

@ -0,0 +1 @@
<svg id="visual" viewBox="0 0 900 600" width="900" height="600" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"><rect x="0" y="0" width="900" height="600" fill="#999"></rect><path d="M0 402L129 365L257 425L386 435L514 371L643 433L771 356L900 436L900 601L771 601L643 601L514 601L386 601L257 601L129 601L0 601Z" fill="#777777"></path><path d="M0 427L129 444L257 416L386 405L514 415L643 423L771 432L900 454L900 601L771 601L643 601L514 601L386 601L257 601L129 601L0 601Z" fill="#656565"></path><path d="M0 507L129 485L257 451L386 474L514 463L643 479L771 456L900 450L900 601L771 601L643 601L514 601L386 601L257 601L129 601L0 601Z" fill="#545454"></path><path d="M0 511L129 482L257 526L386 503L514 495L643 514L771 486L900 535L900 601L771 601L643 601L514 601L386 601L257 601L129 601L0 601Z" fill="#434343"></path><path d="M0 568L129 529L257 552L386 554L514 564L643 555L771 550L900 571L900 601L771 601L643 601L514 601L386 601L257 601L129 601L0 601Z" fill="#333333"></path></svg>

After

Width:  |  Height:  |  Size: 1,014 B

View file

@ -0,0 +1 @@
<svg id="visual" viewBox="0 0 900 600" width="900" height="600" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"><path d="M0 265L82 271L164 133L245 205L327 241L409 187L491 187L573 217L655 235L736 229L818 163L900 235L900 0L818 0L736 0L655 0L573 0L491 0L409 0L327 0L245 0L164 0L82 0L0 0Z" fill="#cccccc"></path><path d="M0 325L82 331L164 271L245 349L327 307L409 253L491 259L573 343L655 319L736 301L818 247L900 319L900 233L818 161L736 227L655 233L573 215L491 185L409 185L327 239L245 203L164 131L82 269L0 263Z" fill="#a3a3a3"></path><path d="M0 463L82 451L164 451L245 415L327 493L409 451L491 499L573 433L655 415L736 439L818 427L900 427L900 317L818 245L736 299L655 317L573 341L491 257L409 251L327 305L245 347L164 269L82 329L0 323Z" fill="#7b7b7b"></path><path d="M0 529L82 487L164 511L245 505L327 559L409 487L491 553L573 481L655 493L736 529L818 511L900 469L900 425L818 425L736 437L655 413L573 431L491 497L409 449L327 491L245 413L164 449L82 449L0 461Z" fill="#565656"></path><path d="M0 601L82 601L164 601L245 601L327 601L409 601L491 601L573 601L655 601L736 601L818 601L900 601L900 467L818 509L736 527L655 491L573 479L491 551L409 485L327 557L245 503L164 509L82 485L0 527Z" fill="#333333"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1 @@
<svg id="visual" viewBox="0 0 900 600" width="900" height="600" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"><rect width="900" height="600" fill="#222"></rect><g><g transform="translate(228 438)"><path d="M0 -120.8L104.6 -60.4L104.6 60.4L0 120.8L-104.6 60.4L-104.6 -60.4Z" fill="none" stroke="#999" stroke-width="2"></path></g><g transform="translate(225 182)"><path d="M0 -82L71 -41L71 41L0 82L-71 41L-71 -41Z" fill="none" stroke="#999" stroke-width="2"></path></g><g transform="translate(630 373)"><path d="M0 -69L59.8 -34.5L59.8 34.5L0 69L-59.8 34.5L-59.8 -34.5Z" stroke="#999" fill="none" stroke-width="2"></path></g><g transform="translate(761 170)"><path d="M0 -60L52 -30L52 30L0 60L-52 30L-52 -30Z" stroke="#999" fill="none" stroke-width="2"></path></g><g transform="translate(481 155)"><path d="M0 -86L74.5 -43L74.5 43L0 86L-74.5 43L-74.5 -43Z" stroke="#999" fill="none" stroke-width="2"></path></g></g></svg>

After

Width:  |  Height:  |  Size: 964 B

View file

@ -0,0 +1 @@
<svg id="visual" viewBox="0 0 900 600" width="900" height="600" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"><path d="M0 193L82 193L82 223L164 223L164 187L245 187L245 193L327 193L327 103L409 103L409 163L491 163L491 115L573 115L573 193L655 193L655 205L736 205L736 133L818 133L818 79L900 79L900 187L900 0L900 0L818 0L818 0L736 0L736 0L655 0L655 0L573 0L573 0L491 0L491 0L409 0L409 0L327 0L327 0L245 0L245 0L164 0L164 0L82 0L82 0L0 0Z" fill="#222222"></path><path d="M0 325L82 325L82 427L164 427L164 295L245 295L245 295L327 295L327 397L409 397L409 397L491 397L491 367L573 367L573 307L655 307L655 313L736 313L736 349L818 349L818 283L900 283L900 409L900 185L900 77L818 77L818 131L736 131L736 203L655 203L655 191L573 191L573 113L491 113L491 161L409 161L409 101L327 101L327 191L245 191L245 185L164 185L164 221L82 221L82 191L0 191Z" fill="#353535"></path><path d="M0 379L82 379L82 463L164 463L164 397L245 397L245 391L327 391L327 457L409 457L409 475L491 475L491 415L573 415L573 355L655 355L655 403L736 403L736 409L818 409L818 379L900 379L900 451L900 407L900 281L818 281L818 347L736 347L736 311L655 311L655 305L573 305L573 365L491 365L491 395L409 395L409 395L327 395L327 293L245 293L245 293L164 293L164 425L82 425L82 323L0 323Z" fill="#4a4a4a"></path><path d="M0 505L82 505L82 529L164 529L164 445L245 445L245 481L327 481L327 535L409 535L409 517L491 517L491 541L573 541L573 433L655 433L655 481L736 481L736 523L818 523L818 451L900 451L900 505L900 449L900 377L818 377L818 407L736 407L736 401L655 401L655 353L573 353L573 413L491 413L491 473L409 473L409 455L327 455L327 389L245 389L245 395L164 395L164 461L82 461L82 377L0 377Z" fill="#606060"></path><path d="M0 601L82 601L82 601L164 601L164 601L245 601L245 601L327 601L327 601L409 601L409 601L491 601L491 601L573 601L573 601L655 601L655 601L736 601L736 601L818 601L818 601L900 601L900 601L900 503L900 449L818 449L818 521L736 521L736 479L655 479L655 431L573 431L573 539L491 539L491 515L409 515L409 533L327 533L327 479L245 479L245 443L164 443L164 527L82 527L82 503L0 503Z" fill="#777777"></path></svg>

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -0,0 +1 @@
<svg id="visual" viewBox="0 0 900 600" width="900" height="600" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"><rect width="900" height="600" fill="#999"></rect><g><g transform="translate(533 443)"><path d="M51.8 -44.1C63.2 -27 65.9 -5.7 61.8 14.9C57.8 35.5 47 55.5 30.3 63.8C13.6 72.1 -9 68.9 -28.1 59.3C-47.1 49.8 -62.5 33.8 -67.4 14.8C-72.2 -4.3 -66.4 -26.3 -53.4 -43.8C-40.4 -61.2 -20.2 -74.1 0 -74.1C20.2 -74.1 40.3 -61.2 51.8 -44.1Z" stroke="#333" fill="none" stroke-width="20"></path></g><g transform="translate(237 212)"><path d="M40.5 -31C50.9 -19.2 56.7 -2.5 53.6 12.7C50.5 27.8 38.5 41.4 23.9 47.7C9.3 53.9 -7.9 53 -24.2 46.6C-40.6 40.1 -56.1 28.2 -59.6 13.4C-63.1 -1.4 -54.7 -19.1 -42.8 -31.2C-30.8 -43.3 -15.4 -49.8 -0.2 -49.6C15 -49.5 30 -42.7 40.5 -31Z" stroke="#333" fill="none" stroke-width="20"></path></g><g transform="translate(454 167)"><path d="M39.9 -32.8C48.6 -20.9 50.4 -4.5 46.2 9.1C42 22.7 31.6 33.4 19 39.4C6.4 45.5 -8.4 46.8 -21.7 41.7C-35 36.6 -46.7 25 -48.9 12.1C-51.1 -0.7 -43.6 -14.8 -33.9 -27C-24.1 -39.2 -12.1 -49.5 1.8 -50.9C15.6 -52.3 31.2 -44.8 39.9 -32.8Z" stroke="#333" fill="none" stroke-width="20"></path></g><g transform="translate(645 265)"><path d="M55.5 -41.4C69.6 -26.5 77.2 -3.6 72.3 16C67.4 35.5 50 51.6 29.6 61.4C9.2 71.2 -14.2 74.6 -33.1 66.4C-52.1 58.2 -66.6 38.4 -69.8 17.9C-72.9 -2.7 -64.7 -23.8 -51.2 -38.6C-37.6 -53.3 -18.8 -61.6 0.9 -62.3C20.7 -63.1 41.3 -56.3 55.5 -41.4Z" stroke="#333" fill="none" stroke-width="20"></path></g><g transform="translate(775 446)"><path d="M47.7 -37.6C59.3 -23.5 64.5 -3.8 60.1 13.1C55.7 29.9 41.8 44 24.6 52.9C7.5 61.8 -13 65.6 -29.7 58.7C-46.4 51.8 -59.3 34.2 -63 15.2C-66.7 -3.8 -61.1 -24 -49 -38.3C-37 -52.5 -18.5 -60.8 -0.2 -60.6C18 -60.4 36.1 -51.8 47.7 -37.6Z" stroke="#333" fill="none" stroke-width="20"></path></g><g transform="translate(785 95)"><path d="M56.4 -45.4C68.5 -29.7 70.8 -6.5 64.8 12.8C58.8 32.1 44.6 47.6 27.6 54.8C10.7 62 -9.1 60.9 -27 53.3C-45 45.8 -61.2 31.7 -67.2 13.2C-73.1 -5.4 -68.8 -28.4 -55.9 -44.3C-43 -60.1 -21.5 -68.8 0.3 -69.1C22.1 -69.3 44.2 -61.1 56.4 -45.4Z" stroke="#333" fill="none" stroke-width="20"></path></g><g transform="translate(277 494)"><path d="M58.4 -44.6C72.3 -29.1 77.9 -5 72.8 16.3C67.6 37.5 51.8 56 31.5 66.1C11.1 76.2 -13.6 77.9 -32.4 68.4C-51.2 58.9 -64 38.2 -67.1 17.4C-70.2 -3.4 -63.5 -24.3 -50.7 -39.6C-37.9 -54.9 -18.9 -64.6 1.6 -65.9C22.2 -67.2 44.4 -60.2 58.4 -44.6Z" stroke="#333" fill="none" stroke-width="20"></path></g><g transform="translate(101 416)"><path d="M39.6 -30.4C49.9 -18.7 55.9 -2.3 52.3 11.4C48.8 25.1 35.8 36.1 21.3 42.5C6.7 48.9 -9.2 50.6 -21.9 44.7C-34.5 38.9 -43.9 25.5 -48.1 10.1C-52.3 -5.4 -51.5 -22.9 -42.6 -34.2C-33.7 -45.6 -16.9 -50.9 -1.1 -50C14.7 -49.2 29.3 -42.1 39.6 -30.4Z" stroke="#333" fill="none" stroke-width="20"></path></g></g></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -0,0 +1 @@
<svg id="visual" viewBox="0 0 900 600" width="900" height="600" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"><path d="M0 73L50 89C100 105 200 137 300 156C400 175 500 181 600 181C700 181 800 175 850 172L900 169L900 0L850 0C800 0 700 0 600 0C500 0 400 0 300 0C200 0 100 0 50 0L0 0Z" fill="#646464"></path><path d="M0 187L50 196C100 205 200 223 300 235C400 247 500 253 600 252C700 251 800 243 850 239L900 235L900 167L850 170C800 173 700 179 600 179C500 179 400 173 300 154C200 135 100 103 50 87L0 71Z" fill="#919191"></path><path d="M0 283L50 282C100 281 200 279 300 290C400 301 500 325 600 332C700 339 800 329 850 324L900 319L900 233L850 237C800 241 700 249 600 250C500 251 400 245 300 233C200 221 100 203 50 194L0 185Z" fill="#ababab"></path><path d="M0 481L50 475C100 469 200 457 300 456C400 455 500 465 600 467C700 469 800 463 850 460L900 457L900 317L850 322C800 327 700 337 600 330C500 323 400 299 300 288C200 277 100 279 50 280L0 281Z" fill="#919191"></path><path d="M0 601L50 601C100 601 200 601 300 601C400 601 500 601 600 601C700 601 800 601 850 601L900 601L900 455L850 458C800 461 700 467 600 465C500 463 400 453 300 454C200 455 100 467 50 473L0 479Z" fill="#646464"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1 @@
<svg id="visual" viewBox="0 0 900 600" width="900" height="600" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"><rect x="0" y="0" width="900" height="600" fill="#222"></rect><defs><linearGradient id="grad1_0" x1="33.3%" y1="0%" x2="100%" y2="100%"><stop offset="20%" stop-color="#222222" stop-opacity="1"></stop><stop offset="80%" stop-color="#222222" stop-opacity="1"></stop></linearGradient></defs><defs><linearGradient id="grad2_0" x1="0%" y1="0%" x2="66.7%" y2="100%"><stop offset="20%" stop-color="#222222" stop-opacity="1"></stop><stop offset="80%" stop-color="#222222" stop-opacity="1"></stop></linearGradient></defs><g transform="translate(900, 0)"><path d="M0 486.7C-60.9 466.7 -121.8 446.7 -173.4 418.5C-225 390.3 -267.3 353.9 -316.1 316.1C-364.9 278.3 -420.2 239.2 -449.7 186.3C-479.2 133.4 -483 66.7 -486.7 0L0 0Z" fill="#aaa"></path></g><g transform="translate(0, 600)"><path d="M0 -486.7C49.1 -448.4 98.2 -410.1 163 -393.6C227.9 -377 308.4 -382.2 344.2 -344.2C379.9 -306.2 370.8 -224.9 387.1 -160.3C403.4 -95.8 445.1 -47.9 486.7 0L0 0Z" fill="#aaa"></path></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1 @@
<svg id="visual" viewBox="0 0 900 600" width="900" height="600" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"><rect x="0" y="0" width="900" height="600" fill="#111"></rect><g transform="translate(452.3432295685876 318.99801093507597)"><path d="M133.1 -219.4C166 -186 181.7 -138.1 184.2 -95.1C186.7 -52.2 176 -14.2 180.9 33.6C185.8 81.3 206.2 138.9 185.7 158.9C165.2 178.9 103.8 161.4 53 172.8C2.3 184.2 -37.8 224.5 -70.7 223C-103.7 221.5 -129.7 178.2 -150.4 138.9C-171.1 99.6 -186.5 64.3 -194.4 26.4C-202.2 -11.5 -202.5 -52.1 -182.6 -78.2C-162.6 -104.4 -122.4 -116.1 -88.6 -149.2C-54.8 -182.2 -27.4 -236.6 11.4 -254.3C50.1 -272 100.2 -252.9 133.1 -219.4" fill="#999"></path></g></svg>

After

Width:  |  Height:  |  Size: 730 B

View file

@ -17,5 +17,7 @@ function datetime_to_local(input) {
}),
value: local_datetime.valueOf(),
ms: local_datetime.getMilliseconds(),
};
}

View file

@ -145,14 +145,6 @@ async function load_active_topic_for_chat() {
topic_chat_content.innerHTML = "";
const topic_selectors = document.querySelectorAll("li.topic");
for (const topic_selector of topic_selectors) {
topic_selector.classList.remove("active");
if (topic_selector.id === `topic-selector-${topic_id}`) {
topic_selector.classList.add("active");
}
}
const events_response = await api.fetch(
`/api/topics/${topic_id}/events?type=chat&limit=100&sort=newest`,
);

View file

@ -1,8 +1,128 @@
<style>
.forum-container {
padding: 2rem;
#forum-posts.container {
padding: 2rem 0.5rem;
}
.post-container {
position: relative;
display: grid;
grid-template-rows: auto auto 1fr;
grid-template-columns: auto auto 1fr;
grid-template-areas:
"expander preview info"
"expander preview subject"
"expander preview content";
max-height: 6rem;
padding: 1rem;
border: 1px solid var(--border-subtle);
overflow-y: hidden;
transition: all ease-in-out 0.33s;
margin-bottom: 2rem;
}
.post-container:has(input[name="expanded"]:checked) {
max-height: 40rem;
overflow-y: scroll;
}
.expand-toggle-container input[name="expanded"] {
display: none;
visibility: hidden;
opacity: 0;
}
.post-container:has(input[name="expanded"]) .icon.minus,
.post-container:has(input[name="expanded"]:checked) .icon.plus {
display: block;
opacity: 1;
visibility: visible;
}
.post-container:has(input[name="expanded"]) .icon.plus,
.post-container:has(input[name="expanded"]:checked) .icon.minus {
display: none;
opacity: 0;
visibility: hidden;
}
.post-container .expand-toggle-container {
grid-area: expander;
margin-right: 0.5rem;
}
.post-container .expand-toggle-container label {
display: flex;
align-items: center;
width: 22px;
height: 22px;
}
.post-container .media-preview-container {
grid-area: preview;
width: 6rem;
height: 4rem;
margin-right: 1rem;
}
.post-container .media-preview-container img {
max-width: 100%;
max-height: 100%;
}
.post-container .info-container {
grid-area: info;
display: flex;
}
.post-container .info-container .avatar-container {
display: block;
margin: 0.5rem;
width: 3rem;
height: 3rem;
border-radius: 16%;
overflow: hidden;
}
.post-container .info-container .avatar-container img {
width: 100%;
}
.post-container .info-container .username-container {
display: block;
margin: 0 0.5rem;
font-weight: bold;
}
.post-container .info-container .datetime-container {
display: block;
margin: 0 0.5rem;
}
.post-container .info-container .datetime-container .long {
font-size: x-small;
text-transform: uppercase;
visibility: hidden;
display: none;
}
.post-container .info-container .datetime-container .short {
font-size: xx-small;
}
.post-container .subject-container {
grid-area: subject;
padding-left: 5rem;
margin-top: -2.5rem;
font-size: larger;
font-weight: bold;
}
.post-container .content-container {
grid-area: content;
padding-left: 5rem;
margin-top: 2rem;
}
</style>
<div id="forum" class="tab">
<input
type="radio"
@ -11,77 +131,186 @@
class="tab-switch"
data-view="forum"
/>
<label for="forum-tab-input" class="tab-label mockup"
<label for="forum-tab-input" class="tab-label"
><div class="icon forum"></div>
<div class="label">Forum</div></label
>
<div class="tab-content forum-container">
<div id="forum-thread-list"></div>
<div id="forum-posts" class="container">
<div id="forum-posts-list"></div>
<script>
const forum_posts_list = document.getElementById("forum-posts-list");
<div id="thread-creation-container" data-requires-permission="topics.threads.write">
async function render_post(post) {
const creator = await USERS.get(post.creator_id);
const existing_element =
document.getElementById(`post-${post.id.substring(0, 49)}`) ??
document.getElementById(`post-${post.meta.temp_id}`);
const post_datetime = datetime_to_local(post.timestamps.created);
const html_content = `<div class="post-container" data-creator_id="${creator.id}">
<div class="expand-toggle-container">
<label>
<input type="checkbox" name="expanded"/><i class="icon plus"></i><i class="icon minus"></i>
</label>
</div>
<div class="media-preview-container">
<img src="/images/placeholders/${String(post_datetime.ms % 10).padStart(2, "0")}.svg" />
</div>
<div class="info-container">
<div class="avatar-container">
<img src="${creator.meta?.avatar ?? "/images/default_avatar.gif"}" alt="user avatar" />
</div>
<div class="username-container">
<span class="username">${creator.username}</span>
</div>
<div class="datetime-container">
<span class="long">${post_datetime.long}</span>
<span class="short">${post_datetime.short}</span>
</div>
</div>
<div class="subject-container">${post.data.subject}</div>
<div class="content-container">${htmlify(post.data.content)}</div>
</div>`;
if (existing_element) {
const template = document.createElement("template");
template.innerHTML = html_content;
existing_element.replaceWith(template.content.firstChild);
} else {
forum_posts_list.insertAdjacentHTML("beforeend", html_content);
}
}
async function append_posts(posts) {
let last_post_id = forum_posts_list.dataset.last_post_id ?? "";
for await (const post of posts) {
// if the last post is undefined, it becomes this event, otherwise, if this event's id is newer,
// it becomes the latest post
last_post_id =
post.id > last_post_id && post.id.indexOf("TEMP") !== 0
? post.id
: last_post_id;
// if the last post has been updated, update the content's dataset to reflect that
if (last_post_id !== forum_posts_list.dataset.last_post_id) {
forum_posts_list.dataset.last_post_id = last_post_id;
}
await render_post(post);
}
forum_posts_list.scrollTop = 0;
}
// TODO: we need some abortcontroller handling here or something
// similar for when we change topics, this is the most basic
// first pass outline
let post_polling_abort_controller = null;
async function poll_for_new_posts() {
const forum_posts_list = document.getElementById("forum-posts-list");
const topic_id = document.body.dataset.topic;
const last_post_id = forum_posts_list.dataset.last_post_id;
if (!topic_id) {
return;
}
const message_polling_url = `/api/topics/${topic_id}/events?type=post&limit=100&sort=newest&wait=true${last_post_id ? `&after_id=${last_post_id}` : ""}`;
post_polling_abort_controller =
post_polling_abort_controller || new AbortController();
api.fetch(message_polling_url, {
signal: post_polling_abort_controller.signal,
})
.then(async (new_events_response) => {
const new_events = (await new_events_response.json()) ?? [];
await append_posts(new_events);
poll_for_new_posts(topic_id);
})
.catch((error) => {
// TODO: poll again? back off?
console.error(error);
});
}
async function load_active_topic_for_forum() {
const topic_id = document.body.dataset.topic;
if (!topic_id) return;
const user = document.body.dataset.user
? JSON.parse(document.body.dataset.user)
: null;
if (!user) return;
const forum_posts_list = document.getElementById("forum-posts-list");
if (post_polling_abort_controller) {
post_polling_abort_controller.abort();
post_polling_abort_controller = null;
delete forum_posts_list.dataset.last_post_id;
}
const topic_response = await api.fetch(`/api/topics/${topic_id}`);
if (!topic_response.ok) {
const error = await topic_response.json();
alert(error.message ?? JSON.stringify(error));
return;
}
const topic = await topic_response.json();
forum_posts_list.innerHTML = "";
const events_response = await api.fetch(
`/api/topics/${topic_id}/events?type=post&limit=100&sort=newest`,
);
if (!events_response.ok) {
const error = await events_response.json();
alert(error.message ?? JSON.stringify(error));
return;
}
const events = await events_response.json();
await append_posts(events);
poll_for_new_posts();
}
document.addEventListener("topic_changed", load_active_topic_for_forum);
document.addEventListener("user_logged_in", load_active_topic_for_forum);
</script>
</div>
<div id="post-creation-container" data-requires-permission="topics.posts.create">
<button
onclick="(() => { document.querySelector( '#thread-creation' ).classList.toggle( 'collapsed' ); })()"
class="mockup"
onclick="(() => { document.querySelector( '#post-creation' ).classList.toggle( 'collapsed' ); })()"
>
<i class="icon plus" style="display: inline-block; margin-right: 1rem"></i>New
Thread
<i class="icon plus" style="display: inline-block; margin-right: 1rem"></i>New Post
</button>
<form
id="thread-creation"
id="post-creation"
data-smart="true"
action="/api/topics"
method="POST"
class="collapsed mockup"
class="collapsed"
style="
margin-top: 1rem;
margin-top: 1rem
width: 100%;
overflow: hidden;
overflow: hidden;
transition: all 0.5s;
"
>
<input
id="new-thread-subject"
type="text"
name="data.subject"
value=""
placeholder="Thread subject..."
style="margin-bottom: 1rem"
class="mockup"
/>
<input
id="file-upload-and-share-input-for-thread"
aria-label="Upload and share file"
type="file"
multiple
/>
<label for="file-upload-and-share-input-for-thread" class="mockup">
<div class="icon attachment"></div>
</label>
<textarea
id="new-thread-content"
type="text"
name="data.content"
value=""
placeholder=" ... "
class="mockup"
></textarea>
<input type="submit" hidden />
<script>
{
const form = document.currentScript.closest("form");
const file_input = document.querySelector('input[type="file"]');
const subject_input = document.getElementById('input[name="subject"]');
const content_input = document.getElementById('input[name="content"]');
const form = document.currentScript.closest("form");
const parent_id_input = document.getElementById("parent-id");
const forum_thread_list = document.getElementById("forum-thread-list");
let threads_in_flight = {};
document.addEventListener("DOMContentLoaded", () => {
const file_input = form.querySelector('input[type="file"]');
const subject_input = form.querySelector('[name="data.subject"]');
const content_input = form.querySelector('[name="data.content"]');
const parent_id_input = form.querySelector('[name="parent_id"]');
form.on_submit = async (event) => {
const user = JSON.parse(document.body.dataset.user);
@ -120,8 +349,10 @@
return false;
}
const message = chat_input.value.trim();
if (form.uploaded_urls.length === 0 && message.length === 0) {
const subject = subject_input.value.trim();
const content = content_input.value.trim();
if (subject.length === 0) {
return false;
}
@ -133,7 +364,7 @@
const temp_id = `TEMP-${now}`;
json.id = temp_id;
json.type = "chat";
json.type = "post";
json.meta = {
temp_id,
};
@ -141,9 +372,9 @@
created: now,
updated: now,
};
json.data = json.data ?? {};
if (form.uploaded_urls.length) {
json.data = json.data ?? {};
json.data.content =
(typeof json.data.content === "string" &&
json.data.content.trim().length
@ -151,29 +382,58 @@
: "") + form.uploaded_urls.join("\n");
}
const user = JSON.parse(document.body.dataset.user);
render_text_event(topic_chat_content, json, user);
document.getElementById(`chat-${temp_id}`)?.classList.add("sending");
render_post(json);
document.getElementById(`post-${temp_id}`)?.classList.add("sending");
};
form.on_error = (error) => {
// TODO: mark the temporary message element with the failed class?
alert(error);
chat_input.focus();
content_input.focus();
};
form.on_reply = (sent_message) => {
document
.getElementById(`chat-${sent_message.meta?.temp_id ?? ""}`)
?.classList.remove("sending");
form.on_reply = (post) => {
const post_id = `post-${post.meta?.temp_id ?? ""}`;
document.getElementById(post_id)?.classList.remove("sending");
append_topic_events([sent_message]);
append_posts([post]);
parent_id_input.value = "";
chat_input.value = "";
chat_input.focus();
subject_input.value = "";
content_input.value = "";
};
}
});
</script>
<input type="hidden" name="parent_id" value="" />
<input
id="new-thread-subject"
type="text"
name="data.subject"
value=""
placeholder="Thread subject..."
style="margin-bottom: 1rem"
/>
<input
id="file-upload-and-share-input-for-thread"
aria-label="Upload and share file"
type="file"
multiple
/>
<label for="file-upload-and-share-input-for-thread">
<div class="icon attachment"></div>
</label>
<textarea
id="new-thread-content"
type="text"
name="data.content"
value=""
placeholder=" ... "
></textarea>
<input type="submit" value="Post" />
</form>
</div>
</div>