this repo has no description
0
fork

Configure Feed

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

Implement strict mode feature for content-aware service filtering

- Add strictMode option to filter services based on content support level
- Add contentSupport field to ServiceConfig with four levels:
- 'only-profiles': Profile-only services (6 services)
- 'only-posts': Posts-only services (skythread)
- 'profiles-and-posts': Profiles and posts only (toolify.blue)
- 'full': Complete content support (4 services)
- Update buildDestinations() with smart filtering logic:
- Posts: Show only-posts, profiles-and-posts, and full services
- Feeds/Lists: Show only full support services
- Profiles: Show all applicable services
- Add strict mode checkbox to options page
- Add comprehensive test suite (6 new tests, 72 total passing)
- Maintain backward compatibility (defaults to false)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

TKTK 84da73f2 df69621d

+345 -15
+151 -7
CLAUDE.md
··· 316 316 317 317 ### Status: COMPLETED ✅ 318 318 319 - Basic options page infrastructure has been successfully implemented with a single "show emojis" checkbox setting. 319 + Options page infrastructure has been successfully implemented with "show emojis" and "strict mode" checkbox settings. 320 320 321 321 **Completed work:** 322 322 323 - - ✅ `src/options/options.html` - Basic options page structure with checkbox 323 + - ✅ `src/options/options.html` - Options page structure with checkboxes for both settings 324 324 - ✅ `src/options/options.css` - Clean, extension-appropriate styling 325 - - ✅ `src/options/options.ts` - Checkbox logic and storage integration 325 + - ✅ `src/options/options.ts` - Checkbox logic and storage integration for both options 326 326 - ✅ `public/manifest.json` - Added `options_ui` configuration 327 327 - ✅ All validation commands pass (lint, typecheck, tests, build) 328 328 329 329 **Implementation details:** 330 330 331 331 - Uses `chrome.storage.sync` for cross-device synchronization 332 - - Storage key: `showEmojis` (boolean, defaults to true) 332 + - Storage keys: `showEmojis` (boolean, defaults to true), `strictMode` (boolean, defaults to false) 333 333 - Follows existing storage patterns from cache system 334 334 - Chrome/Firefox compatible 335 335 - Proper TypeScript types and error handling 336 - 337 - **Next step:** Implement the actual "show emojis" feature functionality. 338 336 339 337 ## ✅ Show Emojis Feature Implementation - COMPLETED 340 338 ··· 369 367 370 368 **Testing results:** 371 369 372 - - All 60 existing tests continue to pass 370 + - All existing tests continue to pass 373 371 - New tests verify emoji functionality works correctly 374 372 - Both enabled (🦋 bsky.app) and disabled (bsky.app) scenarios tested 375 373 ··· 428 426 - ✅ Documentation update - CLAUDE.md and URL Pattern Recognition updated 429 427 430 428 **Final Results:** 429 + 431 430 - **66 total tests passing** (4 new toolify.blue tests added) 432 431 - **Service successfully integrated** into modular architecture 433 432 - **Full handle and DID support** like bsky.app and deer.social ··· 436 435 - **No breaking changes** to existing functionality 437 436 438 437 **Implementation Complete** - toolify.blue is now fully supported in the extension! 🎉 438 + 439 + ## 🔒 Strict Mode Feature Implementation - COMPLETED 440 + 441 + ### Status: COMPLETED ✅ 442 + 443 + Successfully implemented the "strict mode" option to prevent fallback behavior when viewing posts or specific content types. When enabled, the extension only shows services that support the current content level (e.g., posts) rather than falling back to profile-level URLs. 444 + 445 + **Current Behavior (Fallback Mode)**: 446 + 447 + - When viewing a post on deer.social or pdsls.dev 448 + - Extension shows destinations for ALL services 449 + - Services that don't support posts (like cred.blue, tangled.sh) fall back to profile URLs 450 + - User gets mixed results: some post URLs, some profile URLs 451 + 452 + **Strict Mode Behavior**: 453 + 454 + - When viewing a post, only show services that support post-level URLs 455 + - When viewing a profile, show all applicable services 456 + - No fallback behavior - strict content-type matching 457 + 458 + ### Service Categorization Analysis 459 + 460 + **Full Content Support** (`'full'` - profiles, posts, feeds, lists): 461 + 462 + - 🦌 **deer.social** - Uses `bskyAppPath` (supports `/profile/handle/post|feed|lists/xyz`) 463 + - 🦋 **bsky.app** - Uses `bskyAppPath` (supports `/profile/handle/post|feed|lists/xyz`) 464 + - ⚙️ **pdsls.dev** - Uses full `atUri` (complete AT Protocol support for all content types) 465 + - 🛠️ **atp.tools** - Uses full `atUri` (complete AT Protocol support for all content types) 466 + 467 + **Profiles and Posts Support** (`'profiles-and-posts'` - profiles and posts only): 468 + 469 + - 🔧 **toolify.blue** - Uses `bskyAppPath` (supports `/profile/handle/post/xyz` but not feeds/lists) 470 + 471 + **Posts-Only Services** (`'only-posts'` - require rkey, posts only): 472 + 473 + - ☁️ **skythread** - Requires `rkey`, returns `null` without it (posts only, not feeds/lists) 474 + 475 + **Profile-Only Services** (`'only-profiles'` - profiles only, no content): 476 + 477 + - 🍥 **cred.blue** - Only supports handles, no content URLs 478 + - 🪢 **tangled.sh** - Only supports handles, no content URLs 479 + - 📰 **frontpage.fyi** - Only supports handles, no content URLs 480 + - ☀️ **clearsky** - Profile-level DID checking only 481 + - ⛵ **boat.kelinci** - PLC oplog viewer, profile-level only 482 + - 🪪 **plc.directory** - PLC directory, profile-level only 483 + 484 + **Completed work:** 485 + 486 + - ✅ **Updated OptionsData interface** - Added `strictMode: boolean` field (defaults to false) 487 + - ✅ **Enhanced ServiceConfig interface** - Added `contentSupport` field with values: `'only-profiles'` | `'only-posts'` | `'profiles-and-posts'` | `'full'` 488 + - ✅ **Updated all service configurations** - Added appropriate contentSupport values for all 12 services 489 + - ✅ **Modified buildDestinations() function** - Added strictMode parameter with filtering logic for posts/feeds/lists 490 + - ✅ **Updated options page** - Added strict mode checkbox to HTML/CSS/TS 491 + - ✅ **Updated popup integration** - Loads and passes strictMode option to buildDestinations 492 + - ✅ **Added comprehensive tests** - 6 new strict mode tests covering all scenarios 493 + - ✅ **All validation commands pass** - Format, lint, typecheck, test (72 tests), and build all successful 494 + 495 + **Implementation details:** 496 + 497 + - **Backward compatibility**: `strictMode` defaults to `false` (maintains existing fallback behavior) 498 + - **Service categorization**: Each service now has explicit content support level 499 + - **Smart filtering**: Different logic for posts vs feeds/lists in strict mode 500 + - **Options integration**: Uses existing options system with shared storage 501 + - **Comprehensive testing**: Added tests for all content types and combinations 502 + 503 + **How it works:** 504 + 505 + 1. **Options page**: User toggles "strict mode" checkbox → setting saved to `chrome.storage.sync` 506 + 2. **Popup initialization**: Loads options via `loadOptions()` utility 507 + 3. **Content filtering**: When `strictMode = true` and viewing content (posts/feeds/lists): 508 + - For posts: Shows services with `'only-posts'`, `'profiles-and-posts'`, or `'full'` support 509 + - For feeds/lists: Shows only services with `'full'` support 510 + - Excludes services with `'only-profiles'` support 511 + 4. **Profile viewing**: No filtering applied (shows all applicable services) 512 + 513 + 514 + ### Content Support Classification 515 + 516 + ```typescript 517 + // Example service configurations with contentSupport field 518 + SERVICES.DEER_SOCIAL = { 519 + emoji: '🦌', 520 + name: 'deer.social', 521 + contentSupport: 'full', // Supports profiles, posts, feeds, lists 522 + // ... existing config 523 + }; 524 + 525 + SERVICES.TOOLIFY_BLUE = { 526 + emoji: '🔧', 527 + name: 'toolify.blue', 528 + contentSupport: 'profiles-and-posts', // Supports profiles and posts only 529 + // ... existing config 530 + }; 531 + 532 + SERVICES.CRED_BLUE = { 533 + emoji: '🍥', 534 + name: 'cred.blue', 535 + contentSupport: 'only-profiles', // Profile-only service 536 + // ... existing config 537 + }; 538 + 539 + SERVICES.SKYTHREAD = { 540 + emoji: '☁️', 541 + name: 'skythread', 542 + contentSupport: 'only-posts', // Posts-only service (no feeds/lists/profiles) 543 + // ... existing config 544 + }; 545 + ``` 546 + 547 + ### User Experience Impact 548 + 549 + **Strict Mode OFF (Default)**: 550 + 551 + - Viewing deer.social post/feed/list → Shows all services (current behavior) 552 + - Some destinations are content-level, some fall back to profiles 553 + - Maximum destinations shown, mixed content levels 554 + 555 + **Strict Mode ON**: 556 + 557 + - Viewing deer.social post/feed/list → Only shows content-capable services 558 + - All destinations are content-level URLs, no profile fallbacks 559 + - Fewer but more relevant destinations 560 + 561 + **Benefits**: 562 + 563 + - Reduces cognitive load when viewing content (posts/feeds/lists) 564 + - Ensures consistent content-level navigation 565 + - Users who want post-to-post navigation get cleaner experience 566 + - Advanced users can enable for more precise behavior 567 + 568 + **Testing results:** 569 + 570 + - All 72 tests pass (6 new strict mode tests added) 571 + - Comprehensive coverage of all content types and service combinations 572 + - Emoji + strict mode combinations work correctly 573 + - Backward compatibility maintained (defaults to false) 574 + 575 + **Final Results:** 576 + - **Service categorization**: 12 services properly categorized by content support 577 + - **Smart filtering**: Context-aware service filtering in strict mode 578 + - **Zero breaking changes**: Existing behavior preserved when strict mode is off 579 + - **Comprehensive testing**: All scenarios covered with automated tests 580 + - **Clean options integration**: Follows existing patterns and storage system 581 + 582 + **Implementation Complete** - Strict mode is now fully functional in the extension! 🎉
+7
src/options/options.html
··· 16 16 <span class="checkbox-text">Show emojis</span> 17 17 </label> 18 18 </div> 19 + 20 + <div class="option-group"> 21 + <label class="checkbox-label"> 22 + <input type="checkbox" id="strictMode" /> 23 + <span class="checkbox-text">Strict mode</span> 24 + </label> 25 + </div> 19 26 </div> 20 27 21 28 <script type="module" src="options.ts"></script>
+13 -4
src/options/options.ts
··· 1 1 interface OptionsData { 2 2 showEmojis: boolean; 3 + strictMode: boolean; 3 4 } 4 5 5 6 const DEFAULT_OPTIONS: OptionsData = { 6 7 showEmojis: true, 8 + strictMode: false, 7 9 }; 8 10 9 11 const STORAGE_KEY = 'wormhole-options'; ··· 17 19 const options = data as Record<string, unknown>; 18 20 return { 19 21 showEmojis: typeof options.showEmojis === 'boolean' ? options.showEmojis : DEFAULT_OPTIONS.showEmojis, 22 + strictMode: typeof options.strictMode === 'boolean' ? options.strictMode : DEFAULT_OPTIONS.strictMode, 20 23 }; 21 24 } 22 25 ··· 37 40 38 41 async function initializeOptions(): Promise<void> { 39 42 const showEmojisCheckbox = document.getElementById('showEmojis') as HTMLInputElement | null; 43 + const strictModeCheckbox = document.getElementById('strictMode') as HTMLInputElement | null; 40 44 41 - if (!showEmojisCheckbox) { 42 - console.error('showEmojis checkbox not found'); 45 + if (!showEmojisCheckbox || !strictModeCheckbox) { 46 + console.error('Required checkboxes not found'); 43 47 return; 44 48 } 45 49 46 50 const options = await loadOptions(); 47 51 showEmojisCheckbox.checked = options.showEmojis; 52 + strictModeCheckbox.checked = options.strictMode; 48 53 49 - showEmojisCheckbox.addEventListener('change', () => { 54 + const updateOptions = () => { 50 55 const newOptions: OptionsData = { 51 56 showEmojis: showEmojisCheckbox.checked, 57 + strictMode: strictModeCheckbox.checked, 52 58 }; 53 59 void saveOptions(newOptions); 54 - }); 60 + }; 61 + 62 + showEmojisCheckbox.addEventListener('change', updateOptions); 63 + strictModeCheckbox.addEventListener('change', updateOptions); 55 64 } 56 65 57 66 if (document.readyState === 'loading') {
+3 -3
src/popup/popup.ts
··· 189 189 return; 190 190 } 191 191 192 - let ds = buildDestinations(info, options.showEmojis); 192 + let ds = buildDestinations(info, options.showEmojis, options.strictMode); 193 193 render(ds); 194 194 195 195 if (info.did && !info.handle) { ··· 221 221 // After attempting to get handle from cache or by fetching: 222 222 if (handleToUse) { 223 223 info.handle = handleToUse; 224 - ds = buildDestinations(info, options.showEmojis); // Re-build destinations with the handle 224 + ds = buildDestinations(info, options.showEmojis, options.strictMode); // Re-build destinations with the handle 225 225 render(ds); // Re-render the list 226 226 } else { 227 227 // Handle was not obtained. An error status might have already been set. ··· 258 258 } 259 259 if (didToUse) { 260 260 info.did = didToUse; 261 - ds = buildDestinations(info, options.showEmojis); 261 + ds = buildDestinations(info, options.showEmojis, options.strictMode); 262 262 render(ds); 263 263 } else if (!ds.length && !errorStatusWasSet) { 264 264 showStatus('No actions available');
+3
src/shared/options.ts
··· 1 1 interface OptionsData { 2 2 showEmojis: boolean; 3 + strictMode: boolean; 3 4 } 4 5 5 6 const DEFAULT_OPTIONS: OptionsData = { 6 7 showEmojis: true, 8 + strictMode: false, 7 9 }; 8 10 9 11 const STORAGE_KEY = 'wormhole-options'; ··· 23 25 const options = data as Record<string, unknown>; 24 26 cachedOptions = { 25 27 showEmojis: typeof options.showEmojis === 'boolean' ? options.showEmojis : DEFAULT_OPTIONS.showEmojis, 28 + strictMode: typeof options.strictMode === 'boolean' ? options.strictMode : DEFAULT_OPTIONS.strictMode, 26 29 }; 27 30 } else { 28 31 cachedOptions = DEFAULT_OPTIONS;
+34 -1
src/shared/services.ts
··· 3 3 export interface ServiceConfig { 4 4 emoji: string; 5 5 name: string; 6 + contentSupport: 'only-profiles' | 'only-posts' | 'profiles-and-posts' | 'full'; 6 7 7 8 // Input parsing configuration 8 9 parsing?: { ··· 38 39 DEER_SOCIAL: { 39 40 emoji: '🦌', 40 41 name: 'deer.social', 42 + contentSupport: 'full', 41 43 parsing: { 42 44 hostname: 'deer.social', 43 45 patterns: { ··· 51 53 BSKY_APP: { 52 54 emoji: '🦋', 53 55 name: 'bsky.app', 56 + contentSupport: 'full', 54 57 parsing: { 55 58 hostname: 'bsky.app', 56 59 patterns: { ··· 64 67 PDSLS_DEV: { 65 68 emoji: '⚙️', 66 69 name: 'pdsls.dev', 70 + contentSupport: 'full', 67 71 parsing: { 68 72 hostname: 'pdsls.dev', 69 73 patterns: { ··· 80 84 ATP_TOOLS: { 81 85 emoji: '🛠️', 82 86 name: 'atp.tools', 87 + contentSupport: 'full', 83 88 parsing: { 84 89 hostname: 'atp.tools', 85 90 patterns: { ··· 100 105 CLEARSKY: { 101 106 emoji: '☀️', 102 107 name: 'clearsky', 108 + contentSupport: 'only-profiles', 103 109 parsing: { 104 110 hostname: 'clearsky.app', 105 111 patterns: { ··· 113 119 SKYTHREAD: { 114 120 emoji: '☁️', 115 121 name: 'skythread', 122 + contentSupport: 'only-posts', 116 123 parsing: { 117 124 hostname: 'blue.mackuba.eu', 118 125 patterns: { ··· 136 143 CRED_BLUE: { 137 144 emoji: '🍥', 138 145 name: 'cred.blue', 146 + contentSupport: 'only-profiles', 139 147 parsing: { 140 148 hostname: 'cred.blue', 141 149 patterns: { ··· 150 158 TANGLED_SH: { 151 159 emoji: '🪢', 152 160 name: 'tangled.sh', 161 + contentSupport: 'only-profiles', 153 162 parsing: { 154 163 hostname: 'tangled.sh', 155 164 patterns: { ··· 164 173 FRONTPAGE_FYI: { 165 174 emoji: '📰', 166 175 name: 'frontpage.fyi', 176 + contentSupport: 'only-profiles', 167 177 parsing: { 168 178 hostname: 'frontpage.fyi', 169 179 patterns: { ··· 178 188 BOAT_KELINCI: { 179 189 emoji: '⛵', 180 190 name: 'boat.kelinci', 191 + contentSupport: 'only-profiles', 181 192 parsing: { 182 193 hostname: 'boat.kelinci.net', 183 194 patterns: { ··· 192 203 PLC_DIRECTORY: { 193 204 emoji: '🪪', 194 205 name: 'plc.directory', 206 + contentSupport: 'only-profiles', 195 207 parsing: { 196 208 hostname: 'plc.directory', 197 209 patterns: { ··· 206 218 TOOLIFY_BLUE: { 207 219 emoji: '🔧', 208 220 name: 'toolify.blue', 221 + contentSupport: 'profiles-and-posts', 209 222 parsing: { 210 223 hostname: 'toolify.blue', 211 224 patterns: { ··· 285 298 /** 286 299 * Builds a list of destination link objects from canonical info using service configuration. 287 300 */ 288 - export function buildDestinations(info: TransformInfo, showEmojis = true): { label: string; url: string }[] { 301 + export function buildDestinations( 302 + info: TransformInfo, 303 + showEmojis = true, 304 + strictMode = false, 305 + ): { label: string; url: string }[] { 289 306 const isDidWeb = info.did?.startsWith('did:web:') ?? false; 290 307 const destinations: { label: string; url: string }[] = []; 291 308 ··· 295 312 if (service.requiredFields.handle && !info.handle) continue; 296 313 if (service.requiredFields.rkey && !info.rkey) continue; 297 314 if (service.requiredFields.plcOnly && isDidWeb) continue; 315 + } 316 + 317 + // Strict mode filtering 318 + if (strictMode && info.rkey) { 319 + // When viewing content (posts/feeds/lists), apply strict filtering 320 + if (info.nsid === 'app.bsky.feed.post') { 321 + // For posts: include only-posts, profiles-and-posts, and full 322 + if (!['only-posts', 'profiles-and-posts', 'full'].includes(service.contentSupport)) { 323 + continue; 324 + } 325 + } else if (info.nsid === 'app.bsky.feed.generator' || info.nsid === 'app.bsky.graph.list') { 326 + // For feeds/lists: include only full support services 327 + if (service.contentSupport !== 'full') { 328 + continue; 329 + } 330 + } 298 331 } 299 332 300 333 const url = service.buildUrl(info);
+134
tests/transform.test.ts
··· 397 397 const toolifyDestination = destinations.find((dest) => dest.url.includes('toolify.blue')); 398 398 expect(toolifyDestination?.label).toBe('toolify.blue'); 399 399 }); 400 + 401 + describe('strict mode', () => { 402 + const postInfo = { 403 + atUri: 'at://did:plc:kkkcb7sys7623hcf7oefcffg/app.bsky.feed.post/3lqcw7n4gly2u', 404 + did: 'did:plc:kkkcb7sys7623hcf7oefcffg', 405 + handle: 'now.alice.mosphere.at', 406 + rkey: '3lqcw7n4gly2u', 407 + nsid: 'app.bsky.feed.post', 408 + bskyAppPath: '/profile/now.alice.mosphere.at/post/3lqcw7n4gly2u', 409 + }; 410 + 411 + const feedInfo = { 412 + atUri: 'at://why.bsky.team/app.bsky.feed.generator/cozy', 413 + did: 'did:plc:vpkhqolt662uhesyj6nxm7ys', 414 + handle: 'why.bsky.team', 415 + rkey: 'cozy', 416 + nsid: 'app.bsky.feed.generator', 417 + bskyAppPath: '/profile/why.bsky.team/feed/cozy', 418 + }; 419 + 420 + const listInfo = { 421 + atUri: 'at://alice.mosphere.at/app.bsky.graph.list/3l7vfhhfqcz2u', 422 + did: 'did:plc:by3jhwdqgbtrcc7q4tkkv3cf', 423 + handle: 'alice.mosphere.at', 424 + rkey: '3l7vfhhfqcz2u', 425 + nsid: 'app.bsky.graph.list', 426 + bskyAppPath: '/profile/alice.mosphere.at/lists/3l7vfhhfqcz2u', 427 + }; 428 + 429 + const profileInfo = { 430 + atUri: 'at://alice.mosphere.at', 431 + did: 'did:plc:by3jhwdqgbtrcc7q4tkkv3cf', 432 + handle: 'alice.mosphere.at', 433 + bskyAppPath: '/profile/alice.mosphere.at', 434 + }; 435 + 436 + test('should maintain all services in non-strict mode (default)', () => { 437 + const destinations = buildDestinations(postInfo, true, false); 438 + 439 + // Should include all service types 440 + expect(destinations.some((d) => d.url.includes('deer.social'))).toBe(true); // full 441 + expect(destinations.some((d) => d.url.includes('toolify.blue'))).toBe(true); // profiles-and-posts 442 + expect(destinations.some((d) => d.url.includes('skythread'))).toBe(true); // only-posts 443 + expect(destinations.some((d) => d.url.includes('cred.blue'))).toBe(true); // only-profiles (fallback) 444 + }); 445 + 446 + test('should exclude profile-only services in strict mode for posts', () => { 447 + const destinations = buildDestinations(postInfo, true, true); 448 + 449 + // Should include post-supporting services 450 + expect(destinations.some((d) => d.url.includes('deer.social'))).toBe(true); // full 451 + expect(destinations.some((d) => d.url.includes('bsky.app'))).toBe(true); // full 452 + expect(destinations.some((d) => d.url.includes('toolify.blue'))).toBe(true); // profiles-and-posts 453 + expect(destinations.some((d) => d.url.includes('skythread'))).toBe(true); // only-posts 454 + 455 + // Should exclude profile-only services 456 + expect(destinations.some((d) => d.url.includes('cred.blue'))).toBe(false); 457 + expect(destinations.some((d) => d.url.includes('tangled.sh'))).toBe(false); 458 + expect(destinations.some((d) => d.url.includes('frontpage.fyi'))).toBe(false); 459 + expect(destinations.some((d) => d.url.includes('clearsky'))).toBe(false); 460 + expect(destinations.some((d) => d.url.includes('boat.kelinci'))).toBe(false); 461 + expect(destinations.some((d) => d.url.includes('plc.directory'))).toBe(false); 462 + }); 463 + 464 + test('should exclude toolify.blue in strict mode for feeds', () => { 465 + const destinations = buildDestinations(feedInfo, true, true); 466 + 467 + // Should include full content support services 468 + expect(destinations.some((d) => d.url.includes('deer.social'))).toBe(true); 469 + expect(destinations.some((d) => d.url.includes('bsky.app'))).toBe(true); 470 + expect(destinations.some((d) => d.url.includes('pdsls.dev'))).toBe(true); 471 + expect(destinations.some((d) => d.url.includes('atp.tools'))).toBe(true); 472 + 473 + // Should exclude toolify.blue (only supports posts, not feeds) 474 + expect(destinations.some((d) => d.url.includes('toolify.blue'))).toBe(false); 475 + 476 + // Should exclude skythread (only supports posts, not feeds) 477 + expect(destinations.some((d) => d.url.includes('skythread'))).toBe(false); 478 + 479 + // Should exclude profile-only services 480 + expect(destinations.some((d) => d.url.includes('cred.blue'))).toBe(false); 481 + }); 482 + 483 + test('should exclude toolify.blue in strict mode for lists', () => { 484 + const destinations = buildDestinations(listInfo, true, true); 485 + 486 + // Should include full content support services 487 + expect(destinations.some((d) => d.url.includes('deer.social'))).toBe(true); 488 + expect(destinations.some((d) => d.url.includes('bsky.app'))).toBe(true); 489 + expect(destinations.some((d) => d.url.includes('pdsls.dev'))).toBe(true); 490 + expect(destinations.some((d) => d.url.includes('atp.tools'))).toBe(true); 491 + 492 + // Should exclude toolify.blue (only supports posts, not lists) 493 + expect(destinations.some((d) => d.url.includes('toolify.blue'))).toBe(false); 494 + 495 + // Should exclude skythread (only supports posts, not lists) 496 + expect(destinations.some((d) => d.url.includes('skythread'))).toBe(false); 497 + 498 + // Should exclude profile-only services 499 + expect(destinations.some((d) => d.url.includes('cred.blue'))).toBe(false); 500 + }); 501 + 502 + test('should include all applicable services in strict mode for profiles', () => { 503 + const destinations = buildDestinations(profileInfo, true, true); 504 + 505 + // Profile viewing should include all services that don't require rkey 506 + expect(destinations.some((d) => d.url.includes('deer.social'))).toBe(true); 507 + expect(destinations.some((d) => d.url.includes('bsky.app'))).toBe(true); 508 + expect(destinations.some((d) => d.url.includes('toolify.blue'))).toBe(true); 509 + expect(destinations.some((d) => d.url.includes('cred.blue'))).toBe(true); 510 + expect(destinations.some((d) => d.url.includes('tangled.sh'))).toBe(true); 511 + expect(destinations.some((d) => d.url.includes('frontpage.fyi'))).toBe(true); 512 + expect(destinations.some((d) => d.url.includes('clearsky'))).toBe(true); 513 + expect(destinations.some((d) => d.url.includes('boat.kelinci'))).toBe(true); 514 + expect(destinations.some((d) => d.url.includes('plc.directory'))).toBe(true); 515 + 516 + // Should exclude skythread (requires rkey) 517 + expect(destinations.some((d) => d.url.includes('skythread'))).toBe(false); 518 + }); 519 + 520 + test('should work correctly with emoji settings in strict mode', () => { 521 + const destinationsWithEmoji = buildDestinations(postInfo, true, true); 522 + const destinationsWithoutEmoji = buildDestinations(postInfo, false, true); 523 + 524 + // Should have same filtering but different labels 525 + expect(destinationsWithEmoji.length).toBe(destinationsWithoutEmoji.length); 526 + 527 + const bskyWithEmoji = destinationsWithEmoji.find((d) => d.url.includes('bsky.app')); 528 + const bskyWithoutEmoji = destinationsWithoutEmoji.find((d) => d.url.includes('bsky.app')); 529 + 530 + expect(bskyWithEmoji?.label).toBe('🦋 bsky.app'); 531 + expect(bskyWithoutEmoji?.label).toBe('bsky.app'); 532 + }); 533 + }); 400 534 });