frontend for xcvr appview
at main 1045 lines 34 kB view raw
1import type * as xcvr from "./types" 2import { isMessage, isImage, isMedia } from "./types" 3import * as lrc from '@rachel-mp4/lrcproto/gen/ts/lrc' 4 5export class WSContext { 6 existingindices: Map<number, boolean> = new Map() 7 existinguris: Map<string, string> = new Map() 8 items: Array<xcvr.Item> = $state(new Array()) 9 log: Array<xcvr.LogItem> = $state(new Array()) 10 topic: string = $state("") 11 connected: boolean = $state(false) 12 conncount = $state(0) 13 ws: WebSocket | null = null 14 ls: WebSocket | null = null 15 color: number = $state(Math.floor(Math.random() * 16777216)) 16 17 channelUri: string 18 nick: string = "wanderer" 19 handle: string = "" 20 curMsg: string = $state("") 21 myMessage: xcvr.Message | undefined 22 messageactive: boolean = false 23 myMedia: xcvr.Media | undefined 24 atpblob: xcvr.AtpBlob | undefined = $state() 25 atpblobtoken: string | undefined 26 mediaactive: boolean = false 27 28 audio: HTMLAudioElement = new Audio('/notif.wav') 29 shortaudio: HTMLAudioElement = new Audio('/shortnotif.wav') 30 31 beepcoefficient: number = $state(0.0) 32 junkword: string = $state("beep") 33 shouldSend: boolean = $state(true) 34 defaultmessage: string = $state("") 35 postToMyRepo: boolean = $state(false) 36 shouldTransmit: boolean = $state(true) 37 lrceventqueue: Array<lrc.Edit> = [] 38 39 constructor(channelUri: string, defaultHandle: string, defaultNick: string, defaultColor: number) { 40 console.log(channelUri) 41 this.channelUri = channelUri 42 this.handle = defaultHandle 43 this.nick = defaultNick 44 this.color = defaultColor 45 } 46 47 connect(url: string) { 48 this.ws?.close() 49 this.ls?.close() 50 connectTo(url, this) 51 } 52 53 reconnect = (url: string) => { 54 this.ws?.close() 55 this.ls?.close() 56 connectTo(url, this) 57 this.items = [] 58 } 59 60 disconnect = () => { 61 this.ws?.close() 62 this.ws = null 63 this.ls?.close() 64 this.ls = null 65 this.items = [] 66 } 67 68 starttransmit = () => { 69 if (this.lrceventqueue.length != 0) { 70 const evt: lrc.Event = { 71 msg: { 72 oneofKind: "editbatch", 73 editbatch: { 74 edits: this.lrceventqueue, 75 } 76 } 77 } 78 const byteArray = lrc.Event.toBinary(evt) 79 this.ws?.send(byteArray) 80 this.lrceventqueue = [] 81 } 82 } 83 84 insertLineBreak = () => { 85 if (this.myMessage) { 86 this.starttransmit() 87 pubMessage(this) 88 const api = import.meta.env.VITE_API_URL 89 let body = this.defaultmessage != "" ? this.defaultmessage : this.curMsg 90 if (this.beepcoefficient > 0.0 && this.junkword != "") { 91 if (body.length < (this.junkword.length + 1)) { 92 body = this.junkword 93 } 94 const nb = Math.floor(1.0 * body.length * this.beepcoefficient / this.junkword.length) 95 for (let i = 0; i < nb; i++) { 96 const start = Math.floor((body.length - this.junkword.length) * Math.random()) 97 body = body.slice(0, start) + this.junkword + body.slice(start + this.junkword.length) 98 } 99 } 100 console.log(body) 101 const record = { 102 ...(this.myMessage.signetView && { signetURI: this.myMessage.signetView.uri }), 103 ...(this.channelUri && { channelURI: this.channelUri }), 104 messageID: this.myMessage.id, 105 ...(this.myMessage.lrcdata?.init?.nonce && { nonce: b64encodebytearray(this.myMessage.lrcdata.init.nonce) }), 106 body: body, 107 ...(this.nick && { nick: this.nick }), 108 ...(this.color && { color: this.color }), 109 } 110 const endpoint = this.postToMyRepo ? `${api}/lrc/mymessage` : `${api}/lrc/message` 111 if (this.shouldSend) { 112 const recordstrungified = JSON.stringify(record) 113 fetch(endpoint, { 114 method: "POST", 115 headers: { 116 "Content-Type": "application/json", 117 }, 118 body: recordstrungified, 119 }).then((response) => { 120 if (response.ok) { 121 console.log(response) 122 } else { 123 throw new Error(`HTTP ${response.status}`) 124 } 125 }).catch(() => { 126 setTimeout(() => { 127 fetch(endpoint, { 128 method: "POST", 129 headers: { 130 "Content-Type": "application/json", 131 }, 132 body: recordstrungified, 133 }).then((val) => console.log(val), (val) => console.log(val)) 134 }, 2000) 135 }) 136 } 137 this.myMessage = undefined 138 this.messageactive = false 139 this.curMsg = "" 140 } else if (this.messageactive) { 141 this.starttransmit() 142 pubMessage(this) 143 this.messageactive = false 144 this.curMsg = "" 145 } 146 } 147 148 pubImage = (alt: string, width: number | undefined, height: number | undefined) => { 149 if (this.myMedia) { 150 let aspectRatio: xcvr.AspectRatio | undefined 151 if (width && height) { 152 aspectRatio = { 153 width: width, 154 height: height 155 } 156 } 157 const image: xcvr.AtpImage = { 158 $type: "org.xcvr.lrc.image", 159 alt: alt, 160 ...(this.atpblob && { blob: this.atpblob }), 161 ...(aspectRatio && { aspectRatio: aspectRatio }) 162 } 163 const record = { 164 ...(this.myMedia.signetView && { signetURI: this.myMedia.signetView.uri }), 165 ...(this.channelUri && { channelURI: this.channelUri }), 166 messageID: this.myMedia.id, 167 ...(this.myMedia.lrcdata?.init?.nonce && { nonce: b64encodebytearray(this.myMedia.lrcdata.init.nonce) }), 168 image: image, 169 ...(this.nick && { nick: this.nick }), 170 ...(this.color && { color: this.color }), 171 type: "image" 172 } 173 const api = import.meta.env.VITE_API_URL 174 const recordstrungified = JSON.stringify(record) 175 const endpoint = `${api}/lrc/media` 176 fetch(endpoint, { 177 method: "POST", 178 headers: { 179 "Content-Type": "application/json", 180 }, 181 body: recordstrungified, 182 }).then((response) => { 183 if (response.ok) { 184 console.log(response) 185 } else { 186 throw new Error(`HTTP ${response.status}`) 187 } 188 }).catch(() => { 189 setTimeout(() => { 190 fetch(endpoint, { 191 method: "POST", 192 headers: { 193 "Content-Type": "application/json", 194 }, 195 body: recordstrungified, 196 }).then((val) => console.log(val), (val) => console.log(val)) 197 }, 2000) 198 }) 199 if (this.atpblob) { 200 const contentAddress = `${api}/xrpc/org.xcvr.lrc.getImage?handle=${this.handle}&cid=${this.atpblob.ref["$link"]}` 201 pubImage(alt, contentAddress, this) 202 } else { 203 pubImage(alt, undefined, this) 204 } 205 this.myMedia = undefined 206 this.atpblob = undefined 207 this.mediaactive = false 208 } else if (this.mediaactive) { 209 if (this.atpblob) { 210 console.error("atpblob should be undefined in this case") 211 this.atpblob = undefined 212 } 213 pubImage(alt, undefined, this) 214 this.mediaactive = false 215 } 216 } 217 218 cancelImage = () => { 219 if (this.mediaactive) { 220 pubImage(undefined, undefined, this) 221 this.myMedia = undefined 222 this.atpblob = undefined 223 this.mediaactive = false 224 } 225 } 226 227 initImage = (blob: File) => { 228 if (!this.myMedia) { 229 initImage(this) 230 this.mediaactive = true 231 const uuid = crypto.randomUUID() 232 const api = import.meta.env.VITE_API_URL 233 const endpoint = `${api}/lrc/image` 234 const formData = new FormData() 235 formData.append("image", blob) 236 formData.append("uuid", uuid) 237 this.atpblobtoken = uuid 238 fetch(endpoint, { 239 method: "POST", 240 body: formData 241 }).then((response) => { 242 if (response.ok) { 243 response.json().then((data) => { 244 if (this.atpblobtoken === data.uuid) { 245 this.atpblob = data.blob 246 this.atpblobtoken = undefined 247 console.log("here's atpblob") 248 } else { 249 console.error("atpblobtoken mismatch!!!") 250 } 251 }) 252 } else { 253 throw new Error(`HTTP ${response.status}`) 254 } 255 }).catch((err) => { console.log(err) }) 256 } 257 } 258 259 260 insert = (idx: number, s: string) => { 261 if (!this.messageactive) { 262 initMessage(this) 263 this.messageactive = true 264 } 265 insertMessage(idx, s, this) 266 this.curMsg = insertSIntoAStringAtIdx(s, this.curMsg, idx) 267 } 268 269 delete = (idx: number, idx2: number) => { 270 if (!this.messageactive) { 271 return 272 } 273 deleteMessage(idx, idx2, this) 274 this.curMsg = deleteFromAStringBetweenIdxs(this.curMsg, idx, idx2) 275 } 276 mute = (id: number) => { 277 muteMessage(id, this) 278 } 279 280 unmute = (id: number) => { 281 unmuteMessage(id, this) 282 } 283 284 setNick = (nick: string) => { 285 setNick(nick, this) 286 } 287 setColor = (color: number) => { 288 setColor(color, this) 289 } 290 setHandle = (handle: string) => { 291 setHandle(handle, this) 292 } 293 294 setTopic = (topic: string) => { 295 console.log("new topic:", topic) 296 this.topic = topic 297 } 298 299 setConncount = (cc: number) => { 300 this.conncount = cc 301 } 302 303 pushItem = (item: xcvr.Item) => { 304 if (this.existingindices.get(item.id)) { 305 console.log("you tried to push an item who exists!") 306 return 307 } 308 if (document.hidden || !document.hasFocus()) { 309 this.audio.currentTime = 0 310 this.audio.play() 311 } else if (!item.lrcdata.mine) { 312 this.shortaudio.currentTime = 0 313 this.shortaudio.play() 314 } 315 if (item.lrcdata.mine) { 316 if (isMessage(item)) { 317 this.myMessage = item 318 } else if (isMedia(item)) { 319 this.myMedia = item 320 } 321 } 322 this.items.push(item) 323 this.existingindices.set(item.id, true) 324 } 325 326 initMessage = (id: number, init: xcvr.LrcInit, mine: boolean) => { 327 if (this.existingindices.get(id)) { 328 this.items = this.items.map((item: xcvr.Item) => { 329 return item.id === id && isMessage(item) 330 ? { ...item, type: "message", lrcdata: { ...item.lrcdata, init: init } } 331 : item 332 }) 333 } else { 334 console.log("push message init") 335 this.pushItem({ 336 type: 'message', 337 id: id, 338 lrcdata: { 339 body: '', 340 mine: mine, 341 muted: false, 342 init: init, 343 }, 344 }) 345 } 346 } 347 348 initMedia = (id: number, init: xcvr.LrcInit, mine: boolean) => { 349 if (this.existingindices.get(id)) { 350 this.items = this.items.map((item: xcvr.Item) => { 351 return item.id === id && isImage(item) 352 ? { ...item, type: "image", lrcdata: { ...item.lrcdata, init: init } } 353 : item 354 }) 355 } else { 356 console.log("push media init") 357 this.pushItem({ 358 type: 'image', 359 id: id, 360 lrcdata: { 361 mine: mine, 362 muted: false, 363 init: init, 364 }, 365 }) 366 } 367 } 368 369 initMute = (id: number) => { 370 if (this.existingindices.get(id)) { 371 this.items = this.items.map((item: xcvr.Item) => { 372 return item.id === id 373 ? { ...item, lrcdata: { ...item.lrcdata, muted: true } } as typeof item 374 : item 375 }) 376 } else { 377 console.log("push mute init") 378 this.pushItem({ 379 type: 'enby', 380 id: id, 381 lrcdata: { 382 mine: false, 383 muted: true, 384 } 385 }) 386 } 387 } 388 389 pubMessage = (id: number) => { 390 if (this.existingindices.get(id)) { 391 this.items = this.items.map((item: xcvr.Item) => { 392 return item.id === id && isMessage(item) 393 ? { ...item, type: "message", lrcdata: { ...item.lrcdata, pub: true } } 394 : item 395 }) 396 } else { 397 console.log("push message pub") 398 this.pushItem({ 399 type: "message", 400 id: id, 401 lrcdata: { 402 mine: false, 403 muted: false, 404 body: "", 405 }, 406 }) 407 } 408 } 409 410 pubMedia = (id: number, pub: xcvr.LrcMediaPub) => { 411 if (this.existingindices.get(id)) { 412 this.items = this.items.map((item: xcvr.Item) => { 413 return item.id === id && isMedia(item) 414 ? { 415 ...item, type: "image", 416 lrcdata: { 417 ...item.lrcdata, 418 pub: pub 419 } 420 } 421 : item 422 }) 423 } else { 424 console.log("push media pub") 425 this.pushItem({ 426 type: "image", 427 id: id, 428 lrcdata: { 429 mine: false, 430 muted: false, 431 pub: pub, 432 }, 433 }) 434 } 435 } 436 437 insertMessage = (id: number, idx: number, s: string) => { 438 if (this.existingindices.get(id)) { 439 this.items = this.items.map((item: xcvr.Item) => { 440 return item.id === id && isMessage(item) 441 ? { ...item, type: "message", lrcdata: { ...item.lrcdata, body: insertSIntoAStringAtIdx(s, item.lrcdata.body, idx) } } 442 : item 443 }) 444 } else { 445 446 console.log("push message insert") 447 this.pushItem({ 448 type: "message", 449 id: id, 450 lrcdata: { 451 mine: false, 452 muted: false, 453 body: insertSIntoAStringAtIdx(s, "", idx), 454 pub: false 455 }, 456 }) 457 } 458 } 459 460 deleteMessage = (id: number, idx1: number, idx2: number) => { 461 if (this.existingindices.get(id)) { 462 this.items = this.items.map((item: xcvr.Item) => { 463 return item.id === id && isMessage(item) 464 ? { ...item, type: "message", lrcdata: { ...item.lrcdata, body: deleteFromAStringBetweenIdxs(item.lrcdata.body, idx1, idx2) } } 465 : item 466 }) 467 } else { 468 469 console.log("push message delete") 470 this.pushItem({ 471 type: "message", 472 id: id, 473 lrcdata: { 474 mine: false, 475 muted: false, 476 body: deleteFromAStringBetweenIdxs("", idx1, idx2), 477 pub: false 478 }, 479 }) 480 } 481 } 482 483 addSignet = (signet: xcvr.SignetView) => { 484 if (this.existingindices.get(signet.lrcId)) { 485 this.items = this.items.map((item: xcvr.Item) => { 486 return item.id === signet.lrcId 487 ? { ...item, signetView: signet } 488 : item 489 }) 490 } else { 491 console.log("push signet") 492 this.pushItem({ 493 type: "enby", 494 id: signet.lrcId, 495 lrcdata: { mine: false, muted: false }, 496 signetView: signet 497 }) 498 } 499 this.existinguris.set(signet.uri, signet.author) 500 } 501 502 addMessageView = (message: xcvr.MessageView) => { 503 if (this.existinguris.get(message.signetURI) === message.author.did) { 504 this.items = this.items.map((item: xcvr.Item) => { 505 return item.signetView?.uri === message.signetURI && isMessage(item) 506 ? { ...item, type: "message", messageView: message } 507 : item 508 }) 509 this.existinguris.delete(message.signetURI) 510 } else { 511 console.error("recieved a messageview who doesn't have a matching signet, rejecting: ", message) 512 } 513 } 514 515 addImageView = (media: xcvr.MediaView) => { 516 if (!media.imageView) { 517 console.log("called add imageview when i don't have an imageview") 518 return 519 } 520 if (this.existinguris.get(media.signetURI) === media.author.did) { 521 this.items = this.items.map((item: xcvr.Item) => { 522 return item.signetView?.uri === media.signetURI && isImage(item) ? 523 { ...item, type: "image", mediaView: media } : item 524 }) 525 this.existinguris.delete(media.signetURI) 526 } else { 527 console.error("recieved a mediaview who doesn't have a matching signet, rejecting: ", media) 528 } 529 } 530 531 pushToLog = (id: number, ba: Uint8Array, type: string) => { 532 const bstring = Array.from(ba).map(byte => byte.toString(16).padStart(2, "0")).join('') 533 const time = Date.now() 534 this.log = [...this.log.filter(li => li.time > Date.now() - 3000), { id: id, binary: bstring, time: time, type: type, key: Math.random() }] 535 console.log(this.log.length) 536 } 537} 538 539const b64encodebytearray = (u8: Uint8Array): string => { 540 return btoa(String.fromCharCode(...u8)) 541} 542 543const insertSIntoAStringAtIdx = (s: string, a: string, idx: number) => { 544 if (a === undefined) { 545 a = "" 546 } 547 if (idx > a.length) { 548 a = a.padEnd(idx) 549 } 550 return a.slice(0, idx) + s + a.slice(idx) 551} 552 553const deleteFromAStringBetweenIdxs = (a: string, idx1: number, idx2: number) => { 554 if (a === undefined) { 555 a = "" 556 } 557 if (idx2 > a.length) { 558 a = a.padEnd(idx2) 559 } 560 return a.slice(0, idx1) + a.slice(idx2) 561} 562 563export const connectTo = (url: string, ctx: WSContext) => { 564 const ws = new WebSocket(url, "lrc.v1"); 565 ws.binaryType = "arraybuffer"; 566 ws.onopen = () => { 567 console.log("connected") 568 ctx.connected = true 569 getTopic(ctx) 570 setNick(ctx.nick, ctx) 571 setColor(ctx.color, ctx) 572 setHandle(ctx.handle, ctx) 573 }; 574 ws.onmessage = (event) => { 575 console.log(event) 576 parseEvent(event, ctx) 577 // if (shouldScroll) { 578 // setTimeout(() => { 579 // window.scrollTo(0, document.body.scrollHeight) 580 // }, 0) 581 // } 582 583 }; 584 ws.onclose = () => { 585 console.log("closed") 586 if (ws === ctx.ws) { 587 ctx.connected = false 588 } 589 }; 590 ws.onerror = (event) => { 591 console.log("errored:", event) 592 console.log("readyState:", ws.readyState) 593 if (ws === ctx.ws) { 594 ctx.connected = false 595 } 596 } 597 ctx.ws = ws 598 const lsURI = `${import.meta.env.VITE_API_URL}/xrpc/org.xcvr.lrc.subscribeLexStream?uri=${ctx.channelUri}` 599 const ls = new WebSocket(lsURI) 600 ls.onmessage = (event) => { 601 console.log("recieved lexicon event:", event) 602 parseLexStreamEvent(event, ctx) 603 } 604 ls.onclose = () => { 605 console.log("closed ls") 606 } 607 ls.onerror = (event) => { 608 console.log("errored:", event) 609 } 610 ctx.ls = ls 611} 612 613const parseLexStreamEvent = (event: MessageEvent<any>, ctx: WSContext) => { 614 console.log("parsing!!!!") 615 const lex = JSON.parse(event.data) 616 console.log(lex.$type) 617 switch (lex.$type) { 618 case "org.xcvr.lrc.defs#signetView": { 619 console.log("parsing signet!!!") 620 const uri = lex.uri 621 const issuerHandle = lex.issuerHandle 622 const channelURI = lex.channelURI 623 const lrcID = lex.lrcID 624 const author = lex.author 625 const authorHandle = lex.authorHandle 626 const startedAt = lex.startedAt 627 ctx.addSignet({ 628 $type: "org.xcvr.lrc.defs#signetView", 629 uri: uri, 630 issuer: issuerHandle, 631 channelURI: channelURI, 632 lrcId: lrcID, 633 author: author, 634 authorHandle: authorHandle, 635 startedAt: startedAt 636 }) 637 return 638 } 639 case "org.xcvr.lrc.defs#messageView": { 640 console.log("parsing message!!!") 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 const body = lex.body 651 const nick = lex.nick 652 const color = lex.color 653 const signetURI = lex.signetURI 654 const postedAt = lex.postedAt 655 ctx.addMessageView({ 656 uri: uri, 657 author: author, 658 body: body, 659 ...(nick && { nick: nick }), 660 ...(color && { color: color }), 661 ...(signetURI && { signetURI: signetURI }), 662 ...(postedAt && { postedAt: postedAt }), 663 }) 664 return 665 } 666 case "org.xcvr.lrc.defs#mediaView": { 667 console.log("parsing media!!!") 668 const uri = lex.uri 669 const author = { 670 did: lex.author.did, 671 handle: lex.author.handle, 672 ...(lex.author.displayName && { displayName: lex.author.displayName }), 673 ...(lex.author.status && { status: lex.author.status }), 674 ...(lex.author.color && { color: lex.author.color }), 675 ...(lex.author.avatar && { avatar: lex.author.avatar }), 676 } 677 var imageView: xcvr.ImageView | undefined 678 if (lex.imageView) { 679 console.log("has an image!") 680 imageView = { 681 alt: lex.imageView.alt, 682 ...(lex.imageView.src && { src: lex.imageView.src }), 683 ...(lex.imageView.aspectRatio && { aspectRatio: lex.imageView.aspectRatio }), 684 } 685 } 686 const nick = lex.nick 687 const color = lex.color 688 const signetURI = lex.signetURI 689 const postedAt = lex.postedAt 690 ctx.addImageView({ 691 uri: uri, 692 author: author, 693 ...(imageView && { imageView: imageView }), 694 ...(nick && { nick: nick }), 695 ...(color && { color: color }), 696 ...(signetURI && { signetURI: signetURI }), 697 ...(postedAt && { postedAt: postedAt }), 698 }) 699 return 700 } 701 } 702} 703 704export const initMessage = (ctx: WSContext) => { 705 const evt: lrc.Event = { 706 msg: { 707 oneofKind: "init", 708 init: { 709 nick: ctx.nick, 710 color: ctx.color, 711 externalID: ctx.handle 712 } 713 } 714 } 715 const byteArray = lrc.Event.toBinary(evt) 716 ctx.ws?.send(byteArray) 717} 718 719export const initImage = (ctx: WSContext) => { 720 console.log("send media init!!!") 721 const evt: lrc.Event = { 722 msg: { 723 oneofKind: "mediainit", 724 mediainit: { 725 nick: ctx.nick, 726 color: ctx.color, 727 externalID: ctx.handle 728 } 729 } 730 } 731 const byteArray = lrc.Event.toBinary(evt) 732 ctx.ws?.send(byteArray) 733} 734 735export const pubImage = (alt: string | undefined, contentAddress: string | undefined, ctx: WSContext) => { 736 const evt: lrc.Event = { 737 msg: { 738 oneofKind: "mediapub", 739 mediapub: { 740 alt: alt, 741 contentAddress: contentAddress, 742 } 743 } 744 } 745 const byteArray = lrc.Event.toBinary(evt) 746 ctx.ws?.send(byteArray) 747} 748 749export const insertMessage = (idx: number, s: string, ctx: WSContext) => { 750 if (ctx.shouldTransmit) { 751 const evt: lrc.Event = { 752 msg: { 753 oneofKind: "insert", 754 insert: { 755 utf16Index: idx, 756 body: s 757 } 758 } 759 } 760 const byteArray = lrc.Event.toBinary(evt) 761 ctx.ws?.send(byteArray) 762 } else { 763 const edit: lrc.Edit = { 764 edit: { 765 oneofKind: "insert", 766 insert: { 767 utf16Index: idx, 768 body: s 769 } 770 } 771 } 772 ctx.lrceventqueue.push(edit) 773 } 774} 775 776export const pubMessage = (ctx: WSContext) => { 777 const evt: lrc.Event = { 778 msg: { 779 oneofKind: "pub", 780 pub: { 781 } 782 } 783 } 784 const byteArray = lrc.Event.toBinary(evt) 785 ctx.ws?.send(byteArray) 786} 787 788export const deleteMessage = (idx: number, idx2: number, ctx: WSContext) => { 789 if (ctx.shouldTransmit) { 790 791 const evt: lrc.Event = { 792 msg: { 793 oneofKind: "delete", 794 delete: { 795 utf16Start: idx, 796 utf16End: idx2 797 } 798 } 799 } 800 const byteArray = lrc.Event.toBinary(evt) 801 ctx.ws?.send(byteArray) 802 } else { 803 const edit: lrc.Edit = { 804 edit: { 805 oneofKind: "delete", 806 delete: { 807 utf16Start: idx, 808 utf16End: idx2 809 } 810 } 811 } 812 ctx.lrceventqueue.push(edit) 813 } 814} 815 816export const muteMessage = (id: number, ctx: WSContext) => { 817 const evt: lrc.Event = { 818 msg: { 819 oneofKind: "mute", 820 mute: { 821 id: id, 822 } 823 } 824 } 825 const byteArray = lrc.Event.toBinary(evt) 826 ctx.ws?.send(byteArray) 827} 828 829export const unmuteMessage = (id: number, ctx: WSContext) => { 830 const evt: lrc.Event = { 831 msg: { 832 oneofKind: "unmute", 833 unmute: { 834 id: id, 835 } 836 } 837 } 838 const byteArray = lrc.Event.toBinary(evt) 839 ctx.ws?.send(byteArray) 840} 841 842export const getTopic = (ctx: WSContext) => { 843 const evt: lrc.Event = { 844 msg: { 845 oneofKind: "get", 846 get: { 847 topic: "_" 848 } 849 } 850 } 851 const byteArray = lrc.Event.toBinary(evt) 852 ctx.ws?.send(byteArray) 853} 854 855export const setNick = (nick: string, ctx: WSContext) => { 856 ctx.nick = nick 857 const evt: lrc.Event = { 858 msg: { 859 oneofKind: "set", 860 set: { 861 nick: nick 862 } 863 } 864 } 865 const byteArray = lrc.Event.toBinary(evt) 866 ctx.ws?.send(byteArray) 867} 868 869export const setHandle = (handle: string, ctx: WSContext) => { 870 ctx.handle = handle 871 const evt: lrc.Event = { 872 msg: { 873 oneofKind: "set", 874 set: { 875 externalID: handle 876 } 877 } 878 } 879 const byteArray = lrc.Event.toBinary(evt) 880 ctx.ws?.send(byteArray) 881} 882export const setColor = (color: number, ctx: WSContext) => { 883 ctx.color = color 884 const evt: lrc.Event = { 885 msg: { 886 oneofKind: "set", 887 set: { 888 color: color 889 } 890 } 891 } 892 const byteArray = lrc.Event.toBinary(evt) 893 ctx.ws?.send(byteArray) 894} 895 896function parseEvent(binary: MessageEvent<any>, ctx: WSContext): boolean { 897 const byteArray = new Uint8Array(binary.data); 898 const event = lrc.Event.fromBinary(byteArray) 899 switch (event.msg.oneofKind) { 900 case "ping": { 901 return false; 902 } 903 904 case "pong": { 905 return false 906 } 907 908 case "init": { 909 const id = event.msg.init.id ?? 0 910 if (id === 0) return false 911 const color = event.msg.init.color 912 const nick = event.msg.init.nick 913 const handle = event.msg.init.externalID 914 const nonce = event.msg.init.nonce 915 const mine = event.msg.init.echoed ?? false 916 const init: xcvr.LrcInit = { 917 ...(color && { color: color }), 918 ...(nick && { nick: nick }), 919 ...(handle && { handle: handle }), 920 ...(nonce && { nonce: nonce }), 921 } 922 ctx.initMessage(id, init, mine) 923 ctx.pushToLog(id, byteArray, "init") 924 return true 925 } 926 927 case "mediainit": { 928 const id = event.msg.mediainit.id ?? 0 929 if (id === 0) return false 930 const color = event.msg.mediainit.color 931 const nick = event.msg.mediainit.nick 932 const handle = event.msg.mediainit.externalID 933 const nonce = event.msg.mediainit.nonce 934 const mine = event.msg.mediainit.echoed ?? false 935 const init: xcvr.LrcInit = { 936 ...(color && { color: color }), 937 ...(nick && { nick: nick }), 938 ...(handle && { handle: handle }), 939 ...(nonce && { nonce: nonce }), 940 } 941 ctx.initMedia(id, init, mine) 942 ctx.pushToLog(id, byteArray, "init") 943 return true 944 } 945 946 case "pub": { 947 const id = event.msg.pub.id ?? 0 948 if (id === 0) return false 949 ctx.pubMessage(id) 950 ctx.pushToLog(id, byteArray, "pub") 951 return false 952 } 953 954 case "mediapub": { 955 const id = event.msg.mediapub.id ?? 0 956 if (id === 0) return false 957 const pub: xcvr.LrcMediaPub = { 958 alt: event.msg.mediapub.alt ?? "", 959 contentAddress: event.msg.mediapub.contentAddress 960 } 961 ctx.pubMedia(id, pub) 962 ctx.pushToLog(id, byteArray, "pub") 963 return false 964 } 965 966 case "insert": { 967 const id = event.msg.insert.id ?? 0 968 if (id === 0) return false 969 ctx.pushToLog(id, byteArray, "insert") 970 doinsert(id, event.msg.insert, ctx) 971 return false 972 } 973 974 case "delete": { 975 const id = event.msg.delete.id ?? 0 976 if (id === 0) return false 977 ctx.pushToLog(event.msg.delete.id ?? 0, byteArray, "delete") 978 dodelete(id, event.msg.delete, ctx) 979 return false 980 } 981 982 983 case "mute": { 984 const id = event.msg.mute.id ?? 0 985 if (id === 0) return false 986 ctx.initMute(id) 987 return false 988 } 989 990 case "unmute": { 991 return false 992 } 993 994 case "set": { 995 return false 996 } 997 998 case "get": { 999 if (event.msg.get.connected !== undefined) { 1000 ctx.setConncount(event.msg.get.connected) 1001 } 1002 if (event.msg.get.topic !== undefined) { 1003 ctx.setTopic(event.msg.get.topic) 1004 } 1005 return false 1006 } 1007 //TODO: better logging system so that way even non hrt messages 1008 // can have the background effect! 1009 case "editbatch": { 1010 const id = event.id ?? 0 1011 if (id === 0) { 1012 return false 1013 } 1014 event.msg.editbatch.edits.forEach((edit: lrc.Edit) => { 1015 switch (edit.edit.oneofKind) { 1016 case "insert": { 1017 doinsert(id, edit.edit.insert, ctx) 1018 return 1019 } 1020 case "delete": { 1021 dodelete(id, edit.edit.delete, ctx) 1022 return 1023 } 1024 } 1025 }) 1026 return false 1027 1028 } 1029 1030 } 1031 return false 1032} 1033 1034function doinsert(id: number, insert: lrc.Insert, ctx: WSContext) { 1035 const idx = insert.utf16Index 1036 const s = insert.body 1037 ctx.insertMessage(id, idx, s) 1038} 1039 1040function dodelete(id: number, del: lrc.Delete, ctx: WSContext) { 1041 const idx = del.utf16Start 1042 const idx2 = del.utf16End 1043 ctx.deleteMessage(id, idx, idx2) 1044} 1045