a tool for shared writing and social publishing
at update/delete-leaflets 167 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 } 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 <div 60 className={`w-full ${isSelected ? "block-border-selected" : "block-border"}`} 61 > 62 <PostNotAvailable /> 63 </div> 64 ); 65 66 case AppBskyFeedDefs.isThreadViewPost(post): 67 let record = post.post 68 .record as AppBskyFeedDefs.FeedViewPost["post"]["record"]; 69 let facets = record.facets; 70 71 // silliness to get the text and timestamp from the record with proper types 72 let text: string | null = null; 73 let timestamp: string | undefined = undefined; 74 if (AppBskyFeedPost.isRecord(record)) { 75 text = (record as AppBskyFeedPost.Record).text; 76 timestamp = (record as AppBskyFeedPost.Record).createdAt; 77 } 78 79 //getting the url to the post 80 let postId = post.post.uri.split("/")[4]; 81 let url = `https://bsky.app/profile/${post.post.author.handle}/post/${postId}`; 82 83 return ( 84 <div 85 className={` 86 flex flex-col gap-2 relative w-full overflow-hidden group/blueskyPostBlock sm:p-3 p-2 text-sm text-secondary bg-bg-page 87 ${isSelected ? "block-border-selected " : "block-border"} 88 `} 89 > 90 {post.post.author && record && ( 91 <> 92 <div className="bskyAuthor w-full flex items-center gap-2"> 93 {post.post.author?.avatar ? ( 94 <img 95 src={post.post.author?.avatar} 96 alt={`${post.post.author?.displayName}'s avatar`} 97 className="shrink-0 w-8 h-8 rounded-full border border-border-light" 98 /> 99 ) : ( 100 <div className="shrink-0 w-8 h-8 rounded-full border border-border-light bg-border"></div> 101 )} 102 <div className="grow flex flex-col gap-0.5 leading-tight"> 103 <div className=" font-bold text-secondary"> 104 {post.post.author?.displayName} 105 </div> 106 <a 107 className="text-xs text-tertiary hover:underline" 108 target="_blank" 109 href={`https://bsky.app/profile/${post.post.author?.handle}`} 110 > 111 @{post.post.author?.handle} 112 </a> 113 </div> 114 </div> 115 116 <div className="flex flex-col gap-2 "> 117 <div> 118 <pre className="whitespace-pre-wrap"> 119 {BlueskyRichText({ 120 record: record as AppBskyFeedPost.Record | null, 121 })} 122 </pre> 123 </div> 124 {post.post.embed && ( 125 <BlueskyEmbed embed={post.post.embed} postUrl={url} /> 126 )} 127 </div> 128 </> 129 )} 130 <div className="w-full flex gap-2 items-center justify-between"> 131 {timestamp && <PostDate timestamp={timestamp} />} 132 <div className="flex gap-2 items-center"> 133 {post.post.replyCount && post.post.replyCount > 0 && ( 134 <> 135 <a 136 className="flex items-center gap-1 hover:no-underline" 137 target="_blank" 138 href={url} 139 > 140 {post.post.replyCount} 141 <CommentTiny /> 142 </a> 143 <Separator classname="h-4" /> 144 </> 145 )} 146 147 <a className="" target="_blank" href={url}> 148 <BlueskyTiny /> 149 </a> 150 </div> 151 </div> 152 </div> 153 ); 154 } 155}; 156 157function PostDate(props: { timestamp: string }) { 158 const formattedDate = useLocalizedDate(props.timestamp, { 159 month: "short", 160 day: "numeric", 161 year: "numeric", 162 hour: "numeric", 163 minute: "numeric", 164 hour12: true, 165 }); 166 return <div className="text-xs text-tertiary">{formattedDate}</div>; 167}