a tool for shared writing and social publishing
at feature/reader 168 lines 6.3 kB view raw
1import { useEntitySetContext } from "components/EntitySetProvider"; 2import { useEffect, useState } from "react"; 3import { useEntity } from "src/replicache"; 4import { useUIState } from "src/useUIState"; 5import { BlockProps } from "../Block"; 6import { elementId } from "src/utils/elementId"; 7import { focusBlock } from "src/utils/focusBlock"; 8import { AppBskyFeedDefs, AppBskyFeedPost, RichText } from "@atproto/api"; 9import { BlueskyEmbed, PostNotAvailable } from "./BlueskyEmbed"; 10import { BlueskyPostEmpty } from "./BlueskyEmpty"; 11import { BlueskyRichText } from "./BlueskyRichText"; 12import { Separator } from "components/Layout"; 13import { useInitialPageLoad } from "components/InitialPageLoadProvider"; 14import { BlueskyTiny } from "components/Icons/BlueskyTiny"; 15import { CommentTiny } from "components/Icons/CommentTiny"; 16 17export const BlueskyPostBlock = (props: BlockProps & { preview?: boolean }) => { 18 let { permissions } = useEntitySetContext(); 19 let isSelected = useUIState((s) => 20 s.selectedBlocks.find((b) => b.value === props.entityID), 21 ); 22 let post = useEntity(props.entityID, "block/bluesky-post")?.data.value; 23 24 useEffect(() => { 25 if (props.preview) return; 26 let input = document.getElementById(elementId.block(props.entityID).input); 27 if (isSelected) { 28 input?.focus(); 29 } else input?.blur(); 30 }, [isSelected, props.entityID, props.preview]); 31 32 let initialPageLoad = useInitialPageLoad(); 33 34 switch (true) { 35 case !post: 36 if (!permissions.write) return null; 37 return ( 38 <label 39 id={props.preview ? undefined : elementId.block(props.entityID).input} 40 className={` 41 w-full h-[104px] p-2 42 text-tertiary hover:text-accent-contrast hover:cursor-pointer 43 flex flex-auto gap-2 items-center justify-center hover:border-2 border-dashed rounded-lg 44 ${isSelected ? "border-2 border-tertiary" : "border border-border"} 45 ${props.pageType === "canvas" && "bg-bg-page"}`} 46 onMouseDown={() => { 47 focusBlock( 48 { type: props.type, value: props.entityID, parent: props.parent }, 49 { type: "start" }, 50 ); 51 }} 52 > 53 <BlueskyPostEmpty {...props} /> 54 </label> 55 ); 56 57 case AppBskyFeedDefs.isBlockedPost(post) || 58 AppBskyFeedDefs.isBlockedAuthor(post) || 59 AppBskyFeedDefs.isNotFoundPost(post): 60 return ( 61 <div 62 className={`w-full ${isSelected ? "block-border-selected" : "block-border"}`} 63 > 64 <PostNotAvailable /> 65 </div> 66 ); 67 68 case AppBskyFeedDefs.isThreadViewPost(post): 69 let record = post.post 70 .record as AppBskyFeedDefs.FeedViewPost["post"]["record"]; 71 let facets = record.facets; 72 73 // silliness to get the text and timestamp from the record with proper types 74 let text: string | null = null; 75 let timestamp: string | undefined = undefined; 76 if (AppBskyFeedPost.isRecord(record)) { 77 text = (record as AppBskyFeedPost.Record).text; 78 timestamp = (record as AppBskyFeedPost.Record).createdAt; 79 } 80 81 //getting the url to the post 82 let postId = post.post.uri.split("/")[4]; 83 let url = `https://bsky.app/profile/${post.post.author.handle}/post/${postId}`; 84 85 let datetimeFormatted = initialPageLoad 86 ? new Date(timestamp ? timestamp : "").toLocaleString("en-US", { 87 month: "short", 88 day: "numeric", 89 year: "numeric", 90 hour: "numeric", 91 minute: "numeric", 92 hour12: true, 93 }) 94 : ""; 95 96 return ( 97 <div 98 className={` 99 flex flex-col gap-2 relative w-full overflow-hidden group/blueskyPostBlock sm:p-3 p-2 text-sm text-secondary bg-bg-page 100 ${isSelected ? "block-border-selected " : "block-border"} 101 `} 102 > 103 {post.post.author && record && ( 104 <> 105 <div className="bskyAuthor w-full flex items-center gap-2"> 106 {post.post.author?.avatar ? ( 107 <img 108 src={post.post.author?.avatar} 109 alt={`${post.post.author?.displayName}'s avatar`} 110 className="shrink-0 w-8 h-8 rounded-full border border-border-light" 111 /> 112 ) : ( 113 <div className="shrink-0 w-8 h-8 rounded-full border border-border-light bg-border"></div> 114 )} 115 <div className="grow flex flex-col gap-0.5 leading-tight"> 116 <div className=" font-bold text-secondary"> 117 {post.post.author?.displayName} 118 </div> 119 <a 120 className="text-xs text-tertiary hover:underline" 121 target="_blank" 122 href={`https://bsky.app/profile/${post.post.author?.handle}`} 123 > 124 @{post.post.author?.handle} 125 </a> 126 </div> 127 </div> 128 129 <div className="flex flex-col gap-2 "> 130 <div> 131 <pre className="whitespace-pre-wrap"> 132 {BlueskyRichText({ 133 record: record as AppBskyFeedPost.Record | null, 134 })} 135 </pre> 136 </div> 137 {post.post.embed && ( 138 <BlueskyEmbed embed={post.post.embed} postUrl={url} /> 139 )} 140 </div> 141 </> 142 )} 143 <div className="w-full flex gap-2 items-center justify-between"> 144 <div className="text-xs text-tertiary">{datetimeFormatted}</div> 145 <div className="flex gap-2 items-center"> 146 {post.post.replyCount && post.post.replyCount > 0 && ( 147 <> 148 <a 149 className="flex items-center gap-1 hover:no-underline" 150 target="_blank" 151 href={url} 152 > 153 {post.post.replyCount} 154 <CommentTiny /> 155 </a> 156 <Separator classname="h-4" /> 157 </> 158 )} 159 160 <a className="" target="_blank" href={url}> 161 <BlueskyTiny /> 162 </a> 163 </div> 164 </div> 165 </div> 166 ); 167 } 168};