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