a tool for shared writing and social publishing
at fix/bottom-scroll-margin-on-docs 284 lines 8.1 kB view raw
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};