···
import { type AtpClient } from '$lib/at/client';
···
import * as TID from '@atcute/tid';
import type { PostWithUri } from '$lib/at/fetch';
import { onMount } from 'svelte';
30
-
import type { AtprotoDid } from '@atcute/lexicons/syntax';
31
+
import { isActorIdentifier, type AtprotoDid } from '@atcute/lexicons/syntax';
import { derived } from 'svelte/store';
import Device from 'svelte-device-info';
import Dropdown from './Dropdown.svelte';
···
? Promise.resolve(ok(data))
: client.getRecord(AppBskyFeedPost.mainSchema, did, rkey);
78
+
let profile: AppBskyActorProfile.Main | null = $state(null);
79
+
onMount(async () => {
80
+
const p = await client.getProfile(did);
83
+
console.log(profile.description);
// const replies = replyBacklinks
// ? Promise.resolve(ok(replyBacklinks))
// : client.getBacklinks(
···
actionsPos = { x: event.clientX, y: event.clientY };
235
+
event.stopPropagation();
let deleteState: 'waiting' | 'confirm' | 'deleted' = $state('waiting');
···
266
+
let profileOpen = $state(false);
267
+
let profilePopoutShowDid = $state(false);
{#snippet embedBadge(embed: AppBskyEmbeds)}
···
282
+
{#snippet profileInline()}
285
+
flex min-w-0 items-center gap-2 font-bold {isOnPostComposer ? 'contrast-200' : ''}
286
+
rounded-sm pr-1 transition-colors duration-100 ease-in-out hover:bg-white/10
288
+
style="color: {color};"
289
+
onclick={() => (profileOpen = !profileOpen)}
291
+
<ProfilePicture {client} {did} size={8} />
294
+
<span class="w-min max-w-sm min-w-0 overflow-hidden text-nowrap overflow-ellipsis"
295
+
>{profile.displayName}</span
296
+
><span class="shrink-0 text-sm text-nowrap opacity-70">(@{handle})</span>
303
+
<!-- eslint-disable svelte/no-navigation-without-resolve -->
304
+
{#snippet profilePopout()}
305
+
{@const profileDesc = profile?.description?.trim() ?? ''}
307
+
class="post-dropdown max-w-xl gap-2! p-2.5! backdrop-blur-3xl! backdrop-brightness-25!"
308
+
style="background: {color}36; border-color: {color}99;"
309
+
bind:isOpen={profileOpen}
310
+
trigger={profileInline}
312
+
<div class="flex items-center gap-2">
313
+
<ProfilePicture {client} {did} size={20} />
315
+
<div class="flex flex-col items-start overflow-hidden overflow-ellipsis">
316
+
<span class="mb-1.5 min-w-0 overflow-hidden text-2xl text-nowrap overflow-ellipsis">
317
+
{profile?.displayName ?? handle}
318
+
{#if profile?.pronouns}
319
+
<span class="shrink-0 text-sm text-nowrap opacity-60">({profile.pronouns})</span>
323
+
oncontextmenu={(e) => {
324
+
const node = e.target as Node;
325
+
const selection = window.getSelection() ?? new Selection();
326
+
const range = document.createRange();
327
+
range.selectNodeContents(node);
328
+
selection.removeAllRanges();
329
+
selection.addRange(range);
330
+
e.stopPropagation();
332
+
onclick={() => (profilePopoutShowDid = !profilePopoutShowDid)}
333
+
class="mb-0.5 text-nowrap opacity-85 select-text hover:underline"
335
+
{profilePopoutShowDid ? did : `@${handle}`}
337
+
{#if profile?.website}
340
+
rel="noopener noreferrer"
341
+
href={profile.website}
342
+
class="text-sm text-nowrap opacity-60">{profile.website}</a
348
+
{#if profileDesc.length > 0}
349
+
<p class="rounded-sm bg-black/25 p-1.5 text-wrap wrap-break-word">
350
+
{#each profileDesc.split(/(\s)/) as line, idx (idx)}
351
+
{#if line === '\n'}
353
+
{:else if isActorIdentifier(line.replace(/^@/, ''))}
356
+
rel="noopener noreferrer"
357
+
class="text-(--nucleus-accent2)"
358
+
href={`${$settings.socialAppUrl}/profile/${line.replace(/^@/, '')}`}>{line}</a
360
+
{:else if line.startsWith('https://')}
363
+
rel="noopener noreferrer"
364
+
class="text-(--nucleus-accent2)"
365
+
href={line}>{line.replace(/https?:\/\//, '')}</a
<div class="text-sm opacity-60">
···
329
-
mb-3 flex w-fit max-w-full items-center gap-1.5 rounded-sm pr-1
435
+
mb-3 flex w-fit max-w-full items-center gap-1 rounded-sm pr-1
style="background: {color}33;"
333
-
<ProfilePicture {client} {did} size={8} />
439
+
{@render profilePopout()}
337
-
flex min-w-0 items-center gap-2 font-bold
338
-
{isOnPostComposer ? 'contrast-200' : ''}
340
-
style="color: {color};"
442
+
title={new Date(record.createdAt).toLocaleString()}
443
+
class="pl-0.5 text-nowrap text-(--nucleus-fg)/67"
342
-
{#await client.getProfile(did)}
346
-
{@const profileValue = profile.value}
347
-
<span class="w-min min-w-0 overflow-hidden text-nowrap overflow-ellipsis"
348
-
>{profileValue.displayName}</span
349
-
><span class="text-nowrap opacity-70">(@{handle})</span>
445
+
{getRelativeTime(new Date(record.createdAt))}
356
-
<span class="text-nowrap text-(--nucleus-fg)/67"
357
-
>{getRelativeTime(new Date(record.createdAt))}</span
<p class="leading-normal text-wrap wrap-break-word">
···
embed: AppBskyEmbedImages.Main | AppBskyEmbedVideo.Main | AppBskyEmbedExternal.Main
389
-
{#if embed.$type === 'app.bsky.embed.images'}
390
-
<!-- todo: improve how images are displayed, and pop out on click -->
391
-
{#each embed.images as image (image.image)}
392
-
{#if isBlob(image.image)}
395
-
src={img('feed_thumbnail', did, image.image.ref.$link)}
400
-
{:else if embed.$type === 'app.bsky.embed.video'}
401
-
{#if isBlob(embed.video)}
402
-
{#await didDoc then didDoc}
404
-
<!-- svelte-ignore a11y_media_has_caption -->
407
-
src={blob(didDoc.value.pds, did, embed.video.ref.$link)}
477
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
478
+
<div oncontextmenu={(e) => e.stopPropagation()}>
479
+
{#if embed.$type === 'app.bsky.embed.images'}
480
+
<!-- todo: improve how images are displayed, and pop out on click -->
481
+
{#each embed.images as image (image.image)}
482
+
{#if isBlob(image.image)}
484
+
class="w-full rounded-sm"
485
+
src={img('feed_thumbnail', did, image.image.ref.$link)}
490
+
{:else if embed.$type === 'app.bsky.embed.video'}
491
+
{#if isBlob(embed.video)}
492
+
{#await didDoc then didDoc}
494
+
<!-- svelte-ignore a11y_media_has_caption -->
497
+
src={blob(didDoc.value.pds, did, embed.video.ref.$link)}
{#snippet embedPost(uri: ResourceUri)}
···
513
-
class="flex min-w-54 flex-col gap-1 rounded-sm border-2 p-1 shadow-2xl backdrop-blur-xl backdrop-brightness-60"
604
+
class="post-dropdown"
style="background: {color}36; border-color: {color}99;"
bind:isOpen={actionsOpen}
bind:position={actionsPos}
608
+
placement="bottom-end"
{@render dropdownItem('heroicons:link-20-solid', 'copy link to post', () =>
navigator.clipboard.writeText(`${$settings.socialAppUrl}/profile/${did}/post/${rkey}`)
···
<Icon class="h-6 w-6" {icon} />
674
+
@reference "../app.css";
676
+
:global(.post-dropdown) {
677
+
@apply flex min-w-54 flex-col gap-1 rounded-sm border-2 p-1 shadow-2xl backdrop-blur-xl backdrop-brightness-60;