frontend for xcvr appview
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