frontend for xcvr appview

initial image stuff

+159 -61
+18
src/lib/components/AutoGrowTextArea.svelte
··· 7 interface Props { 8 onBeforeInput?: (event: InputEvent) => void; 9 onInputEl?: (el: HTMLTextAreaElement) => void; 10 placeholder?: string; 11 value?: string; 12 maxlength?: number; ··· 20 placeholder, 21 value = $bindable(""), 22 onInputEl, 23 maxlength, 24 bold = false, 25 color, ··· 173 }); 174 } 175 }); 176 </script> 177 178 <div class="autogrowwrapper"> ··· 183 {maxlength} 184 oninput={adjust} 185 onkeydown={emojifier} 186 onbeforeinput={bi} 187 style:font-weight={bold ? "700" : "inherit"} 188 style:--theme={color}
··· 7 interface Props { 8 onBeforeInput?: (event: InputEvent) => void; 9 onInputEl?: (el: HTMLTextAreaElement) => void; 10 + imageHandler?: (image: File) => void; 11 placeholder?: string; 12 value?: string; 13 maxlength?: number; ··· 21 placeholder, 22 value = $bindable(""), 23 onInputEl, 24 + imageHandler, 25 maxlength, 26 bold = false, 27 color, ··· 175 }); 176 } 177 }); 178 + const pastifier = (event: ClipboardEvent) => { 179 + const items = event.clipboardData?.items; 180 + if (items === undefined) { 181 + return; 182 + } 183 + for (const item of items) { 184 + if (item.type.startsWith("image/")) { 185 + const blob = item.getAsFile(); 186 + if (blob === null) { 187 + return; 188 + } 189 + imageHandler?.(blob); 190 + } 191 + } 192 + }; 193 </script> 194 195 <div class="autogrowwrapper"> ··· 200 {maxlength} 201 oninput={adjust} 202 onkeydown={emojifier} 203 + onpaste={pastifier} 204 onbeforeinput={bi} 205 style:font-weight={bold ? "700" : "inherit"} 206 style:--theme={color}
+16 -13
src/lib/components/Receiever.svelte
··· 1 <script lang="ts"> 2 import Transmission from "$lib/components/Transmission.svelte"; 3 - import type { Message } from "$lib/types"; 4 interface Props { 5 - messages: Array<Message>; 6 mylocaltext?: string; 7 onmute?: (id: number) => void; 8 onunmute?: (id: number) => void; 9 } 10 - let { messages, mylocaltext, onmute, onunmute }: Props = $props(); 11 - let length = $derived(messages.length); 12 let innerWidth = $state(0); 13 let isDesktop = $derived(innerWidth > 1000); 14 </script> 15 16 <svelte:window bind:innerWidth /> 17 <div id="receiver"> 18 - {#each messages as message, index} 19 {@const last = length - 1} 20 {@const diff = last - index} 21 {@const guess = 2 + (Math.atan((diff - 19) * 0.25) / -2.8 - 0.45)} 22 {@const res = Math.min(Math.max(guess, 1), 2)} 23 - <Transmission 24 - {message} 25 - mylocaltext={message.active && message.mine ? mylocaltext : undefined} 26 - margin={0} 27 - {onmute} 28 - {onunmute} 29 - fs={isDesktop ? `${res}rem` : "1rem"} 30 - /> 31 {/each} 32 </div> 33
··· 1 <script lang="ts"> 2 import Transmission from "$lib/components/Transmission.svelte"; 3 + import type { Message, Image, Item } from "$lib/types"; 4 + import { isMessage, isImage } from "$lib/types"; 5 interface Props { 6 + items: Array<Item>; 7 mylocaltext?: string; 8 onmute?: (id: number) => void; 9 onunmute?: (id: number) => void; 10 } 11 + let { items, mylocaltext, onmute, onunmute }: Props = $props(); 12 + let length = $derived(items.length); 13 let innerWidth = $state(0); 14 let isDesktop = $derived(innerWidth > 1000); 15 </script> 16 17 <svelte:window bind:innerWidth /> 18 <div id="receiver"> 19 + {#each items as item, index} 20 {@const last = length - 1} 21 {@const diff = last - index} 22 {@const guess = 2 + (Math.atan((diff - 19) * 0.25) / -2.8 - 0.45)} 23 {@const res = Math.min(Math.max(guess, 1), 2)} 24 + {#if isMessage(item)} 25 + <Transmission 26 + message={item} 27 + mylocaltext={item.active && item.mine ? mylocaltext : undefined} 28 + margin={0} 29 + {onmute} 30 + {onunmute} 31 + fs={isDesktop ? `${res}rem` : "1rem"} 32 + /> 33 + {/if} 34 {/each} 35 </div> 36
+30 -3
src/lib/types.ts
··· 26 avatar?: string 27 } 28 29 - export type Message = { 30 uri?: string 31 - body: string 32 - mbody?: string 33 id: number 34 active: boolean 35 mine: boolean ··· 41 nick?: string 42 startedAt: number 43 } 44 45 export type LogItem = { 46 id: number ··· 69 color?: number 70 signetURI: string 71 postedAt: string 72 } 73 74 export type SignedMessageView = {
··· 26 avatar?: string 27 } 28 29 + type BaseItem = { 30 uri?: string 31 id: number 32 active: boolean 33 mine: boolean ··· 39 nick?: string 40 startedAt: number 41 } 42 + export type Image = BaseItem & { 43 + type: 'image' 44 + image?: HTMLImageElement 45 + } 46 + 47 + export type Message = BaseItem & { 48 + type: 'message' 49 + body: string 50 + mbody?: string 51 + } 52 + export type Item = Message | Image 53 + 54 + export function isMessage(item: Item): item is Message { 55 + return item.type === 'message' 56 + } 57 + 58 + export function isImage(item: Item): item is Image { 59 + return item.type === 'image' 60 + } 61 62 export type LogItem = { 63 id: number ··· 86 color?: number 87 signetURI: string 88 postedAt: string 89 + } 90 + 91 + export type ImageView = { 92 + $type?: string 93 + uri: string 94 + author: ProfileView 95 + imageUri: string 96 + nick?: string 97 + color?: number 98 + signetURI: string 99 } 100 101 export type SignedMessageView = {
+1
src/lib/utils.ts
··· 61 62 export function signedMessageViewToMessage(sm: SignedMessageView): Message { 63 return { 64 uri: sm.uri, 65 body: sm.body, 66 id: sm.signet.lrcId,
··· 61 62 export function signedMessageViewToMessage(sm: SignedMessageView): Message { 63 return { 64 + type: 'message', 65 uri: sm.uri, 66 body: sm.body, 67 id: sm.signet.lrcId,
+92 -43
src/lib/wscontext.svelte.ts
··· 1 - import type { Message, LogItem, SignetView, MessageView } from "./types" 2 import * as lrc from '@rachel-mp4/lrcproto/gen/ts/lrc' 3 4 // so the thing with the current message is that i require a signet to post ··· 10 // so i want to make that side of things better 11 12 export class WSContext { 13 - messages: Array<Message> = $state(new Array()) 14 orphanedSignets: Map<string, SignetView> = new Map() 15 orphanedMessages: Map<string, MessageView> = new Map() 16 log: Array<LogItem> = $state(new Array()) 17 topic: string = $state("") 18 connected: boolean = $state(false) ··· 61 this.ws?.close() 62 this.ls?.close() 63 connectTo(url, this) 64 - this.messages = [] 65 this.orphanedMessages = new Map() 66 this.orphanedSignets = new Map() 67 this.mySignet = undefined 68 this.myID = undefined ··· 74 this.ws = null 75 this.ls?.close() 76 this.ls = null 77 - this.messages = [] 78 this.orphanedMessages = new Map() 79 this.orphanedSignets = new Map() 80 this.mySignet = undefined 81 this.myID = undefined ··· 203 204 // theoretically this could occur _after we have an orphaned signet or an orphanedmessage or both! so, 205 // TODO: make it work in that case 206 - pushMessage = (message: Message) => { 207 if (document.hidden || !document.hasFocus()) { 208 this.audio.currentTime = 0 209 this.audio.play() 210 - } else if (!message.mine) { 211 this.shortaudio.currentTime = 0 212 this.shortaudio.play() 213 } 214 - if (this.messages.length > 200) { 215 - this.messages = [...this.messages.slice(this.messages.length - 199), message] 216 } else { 217 - this.messages.push(message) 218 } 219 } 220 221 - editMessage = (id: number, newMessage: Message) => { 222 - this.messages = this.messages.map((msg: Message) => { 223 - return msg.id === id ? newMessage : msg 224 }) 225 } 226 227 - pubMessage = (id: number) => { 228 - this.messages = this.messages.map((msg: Message) => { 229 - return msg.id === id ? { ...msg, active: false } : msg 230 }) 231 } 232 233 insertMessage = (id: number, idx: number, s: string) => { 234 - this.ensureExistenceOf(id) 235 - this.messages = this.messages.map((msg: Message) => { 236 - return msg.id === id ? { ...msg, body: insertSIntoAStringAtIdx(s, msg.body, idx) } : msg 237 }) 238 } 239 240 deleteMessage = (id: number, idx1: number, idx2: number) => { 241 - this.ensureExistenceOf(id) 242 - this.messages = this.messages.map((msg: Message) => { 243 - return msg.id === id ? { ...msg, body: deleteFromAStringBetweenIdxs(msg.body, idx1, idx2) } : msg 244 }) 245 } 246 247 - ensureExistenceOf = (id: number) => { 248 - const idx = this.messages.findIndex((msg) => { return msg.id === id }) 249 if (idx === -1) { 250 - this.pushMessage({ 251 body: "", 252 id: id, 253 active: true, ··· 263 this.mySignet = signet 264 } 265 console.log("now we are signing") 266 - const arrayIdx = this.messages.findIndex(msg => msg.id === signet.lrcId) 267 if (arrayIdx !== -1) { 268 console.log("found appropriate signet c:") 269 - this.messages = this.messages.map((msg: Message) => { 270 - return msg.id === signet.lrcId ? { ...msg, signetView: signet } : msg 271 }) 272 } else { 273 console.log("couldn't find appropriate signet :c") ··· 275 if (om !== undefined) { 276 console.log("some orphan logic") 277 const message = makeMessageFromSignetAndMessageViews(om, signet) 278 - const idx = this.messages.findIndex(msg => msg.id > signet.lrcId) 279 if (idx === -1) { 280 - this.messages.push(message) 281 } else { 282 - this.messages = [...this.messages.slice(0, idx), message, ...this.messages.slice(idx)] 283 } 284 this.orphanedMessages.delete(signet.uri) 285 - } else { 286 - this.orphanedSignets.set(signet.uri, signet) 287 } 288 } 289 } 290 291 verifyMessage = (message: MessageView) => { 292 console.log("now we are verifying!") 293 console.log(message.signetURI) 294 - const arrayIdx = this.messages.findIndex(msg => msg.signetView?.uri === message.signetURI && msg.signetView?.authorHandle === message.author.handle) 295 if (arrayIdx !== -1) { 296 console.log("found appropriate message c:") 297 - this.messages = this.messages.map((msg: Message) => { 298 - return msg.signetView?.uri === message.signetURI ? 299 - { ...makeMessageFromSignetAndMessageViews(message, msg.signetView), body: msg.body, mine: msg.mine } : msg 300 }) 301 } 302 else { ··· 305 if (os !== undefined) { 306 console.log("some orphan logic") 307 const m = makeMessageFromSignetAndMessageViews(message, os) 308 - const idx = this.messages.findIndex(msg => msg.id > os.lrcId) 309 if (idx === -1) { 310 - this.messages.push(m) 311 } else { 312 - this.messages = [...this.messages.slice(0, idx), m, ...this.messages.slice(idx)] 313 } 314 this.orphanedSignets.delete(os.uri) 315 } else { ··· 317 } 318 } 319 } 320 pushToLog = (id: number, ba: Uint8Array, type: string) => { 321 const bstring = Array.from(ba).map(byte => byte.toString(16).padStart(2, "0")).join('') 322 const time = Date.now() ··· 331 332 const makeMessageFromSignetAndMessageViews = (m: MessageView, s: SignetView): Message => { 333 return { 334 uri: m.uri, 335 body: "i didn't catch the lrc message body :c", 336 mbody: m.body, ··· 344 signetView: s, 345 ...(m.nick && { nick: m.nick }), 346 startedAt: Date.parse(s.startedAt) 347 } 348 } 349 ··· 657 const mine = echoed 658 const muted = false 659 const startedAt = Date.now() 660 - const msg = { 661 body: body, 662 id: id, 663 active: active, ··· 668 ...(nick && { nick: nick }), 669 startedAt: startedAt 670 } 671 - ctx.pushMessage(msg) 672 ctx.pushToLog(id, byteArray, "init") 673 return true 674 } ··· 676 case "pub": { 677 const id = event.msg.pub.id ?? 0 678 if (id === 0) return false 679 - ctx.pubMessage(id) 680 ctx.pushToLog(id, byteArray, "pub") 681 return false 682 } ··· 706 const mine = false 707 const body = "" 708 const startedAt = Date.now() 709 - ctx.pushMessage({ id, body, muted, active, mine, startedAt }) 710 return false 711 } 712
··· 1 + import type { Item, Image, Message, LogItem, SignetView, MessageView, ImageView } from "./types" 2 + import { isMessage, isImage } from "./types" 3 import * as lrc from '@rachel-mp4/lrcproto/gen/ts/lrc' 4 5 // so the thing with the current message is that i require a signet to post ··· 11 // so i want to make that side of things better 12 13 export class WSContext { 14 + items: Array<Item> = $state(new Array()) 15 orphanedSignets: Map<string, SignetView> = new Map() 16 orphanedMessages: Map<string, MessageView> = new Map() 17 + orphanedImages: Map<string, ImageView> = new Map() 18 log: Array<LogItem> = $state(new Array()) 19 topic: string = $state("") 20 connected: boolean = $state(false) ··· 63 this.ws?.close() 64 this.ls?.close() 65 connectTo(url, this) 66 + this.items = [] 67 this.orphanedMessages = new Map() 68 + this.orphanedImages = new Map() 69 this.orphanedSignets = new Map() 70 this.mySignet = undefined 71 this.myID = undefined ··· 77 this.ws = null 78 this.ls?.close() 79 this.ls = null 80 + this.items = [] 81 this.orphanedMessages = new Map() 82 + this.orphanedImages = new Map() 83 this.orphanedSignets = new Map() 84 this.mySignet = undefined 85 this.myID = undefined ··· 207 208 // theoretically this could occur _after we have an orphaned signet or an orphanedmessage or both! so, 209 // TODO: make it work in that case 210 + pushItem = (item: Item) => { 211 if (document.hidden || !document.hasFocus()) { 212 this.audio.currentTime = 0 213 this.audio.play() 214 + } else if (!item.mine) { 215 this.shortaudio.currentTime = 0 216 this.shortaudio.play() 217 } 218 + if (this.items.length > 200) { 219 + this.items = [...this.items.slice(this.items.length - 199), item] 220 } else { 221 + this.items.push(item) 222 } 223 } 224 225 + replaceItem = (id: number, newItem: Item) => { 226 + this.items = this.items.map((item: Item) => { 227 + return item.id === id ? newItem : item 228 }) 229 } 230 231 + pubItem = (id: number) => { 232 + this.items = this.items.map((item: Item) => { 233 + return isMessage(item) && item.id === id ? { ...item, active: false } : item 234 }) 235 } 236 237 insertMessage = (id: number, idx: number, s: string) => { 238 + this.ensureExistenceOfMessage(id) 239 + this.items = this.items.map((item: Item) => { 240 + return isMessage(item) && item.id === id ? { ...item, body: insertSIntoAStringAtIdx(s, item.body, idx) } : item 241 }) 242 } 243 244 deleteMessage = (id: number, idx1: number, idx2: number) => { 245 + this.ensureExistenceOfMessage(id) 246 + this.items = this.items.map((item: Item) => { 247 + return isMessage(item) && item.id === id ? { ...item, body: deleteFromAStringBetweenIdxs(item.body, idx1, idx2) } : item 248 }) 249 } 250 251 + ensureExistenceOfMessage = (id: number) => { 252 + const idx = this.items.findIndex((item) => { return item.id === id }) 253 if (idx === -1) { 254 + this.pushItem({ 255 + type: 'message', 256 body: "", 257 id: id, 258 active: true, ··· 268 this.mySignet = signet 269 } 270 console.log("now we are signing") 271 + const arrayIdx = this.items.findIndex(item => item.id === signet.lrcId) 272 if (arrayIdx !== -1) { 273 console.log("found appropriate signet c:") 274 + this.items = this.items.map((item: Item) => { 275 + return item.id === signet.lrcId ? { ...item, signetView: signet } : item 276 }) 277 } else { 278 console.log("couldn't find appropriate signet :c") ··· 280 if (om !== undefined) { 281 console.log("some orphan logic") 282 const message = makeMessageFromSignetAndMessageViews(om, signet) 283 + const idx = this.items.findIndex(item => item.id > signet.lrcId) 284 if (idx === -1) { 285 + this.items.push(message) 286 } else { 287 + this.items = [...this.items.slice(0, idx), message, ...this.items.slice(idx)] 288 } 289 this.orphanedMessages.delete(signet.uri) 290 + return 291 + } 292 + const oi = this.orphanedImages.get(signet.uri) 293 + if (oi !== undefined) { 294 + console.log("comse orphan logic 2") 295 + const image = makeImageFromSignetAndImageViews(oi, signet) 296 + const idx = this.items.findIndex(item => item.id > signet.lrcId) 297 + if (idx === -1) { 298 + this.items.push(image) 299 + } else { 300 + this.items = [...this.items.slice(0, idx), image, ...this.items.slice(idx)] 301 + } 302 + this.orphanedImages.delete(signet.uri) 303 + return 304 } 305 + this.orphanedSignets.set(signet.uri, signet) 306 } 307 } 308 309 verifyMessage = (message: MessageView) => { 310 console.log("now we are verifying!") 311 console.log(message.signetURI) 312 + const arrayIdx = this.items.findIndex(item => item.signetView?.uri === message.signetURI && item.signetView?.authorHandle === message.author.handle) 313 if (arrayIdx !== -1) { 314 console.log("found appropriate message c:") 315 + this.items = this.items.map((item: Item) => { 316 + return item.signetView?.uri === message.signetURI && isMessage(item) ? 317 + { ...makeMessageFromSignetAndMessageViews(message, item.signetView), body: item.body, mine: item.mine } : item 318 }) 319 } 320 else { ··· 323 if (os !== undefined) { 324 console.log("some orphan logic") 325 const m = makeMessageFromSignetAndMessageViews(message, os) 326 + const idx = this.items.findIndex(item => item.id > os.lrcId) 327 if (idx === -1) { 328 + this.items.push(m) 329 } else { 330 + this.items = [...this.items.slice(0, idx), m, ...this.items.slice(idx)] 331 } 332 this.orphanedSignets.delete(os.uri) 333 } else { ··· 335 } 336 } 337 } 338 + 339 pushToLog = (id: number, ba: Uint8Array, type: string) => { 340 const bstring = Array.from(ba).map(byte => byte.toString(16).padStart(2, "0")).join('') 341 const time = Date.now() ··· 350 351 const makeMessageFromSignetAndMessageViews = (m: MessageView, s: SignetView): Message => { 352 return { 353 + type: 'message', 354 uri: m.uri, 355 body: "i didn't catch the lrc message body :c", 356 mbody: m.body, ··· 364 signetView: s, 365 ...(m.nick && { nick: m.nick }), 366 startedAt: Date.parse(s.startedAt) 367 + } 368 + } 369 + 370 + const makeImageFromSignetAndImageViews = (i: ImageView, s: SignetView): Image => { 371 + return { 372 + type: 'image', 373 + uri: i.uri, 374 + id: s.lrcId, 375 + active: false, 376 + mine: false, 377 + muted: false, 378 + //image: fetch(i.imageUri) 379 + ...(i.nick && { nick: i.nick }), 380 + ...(i.color && { color: i.color }), 381 + handle: i.author.handle, 382 + profileView: i.author, 383 + signetView: s, 384 + startedAt: Date.parse(s.startedAt), 385 } 386 } 387 ··· 695 const mine = echoed 696 const muted = false 697 const startedAt = Date.now() 698 + const msg: Message = { 699 + type: 'message', 700 body: body, 701 id: id, 702 active: active, ··· 707 ...(nick && { nick: nick }), 708 startedAt: startedAt 709 } 710 + ctx.pushItem(msg) 711 ctx.pushToLog(id, byteArray, "init") 712 return true 713 } ··· 715 case "pub": { 716 const id = event.msg.pub.id ?? 0 717 if (id === 0) return false 718 + ctx.pubItem(id) 719 ctx.pushToLog(id, byteArray, "pub") 720 return false 721 } ··· 745 const mine = false 746 const body = "" 747 const startedAt = Date.now() 748 + const msg: Message = { 749 + type: "message", 750 + id: id, 751 + body: body, 752 + muted: muted, 753 + active: active, 754 + mine: mine, 755 + startedAt: startedAt 756 + } 757 + 758 + ctx.pushItem(msg) 759 return false 760 } 761
+2 -2
src/routes/c/[handle]/[rkey]/+page.svelte
··· 49 </div> 50 {/if} 51 {#if ctx} 52 - {#if ctx.messages.length === 0 && ctx.connected} 53 <div>connecting...</div> 54 <div>and you're connected.</div> 55 <div>messages will go here, start typing down below</div> ··· 94 </div> 95 {/if} 96 <Receiever 97 - messages={ctx.messages} 98 mylocaltext={ctx.curMsg} 99 onmute={ctx.mute} 100 onunmute={ctx.unmute}
··· 49 </div> 50 {/if} 51 {#if ctx} 52 + {#if ctx.items.length === 0 && ctx.connected} 53 <div>connecting...</div> 54 <div>and you're connected.</div> 55 <div>messages will go here, start typing down below</div> ··· 94 </div> 95 {/if} 96 <Receiever 97 + items={ctx.items} 98 mylocaltext={ctx.curMsg} 99 onmute={ctx.mute} 100 onunmute={ctx.unmute}