a tool for shared writing and social publishing
at update/thread-viewer 279 lines 8.3 kB view raw
1"use client"; 2import { ButtonPrimary } from "components/Buttons"; 3import { useActionState, useEffect, useState } from "react"; 4import { Input } from "components/Input"; 5import { useIdentityData } from "components/IdentityProvider"; 6import { 7 confirmEmailAuthToken, 8 requestAuthEmailToken, 9} from "actions/emailAuth"; 10import { subscribeToPublicationWithEmail } from "actions/subscribeToPublicationWithEmail"; 11import { ArrowRightTiny } from "components/Icons/ArrowRightTiny"; 12import { ShareSmall } from "components/Icons/ShareSmall"; 13import { Popover } from "components/Popover"; 14import { BlueskyTiny } from "components/Icons/BlueskyTiny"; 15import { useToaster } from "components/Toast"; 16import * as Dialog from "@radix-ui/react-dialog"; 17import { 18 subscribeToPublication, 19 unsubscribeToPublication, 20} from "./subscribeToPublication"; 21import { DotLoader } from "components/utils/DotLoader"; 22import { addFeed } from "./addFeed"; 23import { useSearchParams } from "next/navigation"; 24import LoginForm from "app/login/LoginForm"; 25import { RSSSmall } from "components/Icons/RSSSmall"; 26import { OAuthErrorMessage, isOAuthSessionError } from "components/OAuthError"; 27 28export const SubscribeWithBluesky = (props: { 29 pubName: string; 30 pub_uri: string; 31 base_url: string; 32 subscribers: { identity: string }[]; 33}) => { 34 let { identity } = useIdentityData(); 35 let searchParams = useSearchParams(); 36 let [successModalOpen, setSuccessModalOpen] = useState( 37 !!searchParams.has("showSubscribeSuccess"), 38 ); 39 let subscribed = 40 identity?.atp_did && 41 props.subscribers.find((s) => s.identity === identity.atp_did); 42 43 if (successModalOpen) 44 return ( 45 <SubscribeSuccessModal 46 open={successModalOpen} 47 setOpen={setSuccessModalOpen} 48 /> 49 ); 50 if (subscribed) { 51 return <ManageSubscription {...props} />; 52 } 53 return ( 54 <div className="flex flex-col gap-2 text-center justify-center"> 55 <div className="flex flex-row gap-2 place-self-center"> 56 <BlueskySubscribeButton 57 pub_uri={props.pub_uri} 58 setSuccessModalOpen={setSuccessModalOpen} 59 /> 60 <a 61 href={`${props.base_url}/rss`} 62 className="flex" 63 target="_blank" 64 aria-label="Subscribe to RSS" 65 > 66 <RSSSmall className="self-center" aria-hidden /> 67 </a> 68 </div> 69 </div> 70 ); 71}; 72 73export const ManageSubscription = (props: { 74 pub_uri: string; 75 subscribers: { identity: string }[]; 76 base_url: string; 77}) => { 78 let toaster = useToaster(); 79 let [hasFeed] = useState(false); 80 let [, unsubscribe, unsubscribePending] = useActionState(async () => { 81 await unsubscribeToPublication(props.pub_uri); 82 toaster({ 83 content: "You unsubscribed.", 84 type: "success", 85 }); 86 }, null); 87 return ( 88 <Popover 89 trigger={ 90 <div className="text-accent-contrast text-sm">Manage Subscription</div> 91 } 92 > 93 <div className="max-w-sm flex flex-col gap-1"> 94 <h4>Update Options</h4> 95 96 {!hasFeed && ( 97 <a 98 href="https://bsky.app/profile/leaflet.pub/feed/subscribedPublications" 99 target="_blank" 100 className=" place-self-center" 101 > 102 <ButtonPrimary fullWidth compact className="!px-4"> 103 View Bluesky Custom Feed 104 </ButtonPrimary> 105 </a> 106 )} 107 108 <a 109 href={`${props.base_url}/rss`} 110 className="flex" 111 target="_blank" 112 aria-label="Subscribe to RSS" 113 > 114 <ButtonPrimary fullWidth compact> 115 Get RSS 116 </ButtonPrimary> 117 </a> 118 119 <hr className="border-border-light my-1" /> 120 121 <form action={unsubscribe}> 122 <button className="font-bold text-accent-contrast w-max place-self-center"> 123 {unsubscribePending ? <DotLoader /> : "Unsubscribe"} 124 </button> 125 </form> 126 </div> 127 </Popover> 128 ); 129}; 130 131let BlueskySubscribeButton = (props: { 132 pub_uri: string; 133 setSuccessModalOpen: (open: boolean) => void; 134}) => { 135 let { identity } = useIdentityData(); 136 let toaster = useToaster(); 137 let [oauthError, setOauthError] = useState< 138 import("src/atproto-oauth").OAuthSessionError | null 139 >(null); 140 let [, subscribe, subscribePending] = useActionState(async () => { 141 setOauthError(null); 142 let result = await subscribeToPublication( 143 props.pub_uri, 144 window.location.href + "?refreshAuth", 145 ); 146 if (!result.success) { 147 if (isOAuthSessionError(result.error)) { 148 setOauthError(result.error); 149 } 150 return; 151 } 152 if (result.hasFeed === false) { 153 props.setSuccessModalOpen(true); 154 } 155 toaster({ content: <div>You're Subscribed!</div>, type: "success" }); 156 }, null); 157 158 let [isClient, setIsClient] = useState(false); 159 useEffect(() => { 160 setIsClient(true); 161 }, []); 162 163 if (!identity?.atp_did) { 164 return ( 165 <Popover 166 asChild 167 trigger={ 168 <ButtonPrimary className="place-self-center"> 169 <BlueskyTiny /> Subscribe with Bluesky 170 </ButtonPrimary> 171 } 172 > 173 {isClient && ( 174 <LoginForm 175 text="Log in to subscribe to this publication!" 176 noEmail 177 redirectRoute={window?.location.href + "?refreshAuth"} 178 action={{ action: "subscribe", publication: props.pub_uri }} 179 /> 180 )} 181 </Popover> 182 ); 183 } 184 185 return ( 186 <div className="flex flex-col gap-2 place-self-center"> 187 <form 188 action={subscribe} 189 className="place-self-center flex flex-row gap-1" 190 > 191 <ButtonPrimary> 192 {subscribePending ? ( 193 <DotLoader /> 194 ) : ( 195 <> 196 <BlueskyTiny /> Subscribe with Bluesky 197 </> 198 )} 199 </ButtonPrimary> 200 </form> 201 {oauthError && ( 202 <OAuthErrorMessage 203 error={oauthError} 204 className="text-center text-sm text-accent-1" 205 /> 206 )} 207 </div> 208 ); 209}; 210 211const SubscribeSuccessModal = ({ 212 open, 213 setOpen, 214}: { 215 open: boolean; 216 setOpen: (open: boolean) => void; 217}) => { 218 let searchParams = useSearchParams(); 219 let [loading, setLoading] = useState(false); 220 let toaster = useToaster(); 221 return ( 222 <Dialog.Root open={open} onOpenChange={setOpen}> 223 <Dialog.Trigger asChild></Dialog.Trigger> 224 <Dialog.Portal> 225 <Dialog.Overlay className="fixed inset-0 bg-primary data-[state=open]:animate-overlayShow opacity-10 blur-xs" /> 226 <Dialog.Content 227 className={` 228 z-20 opaque-container 229 fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 230 w-96 px-3 py-4 231 max-w-(--radix-popover-content-available-width) 232 max-h-(--radix-popover-content-available-height) 233 overflow-y-scroll no-scrollbar 234 flex flex-col gap-1 text-center justify-center 235 `} 236 > 237 <Dialog.Title asChild={true}> 238 <h3>Subscribed!</h3> 239 </Dialog.Title> 240 <Dialog.Description className="w-full flex flex-col"> 241 You'll get updates about this publication via a Feed just for you. 242 <ButtonPrimary 243 className="place-self-center mt-4" 244 onClick={async () => { 245 if (loading) return; 246 247 setLoading(true); 248 let feedurl = 249 "https://bsky.app/profile/leaflet.pub/feed/subscribedPublications"; 250 await addFeed(); 251 toaster({ content: "Feed added!", type: "success" }); 252 setLoading(false); 253 window.open(feedurl, "_blank"); 254 }} 255 > 256 {loading ? <DotLoader /> : "Add Bluesky Feed"} 257 </ButtonPrimary> 258 <button 259 className="text-accent-contrast mt-1" 260 onClick={() => { 261 const newUrl = new URL(window.location.href); 262 newUrl.searchParams.delete("showSubscribeSuccess"); 263 window.history.replaceState({}, "", newUrl.toString()); 264 setOpen(false); 265 }} 266 > 267 No thanks 268 </button> 269 </Dialog.Description> 270 <Dialog.Close /> 271 </Dialog.Content> 272 </Dialog.Portal> 273 </Dialog.Root> 274 ); 275}; 276 277export const SubscribeOnPost = () => { 278 return <div></div>; 279};