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