Tend your corner of the atmosphere. spores.garden turns your AT Protocol records into a personal site with unique themes. Your data never leaves your PDS. Grow something that's truly yours. spores.garden
3
fork

Configure Feed

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

Merge pull request #126 from hyphacoop/feat-add-any-bsky-post

feat: add bsky post from anyone on the atmosphere

authored by charlebois.info and committed by

GitHub 40634eac 725d51d6

+141 -22
+47 -9
src/components/site-editor.ts
··· 1 1 import { getConfig, saveConfig, getSiteOwnerDid } from '../config'; 2 2 import { getCurrentDid, putRecord, uploadBlob } from '../oauth'; 3 - import { getRecord, getProfile, buildAtUri } from '../at-client'; 3 + import { getRecord, getProfile, buildAtUri, resolveHandle } from '../at-client'; 4 4 import { getCollectionRecords, getCollections, getRecordByUri } from '../records/loader'; 5 5 import { getCollection } from '../config/nsid'; 6 6 import { setCachedActivity } from './recent-gardens'; ··· 321 321 if (posts.length === 0) { 322 322 modal.querySelector('.modal-content')!.innerHTML = ` 323 323 <p>No posts found in your Bluesky feed.</p> 324 - <button class="button modal-close">Close</button> 324 + <div class="selector-actions"><button class="button button-secondary modal-close">Close</button></div> 325 325 `; 326 326 modal.querySelector('.modal-close')?.addEventListener('click', closeModal); 327 327 return; ··· 331 331 } catch (error) { 332 332 modal.querySelector('.modal-content')!.innerHTML = ` 333 333 <p>Failed to load posts. Please try again.</p> 334 - <button class="button modal-close">Close</button> 334 + <div class="selector-actions"><button class="button button-secondary modal-close">Close</button></div> 335 335 `; 336 336 modal.querySelector('.modal-close')?.addEventListener('click', closeModal); 337 337 } ··· 341 341 modal.querySelector('.modal-content')!.innerHTML = ` 342 342 <div class="welcome-selector"> 343 343 <h2>Select Bluesky Post</h2> 344 - <p>Choose a post to add to your garden</p> 344 + <p>Choose one of your posts to add to your garden</p> 345 345 <div class="post-list"> 346 346 ${posts.map((post) => { 347 347 const uri = post.uri || ''; ··· 349 349 const text = val?.text?.slice(0, 200) || 'Post'; 350 350 const createdAt = val?.createdAt ? new Date(val.createdAt).toLocaleDateString() : ''; 351 351 return ` 352 - <button class="post-item-selectable" data-uri="${uri}"> 352 + <button class="post-item" data-uri="${uri}"> 353 353 <div class="post-content"> 354 - <p class="post-text">${escapeHtml(text)}${text.length >= 200 ? '...' : ''}</p> 354 + <p class="post-text">${escapeHtml(text)}</p> 355 355 ${createdAt ? `<time class="post-date">${createdAt}</time>` : ''} 356 356 </div> 357 357 </button> 358 358 `; 359 359 }).join('')} 360 360 </div> 361 - <button class="button button-secondary modal-close">Cancel</button> 361 + <p>Choose a post from another user to add to your garden</p> 362 + <div class="bsky-user-search"> 363 + <input type="text" class="input" placeholder="@alice.bsky.social" id="bsky-other-handle"> 364 + <button class="button" data-action="search-user">Search another user</button> 365 + </div> 366 + <div class="selector-actions"> 367 + <button class="button button-secondary modal-close">Cancel</button> 368 + </div> 362 369 </div> 363 370 `; 364 371 365 - modal.querySelectorAll('.post-item-selectable').forEach(btn => { 372 + modal.querySelectorAll('.post-item').forEach(btn => { 366 373 btn.addEventListener('click', async () => { 367 374 const uri = btn.getAttribute('data-uri'); 368 375 if (uri) { ··· 376 383 }; 377 384 config.sections = [...(config.sections || []), section]; 378 385 this.renderCallback(); 379 - 380 386 closeModal(); 381 387 } 382 388 }); 389 + }); 390 + 391 + modal.querySelector('[data-action="search-user"]')?.addEventListener('click', async () => { 392 + const input = modal.querySelector<HTMLInputElement>('#bsky-other-handle'); 393 + const handleOrDid = (input?.value || '').trim().replace(/^@/, ''); 394 + if (!handleOrDid) return; 395 + 396 + modal.querySelector('.modal-content')!.innerHTML = ` 397 + <div class="welcome-loading"><div class="spinner"></div><p>Loading posts...</p></div> 398 + `; 399 + 400 + try { 401 + const did = handleOrDid.startsWith('did:') ? handleOrDid : await resolveHandle(handleOrDid); 402 + const posts = await getCollectionRecords(did, 'app.bsky.feed.post', { limit: 50 }); 403 + 404 + if (posts.length === 0) { 405 + modal.querySelector('.modal-content')!.innerHTML = ` 406 + <p>No posts found for that user.</p> 407 + <div class="selector-actions"><button class="button button-secondary modal-close">Close</button></div> 408 + `; 409 + modal.querySelector('.modal-close')?.addEventListener('click', closeModal); 410 + return; 411 + } 412 + 413 + this.renderPostSelector(modal, posts, closeModal); 414 + } catch (error) { 415 + modal.querySelector('.modal-content')!.innerHTML = ` 416 + <p>Failed to load posts. Please check the handle and try again.</p> 417 + <div class="selector-actions"><button class="button button-secondary modal-close">Close</button></div> 418 + `; 419 + modal.querySelector('.modal-close')?.addEventListener('click', closeModal); 420 + } 383 421 }); 384 422 385 423 modal.querySelector('.modal-close')?.addEventListener('click', closeModal);
+35 -9
src/components/welcome-modal.ts
··· 11 11 import { getCollections, getCollectionRecords } from '../records/loader'; 12 12 import { getCurrentDid } from '../oauth'; 13 13 import { addSection, updateConfig, updateTheme, saveConfig } from '../config'; 14 - import { getProfile } from '../at-client'; 14 + import { getProfile, resolveHandle } from '../at-client'; 15 15 import { generateThemeFromDid } from '../themes/engine'; 16 16 import { getSafeHandle, getDisplayHandle } from '../utils/identity'; 17 17 import { createHelpTooltip } from '../utils/help-tooltip'; ··· 218 218 this.showLoading('Loading your Bluesky posts...'); 219 219 220 220 try { 221 - // Use the same method that works in "Explore your data" 222 221 const posts = await getCollectionRecords(this.did, 'app.bsky.feed.post', { limit: 50 }); 223 222 224 223 if (posts.length === 0) { ··· 403 402 content.innerHTML = ` 404 403 <div class="welcome-selector"> 405 404 <h2>Select Bluesky Post</h2> 406 - <p>Choose a post to add to your garden</p> 405 + <p>Choose one of your posts to add to your garden</p> 407 406 <div class="post-list"> 408 - ${posts.map((post, idx) => { 409 - const rkey = post.uri?.split('/').pop() || idx.toString(); 407 + ${posts.map((post) => { 410 408 const uri = post.uri || ''; 411 409 const val = post.value as any; 412 410 const text = val?.text?.slice(0, 200) || 'Post'; ··· 414 412 ? new Date(val.createdAt).toLocaleDateString() 415 413 : ''; 416 414 return ` 417 - <button class="post-item-selectable" data-uri="${uri}" data-rkey="${rkey}"> 415 + <button class="post-item" data-uri="${uri}"> 418 416 <div class="post-content"> 419 - <p class="post-text">${this.escapeHtml(text)}${text.length >= 200 ? '...' : ''}</p> 417 + <p class="post-text">${this.escapeHtml(text)}</p> 420 418 ${createdAt ? `<time class="post-date">${createdAt}</time>` : ''} 421 419 </div> 422 420 </button> 423 421 `; 424 422 }).join('')} 423 + </div> 424 + <p>Choose a post from another user to add to your garden</p> 425 + <div class="bsky-user-search"> 426 + <input type="text" class="input" placeholder="@alice.bsky.social" id="bsky-other-handle"> 427 + <button class="button" data-action="search-user">Search another user</button> 425 428 </div> 426 429 <div class="selector-actions"> 427 430 <button class="button button-secondary" data-action="back-main">Back</button> ··· 429 432 </div> 430 433 `; 431 434 432 - // Attach event listeners - clicking a post adds it 433 - content.querySelectorAll('.post-item-selectable').forEach(btn => { 435 + content.querySelectorAll('.post-item').forEach(btn => { 434 436 btn.addEventListener('click', () => { 435 437 const uri = btn.getAttribute('data-uri'); 436 438 if (uri) { 437 439 this.addSinglePost(uri); 438 440 } 439 441 }); 442 + }); 443 + 444 + content.querySelector('[data-action="search-user"]')?.addEventListener('click', async () => { 445 + const input = content.querySelector<HTMLInputElement>('#bsky-other-handle'); 446 + const handleOrDid = (input?.value || '').trim().replace(/^@/, ''); 447 + if (!handleOrDid) return; 448 + 449 + this.showLoading('Loading posts...'); 450 + 451 + try { 452 + const did = handleOrDid.startsWith('did:') ? handleOrDid : await resolveHandle(handleOrDid); 453 + const newPosts = await getCollectionRecords(did, 'app.bsky.feed.post', { limit: 50 }); 454 + 455 + if (newPosts.length === 0) { 456 + this.showMessage('No posts found for that user.'); 457 + return; 458 + } 459 + 460 + this.showPostSelector(newPosts); 461 + } catch (error) { 462 + console.error('Failed to load posts:', error); 463 + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 464 + this.showMessage(`Failed to load posts: ${errorMessage}. Please check the handle and try again.`); 465 + } 440 466 }); 441 467 442 468 content.querySelector('[data-action="back-main"]')?.addEventListener('click', () => {
+59 -4
src/themes/base.css
··· 2414 2414 .record-item, 2415 2415 .post-item { 2416 2416 display: flex; 2417 - align-items: center; 2418 2417 gap: var(--spacing-md); 2419 2418 padding: var(--spacing-md); 2420 2419 background: var(--color-surface); ··· 2426 2425 text-align: left; 2427 2426 min-width: 0; 2428 2427 max-width: 100%; 2428 + } 2429 + 2430 + .collection-item, 2431 + .record-item { 2432 + align-items: center; 2433 + } 2434 + 2435 + .post-item { 2436 + align-items: flex-start; 2429 2437 } 2430 2438 2431 2439 .collection-item:hover, ··· 2466 2474 flex-direction: column; 2467 2475 gap: var(--spacing-xs); 2468 2476 min-width: 0; 2469 - overflow: hidden; 2470 2477 } 2471 2478 2472 2479 .post-text { 2473 2480 margin: 0; 2474 2481 line-height: 1.5; 2475 - word-break: break-word; 2476 - overflow-wrap: break-word; 2482 + display: -webkit-box; 2483 + -webkit-line-clamp: 3; 2484 + -webkit-box-orient: vertical; 2485 + overflow: hidden; 2477 2486 } 2478 2487 2479 2488 .post-date { ··· 2524 2533 display: flex; 2525 2534 gap: var(--spacing-md); 2526 2535 justify-content: flex-end; 2536 + } 2537 + 2538 + .bsky-user-search { 2539 + display: flex; 2540 + gap: var(--spacing-sm); 2541 + align-items: stretch; 2542 + margin-bottom: var(--spacing-md); 2543 + } 2544 + 2545 + .bsky-user-search .input { 2546 + flex: 1; 2547 + min-width: 0; 2548 + } 2549 + 2550 + .bsky-user-search .button { 2551 + white-space: nowrap; 2552 + } 2553 + 2554 + /* Post selector mobile: add side padding to elements that lose it when modal-content padding is 0 */ 2555 + @media (max-width: 479px) { 2556 + .modal-content .welcome-selector p { 2557 + padding-left: var(--spacing-md); 2558 + padding-right: var(--spacing-md); 2559 + } 2560 + 2561 + .modal-content .bsky-user-search { 2562 + flex-direction: column; 2563 + align-items: stretch; 2564 + padding: 0 var(--spacing-md); 2565 + } 2566 + 2567 + .modal-content .bsky-user-search .button { 2568 + width: 100%; 2569 + min-height: 44px; 2570 + justify-content: center; 2571 + } 2572 + 2573 + .modal-content .selector-actions { 2574 + padding: 0 var(--spacing-md) var(--spacing-lg); 2575 + } 2576 + 2577 + .modal-content .selector-actions .button { 2578 + width: 100%; 2579 + min-height: 44px; 2580 + justify-content: center; 2581 + } 2527 2582 } 2528 2583 2529 2584 /* ============================================