
You can edit relays, embed options, and sign out. This moves the signout button from the nav to the settings area.
355 lines
10 KiB
JavaScript
355 lines
10 KiB
JavaScript
/* This file contains utility functions related to UI manipulation. Some code
|
|
* may be specific to areas of the UI and others are more utility based. As
|
|
* this file grows specific UI area code should be migrated to its own file.
|
|
*/
|
|
|
|
/* toggle_cw changes the active stage of the Content Warning for a post. It is
|
|
* relative to the element that is pressed.
|
|
*/
|
|
function toggle_cw(el) {
|
|
el.classList.toggle("active");
|
|
const isOn = el.classList.contains("active");
|
|
const input = el.parentElement.querySelector("input.cw");
|
|
input.classList.toggle("hide", !isOn);
|
|
}
|
|
|
|
/* toggle_gnav hides or shows the global navigation's additional buttons based
|
|
* on its opened state.
|
|
*/
|
|
function toggle_gnav(el) {
|
|
el.parentElement.classList.toggle("open");
|
|
}
|
|
|
|
function close_gnav() {
|
|
find_node("#gnav").classList.remove("open");
|
|
}
|
|
|
|
/* post_input_changed checks the content of the textarea and updates the size
|
|
* of it's element. Additionally I will toggle the enabled state of the sending
|
|
* button.
|
|
*/
|
|
function post_input_changed(el) {
|
|
el.style.height = `0px`;
|
|
el.style.height = `${el.scrollHeight}px`;
|
|
let btn = el.parentElement.querySelector("button[role=send]");
|
|
if (btn) btn.disabled = el.value === "";
|
|
}
|
|
|
|
/* init_message_textareas finds all message textareas and updates their initial
|
|
* height based on their content (0). This is so there is no jaring affect when
|
|
* the page loads.
|
|
*/
|
|
function init_message_textareas() {
|
|
const els = document.querySelectorAll(".post-input");
|
|
for (const el of els) {
|
|
post_input_changed(el);
|
|
}
|
|
}
|
|
|
|
// update_notification_markers will find all markers and hide or show them
|
|
// based on the passed in state of 'active'. This applies to the navigation
|
|
// icons.
|
|
function update_notification_markers(active) {
|
|
let els = document.querySelectorAll(".new-notifications")
|
|
for (const el of els) {
|
|
el.classList.toggle("hide", !active)
|
|
}
|
|
}
|
|
|
|
/* show_profile updates the current view to the profile display and updates the
|
|
* information to the relevant profile based on the public key passed.
|
|
* TODO handle async waiting for relay not yet connected
|
|
*/
|
|
function show_profile(pk) {
|
|
switch_view("profile");
|
|
const model = DAMUS;
|
|
const profile = model_get_profile(model, pk).data;
|
|
const el = find_node("#profile-view");
|
|
// TODO show loading indicator then render
|
|
|
|
find_node("[role='profile-image']", el).src = get_picture(pk, profile);
|
|
find_nodes("[role='profile-name']", el).forEach(el => {
|
|
el.innerText = fmt_profile_name(profile, fmt_pubkey(pk));
|
|
});
|
|
|
|
const el_nip5 = find_node("[role='profile-nip5']", el)
|
|
el_nip5.innerText = profile.nip05;
|
|
el_nip5.classList.toggle("hide", !profile.nip05);
|
|
|
|
const el_desc = find_node("[role='profile-desc']", el)
|
|
el_desc.innerHTML = newlines_to_br(profile.about);
|
|
el_desc.classList.toggle("hide", !profile.about);
|
|
|
|
find_node("button[role='copy-pk']", el).dataset.pk = pk;
|
|
|
|
const btn_follow = find_node("button[role='follow-user']", el)
|
|
btn_follow.dataset.pk = pk;
|
|
// TODO check follow status
|
|
btn_follow.innerText = 1 == 1 ? "Follow" : "Unfollow";
|
|
btn_follow.classList.toggle("hide", pk == model.pubkey);
|
|
}
|
|
|
|
/* newlines_to_br takes a string and converts all newlines to HTML 'br' tags.
|
|
*/
|
|
function newlines_to_br(str="") {
|
|
return str.split("\n").reduce((acc, part, index) => {
|
|
return acc + part + "<br/>";
|
|
}, "");
|
|
}
|
|
|
|
/* click_copy_pk writes the element's dataset.pk field to the users OS'
|
|
* clipboard. No we don't use fallback APIs, use a recent browser.
|
|
*/
|
|
function click_copy_pk(el) {
|
|
// TODO show toast
|
|
navigator.clipboard.writeText(el.dataset.pk);
|
|
}
|
|
|
|
/* click_follow_user sends the event to the relay to subscribe the active user
|
|
* to the target public key of the element's dataset.pk field.
|
|
*/
|
|
function click_toggle_follow_user(el) {
|
|
const { contacts } = DAMUS;
|
|
const pubkey = el.dataset.pk;
|
|
const is_friend = contacts.friends.has(pubkey);
|
|
if (is_friend) {
|
|
contacts.friends.delete(pubkey);
|
|
} else {
|
|
contacts.friends.add(pubkey);
|
|
}
|
|
el.innerText = is_friend ? "Follow" : "Unfollow";
|
|
contacts_save(contacts);
|
|
if (window.confirm("Contacts are saved locally. Do you want to sync you contacts with all relays?")) {
|
|
update_contacts();
|
|
}
|
|
}
|
|
|
|
function show_new() {
|
|
view_timeline_show_new(DAMUS);
|
|
}
|
|
|
|
function click_share(el) {
|
|
share(el.dataset.evid);
|
|
}
|
|
|
|
function click_toggle_like(el) {
|
|
// Disable the button to prevent multiple presses
|
|
el.disabled = true;
|
|
if (el.dataset.liked == "yes") {
|
|
delete_post(el.dataset.reactionId);
|
|
return;
|
|
}
|
|
send_reply(R_HEART, el.dataset.reactingTo);
|
|
}
|
|
|
|
/* open_media_preview presents a modal to display an image via "url".
|
|
*/
|
|
function open_media_preview(url, type) {
|
|
const el = find_node("#media-preview");
|
|
el.classList.remove("closed");
|
|
find_node("img", el).src = url;
|
|
// TODO handle different medias such as audio and video
|
|
// TODO add loading state & error checking
|
|
}
|
|
|
|
/* close_media_preview closes any present media modal.
|
|
*/
|
|
function close_media_preview() {
|
|
find_node("#media-preview").classList.add("closed");
|
|
}
|
|
|
|
function close_reply() {
|
|
const modal = document.querySelector("#reply-modal")
|
|
modal.classList.add("closed");
|
|
}
|
|
|
|
function delete_post_confirm(evid) {
|
|
if (!confirm("Are you sure you want to delete this post?"))
|
|
return;
|
|
const reason = (prompt("Why you are deleting this? Leave empty to not specify. Type CANCEL to cancel.") || "").trim()
|
|
if (reason.toLowerCase() === "cancel")
|
|
return;
|
|
delete_post(evid, reason)
|
|
}
|
|
|
|
async function do_send_reply() {
|
|
const modal = document.querySelector("#reply-modal");
|
|
const replying_to = modal.querySelector("#replying-to");
|
|
const evid = replying_to.dataset.evid;
|
|
const all = replying_to.dataset.toAll != "";
|
|
const reply_content_el = document.querySelector("#reply-content");
|
|
const content = reply_content_el.value;
|
|
await send_reply(content, evid, all);
|
|
reply_content_el.value = "";
|
|
close_reply();
|
|
}
|
|
|
|
function reply(evid, all=false) {
|
|
const ev = DAMUS.all_events[evid]
|
|
const modal = document.querySelector("#reply-modal")
|
|
const replybox = modal.querySelector("#reply-content")
|
|
const replying_to = modal.querySelector("#replying-to")
|
|
replying_to.dataset.evid = evid
|
|
replying_to.dataset.toAll = all ? "all" : "";
|
|
replying_to.innerHTML = render_event_nointeract(DAMUS, ev, {
|
|
is_composing: true,
|
|
nobar: true
|
|
});
|
|
modal.classList.remove("closed")
|
|
replybox.focus()
|
|
}
|
|
|
|
function reply_author(evid) {
|
|
reply(evid);
|
|
}
|
|
|
|
function reply_all(evid) {
|
|
reply(evid, true);
|
|
}
|
|
|
|
function redraw_my_pfp(model) {
|
|
const p = model_get_profile(model, model.pubkey).data;
|
|
const html = render_pfp(model.pubkey, p || {});
|
|
const el = document.querySelector(".my-userpic");
|
|
el.innerHTML = html;
|
|
}
|
|
|
|
function update_favicon(path) {
|
|
let link = document.querySelector("link[rel~='icon']");
|
|
const head = document.getElementsByTagName('head')[0]
|
|
|
|
if (!link) {
|
|
link = document.createElement('link');
|
|
link.rel = 'icon';
|
|
head.appendChild(link);
|
|
}
|
|
|
|
link.href = path;
|
|
}
|
|
|
|
// update_notifications updates the document title & visual indicators based on if the
|
|
// number of notifications that are unseen by the user.
|
|
function update_notifications(model) {
|
|
const { count } = model.notifications;
|
|
const suffix = "Yo Sup";
|
|
document.title = count ? `(${count}) ${suffix}` : suffix;
|
|
// TODO I broke the favicons. I will fix with notications update
|
|
//update_favicon(has_notes ? "img/damus_notif.svg" : "img/damus.svg");
|
|
update_notification_markers(count);
|
|
}
|
|
|
|
async function get_pubkey(use_prompt=true) {
|
|
let pubkey = get_local_state('pubkey')
|
|
if (pubkey)
|
|
return pubkey
|
|
if (window.nostr && window.nostr.getPublicKey) {
|
|
log_debug("calling window.nostr.getPublicKey()...")
|
|
try {
|
|
pubkey = await window.nostr.getPublicKey()
|
|
return await handle_pubkey(pubkey)
|
|
} catch (err) {
|
|
return;
|
|
}
|
|
log_debug("got %s pubkey from nos2x", pubkey)
|
|
}
|
|
if (!use_prompt)
|
|
return;
|
|
pubkey = prompt("Enter Nostr ID (eg: jb55@jb55.com) or public key (hex or npub).")
|
|
if (!pubkey.trim())
|
|
return;
|
|
return await handle_pubkey(pubkey)
|
|
}
|
|
|
|
function get_privkey() {
|
|
let privkey = get_local_state('privkey')
|
|
if (privkey)
|
|
return privkey
|
|
if (!privkey)
|
|
privkey = prompt("Enter private key")
|
|
if (!privkey)
|
|
throw new Error("can't get privkey")
|
|
if (privkey[0] === "n") {
|
|
privkey = bech32_decode(privkey)
|
|
}
|
|
set_local_state('privkey', privkey)
|
|
return privkey
|
|
}
|
|
|
|
function open_thread(thread_id) {
|
|
view_timeline_apply_mode(DAMUS, VM_THREAD, { thread_id });
|
|
}
|
|
|
|
function open_profile(pubkey) {
|
|
view_timeline_apply_mode(DAMUS, VM_USER, { pubkey });
|
|
view_update_profile(DAMUS, pubkey);
|
|
}
|
|
|
|
function open_faqs() {
|
|
find_node("#faqs").classList.remove("closed");
|
|
}
|
|
|
|
function close_modal(el) {
|
|
while (el.parentElement) {
|
|
if (el.classList.contains("modal")) {
|
|
el.classList.add("closed");
|
|
break;
|
|
}
|
|
el = el.parentElement;
|
|
}
|
|
}
|
|
|
|
function view_update_profile(model, pubkey) {
|
|
const profile = model_get_profile(model, pubkey);
|
|
const el = find_node("[role='profile-info']");
|
|
|
|
const name = fmt_profile_name(profile.data, fmt_pubkey(pubkey));
|
|
find_node("#view header > label").innerText = name;
|
|
find_node("[role='profile-image']", el).src = get_picture(pubkey, profile.data);
|
|
find_nodes("[role='profile-name']", el).forEach(el => {
|
|
el.innerText = name;
|
|
});
|
|
|
|
const el_nip5 = find_node("[role='profile-nip5']", el)
|
|
el_nip5.innerText = profile.data.nip05;
|
|
el_nip5.classList.toggle("hide", !profile.data.nip05);
|
|
|
|
const el_desc = find_node("[role='profile-desc']", el)
|
|
el_desc.innerHTML = newlines_to_br(linkify(profile.data.about));
|
|
el_desc.classList.toggle("hide", !profile.data.about);
|
|
|
|
find_node("button[role='copy-pk']", el).dataset.pk = pubkey;
|
|
find_node("button[role='edit-profile']", el)
|
|
.classList.toggle("hide", pubkey != model.pubkey);
|
|
|
|
const btn_follow = find_node("button[role='follow-user']", el)
|
|
btn_follow.dataset.pk = pubkey;
|
|
// TODO check follow status
|
|
btn_follow.innerText = contact_is_friend(model.contacts, pubkey) ? "Unfollow" : "Follow";
|
|
btn_follow.classList.toggle("hide", pubkey == model.pubkey);
|
|
}
|
|
|
|
const PROFILE_FIELDS = ['name', 'picture', 'nip05', 'about'];
|
|
|
|
function show_profile_editor() {
|
|
const p = model_get_profile(DAMUS, DAMUS.pubkey);
|
|
const el = find_node("#profile-editor");
|
|
el.classList.remove("closed");
|
|
for (const key of PROFILE_FIELDS) {
|
|
find_node(`[name='${key}']`, el).value = p.data[key];
|
|
}
|
|
}
|
|
|
|
function click_update_profile() {
|
|
const el = find_node("#profile-editor");
|
|
const btn = find_node("button.action", el);
|
|
const p = {
|
|
name: find_node("input[name='name']", el).value,
|
|
picture: find_node("input[name='picture']", el).value,
|
|
nip05: find_node("input[name='nip05']", el).value,
|
|
about: find_node("textarea[name='about']", el).value,
|
|
};
|
|
Object.assign(p, model_get_profile(DAMUS, DAMUS.pubkey).data, p);
|
|
update_profile(p);
|
|
close_modal(el);
|
|
// TODO show toast that say's "broadcasted!"
|
|
}
|