frontend for xcvr appview

initial image stuff

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