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