a tool for shared writing and social publishing
1import { useReplicache } from "src/replicache";
2import React, { useEffect, useState } from "react";
3import { getShareLink } from "./getShareLink";
4import { useEntitySetContext } from "components/EntitySetProvider";
5import { useSmoker } from "components/Toast";
6import { Menu, MenuItem } from "components/Layout";
7import { ActionButton } from "components/ActionBar/ActionButton";
8import useSWR from "swr";
9import { useTemplateState } from "app/(home-pages)/home/Actions/CreateNewButton";
10import LoginForm from "app/login/LoginForm";
11import { CustomDomainMenu } from "./DomainOptions";
12import { useIdentityData } from "components/IdentityProvider";
13import {
14 useLeafletDomains,
15 useLeafletPublicationData,
16} from "components/PageSWRDataProvider";
17import { ShareSmall } from "components/Icons/ShareSmall";
18import { PubLeafletDocument } from "lexicons/api";
19import { getPublicationURL } from "app/lish/createPub/getPublicationURL";
20import { AtUri } from "@atproto/syntax";
21import { useIsMobile } from "src/hooks/isMobile";
22
23export type ShareMenuStates = "default" | "login" | "domain";
24
25export let usePublishLink = () => {
26 let { permission_token, rootEntity } = useReplicache();
27 let entity_set = useEntitySetContext();
28 let { data: publishLink } = useSWR(
29 "publishLink-" + permission_token.id,
30 async () => {
31 if (
32 !permission_token.permission_token_rights.find(
33 (s) => s.entity_set === entity_set.set && s.create_token,
34 )
35 )
36 return;
37 let shareLink = await getShareLink(
38 { id: permission_token.id, entity_set: entity_set.set },
39 rootEntity,
40 );
41 return shareLink?.id;
42 },
43 );
44 return publishLink;
45};
46
47export function ShareOptions() {
48 let [menuState, setMenuState] = useState<ShareMenuStates>("default");
49 let { data: pub } = useLeafletPublicationData();
50 let isMobile = useIsMobile();
51
52 return (
53 <Menu
54 asChild
55 side={isMobile ? "top" : "right"}
56 align={isMobile ? "center" : "start"}
57 className="max-w-xs"
58 onOpenChange={() => {
59 setMenuState("default");
60 }}
61 trigger={
62 <ActionButton
63 icon=<ShareSmall />
64 primary={!!!pub}
65 secondary={!!pub}
66 label={`Share ${pub ? "Draft" : ""}`}
67 />
68 }
69 >
70 {menuState === "login" ? (
71 <div className="px-3 py-1">
72 <LoginForm text="Save your Leaflets and access them on multiple devices!" />
73 </div>
74 ) : menuState === "domain" ? (
75 <CustomDomainMenu setShareMenuState={setMenuState} />
76 ) : (
77 <ShareMenu
78 setMenuState={setMenuState}
79 domainConnected={false}
80 isPub={!!pub}
81 />
82 )}
83 </Menu>
84 );
85}
86
87const ShareMenu = (props: {
88 setMenuState: (state: ShareMenuStates) => void;
89 domainConnected: boolean;
90 isPub?: boolean;
91}) => {
92 let { permission_token } = useReplicache();
93 let { data: pub } = useLeafletPublicationData();
94
95 let record = pub?.documents?.data as PubLeafletDocument.Record | null;
96
97 let postLink =
98 pub?.publications && pub.documents
99 ? `${getPublicationURL(pub.publications)}/${new AtUri(pub?.documents.uri).rkey}`
100 : null;
101 let publishLink = usePublishLink();
102 let [collabLink, setCollabLink] = useState<null | string>(null);
103 useEffect(() => {
104 // strip leading '/' character from pathname
105 setCollabLink(window.location.pathname.slice(1));
106 }, []);
107 let { data: domains } = useLeafletDomains();
108
109 let isTemplate = useTemplateState(
110 (s) => !!s.templates.find((t) => t.id === permission_token.id),
111 );
112
113 return (
114 <>
115 {isTemplate && (
116 <>
117 <ShareButton
118 text="Share Template"
119 subtext="Let others make new Leaflets as copies of this template"
120 smokerText="Template link copied!"
121 id="get-template-link"
122 link={`template/${publishLink}` || ""}
123 />
124 <hr className="border-border my-1" />
125 </>
126 )}
127
128 <ShareButton
129 text={`Share ${postLink ? "Draft" : ""} Edit Link`}
130 subtext=""
131 smokerText="Edit link copied!"
132 id="get-edit-link"
133 link={collabLink}
134 />
135 <ShareButton
136 text={`Share ${postLink ? "Draft" : ""} View Link`}
137 subtext=<>
138 {domains?.[0] ? (
139 <>
140 This Leaflet is published on{" "}
141 <span className="italic underline">
142 {domains[0].domain}
143 {domains[0].route}
144 </span>
145 </>
146 ) : (
147 ""
148 )}
149 </>
150 smokerText="View link copied!"
151 id="get-view-link"
152 fullLink={
153 domains?.[0]
154 ? `https://${domains[0].domain}${domains[0].route}`
155 : undefined
156 }
157 link={publishLink || ""}
158 />
159 {postLink && (
160 <>
161 <hr className="border-border-light" />
162
163 <ShareButton
164 text="Share Published Link"
165 subtext=""
166 smokerText="Post link copied!"
167 id="get-post-link"
168 fullLink={postLink.includes("http") ? postLink : undefined}
169 link={postLink}
170 />
171 </>
172 )}
173 {!props.isPub && (
174 <>
175 <hr className="border-border mt-1" />
176 <DomainMenuItem setMenuState={props.setMenuState} />
177 </>
178 )}
179 </>
180 );
181};
182
183export const ShareButton = (props: {
184 text: React.ReactNode;
185 subtext: React.ReactNode;
186 helptext?: string;
187 smokerText: string;
188 id: string;
189 link: null | string;
190 fullLink?: string;
191 className?: string;
192}) => {
193 let smoker = useSmoker();
194
195 return (
196 <MenuItem
197 id={props.id}
198 onSelect={(e) => {
199 e.preventDefault();
200 let rect = document.getElementById(props.id)?.getBoundingClientRect();
201 if (props.link || props.fullLink) {
202 navigator.clipboard.writeText(
203 props.fullLink
204 ? props.fullLink
205 : `${location.protocol}//${location.host}/${props.link}`,
206 );
207 smoker({
208 position: {
209 x: rect ? rect.left + (rect.right - rect.left) / 2 : 0,
210 y: rect ? rect.top + 26 : 0,
211 },
212 text: props.smokerText,
213 });
214 }
215 }}
216 >
217 <div className={`group/${props.id} ${props.className}`}>
218 <div className={`group-hover/${props.id}:text-accent-contrast`}>
219 {props.text}
220 </div>
221 <div
222 className={`text-sm font-normal text-tertiary group-hover/${props.id}:text-accent-contrast`}
223 >
224 {props.subtext}
225 </div>
226 {/* optional help text */}
227 {props.helptext && (
228 <div
229 className={`text-sm italic font-normal text-tertiary group-hover/${props.id}:text-accent-contrast`}
230 >
231 {props.helptext}
232 </div>
233 )}
234 </div>
235 </MenuItem>
236 );
237};
238
239const DomainMenuItem = (props: {
240 setMenuState: (state: ShareMenuStates) => void;
241}) => {
242 let { identity } = useIdentityData();
243 let { data: domains } = useLeafletDomains();
244
245 if (identity === null)
246 return (
247 <div className="text-tertiary font-normal text-sm px-3 py-1">
248 <button
249 className="text-accent-contrast hover:font-bold"
250 onClick={() => {
251 props.setMenuState("login");
252 }}
253 >
254 Log In
255 </button>{" "}
256 to publish on a custom domain!
257 </div>
258 );
259 else
260 return (
261 <>
262 {domains?.[0] ? (
263 <button
264 className="px-3 py-1 text-accent-contrast text-sm hover:font-bold w-fit text-left"
265 onMouseDown={() => {
266 props.setMenuState("domain");
267 }}
268 >
269 Edit custom domain
270 </button>
271 ) : (
272 <MenuItem
273 className="font-normal text-tertiary text-sm"
274 onSelect={(e) => {
275 e.preventDefault();
276 props.setMenuState("domain");
277 }}
278 >
279 Publish on a custom domain
280 </MenuItem>
281 )}
282 </>
283 );
284};