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