yosup/js/ui/render.js
Thomas Mathews e3b31af127 Added basic settings support.
You can edit relays, embed options, and sign out. This moves the signout
button from the nav to the settings area.
2022-12-29 21:56:06 -08:00

249 lines
7.6 KiB
JavaScript

// This file contains all methods related to rendering UI elements. Rendering
// is done by simple string manipulations & templates. If you need to write
// loops simply write it in code and return strings.
function render_replying_to(model, ev) {
if (!(ev.refs && ev.refs.reply))
return "";
let pubkeys = ev.refs.pubkeys || []
if (pubkeys.length === 0 && ev.refs.reply) {
const replying_to = model.all_events[ev.refs.reply]
// If there is no profile being replied to, it is simply a reply to an
// event itself, thus render it differently.
if (!replying_to) {
return html`<span class="replying-to small-txt">
replying in thread
<span class="thread-id clickable"
onclick="open_thread('${ev.refs.reply}')">
${fmt_pubkey(ev.refs.reply)}</span></span>`;
} else {
pubkeys = [replying_to.pubkey];
}
}
const names = pubkeys.map((pk) => {
return render_name(pk, model_get_profile(model, pk).data);
}).join(", ")
return `
<span class="replying-to small-txt">
replying to ${names}
</span>
`
}
function render_share(model, ev, opts) {
const shared_ev = model.all_events[ev.refs && ev.refs.root]
// If the shared event hasn't been resolved or leads to a circular event
// kind we will skip out on it.
if (!shared_ev || shared_ev.kind == KIND_SHARE)
return "";
opts.shared = {
pubkey: ev.pubkey,
profile: model_get_profile(model, ev.pubkey),
share_time: ev.created_at,
share_evid: ev.id,
}
return render_event(model, shared_ev, opts)
}
function render_shared_by(ev, opts) {
if (!opts.shared)
return "";
const { profile, pubkey } = opts.shared
return `<div class="shared-by">Shared by ${render_name(pubkey, profile)}
</div>`
}
function render_event(model, ev, opts={}) {
if (ev.kind == KIND_SHARE) {
return render_share(model, ev, opts);
}
const profile = model_get_profile(model, ev.pubkey);
const delta = fmt_since_str(new Date().getTime(), ev.created_at*1000)
const border_bottom = opts.is_composing ? "" : "bottom-border";
let thread_btn = "";
return html`<div id="ev${ev.id}" class="event ${border_bottom}">
<div class="userpic">
$${render_pfp(ev.pubkey, profile.data)}
</div>
<div class="event-content">
<div class="info">
$${render_name(ev.pubkey, profile.data)}
<span class="timestamp" data-timestamp="${ev.created_at}">${delta}</span>
</div>
<div class="comment">
$${render_event_body(model, ev, opts)}
</div>
</div>
</div>`
}
function render_event_nointeract(model, ev, opts={}) {
const profile = model_get_profiles(model, ev.pubkey);
const delta = fmt_since_str(new Date().getTime(), ev.created_at*1000)
return html`<div class="event border-bottom">
<div class="userpic">
$${render_pfp(ev.pubkey, profile.data)}
</div>
<div class="event-content">
<div class="info">
$${render_name(ev.pubkey, profile.data)}
<span class="timestamp" data-timestamp="${ev.created_at}">${delta}</span>
</div>
<div class="comment">
$${render_event_body(model, ev, opts)}
</div>
</div>
</div>`
}
function render_event_body(model, ev, opts) {
const { shared } = opts;
const can_delete = model.pubkey === ev.pubkey ||
(opts.shared && model.pubkey == opts.shared.pubkey);
// Only show media for content that is by friends.
let show_media = true;
if (opts.is_composing) {
show_media = false;
} else if (model.embeds == "friends") {
show_media = model.contacts.friends.has(ev.pubkey);
}
let str = "<div>";
str += shared ? render_shared_by(ev, opts) : render_replying_to(model, ev);
str += `</div><p>
${format_content(ev, show_media)}
</p>`;
str += render_reactions(model, ev);
str += opts.nobar ? "" :
render_action_bar(model, ev, {can_delete, shared});
return str;
}
function render_react_onclick(our_pubkey, reacting_to, emoji, reactions) {
const reaction = reactions[our_pubkey]
if (!reaction) {
return html`onclick="send_reply('${emoji}', '${reacting_to}')"`
} else {
return html`onclick="delete_post('${reaction.id}')"`
}
}
function render_reaction_group(model, emoji, reactions, reacting_to) {
let count = 0;
let str = "";
for (const k in reactions) {
count++;
if (count > 5) {
str = `${count}`;
break;
}
const pubkey = reactions[k].pubkey;
str += render_pfp(pubkey, model_get_profile(model, pubkey).data, {noclick:true});
}
let onclick = render_react_onclick(model.pubkey,
reacting_to.id, emoji, reactions);
return html`
<span $${onclick} class="reaction-group clickable">
<span class="reaction-emoji">
${emoji}
</span>
$${str}
</span>`;
}
function render_action_bar(model, ev, opts={}) {
const { pubkey } = model;
let { can_delete, shared } = opts;
// TODO rewrite all of the toggle heart code. It's mine & I hate it.
const thread_root = (ev.refs && ev.refs.root) || ev.id;
const reaction = model_get_reacts_to(model, pubkey, ev.id, R_HEART);
const liked = !!reaction;
const reaction_id = reaction ? reaction.id : "";
let str = html`<div class="action-bar">`;
if (!shared && event_can_reply(ev)) {
str += html`<button class="icon" title="Reply" onclick="reply_author('${ev.id}')">
<img class="icon svg small" src="icon/event-reply.svg"/>
</button>
<button class="icon" title="Reply All" onclick="reply_all('${ev.id}')">
<img class="icon svg small" src="icon/event-reply-all.svg"/>
</button>
<button class="icon react heart ${ab(liked, 'liked', '')}"
onclick="click_toggle_like(this)"
data-reaction-id="${reaction_id}"
data-reacting-to="${ev.id}"
title="$${ab(liked, 'Unlike', 'Like')}">
<img class="icon svg small ${ab(liked, 'dark-noinvert', '')}"
src="$${ab(liked, IMG_EVENT_LIKED, IMG_EVENT_LIKE)}"/>
</button>`;
}
if (!shared) {
str += html`<button class="icon" title="Share" data-evid="${ev.id}" onclick="click_share(this)">
<img class="icon svg small" src="icon/event-share.svg"/>
</button>`;
}
if (can_delete) {
const delete_id = shared ? shared.share_evid : ev.id;
str += html`
<button class="icon" title="Delete" onclick="delete_post_confirm('${delete_id}')">
<img class="icon svg small" src="icon/event-delete.svg"/>
</button>`
}
return str + `
<button class="icon" title="View Thread" role="view-event"
onclick="open_thread('${thread_root}')">
<img class="icon svg small" src="icon/open-thread.svg"/>
</button>
<button class="icon" title="View Replies" role="view-event"
onclick="open_thread('${ev.id}')">
<img class="icon svg small" src="icon/open-thread-here.svg"/>
</button></div>`;
}
function render_reactions_inner(model, ev) {
const groups = get_reactions(model, ev.id)
let str = ""
for (const emoji of Object.keys(groups)) {
str += render_reaction_group(model, emoji, groups[emoji], ev)
}
return str;
}
function render_reactions(model, ev) {
return html`<div class="reactions">$${render_reactions_inner(model, ev)}</div>`
}
// Utility Methods
function render_pubkey(pk) {
return fmt_pubkey(pk);
}
function render_username(pk, profile)
{
return (profile && profile.name) || render_pubkey(pk)
}
function render_mentioned_name(pk, profile) {
return render_name(pk, profile, "");
}
function render_name(pk, profile, prefix="") {
// Beware of whitespace.
return html`<span>${prefix}<span class="username clickable" data-pubkey="${pk}"
onclick="open_profile('${pk}')"
>${fmt_profile_name(profile, fmt_pubkey(pk))}</span></span>`
}
function render_pfp(pk, profile, opts={}) {
const name = fmt_profile_name(profile, fmt_pubkey(pk));
let str = html`class="pfp clickable" onclick="open_profile('${pk}')"`;
if (opts.noclick)
str = "class='pfp'";
return html`<img
$${str}
data-pubkey="${pk}"
title="${name}"
onerror="this.onerror=null;this.src='${IMG_NO_USER}';"
src="${get_picture(pk, profile)}"/>`
}