frontend for xcvr appview

just gonna try this and see what breaks

+217 -17
+3 -3
package-lock.json
··· 558 558 "license": "(Apache-2.0 AND BSD-3-Clause)" 559 559 }, 560 560 "node_modules/@rachel-mp4/lrcproto": { 561 - "version": "1.0.4", 562 - "resolved": "https://registry.npmjs.org/@rachel-mp4/lrcproto/-/lrcproto-1.0.4.tgz", 563 - "integrity": "sha512-C5wtSj1oa8YhKb7U0v4YgNx4OMfrtJmhCDlZ0BMmCrBA93IX153pbjro8WtXDNfZXyyUgOLkjzd1VrsMgUA3KA==", 561 + "version": "1.2.0", 562 + "resolved": "https://registry.npmjs.org/@rachel-mp4/lrcproto/-/lrcproto-1.2.0.tgz", 563 + "integrity": "sha512-DSbEf5wxS0ArjgYkh6ZVLqpB3qvGlGB8Co9AzDeK9fnYZIfwmVgSQemT/Um2e2Oaq/mioNAx8gph6T01wQz1dQ==", 564 564 "license": "MIT", 565 565 "dependencies": { 566 566 "typescript": "^3.9.10"
+1
src/lib/components/AutoGrowTextArea.svelte
··· 230 230 position: absolute; 231 231 top: 0; 232 232 left: 0; 233 + max-height: 16rem; 233 234 } 234 235 .selected.emoji-result { 235 236 background: var(--fg);
+40 -13
src/lib/components/Transmitter.svelte
··· 14 14 let nick = $state(defaultNick ?? "wanderer"); 15 15 let innerWidth = $state(0); 16 16 let isDesktop = $derived(innerWidth > 1000); 17 + let imageURL: string | undefined = $state(); 18 + let imageAlt: string = $state(""); 17 19 $effect(() => { 18 20 if (ctx) { 19 21 ctx.setNick(nick); ··· 75 77 }; 76 78 const convertFileToImageItem = (blob: File) => { 77 79 const blobUrl = URL.createObjectURL(blob); 78 - const img = document.createElement("img"); 79 - img.src = blobUrl; 80 - const image: Image = { 81 - type: "image", 82 - image: img, 83 - id: 0, 84 - active: false, 85 - mine: false, 86 - muted: false, 87 - startedAt: Date.now(), 88 - }; 89 - ctx.pushItem(image); 90 - console.log("pushed image item!"); 80 + ctx.initImage(blob); 81 + imageURL = blobUrl; 82 + }; 83 + const cancelimagepost = () => { 84 + if (imageURL) { 85 + URL.revokeObjectURL(imageURL); 86 + } 87 + ctx.atpblob = undefined; 88 + ctx.pubImage(""); 89 + imageAlt = ""; 90 + imageURL = undefined; 91 + }; 92 + const uploadimage = () => { 93 + ctx.pubImage(imageAlt); 94 + if (imageURL) { 95 + URL.revokeObjectURL(imageURL); 96 + } 97 + imageAlt = ""; 98 + imageURL = undefined; 91 99 }; 92 100 </script> 93 101 ··· 136 144 maxlength={65535} 137 145 fs={isDesktop ? "2rem" : "1rem"} 138 146 /> 147 + {#if imageURL !== undefined} 148 + <div> 149 + <img src={imageURL} alt={imageAlt} /> 150 + <AutoGrowInput 151 + bind:value={imageAlt} 152 + placeholder="alt text" 153 + size={10} 154 + bold={false} 155 + fs={isDesktop ? "2rem" : "1rem"} 156 + /> 157 + <button onclick={cancelimagepost}> cancel </button> 158 + {#if ctx.atpblob} 159 + confirm 160 + {:else} 161 + uploading... 162 + {/if} 163 + <button onclick={uploadimage}> confirm</button> 164 + </div> 165 + {/if} 139 166 </div> 140 167 141 168 <style>
+173 -1
src/lib/wscontext.svelte.ts
··· 9 9 // however long it takes for atproto to propogate, you can't submit your 10 10 // message either. 11 11 // so i want to make that side of things better 12 + type ATPBlob = { 13 + $type: string 14 + ref: string 15 + mimeType: string 16 + size: number 17 + } 18 + 19 + type ATPImage = { 20 + $type: string 21 + alt: string 22 + aspectRatio?: ATPAspectRatio 23 + blob?: ATPBlob 24 + } 25 + 26 + type ATPAspectRatio = { 27 + width: number 28 + height: number 29 + } 12 30 13 31 export class WSContext { 14 32 items: Array<Item> = $state(new Array()) ··· 25 43 26 44 channelUri: string 27 45 active: boolean = false 46 + mediaactive: boolean = false 28 47 nick: string = "wanderer" 29 48 handle: string = "" 30 49 31 50 myID: undefined | number 32 51 myNonce: undefined | Uint8Array 33 52 curMsg: string = $state("") 53 + mySignet: undefined | SignetView 34 54 35 - mySignet: undefined | SignetView 55 + myMediaID: undefined | number 56 + myMediaNonce: undefined | Uint8Array 57 + atpblob: ATPBlob | undefined 58 + myMediaSignet: undefined | SignetView 36 59 37 60 audio: HTMLAudioElement = new Audio('/notif.wav') 38 61 shortaudio: HTMLAudioElement = new Audio('/shortnotif.wav') ··· 162 185 } 163 186 } 164 187 188 + pubImage = (alt: string) => { 189 + if (this.atpblob) { 190 + const image: ATPImage = { $type: "string", alt: alt, blob: this.atpblob } 191 + const record = { 192 + ...(this.mySignet && { signetURI: this.mySignet.uri }), 193 + ...(this.channelUri && { channelURI: this.channelUri }), 194 + ...(this.myID && { messageID: this.myID }), 195 + ...(this.myNonce && { nonce: b64encodebytearray(this.myNonce) }), 196 + image: image, 197 + ...(this.nick && { nick: this.nick }), 198 + ...(this.color && { color: this.color }), 199 + } 200 + const api = import.meta.env.VITE_API_URL 201 + const recordstrungified = JSON.stringify(record) 202 + const endpoint = `${api}/lrc/media` 203 + fetch(endpoint, { 204 + method: "POST", 205 + headers: { 206 + "Content-Type": "application/json", 207 + }, 208 + body: recordstrungified, 209 + }).then((response) => { 210 + if (response.ok) { 211 + console.log(response) 212 + } else { 213 + throw new Error(`HTTP ${response.status}`) 214 + } 215 + }).catch(() => { 216 + setTimeout(() => { 217 + fetch(endpoint, { 218 + method: "POST", 219 + headers: { 220 + "Content-Type": "application/json", 221 + }, 222 + body: recordstrungified, 223 + }).then((val) => console.log(val), (val) => console.log(val)) 224 + }, 2000) 225 + }) 226 + const uri = "beep" 227 + const contentAddress = `${api}/lrc/getImage?uri=${uri}` 228 + if (this.mediaactive) { 229 + pubImage(alt, contentAddress, this) 230 + } 231 + } else { 232 + pubImage(alt, undefined, this) 233 + } 234 + } 235 + 236 + initImage = (blob: File) => { 237 + if (!this.mediaactive) { 238 + initImage(this) 239 + this.mediaactive = true 240 + const api = import.meta.env.VITE_API_URL 241 + const endpoint = `${api}/lrc/image` 242 + const formData = new FormData() 243 + formData.append("image", blob) 244 + fetch(endpoint, { 245 + method: "POST", 246 + body: formData, 247 + }).then((response) => { 248 + if (response.ok) { 249 + response.json().then((atpblob) => 250 + this.atpblob = atpblob) 251 + } else { 252 + throw new Error(`HTTP ${response.status}`) 253 + } 254 + }).catch((err) => { console.log(err) }) 255 + } 256 + } 257 + 258 + 165 259 insert = (idx: number, s: string) => { 166 260 if (!this.active) { 167 261 initMessage(this) ··· 234 328 }) 235 329 } 236 330 331 + mediapubItem = (id: number, alt: string | undefined, contentAddress: string | undefined) => { 332 + this.items = this.items.map((item: Item) => { 333 + return isImage(item) && item.id === id ? { ...item, active: false, alt: alt, contentAddress: contentAddress } : item 334 + }) 335 + } 336 + 237 337 insertMessage = (id: number, idx: number, s: string) => { 238 338 this.ensureExistenceOfMessage(id) 239 339 this.items = this.items.map((item: Item) => { ··· 266 366 addSignet = (signet: SignetView) => { 267 367 if (signet.lrcId === this.myID) { 268 368 this.mySignet = signet 369 + } 370 + if (signet.lrcId === this.myMediaID) { 371 + this.myMediaSignet = signet 269 372 } 270 373 console.log("now we are signing") 271 374 const arrayIdx = this.items.findIndex(item => item.id === signet.lrcId) ··· 518 621 ctx.ws?.send(byteArray) 519 622 } 520 623 624 + export const initImage = (ctx: WSContext) => { 625 + const evt: lrc.Event = { 626 + msg: { 627 + oneofKind: "mediainit", 628 + mediainit: { 629 + nick: ctx.nick, 630 + color: ctx.color, 631 + externalID: ctx.handle 632 + } 633 + } 634 + } 635 + const byteArray = lrc.Event.toBinary(evt) 636 + ctx.ws?.send(byteArray) 637 + } 638 + 639 + export const pubImage = (alt: string | undefined, contentAddress: string | undefined, ctx: WSContext) => { 640 + const evt: lrc.Event = { 641 + msg: { 642 + oneofKind: "mediapub", 643 + mediapub: { 644 + alt: alt, 645 + contentAddress: contentAddress, 646 + } 647 + } 648 + } 649 + const byteArray = lrc.Event.toBinary(evt) 650 + ctx.ws?.send(byteArray) 651 + } 652 + 521 653 export const insertMessage = (idx: number, s: string, ctx: WSContext) => { 522 654 if (ctx.shouldTransmit) { 523 655 const evt: lrc.Event = { ··· 712 844 return true 713 845 } 714 846 847 + case "mediainit": { 848 + const id = event.msg.mediainit.id ?? 0 849 + if (id === 0) return false 850 + const echoed = event.msg.mediainit.echoed ?? false 851 + if (echoed) { 852 + ctx.myMediaID = id 853 + ctx.myMediaNonce = event.msg.mediainit.nonce 854 + // return false 855 + } 856 + const nick = event.msg.mediainit.nick 857 + const handle = event.msg.mediainit.externalID 858 + const color = event.msg.mediainit.color 859 + const active = true 860 + const mine = echoed 861 + const muted = false 862 + const startedAt = Date.now() 863 + const msg: Image = { 864 + type: 'image', 865 + id: id, 866 + active: active, 867 + mine: mine, 868 + muted: muted, 869 + ...(color && { color: color }), 870 + ...(handle && { handle: handle }), 871 + ...(nick && { nick: nick }), 872 + startedAt: startedAt 873 + } 874 + ctx.pushItem(msg) 875 + ctx.pushToLog(id, byteArray, "init") 876 + return true 877 + } 878 + 715 879 case "pub": { 716 880 const id = event.msg.pub.id ?? 0 717 881 if (id === 0) return false 718 882 ctx.pubItem(id) 883 + ctx.pushToLog(id, byteArray, "pub") 884 + return false 885 + } 886 + 887 + case "mediapub": { 888 + const id = event.msg.mediapub.id ?? 0 889 + if (id === 0) return false 890 + ctx.mediapubItem(id, event.msg.mediapub.alt, event.msg.mediapub.contentAddress) 719 891 ctx.pushToLog(id, byteArray, "pub") 720 892 return false 721 893 }