a tool for shared writing and social publishing
1import Link from "next/link";
2import { useLeafletPublicationData } from "components/PageSWRDataProvider";
3import { useRef } from "react";
4import { useReplicache } from "src/replicache";
5import { AsyncValueAutosizeTextarea } from "components/utils/AutosizeTextarea";
6import { Separator } from "components/Layout";
7import { AtUri } from "@atproto/syntax";
8import { PubLeafletDocument } from "lexicons/api";
9import {
10 getBasePublicationURL,
11 getPublicationURL,
12} from "app/lish/createPub/getPublicationURL";
13import { useSubscribe } from "src/replicache/useSubscribe";
14import { useEntitySetContext } from "components/EntitySetProvider";
15import { timeAgo } from "src/utils/timeAgo";
16import { useIdentityData } from "components/IdentityProvider";
17export const PublicationMetadata = () => {
18 let { rep } = useReplicache();
19 let { data: pub } = useLeafletPublicationData();
20 let { identity } = useIdentityData();
21 let title = useSubscribe(rep, (tx) => tx.get<string>("publication_title"));
22 let description = useSubscribe(rep, (tx) =>
23 tx.get<string>("publication_description"),
24 );
25 let record = pub?.documents?.data as PubLeafletDocument.Record | null;
26 let publishedAt = record?.publishedAt;
27
28 if (!pub) return null;
29
30 if (typeof title !== "string") {
31 title = pub?.title || "";
32 }
33 if (typeof description !== "string") {
34 description = pub?.description || "";
35 }
36 return (
37 <div className={`flex flex-col px-3 sm:px-4 pb-5 sm:pt-3 pt-2`}>
38 <div className="flex gap-2">
39 {pub.publications && (
40 <Link
41 href={
42 identity?.atp_did === pub.publications?.identity_did
43 ? `${getBasePublicationURL(pub.publications)}/dashboard`
44 : getPublicationURL(pub.publications)
45 }
46 className="leafletMetadata text-accent-contrast font-bold hover:no-underline"
47 >
48 {pub.publications?.name}
49 </Link>
50 )}
51 <div className="font-bold text-tertiary px-1 text-sm flex place-items-center bg-border-light rounded-md ">
52 Editor
53 </div>
54 </div>
55 <TextField
56 className="text-xl font-bold outline-hidden bg-transparent"
57 value={title}
58 onChange={async (newTitle) => {
59 await rep?.mutate.updatePublicationDraft({
60 title: newTitle,
61 description,
62 });
63 }}
64 placeholder="Untitled"
65 />
66 <TextField
67 placeholder="add an optional description..."
68 className="italic text-secondary outline-hidden bg-transparent"
69 value={description}
70 onChange={async (newDescription) => {
71 await rep?.mutate.updatePublicationDraft({
72 title,
73 description: newDescription,
74 });
75 }}
76 />
77 {pub.doc ? (
78 <div className="flex flex-row items-center gap-2 pt-3">
79 <p className="text-sm text-tertiary">
80 Published {publishedAt && timeAgo(publishedAt)}
81 </p>
82 <Separator classname="h-4" />
83 <Link
84 target="_blank"
85 className="text-sm"
86 href={
87 pub.publications
88 ? `${getPublicationURL(pub.publications)}/${new AtUri(pub.doc).rkey}`
89 : `/p/${new AtUri(pub.doc).host}/${new AtUri(pub.doc).rkey}`
90 }
91 >
92 View Post
93 </Link>
94 </div>
95 ) : (
96 <p className="text-sm text-tertiary pt-2">Draft</p>
97 )}
98 </div>
99 );
100};
101
102export const TextField = ({
103 value,
104 onChange,
105 className,
106 placeholder,
107}: {
108 value: string;
109 onChange: (v: string) => Promise<void>;
110 className: string;
111 placeholder: string;
112}) => {
113 let { undoManager } = useReplicache();
114 let actionTimeout = useRef<number | null>(null);
115 let { permissions } = useEntitySetContext();
116 let previousSelection = useRef<null | { start: number; end: number }>(null);
117 let ref = useRef<HTMLTextAreaElement | null>(null);
118 return (
119 <AsyncValueAutosizeTextarea
120 ref={ref}
121 disabled={!permissions.write}
122 onSelect={(e) => {
123 let start = e.currentTarget.selectionStart,
124 end = e.currentTarget.selectionEnd;
125 previousSelection.current = { start, end };
126 }}
127 className={className}
128 value={value}
129 onBlur={async () => {
130 if (actionTimeout.current) {
131 undoManager.endGroup();
132 window.clearTimeout(actionTimeout.current);
133 actionTimeout.current = null;
134 }
135 }}
136 onChange={async (e) => {
137 let newValue = e.currentTarget.value;
138 let oldValue = value;
139 let start = e.currentTarget.selectionStart,
140 end = e.currentTarget.selectionEnd;
141 await onChange(e.currentTarget.value);
142
143 if (actionTimeout.current) {
144 window.clearTimeout(actionTimeout.current);
145 } else {
146 undoManager.startGroup();
147 }
148
149 actionTimeout.current = window.setTimeout(() => {
150 undoManager.endGroup();
151 actionTimeout.current = null;
152 }, 200);
153 let previousStart = previousSelection.current?.start || null,
154 previousEnd = previousSelection.current?.end || null;
155 undoManager.add({
156 redo: async () => {
157 await onChange(newValue);
158 ref.current?.setSelectionRange(start, end);
159 ref.current?.focus();
160 },
161 undo: async () => {
162 await onChange(oldValue);
163 ref.current?.setSelectionRange(previousStart, previousEnd);
164 ref.current?.focus();
165 },
166 });
167 }}
168 placeholder={placeholder}
169 />
170 );
171};
172
173export const PublicationMetadataPreview = () => {
174 let { data: pub } = useLeafletPublicationData();
175 let record = pub?.documents?.data as PubLeafletDocument.Record | null;
176 let publishedAt = record?.publishedAt;
177
178 if (!pub) return null;
179
180 return (
181 <div className={`flex flex-col px-3 sm:px-4 pb-5 sm:pt-3 pt-2`}>
182 <div className="text-accent-contrast font-bold hover:no-underline">
183 {pub.publications?.name}
184 </div>
185
186 <div
187 className={`text-xl font-bold outline-hidden bg-transparent ${!pub.title && "text-tertiary italic"}`}
188 >
189 {pub.title ? pub.title : "Untitled"}
190 </div>
191 <div className="italic text-secondary outline-hidden bg-transparent">
192 {pub.description}
193 </div>
194
195 {pub.doc ? (
196 <div className="flex flex-row items-center gap-2 pt-3">
197 <p className="text-sm text-tertiary">
198 Published {publishedAt && timeAgo(publishedAt)}
199 </p>
200 </div>
201 ) : (
202 <p className="text-sm text-tertiary pt-2">Draft</p>
203 )}
204 </div>
205 );
206};