diff --git a/deno.lock b/deno.lock index 64bed51..aed1b07 100644 --- a/deno.lock +++ b/deno.lock @@ -33,8 +33,8 @@ "npm:svelte-awesome-color-picker@^4.1.0": "4.1.0_svelte@5.43.2__acorn@8.15.0", "npm:svelte-check@^4.3.3": "4.3.3_svelte@5.43.2__acorn@8.15.0_typescript@5.9.3", "npm:svelte-device-info@^1.0.6": "1.0.6", - "npm:svelte-floating-ui@^1.6.2": "1.6.2", "npm:svelte-infinite@~0.5.1": "0.5.1_svelte@5.43.2__acorn@8.15.0", + "npm:svelte-portal@^2.2.1": "2.2.1", "npm:svelte@^5.43.2": "5.43.2_acorn@8.15.0", "npm:tailwindcss@^4.1.16": "4.1.16", "npm:typescript-eslint@^8.46.3": "8.46.3_eslint@9.39.0_typescript@5.9.3_@typescript-eslint+parser@8.46.3__eslint@9.39.0__typescript@5.9.3", @@ -1661,18 +1661,15 @@ "svelte" ] }, - "svelte-floating-ui@1.6.2": { - "integrity": "sha512-EC+DZtBey50P6l3NSzNQWon3cip8a1bzwdpmCdc45kymqEWL4BKhPemAq7SQ9QLebDPaMECW6YodxFbs2d+O/w==", - "dependencies": [ - "@floating-ui/dom" - ] - }, "svelte-infinite@0.5.1_svelte@5.43.2__acorn@8.15.0": { "integrity": "sha512-NvpYWrHPcLHZQMnqUXgKGpOSMq9kMQ6sa8+WO80jLrgBFX+LWoKvAsrc1d1g+eiaagNAE9HalWWJ4KDtYi/+sw==", "dependencies": [ "svelte" ] }, + "svelte-portal@2.2.1": { + "integrity": "sha512-uF7is5sM4aq5iN7QF/67XLnTUvQCf2iiG/B1BHTqLwYVY1dsVmTeXZ/LeEyU6dLjApOQdbEG9lkqHzxiQtOLEQ==" + }, "svelte@5.43.2_acorn@8.15.0": { "integrity": "sha512-ro1umEzX8rT5JpCmlf0PPv7ncD8MdVob9e18bhwqTKNoLjS8kDvhVpaoYVPc+qMwDAOfcwJtyY7ZFSDbOaNPgA==", "dependencies": [ @@ -1839,8 +1836,8 @@ "npm:svelte-awesome-color-picker@^4.1.0", "npm:svelte-check@^4.3.3", "npm:svelte-device-info@^1.0.6", - "npm:svelte-floating-ui@^1.6.2", "npm:svelte-infinite@~0.5.1", + "npm:svelte-portal@^2.2.1", "npm:svelte@^5.43.2", "npm:tailwindcss@^4.1.16", "npm:typescript-eslint@^8.46.3", diff --git a/package.json b/package.json index 9088ee3..daef808 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,8 @@ "hash-wasm": "^4.12.0", "lru-cache": "^11.2.2", "svelte-device-info": "^1.0.6", - "svelte-infinite": "^0.5.1" + "svelte-infinite": "^0.5.1", + "svelte-portal": "^2.2.1" }, "devDependencies": { "@eslint/compat": "^1.4.1", diff --git a/src/components/AccountSelector.svelte b/src/components/AccountSelector.svelte index 1c45898..7f30668 100644 --- a/src/components/AccountSelector.svelte +++ b/src/components/AccountSelector.svelte @@ -90,7 +90,10 @@ }; - + {#snippet trigger()} - {/each} - -
- {/if} - - + + + {/snippet} + + {/each} + +
+ {/if} +
diff --git a/src/components/BskyPost.svelte b/src/components/BskyPost.svelte index 015c605..0c140b2 100644 --- a/src/components/BskyPost.svelte +++ b/src/components/BskyPost.svelte @@ -31,6 +31,8 @@ import { derived } from 'svelte/store'; import Device from 'svelte-device-info'; import Dropdown from './Dropdown.svelte'; + import { type AppBskyEmbeds } from '$lib/at/types'; + import { settings } from '$lib/settings'; interface Props { client: AtpClient; @@ -215,20 +217,25 @@ }; let actionsOpen = $state(false); + let actionsPos = $state({ x: 0, y: 0 }); + + const handleRightClick = (event: MouseEvent) => { + actionsOpen = true; + actionsPos = { x: event.clientX, y: event.clientY }; + event.preventDefault(); + }; -{#snippet embedBadge(record: AppBskyFeedPost.Main)} - {#if record.embed} - - {getEmbedText(record.embed.$type)} - - {/if} +{#snippet embedBadge(embed: AppBskyEmbeds)} + + {getEmbedText(embed.$type!)} + {/snippet} {#if mini} @@ -244,7 +251,10 @@ onclick={() => scrollToAndPulse(post.value.uri)} class="select-none hover:cursor-pointer hover:underline" > - @{handle}: {@render embedBadge(record)} + @{handle}: + {#if record.embed} + {@render embedBadge(record.embed)} + {/if} {record.text} {:else} @@ -269,32 +279,45 @@ {:then post} {#if post.ok} {@const record = post.value.record} +
- + {#await client.getProfile(did)} {handle} {:then profile} {#if profile.ok} {@const profileValue = profile.value} - {profileValue.displayName}(@{handle}) + >(@{handle}) {:else} {handle} {/if} @@ -305,76 +328,16 @@ >{getRelativeTime(new Date(record.createdAt))}
-

+

{record.text} - {#if isOnPostComposer} - {@render embedBadge(record)} + {#if isOnPostComposer && record.embed} + {@render embedBadge(record.embed)} {/if}

{#if !isOnPostComposer && record.embed} {@const embed = record.embed}
- {#snippet embedMedia( - embed: AppBskyEmbedImages.Main | AppBskyEmbedVideo.Main | AppBskyEmbedExternal.Main - )} - {#if embed.$type === 'app.bsky.embed.images'} - - {#each embed.images as image (image.image)} - {#if isBlob(image.image)} - {image.alt} - {/if} - {/each} - {:else if embed.$type === 'app.bsky.embed.video'} - {#if isBlob(embed.video)} - {#await didDoc then didDoc} - {#if didDoc.ok} - - - {/if} - {/await} - {/if} - {/if} - {/snippet} - {#snippet embedPost(uri: ResourceUri)} - {#if quoteDepth < 2} - {@const parsedUri = expect(parseCanonicalResourceUri(uri))} - - {#if !(did === parsedUri.repo && rkey === parsedUri.rkey)} - - {:else} - you think you're funny with that recursive quote but i'm onto you - {/if} - {:else} - {@render embedBadge(record)} - {/if} - {/snippet} - {#if embed.$type === 'app.bsky.embed.images' || embed.$type === 'app.bsky.embed.video'} - {@render embedMedia(embed)} - {:else if embed.$type === 'app.bsky.embed.record'} - {@render embedPost(embed.record.uri)} - {:else if embed.$type === 'app.bsky.embed.recordWithMedia'} -
- {@render embedPost(embed.record.record.uri)} - {@render embedMedia(embed.media)} -
- {/if} - + {@render postEmbed(embed)}
{/if} {#if !isOnPostComposer} @@ -390,6 +353,70 @@ {/await} {/if} +{#snippet postEmbed(embed: AppBskyEmbeds)} + {#snippet embedMedia( + embed: AppBskyEmbedImages.Main | AppBskyEmbedVideo.Main | AppBskyEmbedExternal.Main + )} + {#if embed.$type === 'app.bsky.embed.images'} + + {#each embed.images as image (image.image)} + {#if isBlob(image.image)} + {image.alt} + {/if} + {/each} + {:else if embed.$type === 'app.bsky.embed.video'} + {#if isBlob(embed.video)} + {#await didDoc then didDoc} + {#if didDoc.ok} + + + {/if} + {/await} + {/if} + {/if} + {/snippet} + {#snippet embedPost(uri: ResourceUri)} + {#if quoteDepth < 2} + {@const parsedUri = expect(parseCanonicalResourceUri(uri))} + + {#if !(did === parsedUri.repo && rkey === parsedUri.rkey)} + + {:else} + you think you're funny with that recursive quote but i'm onto you + {/if} + {:else} + {@render embedBadge(embed)} + {/if} + {/snippet} + {#if embed.$type === 'app.bsky.embed.images' || embed.$type === 'app.bsky.embed.video'} + {@render embedMedia(embed)} + {:else if embed.$type === 'app.bsky.embed.record'} + {@render embedPost(embed.record.uri)} + {:else if embed.$type === 'app.bsky.embed.recordWithMedia'} +
+ {@render embedPost(embed.record.record.uri)} + {@render embedMedia(embed.media)} +
+ {/if} + +{/snippet} + {#snippet postControls(post: PostWithUri, backlinks?: PostActions)} {#snippet control( name: string, @@ -453,24 +480,55 @@ true )}
-
- - {#snippet trigger()} + {@render dropdownItem('heroicons:link-20-solid', 'copy link to post', () => + navigator.clipboard.writeText(`${$settings.socialAppUrl}/profile/${did}/post/${rkey}`) + )} + {@render dropdownItem('heroicons:link-20-solid', 'copy at uri', () => + navigator.clipboard.writeText(post.uri) + )} +
+ {@render dropdownItem('heroicons:clipboard', 'copy post text', () => + navigator.clipboard.writeText(post.record.text) + )} + + {#snippet trigger()} +
{@render control('actions', 'heroicons:ellipsis-horizontal-16-solid', (e) => { e.stopPropagation(); actionsOpen = !actionsOpen; + actionsPos = { x: 0, y: 0 }; })} - {/snippet} - - woof - -
+
+ {/snippet} +
{/snippet} + +{#snippet dropdownItem(icon: string, label: string, onClick: () => void)} + +{/snippet} diff --git a/src/components/Dropdown.svelte b/src/components/Dropdown.svelte index e24bd0b..053d22c 100644 --- a/src/components/Dropdown.svelte +++ b/src/components/Dropdown.svelte @@ -8,13 +8,18 @@ type Placement } from '@floating-ui/dom'; import { onMount } from 'svelte'; + import { portal } from 'svelte-portal'; + import type { ClassValue } from 'svelte/elements'; interface Props { + class?: ClassValue; + style?: string; isOpen?: boolean; trigger?: import('svelte').Snippet; children?: import('svelte').Snippet; placement?: Placement; offsetDistance?: number; + position?: { x: number; y: number }; } let { @@ -22,7 +27,9 @@ trigger, children, placement = 'bottom-start', - offsetDistance = 8 + offsetDistance = 2, + position = $bindable(), + ...restProps }: Props = $props(); let triggerRef: HTMLElement | undefined = $state(); @@ -86,7 +93,14 @@ {#if isOpen} -