render.js (9464B)
1 // This file contains all methods related to rendering UI elements. Rendering 2 // is done by simple string manipulations & templates. If you need to write 3 // loops simply write it in code and return strings. 4 5 function render_timeline_event(damus, view, ev) 6 { 7 const root_id = get_thread_root_id(damus, ev.id) 8 const max_depth = root_id ? get_thread_max_depth(damus, view, root_id) : get_default_max_depth(damus, view) 9 10 if (ev.refs && ev.refs.root && view.expanded.has(ev.refs.root)) 11 max_depth = null 12 13 return render_event(damus, view, ev, {max_depth}) 14 } 15 16 function render_events(damus, view) { 17 log_debug("rendering events") 18 return view.events 19 .filter((ev, i) => i < 140) 20 .map((ev) => render_timeline_event(damus, view, ev)).join("\n") 21 } 22 23 function render_reply_line_top(has_top_line) { 24 const classes = has_top_line ? "" : "invisible" 25 return html`<div class="line-top ${classes}"></div>` 26 } 27 28 function render_reply_line_bot() { 29 return html`<div class="line-bot"></div>` 30 } 31 32 function render_thread_collapsed(model, ev, opts) 33 { 34 if (opts.is_composing) 35 return "" 36 return html`<div onclick="expand_thread('${ev.id}')" class="thread-collapsed"> 37 <div class="thread-summary clickable event-message"> 38 Read More 39 <img class="icon svg small" src="icon/read-more.svg"/> 40 </div> 41 </div>` 42 } 43 44 function render_replied_events(damus, view, ev, opts) 45 { 46 if (!(ev.refs && ev.refs.reply)) 47 return "" 48 49 const reply_ev = damus.all_events[ev.refs.reply] 50 if (!reply_ev) 51 return "" 52 53 opts.replies = opts.replies == null ? 1 : opts.replies + 1 54 if (!(opts.max_depth == null || opts.replies < opts.max_depth)) 55 return render_thread_collapsed(damus, ev, opts) 56 57 opts.is_reply = true 58 return render_event(damus, view, reply_ev, opts) 59 } 60 61 function render_replying_to_chat(damus, ev) { 62 const chatroom = (ev.refs.root && damus.chatrooms[ev.refs.root]) || {} 63 const roomname = chatroom.name || ev.refs.root || "??" 64 const pks = ev.refs.pubkeys || [] 65 const names = pks.map(pk => render_mentioned_name(pk, damus.profiles[pk])).join(", ") 66 const to_users = pks.length === 0 ? "" : ` to ${names}` 67 68 return html`<div class="replying-to">replying${to_users} in <span class="chatroom-name">${roomname}</span></div>` 69 } 70 71 function render_replying_to(model, ev) { 72 if (!(ev.refs && ev.refs.reply)) 73 return "" 74 75 if (ev.kind === 42) 76 return render_replying_to_chat(model, ev) 77 78 let pubkeys = ev.refs.pubkeys || [] 79 if (pubkeys.length === 0 && ev.refs.reply) { 80 const replying_to = model.all_events[ev.refs.reply] 81 if (!replying_to) 82 return html`<div class="replying-to small-txt">reply to ${ev.refs.reply}</div>` 83 84 pubkeys = [replying_to.pubkey] 85 } 86 87 const names = ev.refs.pubkeys.map(pk => render_mentioned_name(pk, model.profiles[pk])).join(", ") 88 89 return html` 90 <span class="replying-to small-txt"> 91 replying to $${names} 92 </span> 93 ` 94 } 95 96 function render_unknown_event(damus, ev) { 97 return "Unknown event " + ev.kind 98 } 99 100 function render_share(damus, view, ev, opts) { 101 //todo validate content 102 const shared_ev = damus.all_events[ev.refs && ev.refs.root] 103 // share isn't resolved yet. that's ok, we can render this when we have 104 // the event 105 if (!shared_ev) 106 return "" 107 108 opts.shared = { 109 pubkey: ev.pubkey, 110 profile: damus.profiles[ev.pubkey] 111 } 112 return render_event(damus, view, shared_ev, opts) 113 } 114 115 function render_comment_body(damus, ev, opts) { 116 const can_delete = damus.pubkey === ev.pubkey; 117 const bar = !can_reply(ev) || opts.nobar? "" : render_action_bar(damus, ev, can_delete) 118 const show_media = !opts.is_composing 119 120 return html` 121 <div> 122 $${render_replying_to(damus, ev)} 123 $${render_shared_by(ev, opts)} 124 </div> 125 <p> 126 $${format_content(ev, show_media)} 127 </p> 128 $${render_reactions(damus, ev)} 129 $${bar} 130 ` 131 } 132 133 function render_shared_by(ev, opts) { 134 const b = opts.shared 135 if (!b) { 136 return "" 137 } 138 return html` 139 <div class="shared-by"> 140 Shared by $${render_name(b.pubkey, b.profile)} 141 </div> 142 ` 143 } 144 145 function render_deleted_comment_body(ev, deleted) { 146 if (deleted.content) { 147 return html` 148 <div class="deleted-comment event-message"> 149 This content was deleted with reason: 150 <div class="quote">${format_content(deleted, false)}</div> 151 </div> 152 ` 153 } 154 return html` 155 <div class="deleted-comment event-message"> 156 This content was deleted. 157 </div> 158 ` 159 } 160 161 function render_event(damus, view, ev, opts={}) { 162 if (ev.kind === 6) 163 return render_share(damus, view, ev, opts) 164 if (shouldnt_render_event(damus.pubkey, view, ev, opts)) 165 return "" 166 167 view.rendered.add(ev.id) 168 169 const profile = damus.profiles[ev.pubkey] 170 const delta = time_delta(new Date().getTime(), ev.created_at*1000) 171 172 const has_bot_line = opts.is_reply 173 const reply_line_bot = (has_bot_line && render_reply_line_bot()) || "" 174 175 const deleted = is_deleted(damus, ev.id) 176 if (deleted && !opts.is_reply) 177 return "" 178 179 const replied_events = render_replied_events(damus, view, ev, opts) 180 181 let name = "" 182 if (!deleted) { 183 name = render_name_plain(profile) 184 } 185 186 const has_top_line = replied_events !== "" 187 const border_bottom = opts.is_composing || has_bot_line ? "" : "bottom-border"; 188 return html` 189 $${replied_events} 190 <div id="ev${ev.id}" class="event ${border_bottom}"> 191 <div class="userpic"> 192 $${render_reply_line_top(has_top_line)} 193 $${deleted ? render_deleted_pfp() : render_pfp(ev.pubkey, profile)} 194 $${reply_line_bot} 195 </div> 196 <div class="event-content"> 197 <div class="info"> 198 $${render_name(ev.pubkey, profile)} 199 <span class="timestamp">${delta}</span> 200 <button class="icon" title="View Thread" role="view-event" data-eid="${ev.id}" onclick="click_event(this)"> 201 <img class="icon svg small" src="icon/open-thread.svg"/> 202 </button> 203 </div> 204 <div class="comment"> 205 $${deleted ? render_deleted_comment_body(ev, deleted) : render_comment_body(damus, ev, opts)} 206 </div> 207 </div> 208 </div> 209 ` 210 } 211 212 function render_react_onclick(our_pubkey, reacting_to, emoji, reactions) { 213 const reaction = reactions[our_pubkey] 214 if (!reaction) { 215 return html`onclick="send_reply('${emoji}', '${reacting_to}')"` 216 } else { 217 return html`onclick="delete_post('${reaction.id}')"` 218 } 219 } 220 221 function render_reaction_group(model, emoji, reactions, reacting_to) { 222 const pfps = Object.keys(reactions).map((pk) => render_reaction(model, reactions[pk])) 223 224 let onclick = render_react_onclick(model.pubkey, reacting_to.id, emoji, reactions) 225 226 return html` 227 <span $${onclick} class="reaction-group clickable"> 228 <span class="reaction-emoji"> 229 ${emoji} 230 </span> 231 $${pfps.join("\n")} 232 </span> 233 ` 234 } 235 236 function render_reaction(model, reaction) { 237 const profile = model.profiles[reaction.pubkey] 238 let emoji = reaction.content[0] 239 if (reaction.content === "+" || reaction.content === "") 240 emoji = "❤️" 241 242 return render_pfp(reaction.pubkey, profile) 243 } 244 245 function render_action_bar(damus, ev, can_delete) { 246 let delete_html = "" 247 if (can_delete) 248 delete_html = html` 249 <button class="icon" title="Delete" onclick="delete_post_confirm('${ev.id}')"> 250 <img class="icon svg small" src="icon/event-delete.svg"/> 251 </button>` 252 253 const groups = get_reactions(damus, ev.id) 254 const like = "❤️" 255 const likes = groups[like] || {} 256 const react_onclick = render_react_onclick(damus.pubkey, ev.id, like, likes) 257 return html` 258 <div class="action-bar"> 259 <button class="icon" title="Reply" onclick="reply_to('${ev.id}')"> 260 <img class="icon svg small" src="icon/event-reply.svg"/> 261 </button> 262 <button class="icon react heart" $${react_onclick} title="Like"> 263 <img class="icon svg small" src="icon/event-like.svg"/> 264 </button> 265 <!--<button class="icon" title="Share" onclick=""><i class="fa fa-fw fa-link"></i></a>--> 266 $${delete_html} 267 <!--<button class="icon" title="View raw Nostr event." onclick=""><i class="fa-solid fa-fw fa-code"></i></a>--> 268 </div> 269 ` 270 } 271 272 function render_reactions(model, ev) { 273 const groups = get_reactions(model, ev.id) 274 let str = "" 275 276 for (const emoji of Object.keys(groups)) { 277 str += render_reaction_group(model, emoji, groups[emoji], ev) 278 } 279 280 return html` 281 <div class="reactions"> 282 $${str} 283 </div> 284 ` 285 } 286 287 // Utility Methods 288 289 /* render_name_plain takes in a profile and tries it's best to return a string 290 * that is best suited for the profile. 291 */ 292 function render_name_plain(profile=DEFAULT_PROFILE) { 293 const display_name = profile.display_name || profile.user 294 const username = profile.name || "anon" 295 const name = display_name || username 296 297 return profile.name 298 } 299 300 function render_pubkey(pk) 301 { 302 return pk.slice(-8) 303 } 304 305 function render_username(pk, profile) 306 { 307 return (profile && profile.name) || render_pubkey(pk) 308 } 309 310 function render_mentioned_name(pk, profile) { 311 return render_name(pk, profile, "@"); 312 //return `<span class="username">@${render_username(pk, profile)}</span>` 313 } 314 315 function render_name(pk, profile, prefix="") { 316 return html` 317 <span class="username clickable" onclick="show_profile('${pk}')" 318 data-pk="${pk}">${prefix}${render_name_plain(profile)} 319 </span>` 320 } 321 322 function render_deleted_name() { 323 return "" 324 } 325 326 function render_pfp(pk, profile) { 327 const name = render_name_plain(profile) 328 return html`<img class="pfp" title="${name}" 329 onerror="this.onerror=null;this.src='${robohash(pk)}';" 330 src="${get_picture(pk, profile)}">` 331 } 332 333 function render_deleted_pfp() { 334 return html` 335 <div class="pfp deleted"> 336 <i class="fa-solid fa-fw fa-ghost"></i> 337 </div>` 338 } 339 340 function render_loading_spinner() 341 { 342 return html` 343 <div class="loading-events"> 344 <div class="loader" title="Loading..."> 345 <img class="dark-invert" src="icon/loader-fragment.svg"/> 346 </div> 347 </div>` 348 }