damus.io

damus.io website
git clone git://jb55.com/damus.io
Log | Files | Refs | README | LICENSE

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 }