an independent Bluesky client using Constellation, PDS Queries, and other services reddwarf.app
frontend spa bluesky reddwarf microcosm client app
99
fork

Configure Feed

Select the types of activity you want to include in your feed.

scrollable post composer + radix dialog

rimar1337 fa673e49 b9c7d025

+147 -110
+1
package-lock.json
··· 8 8 "dependencies": { 9 9 "@atproto/api": "^0.16.6", 10 10 "@atproto/oauth-client-browser": "^0.3.33", 11 + "@radix-ui/react-dialog": "^1.1.15", 11 12 "@radix-ui/react-dropdown-menu": "^2.1.16", 12 13 "@radix-ui/react-slider": "^1.3.6", 13 14 "@tailwindcss/vite": "^4.0.6",
+1
package.json
··· 12 12 "dependencies": { 13 13 "@atproto/api": "^0.16.6", 14 14 "@atproto/oauth-client-browser": "^0.3.33", 15 + "@radix-ui/react-dialog": "^1.1.15", 15 16 "@radix-ui/react-dropdown-menu": "^2.1.16", 16 17 "@radix-ui/react-slider": "^1.3.6", 17 18 "@tailwindcss/vite": "^4.0.6",
+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
··· 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
··· 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 }