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

Configure Feed

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

filtering logic in notifications post interactions

rimar1337 1e8e6b78 1751dc48

+151 -23
+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