grain.social is a photo sharing platform built on atproto.

feat: add share gallery dialog with additional copy link support

+98 -29
+3 -3
src/components/GalleryPage.tsx
··· 7 7 import { GalleryInfo } from "./GalleryInfo.tsx"; 8 8 import { GalleryLayout } from "./GalleryLayout.tsx"; 9 9 import { ModerationWrapper } from "./ModerationWrapper.tsx"; 10 - import { ShareGalleryButton } from "./ShareGalleryButton.tsx"; 10 + import { ShareGalleryDialogButton } from "./ShareGalleryDialog.tsx"; 11 11 12 12 export function GalleryPage({ 13 13 gallery, ··· 30 30 ? ( 31 31 <div class="flex self-start gap-2 w-full sm:w-fit flex-col sm:flex-row sm:flex-wrap sm:justify-end"> 32 32 <EditGalleryButton gallery={gallery} /> 33 - <ShareGalleryButton gallery={gallery} /> 33 + <ShareGalleryDialogButton gallery={gallery} /> 34 34 <FavoriteButton gallery={gallery} /> 35 35 </div> 36 36 ) ··· 38 38 {!isCreator 39 39 ? ( 40 40 <div class="flex self-start gap-2 w-full sm:w-fit flex-col sm:flex-row"> 41 - <ShareGalleryButton gallery={gallery} /> 41 + <ShareGalleryDialogButton gallery={gallery} /> 42 42 <FavoriteButton gallery={gallery} /> 43 43 </div> 44 44 )
-26
src/components/ShareGalleryButton.tsx
··· 1 - import { GalleryView } from "$lexicon/types/social/grain/gallery/defs.ts"; 2 - import { publicGalleryLink } from "../utils.ts"; 3 - import { Button } from "./Button.tsx"; 4 - 5 - export function ShareGalleryButton( 6 - { gallery }: Readonly<{ gallery: GalleryView }>, 7 - ) { 8 - const intentLink = `https://bsky.app/intent/compose?text=${ 9 - encodeURIComponent( 10 - "Check out this gallery on @grain.social \n" + 11 - publicGalleryLink(gallery.creator.handle, gallery.uri), 12 - ) 13 - }`; 14 - return ( 15 - <Button 16 - variant="primary" 17 - class="whitespace-nowrap" 18 - asChild 19 - > 20 - <a href={intentLink} target="_blank" rel="noopener noreferrer"> 21 - <i class="fa-solid fa-arrow-up-from-bracket mr-2" /> 22 - Share to Bluesky 23 - </a> 24 - </Button> 25 - ); 26 - }
+72
src/components/ShareGalleryDialog.tsx
··· 1 + import { GalleryView } from "$lexicon/types/social/grain/gallery/defs.ts"; 2 + import { AtUri } from "@atproto/syntax"; 3 + import { publicGalleryLink } from "../utils.ts"; 4 + import { Button } from "./Button.tsx"; 5 + import { Dialog } from "./Dialog.tsx"; 6 + 7 + export function ShareGalleryDialog({ gallery }: Readonly<{ 8 + gallery: GalleryView; 9 + }>) { 10 + const publicLink = publicGalleryLink( 11 + gallery.creator.handle, 12 + gallery.uri, 13 + ); 14 + const intentLink = `https://bsky.app/intent/compose?text=${ 15 + encodeURIComponent( 16 + "Check out this gallery on @grain.social \n" + 17 + publicLink, 18 + ) 19 + }`; 20 + return ( 21 + <Dialog> 22 + <Dialog.Content class="gap-4"> 23 + <Dialog.Title>Share gallery</Dialog.Title> 24 + <Dialog.X class="fill-zinc-950 dark:fill-zinc-50" /> 25 + 26 + <ul class="divide-y divide-zinc-200 dark:divide-zinc-800 border-t border-b border-zinc-200 dark:border-zinc-800"> 27 + <li class="w-full hover:bg-zinc-200 dark:hover:bg-zinc-800"> 28 + <a 29 + href={intentLink} 30 + target="_blank" 31 + rel="noopener noreferrer" 32 + class="flex gap-2 justify-start items-center text-left w-full px-2 py-4 cursor-pointer" 33 + > 34 + <i class="fa-brands fa-bluesky" /> 35 + Share to Bluesky 36 + </a> 37 + </li> 38 + <li class="w-full hover:bg-zinc-200 dark:hover:bg-zinc-800"> 39 + <button 40 + type="button" 41 + class="flex gap-2 justify-start items-center text-left w-full px-2 py-4 cursor-pointer" 42 + _={`on click call Grain.utils.copyToClipboard("${publicLink}")`} 43 + > 44 + <i class="fa-solid fa-link"></i> 45 + Copy link 46 + </button> 47 + </li> 48 + </ul> 49 + <Dialog.Close variant="secondary">Close</Dialog.Close> 50 + </Dialog.Content> 51 + </Dialog> 52 + ); 53 + } 54 + 55 + export function ShareGalleryDialogButton( 56 + { gallery }: Readonly<{ gallery: GalleryView }>, 57 + ) { 58 + const rkey = new AtUri(gallery.uri).rkey; 59 + return ( 60 + <Button 61 + variant="primary" 62 + class="whitespace-nowrap" 63 + hx-get={`/dialogs/${gallery.creator.did}/gallery/${rkey}/share`} 64 + hx-trigger="click" 65 + hx-target="#dialog-target" 66 + hx-swap="innerHTML" 67 + > 68 + <i class="fa-solid fa-arrow-up-from-bracket mr-2" /> 69 + Share 70 + </Button> 71 + ); 72 + }
+1
src/main.tsx
··· 78 78 route("/dialogs/gallery/:rkey/edit", dialogs.editGalleryDetails), 79 79 route("/dialogs/gallery/:rkey/sort", dialogs.sortGallery), 80 80 route("/dialogs/gallery/:rkey/library", dialogs.galleryAddFromLibrary), 81 + route("/dialogs/:creatorDid/gallery/:rkey/share", dialogs.galleryShare), 81 82 route("/dialogs/gallery/:did/select", dialogs.gallerySelect), 82 83 route("/dialogs/label/:src/:val", dialogs.labelValueDefinition), 83 84 route("/dialogs/profile", dialogs.editProfile),
+15
src/routes/dialogs.tsx
··· 23 23 import { PhotoExifDialog } from "../components/PhotoExifDialog.tsx"; 24 24 import { ProfileDialog } from "../components/ProfileDialog.tsx"; 25 25 import { RemovePhotoDialog } from "../components/RemovePhotoDialog.tsx"; 26 + import { ShareGalleryDialog } from "../components/ShareGalleryDialog.tsx"; 26 27 import { 27 28 getActorGalleries, 28 29 getActorPhotos, ··· 226 227 galleryUri={galleryUri} 227 228 photos={photos} 228 229 />, 230 + ); 231 + }; 232 + 233 + export const galleryShare: RouteHandler = ( 234 + _req, 235 + params, 236 + ctx: BffContext<State>, 237 + ) => { 238 + const did = params.creatorDid; 239 + const rkey = params.rkey; 240 + const gallery = getGallery(did, rkey, ctx); 241 + if (!gallery) return ctx.next(); 242 + return ctx.html( 243 + <ShareGalleryDialog gallery={gallery} />, 229 244 ); 230 245 }; 231 246
+3
src/static/mod.ts
··· 6 6 import { PhotoDialog } from "./photo_dialog.ts"; 7 7 import { ProfileDialog } from "./profile_dialog.ts"; 8 8 import { UploadPage } from "./upload_page.ts"; 9 + import * as utils from "./utils.ts"; 9 10 10 11 const galleryLayout = new GalleryLayout({ 11 12 layoutMode: "justified", ··· 36 37 profileDialog?: ProfileDialog; 37 38 galleryLayout?: GalleryLayout; 38 39 galleryPhotosDialog?: GalleryPhotosDialog; 40 + utils?: typeof utils; 39 41 }; 40 42 }; 41 43 ··· 47 49 g.Grain.profileDialog = new ProfileDialog(); 48 50 g.Grain.galleryPhotosDialog = new GalleryPhotosDialog(); 49 51 g.Grain.galleryLayout = galleryLayout; 52 + g.Grain.utils = utils;
+4
src/static/utils.ts
··· 1 + export async function copyToClipboard(link: string) { 2 + await navigator.clipboard.writeText(link); 3 + alert("Link copied to clipboard!"); 4 + }