small constellation + pds based little profile viewer karitham.tngl.io/gpreview?user=karitham.dev
gleam bsky-profile
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

src/gpreview: code cleanup

karitham 0103728c 809d3f5f

+154 -174
+2 -6
src/gpreview/effects.gleam
··· 53 53 ) 54 54 } 55 55 56 - pub fn fetch_thread_counts(_pds_host: String, uri: String) -> Effect(Msg) { 57 - let encoded_uri = 58 - uri 59 - |> string.replace(":", "%3A") 60 - |> string.replace("/", "%2F") 56 + pub fn fetch_thread_counts(_pds_host: String, url: String) -> Effect(Msg) { 61 57 let url = 62 58 "https://public.api.bsky.app" 63 59 <> "/xrpc/app.bsky.feed.getPostThread?uri=" 64 - <> encoded_uri 60 + <> url |> uri.percent_encode 65 61 <> "&depth=0" 66 62 rsvp.get(url, rsvp.expect_json(decode_thread_response(), ThreadWasFetched)) 67 63 }
+152 -168
src/gpreview/views.gleam
··· 2 2 import gleam/int 3 3 import gleam/list 4 4 import gleam/option.{type Option, None, Some} 5 + import gleam/result 5 6 import gleam/string 6 7 import gpreview/effects 7 8 import gpreview/record.{Record} ··· 19 20 } 20 21 21 22 fn input_zone(model: Model) -> Element(Msg) { 22 - let error = case model.post { 23 - Some(Error(e)) -> Some(e) 24 - _ -> None 25 - } 26 23 html.div([attribute.attribute("class", "input-zone")], [ 27 24 html.div([attribute.attribute("class", "input-zone__row")], [ 28 25 html.input([ ··· 43 40 [html.text("Show")], 44 41 ), 45 42 ]), 46 - case error { 47 - None -> element.none() 48 - Some(msg) -> error_badge(msg) 49 - }, 43 + elem_or_none( 44 + case model.post { 45 + Some(Error(e)) -> Some(e) 46 + _ -> None 47 + }, 48 + error_badge, 49 + ), 50 50 ]) 51 51 } 52 52 ··· 147 147 ]) 148 148 } 149 149 150 + fn elem_or_none( 151 + value: Option(a), 152 + f: fn(a) -> element.Element(b), 153 + ) -> element.Element(b) { 154 + value |> option.map(f) |> option.unwrap(element.none()) 155 + } 156 + 150 157 fn reply_context(reply: Option(decoders.ReplyRefJson)) -> Element(Msg) { 151 - case reply { 152 - Some(reply_ref) -> { 153 - let parent_did = 154 - reply_ref.parent.uri 155 - |> effects.extract_did_from_uri 156 - |> fn(r) { 157 - case r { 158 - Ok(d) -> d 159 - Error(_) -> "unknown" 160 - } 161 - } 162 - let short_did = string.slice(parent_did, 0, 20) <> "…" 163 - html.div([attribute.attribute("class", "reply-context")], [ 164 - html.span([attribute.attribute("class", "reply-context__label")], [ 165 - html.text("Replying to " <> short_did), 166 - ]), 167 - ]) 168 - } 169 - None -> element.none() 170 - } 158 + elem_or_none(reply, fn(reply_ref) { 159 + let parent_did = 160 + reply_ref.parent.uri 161 + |> effects.extract_did_from_uri 162 + |> result.unwrap("unknown") 163 + 164 + html.div([attribute.attribute("class", "reply-context")], [ 165 + html.span([attribute.attribute("class", "reply-context__label")], [ 166 + html.text("Replying to " <> string.slice(parent_did, 0, 20) <> "..."), 167 + ]), 168 + ]) 169 + }) 171 170 } 172 171 173 172 fn post_header( ··· 205 204 attribute.attribute("referrerpolicy", "no-referrer"), 206 205 ]) 207 206 None -> { 208 - let initial = case profile.display_name { 209 - Some(n) -> string.slice(n, 0, 1) 210 - None -> "@" 211 - } 212 207 html.div([attribute.attribute("class", "avatar-fallback")], [ 213 - html.text(initial), 208 + html.text( 209 + profile.display_name 210 + |> option.map(fn(n: String) { string.slice(n, 0, 1) }) 211 + |> option.unwrap("@"), 212 + ), 214 213 ]) 215 214 } 216 215 } 217 216 } 218 217 219 218 fn display_name(name: Option(String)) -> Element(Msg) { 220 - case name { 221 - Some(n) -> 222 - html.span( 223 - [ 224 - attribute.attribute("class", "post-header__name"), 225 - ], 226 - [html.text(n)], 227 - ) 228 - None -> element.none() 229 - } 219 + elem_or_none(name, fn(n) { 220 + html.span( 221 + [ 222 + attribute.attribute("class", "post-header__name"), 223 + ], 224 + [html.text(n)], 225 + ) 226 + }) 230 227 } 231 228 232 229 fn description_line(desc: Option(String)) -> Element(Msg) { 233 - case desc { 234 - Some(d) -> 235 - html.p( 236 - [ 237 - attribute.attribute("class", "post-header__bio"), 238 - ], 239 - [html.text(d)], 240 - ) 241 - None -> element.none() 242 - } 230 + elem_or_none(desc, fn(d) { 231 + html.p( 232 + [ 233 + attribute.attribute("class", "post-header__bio"), 234 + ], 235 + [html.text(d)], 236 + ) 237 + }) 243 238 } 244 239 245 240 pub fn blob_ref_to_url(pds_host: String, did: String, blob: String) -> String { ··· 368 363 pds_host: String, 369 364 did: String, 370 365 ) -> Element(Msg) { 371 - case embed { 372 - None -> element.none() 373 - Some(embed_obj) -> 374 - case embed_obj { 375 - decoders.Images(images) -> 376 - html.div([attribute.attribute("class", "post-embed")], [ 377 - html.div( 378 - [attribute.attribute("class", "image-grid")], 379 - list.map(images, fn(img) { 380 - html.img([ 381 - attribute.attribute( 382 - "src", 383 - blob_ref_to_url(pds_host, did, img.ref), 384 - ), 385 - attribute.attribute("alt", img.alt), 386 - attribute.attribute("class", "image-cover"), 387 - attribute.attribute("referrerpolicy", "no-referrer"), 388 - ]) 389 - }), 390 - ), 391 - ]) 392 - decoders.ExternalLink(external) -> 393 - html.div([attribute.attribute("class", "post-embed")], [ 394 - html.a( 395 - [ 396 - attribute.attribute("href", external.uri), 397 - attribute.attribute("target", "_blank"), 398 - attribute.attribute("rel", "noopener noreferrer"), 399 - attribute.attribute("class", "link-card"), 400 - ], 401 - [ 402 - html.div( 403 - [ 404 - attribute.attribute("class", "external-link-preview"), 405 - ], 406 - [ 407 - html.h3( 408 - [ 409 - attribute.attribute("class", "external-link-title"), 410 - ], 411 - [html.text(external.title)], 412 - ), 413 - html.p( 414 - [ 415 - attribute.attribute("class", "external-link-desc"), 416 - ], 417 - [html.text(external.description)], 418 - ), 419 - ], 366 + elem_or_none(embed, fn(embed_obj) { 367 + case embed_obj { 368 + decoders.Images(images) -> 369 + html.div([attribute.attribute("class", "post-embed")], [ 370 + html.div( 371 + [attribute.attribute("class", "image-grid")], 372 + list.map(images, fn(img) { 373 + html.img([ 374 + attribute.attribute( 375 + "src", 376 + blob_ref_to_url(pds_host, did, img.ref), 420 377 ), 421 - ], 422 - ), 423 - ]) 424 - decoders.Record(r) -> html.text(r.record.uri) 425 - } 426 - } 378 + attribute.attribute("alt", img.alt), 379 + attribute.attribute("class", "image-cover"), 380 + attribute.attribute("referrerpolicy", "no-referrer"), 381 + ]) 382 + }), 383 + ), 384 + ]) 385 + decoders.ExternalLink(external) -> 386 + html.div([attribute.attribute("class", "post-embed")], [ 387 + html.a( 388 + [ 389 + attribute.attribute("href", external.uri), 390 + attribute.attribute("target", "_blank"), 391 + attribute.attribute("rel", "noopener noreferrer"), 392 + attribute.attribute("class", "link-card"), 393 + ], 394 + [ 395 + html.div( 396 + [ 397 + attribute.attribute("class", "external-link-preview"), 398 + ], 399 + [ 400 + html.h3( 401 + [ 402 + attribute.attribute("class", "external-link-title"), 403 + ], 404 + [html.text(external.title)], 405 + ), 406 + html.p( 407 + [ 408 + attribute.attribute("class", "external-link-desc"), 409 + ], 410 + [html.text(external.description)], 411 + ), 412 + ], 413 + ), 414 + ], 415 + ), 416 + ]) 417 + decoders.Record(r) -> html.text(r.record.uri) 418 + } 419 + }) 427 420 } 428 421 429 422 fn post_footer( ··· 432 425 created_at: String, 433 426 thread_counts: Option(decoders.ThreadCountsJson), 434 427 ) -> Element(Msg) { 435 - let badges = case labels { 436 - None -> [] 437 - Some(lbls) -> 438 - list.map(lbls, fn(label) { 439 - html.span([attribute.attribute("class", "badge")], [ 440 - html.text(label.val), 441 - ]) 442 - }) 443 - } 428 + let badges = 429 + labels 430 + |> option.unwrap([]) 431 + |> list.map(fn(label) { 432 + html.span([attribute.attribute("class", "badge")], [ 433 + html.text(label.val), 434 + ]) 435 + }) 444 436 445 - let tag_badges = case tags { 446 - None -> [] 447 - Some(ts) -> 448 - list.map(ts, fn(tag) { 449 - html.a( 450 - [ 451 - attribute.attribute("href", "https://bsky.app/hashtag/" <> tag), 452 - attribute.attribute("target", "_blank"), 453 - attribute.attribute("rel", "noopener noreferrer"), 454 - attribute.attribute("class", "tag-badge"), 455 - ], 456 - [html.text("#" <> tag)], 457 - ) 458 - }) 459 - } 437 + let tag_badges = 438 + tags 439 + |> option.unwrap([]) 440 + |> list.map(fn(tag) { 441 + html.a( 442 + [ 443 + attribute.attribute("href", "https://bsky.app/hashtag/" <> tag), 444 + attribute.attribute("target", "_blank"), 445 + attribute.attribute("rel", "noopener noreferrer"), 446 + attribute.attribute("class", "tag-badge"), 447 + ], 448 + [html.text("#" <> tag)], 449 + ) 450 + }) 460 451 461 452 let all_badges = list.append(badges, tag_badges) 462 453 ··· 481 472 fn engagement_bar( 482 473 thread_counts: Option(decoders.ThreadCountsJson), 483 474 ) -> Element(Msg) { 484 - case thread_counts { 485 - Some(counts) -> { 486 - let like_count = case counts.like_count { 487 - Some(n) -> int.to_string(n) 488 - None -> "0" 489 - } 490 - let repost_count = case counts.repost_count { 491 - Some(n) -> int.to_string(n) 492 - None -> "0" 493 - } 494 - let reply_count = case counts.reply_count { 495 - Some(n) -> int.to_string(n) 496 - None -> "0" 497 - } 498 - 499 - html.div([attribute.attribute("class", "engagement-bar")], [ 500 - html.span( 501 - [attribute.attribute("class", "engagement-bar__likes tabular-nums")], 502 - [ 503 - html.text("♥ " <> like_count), 504 - ], 505 - ), 506 - html.span( 507 - [attribute.attribute("class", "engagement-bar__reposts tabular-nums")], 508 - [html.text("↗ " <> repost_count)], 509 - ), 510 - html.span( 511 - [attribute.attribute("class", "engagement-bar__replies tabular-nums")], 512 - [html.text("💬 " <> reply_count)], 513 - ), 514 - ]) 515 - } 516 - None -> element.none() 517 - } 475 + elem_or_none(thread_counts, fn(counts) { 476 + html.div([attribute.attribute("class", "engagement-bar")], [ 477 + html.span( 478 + [attribute.attribute("class", "engagement-bar__likes tabular-nums")], 479 + [ 480 + html.text( 481 + "♥ " <> counts.like_count |> option.unwrap(0) |> int.to_string, 482 + ), 483 + ], 484 + ), 485 + html.span( 486 + [attribute.attribute("class", "engagement-bar__reposts tabular-nums")], 487 + [ 488 + html.text( 489 + "↗ " <> counts.repost_count |> option.unwrap(0) |> int.to_string, 490 + ), 491 + ], 492 + ), 493 + html.span( 494 + [attribute.attribute("class", "engagement-bar__replies tabular-nums")], 495 + [ 496 + html.text( 497 + "💬 " <> counts.reply_count |> option.unwrap(0) |> int.to_string, 498 + ), 499 + ], 500 + ), 501 + ]) 502 + }) 518 503 } 519 504 520 505 fn badge_group(badges: List(Element(Msg))) -> Element(Msg) { ··· 525 510 } 526 511 527 512 fn format_timestamp(iso_timestamp: String) -> String { 528 - let parts = string.split(iso_timestamp, "T") 529 - case parts { 513 + case string.split(iso_timestamp, "T") { 530 514 [date_part, ..] -> date_part 531 515 _ -> iso_timestamp 532 516 }