frontend for xcvr appview

i hope this works

+337 -15
+231
src/lib/components/ImageTransmission.svelte
··· 1 + <script lang="ts"> 2 + import type { Image } from "$lib/types"; 3 + import { computePosition, flip, shift, offset } from "@floating-ui/dom"; 4 + import { hexToContrast, hexToTransparent, numToHex } from "$lib/colors"; 5 + import { smartAbsoluteTimestamp, dumbAbsoluteTimestamp } from "$lib/utils"; 6 + import ProfileCard from "./ProfileCard.svelte"; 7 + interface Props { 8 + image: Image; 9 + margin: number; 10 + onmute?: (id: number) => void; 11 + onunmute?: (id: number) => void; 12 + fs?: string; 13 + } 14 + let { image, margin, onmute, onunmute, fs }: Props = $props(); 15 + let color: string = numToHex(image.color ?? 16777215); 16 + let cpartial: string = hexToTransparent(color); 17 + let contrast: string = hexToContrast(color); 18 + let partial: string = hexToTransparent(contrast); 19 + let triggerEl: HTMLElement | undefined = $state(); 20 + let profileEl: HTMLElement | undefined = $state(); 21 + let showProfile = $state(false); 22 + async function updatePosition() { 23 + if (triggerEl && profileEl) { 24 + const { x, y } = await computePosition(triggerEl, profileEl, { 25 + middleware: [offset(0), flip(), shift()], 26 + }); 27 + Object.assign(profileEl.style, { 28 + left: `${x}px`, 29 + top: `${y}px`, 30 + }); 31 + } 32 + } 33 + $effect(() => { 34 + if (showProfile) { 35 + updatePosition(); 36 + } 37 + }); 38 + let pinned = $state(false); 39 + </script> 40 + 41 + {#if image.muted === false} 42 + <div 43 + style:--theme={color} 44 + style:--themep={cpartial} 45 + style:--tcontrast={contrast} 46 + style:--tpartial={partial} 47 + style:--margin={margin + "rem"} 48 + style:--size={fs ?? "1rem"} 49 + class="{image.active ? 'active' : ''} 50 + {image.profileView ? 'signed' : ''} 51 + {pinned ? 'pinned' : ''} 52 + {image.nick ? '' : 'late'} 53 + transmission" 54 + > 55 + <div class="header"> 56 + <span class="nick">{image.nick ?? "???"}</span 57 + >{#if image.handle}{#if !image.profileView}<span class="handle" 58 + >@{image.handle}</span 59 + >{:else}<div 60 + role="button" 61 + tabindex="0" 62 + class="handle-container" 63 + onmouseenter={() => (showProfile = true)} 64 + onmouseleave={() => (showProfile = false)} 65 + > 66 + <a 67 + bind:this={triggerEl} 68 + class="handle" 69 + href={`/p/${image.handle}`}>@{image.handle}</a 70 + > 71 + {#if showProfile} 72 + <div 73 + class="profile-container" 74 + bind:this={profileEl} 75 + > 76 + <ProfileCard profile={image.profileView} /> 77 + </div> 78 + {/if} 79 + </div> 80 + {/if} 81 + <span 82 + class="time clickable" 83 + title={dumbAbsoluteTimestamp(image.startedAt)} 84 + > 85 + {smartAbsoluteTimestamp(image.startedAt)} 86 + </span> 87 + <button 88 + class="clickable" 89 + onclick={() => { 90 + pinned = !pinned; 91 + }} 92 + > 93 + {pinned ? "unpin" : "pin"} 94 + </button> 95 + {#if image.mine !== true} 96 + <button 97 + class="mute clickable" 98 + onclick={() => { 99 + image.muted = true; 100 + onmute?.(image.id); 101 + }} 102 + > 103 + mute 104 + </button> 105 + {/if} 106 + {/if} 107 + </div> 108 + {#if image.src || image.msrc} 109 + <img src={image.msrc ? image.msrc : image.src} alt={image.alt} /> 110 + {/if} 111 + </div> 112 + {:else} 113 + muted. 114 + <button 115 + class="unmute" 116 + onclick={() => { 117 + image.muted = false; 118 + onunmute?.(image.id); 119 + }} 120 + > 121 + unmute 122 + </button> 123 + {/if} 124 + 125 + <style> 126 + .active { 127 + position: relative; 128 + background-color: var(--themep); 129 + color: var(--tcontrast); 130 + } 131 + .active::before { 132 + position: absolute; 133 + content: ""; 134 + inset: 0; 135 + z-index: -1; 136 + background-color: var(--theme); 137 + } 138 + .transmission:not(:hover) .clickable { 139 + display: none; 140 + } 141 + .active .clickable { 142 + color: var(--tpartial); 143 + } 144 + .clickable { 145 + color: var(--fl); 146 + cursor: pointer; 147 + } 148 + .clickable:hover { 149 + color: var(--fg); 150 + } 151 + .active .clickable:hover { 152 + color: var(--contrast); 153 + } 154 + .pinned { 155 + order: 1; 156 + } 157 + 158 + .header { 159 + font-weight: 700; 160 + } 161 + .active .handle { 162 + color: var(--tpartial); 163 + } 164 + .active.signed .handle { 165 + color: var(--tcontrast); 166 + } 167 + .active a { 168 + color: var(--tcontrast); 169 + } 170 + .signed .handle { 171 + color: var(--fg); 172 + } 173 + .handle { 174 + color: var(--fl); 175 + position: relative; 176 + } 177 + 178 + .nick { 179 + white-space: pre-wrap; 180 + } 181 + 182 + .handle::after { 183 + content: ""; 184 + color: var(--fg); 185 + background: var(--bg); 186 + position: absolute; 187 + left: 0; 188 + right: 0; 189 + top: calc(55% - calc(var(--size) / 8)); 190 + bottom: calc(45% - calc(var(--size) / 8)); 191 + transform: scaleX(0); 192 + transform-origin: center left; 193 + transition: transform 0.17s 3s; 194 + } 195 + 196 + .transmission:not(.signed):not(.active) .handle::after { 197 + transform: scaleX(1); 198 + } 199 + .transmission:not(.signed):not(.active) .handle:hover::after { 200 + content: "i couldn't find a record :c"; 201 + } 202 + 203 + .transmission { 204 + padding-bottom: 1rem; 205 + margin-top: var(--margin); 206 + font-size: var(--size); 207 + } 208 + 209 + .transmission:not(.active) .header { 210 + color: var(--theme); 211 + } 212 + .profile-container { 213 + position: absolute; 214 + z-index: 1; 215 + } 216 + .handle-container { 217 + display: inline-block; 218 + color: var(--fg); 219 + } 220 + button { 221 + font-size: var(--size); 222 + background-color: transparent; 223 + border: none; 224 + color: var(--fg); 225 + padding: 0; 226 + cursor: pointer; 227 + } 228 + button:hover { 229 + font-weight: 700; 230 + } 231 + </style>
+9 -2
src/lib/components/Receiever.svelte
··· 1 1 <script lang="ts"> 2 2 import Transmission from "$lib/components/Transmission.svelte"; 3 + import ImageTransmission from "$lib/components/ImageTransmission.svelte"; 3 4 import type { Message, Image, Item } from "$lib/types"; 4 5 import { isMessage, isImage } from "$lib/types"; 5 6 import type { Action } from "svelte/action"; ··· 43 44 {onunmute} 44 45 fs={isDesktop ? `${res}rem` : "1rem"} 45 46 /> 46 - {:else if isImage(item) && item.image !== undefined} 47 - <img src={item.image.src} alt="beep" /> 47 + {:else if isImage(item)} 48 + <ImageTransmission 49 + image={item} 50 + margin={0} 51 + {onmute} 52 + {onunmute} 53 + fs={isDesktop ? `${res}rem` : "1rem"} 54 + /> 48 55 {/if} 49 56 {/each} 50 57 </div>
+21 -3
src/lib/types.ts
··· 39 39 nick?: string 40 40 startedAt: number 41 41 } 42 + 43 + export type AspectRatio = { 44 + width: number 45 + height: number 46 + } 47 + 42 48 export type Image = BaseItem & { 43 49 type: 'image' 44 - image?: HTMLImageElement 50 + alt?: string 51 + malt?: string 52 + src?: string 53 + msrc?: string 54 + aspectRatio?: AspectRatio 55 + maspectRatio?: AspectRatio 45 56 } 46 57 47 58 export type Message = BaseItem & { ··· 88 99 postedAt: string 89 100 } 90 101 91 - export type ImageView = { 102 + export type MediaView = { 92 103 $type?: string 93 104 uri: string 94 105 author: ProfileView 95 - imageUri: string 106 + imageView?: ImageView 96 107 nick?: string 97 108 color?: number 98 109 signetURI: string 110 + } 111 + 112 + export type ImageView = { 113 + $type?: string 114 + alt: string 115 + src?: string 116 + aspectRatio?: AspectRatio 99 117 } 100 118 101 119 export type SignedMessageView = {
+76 -10
src/lib/wscontext.svelte.ts
··· 1 - import type { Item, Image, Message, LogItem, SignetView, MessageView, ImageView } from "./types" 1 + import type { Item, Image, Message, LogItem, SignetView, MessageView, MediaView, ImageView } from "./types" 2 2 import { isMessage, isImage } from "./types" 3 3 import * as lrc from '@rachel-mp4/lrcproto/gen/ts/lrc' 4 4 ··· 32 32 items: Array<Item> = $state(new Array()) 33 33 orphanedSignets: Map<string, SignetView> = new Map() 34 34 orphanedMessages: Map<string, MessageView> = new Map() 35 - orphanedImages: Map<string, ImageView> = new Map() 35 + orphanedMedias: Map<string, MediaView> = new Map() 36 36 log: Array<LogItem> = $state(new Array()) 37 37 topic: string = $state("") 38 38 connected: boolean = $state(false) ··· 88 88 connectTo(url, this) 89 89 this.items = [] 90 90 this.orphanedMessages = new Map() 91 - this.orphanedImages = new Map() 91 + this.orphanedMedias = new Map() 92 92 this.orphanedSignets = new Map() 93 93 this.mySignet = undefined 94 94 this.myID = undefined ··· 102 102 this.ls = null 103 103 this.items = [] 104 104 this.orphanedMessages = new Map() 105 - this.orphanedImages = new Map() 105 + this.orphanedMedias = new Map() 106 106 this.orphanedSignets = new Map() 107 107 this.mySignet = undefined 108 108 this.myID = undefined ··· 331 331 332 332 mediapubItem = (id: number, alt: string | undefined, contentAddress: string | undefined) => { 333 333 this.items = this.items.map((item: Item) => { 334 - return isImage(item) && item.id === id ? { ...item, active: false, alt: alt, contentAddress: contentAddress } : item 334 + return isImage(item) && item.id === id ? { ...item, active: false, alt: alt, src: contentAddress } : item 335 335 }) 336 336 } 337 337 ··· 393 393 this.orphanedMessages.delete(signet.uri) 394 394 return 395 395 } 396 - const oi = this.orphanedImages.get(signet.uri) 396 + const oi = this.orphanedMedias.get(signet.uri) 397 397 if (oi !== undefined) { 398 398 console.log("comse orphan logic 2") 399 - const image = makeImageFromSignetAndImageViews(oi, signet) 399 + const image = makeImageFromSignetAndImageMediaViews(oi, signet) 400 400 const idx = this.items.findIndex(item => item.id > signet.lrcId) 401 401 if (idx === -1) { 402 402 this.items.push(image) 403 403 } else { 404 404 this.items = [...this.items.slice(0, idx), image, ...this.items.slice(idx)] 405 405 } 406 - this.orphanedImages.delete(signet.uri) 406 + this.orphanedMedias.delete(signet.uri) 407 407 return 408 408 } 409 409 this.orphanedSignets.set(signet.uri, signet) ··· 440 440 } 441 441 } 442 442 443 + verifyImageMediaView = (media: MediaView) => { 444 + console.log("now we are verifying!") 445 + console.log(media.signetURI) 446 + const arrayIdx = this.items.findIndex(item => item.signetView?.uri === media.signetURI && item.signetView?.authorHandle === media.author.handle) 447 + if (arrayIdx !== -1) { 448 + console.log("found appropriate message c:") 449 + this.items = this.items.map((item: Item) => { 450 + return item.signetView?.uri === media.signetURI && isMessage(item) ? 451 + { ...makeImageFromSignetAndImageMediaViews(media, item.signetView), body: item.body, mine: item.mine } : item 452 + }) 453 + } 454 + else { 455 + console.log("couldn't find appropriate message :c") 456 + const os = this.orphanedSignets.get(media.signetURI) 457 + if (os !== undefined) { 458 + console.log("some orphan logic") 459 + const m = makeImageFromSignetAndImageMediaViews(media, os) 460 + const idx = this.items.findIndex(item => item.id > os.lrcId) 461 + if (idx === -1) { 462 + this.items.push(m) 463 + } else { 464 + this.items = [...this.items.slice(0, idx), m, ...this.items.slice(idx)] 465 + } 466 + this.orphanedSignets.delete(os.uri) 467 + } else { 468 + this.orphanedMedias.set(media.signetURI, media) 469 + } 470 + } 471 + } 472 + 443 473 pushToLog = (id: number, ba: Uint8Array, type: string) => { 444 474 const bstring = Array.from(ba).map(byte => byte.toString(16).padStart(2, "0")).join('') 445 475 const time = Date.now() ··· 471 501 } 472 502 } 473 503 474 - const makeImageFromSignetAndImageViews = (i: ImageView, s: SignetView): Image => { 504 + const makeImageFromSignetAndImageMediaViews = (i: MediaView, s: SignetView): Image => { 475 505 return { 476 506 type: 'image', 477 507 uri: i.uri, ··· 479 509 active: false, 480 510 mine: false, 481 511 muted: false, 482 - //image: fetch(i.imageUri) 512 + malt: i.imageView?.alt, 513 + ...(i.imageView?.src && { msrc: i.imageView.src }), 514 + ...(i.imageView?.aspectRatio && { maspectRatio: i.imageView.aspectRatio }), 483 515 ...(i.nick && { nick: i.nick }), 484 516 ...(i.color && { color: i.color }), 485 517 handle: i.author.handle, ··· 597 629 uri: uri, 598 630 author: author, 599 631 body: body, 632 + ...(nick && { nick: nick }), 633 + ...(color && { color: color }), 634 + ...(signetURI && { signetURI: signetURI }), 635 + ...(postedAt && { postedAt: postedAt }), 636 + }) 637 + return 638 + } 639 + case "org.xcvr.lrc.defs#mediaView": { 640 + console.log("parsing media!!!") 641 + const uri = lex.uri 642 + const author = { 643 + did: lex.author.did, 644 + handle: lex.author.handle, 645 + ...(lex.author.displayName && { displayName: lex.author.displayName }), 646 + ...(lex.author.status && { status: lex.author.status }), 647 + ...(lex.author.color && { color: lex.author.color }), 648 + ...(lex.author.avatar && { avatar: lex.author.avatar }), 649 + } 650 + var imageView: ImageView | undefined 651 + if (lex.imageView) { 652 + imageView = { 653 + alt: lex.imageView.alt, 654 + ...(lex.imageView.src && { src: lex.imageView.src }), 655 + ...(lex.imageView.aspectRatio && { aspectRatio: lex.imageView.aspectRatio }), 656 + } 657 + } 658 + const nick = lex.nick 659 + const color = lex.color 660 + const signetURI = lex.signetURI 661 + const postedAt = lex.postedAt 662 + ctx.verifyImageMediaView({ 663 + uri: uri, 664 + author: author, 665 + ...(imageView && { imageView: imageView }), 600 666 ...(nick && { nick: nick }), 601 667 ...(color && { color: color }), 602 668 ...(signetURI && { signetURI: signetURI }),