+1
package-lock.json
+1
package-lock.json
+1
package.json
+1
package.json
+137
-106
src/components/Composer.tsx
+137
-106
src/components/Composer.tsx
···
1
1
import { RichText } from "@atproto/api";
2
2
import { useAtom } from "jotai";
3
+
import { Dialog } from "radix-ui";
3
4
import { useEffect, useRef, useState } from "react";
4
5
5
6
import { useAuth } from "~/providers/UnifiedAuthProvider";
···
9
10
import { ProfileThing } from "./Login";
10
11
import { UniversalPostRendererATURILoader } from "./UniversalPostRenderer";
11
12
12
-
const MAX_POST_LENGTH = 300
13
+
const MAX_POST_LENGTH = 300;
13
14
14
15
export function Composer() {
15
16
const [composerState, setComposerState] = useAtom(composerAtom);
···
31
32
composerState.kind === "reply"
32
33
? composerState.parent
33
34
: composerState.kind === "quote"
34
-
? composerState.subject
35
-
: undefined;
35
+
? composerState.subject
36
+
: undefined;
36
37
37
-
const { data: parentPost, isLoading: isParentLoading } = useQueryPost(parentUri);
38
+
const { data: parentPost, isLoading: isParentLoading } =
39
+
useQueryPost(parentUri);
38
40
39
41
async function handlePost() {
40
42
if (!agent || !postText.trim() || postText.length > MAX_POST_LENGTH) return;
···
95
97
setPosting(false);
96
98
}
97
99
}
98
-
99
-
if (composerState.kind === "closed") {
100
-
return null;
101
-
}
100
+
// if (composerState.kind === "closed") {
101
+
// return null;
102
+
// }
102
103
103
104
const getPlaceholder = () => {
104
105
switch (composerState.kind) {
···
111
112
return "What's happening?!";
112
113
}
113
114
};
114
-
115
+
115
116
const charsLeft = MAX_POST_LENGTH - postText.length;
116
117
const isPostButtonDisabled =
117
-
posting ||
118
-
!postText.trim() ||
119
-
isParentLoading ||
120
-
charsLeft < 0;
118
+
posting || !postText.trim() || isParentLoading || charsLeft < 0;
121
119
122
120
return (
123
-
<div className="fixed inset-0 z-50 flex items-start justify-center pt-10 sm:pt-20 bg-black/40 dark:bg-black/50">
124
-
<div className="bg-gray-50 dark:bg-gray-950 border border-gray-200 dark:border-gray-700 rounded-2xl shadow-xl w-full max-w-xl relative mx-4">
125
-
<div className="flex flex-row justify-between p-2">
126
-
<button
127
-
className="h-8 w-8 flex items-center justify-center rounded-full text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800"
128
-
onClick={() => !posting && setComposerState({ kind: "closed" })}
129
-
disabled={posting}
130
-
aria-label="Close"
131
-
>
132
-
<svg
133
-
xmlns="http://www.w3.org/2000/svg"
134
-
width="20"
135
-
height="20"
136
-
viewBox="0 0 24 24"
137
-
fill="none"
138
-
stroke="currentColor"
139
-
strokeWidth="2.5"
140
-
strokeLinecap="round"
141
-
strokeLinejoin="round"
142
-
>
143
-
<line x1="18" y1="6" x2="6" y2="18"></line>
144
-
<line x1="6" y1="6" x2="18" y2="18"></line>
145
-
</svg>
146
-
</button>
147
-
<div className="flex-1" />
148
-
<div className="flex items-center gap-4">
149
-
<span className={`text-sm ${charsLeft < 0 ? 'text-red-500' : 'text-gray-500'}`}>
150
-
{charsLeft}
151
-
</span>
152
-
153
-
<button
154
-
className="bg-gray-600 hover:bg-gray-700 text-white font-bold py-1 px-4 rounded-full disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
155
-
onClick={handlePost}
156
-
disabled={isPostButtonDisabled}
157
-
>
158
-
{posting ? "Posting..." : "Post"}
159
-
</button>
160
-
</div>
161
-
</div>
121
+
<Dialog.Root
122
+
open={composerState.kind !== "closed"}
123
+
onOpenChange={(open) => {
124
+
if (!open) setComposerState({ kind: "closed" });
125
+
}}
126
+
>
127
+
<Dialog.Portal>
128
+
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/40 dark:bg-black/50 data-[state=open]:animate-fadeIn" />
162
129
163
-
{postSuccess ? (
164
-
<div className="flex flex-col items-center justify-center py-16">
165
-
<span className="text-gray-500 text-6xl mb-4">✓</span>
166
-
<span className="text-xl font-bold text-black dark:text-white">Posted!</span>
167
-
</div>
168
-
) : (
169
-
<div className="px-4">
170
-
{(composerState.kind === "reply") && (
171
-
<div className="mb-1 -mx-4">
172
-
{isParentLoading ? (
173
-
<div className="text-sm text-gray-500 animate-pulse">
174
-
Loading parent post...
175
-
</div>
176
-
) : parentUri ? (
177
-
<UniversalPostRendererATURILoader atUri={parentUri} bottomReplyLine bottomBorder={false} />
178
-
) : (
179
-
<div className="text-sm text-red-500 rounded-lg border border-red-500/50 p-3">
180
-
Could not load parent post.
181
-
</div>
182
-
)}
183
-
</div>
184
-
)}
185
-
186
-
<div className="flex w-full gap-1 flex-col">
187
-
<ProfileThing agent={agent} large/>
188
-
<div className="flex pl-[50px]">
189
-
<AutoGrowTextarea
190
-
className="w-full text-lg bg-transparent focus:outline-none resize-none placeholder:text-gray-500 text-black dark:text-white pb-2"
191
-
rows={5}
192
-
placeholder={getPlaceholder()}
193
-
value={postText}
194
-
onChange={(e) => setPostText(e.target.value)}
130
+
<Dialog.Content className="fixed overflow-y-scroll inset-0 z-50 flex items-start justify-center py-10 sm:py-20">
131
+
<div className="bg-gray-50 dark:bg-gray-950 border border-gray-200 dark:border-gray-700 rounded-2xl shadow-xl w-full max-w-xl relative mx-4">
132
+
<div className="flex flex-row justify-between p-2">
133
+
<Dialog.Close asChild>
134
+
<button
135
+
className="h-8 w-8 flex items-center justify-center rounded-full text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800"
195
136
disabled={posting}
196
-
autoFocus
197
-
/>
137
+
aria-label="Close"
138
+
>
139
+
<svg
140
+
xmlns="http://www.w3.org/2000/svg"
141
+
width="20"
142
+
height="20"
143
+
viewBox="0 0 24 24"
144
+
fill="none"
145
+
stroke="currentColor"
146
+
strokeWidth="2.5"
147
+
strokeLinecap="round"
148
+
strokeLinejoin="round"
149
+
>
150
+
<line x1="18" y1="6" x2="6" y2="18"></line>
151
+
<line x1="6" y1="6" x2="18" y2="18"></line>
152
+
</svg>
153
+
</button>
154
+
</Dialog.Close>
155
+
156
+
<div className="flex-1" />
157
+
<div className="flex items-center gap-4">
158
+
<span
159
+
className={`text-sm ${charsLeft < 0 ? "text-red-500" : "text-gray-500"}`}
160
+
>
161
+
{charsLeft}
162
+
</span>
163
+
<button
164
+
className="bg-gray-600 hover:bg-gray-700 text-white font-bold py-1 px-4 rounded-full disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
165
+
onClick={handlePost}
166
+
disabled={isPostButtonDisabled}
167
+
>
168
+
{posting ? "Posting..." : "Post"}
169
+
</button>
198
170
</div>
199
171
</div>
200
-
{(composerState.kind === "quote") && (
201
-
<div className="mb-4 ml-[50px] rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
202
-
{isParentLoading ? (
203
-
<div className="text-sm text-gray-500 animate-pulse">
204
-
Loading parent post...
172
+
173
+
{postSuccess ? (
174
+
<div className="flex flex-col items-center justify-center py-16">
175
+
<span className="text-gray-500 text-6xl mb-4">✓</span>
176
+
<span className="text-xl font-bold text-black dark:text-white">
177
+
Posted!
178
+
</span>
179
+
</div>
180
+
) : (
181
+
<div className="px-4">
182
+
{composerState.kind === "reply" && (
183
+
<div className="mb-1 -mx-4">
184
+
{isParentLoading ? (
185
+
<div className="text-sm text-gray-500 animate-pulse">
186
+
Loading parent post...
187
+
</div>
188
+
) : parentUri ? (
189
+
<UniversalPostRendererATURILoader
190
+
atUri={parentUri}
191
+
bottomReplyLine
192
+
bottomBorder={false}
193
+
/>
194
+
) : (
195
+
<div className="text-sm text-red-500 rounded-lg border border-red-500/50 p-3">
196
+
Could not load parent post.
197
+
</div>
198
+
)}
199
+
</div>
200
+
)}
201
+
202
+
<div className="flex w-full gap-1 flex-col">
203
+
<ProfileThing agent={agent} large />
204
+
<div className="flex pl-[50px]">
205
+
<AutoGrowTextarea
206
+
className="w-full text-lg bg-transparent focus:outline-none resize-none placeholder:text-gray-500 text-black dark:text-white pb-2"
207
+
rows={5}
208
+
placeholder={getPlaceholder()}
209
+
value={postText}
210
+
onChange={(e) => setPostText(e.target.value)}
211
+
disabled={posting}
212
+
autoFocus
213
+
/>
205
214
</div>
206
-
) : parentUri ? (
207
-
<UniversalPostRendererATURILoader atUri={parentUri} isQuote />
208
-
) : (
209
-
<div className="text-sm text-red-500 rounded-lg border border-red-500/50 p-3">
210
-
Could not load parent post.
215
+
</div>
216
+
217
+
{composerState.kind === "quote" && (
218
+
<div className="mb-4 ml-[50px] rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
219
+
{isParentLoading ? (
220
+
<div className="text-sm text-gray-500 animate-pulse">
221
+
Loading parent post...
222
+
</div>
223
+
) : parentUri ? (
224
+
<UniversalPostRendererATURILoader
225
+
atUri={parentUri}
226
+
isQuote
227
+
/>
228
+
) : (
229
+
<div className="text-sm text-red-500 rounded-lg border border-red-500/50 p-3">
230
+
Could not load parent post.
231
+
</div>
232
+
)}
233
+
</div>
234
+
)}
235
+
236
+
{postError && (
237
+
<div className="text-red-500 text-sm my-2 text-center">
238
+
{postError}
211
239
</div>
212
240
)}
213
241
</div>
214
242
)}
215
-
216
-
{postError && (
217
-
<div className="text-red-500 text-sm my-2 text-center">{postError}</div>
218
-
)}
219
-
220
243
</div>
221
-
)}
222
-
</div>
223
-
</div>
244
+
</Dialog.Content>
245
+
</Dialog.Portal>
246
+
</Dialog.Root>
224
247
);
225
248
}
226
249
227
-
function AutoGrowTextarea({ value, className, onChange, ...props }: React.DetailedHTMLProps<React.TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>) {
250
+
function AutoGrowTextarea({
251
+
value,
252
+
className,
253
+
onChange,
254
+
...props
255
+
}: React.DetailedHTMLProps<
256
+
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
257
+
HTMLTextAreaElement
258
+
>) {
228
259
const ref = useRef<HTMLTextAreaElement>(null);
229
260
230
261
useEffect(() => {
···
243
274
{...props}
244
275
/>
245
276
);
246
-
}
277
+
}
+3
-4
src/components/Login.tsx
+3
-4
src/components/Login.tsx
···
311
311
312
312
// --- Profile Component (now supports a `large` prop for styling) ---
313
313
export const ProfileThing = ({
314
-
agent: _unused,
314
+
agent,
315
315
large = false,
316
316
}: {
317
-
agent?: Agent | null;
317
+
agent: Agent | null;
318
318
large?: boolean;
319
319
}) => {
320
-
const { agent } = useAuth();
321
-
const did = ((agent as AtpAgent).session?.did ??
320
+
const did = ((agent as AtpAgent)?.session?.did ??
322
321
(agent as AtpAgent)?.assertDid ??
323
322
agent?.did) as string | undefined;
324
323
const { data: identity } = useQueryIdentity(did);
+5
src/styles/app.css
+5
src/styles/app.css
···
222
222
/* placeholder trick */
223
223
.m3input-field.m3input-label.m3input-border input::placeholder {
224
224
color: transparent;
225
+
}
226
+
227
+
/* radix i love you but like cmon man */
228
+
body[data-scroll-locked]{
229
+
margin-left: var(--removed-body-scroll-bar-size) !important;
225
230
}