a tool for shared writing and social publishing
298
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 8f68be5863d8e7c1944cc5ce848a9a9fc6a032aa 402 lines 13 kB view raw
1import { ButtonPrimary } from "components/Buttons"; 2import { Popover } from "components/Popover"; 3import { MenuItem } from "components/Menu"; 4import { Separator } from "components/Layout"; 5import { useUIState } from "src/useUIState"; 6import { useState } from "react"; 7import { useSmoker, useToaster } from "components/Toast"; 8import { BlockProps, BlockLayout } from "./Block"; 9import { useEntity, useReplicache } from "src/replicache"; 10import { useEntitySetContext } from "components/EntitySetProvider"; 11import { subscribeToMailboxWithEmail } from "actions/subscriptions/subscribeToMailboxWithEmail"; 12import { confirmEmailSubscription } from "actions/subscriptions/confirmEmailSubscription"; 13import { focusPage } from "src/utils/focusPage"; 14import { v7 } from "uuid"; 15import { sendPostToSubscribers } from "actions/subscriptions/sendPostToSubscribers"; 16import { getBlocksWithType } from "src/hooks/queries/useBlocks"; 17import { getBlocksAsHTML } from "src/utils/getBlocksAsHTML"; 18import { htmlToMarkdown } from "src/htmlMarkdownParsers"; 19import { 20 addSubscription, 21 removeSubscription, 22 unsubscribe, 23 useSubscriptionStatus, 24} from "src/hooks/useSubscriptionStatus"; 25import { usePageTitle } from "components/utils/UpdateLeafletTitle"; 26import { ArrowDownTiny } from "components/Icons/ArrowDownTiny"; 27import { InfoSmall } from "components/Icons/InfoSmall"; 28 29export const MailboxBlock = ( 30 props: BlockProps & { 31 areYouSure?: boolean; 32 setAreYouSure?: (value: boolean) => void; 33 }, 34) => { 35 let isSubscribed = useSubscriptionStatus(props.entityID); 36 let isSelected = useUIState((s) => 37 s.selectedBlocks.find((b) => b.value === props.entityID), 38 ); 39 40 let permission = useEntitySetContext().permissions.write; 41 let { rep } = useReplicache(); 42 let smoke = useSmoker(); 43 let draft = useEntity(props.entityID, "mailbox/draft"); 44 let entity_set = useEntitySetContext(); 45 46 let subscriber_count = useEntity(props.entityID, "mailbox/subscriber-count"); 47 if (!permission) 48 return ( 49 <MailboxReaderView 50 entityID={props.entityID} 51 parent={props.parent} 52 /> 53 ); 54 55 return ( 56 <div className={`mailboxContent relative w-full flex flex-col gap-1`}> 57 <BlockLayout 58 isSelected={!!isSelected} 59 hasBackground={"accent"} 60 areYouSure={props.areYouSure} 61 setAreYouSure={props.setAreYouSure} 62 className="flex gap-2 items-center justify-center" 63 > 64 <ButtonPrimary 65 onClick={async () => { 66 let entity; 67 if (draft) { 68 entity = draft.data.value; 69 } else { 70 entity = v7(); 71 await rep?.mutate.createDraft({ 72 mailboxEntity: props.entityID, 73 permission_set: entity_set.set, 74 newEntity: entity, 75 firstBlockEntity: v7(), 76 firstBlockFactID: v7(), 77 }); 78 } 79 useUIState.getState().openPage(props.parent, entity); 80 if (rep) focusPage(entity, rep, "focusFirstBlock"); 81 return; 82 }} 83 > 84 {draft ? "Edit Draft" : "Write a Post"} 85 </ButtonPrimary> 86 <MailboxInfo /> 87 </BlockLayout> 88 <div className="flex gap-3 items-center justify-between"> 89 { 90 <> 91 {!isSubscribed?.confirmed ? ( 92 <SubscribePopover 93 entityID={props.entityID} 94 unconfirmed={!!isSubscribed && !isSubscribed.confirmed} 95 parent={props.parent} 96 /> 97 ) : ( 98 <button 99 className="text-tertiary hover:text-accent-contrast" 100 onClick={(e) => { 101 let rect = e.currentTarget.getBoundingClientRect(); 102 unsubscribe(isSubscribed); 103 smoke({ 104 text: "unsubscribed!", 105 position: { x: rect.left, y: rect.top - 8 }, 106 }); 107 }} 108 > 109 Unsubscribe 110 </button> 111 )} 112 <div className="flex gap-2 place-items-center"> 113 <span className="text-tertiary"> 114 {!subscriber_count || 115 subscriber_count?.data.value === undefined || 116 subscriber_count?.data.value === 0 117 ? "no" 118 : subscriber_count?.data.value}{" "} 119 reader 120 {subscriber_count?.data.value === 1 ? "" : "s"} 121 </span> 122 <Separator classname="h-5" /> 123 124 <GoToArchive entityID={props.entityID} parent={props.parent} /> 125 </div> 126 </> 127 } 128 </div> 129 </div> 130 ); 131}; 132 133const MailboxReaderView = (props: { 134 entityID: string; 135 parent: string; 136 137}) => { 138 let isSubscribed = useSubscriptionStatus(props.entityID); 139 let isSelected = useUIState((s) => 140 s.selectedBlocks.find((b) => b.value === props.entityID), 141 ); 142 let archive = useEntity(props.entityID, "mailbox/archive"); 143 let smoke = useSmoker(); 144 let { rep } = useReplicache(); 145 return ( 146 <div className={`mailboxContent relative w-full flex flex-col gap-1 h-32`}> 147 <BlockLayout 148 isSelected={!!isSelected} 149 hasBackground={"accent"} 150 151 className="`h-full flex flex-col gap-2 items-center justify-center" 152 > 153 {!isSubscribed?.confirmed ? ( 154 <> 155 <SubscribeForm 156 entityID={props.entityID} 157 role={"reader"} 158 parent={props.parent} 159 /> 160 </> 161 ) : ( 162 <div className="flex flex-col gap-2 items-center place-self-center"> 163 <div className=" font-bold text-secondary "> 164 You&apos;re Subscribed! 165 </div> 166 <div className="flex flex-col gap-1 items-center place-self-center"> 167 {archive ? ( 168 <ButtonPrimary 169 onMouseDown={(e) => { 170 e.preventDefault(); 171 if (rep) { 172 useUIState 173 .getState() 174 .openPage(props.parent, archive.data.value); 175 focusPage(archive.data.value, rep); 176 } 177 }} 178 > 179 See All Posts 180 </ButtonPrimary> 181 ) : ( 182 <div className="text-tertiary">Nothing has been posted yet</div> 183 )} 184 <button 185 className="text-accent-contrast hover:underline text-sm" 186 onClick={(e) => { 187 let rect = e.currentTarget.getBoundingClientRect(); 188 unsubscribe(isSubscribed); 189 smoke({ 190 text: "unsubscribed!", 191 position: { x: rect.left, y: rect.top - 8 }, 192 }); 193 }} 194 > 195 unsubscribe 196 </button> 197 </div> 198 </div> 199 )} 200 </BlockLayout> 201 </div> 202 ); 203}; 204 205const MailboxInfo = (props: { subscriber?: boolean }) => { 206 return ( 207 <Popover 208 className="max-w-xs" 209 trigger={<InfoSmall className="shrink-0 text-accent-contrast" />} 210 > 211 <div className="text-sm text-secondary flex flex-col gap-2"> 212 {props.subscriber ? ( 213 <> 214 <p className="font-bold"> 215 Get a notification whenever the creator posts to this mailbox! 216 </p> 217 <p> 218 Your contact info will be kept private, and you can unsubscribe 219 anytime. 220 </p> 221 </> 222 ) : ( 223 <> 224 <p className="font-bold"> 225 When you post to this mailbox, subscribers will be notified! 226 </p> 227 <p>Reader contact info is kept private.</p> 228 <p>You can have one draft post at a time.</p> 229 </> 230 )} 231 </div> 232 </Popover> 233 ); 234}; 235 236const SubscribePopover = (props: { 237 entityID: string; 238 parent: string; 239 unconfirmed: boolean; 240}) => { 241 return ( 242 <Popover 243 className="max-w-sm" 244 trigger={ 245 <div className="font-bold text-accent-contrast"> 246 {props.unconfirmed ? "Confirm" : "Subscribe"} 247 </div> 248 } 249 > 250 <div className="text-secondary flex flex-col gap-2 py-1"> 251 <SubscribeForm 252 compact 253 entityID={props.entityID} 254 role="author" 255 parent={props.parent} 256 /> 257 </div> 258 </Popover> 259 ); 260}; 261 262const SubscribeForm = (props: { 263 entityID: string; 264 parent: string; 265 role: "author" | "reader"; 266 compact?: boolean; 267}) => { 268 let smoke = useSmoker(); 269 let [channel, setChannel] = useState<"email" | "sms">("email"); 270 let [email, setEmail] = useState(""); 271 let [sms, setSMS] = useState(""); 272 273 let subscription = useSubscriptionStatus(props.entityID); 274 let [code, setCode] = useState(""); 275 let { permission_token } = useReplicache(); 276 if (subscription && !subscription.confirmed) { 277 return ( 278 <div className="flex flex-col gap-3 justify-center text-center "> 279 <div className="font-bold text-secondary "> 280 Enter the code we sent to{" "} 281 <code 282 className="italic" 283 style={{ fontFamily: "var(--font-quattro)" }} 284 > 285 {subscription.email} 286 </code>{" "} 287 here! 288 </div> 289 <div className="flex flex-col gap-1"> 290 <form 291 onSubmit={async (e) => { 292 e.preventDefault(); 293 let result = await confirmEmailSubscription( 294 subscription.id, 295 code, 296 ); 297 298 let rect = document 299 .getElementById("confirm-code-button") 300 ?.getBoundingClientRect(); 301 302 if (!result) { 303 smoke({ 304 error: true, 305 text: "oops, incorrect code", 306 position: { 307 x: rect ? rect.left + 45 : 0, 308 y: rect ? rect.top + 15 : 0, 309 }, 310 }); 311 return; 312 } 313 addSubscription(result.subscription); 314 }} 315 className="mailboxConfirmCodeInput flex gap-2 items-center mx-auto" 316 > 317 <input 318 type="number" 319 value={code} 320 className="appearance-none focus:outline-hidden focus:border-border w-20 border border-border-light bg-bg-page rounded-md p-1" 321 onChange={(e) => setCode(e.currentTarget.value)} 322 /> 323 324 <ButtonPrimary type="submit" id="confirm-code-button"> 325 Confirm! 326 </ButtonPrimary> 327 </form> 328 329 <button 330 onMouseDown={() => { 331 removeSubscription(subscription); 332 setEmail(""); 333 }} 334 className="text-accent-contrast hover:underline text-sm" 335 > 336 use another contact 337 </button> 338 </div> 339 </div> 340 ); 341 } 342 return ( 343 <> 344 <div className="flex flex-col gap-1"> 345 <form 346 onSubmit={async (e) => { 347 e.preventDefault(); 348 let subscriptionID = await subscribeToMailboxWithEmail( 349 props.entityID, 350 email, 351 permission_token, 352 ); 353 if (subscriptionID) addSubscription(subscriptionID); 354 }} 355 className={`mailboxSubscribeForm flex sm:flex-row flex-col ${props.compact && "sm:flex-col sm:gap-2"} gap-2 sm:gap-3 items-center place-self-center mx-auto`} 356 > 357 <div className="mailboxChannelInput flex gap-2 border border-border-light bg-bg-page rounded-md py-1 px-2 grow max-w-72 "> 358 <input 359 value={email} 360 type="email" 361 onChange={(e) => setEmail(e.target.value)} 362 className="w-full appearance-none focus:outline-hidden bg-transparent" 363 placeholder="youremail@email.com" 364 /> 365 </div> 366 <ButtonPrimary type="submit">Subscribe!</ButtonPrimary> 367 </form> 368 {props.role === "reader" && ( 369 <GoToArchive entityID={props.entityID} parent={props.parent} small /> 370 )} 371 </div> 372 </> 373 ); 374}; 375 376const GoToArchive = (props: { 377 entityID: string; 378 parent: string; 379 small?: boolean; 380}) => { 381 let archive = useEntity(props.entityID, "mailbox/archive"); 382 let { rep } = useReplicache(); 383 384 return archive ? ( 385 <button 386 className={`text-tertiary hover:text-accent-contrast ${props.small && "text-sm"}`} 387 onMouseDown={(e) => { 388 e.preventDefault(); 389 if (rep) { 390 useUIState.getState().openPage(props.parent, archive.data.value); 391 focusPage(archive.data.value, rep); 392 } 393 }} 394 > 395 past posts 396 </button> 397 ) : ( 398 <div className={`text-tertiary text-center ${props.small && "text-sm"}`}> 399 no posts yet 400 </div> 401 ); 402};