// 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` replying in thread ${fmt_pubkey(ev.refs.reply)}`; } else { pubkeys = [replying_to.pubkey]; } } const names = pubkeys.map((pk) => { return render_name(pk, model_get_profile(model, pk).data); }).join(", ") return ` replying to ${names} ` } 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 `
Shared by ${render_name(pubkey, profile)}
` } function render_event(model, ev, opts={}) { switch(ev.kind) { case KIND_SHARE: return render_share(model, ev, opts); case KIND_DM: return render_dm(model, ev, opts); } const profile = model_get_profile(model, ev.pubkey); const delta = fmt_since_str(new Date().getTime(), ev.created_at*1000) let classes = "event" if (!opts.is_composing) classes += " bottom-border"; return html`
$${render_profile_img(profile)}
$${render_name(ev.pubkey, profile.data)} ${delta}
$${render_event_body(model, ev, opts)}
` } function render_dm(model, ev, opts) { let classes = "event" if (ev.kind == KIND_DM) { classes += " dm"; if (ev.pubkey == model.pubkey) classes += " mine"; } const profile = model_get_profile(model, ev.pubkey); const delta = fmt_since_str(new Date().getTime(), ev.created_at*1000) let show_media = event_shows_media(model, ev, model.embeds); return html`

$${format_content(model, ev, show_media)}

${delta}
` } function event_shows_media(model, ev, mode) { if (mode == "friends") return model.contacts.friends.has(ev.pubkey); return true; } function rerender_dm(model, ev, el) { let show_media = event_shows_media(model, ev, model.embeds); find_node(".body > p", el).innerHTML = format_content(model, ev, show_media); } function render_event_nointeract(model, ev, opts={}) { const profile = model_get_profile(model, ev.pubkey); const delta = fmt_since_str(new Date().getTime(), ev.created_at*1000) return html`
$${render_profile_img(profile)}
$${render_name(ev.pubkey, profile.data)} ${delta}
$${render_event_body(model, ev, opts)}
` } 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 = "
"; str += shared ? render_shared_by(ev, opts) : render_replying_to(model, ev); str += `

${format_content(model, ev, show_media)}

`; str += render_reactions(model, ev); str += opts.nobar || ev.kind == KIND_DM ? "" : 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`action="reply" data-emoji="${emoji}" data-to="${reacting_to}"`; } else { return html`action="delete" data-evid="${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) continue; const pubkey = reactions[k].pubkey; str += render_profile_img(model_get_profile(model, pubkey), {noclick:true}); } if (count > 5) str = `${count}`; let onclick = render_react_onclick(model.pubkey, reacting_to.id, emoji, reactions); return html` ${emoji} $${str} `; } 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`
`; if (!shared && event_can_reply(ev)) { str += html` `; } if (!shared) { str += html``; } str += ` `; if (can_delete) { const delete_id = shared ? shared.share_evid : ev.id; str += html` ` } return str + "
"; } 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`
$${render_reactions_inner(model, ev)}
` } // 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`${prefix} ${fmt_profile_name(profile, fmt_pubkey(pk))}` } function render_profile_img(profile, noclick=false) { const name = fmt_name(profile); let str = html`class="pfp clickable" action="open-profile"`; if (noclick) str = "class='pfp'"; return html`` //onerror="this.onerror=null;this.src='${IMG_NO_USER}';" }