Retro Bulletin Board Systems on atproto. Web app and TUI. lazy mirror of alyraffauf/atbbs atbbs.xyz
forums python tui atproto bbs
3
fork

Configure Feed

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

web: allow multiple attachments

+34 -27
+25 -18
web/src/components/ComposeForm.tsx
··· 12 12 onTitleChange?: (value: string) => void; 13 13 titlePlaceholder?: string; 14 14 titleMaxLength?: number; 15 - files: FileList | null; 16 - onFilesChange: (files: FileList | null) => void; 15 + files: File[]; 16 + onFilesChange: (files: File[]) => void; 17 17 quote?: { uri: string; handle: string } | null; 18 18 onClearQuote?: () => void; 19 19 submitLabel?: string; ··· 40 40 bodyMaxLength, 41 41 titleMaxLength, 42 42 }: ComposeFormProps) { 43 - const fileNames = files?.length 44 - ? Array.from(files) 45 - .map((f) => f.name) 46 - .join(", ") 47 - : ""; 43 + function addFiles(fileList: FileList | null) { 44 + if (!fileList) return; 45 + onFilesChange([...files, ...Array.from(fileList)]); 46 + } 47 + 48 + function removeFile(index: number) { 49 + onFilesChange(files.filter((_, i) => i !== index)); 50 + } 48 51 49 52 return ( 50 53 <form onSubmit={onSubmit} className={`space-y-3 ${className}`}> ··· 88 91 maxLength={bodyMaxLength} 89 92 /> 90 93 91 - {fileNames && ( 92 - <div className="flex items-center gap-2 text-xs text-neutral-500"> 93 - <span className="truncate">{fileNames}</span> 94 - <button 95 - type="button" 96 - onClick={() => onFilesChange(null)} 97 - className="text-neutral-500 hover:text-red-400 shrink-0" 98 - > 99 - 100 - </button> 94 + {files.length > 0 && ( 95 + <div className="flex flex-wrap gap-2 text-xs text-neutral-500"> 96 + {files.map((f, i) => ( 97 + <span key={i} className="flex items-center gap-1 bg-neutral-800 px-2 py-1 rounded"> 98 + {f.name} 99 + <button 100 + type="button" 101 + onClick={() => removeFile(i)} 102 + className="text-neutral-500 hover:text-red-400" 103 + > 104 + 105 + </button> 106 + </span> 107 + ))} 101 108 </div> 102 109 )} 103 110 ··· 111 118 name="attachments" 112 119 type="file" 113 120 multiple 114 - onChange={(e) => onFilesChange(e.target.files)} 121 + onChange={(e) => addFiles(e.target.files)} 115 122 className="hidden" 116 123 /> 117 124 </label>
+3 -3
web/src/lib/writes.ts
··· 134 134 135 135 export async function uploadAttachments( 136 136 rpc: Client, 137 - files: FileList | File[] | null, 137 + files: File[], 138 138 ): Promise<Attachment[]> { 139 - if (!files || files.length === 0) return []; 139 + if (files.length === 0) return []; 140 140 const out: Attachment[] = []; 141 - for (const file of Array.from(files)) { 141 + for (const file of files) { 142 142 if (file.size === 0) continue; 143 143 if (file.size > 1_000_000) throw new Error(`${file.name} exceeds 1MB`); 144 144 const blob = await uploadBlob(rpc, file);
+2 -2
web/src/pages/BBS.tsx
··· 19 19 const { user, agent } = useAuth(); 20 20 const [newsTitle, setNewsTitle] = useState(""); 21 21 const [newsBody, setNewsBody] = useState(""); 22 - const [newsFiles, setNewsFiles] = useState<FileList | null>(null); 22 + const [newsFiles, setNewsFiles] = useState<File[]>([]); 23 23 const [pendingNews, setPendingNews] = useState<News[]>([]); 24 24 const [deletedTids, setDeletedTids] = useState<Set<string>>(new Set()); 25 25 const [showAllNews, setShowAllNews] = useState(false); ··· 52 52 ]); 53 53 setNewsTitle(""); 54 54 setNewsBody(""); 55 - setNewsFiles(null); 55 + setNewsFiles([]); 56 56 } 57 57 58 58 async function removeNews(tid: string) {
+2 -2
web/src/pages/Board.tsx
··· 49 49 50 50 const [title, setTitle] = useState(""); 51 51 const [body, setBody] = useState(""); 52 - const [files, setFiles] = useState<FileList | null>(null); 52 + const [files, setFiles] = useState<File[]>([]); 53 53 54 54 useTitle(`${board.name} — ${bbs.site.name}`); 55 55 useBreadcrumb( ··· 92 92 ); 93 93 setTitle(""); 94 94 setBody(""); 95 - setFiles(null); 95 + setFiles([]); 96 96 setTimeout(() => revalidator.revalidate(), 1500); 97 97 const { did, rkey } = parseAtUri(resp.data.uri); 98 98 navigate(`/bbs/${handle}/thread/${did}/${rkey}`);
+2 -2
web/src/pages/Thread.tsx
··· 64 64 } = useThreadReplies(loaded); 65 65 66 66 const [body, setBody] = useState(""); 67 - const [files, setFiles] = useState<FileList | null>(null); 67 + const [files, setFiles] = useState<File[]>([]); 68 68 const [quote, setQuote] = useState<{ uri: string; handle: string } | null>( 69 69 null, 70 70 ); ··· 101 101 attachments: attachments as Reply["attachments"], 102 102 }); 103 103 setBody(""); 104 - setFiles(null); 104 + setFiles([]); 105 105 setQuote(null); 106 106 } catch { 107 107 alert("Failed to post reply.");