This repository has no description
0

Configure Feed

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

add copy paste feature and color differences

+316 -15
+10 -3
src/components/CollectionsFeed/ActivityChart.js
··· 35 35 const atprotoBorderColor = 'rgba(0, 51, 102, 1)'; 36 36 37 37 useEffect(() => { 38 - // Only generate chart data if we have records and we're not loading 39 - if (records && records.length > 0 && !loading) { 38 + // Generate chart data whenever records change or time period changes 39 + // Don't wait for loading to be false - this ensures we regenerate as soon as we get new data 40 + if (records && records.length > 0) { 40 41 console.log(`Generating chart data for ${records.length} records with period ${timePeriod}`); 41 42 generateChartData(records, timePeriod); 43 + } else if (!loading) { 44 + // If we have no records and we're not loading, reset chart data 45 + setChartData({ 46 + labels: [], 47 + datasets: [] 48 + }); 42 49 } 43 - }, [records, timePeriod, loading]); 50 + }, [records, timePeriod]); 44 51 45 52 // Function to generate data for the chart based on selected time period 46 53 const generateChartData = (allRecords, period) => {
+62
src/components/CollectionsFeed/CollectionsFeed.css
··· 62 62 opacity: 0.8; 63 63 } 64 64 65 + .user-did, 66 + .user-endpoint { 67 + font-size: 0.8rem; 68 + margin: 3px 0 0; 69 + color: var(--text); 70 + opacity: 0.6; 71 + word-break: break-all; 72 + font-family: monospace; 73 + display: flex; 74 + align-items: center; 75 + justify-content: center; 76 + gap: 8px; 77 + } 78 + 79 + .copy-button { 80 + background: none; 81 + border: none; 82 + cursor: pointer; 83 + display: flex; 84 + align-items: center; 85 + justify-content: center; 86 + padding: 3px; 87 + border-radius: 4px; 88 + color: var(--text); 89 + opacity: 0.5; 90 + transition: opacity 0.2s, background-color 0.2s; 91 + } 92 + 93 + .copy-button:hover { 94 + opacity: 1; 95 + background-color: rgba(0, 0, 0, 0.05); 96 + } 97 + 98 + .dark-mode .copy-button:hover { 99 + background-color: rgba(255, 255, 255, 0.1); 100 + } 101 + 102 + .copy-button.copied { 103 + color: #4CAF50; 104 + opacity: 1; 105 + position: relative; 106 + } 107 + 108 + .copy-button.copied::after { 109 + content: "Copied!"; 110 + position: absolute; 111 + bottom: -20px; 112 + left: 50%; 113 + transform: translateX(-50%); 114 + background-color: var(--button-bg); 115 + color: white; 116 + padding: 3px 8px; 117 + border-radius: 4px; 118 + font-size: 0.7rem; 119 + white-space: nowrap; 120 + } 121 + 65 122 /* Feed Controls Styles */ 66 123 .feed-controls { 67 124 display: flex; ··· 423 480 424 481 .user-header h2 { 425 482 font-size: 1rem; 483 + } 484 + 485 + .user-did, 486 + .user-endpoint { 487 + font-size: 0.7rem; 426 488 } 427 489 }
+102 -6
src/components/CollectionsFeed/CollectionsFeed.js
··· 196 196 try { 197 197 setFetchingMore(isLoadMore); 198 198 199 - // If this is an initial load for chart data, set chartLoading 200 - if (!isLoadMore) { 199 + // Set chartLoading for initial load or when refreshing 200 + if (!isLoadMore || collectionsList.length > 0) { 201 201 setChartLoading(true); 202 - console.log("Setting chart loading to TRUE"); 202 + console.log(`Setting chart loading to TRUE for ${isLoadMore ? 'refresh' : 'initial load'}`); 203 203 } 204 204 205 205 // Array to store all fetched records ··· 408 408 409 409 console.log(`Fetched ${sortedChartRecords.length} total records for chart, showing ${displayRecords.length} in timeline`); 410 410 411 + // In the case of a refresh, sortedChartRecords only contains fresh data for selected collections 412 + // We need to merge this with any existing data for other collections 413 + const existingRecordsToKeep = isLoadMore ? [] : allRecordsForChart.filter(record => 414 + !collectionsList.includes(record.collection) 415 + ); 416 + 417 + // Variable to hold our final merged records 418 + let mergedRecords; 419 + 420 + // For refresh, remove old data for the collections we just refreshed 421 + if (!isLoadMore) { 422 + console.log(`Removing old data for refreshed collections: ${collectionsList.join(', ')}`); 423 + 424 + // Count for stats 425 + const beforeCount = existingRecordsToKeep.length + sortedChartRecords.length; 426 + 427 + // Remove duplicates that might exist in both arrays 428 + // This can happen if we refreshed a collection we already had data for 429 + const uniqueNewRecords = sortedChartRecords.filter(newRecord => { 430 + // Check if this record has the exact same URI as an existing record 431 + return !existingRecordsToKeep.some(existingRecord => 432 + existingRecord.uri === newRecord.uri 433 + ); 434 + }); 435 + 436 + // Merge fresh data with existing data for other collections 437 + mergedRecords = [...existingRecordsToKeep, ...uniqueNewRecords]; 438 + console.log(`Merged ${existingRecordsToKeep.length} existing records with ${uniqueNewRecords.length} new unique records`); 439 + console.log(`Total records: ${beforeCount} before deduplication, ${mergedRecords.length} after`); 440 + } 441 + else { 442 + // For load more, just add all the new records 443 + mergedRecords = [...existingRecordsToKeep, ...sortedChartRecords]; 444 + } 445 + 446 + console.log(`Final chart dataset size: ${mergedRecords.length} records`); 447 + 411 448 // Update state 412 449 setRecords(displayRecords); 413 - setAllRecordsForChart(sortedChartRecords); 450 + setAllRecordsForChart(mergedRecords); 414 451 setCollectionCursors(newCursors); 415 452 setFetchingMore(false); 416 453 ··· 454 491 // Handle refresh button click 455 492 const handleRefresh = async () => { 456 493 if (did && serviceEndpoint) { 457 - await fetchCollectionRecords(did, serviceEndpoint, selectedCollections); 494 + console.log("Refresh requested for selected collections:", selectedCollections); 495 + 496 + // Set loading state for chart to true 497 + setChartLoading(true); 498 + 499 + try { 500 + // The fetchCollectionRecords function will handle merging the new data 501 + // with existing data for other collections 502 + await fetchCollectionRecords(did, serviceEndpoint, selectedCollections, false); 503 + 504 + console.log("Refresh completed successfully"); 505 + } catch (err) { 506 + console.error("Error during refresh:", err); 507 + setError('Failed to refresh records. Please try again.'); 508 + } finally { 509 + // Ensure loading state is reset 510 + setChartLoading(false); 511 + } 458 512 } 459 513 }; 460 514 ··· 574 628 <div className="user-header"> 575 629 <h1>{displayName}</h1> 576 630 <h2>@{handle}</h2> 631 + {did && ( 632 + <div className="user-did"> 633 + <span>DID: {did}</span> 634 + <button 635 + className="copy-button" 636 + onClick={() => { 637 + navigator.clipboard.writeText(did); 638 + // Show temporary success message 639 + const button = event.currentTarget; 640 + button.classList.add('copied'); 641 + setTimeout(() => button.classList.remove('copied'), 2000); 642 + }} 643 + title="Copy DID" 644 + > 645 + <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> 646 + <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect> 647 + <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path> 648 + </svg> 649 + </button> 650 + </div> 651 + )} 652 + {serviceEndpoint && ( 653 + <div className="user-endpoint"> 654 + <span>Service: {serviceEndpoint}</span> 655 + <button 656 + className="copy-button" 657 + onClick={() => { 658 + navigator.clipboard.writeText(serviceEndpoint); 659 + // Show temporary success message 660 + const button = event.currentTarget; 661 + button.classList.add('copied'); 662 + setTimeout(() => button.classList.remove('copied'), 2000); 663 + }} 664 + title="Copy Service Endpoint" 665 + > 666 + <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> 667 + <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect> 668 + <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path> 669 + </svg> 670 + </button> 671 + </div> 672 + )} 577 673 </div> 578 674 579 675 {/* Activity Chart */} ··· 581 677 records={filteredChartRecords} 582 678 collections={selectedCollections} 583 679 loading={chartLoading} 584 - key={`chart-${chartLoading}-${filteredChartRecords.length}-${selectedCollections.join(',')}`} // Add a key to force re-render 680 + key={`chart-${Date.now()}-${filteredChartRecords.length}-${selectedCollections.join(',')}`} // Use timestamp in key to ensure re-render on refresh 585 681 /> 586 682 587 683 <div className="feed-controls">
+90 -1
src/components/CollectionsFeed/FeedTimeline.css
··· 40 40 color: var(--button-bg); 41 41 } 42 42 43 + /* Different styling for Bluesky vs ATProto collections */ 44 + .bsky-collection { 45 + color: #0085ff; /* Lighter blue for Bluesky */ 46 + } 47 + 48 + .atproto-collection { 49 + color: #003366; /* Darker blue for ATProto */ 50 + } 51 + 52 + /* Different border styling for items */ 53 + .bsky-item { 54 + border-color: rgba(0, 133, 255, 0.5); /* Lighter blue for Bluesky */ 55 + } 56 + 57 + .atproto-item { 58 + border-color: rgba(0, 51, 102, 0.5); /* Darker blue for ATProto */ 59 + } 60 + 43 61 .collection-full { 44 62 margin-left: 8px; 45 63 font-size: 0.8rem; ··· 243 261 color: #b39ddb; 244 262 } 245 263 246 - .record-json { 264 + .record-json-container { 247 265 background-color: var(--background); 248 266 border-radius: 5px; 267 + overflow: hidden; 268 + } 269 + 270 + .record-json-header { 271 + display: flex; 272 + justify-content: space-between; 273 + align-items: center; 274 + padding: 8px 15px; 275 + background-color: rgba(0, 0, 0, 0.05); 276 + border-bottom: 1px solid var(--card-border); 277 + } 278 + 279 + .dark-mode .record-json-header { 280 + background-color: rgba(255, 255, 255, 0.05); 281 + } 282 + 283 + .copy-json-button { 284 + display: flex; 285 + align-items: center; 286 + gap: 6px; 287 + background: none; 288 + border: 1px solid var(--card-border); 289 + border-radius: 4px; 290 + padding: 4px 10px; 291 + font-size: 0.8rem; 292 + cursor: pointer; 293 + color: var(--text); 294 + transition: background-color 0.2s, color 0.2s; 295 + } 296 + 297 + .copy-json-button:hover { 298 + background-color: var(--button-bg); 299 + color: white; 300 + } 301 + 302 + .copy-json-button.copied { 303 + background-color: #4CAF50; 304 + color: white; 305 + border-color: #4CAF50; 306 + } 307 + 308 + .copy-json-button.copied::after { 309 + content: "✓"; 310 + margin-left: 5px; 311 + } 312 + 313 + .record-json { 249 314 padding: 15px; 250 315 font-family: monospace; 251 316 white-space: pre-wrap; ··· 254 319 overflow-y: auto; 255 320 } 256 321 322 + .record-uri { 323 + display: flex; 324 + align-items: center; 325 + gap: 8px; 326 + font-family: monospace; 327 + font-size: 0.9rem; 328 + } 329 + 257 330 /* Dark mode overrides */ 258 331 .dark-mode .feed-item { 259 332 background: #2c2c2c; 260 333 border: 5px solid #444; 261 334 box-shadow: 0 2px 4px rgba(0,0,0,0.6); 335 + } 336 + 337 + .dark-mode .bsky-collection { 338 + color: #4da8ff; /* Lighter blue for Bluesky in dark mode */ 339 + } 340 + 341 + .dark-mode .atproto-collection { 342 + color: #6699cc; /* Slightly lighter blue for ATProto in dark mode */ 343 + } 344 + 345 + .dark-mode .bsky-item { 346 + border-color: rgba(77, 168, 255, 0.5); /* Lighter blue for Bluesky in dark mode */ 347 + } 348 + 349 + .dark-mode .atproto-item { 350 + border-color: rgba(102, 153, 204, 0.5); /* Darker blue for ATProto in dark mode */ 262 351 } 263 352 264 353 .dark-mode .feed-item-header,
+52 -5
src/components/CollectionsFeed/FeedTimeline.js
··· 250 250 const content = getRecordContent(record); 251 251 252 252 return ( 253 - <div key={`${record.collection}-${record.rkey}-${index}`} className="feed-item"> 253 + <div 254 + key={`${record.collection}-${record.rkey}-${index}`} 255 + className={`feed-item ${record.collection.startsWith('app.bsky.') ? 'bsky-item' : 'atproto-item'}`} 256 + > 254 257 <div className="feed-item-header"> 255 258 <div className="collection-type"> 256 - <span className="collection-name">{record.collection.split('.').pop()}</span> 259 + <span 260 + className={`collection-name ${record.collection.startsWith('app.bsky.') ? 'bsky-collection' : 'atproto-collection'}`} 261 + > 262 + {record.collection.split('.').pop()} 263 + </span> 257 264 <span className="collection-full">{record.collection}</span> 258 265 </div> 259 266 <div ··· 382 389 )} 383 390 384 391 {modalData && !modalLoading && !modalError && ( 385 - <div className="record-json"> 386 - {JSON.stringify(modalData, null, 2)} 392 + <div className="record-json-container"> 393 + <div className="record-json-header"> 394 + <span>Record Data</span> 395 + <button 396 + className="copy-json-button" 397 + onClick={() => { 398 + navigator.clipboard.writeText(JSON.stringify(modalData, null, 2)); 399 + // Show temporary success message 400 + const button = event.currentTarget; 401 + button.classList.add('copied'); 402 + setTimeout(() => button.classList.remove('copied'), 2000); 403 + }} 404 + title="Copy JSON" 405 + > 406 + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> 407 + <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect> 408 + <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path> 409 + </svg> 410 + Copy JSON 411 + </button> 412 + </div> 413 + <div className="record-json"> 414 + {JSON.stringify(modalData, null, 2)} 415 + </div> 387 416 </div> 388 417 )} 389 418 </div> 390 419 391 420 <div className="record-modal-footer"> 392 - <span>URI: {selectedRecord.uri}</span> 421 + <div className="record-uri"> 422 + <span>URI: {selectedRecord.uri}</span> 423 + <button 424 + className="copy-button" 425 + onClick={() => { 426 + navigator.clipboard.writeText(selectedRecord.uri); 427 + // Show temporary success message 428 + const button = event.currentTarget; 429 + button.classList.add('copied'); 430 + setTimeout(() => button.classList.remove('copied'), 2000); 431 + }} 432 + title="Copy URI" 433 + > 434 + <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> 435 + <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect> 436 + <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path> 437 + </svg> 438 + </button> 439 + </div> 393 440 </div> 394 441 </div> 395 442 </div>