tangled
alpha
login
or
join now
moth11.net
/
xcvr
2
fork
atom
frontend for xcvr appview
2
fork
atom
overview
issues
pulls
pipelines
i hope this works
moth11.net
4 months ago
93b253ae
2767c006
+337
-15
4 changed files
expand all
collapse all
unified
split
src
lib
components
ImageTransmission.svelte
Receiever.svelte
types.ts
wscontext.svelte.ts
+231
src/lib/components/ImageTransmission.svelte
···
1
1
+
<script lang="ts">
2
2
+
import type { Image } from "$lib/types";
3
3
+
import { computePosition, flip, shift, offset } from "@floating-ui/dom";
4
4
+
import { hexToContrast, hexToTransparent, numToHex } from "$lib/colors";
5
5
+
import { smartAbsoluteTimestamp, dumbAbsoluteTimestamp } from "$lib/utils";
6
6
+
import ProfileCard from "./ProfileCard.svelte";
7
7
+
interface Props {
8
8
+
image: Image;
9
9
+
margin: number;
10
10
+
onmute?: (id: number) => void;
11
11
+
onunmute?: (id: number) => void;
12
12
+
fs?: string;
13
13
+
}
14
14
+
let { image, margin, onmute, onunmute, fs }: Props = $props();
15
15
+
let color: string = numToHex(image.color ?? 16777215);
16
16
+
let cpartial: string = hexToTransparent(color);
17
17
+
let contrast: string = hexToContrast(color);
18
18
+
let partial: string = hexToTransparent(contrast);
19
19
+
let triggerEl: HTMLElement | undefined = $state();
20
20
+
let profileEl: HTMLElement | undefined = $state();
21
21
+
let showProfile = $state(false);
22
22
+
async function updatePosition() {
23
23
+
if (triggerEl && profileEl) {
24
24
+
const { x, y } = await computePosition(triggerEl, profileEl, {
25
25
+
middleware: [offset(0), flip(), shift()],
26
26
+
});
27
27
+
Object.assign(profileEl.style, {
28
28
+
left: `${x}px`,
29
29
+
top: `${y}px`,
30
30
+
});
31
31
+
}
32
32
+
}
33
33
+
$effect(() => {
34
34
+
if (showProfile) {
35
35
+
updatePosition();
36
36
+
}
37
37
+
});
38
38
+
let pinned = $state(false);
39
39
+
</script>
40
40
+
41
41
+
{#if image.muted === false}
42
42
+
<div
43
43
+
style:--theme={color}
44
44
+
style:--themep={cpartial}
45
45
+
style:--tcontrast={contrast}
46
46
+
style:--tpartial={partial}
47
47
+
style:--margin={margin + "rem"}
48
48
+
style:--size={fs ?? "1rem"}
49
49
+
class="{image.active ? 'active' : ''}
50
50
+
{image.profileView ? 'signed' : ''}
51
51
+
{pinned ? 'pinned' : ''}
52
52
+
{image.nick ? '' : 'late'}
53
53
+
transmission"
54
54
+
>
55
55
+
<div class="header">
56
56
+
<span class="nick">{image.nick ?? "???"}</span
57
57
+
>{#if image.handle}{#if !image.profileView}<span class="handle"
58
58
+
>@{image.handle}</span
59
59
+
>{:else}<div
60
60
+
role="button"
61
61
+
tabindex="0"
62
62
+
class="handle-container"
63
63
+
onmouseenter={() => (showProfile = true)}
64
64
+
onmouseleave={() => (showProfile = false)}
65
65
+
>
66
66
+
<a
67
67
+
bind:this={triggerEl}
68
68
+
class="handle"
69
69
+
href={`/p/${image.handle}`}>@{image.handle}</a
70
70
+
>
71
71
+
{#if showProfile}
72
72
+
<div
73
73
+
class="profile-container"
74
74
+
bind:this={profileEl}
75
75
+
>
76
76
+
<ProfileCard profile={image.profileView} />
77
77
+
</div>
78
78
+
{/if}
79
79
+
</div>
80
80
+
{/if}
81
81
+
<span
82
82
+
class="time clickable"
83
83
+
title={dumbAbsoluteTimestamp(image.startedAt)}
84
84
+
>
85
85
+
{smartAbsoluteTimestamp(image.startedAt)}
86
86
+
</span>
87
87
+
<button
88
88
+
class="clickable"
89
89
+
onclick={() => {
90
90
+
pinned = !pinned;
91
91
+
}}
92
92
+
>
93
93
+
{pinned ? "unpin" : "pin"}
94
94
+
</button>
95
95
+
{#if image.mine !== true}
96
96
+
<button
97
97
+
class="mute clickable"
98
98
+
onclick={() => {
99
99
+
image.muted = true;
100
100
+
onmute?.(image.id);
101
101
+
}}
102
102
+
>
103
103
+
mute
104
104
+
</button>
105
105
+
{/if}
106
106
+
{/if}
107
107
+
</div>
108
108
+
{#if image.src || image.msrc}
109
109
+
<img src={image.msrc ? image.msrc : image.src} alt={image.alt} />
110
110
+
{/if}
111
111
+
</div>
112
112
+
{:else}
113
113
+
muted.
114
114
+
<button
115
115
+
class="unmute"
116
116
+
onclick={() => {
117
117
+
image.muted = false;
118
118
+
onunmute?.(image.id);
119
119
+
}}
120
120
+
>
121
121
+
unmute
122
122
+
</button>
123
123
+
{/if}
124
124
+
125
125
+
<style>
126
126
+
.active {
127
127
+
position: relative;
128
128
+
background-color: var(--themep);
129
129
+
color: var(--tcontrast);
130
130
+
}
131
131
+
.active::before {
132
132
+
position: absolute;
133
133
+
content: "";
134
134
+
inset: 0;
135
135
+
z-index: -1;
136
136
+
background-color: var(--theme);
137
137
+
}
138
138
+
.transmission:not(:hover) .clickable {
139
139
+
display: none;
140
140
+
}
141
141
+
.active .clickable {
142
142
+
color: var(--tpartial);
143
143
+
}
144
144
+
.clickable {
145
145
+
color: var(--fl);
146
146
+
cursor: pointer;
147
147
+
}
148
148
+
.clickable:hover {
149
149
+
color: var(--fg);
150
150
+
}
151
151
+
.active .clickable:hover {
152
152
+
color: var(--contrast);
153
153
+
}
154
154
+
.pinned {
155
155
+
order: 1;
156
156
+
}
157
157
+
158
158
+
.header {
159
159
+
font-weight: 700;
160
160
+
}
161
161
+
.active .handle {
162
162
+
color: var(--tpartial);
163
163
+
}
164
164
+
.active.signed .handle {
165
165
+
color: var(--tcontrast);
166
166
+
}
167
167
+
.active a {
168
168
+
color: var(--tcontrast);
169
169
+
}
170
170
+
.signed .handle {
171
171
+
color: var(--fg);
172
172
+
}
173
173
+
.handle {
174
174
+
color: var(--fl);
175
175
+
position: relative;
176
176
+
}
177
177
+
178
178
+
.nick {
179
179
+
white-space: pre-wrap;
180
180
+
}
181
181
+
182
182
+
.handle::after {
183
183
+
content: "";
184
184
+
color: var(--fg);
185
185
+
background: var(--bg);
186
186
+
position: absolute;
187
187
+
left: 0;
188
188
+
right: 0;
189
189
+
top: calc(55% - calc(var(--size) / 8));
190
190
+
bottom: calc(45% - calc(var(--size) / 8));
191
191
+
transform: scaleX(0);
192
192
+
transform-origin: center left;
193
193
+
transition: transform 0.17s 3s;
194
194
+
}
195
195
+
196
196
+
.transmission:not(.signed):not(.active) .handle::after {
197
197
+
transform: scaleX(1);
198
198
+
}
199
199
+
.transmission:not(.signed):not(.active) .handle:hover::after {
200
200
+
content: "i couldn't find a record :c";
201
201
+
}
202
202
+
203
203
+
.transmission {
204
204
+
padding-bottom: 1rem;
205
205
+
margin-top: var(--margin);
206
206
+
font-size: var(--size);
207
207
+
}
208
208
+
209
209
+
.transmission:not(.active) .header {
210
210
+
color: var(--theme);
211
211
+
}
212
212
+
.profile-container {
213
213
+
position: absolute;
214
214
+
z-index: 1;
215
215
+
}
216
216
+
.handle-container {
217
217
+
display: inline-block;
218
218
+
color: var(--fg);
219
219
+
}
220
220
+
button {
221
221
+
font-size: var(--size);
222
222
+
background-color: transparent;
223
223
+
border: none;
224
224
+
color: var(--fg);
225
225
+
padding: 0;
226
226
+
cursor: pointer;
227
227
+
}
228
228
+
button:hover {
229
229
+
font-weight: 700;
230
230
+
}
231
231
+
</style>
+9
-2
src/lib/components/Receiever.svelte
···
1
1
<script lang="ts">
2
2
import Transmission from "$lib/components/Transmission.svelte";
3
3
+
import ImageTransmission from "$lib/components/ImageTransmission.svelte";
3
4
import type { Message, Image, Item } from "$lib/types";
4
5
import { isMessage, isImage } from "$lib/types";
5
6
import type { Action } from "svelte/action";
···
43
44
{onunmute}
44
45
fs={isDesktop ? `${res}rem` : "1rem"}
45
46
/>
46
46
-
{:else if isImage(item) && item.image !== undefined}
47
47
-
<img src={item.image.src} alt="beep" />
47
47
+
{:else if isImage(item)}
48
48
+
<ImageTransmission
49
49
+
image={item}
50
50
+
margin={0}
51
51
+
{onmute}
52
52
+
{onunmute}
53
53
+
fs={isDesktop ? `${res}rem` : "1rem"}
54
54
+
/>
48
55
{/if}
49
56
{/each}
50
57
</div>
+21
-3
src/lib/types.ts
···
39
39
nick?: string
40
40
startedAt: number
41
41
}
42
42
+
43
43
+
export type AspectRatio = {
44
44
+
width: number
45
45
+
height: number
46
46
+
}
47
47
+
42
48
export type Image = BaseItem & {
43
49
type: 'image'
44
44
-
image?: HTMLImageElement
50
50
+
alt?: string
51
51
+
malt?: string
52
52
+
src?: string
53
53
+
msrc?: string
54
54
+
aspectRatio?: AspectRatio
55
55
+
maspectRatio?: AspectRatio
45
56
}
46
57
47
58
export type Message = BaseItem & {
···
88
99
postedAt: string
89
100
}
90
101
91
91
-
export type ImageView = {
102
102
+
export type MediaView = {
92
103
$type?: string
93
104
uri: string
94
105
author: ProfileView
95
95
-
imageUri: string
106
106
+
imageView?: ImageView
96
107
nick?: string
97
108
color?: number
98
109
signetURI: string
110
110
+
}
111
111
+
112
112
+
export type ImageView = {
113
113
+
$type?: string
114
114
+
alt: string
115
115
+
src?: string
116
116
+
aspectRatio?: AspectRatio
99
117
}
100
118
101
119
export type SignedMessageView = {
+76
-10
src/lib/wscontext.svelte.ts
···
1
1
-
import type { Item, Image, Message, LogItem, SignetView, MessageView, ImageView } from "./types"
1
1
+
import type { Item, Image, Message, LogItem, SignetView, MessageView, MediaView, ImageView } from "./types"
2
2
import { isMessage, isImage } from "./types"
3
3
import * as lrc from '@rachel-mp4/lrcproto/gen/ts/lrc'
4
4
···
32
32
items: Array<Item> = $state(new Array())
33
33
orphanedSignets: Map<string, SignetView> = new Map()
34
34
orphanedMessages: Map<string, MessageView> = new Map()
35
35
-
orphanedImages: Map<string, ImageView> = new Map()
35
35
+
orphanedMedias: Map<string, MediaView> = new Map()
36
36
log: Array<LogItem> = $state(new Array())
37
37
topic: string = $state("")
38
38
connected: boolean = $state(false)
···
88
88
connectTo(url, this)
89
89
this.items = []
90
90
this.orphanedMessages = new Map()
91
91
-
this.orphanedImages = new Map()
91
91
+
this.orphanedMedias = new Map()
92
92
this.orphanedSignets = new Map()
93
93
this.mySignet = undefined
94
94
this.myID = undefined
···
102
102
this.ls = null
103
103
this.items = []
104
104
this.orphanedMessages = new Map()
105
105
-
this.orphanedImages = new Map()
105
105
+
this.orphanedMedias = new Map()
106
106
this.orphanedSignets = new Map()
107
107
this.mySignet = undefined
108
108
this.myID = undefined
···
331
331
332
332
mediapubItem = (id: number, alt: string | undefined, contentAddress: string | undefined) => {
333
333
this.items = this.items.map((item: Item) => {
334
334
-
return isImage(item) && item.id === id ? { ...item, active: false, alt: alt, contentAddress: contentAddress } : item
334
334
+
return isImage(item) && item.id === id ? { ...item, active: false, alt: alt, src: contentAddress } : item
335
335
})
336
336
}
337
337
···
393
393
this.orphanedMessages.delete(signet.uri)
394
394
return
395
395
}
396
396
-
const oi = this.orphanedImages.get(signet.uri)
396
396
+
const oi = this.orphanedMedias.get(signet.uri)
397
397
if (oi !== undefined) {
398
398
console.log("comse orphan logic 2")
399
399
-
const image = makeImageFromSignetAndImageViews(oi, signet)
399
399
+
const image = makeImageFromSignetAndImageMediaViews(oi, signet)
400
400
const idx = this.items.findIndex(item => item.id > signet.lrcId)
401
401
if (idx === -1) {
402
402
this.items.push(image)
403
403
} else {
404
404
this.items = [...this.items.slice(0, idx), image, ...this.items.slice(idx)]
405
405
}
406
406
-
this.orphanedImages.delete(signet.uri)
406
406
+
this.orphanedMedias.delete(signet.uri)
407
407
return
408
408
}
409
409
this.orphanedSignets.set(signet.uri, signet)
···
440
440
}
441
441
}
442
442
443
443
+
verifyImageMediaView = (media: MediaView) => {
444
444
+
console.log("now we are verifying!")
445
445
+
console.log(media.signetURI)
446
446
+
const arrayIdx = this.items.findIndex(item => item.signetView?.uri === media.signetURI && item.signetView?.authorHandle === media.author.handle)
447
447
+
if (arrayIdx !== -1) {
448
448
+
console.log("found appropriate message c:")
449
449
+
this.items = this.items.map((item: Item) => {
450
450
+
return item.signetView?.uri === media.signetURI && isMessage(item) ?
451
451
+
{ ...makeImageFromSignetAndImageMediaViews(media, item.signetView), body: item.body, mine: item.mine } : item
452
452
+
})
453
453
+
}
454
454
+
else {
455
455
+
console.log("couldn't find appropriate message :c")
456
456
+
const os = this.orphanedSignets.get(media.signetURI)
457
457
+
if (os !== undefined) {
458
458
+
console.log("some orphan logic")
459
459
+
const m = makeImageFromSignetAndImageMediaViews(media, os)
460
460
+
const idx = this.items.findIndex(item => item.id > os.lrcId)
461
461
+
if (idx === -1) {
462
462
+
this.items.push(m)
463
463
+
} else {
464
464
+
this.items = [...this.items.slice(0, idx), m, ...this.items.slice(idx)]
465
465
+
}
466
466
+
this.orphanedSignets.delete(os.uri)
467
467
+
} else {
468
468
+
this.orphanedMedias.set(media.signetURI, media)
469
469
+
}
470
470
+
}
471
471
+
}
472
472
+
443
473
pushToLog = (id: number, ba: Uint8Array, type: string) => {
444
474
const bstring = Array.from(ba).map(byte => byte.toString(16).padStart(2, "0")).join('')
445
475
const time = Date.now()
···
471
501
}
472
502
}
473
503
474
474
-
const makeImageFromSignetAndImageViews = (i: ImageView, s: SignetView): Image => {
504
504
+
const makeImageFromSignetAndImageMediaViews = (i: MediaView, s: SignetView): Image => {
475
505
return {
476
506
type: 'image',
477
507
uri: i.uri,
···
479
509
active: false,
480
510
mine: false,
481
511
muted: false,
482
482
-
//image: fetch(i.imageUri)
512
512
+
malt: i.imageView?.alt,
513
513
+
...(i.imageView?.src && { msrc: i.imageView.src }),
514
514
+
...(i.imageView?.aspectRatio && { maspectRatio: i.imageView.aspectRatio }),
483
515
...(i.nick && { nick: i.nick }),
484
516
...(i.color && { color: i.color }),
485
517
handle: i.author.handle,
···
597
629
uri: uri,
598
630
author: author,
599
631
body: body,
632
632
+
...(nick && { nick: nick }),
633
633
+
...(color && { color: color }),
634
634
+
...(signetURI && { signetURI: signetURI }),
635
635
+
...(postedAt && { postedAt: postedAt }),
636
636
+
})
637
637
+
return
638
638
+
}
639
639
+
case "org.xcvr.lrc.defs#mediaView": {
640
640
+
console.log("parsing media!!!")
641
641
+
const uri = lex.uri
642
642
+
const author = {
643
643
+
did: lex.author.did,
644
644
+
handle: lex.author.handle,
645
645
+
...(lex.author.displayName && { displayName: lex.author.displayName }),
646
646
+
...(lex.author.status && { status: lex.author.status }),
647
647
+
...(lex.author.color && { color: lex.author.color }),
648
648
+
...(lex.author.avatar && { avatar: lex.author.avatar }),
649
649
+
}
650
650
+
var imageView: ImageView | undefined
651
651
+
if (lex.imageView) {
652
652
+
imageView = {
653
653
+
alt: lex.imageView.alt,
654
654
+
...(lex.imageView.src && { src: lex.imageView.src }),
655
655
+
...(lex.imageView.aspectRatio && { aspectRatio: lex.imageView.aspectRatio }),
656
656
+
}
657
657
+
}
658
658
+
const nick = lex.nick
659
659
+
const color = lex.color
660
660
+
const signetURI = lex.signetURI
661
661
+
const postedAt = lex.postedAt
662
662
+
ctx.verifyImageMediaView({
663
663
+
uri: uri,
664
664
+
author: author,
665
665
+
...(imageView && { imageView: imageView }),
600
666
...(nick && { nick: nick }),
601
667
...(color && { color: color }),
602
668
...(signetURI && { signetURI: signetURI }),