an appview-less Bluesky client using Constellation and PDS Queries reddwarf.app
frontend spa bluesky reddwarf microcosm

filtering logic in notifications post interactions

rimar1337 1e8e6b78 1751dc48

Changed files
+151 -23
src
+1
src/auto-imports.d.ts
··· 19 19 const IconMaterialSymbolsTag: typeof import('~icons/material-symbols/tag.jsx').default 20 20 const IconMdiAccountCircle: typeof import('~icons/mdi/account-circle.jsx').default 21 21 const IconMdiAccountPlus: typeof import('~icons/mdi/account-plus.jsx').default 22 + const IconMdiCheck: typeof import('~icons/mdi/check.jsx').default 22 23 const IconMdiMessageReplyTextOutline: typeof import('~icons/mdi/message-reply-text-outline.jsx').default 23 24 const IconMdiPencilOutline: typeof import('~icons/mdi/pencil-outline.jsx').default 24 25 }
+128 -23
src/routes/notifications.tsx
··· 6 6 7 7 import defaultpfp from "~/../public/favicon.png"; 8 8 import { Header } from "~/components/Header"; 9 - import { ReusableTabRoute, useReusableTabScrollRestore } from "~/components/ReusableTabRoute"; 9 + import { 10 + ReusableTabRoute, 11 + useReusableTabScrollRestore, 12 + } from "~/components/ReusableTabRoute"; 10 13 import { 11 14 MdiCardsHeartOutline, 12 15 MdiCommentOutline, ··· 17 20 import { 18 21 constellationURLAtom, 19 22 imgCDNAtom, 23 + postInteractionsFiltersAtom, 20 24 } from "~/utils/atoms"; 21 25 import { 22 26 useInfiniteQueryAuthorFeed, ··· 102 106 ); 103 107 }, [infiniteMentionsData]); 104 108 105 - 106 109 useReusableTabScrollRestore("Notifications"); 107 110 108 111 if (isLoading) return <LoadingState text="Loading mentions..." />; ··· 169 172 170 173 useReusableTabScrollRestore("Notifications"); 171 174 172 - if (isLoading) return <LoadingState text="Loading mentions..." />; 175 + if (isLoading) return <LoadingState text="Loading follows..." />; 173 176 if (isError) return <ErrorState error={error} />; 174 177 175 - if (!followsAturis?.length) return <EmptyState text="No mentions yet." />; 178 + if (!followsAturis?.length) return <EmptyState text="No follows yet." />; 176 179 177 180 return ( 178 181 <> ··· 224 227 225 228 useReusableTabScrollRestore("Notifications"); 226 229 230 + const [filters] = useAtom(postInteractionsFiltersAtom); 231 + const empty = (!filters.likes && !filters.quotes && !filters.replies && !filters.reposts); 232 + 227 233 return ( 228 234 <> 229 - {posts.map((m) => ( 235 + <PostInteractionsFilterChipBar /> 236 + {!empty && posts.map((m) => ( 230 237 <PostInteractionsItem key={m.uri} uri={m.uri} /> 231 238 ))} 232 239 ··· 243 250 ); 244 251 } 245 252 246 - const ORDER: ("like" | "repost" | "reply" | "quote")[] = [ 247 - "like", 248 - "repost", 249 - "reply", 250 - "quote", 251 - ]; 253 + function PostInteractionsFilterChipBar() { 254 + const [filters, setFilters] = useAtom(postInteractionsFiltersAtom); 255 + // const empty = (!filters.likes && !filters.quotes && !filters.replies && !filters.reposts); 256 + 257 + // useEffect(() => { 258 + // if (empty) { 259 + // setFilters((prev) => ({ 260 + // ...prev, 261 + // likes: true, 262 + // })); 263 + // } 264 + // }, [ 265 + // empty, 266 + // setFilters, 267 + // ]); 268 + 269 + const toggle = (key: keyof typeof filters) => { 270 + setFilters((prev) => ({ 271 + ...prev, 272 + [key]: !prev[key], 273 + })); 274 + }; 275 + 276 + return ( 277 + <div className="flex flex-row flex-wrap gap-2 px-4 pt-4"> 278 + <Chip 279 + state={filters.likes} 280 + text="Likes" 281 + onClick={() => toggle("likes")} 282 + /> 283 + <Chip 284 + state={filters.reposts} 285 + text="Reposts" 286 + onClick={() => toggle("reposts")} 287 + /> 288 + <Chip 289 + state={filters.replies} 290 + text="Replies" 291 + onClick={() => toggle("replies")} 292 + /> 293 + <Chip 294 + state={filters.quotes} 295 + text="Quotes" 296 + onClick={() => toggle("quotes")} 297 + /> 298 + <Chip 299 + state={filters.showAll} 300 + text="Show All Metrics" 301 + onClick={() => toggle("showAll")} 302 + /> 303 + </div> 304 + ); 305 + } 306 + 307 + function Chip({ 308 + state, 309 + text, 310 + onClick, 311 + }: { 312 + state: boolean; 313 + text: string; 314 + onClick: React.MouseEventHandler<HTMLButtonElement>; 315 + }) { 316 + return ( 317 + <button 318 + onClick={onClick} 319 + className={`relative inline-flex items-center px-3 py-1.5 rounded-lg text-sm font-medium transition-all 320 + ${ 321 + state 322 + ? "bg-primary/20 text-primary bg-gray-200 dark:bg-gray-800 border border-transparent" 323 + : "bg-surface-container-low text-on-surface-variant border border-outline" 324 + } 325 + hover:bg-primary/30 active:scale-[0.97] 326 + dark:border-outline-variant 327 + `} 328 + > 329 + {state && ( 330 + <IconMdiCheck 331 + className="mr-1.5 inline-block w-4 h-4 rounded-full bg-primary" 332 + aria-hidden 333 + /> 334 + )} 335 + {text} 336 + </button> 337 + ); 338 + } 252 339 253 340 function PostInteractionsItem({ uri }: { uri: string }) { 341 + const [filters] = useAtom(postInteractionsFiltersAtom); 254 342 const { data: links } = useQueryConstellation({ 255 343 method: "/links/all", 256 344 target: uri, ··· 271 359 272 360 const all = likes + replies + reposts + quotes; 273 361 362 + const failLikes = filters.likes && likes < 1; 363 + const failReposts = filters.reposts && reposts < 1; 364 + const failReplies = filters.replies && replies < 1; 365 + const failQuotes = filters.quotes && quotes < 1; 366 + 367 + const showLikes = filters.showAll || filters.likes 368 + const showReposts = filters.showAll || filters.reposts 369 + const showReplies = filters.showAll || filters.replies 370 + const showQuotes = filters.showAll || filters.quotes 371 + 372 + const showNone = !showLikes && !showReposts && !showReplies && !showQuotes; 373 + 374 + const fail = failLikes || failReposts || failReplies || failQuotes || showNone; 375 + 376 + 377 + if (fail) return; 378 + 274 379 return ( 275 380 <div className="flex flex-col"> 381 + {/* <span>fail likes {failLikes ? "true" : "false"}</span> 382 + <span>fail repost {failReposts ? "true" : "false"}</span> 383 + <span>fail reply {failReplies ? "true" : "false"}</span> 384 + <span>fail qupte {failQuotes ? "true" : "false"}</span> */} 276 385 <div className="border rounded-xl mx-4 mt-4 overflow-hidden"> 277 386 <UniversalPostRendererATURILoader 278 387 isQuote ··· 282 391 concise={true} 283 392 /> 284 393 <div className="flex flex-col divide-x"> 285 - <InteractionsButton 286 - key={likes} 394 + {showLikes &&(<InteractionsButton 287 395 type={"like"} 288 396 uri={uri} 289 397 count={likes} 290 - /> 291 - <InteractionsButton 292 - key={reposts} 398 + />)} 399 + {showReposts && (<InteractionsButton 293 400 type={"repost"} 294 401 uri={uri} 295 402 count={reposts} 296 - /> 297 - <InteractionsButton 298 - key={replies} 403 + />)} 404 + {showReplies && (<InteractionsButton 299 405 type={"reply"} 300 406 uri={uri} 301 407 count={replies} 302 - /> 303 - <InteractionsButton 304 - key={quotes} 408 + />)} 409 + {showQuotes && (<InteractionsButton 305 410 type={"quote"} 306 411 uri={uri} 307 412 count={quotes} 308 - /> 413 + />)} 309 414 {!all && ( 310 415 <div className="text-center text-gray-500 dark:text-gray-400 pb-3 pt-2 border-t"> 311 416 No interactions yet.
+22
src/utils/atoms.ts
··· 25 25 activeTab: string; 26 26 scrollPositions: Record<string, number>; 27 27 }; 28 + /** 29 + * @deprecated should be safe to remove i think 30 + */ 28 31 export const notificationsScrollAtom = atom<TabRouteScrollState>({ 29 32 activeTab: "mentions", 30 33 scrollPositions: {}, 31 34 }); 35 + 36 + export type InteractionFilter = { 37 + likes: boolean; 38 + reposts: boolean; 39 + quotes: boolean; 40 + replies: boolean; 41 + showAll: boolean; 42 + }; 43 + const defaultFilters: InteractionFilter = { 44 + likes: true, 45 + reposts: true, 46 + quotes: true, 47 + replies: true, 48 + showAll: false, 49 + }; 50 + export const postInteractionsFiltersAtom = atomWithStorage<InteractionFilter>( 51 + "postInteractionsFilters", 52 + defaultFilters 53 + ); 32 54 33 55 export const reusableTabRouteScrollAtom = atom<Record<string, TabRouteScrollState | undefined> | undefined>({}); 34 56