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.

at master 170 lines 5.1 kB view raw
1import { useState } from "react"; 2import { useSuspenseQuery } from "@tanstack/react-query"; 3import { useAuth } from "../lib/auth"; 4import { bbsQuery, sysopModerationQuery } from "../lib/queries"; 5import { bbsUrl } from "../lib/routes"; 6import HandleInput from "../components/form/HandleInput"; 7import { Button } from "../components/form/Form"; 8import { useBreadcrumb } from "../hooks/useBreadcrumb"; 9import { usePageTitle } from "../hooks/usePageTitle"; 10import { useModerationMutations } from "../hooks/useModerationMutations"; 11 12interface ModerationListItemProps { 13 label: string; 14 href: string; 15 title: string; 16 actionLabel: string; 17 onAction?: () => void; 18} 19 20function ModerationListItem({ 21 label, 22 href, 23 title, 24 actionLabel, 25 onAction, 26}: ModerationListItemProps) { 27 return ( 28 <div 29 title={title} 30 className="flex items-center justify-between gap-3 px-3 py-2 -mx-3 rounded hover:bg-neutral-800" 31 > 32 <a 33 href={href} 34 target="_blank" 35 rel="noreferrer" 36 aria-label={`${label} (opens in new tab)`} 37 className="truncate text-neutral-300 hover:text-neutral-200" 38 > 39 {label} 40 </a> 41 {onAction && ( 42 <button 43 onClick={onAction} 44 className="text-xs text-neutral-400 hover:text-red-400 shrink-0" 45 > 46 {actionLabel} 47 </button> 48 )} 49 </div> 50 ); 51} 52 53export default function SysopModerate() { 54 const { user } = useAuth(); 55 const [identifier, setIdentifier] = useState(""); 56 const [hideUri, setHideUri] = useState(""); 57 usePageTitle("Moderate community — atbbs"); 58 59 // requireAuthLoader guarantees user is present at render time. 60 const { data: bbs } = useSuspenseQuery(bbsQuery(user!.handle)); 61 const { data: moderation } = useSuspenseQuery( 62 sysopModerationQuery(user!.pdsUrl, user!.did), 63 ); 64 const { banRkeys, bannedHandles, hideRkeys, hidden } = moderation; 65 66 useBreadcrumb( 67 [ 68 { label: bbs.site.name, to: bbsUrl(user!.handle) }, 69 { label: "Moderate" }, 70 ], 71 [bbs, user!.handle], 72 ); 73 74 const { ban, unban, hide, unhide } = useModerationMutations(); 75 76 function onBan() { 77 const id = identifier.trim(); 78 if (!id) return; 79 ban.mutate(id, { onSuccess: () => setIdentifier("") }); 80 } 81 82 function onUnban(rkey: string) { 83 if (!confirm("Unban this user?")) return; 84 unban.mutate(rkey); 85 } 86 87 function onHide() { 88 const uri = hideUri.trim(); 89 if (!uri.startsWith("at://")) { 90 alert("Enter a valid AT-URI."); 91 return; 92 } 93 hide.mutate(uri, { onSuccess: () => setHideUri("") }); 94 } 95 96 function onUnhide(rkey: string) { 97 if (!confirm("Unhide this post?")) return; 98 unhide.mutate(rkey); 99 } 100 101 return ( 102 <> 103 <h1 className="text-lg text-neutral-200 mb-1">Moderate community</h1> 104 <p className="text-neutral-400 mb-6"> 105 Manage banned users and hidden posts for {bbs.site.name}. 106 </p> 107 108 <div className="space-y-8"> 109 <div> 110 <label className="block text-neutral-400 mb-3">Banned Users</label> 111 <div className="space-y-1 mb-3"> 112 {Object.keys(banRkeys).map((did) => ( 113 <ModerationListItem 114 key={did} 115 title={did} 116 label={bannedHandles[did] ?? did} 117 href={`https://pdsls.dev/at/${did}`} 118 actionLabel="unban" 119 onAction={ 120 banRkeys[did] ? () => onUnban(banRkeys[did]) : undefined 121 } 122 /> 123 ))} 124 </div> 125 <div className="flex gap-2"> 126 <HandleInput 127 name="ban-handle" 128 value={identifier} 129 onChange={setIdentifier} 130 className="flex-1" 131 /> 132 <Button onClick={onBan}>ban</Button> 133 </div> 134 </div> 135 136 <div> 137 <label className="block text-neutral-400 mb-3">Hidden Posts</label> 138 <div className="space-y-1 mb-3"> 139 {hidden.map((post) => ( 140 <ModerationListItem 141 key={post.uri} 142 title={post.uri} 143 label={`${post.handle}${post.title || post.body}`} 144 href={`https://pdsls.dev/${post.uri}`} 145 actionLabel="unhide" 146 onAction={ 147 hideRkeys[post.uri] 148 ? () => onUnhide(hideRkeys[post.uri]) 149 : undefined 150 } 151 /> 152 ))} 153 </div> 154 <div className="flex gap-2"> 155 <input 156 name="hide-uri" 157 type="text" 158 value={hideUri} 159 onChange={(e) => setHideUri(e.target.value)} 160 placeholder="at://did/collection/rkey" 161 aria-label="Post URI to hide" 162 className="flex-1 bg-neutral-900 border border-neutral-800 rounded px-3 py-2 text-neutral-200 placeholder-neutral-500 focus:outline-none focus:border-neutral-600" 163 /> 164 <Button onClick={onHide}>hide</Button> 165 </div> 166 </div> 167 </div> 168 </> 169 ); 170}