BlueSky & more on desktop lazurite.stormlightlabs.org/
tauri rust typescript bluesky appview atproto solid
2
fork

Configure Feed

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

feat: add batch size to emitted follow audit event

+85 -41
+6 -1
src-tauri/src/feed.rs
··· 377 377 #[derive(Debug, Clone, Serialize)] 378 378 #[serde(rename_all = "camelCase")] 379 379 struct FollowHygieneProgress { 380 + batch_size: usize, 380 381 current: usize, 381 382 total: usize, 382 383 } ··· 1241 1242 completed += 1; 1242 1243 app.emit( 1243 1244 FOLLOW_HYGIENE_PROGRESS_EVENT, 1244 - FollowHygieneProgress { current: completed, total: total_batches }, 1245 + FollowHygieneProgress { 1246 + batch_size: FOLLOW_AUDIT_PROFILE_BATCH_SIZE, 1247 + current: completed, 1248 + total: total_batches, 1249 + }, 1245 1250 )?; 1246 1251 } 1247 1252
+1 -2
src/components/profile/FollowHygieneList.tsx
··· 33 33 class="tone-muted rounded-2xl p-3 transition-colors duration-150" 34 34 classList={{ "bg-red-500/12": props.selected, "ring-1 ring-[var(--focus-ring)]": props.focused }} 35 35 animate={{ opacity: props.exiting ? 0 : 1, x: props.exiting ? 20 : 0, y: 0 }} 36 - initial={{ opacity: 0, y: 6 }} 37 - transition={{ duration: 0.18, delay: Math.min(props.index * 0.02, 0.2) }} 36 + transition={{ duration: 0.18 }} 38 37 tabIndex={0} 39 38 onFocus={() => props.onFocus()} 40 39 onKeyDown={(event) => {
+68 -33
src/components/profile/FollowHygienePanel.tsx
··· 8 8 import { normalizeError } from "$/lib/utils/text"; 9 9 import { listen } from "@tauri-apps/api/event"; 10 10 import * as logger from "@tauri-apps/plugin-log"; 11 + import { openUrl } from "@tauri-apps/plugin-opener"; 11 12 import { createMemo, onCleanup, onMount, Show } from "solid-js"; 12 13 import { createStore } from "solid-js/store"; 13 14 import { Motion } from "solid-motionone"; ··· 55 56 flagged: [], 56 57 focusedUri: null, 57 58 phase: "idle", 58 - progress: { current: 0, total: 1 }, 59 + progress: { batchSize: 0, current: 0, total: 1 }, 59 60 result: null, 60 61 scanError: null, 61 62 selectedUris: new Set<string>(), ··· 70 71 return null; 71 72 } 72 73 74 + const batchSize = optionalNumber(record.batchSize); 73 75 const current = optionalNumber(record.current); 74 76 const total = optionalNumber(record.total); 75 77 if (current === null || total === null) { 76 78 return null; 77 79 } 78 80 79 - return { current: Math.max(0, Math.floor(current)), total: Math.max(1, Math.floor(total)) }; 81 + return { 82 + batchSize: batchSize === null ? 0 : Math.max(1, Math.floor(batchSize)), 83 + current: Math.max(0, Math.floor(current)), 84 + total: Math.max(1, Math.floor(total)), 85 + }; 86 + } 87 + 88 + function deriveFilters( 89 + selectedUris: Set<string>, flagged: FlaggedFollow[], filters: Record<StatusCategoryKey, StatusCategoryState>, 90 + ) { 91 + const nextFilters = { ...filters }; 92 + for (const category of STATUS_CATEGORIES) { 93 + const categoryUris = flagged.filter((follow) => hasStatus(follow.status, category.bit)).map((follow) => 94 + follow.followUri 95 + ); 96 + const selected = categoryUris.length > 0 && categoryUris.every((uri) => selectedUris.has(uri)); 97 + nextFilters[category.key] = { ...filters[category.key], selected }; 98 + } 99 + 100 + return nextFilters; 80 101 } 81 102 82 103 function FollowHygieneHeader(props: { onClose: () => void }) { ··· 84 105 <header class="flex items-center justify-between gap-3 px-5 py-4"> 85 106 <div class="grid gap-1"> 86 107 <p class="m-0 text-[0.68rem] uppercase tracking-[0.12em] text-on-surface-variant">Account maintenance</p> 87 - <h2 class="m-0 text-xl font-semibold tracking-[-0.02em] text-on-surface">Follow hygiene</h2> 108 + <h2 class="m-0 text-xl font-semibold tracking-[-0.02em] text-on-surface">Follow Audit</h2> 88 109 </div> 89 - <button 90 - class="ui-control ui-control-hoverable flex h-9 w-9 items-center justify-center rounded-full" 91 - type="button" 92 - onClick={() => props.onClose()}> 93 - <Icon iconClass="i-ri-close-line" class="text-base" /> 94 - </button> 110 + <div class="flex items-center gap-2"> 111 + <button 112 + class="ui-control ui-control-hoverable inline-flex min-h-9 items-center gap-1.5 rounded-full px-3 text-sm text-on-surface" 113 + type="button" 114 + onClick={() => void openUrl(FOLLOW_AUDIT_INSPIRATION_URL)}> 115 + <span>Inspiration</span> 116 + <Icon kind="ext-link" class="text-sm" /> 117 + </button> 118 + <button 119 + class="ui-control ui-control-hoverable flex h-9 w-9 items-center justify-center rounded-full" 120 + type="button" 121 + onClick={() => props.onClose()}> 122 + <Icon iconClass="i-ri-close-line" class="text-base" /> 123 + </button> 124 + </div> 95 125 </header> 96 126 ); 97 127 } ··· 270 300 onUnfollow: openConfirmation, 271 301 })); 272 302 273 - function syncCategorySelection(selectedUris: Set<string>, flagged: FlaggedFollow[]) { 274 - for (const category of STATUS_CATEGORIES) { 275 - const categoryUris = flagged.filter((follow) => hasStatus(follow.status, category.bit)).map((follow) => 276 - follow.followUri 277 - ); 278 - const selected = categoryUris.length > 0 && categoryUris.every((uri) => selectedUris.has(uri)); 279 - setState("filters", category.key, "selected", selected); 280 - } 281 - } 282 - 283 303 function updateSelectedUris(nextSelected: Set<string>, flagged: FlaggedFollow[] = state.flagged) { 284 - setState("selectedUris", nextSelected); 285 - syncCategorySelection(nextSelected, flagged); 304 + setState((current) => ({ 305 + filters: deriveFilters(nextSelected, flagged, current.filters), 306 + selectedUris: nextSelected, 307 + })); 286 308 } 287 309 288 310 async function startScan() { ··· 292 314 293 315 requestId += 1; 294 316 const activeRequest = requestId; 317 + 318 + if (exitTimer) { 319 + clearTimeout(exitTimer); 320 + exitTimer = undefined; 321 + } 322 + 295 323 setState({ 296 324 confirmOpen: false, 297 325 exitingUris: new Set<string>(), 298 326 flagged: [], 299 327 focusedUri: null, 300 328 phase: "scanning", 301 - progress: { current: 0, total: 1 }, 329 + progress: { batchSize: 0, current: 0, total: 1 }, 302 330 result: null, 303 331 scanError: null, 304 332 selectedUris: new Set<string>(), ··· 313 341 } 314 342 315 343 const initialSelection = new Set(flagged.map((follow) => follow.followUri)); 316 - setState("flagged", flagged); 317 - updateSelectedUris(initialSelection, flagged); 318 - setState("phase", "ready"); 319 - setState( 320 - "progress", 321 - (progress) => ({ current: Math.max(progress.current, progress.total), total: progress.total }), 322 - ); 344 + setState((current) => ({ 345 + filters: deriveFilters(initialSelection, flagged, current.filters), 346 + flagged, 347 + phase: "ready", 348 + progress: { 349 + batchSize: current.progress.batchSize, 350 + current: Math.max(current.progress.current, current.progress.total), 351 + total: current.progress.total, 352 + }, 353 + selectedUris: initialSelection, 354 + })); 323 355 } catch (error) { 324 356 if (activeRequest !== requestId) { 325 357 return; ··· 419 451 420 452 exitTimer = setTimeout(() => { 421 453 const filtered = state.flagged.filter((follow) => !successfulUris.includes(follow.followUri)); 422 - const selected = new Set(state.selectedUris); 423 - setState("flagged", filtered); 424 - setState("exitingUris", new Set<string>()); 425 - syncCategorySelection(selected, filtered); 454 + setState((current) => ({ 455 + exitingUris: new Set<string>(), 456 + filters: deriveFilters(current.selectedUris, filtered, current.filters), 457 + flagged: filtered, 458 + })); 459 + exitTimer = undefined; 426 460 }, EXIT_ANIMATION_MS); 427 461 } 428 462 ··· 550 584 </> 551 585 ); 552 586 } 587 + const FOLLOW_AUDIT_INSPIRATION_URL = "https://cleanfollow-bsky.pages.dev/";
+7 -2
src/components/profile/FollowHygieneToolbar.tsx
··· 14 14 onScan: () => void; 15 15 }; 16 16 17 - function ProgressMeter(props: { current: number; total: number; percent: number }) { 17 + function ProgressMeter(props: { batchSize: number; current: number; total: number; percent: number }) { 18 18 return ( 19 19 <div class="grid gap-2"> 20 20 <div class="h-2 overflow-hidden rounded-full bg-surface-container-high"> ··· 25 25 </div> 26 26 <p class="m-0 text-xs text-on-surface-variant"> 27 27 Scanning batches: {Math.min(props.current, props.total)} / {props.total} 28 + <Show when={props.batchSize > 0}> ({props.batchSize} per batch)</Show> 28 29 </p> 29 30 </div> 30 31 ); ··· 56 57 </div> 57 58 58 59 <Show when={props.showProgress}> 59 - <ProgressMeter current={props.progress.current} percent={props.progressPercent} total={props.progress.total} /> 60 + <ProgressMeter 61 + batchSize={props.progress.batchSize} 62 + current={props.progress.current} 63 + percent={props.progressPercent} 64 + total={props.progress.total} /> 60 65 </Show> 61 66 62 67 <Show when={props.scanError}>{(error) => <p class="m-0 text-sm text-red-300">{error()}</p>}</Show>
+2 -2
src/components/profile/tests/FollowHygienePanel.test.tsx
··· 67 67 68 68 const listener = listenMock.mock.calls[0]?.[1]; 69 69 expect(listener).toBeTypeOf("function"); 70 - listener({ payload: { current: 1, total: 4 } }); 70 + listener({ payload: { batchSize: 25, current: 1, total: 4 } }); 71 71 72 72 expect(await screen.findByText("@ghost.test")).toBeInTheDocument(); 73 - expect(screen.getByText(/Scanning batches: [1-4] \/ 4/u)).toBeInTheDocument(); 73 + expect(screen.getByText(/Scanning batches: [1-4] \/ 4 \(25 per batch\)/u)).toBeInTheDocument(); 74 74 expect(screen.getByText("3 of 3 visible selected (3 total).")).toBeInTheDocument(); 75 75 76 76 fireEvent.click(screen.getByRole("button", { name: "Hide Deleted" }));
+1 -1
src/lib/types.ts
··· 142 142 143 143 export type FollowBatchResult = { deleted: number; failed: string[] }; 144 144 145 - export type FollowHygieneProgress = { current: number; total: number }; 145 + export type FollowHygieneProgress = { batchSize: number; current: number; total: number }; 146 146 147 147 export type FeedGeneratorView = { 148 148 uri: string;