FossilOrigin-Name: 9d198513fc108e603d50e1f21082a5970f6996504c0541d54e376f9c98a6975c
This commit is contained in:
nekobit 2022-10-14 02:47:04 +00:00
parent cb0b676cd7
commit fe114a4995
5 changed files with 297 additions and 289 deletions

View file

@ -10,9 +10,9 @@ The goal is to create a frontend that's lightweight enough to be viewed without
usable enough to improve the experience with JS.
Treebird uses C with FCGI, mastodont-c (library designed for Treebird, but can be used
for other applications as well), and **optional** JavaScript for the frontend (100% functional without
javascript, it only helps). Uses [RE:DOM](https://redom.js.org/) (3kb js library) to assist with DOM
creation and native JS apis. (bundled, no need to build)
for other applications as well), Perl, and **optional** JavaScript for the frontend (100% functional without
javascript, it only helps). Uses [RE:DOM](https://redom.js.org/) (2kb JS library) to assist with DOM
creation and native JS apis. (Already bundled)
## Why?
@ -26,7 +26,7 @@ This led me to one choice, to develop my own frontend.
Treebird respects compatibility with old browsers, and thus uses HTML table layouts, which are
supported even by most modern terminal web browsers. The core browser we aim to at least maintain compatibility
with is Netsurf, but most other browsers like GNU Emacs EWW, elinks, render Treebird wonderfully.
with is Netsurf, but most other browsers like GNU Emacs EWW, elinks, render Treebird just alright.
## Credits

570
dist/js/main.js vendored
View file

@ -1,312 +1,318 @@
(function(){
Element.prototype.insertAfter = function(element) {
element.parentNode.insertBefore(this, element.nextSibling);
const { el, mount } = redom;
'use strict';
function get_cookie(cookiestr)
{
return document.cookie
.split(';')
.find(row => row.startsWith(cookiestr+'='))
.split('=')[1];
}
// TODO Check if logged in .acct value is the same
function reply_get_mentions(reply, content)
{
const regexpr = /<a target="_parent" class="mention" href="https?:\/\/.*?\/@(.*?)@(.*?)">.*?<\/a>/g;
const arr = [...content.matchAll(regexpr)];
let res = reply ? `@${reply} ` : "";
const matches = content.matchAll(regexpr);
for (let x of matches)
{
res += `@${x[1]}@${x[2]} `;
}
return res;
}
function form_enter_submit(e, that)
{
if ((e.ctrlKey || e.metaKey) && e.keyCode === 13)
that.closest('form').submit();
}
// Submit form entry on enter when in textbox/textarea
document.querySelectorAll("input[type=text], input[type=url], input[type=email], input[type=password], textarea").forEach((i) => {
i.addEventListener("keydown", e => form_enter_submit(e, i));
});
function construct_query(query)
{
let query_string = "";
let keys = Object.keys(query);
let vals = Object.values(query);
const len = keys.length;
for (let i = 0; i < keys.length; ++i)
{
query_string += keys[i] + "=" + vals[i];
if (i !== keys.length-1)
query_string += "&";
}
return query_string;
}
function send_request(url, query, type, cb, cb_args)
{
let query_str = construct_query(query);
let xhr = new XMLHttpRequest();
xhr.open(type, url);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.onreadystatechange = function() {
if (this.readyState === XMLHttpRequest.DONE)
cb(this, cb_args);
};
xhr.send(query_str);
return xhr;
}
function get_cookie(cookiestr)
{
return document.cookie
.split(';')
.find(row => row.startsWith(cookiestr+'='))
.split('=')[1];
}
function upload_file(url, file_name, file, cb, cb_args, onprogress, onload)
{
let xhr = new XMLHttpRequest();
let form_data = new FormData();
xhr.open("post", url);
form_data.append(file_name, file);
// xhr.setRequestHeader("Content-Type", "multipart/form-data");
xhr.onreadystatechange = function() {
if (this.readyState === XMLHttpRequest.DONE)
cb(this, cb_args);
};
xhr.upload.onprogress = onprogress;
xhr.upload.onload = onload;
xhr.send(form_data);
return xhr;
}
// TODO Check if logged in .acct value is the same
function reply_get_mentions(reply, content)
{
const regexpr = /<a target="_parent" class="mention" href="https?:\/\/.*?\/@(.*?)@(.*?)">.*?<\/a>/g;
const arr = [...content.matchAll(regexpr)];
let res = reply ? `@${reply} ` : "";
const matches = content.matchAll(regexpr);
function change_count_text(val, sum)
{
if (val === "")
val = 0
else
val = parseInt(val);
val += sum;
return val > 0 ? val.toString() : "";
}
for (let x of matches)
function interact_action(status, type)
{
let like = status.querySelector(".statbtn .like");
let repeat = status.querySelector(".statbtn .repeat");
let svg;
if (type.value === "like" || type.value === "unlike")
svg = [ like ];
else if (type.value === "repeat" || type.value === "unrepeat")
svg = [ repeat ];
else if (type.value === "likeboost")
svg = [ like, repeat ];
if (svg)
svg.forEach(that => {
let label = that.parentNode;
let counter = label.querySelector(".count");
let is_interacted = label.classList.contains("interacted");
if (counter)
{
res += `@${x[1]}@${x[2]} `;
counter.innerHTML = change_count_text(counter.innerHTML, is_interacted ? -1 : 1);
}
else {
// Nobody interacted with this yet, create counter
const counter = el("span.count", 1)
mount(label, counter);
is_interacted = false;
}
return res;
}
function form_enter_submit(e, that)
{
if ((e.ctrlKey || e.metaKey) && e.keyCode === 13)
that.closest('form').submit();
}
// Submit form entry on enter when in textbox/textarea
document.querySelectorAll("input[type=text], input[type=url], input[type=email], input[type=password], textarea").forEach((i) => {
i.addEventListener("keydown", e => form_enter_submit(e, i));
if (is_interacted)
{
// Animation
that.classList.remove("interacted-anim");
label.classList.remove("interacted");
// Flip itype value
type.value = type.value.replace("un", "");
}
else {
that.classList.add("interacted-anim");
label.classList.add("interacted");
type.value = "un" + type.value;
}
});
function construct_query(query)
}
function status_event(e)
{
let target = e.target.closest(".statbtn");
if (target)
{
query_string = "";
let keys = Object.keys(query);
let vals = Object.values(query);
const len = keys.length;
console.log('huh');
// Don't JS these
if (target.classList.contains("reply-btn") ||
target.classList.contains("view-btn") ||
target.classList.contains("statbtn-last"))
return true;
let type = target.parentNode.querySelector(".itype");
let status = e.target.closest(".status");
for (let i = 0; i < keys.length; ++i)
{
query_string += keys[i] + "=" + vals[i];
if (i !== keys.length-1)
query_string += "&";
}
return query_string;
}
function send_request(url, query, type, cb, cb_args)
{
let query_str = construct_query(query);
let xhr = new XMLHttpRequest();
xhr.open(type, url);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.onreadystatechange = function() {
if (this.readyState === XMLHttpRequest.DONE)
cb(this, cb_args);
};
xhr.send(query_str);
return xhr;
}
function upload_file(url, file_name, file, cb, cb_args, onprogress, onload)
{
let xhr = new XMLHttpRequest();
let form_data = new FormData();
xhr.open("post", url);
form_data.append(file_name, file);
// xhr.setRequestHeader("Content-Type", "multipart/form-data");
xhr.onreadystatechange = function() {
if (this.readyState === XMLHttpRequest.DONE)
cb(this, cb_args);
};
xhr.upload.onprogress = onprogress;
xhr.upload.onload = onload;
xhr.send(form_data);
return xhr;
}
function change_count_text(val, sum)
{
if (val === "")
val = 0
else
val = parseInt(val);
val += sum;
return val > 0 ? val.toString() : "";
}
function interact_action(status, type)
{
let like = status.querySelector(".statbtn .like");
let repeat = status.querySelector(".statbtn .repeat");
let svg;
if (type.value === "like" || type.value === "unlike")
svg = [ like ];
else if (type.value === "repeat" || type.value === "unrepeat")
svg = [ repeat ];
else if (type.value === "likeboost")
svg = [ like, repeat ];
svg.forEach(that => {
let label = that.parentNode;
let counter = label.querySelector(".count");
let is_active = that.classList.contains("active");
that.classList.toggle("active");
if (is_active)
{
// Animation
that.classList.remove("active-anim");
// Flip itype value
type.value = type.value.replace("un", "");
}
else {
that.classList.add("active-anim");
type.value = "un" + type.value;
}
counter.innerHTML = change_count_text(counter.innerHTML, is_active ? -1 : 1);
});
}
function status_event(e)
{
let target = e.target.closest(".statbtn");
console.log(target);
if (target)
{
// Don't JS these
if (target.classList.contains("reply-btn") || target.classList.contains("view-btn"))
return true;
let type = target.parentNode.querySelector(".itype");
if (type === null)
return true;
let status = e.srcElement;
send_request("/treebird_api/v1/interact",
send_request("/treebird_api/v1/interact",
{
id: status.id,
itype: type.value
},
"POST",
(xhr, args) => {
if (xhr.status !== 200)
{
id: status.id,
itype: type.value
},
"POST",
(xhr, args) => {
if (xhr.status !== 200)
{
// Undo action if failure
interact_action(status, type);
}
}, null);
// Undo action if failure
interact_action(status, type);
}
}, null);
interact_action(status, type);
e.preventDefault();
return false;
}
interact_action(status, type);
e.preventDefault();
return false;
}
}
function frame_resize()
function frame_resize()
{
let rightbar_frame = document.querySelector("#rightbar .sidebar-frame");
let rbar_frame_win = rightbar_frame.contentWindow;
rightbar_frame.height = rbar_frame_win.document.body.scrollHeight;
}
function filesize_to_str(bs)
{
const val = bs === 0 ? 0 : Math.floor(Math.log(bs) / Math.log(1024));
return (bs / 1024**val).toFixed(1) * 1 + ['B', 'kB', 'MB', 'GB', 'TB'][val];
}
function html_encode(str)
{
let en = document.createElement("span");
en.textContent = str;
return en.innerHTML;
}
function construct_file_upload(file, file_content)
{
let container = document.createElement("div");
let content = document.createElement("img");
let info = document.createElement("span");
container.className = "file-upload";
info.className = "upload-info";
info.innerHTML = `<span class="filesize">${filesize_to_str(file.size)}</span> &bull; <span class="filename">${html_encode(file.name)}</span>`;
let progress_div = document.createElement("div");
progress_div.className = "file-progress";
info.appendChild(progress_div);
content.src = file_content;
content.className = "upload-content";
container.appendChild(content);
container.appendChild(info);
return container;
}
function update_uploads_json(dom)
{
let root = dom.parentNode;
let items = root.getElementsByClassName("file-upload");
let ids = [];
for (let i of items)
{
let rightbar_frame = document.querySelector("#rightbar .sidebar-frame");
let rbar_frame_win = rightbar_frame.contentWindow;
rightbar_frame.height = rbar_frame_win.document.body.scrollHeight;
if (i.dataset.id)
ids.push(i.dataset.id);
}
function filesize_to_str(bs)
// Goto statusbox
root = root.parentNode;
let file_ids = root.querySelector(".file-ids-json");
if (!file_ids)
{
const val = bs === 0 ? 0 : Math.floor(Math.log(bs) / Math.log(1024));
return (bs / 1024**val).toFixed(1) * 1 + ['B', 'kB', 'MB', 'GB', 'TB'][val];
// Create if doesn't exist
file_ids = document.createElement("input");
file_ids.type = "hidden";
file_ids.className = "file-ids-json";
file_ids.name = "fileids";
root.appendChild(file_ids);
}
function html_encode(str)
file_ids.value = JSON.stringify(ids);
}
function evt_file_upload(e)
{
let target = e.target;
let file_upload_dom = this.closest("form").querySelector(".file-uploads-container");
file_upload_dom.className = "file-uploads-container";
const files = [...this.files];
let reader;
// Clear file input
this.value = '';
// Create file upload
for (let file of files)
{
let en = document.createElement("span");
en.textContent = str;
return en.innerHTML;
}
reader = new FileReader();
reader.onload = (() => {
return (e) => {
let file_dom = construct_file_upload(file, e.target.result);
file_upload_dom.appendChild(file_dom);
function construct_file_upload(file, file_content)
let xhr = upload_file("/treebird_api/v1/attachment",
"file",
file,
(xhr, args) => {
// TODO errors
file_dom.dataset.id = JSON.parse(xhr.response).id;
update_uploads_json(file_dom);
}, null,
(e) => {
let upload_file_progress = file_dom
.querySelector(".file-progress");
// Add offset of 3
upload_file_progress.style.width = 3+((e.loaded/e.total)*97);
},
(e) => {
file_dom.querySelector(".upload-content").style.opacity = "1.0";
file_dom.querySelector(".file-progress").remove();
});
}
})(file);
reader.readAsDataURL(file);
}
}
// Main (when loaded)
document.addEventListener('DOMContentLoaded', () => {
let interact_btn = document.getElementsByClassName("status");
// Add event listener to add specificied buttons
for (let i = 0; i < interact_btn.length; ++i)
{
let container = document.createElement("div");
let content = document.createElement("img");
let info = document.createElement("span");
container.className = "file-upload";
info.className = "upload-info";
info.innerHTML = `<span class="filesize">${filesize_to_str(file.size)}</span> &bull; <span class="filename">${html_encode(file.name)}</span>`;
let progress_div = document.createElement("div");
progress_div.className = "file-progress";
info.appendChild(progress_div);
content.src = file_content;
content.className = "upload-content";
container.appendChild(content);
container.appendChild(info);
return container;
interact_btn[i].addEventListener('click', status_event);
}
function update_uploads_json(dom)
// Resize notifications iFrame to full height
let rightbar_frame = document.querySelector("#rightbar .sidebar-frame");
if (rightbar_frame)
{
let root = dom.parentNode;
let items = root.getElementsByClassName("file-upload");
let ids = [];
for (let i of items)
{
if (i.dataset.id)
ids.push(i.dataset.id);
}
// Goto statusbox
root = root.parentNode;
let file_ids = root.querySelector(".file-ids-json");
if (!file_ids)
{
// Create if doesn't exist
file_ids = document.createElement("input");
file_ids.type = "hidden";
file_ids.className = "file-ids-json";
file_ids.name = "fileids";
root.appendChild(file_ids);
}
file_ids.value = JSON.stringify(ids);
rightbar_frame.contentWindow.addEventListener('DOMContentLoaded', frame_resize);
}
function evt_file_upload(e)
// File upload
let file_inputs = document.querySelectorAll(".statusbox input[type=file]");
for (let file_input of file_inputs)
{
let target = e.target;
let file_upload_dom = this.closest("form").querySelector(".file-uploads-container");
file_upload_dom.className = "file-uploads-container";
const files = [...this.files];
let reader;
// Clear file input
this.value = '';
// Create file upload
for (let file of files)
{
reader = new FileReader();
reader.onload = (() => {
return (e) => {
let file_dom = construct_file_upload(file, e.target.result);
file_upload_dom.appendChild(file_dom);
let xhr = upload_file("/treebird_api/v1/attachment",
"file",
file,
(xhr, args) => {
// TODO errors
file_dom.dataset.id = JSON.parse(xhr.response).id;
update_uploads_json(file_dom);
}, null,
(e) => {
let upload_file_progress = file_dom
.querySelector(".file-progress");
// Add offset of 3
upload_file_progress.style.width = 3+((e.loaded/e.total)*97);
},
(e) => {
file_dom.querySelector(".upload-content").style.opacity = "1.0";
file_dom.querySelector(".file-progress").remove();
});
}
})(file);
reader.readAsDataURL(file);
}
file_input.addEventListener('change', evt_file_upload);
}
// Main (when loaded)
document.addEventListener('DOMContentLoaded', () => {
let interact_btn = document.getElementsByClassName("status");
// Add event listener to add specificied buttons
for (let i = 0; i < interact_btn.length; ++i)
{
interact_btn[i].addEventListener('click', status_event);
}
// Resize notifications iFrame to full height
let rightbar_frame = document.querySelector("#rightbar .sidebar-frame");
if (rightbar_frame)
{
rightbar_frame.contentWindow.addEventListener('DOMContentLoaded', frame_resize);
}
// File upload
let file_inputs = document.querySelectorAll(".statusbox input[type=file]");
for (let file_input of file_inputs)
{
file_input.addEventListener('change', evt_file_upload);
}
});
})();
});

2
dist/treebird.css vendored
View file

@ -1894,7 +1894,7 @@ p}
stroke: #303030;
}
.active-anim
.interacted-anim
{
animation: interact .7s 1;
}

View file

@ -207,7 +207,9 @@
</tr>
</table>
</div>
<!-- Soy filled JS lib -->
<script src="$prefix/js/redom.min.js"></script>
<!-- Source -->
<script src="$prefix/js/main.js"></script>
<script src="$prefix/js/emoji.js"></script>

View file

@ -172,7 +172,7 @@
</label>
</form>
[% END %]
<a target="_parent" class="statbtn statbtn-last" href="$prefix/status/[% status.id %]/react#[% status.id %]" class="pointer statbtn react-btn">
<a target="_parent" class="statbtn pointer statbtn-last react-btn" href="$prefix/status/[% status.id %]/react#[% status.id %]">
[% icon('emoji') %]
</a>
[% IF emoji_picker -%]