@atmo-dev/kit 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -1
- package/dist/components/ExternalEmbed.svelte +174 -0
- package/dist/components/ExternalEmbed.svelte.d.ts +8 -0
- package/dist/components/GrainAlbumPicker.svelte +218 -0
- package/dist/components/GrainAlbumPicker.svelte.d.ts +14 -0
- package/dist/components/GrainAlbumView.svelte +94 -0
- package/dist/components/GrainAlbumView.svelte.d.ts +10 -0
- package/dist/components/NestedComments.svelte +20 -10
- package/dist/components/NestedComments.svelte.d.ts +2 -0
- package/dist/components/PhotoGrid.svelte +53 -0
- package/dist/components/PhotoGrid.svelte.d.ts +9 -0
- package/dist/components/PhotoLightbox.svelte +332 -0
- package/dist/components/PhotoLightbox.svelte.d.ts +12 -0
- package/dist/components/PostContent.svelte +78 -20
- package/dist/components/PostContent.svelte.d.ts +5 -0
- package/dist/components/StreamplacePicker.svelte +94 -0
- package/dist/components/StreamplacePicker.svelte.d.ts +12 -0
- package/dist/events/DatePicker.svelte +222 -0
- package/dist/events/DatePicker.svelte.d.ts +12 -0
- package/dist/events/DateTimePicker.svelte +112 -0
- package/dist/events/DateTimePicker.svelte.d.ts +10 -0
- package/dist/events/EventAttendees.svelte +199 -0
- package/dist/events/EventAttendees.svelte.d.ts +19 -0
- package/dist/events/EventCard.svelte +148 -0
- package/dist/events/EventCard.svelte.d.ts +8 -0
- package/dist/events/EventComments.svelte +98 -0
- package/dist/events/EventComments.svelte.d.ts +8 -0
- package/dist/events/EventEditor.svelte +540 -0
- package/dist/events/EventEditor.svelte.d.ts +18 -0
- package/dist/events/EventRsvp.svelte +178 -0
- package/dist/events/EventRsvp.svelte.d.ts +16 -0
- package/dist/events/EventView.svelte +488 -0
- package/dist/events/EventView.svelte.d.ts +23 -0
- package/dist/events/ImageDropper.svelte +66 -0
- package/dist/events/ImageDropper.svelte.d.ts +7 -0
- package/dist/events/Map.svelte +36 -0
- package/dist/events/Map.svelte.d.ts +8 -0
- package/dist/events/PostToBlueskyModal.svelte +246 -0
- package/dist/events/PostToBlueskyModal.svelte.d.ts +22 -0
- package/dist/events/ShareModal.svelte +174 -0
- package/dist/events/ShareModal.svelte.d.ts +23 -0
- package/dist/events/StarterPackButton.svelte +72 -0
- package/dist/events/StarterPackButton.svelte.d.ts +10 -0
- package/dist/events/ThemeApply.svelte +30 -0
- package/dist/events/ThemeApply.svelte.d.ts +7 -0
- package/dist/events/ThemeBackground.svelte +33 -0
- package/dist/events/ThemeBackground.svelte.d.ts +7 -0
- package/dist/events/ThemePicker.svelte +104 -0
- package/dist/events/ThemePicker.svelte.d.ts +7 -0
- package/dist/events/ThumbnailPresets.svelte +71 -0
- package/dist/events/ThumbnailPresets.svelte.d.ts +11 -0
- package/dist/events/TimePicker.svelte +117 -0
- package/dist/events/TimePicker.svelte.d.ts +10 -0
- package/dist/events/TimezonePicker.svelte +67 -0
- package/dist/events/TimezonePicker.svelte.d.ts +6 -0
- package/dist/events/VodPlayer.svelte +154 -0
- package/dist/events/VodPlayer.svelte.d.ts +14 -0
- package/dist/events/VodTranscript.svelte +75 -0
- package/dist/events/VodTranscript.svelte.d.ts +8 -0
- package/dist/events/atproto-helpers.d.ts +21 -0
- package/dist/events/atproto-helpers.js +61 -0
- package/dist/events/cal/helper.d.ts +1 -0
- package/dist/events/cal/helper.js +20 -0
- package/dist/events/cal/ical.d.ts +22 -0
- package/dist/events/cal/ical.js +188 -0
- package/dist/events/cal/sanitize.d.ts +3 -0
- package/dist/events/cal/sanitize.js +25 -0
- package/dist/events/conference.d.ts +65 -0
- package/dist/events/conference.js +61 -0
- package/dist/events/contrail.d.ts +49 -0
- package/dist/events/contrail.js +12 -0
- package/dist/events/date-format.d.ts +22 -0
- package/dist/events/date-format.js +43 -0
- package/dist/events/editor/LinkedRecordsPicker.svelte +104 -0
- package/dist/events/editor/LinkedRecordsPicker.svelte.d.ts +12 -0
- package/dist/events/editor/LinksSection.svelte +154 -0
- package/dist/events/editor/LinksSection.svelte.d.ts +13 -0
- package/dist/events/editor/LocationSection.svelte +163 -0
- package/dist/events/editor/LocationSection.svelte.d.ts +8 -0
- package/dist/events/editor/RecurringModal.svelte +313 -0
- package/dist/events/editor/RecurringModal.svelte.d.ts +30 -0
- package/dist/events/editor/ThemeSection.svelte +24 -0
- package/dist/events/editor/ThemeSection.svelte.d.ts +7 -0
- package/dist/events/editor/ThumbnailSection.svelte +181 -0
- package/dist/events/editor/ThumbnailSection.svelte.d.ts +13 -0
- package/dist/events/editor/adapter.d.ts +73 -0
- package/dist/events/editor/adapter.js +9 -0
- package/dist/events/editor/location.d.ts +16 -0
- package/dist/events/editor/location.js +50 -0
- package/dist/events/editor/save.d.ts +43 -0
- package/dist/events/editor/save.js +168 -0
- package/dist/events/editor/types.d.ts +39 -0
- package/dist/events/editor/types.js +9 -0
- package/dist/events/event-media.d.ts +4 -0
- package/dist/events/event-media.js +4 -0
- package/dist/events/event-types.d.ts +79 -0
- package/dist/events/event-types.js +11 -0
- package/dist/events/event-view/AddToCalendarButton.svelte +31 -0
- package/dist/events/event-view/AddToCalendarButton.svelte.d.ts +9 -0
- package/dist/events/event-view/EventBadges.svelte +20 -0
- package/dist/events/event-view/EventBadges.svelte.d.ts +7 -0
- package/dist/events/event-view/EventDateBlock.svelte +43 -0
- package/dist/events/event-view/EventDateBlock.svelte.d.ts +7 -0
- package/dist/events/event-view/EventHostedBy.svelte +61 -0
- package/dist/events/event-view/EventHostedBy.svelte.d.ts +16 -0
- package/dist/events/event-view/EventLinksList.svelte +24 -0
- package/dist/events/event-view/EventLinksList.svelte.d.ts +9 -0
- package/dist/events/event-view/EventLocationBlock.svelte +62 -0
- package/dist/events/event-view/EventLocationBlock.svelte.d.ts +7 -0
- package/dist/events/event-view/EventLocationMap.svelte +66 -0
- package/dist/events/event-view/EventLocationMap.svelte.d.ts +7 -0
- package/dist/events/event-view/ExternalRsvpNotice.svelte +34 -0
- package/dist/events/event-view/ExternalRsvpNotice.svelte.d.ts +6 -0
- package/dist/events/event-view/StreamPlacePlayer.svelte +209 -0
- package/dist/events/event-view/StreamPlacePlayer.svelte.d.ts +8 -0
- package/dist/events/event-view/format.d.ts +10 -0
- package/dist/events/event-view/format.js +112 -0
- package/dist/events/event-view/locations.d.ts +36 -0
- package/dist/events/event-view/locations.js +243 -0
- package/dist/events/event-view/plyr-icons.d.ts +3 -0
- package/dist/events/event-view/plyr-icons.js +46 -0
- package/dist/events/linked-records.d.ts +18 -0
- package/dist/events/linked-records.js +11 -0
- package/dist/events/profile-url.d.ts +3 -0
- package/dist/events/profile-url.js +11 -0
- package/dist/events/schedule/ConferenceTimetable.svelte +340 -0
- package/dist/events/schedule/ConferenceTimetable.svelte.d.ts +22 -0
- package/dist/events/schedule/DaySchedule.svelte +298 -0
- package/dist/events/schedule/DaySchedule.svelte.d.ts +26 -0
- package/dist/events/schedule/ScheduleEventCell.svelte +166 -0
- package/dist/events/schedule/ScheduleEventCell.svelte.d.ts +25 -0
- package/dist/events/schedule/schedule-utils.d.ts +44 -0
- package/dist/events/schedule/schedule-utils.js +166 -0
- package/dist/events/starter-pack-url.d.ts +3 -0
- package/dist/events/starter-pack-url.js +16 -0
- package/dist/events/theme.d.ts +9 -0
- package/dist/events/theme.js +36 -0
- package/dist/events/themes/Blobs.svelte +64 -0
- package/dist/events/themes/Blobs.svelte.d.ts +18 -0
- package/dist/events/themes/Butterflies.svelte +183 -0
- package/dist/events/themes/Butterflies.svelte.d.ts +3 -0
- package/dist/events/themes/Fireflies.svelte +134 -0
- package/dist/events/themes/Fireflies.svelte.d.ts +3 -0
- package/dist/events/themes/Kaleidoscope.svelte +183 -0
- package/dist/events/themes/Kaleidoscope.svelte.d.ts +3 -0
- package/dist/events/themes/Matrix.svelte +153 -0
- package/dist/events/themes/Matrix.svelte.d.ts +3 -0
- package/dist/events/themes/Stars.svelte +98 -0
- package/dist/events/themes/Stars.svelte.d.ts +3 -0
- package/dist/events/thumbnails/designs.d.ts +18 -0
- package/dist/events/thumbnails/designs.js +316 -0
- package/dist/events/view-data.d.ts +38 -0
- package/dist/events/view-data.js +1 -0
- package/dist/events-data.d.ts +15 -0
- package/dist/events-data.js +9 -0
- package/dist/events-editor.d.ts +2 -0
- package/dist/events-editor.js +2 -0
- package/dist/events.d.ts +19 -0
- package/dist/events.js +17 -0
- package/dist/grain/types.d.ts +41 -0
- package/dist/grain/types.js +1 -0
- package/dist/grain/urls.d.ts +7 -0
- package/dist/grain/urls.js +74 -0
- package/dist/grain/xrpc.d.ts +3 -0
- package/dist/grain/xrpc.js +129 -0
- package/dist/grain.d.ts +3 -0
- package/dist/grain.js +2 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +9 -0
- package/dist/media.d.ts +10 -0
- package/dist/media.js +1 -0
- package/dist/social/external-embeds.d.ts +39 -0
- package/dist/social/external-embeds.js +107 -0
- package/dist/social/posts.d.ts +59 -17
- package/dist/social/posts.js +99 -12
- package/dist/social.d.ts +2 -0
- package/dist/social.js +1 -0
- package/dist/streamplace.d.ts +6 -0
- package/dist/streamplace.js +1 -0
- package/package.json +58 -4
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { EventViewData } from './view-data.js';
|
|
2
|
+
import { type AttendeeCounts } from './EventAttendees.svelte';
|
|
3
|
+
import { type Snippet } from 'svelte';
|
|
4
|
+
import type { EditorAdapter, EditorViewer } from './editor/adapter.js';
|
|
5
|
+
type $$ComponentProps = {
|
|
6
|
+
data: EventViewData;
|
|
7
|
+
adapter: EditorAdapter;
|
|
8
|
+
viewer: EditorViewer;
|
|
9
|
+
/** Current page URL — used for the OG image link and the calendar button. */
|
|
10
|
+
pageUrl: URL;
|
|
11
|
+
embedMode?: boolean;
|
|
12
|
+
/** When set, the share modal / Bluesky post embed use this URL instead
|
|
13
|
+
* of the canonical atmo.rsvp event URL. Useful for embedders that want
|
|
14
|
+
* share links to point at their own event page. */
|
|
15
|
+
shareUrlOverride?: string;
|
|
16
|
+
/** App-owned actions such as private reporting; omitted in embeds. */
|
|
17
|
+
extraActions?: Snippet;
|
|
18
|
+
/** App-owned actions below the attendee lists, with current RSVP counts. */
|
|
19
|
+
attendeeActions?: Snippet<[AttendeeCounts]>;
|
|
20
|
+
};
|
|
21
|
+
declare const EventView: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
22
|
+
type EventView = ReturnType<typeof EventView>;
|
|
23
|
+
export default EventView;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Portal } from 'bits-ui';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
processImageFile,
|
|
6
|
+
isDragOver = $bindable()
|
|
7
|
+
}: {
|
|
8
|
+
processImageFile: (file: File) => Promise<void>;
|
|
9
|
+
isDragOver: boolean;
|
|
10
|
+
} = $props();
|
|
11
|
+
|
|
12
|
+
function handleDragOver(event: DragEvent) {
|
|
13
|
+
event.preventDefault();
|
|
14
|
+
event.stopPropagation();
|
|
15
|
+
|
|
16
|
+
const dt = event.dataTransfer;
|
|
17
|
+
if (!dt) return;
|
|
18
|
+
|
|
19
|
+
let imageCount = 0;
|
|
20
|
+
if (dt.items) {
|
|
21
|
+
for (let i = 0; i < dt.items.length; i++) {
|
|
22
|
+
const item = dt.items[i];
|
|
23
|
+
if (item && item.kind === 'file' && item.type.startsWith('image/')) {
|
|
24
|
+
imageCount++;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
} else if (dt.files) {
|
|
28
|
+
for (let i = 0; i < dt.files.length; i++) {
|
|
29
|
+
const file = dt.files[i];
|
|
30
|
+
if (file?.type.startsWith('image/')) {
|
|
31
|
+
imageCount++;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
isDragOver = imageCount > 0;
|
|
37
|
+
}
|
|
38
|
+
function handleDragLeave(event: DragEvent) {
|
|
39
|
+
event.preventDefault();
|
|
40
|
+
event.stopPropagation();
|
|
41
|
+
isDragOver = false;
|
|
42
|
+
}
|
|
43
|
+
async function handleDrop(event: DragEvent) {
|
|
44
|
+
event.preventDefault();
|
|
45
|
+
event.stopPropagation();
|
|
46
|
+
isDragOver = false;
|
|
47
|
+
if (!event.dataTransfer?.files?.length) return;
|
|
48
|
+
for (const file of event.dataTransfer.files) {
|
|
49
|
+
if (file?.type.startsWith('image/')) {
|
|
50
|
+
await processImageFile(file);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<svelte:window ondragover={handleDragOver} ondragleave={handleDragLeave} ondrop={handleDrop} />
|
|
57
|
+
|
|
58
|
+
{#if isDragOver}
|
|
59
|
+
<Portal>
|
|
60
|
+
<div
|
|
61
|
+
class="text-primary pointer-events-none absolute inset-0 z-[1000] flex items-center justify-center bg-base-100/80 text-4xl font-bold backdrop-blur-md dark:bg-base-900/80 dark:text-base-100"
|
|
62
|
+
>
|
|
63
|
+
Drop file to add it to your message
|
|
64
|
+
</div>
|
|
65
|
+
</Portal>
|
|
66
|
+
{/if}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
processImageFile: (file: File) => Promise<void>;
|
|
3
|
+
isDragOver: boolean;
|
|
4
|
+
};
|
|
5
|
+
declare const ImageDropper: import("svelte").Component<$$ComponentProps, {}, "isDragOver">;
|
|
6
|
+
type ImageDropper = ReturnType<typeof ImageDropper>;
|
|
7
|
+
export default ImageDropper;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import { setWorkerUrl } from 'maplibre-gl';
|
|
3
|
+
import workerUrl from 'maplibre-gl/dist/maplibre-gl-worker.mjs?url';
|
|
4
|
+
|
|
5
|
+
// Vite moves MapLibre's main module when optimizing dependencies, so its
|
|
6
|
+
// default relative worker URL breaks. Emit the worker as an explicit asset.
|
|
7
|
+
setWorkerUrl(workerUrl);
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<script lang="ts">
|
|
11
|
+
import { MapLibre, Projection, Marker, AttributionControl } from 'svelte-maplibre-gl';
|
|
12
|
+
import type { Map as MapLibreMap } from 'maplibre-gl';
|
|
13
|
+
|
|
14
|
+
let { lat, lng, zoom = 11 }: { lat: number; lng: number; zoom?: number } = $props();
|
|
15
|
+
|
|
16
|
+
let map: MapLibreMap | undefined = $state();
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<MapLibre
|
|
20
|
+
bind:map
|
|
21
|
+
class="h-full w-full overflow-hidden rounded-xl"
|
|
22
|
+
style="https://tiles.openfreemap.org/styles/liberty"
|
|
23
|
+
{zoom}
|
|
24
|
+
center={[lng, lat]}
|
|
25
|
+
attributionControl={false}
|
|
26
|
+
>
|
|
27
|
+
<AttributionControl position="bottom-left" compact={false} />
|
|
28
|
+
<Projection type="globe" />
|
|
29
|
+
<Marker lnglat={[lng, lat]}>
|
|
30
|
+
{#snippet content()}
|
|
31
|
+
<div class="size-10 rounded-full bg-radial from-accent-400 via-transparent p-3">
|
|
32
|
+
<div class="size-4 rounded-full bg-accent-500 ring-2 ring-white"></div>
|
|
33
|
+
</div>
|
|
34
|
+
{/snippet}
|
|
35
|
+
</Marker>
|
|
36
|
+
</MapLibre>
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
Modal,
|
|
4
|
+
Button,
|
|
5
|
+
Checkbox,
|
|
6
|
+
Label,
|
|
7
|
+
PostComposer,
|
|
8
|
+
editorJsonToBlueskyPost,
|
|
9
|
+
createActorSearch,
|
|
10
|
+
LinkCard,
|
|
11
|
+
textToDoc,
|
|
12
|
+
graphemeLength,
|
|
13
|
+
type Editor
|
|
14
|
+
} from '../index.js';
|
|
15
|
+
import { tokenize } from '@atcute/bluesky-richtext-parser';
|
|
16
|
+
import { tokensToFacets } from './editor/save.js';
|
|
17
|
+
import type { EditorAdapter, EditorViewer } from './editor/adapter.js';
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
open = $bindable(false),
|
|
21
|
+
canSetEventComments = false,
|
|
22
|
+
eventDid,
|
|
23
|
+
eventRkey,
|
|
24
|
+
eventName,
|
|
25
|
+
eventUrl,
|
|
26
|
+
eventDescription,
|
|
27
|
+
ogImageUrl,
|
|
28
|
+
initialText,
|
|
29
|
+
adapter,
|
|
30
|
+
viewer,
|
|
31
|
+
onPosted
|
|
32
|
+
}: {
|
|
33
|
+
open: boolean;
|
|
34
|
+
canSetEventComments?: boolean;
|
|
35
|
+
eventDid: string;
|
|
36
|
+
eventRkey: string;
|
|
37
|
+
eventName: string;
|
|
38
|
+
eventUrl: string;
|
|
39
|
+
eventDescription?: string;
|
|
40
|
+
ogImageUrl?: string;
|
|
41
|
+
initialText: string;
|
|
42
|
+
adapter: EditorAdapter;
|
|
43
|
+
viewer: EditorViewer;
|
|
44
|
+
onPosted?: (ref: { uri: string; cid: string; showComments: boolean }) => void;
|
|
45
|
+
} = $props();
|
|
46
|
+
|
|
47
|
+
const searchActors = createActorSearch();
|
|
48
|
+
let text = $state('');
|
|
49
|
+
let editor = $state<Editor | null>(null);
|
|
50
|
+
let prefilledForOpen = $state(false);
|
|
51
|
+
let showComments = $state(true);
|
|
52
|
+
let posting = $state(false);
|
|
53
|
+
let errorMessage = $state<string | null>(null);
|
|
54
|
+
|
|
55
|
+
// Seed once per opening, after the lazy editor has mounted.
|
|
56
|
+
$effect(() => {
|
|
57
|
+
if (!open) {
|
|
58
|
+
prefilledForOpen = false;
|
|
59
|
+
showComments = true;
|
|
60
|
+
errorMessage = null;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (!editor || prefilledForOpen) return;
|
|
64
|
+
text = initialText;
|
|
65
|
+
editor.commands.setContent(textToDoc(initialText));
|
|
66
|
+
prefilledForOpen = true;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Bluesky's external embed accepts a thumb blob up to ~1MB. Fetch the OG
|
|
70
|
+
// image, upload it to the user's PDS, return a clean blob ref. On any failure
|
|
71
|
+
// (CORS, large image, network) we fall back to a thumb-less embed rather than
|
|
72
|
+
// blocking the post.
|
|
73
|
+
async function fetchAndUploadThumbnail(url: string) {
|
|
74
|
+
try {
|
|
75
|
+
const resp = await fetch(url);
|
|
76
|
+
if (!resp.ok) return null;
|
|
77
|
+
const blob = await resp.blob();
|
|
78
|
+
if (!blob.type.startsWith('image/')) return null;
|
|
79
|
+
if (blob.size > 1_000_000) return null;
|
|
80
|
+
const result = await adapter.uploadBlob(blob);
|
|
81
|
+
return {
|
|
82
|
+
$type: result.$type,
|
|
83
|
+
ref: result.ref,
|
|
84
|
+
mimeType: result.mimeType,
|
|
85
|
+
size: result.size
|
|
86
|
+
};
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.warn('PostToBlueskyModal: thumbnail upload failed, posting without thumb', err);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function handlePost() {
|
|
94
|
+
if (!viewer.did || !viewer.isLoggedIn || viewer.canPostToBluesky !== true || posting) return;
|
|
95
|
+
|
|
96
|
+
const liveJson = editor?.getJSON() ?? textToDoc(text);
|
|
97
|
+
|
|
98
|
+
posting = true;
|
|
99
|
+
errorMessage = null;
|
|
100
|
+
try {
|
|
101
|
+
const { text, facets } = editorJsonToBlueskyPost(liveJson);
|
|
102
|
+
if (graphemeLength(text) > 300 || new TextEncoder().encode(text).length > 3000) {
|
|
103
|
+
errorMessage = 'Posts must be 300 characters or fewer.';
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// Also recognize handles typed or pasted without selecting a suggestion.
|
|
107
|
+
const detected = await tokensToFacets(tokenize(text), (handle) =>
|
|
108
|
+
adapter.resolveHandle(handle)
|
|
109
|
+
);
|
|
110
|
+
for (const facet of detected) {
|
|
111
|
+
const index = facet.index;
|
|
112
|
+
if (
|
|
113
|
+
!facets.some(
|
|
114
|
+
(existing) =>
|
|
115
|
+
index.byteStart < existing.index.byteEnd && index.byteEnd > existing.index.byteStart
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
facets.push(facet);
|
|
119
|
+
}
|
|
120
|
+
facets.sort((a, b) => a.index.byteStart - b.index.byteStart);
|
|
121
|
+
|
|
122
|
+
if (!text.trim()) {
|
|
123
|
+
errorMessage = 'Post text cannot be empty';
|
|
124
|
+
posting = false;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const externalEmbed: Record<string, unknown> = {
|
|
129
|
+
uri: eventUrl,
|
|
130
|
+
title: eventName,
|
|
131
|
+
description: eventDescription ?? ''
|
|
132
|
+
};
|
|
133
|
+
if (ogImageUrl) {
|
|
134
|
+
const thumb = await fetchAndUploadThumbnail(ogImageUrl);
|
|
135
|
+
if (thumb) externalEmbed.thumb = thumb;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const postRecord: Record<string, unknown> = {
|
|
139
|
+
$type: 'app.bsky.feed.post',
|
|
140
|
+
text,
|
|
141
|
+
createdAt: new Date().toISOString(),
|
|
142
|
+
embed: {
|
|
143
|
+
$type: 'app.bsky.embed.external',
|
|
144
|
+
external: externalEmbed
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
if (facets.length > 0) postRecord.facets = facets;
|
|
148
|
+
|
|
149
|
+
// The grant may have changed while resolving facets or uploading a thumb.
|
|
150
|
+
if (viewer.canPostToBluesky !== true) return;
|
|
151
|
+
const postResp = await adapter.createRecord({
|
|
152
|
+
collection: 'app.bsky.feed.post',
|
|
153
|
+
record: postRecord
|
|
154
|
+
});
|
|
155
|
+
if (!postResp.uri || !postResp.cid) {
|
|
156
|
+
console.error('PostToBlueskyModal: PDS response missing uri/cid', postResp);
|
|
157
|
+
throw new Error(
|
|
158
|
+
'PDS rejected the post — try logging out and back in to refresh permissions'
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
const postUri = postResp.uri;
|
|
162
|
+
const postCid = postResp.cid;
|
|
163
|
+
|
|
164
|
+
if (canSetEventComments) {
|
|
165
|
+
const fresh = await adapter.getRecord({
|
|
166
|
+
did: eventDid,
|
|
167
|
+
collection: 'community.lexicon.calendar.event',
|
|
168
|
+
rkey: eventRkey
|
|
169
|
+
});
|
|
170
|
+
const updatedRecord = {
|
|
171
|
+
...fresh.value,
|
|
172
|
+
bskyPostRef: {
|
|
173
|
+
uri: postUri,
|
|
174
|
+
cid: postCid,
|
|
175
|
+
showComments
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
await adapter.putRecord({
|
|
179
|
+
collection: 'community.lexicon.calendar.event',
|
|
180
|
+
rkey: eventRkey,
|
|
181
|
+
record: updatedRecord
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
await adapter.notifyUpdate?.(
|
|
185
|
+
`at://${eventDid}/community.lexicon.calendar.event/${eventRkey}`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
onPosted?.({ uri: postUri, cid: postCid, showComments });
|
|
190
|
+
open = false;
|
|
191
|
+
} catch (err) {
|
|
192
|
+
console.error('PostToBlueskyModal: post failed', err);
|
|
193
|
+
errorMessage = err instanceof Error ? err.message : 'Failed to post';
|
|
194
|
+
} finally {
|
|
195
|
+
posting = false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
</script>
|
|
199
|
+
|
|
200
|
+
<Modal bind:open title="Share to Bluesky" dismissible={!posting}>
|
|
201
|
+
<div class="space-y-4">
|
|
202
|
+
<div
|
|
203
|
+
class="space-y-3 rounded-xl border border-base-200 bg-base-50 p-3 dark:border-base-800 dark:bg-base-950/30"
|
|
204
|
+
>
|
|
205
|
+
<PostComposer
|
|
206
|
+
bind:editor
|
|
207
|
+
bind:value={text}
|
|
208
|
+
{searchActors}
|
|
209
|
+
disabled={posting}
|
|
210
|
+
maxLength={300}
|
|
211
|
+
/>
|
|
212
|
+
<LinkCard
|
|
213
|
+
href={eventUrl}
|
|
214
|
+
meta={{
|
|
215
|
+
title: eventName,
|
|
216
|
+
description: eventDescription,
|
|
217
|
+
image: ogImageUrl
|
|
218
|
+
}}
|
|
219
|
+
/>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
{#if canSetEventComments}
|
|
223
|
+
<Label class="flex items-center gap-2">
|
|
224
|
+
<Checkbox bind:checked={showComments} disabled={posting} />
|
|
225
|
+
<span class="text-sm text-base-700 dark:text-base-300"> Show comments on event page </span>
|
|
226
|
+
</Label>
|
|
227
|
+
{/if}
|
|
228
|
+
|
|
229
|
+
{#if errorMessage}
|
|
230
|
+
<p role="alert" class="text-sm text-red-600 dark:text-red-400">{errorMessage}</p>
|
|
231
|
+
{/if}
|
|
232
|
+
|
|
233
|
+
<Button
|
|
234
|
+
class="w-full"
|
|
235
|
+
onclick={handlePost}
|
|
236
|
+
disabled={posting ||
|
|
237
|
+
!viewer.isLoggedIn ||
|
|
238
|
+
!viewer.did ||
|
|
239
|
+
viewer.canPostToBluesky !== true ||
|
|
240
|
+
!text.trim() ||
|
|
241
|
+
graphemeLength(text) > 300}
|
|
242
|
+
>
|
|
243
|
+
{posting ? 'Posting…' : 'Post'}
|
|
244
|
+
</Button>
|
|
245
|
+
</div>
|
|
246
|
+
</Modal>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { EditorAdapter, EditorViewer } from './editor/adapter.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
open: boolean;
|
|
4
|
+
canSetEventComments?: boolean;
|
|
5
|
+
eventDid: string;
|
|
6
|
+
eventRkey: string;
|
|
7
|
+
eventName: string;
|
|
8
|
+
eventUrl: string;
|
|
9
|
+
eventDescription?: string;
|
|
10
|
+
ogImageUrl?: string;
|
|
11
|
+
initialText: string;
|
|
12
|
+
adapter: EditorAdapter;
|
|
13
|
+
viewer: EditorViewer;
|
|
14
|
+
onPosted?: (ref: {
|
|
15
|
+
uri: string;
|
|
16
|
+
cid: string;
|
|
17
|
+
showComments: boolean;
|
|
18
|
+
}) => void;
|
|
19
|
+
};
|
|
20
|
+
declare const PostToBlueskyModal: import("svelte").Component<$$ComponentProps, {}, "open">;
|
|
21
|
+
type PostToBlueskyModal = ReturnType<typeof PostToBlueskyModal>;
|
|
22
|
+
export default PostToBlueskyModal;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Modal, Button, Avatar, LinkCard } from '../index.js';
|
|
3
|
+
import PostToBlueskyModal from './PostToBlueskyModal.svelte';
|
|
4
|
+
import type { EditorAdapter, EditorViewer } from './editor/adapter.js';
|
|
5
|
+
|
|
6
|
+
let {
|
|
7
|
+
open = $bindable(false),
|
|
8
|
+
url,
|
|
9
|
+
title = 'Event created!',
|
|
10
|
+
shareText,
|
|
11
|
+
eventName,
|
|
12
|
+
ogImageUrl,
|
|
13
|
+
canSetEventComments = false,
|
|
14
|
+
eventDid,
|
|
15
|
+
eventRkey,
|
|
16
|
+
eventDescription,
|
|
17
|
+
adapter,
|
|
18
|
+
viewer,
|
|
19
|
+
onPosted
|
|
20
|
+
}: {
|
|
21
|
+
open: boolean;
|
|
22
|
+
url: string;
|
|
23
|
+
title?: string;
|
|
24
|
+
shareText?: string;
|
|
25
|
+
eventName?: string;
|
|
26
|
+
ogImageUrl?: string;
|
|
27
|
+
canSetEventComments?: boolean;
|
|
28
|
+
eventDid?: string;
|
|
29
|
+
eventRkey?: string;
|
|
30
|
+
eventDescription?: string;
|
|
31
|
+
adapter: EditorAdapter;
|
|
32
|
+
viewer: EditorViewer;
|
|
33
|
+
onPosted?: (ref: { uri: string; cid: string; showComments: boolean }) => void;
|
|
34
|
+
} = $props();
|
|
35
|
+
|
|
36
|
+
let copiedUrl = $state(false);
|
|
37
|
+
let copiedText = $state(false);
|
|
38
|
+
let showPostModal = $state(false);
|
|
39
|
+
|
|
40
|
+
let textBeforeUrl = $derived(shareText ? shareText.replace(url, '').trim() : undefined);
|
|
41
|
+
|
|
42
|
+
async function copyUrl() {
|
|
43
|
+
try {
|
|
44
|
+
await navigator.clipboard.writeText(url);
|
|
45
|
+
copiedUrl = true;
|
|
46
|
+
setTimeout(() => (copiedUrl = false), 2000);
|
|
47
|
+
} catch {
|
|
48
|
+
// Clipboard access may be denied; keep the selectable link available.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function copyText() {
|
|
53
|
+
if (!shareText) return;
|
|
54
|
+
try {
|
|
55
|
+
await navigator.clipboard.writeText(shareText);
|
|
56
|
+
copiedText = true;
|
|
57
|
+
setTimeout(() => (copiedText = false), 2000);
|
|
58
|
+
} catch {
|
|
59
|
+
// Clipboard access may be denied; sharing remains available.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let blueskyButton = $state<HTMLButtonElement | HTMLAnchorElement | null>(null);
|
|
64
|
+
|
|
65
|
+
let canPostDirectly = $derived(
|
|
66
|
+
!!eventDid &&
|
|
67
|
+
!!eventRkey &&
|
|
68
|
+
!!eventName &&
|
|
69
|
+
viewer.isLoggedIn &&
|
|
70
|
+
viewer.canPostToBluesky === true
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
$effect(() => {
|
|
74
|
+
if (!canPostDirectly) showPostModal = false;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
let blueskyIntentUrl = $derived(
|
|
78
|
+
`https://bsky.app/intent/compose?text=${encodeURIComponent(shareText || url)}`
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
function handleBlueskyClick() {
|
|
82
|
+
if (canPostDirectly) showPostModal = true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function handlePostedFromInner(ref: { uri: string; cid: string; showComments: boolean }) {
|
|
86
|
+
onPosted?.(ref);
|
|
87
|
+
open = false;
|
|
88
|
+
}
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<Modal
|
|
92
|
+
bind:open
|
|
93
|
+
{title}
|
|
94
|
+
description="Share it with others."
|
|
95
|
+
onOpenAutoFocus={(e) => {
|
|
96
|
+
e.preventDefault();
|
|
97
|
+
blueskyButton?.focus();
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
<div>
|
|
101
|
+
<div
|
|
102
|
+
class="mb-6 overflow-hidden rounded-xl border border-base-400/40 bg-base-200 px-4 py-3 text-left dark:border-base-700 dark:bg-base-950/30"
|
|
103
|
+
>
|
|
104
|
+
{#if viewer.isLoggedIn}
|
|
105
|
+
<div class="flex items-center gap-2 pb-4">
|
|
106
|
+
<Avatar
|
|
107
|
+
src={viewer.avatar}
|
|
108
|
+
name={viewer.displayName || viewer.handle || viewer.did || 'Account'}
|
|
109
|
+
class="size-6"
|
|
110
|
+
/>
|
|
111
|
+
<span class="text-sm font-medium text-base-700 dark:text-base-200"
|
|
112
|
+
>{viewer.handle ?? viewer.did}</span
|
|
113
|
+
>
|
|
114
|
+
</div>
|
|
115
|
+
{/if}
|
|
116
|
+
{#if textBeforeUrl}
|
|
117
|
+
<p class="text-md mb-3 font-semibold text-base-700 dark:text-base-200">{textBeforeUrl}</p>
|
|
118
|
+
{/if}
|
|
119
|
+
{#if eventName}
|
|
120
|
+
<LinkCard
|
|
121
|
+
href={url}
|
|
122
|
+
meta={{
|
|
123
|
+
title: eventName,
|
|
124
|
+
image: ogImageUrl
|
|
125
|
+
}}
|
|
126
|
+
class="mb-1"
|
|
127
|
+
/>
|
|
128
|
+
{/if}
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<div class="flex flex-col gap-2 sm:flex-row">
|
|
132
|
+
<Button class="flex-1" variant="secondary" onclick={copyUrl}>
|
|
133
|
+
{copiedUrl ? 'Copied!' : 'Copy link'}
|
|
134
|
+
</Button>
|
|
135
|
+
{#if shareText}
|
|
136
|
+
<Button class="flex-1" variant="secondary" onclick={copyText}>
|
|
137
|
+
{copiedText ? 'Copied!' : 'Copy text'}
|
|
138
|
+
</Button>
|
|
139
|
+
{/if}
|
|
140
|
+
{#if canPostDirectly}
|
|
141
|
+
<Button bind:ref={blueskyButton} class="flex-1" onclick={handleBlueskyClick}>
|
|
142
|
+
Share to Bluesky
|
|
143
|
+
</Button>
|
|
144
|
+
{:else}
|
|
145
|
+
<Button
|
|
146
|
+
bind:ref={blueskyButton}
|
|
147
|
+
class="flex-1"
|
|
148
|
+
href={blueskyIntentUrl}
|
|
149
|
+
target="_blank"
|
|
150
|
+
rel="noopener"
|
|
151
|
+
>
|
|
152
|
+
Share to Bluesky
|
|
153
|
+
</Button>
|
|
154
|
+
{/if}
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</Modal>
|
|
158
|
+
|
|
159
|
+
{#if canPostDirectly && eventDid && eventRkey && eventName}
|
|
160
|
+
<PostToBlueskyModal
|
|
161
|
+
bind:open={showPostModal}
|
|
162
|
+
{canSetEventComments}
|
|
163
|
+
{eventDid}
|
|
164
|
+
{eventRkey}
|
|
165
|
+
{eventName}
|
|
166
|
+
eventUrl={url}
|
|
167
|
+
{eventDescription}
|
|
168
|
+
{ogImageUrl}
|
|
169
|
+
initialText={textBeforeUrl ?? eventName}
|
|
170
|
+
{adapter}
|
|
171
|
+
{viewer}
|
|
172
|
+
onPosted={handlePostedFromInner}
|
|
173
|
+
/>
|
|
174
|
+
{/if}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { EditorAdapter, EditorViewer } from './editor/adapter.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
open: boolean;
|
|
4
|
+
url: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
shareText?: string;
|
|
7
|
+
eventName?: string;
|
|
8
|
+
ogImageUrl?: string;
|
|
9
|
+
canSetEventComments?: boolean;
|
|
10
|
+
eventDid?: string;
|
|
11
|
+
eventRkey?: string;
|
|
12
|
+
eventDescription?: string;
|
|
13
|
+
adapter: EditorAdapter;
|
|
14
|
+
viewer: EditorViewer;
|
|
15
|
+
onPosted?: (ref: {
|
|
16
|
+
uri: string;
|
|
17
|
+
cid: string;
|
|
18
|
+
showComments: boolean;
|
|
19
|
+
}) => void;
|
|
20
|
+
};
|
|
21
|
+
declare const ShareModal: import("svelte").Component<$$ComponentProps, {}, "open">;
|
|
22
|
+
type ShareModal = ReturnType<typeof ShareModal>;
|
|
23
|
+
export default ShareModal;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button } from '../index.js';
|
|
3
|
+
import { socialClientInfo, type PreferredSocialClient } from '../social.js';
|
|
4
|
+
import { starterPackClientUrl } from './starter-pack-url.js';
|
|
5
|
+
|
|
6
|
+
let {
|
|
7
|
+
eventUri,
|
|
8
|
+
openPack,
|
|
9
|
+
socialClient
|
|
10
|
+
}: {
|
|
11
|
+
eventUri: string;
|
|
12
|
+
socialClient?: PreferredSocialClient | null;
|
|
13
|
+
/** The host owns account access and creation; this component never writes records. */
|
|
14
|
+
openPack: (eventUri: string) => Promise<string>;
|
|
15
|
+
} = $props();
|
|
16
|
+
let pending = $state(false);
|
|
17
|
+
let message: string | null = $state(null);
|
|
18
|
+
let canonicalPackUrl: string | null = $state(null);
|
|
19
|
+
let packUrl = $derived(
|
|
20
|
+
canonicalPackUrl ? starterPackClientUrl(canonicalPackUrl, socialClient) : null
|
|
21
|
+
);
|
|
22
|
+
let clientLabel = $derived(socialClientInfo(socialClient).label);
|
|
23
|
+
|
|
24
|
+
async function open() {
|
|
25
|
+
if (pending) return;
|
|
26
|
+
// Open synchronously with the click so async creation doesn't hit popup blockers.
|
|
27
|
+
const tab = window.open('about:blank', '_blank');
|
|
28
|
+
if (tab) tab.opener = null;
|
|
29
|
+
pending = true;
|
|
30
|
+
message = null;
|
|
31
|
+
try {
|
|
32
|
+
const canonicalUrl = await openPack(eventUri);
|
|
33
|
+
const url = starterPackClientUrl(canonicalUrl, socialClient);
|
|
34
|
+
if (!url) throw new Error('Invalid starter pack URL');
|
|
35
|
+
canonicalPackUrl = canonicalUrl;
|
|
36
|
+
if (tab) {
|
|
37
|
+
if (!tab.closed) tab.location.replace(url);
|
|
38
|
+
} else window.location.assign(url);
|
|
39
|
+
} catch {
|
|
40
|
+
tab?.close();
|
|
41
|
+
message = 'Couldn’t open the starter pack. Please try again.';
|
|
42
|
+
} finally {
|
|
43
|
+
pending = false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<div class="mt-4">
|
|
49
|
+
<p class="mb-1 text-xs text-muted">Follow attendees on {clientLabel}.</p>
|
|
50
|
+
{#if packUrl}
|
|
51
|
+
<a
|
|
52
|
+
href={packUrl}
|
|
53
|
+
target="_blank"
|
|
54
|
+
rel="noopener noreferrer"
|
|
55
|
+
class="rounded text-sm text-action underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-focus"
|
|
56
|
+
>Starter pack ↗</a
|
|
57
|
+
>
|
|
58
|
+
{:else}
|
|
59
|
+
<Button
|
|
60
|
+
variant="link"
|
|
61
|
+
size="sm"
|
|
62
|
+
class="min-h-0 p-0 text-sm font-normal"
|
|
63
|
+
onclick={open}
|
|
64
|
+
disabled={pending}
|
|
65
|
+
>
|
|
66
|
+
{pending ? 'Opening…' : 'Starter pack ↗'}
|
|
67
|
+
</Button>
|
|
68
|
+
{/if}
|
|
69
|
+
{#if message}
|
|
70
|
+
<p role="alert" class="mt-2 text-sm text-muted">{message}</p>
|
|
71
|
+
{/if}
|
|
72
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type PreferredSocialClient } from '../social.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
eventUri: string;
|
|
4
|
+
socialClient?: PreferredSocialClient | null;
|
|
5
|
+
/** The host owns account access and creation; this component never writes records. */
|
|
6
|
+
openPack: (eventUri: string) => Promise<string>;
|
|
7
|
+
};
|
|
8
|
+
declare const StarterPackButton: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type StarterPackButton = ReturnType<typeof StarterPackButton>;
|
|
10
|
+
export default StarterPackButton;
|