a tool for shared writing and social publishing
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 { PostNotAvailable } from "./BlueskyEmbed";
10import { BlueskyPostEmpty } from "./BlueskyEmpty";
11
12import { Separator } from "components/Layout";
13import { BlueskyTiny } from "components/Icons/BlueskyTiny";
14import { CommentTiny } from "components/Icons/CommentTiny";
15import { useLocalizedDate } from "src/hooks/useLocalizedDate";
16import { BskyPostContent } from "app/lish/[did]/[publication]/[rkey]/BskyPostContent";
17import { PostView } from "@atproto/api/dist/client/types/app/bsky/feed/defs";
18
19export const BlueskyPostBlock = (props: BlockProps & { preview?: boolean }) => {
20 let { permissions } = useEntitySetContext();
21 let isSelected = useUIState((s) =>
22 s.selectedBlocks.find((b) => b.value === props.entityID),
23 );
24 let post = useEntity(props.entityID, "block/bluesky-post")?.data.value;
25
26 useEffect(() => {
27 if (props.preview) return;
28 let input = document.getElementById(elementId.block(props.entityID).input);
29 if (isSelected) {
30 input?.focus();
31 } else input?.blur();
32 }, [isSelected, props.entityID, props.preview]);
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 <BlockLayout isSelected={!!isSelected} className="w-full">
62 <PostNotAvailable />
63 </BlockLayout>
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 postView = post.post as PostView;
82 let url = `https://bsky.app/profile/${post.post.author.handle}/post/${postId}`;
83
84 return (
85 <BlockLayout
86 isSelected={!!isSelected}
87 hasBackground="page"
88 borderOnHover
89 className="blueskyPostBlock sm:px-3! sm:py-2! px-2! py-1!"
90 >
91 <BskyPostContent
92 post={postView}
93 parent={undefined}
94 showBlueskyLink={true}
95 showEmbed={true}
96 avatarSize="large"
97 className="text-sm text-secondary "
98 />
99 </BlockLayout>
100 );
101 }
102};
103
104function PostDate(props: { timestamp: string }) {
105 const formattedDate = useLocalizedDate(props.timestamp, {
106 month: "short",
107 day: "numeric",
108 year: "numeric",
109 hour: "numeric",
110 minute: "numeric",
111 hour12: true,
112 });
113 return <div className="text-xs text-tertiary">{formattedDate}</div>;
114}