a tool for shared writing and social publishing
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};