This repository has no description
0

Configure Feed

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

fetch more data

+183 -61
+27 -1
src/components/CollectionsFeed/ActivityChart.css
··· 56 56 opacity: 0.8; 57 57 } 58 58 59 - .activity-chart-empty { 59 + .activity-chart-empty, 60 + .activity-chart-loading { 60 61 height: 300px; 61 62 display: flex; 62 63 justify-content: center; ··· 66 67 border: 1px dashed var(--card-border); 67 68 border-radius: 6px; 68 69 margin: 15px 0; 70 + } 71 + 72 + .activity-chart-loading { 73 + flex-direction: column; 74 + } 75 + 76 + .chart-loading-note { 77 + font-size: 0.8rem; 78 + opacity: 0.7; 79 + margin-top: 5px; 80 + } 81 + 82 + .chart-loading-spinner { 83 + border: 3px solid rgba(var(--button-bg-rgb), 0.3); 84 + border-radius: 50%; 85 + border-top: 3px solid var(--button-bg); 86 + width: 30px; 87 + height: 30px; 88 + animation: spin 1s linear infinite; 89 + margin-bottom: 10px; 90 + } 91 + 92 + @keyframes spin { 93 + 0% { transform: rotate(0deg); } 94 + 100% { transform: rotate(360deg); } 69 95 } 70 96 71 97 @media (max-width: 768px) {
+14 -1
src/components/CollectionsFeed/ActivityChart.js
··· 21 21 Legend 22 22 ); 23 23 24 - const ActivityChart = ({ records, collections }) => { 24 + const ActivityChart = ({ records, collections, loading = false }) => { 25 25 const [timePeriod, setTimePeriod] = useState('7days'); 26 26 const [chartData, setChartData] = useState({ 27 27 labels: [], ··· 312 312 } 313 313 } 314 314 }; 315 + 316 + // Show loading state when fetching records deeply 317 + if (loading) { 318 + return ( 319 + <div className="activity-chart-container"> 320 + <div className="activity-chart-loading"> 321 + <div className="chart-loading-spinner"></div> 322 + <p>Loading record data for visualization...</p> 323 + <p className="chart-loading-note">This may take a moment for accounts with many records</p> 324 + </div> 325 + </div> 326 + ); 327 + } 315 328 316 329 // Ensure records array exists and has items 317 330 if (!records || records.length === 0) {
+142 -59
src/components/CollectionsFeed/CollectionsFeed.js
··· 23 23 const [allRecordsForChart, setAllRecordsForChart] = useState([]); // All records for chart visualization 24 24 const [loading, setLoading] = useState(false); 25 25 const [initialLoad, setInitialLoad] = useState(true); 26 + const [chartLoading, setChartLoading] = useState(false); // Separate loading state for chart data 26 27 const [error, setError] = useState(''); 27 28 const [collectionCursors, setCollectionCursors] = useState({}); 28 29 const [fetchingMore, setFetchingMore] = useState(false); ··· 181 182 182 183 // Array to store all fetched records 183 184 let allRecords = isLoadMore ? [...records] : []; 185 + let allChartRecords = isLoadMore ? [...allRecordsForChart] : []; 184 186 const newCursors = { ...collectionCursors }; 185 187 186 188 // Calculate a cutoff date (90 days ago by default for chart visualization) 187 189 const cutoffDate = new Date(); 188 190 cutoffDate.setDate(cutoffDate.getDate() - 90); // 3 months 189 191 190 - // Fetch records for each collection in parallel 191 - const fetchPromises = collectionsList.map(async (collection) => { 192 - // Fetch up to 100 records per collection to ensure we get a good sample 193 - let url = `${endpoint}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(userDid)}&collection=${encodeURIComponent(collection)}&limit=100`; 192 + // Track if this is the initial load for charting purposes 193 + const isInitialLoad = !isLoadMore && allRecordsForChart.length === 0; 194 + 195 + // Set chart loading state if we're doing deep pagination for the chart 196 + if (isInitialLoad) { 197 + setChartLoading(true); 198 + } 199 + 200 + // Fetch records for each collection 201 + for (const collection of collectionsList) { 202 + let hasMoreRecords = true; 203 + let cursor = isLoadMore ? newCursors[collection] : null; 204 + let pageCount = 0; 205 + let collectionRecords = []; 206 + let reachedCutoff = false; 194 207 195 - // Add cursor if loading more and we have a cursor for this collection 196 - if (isLoadMore && newCursors[collection]) { 197 - url += `&cursor=${encodeURIComponent(newCursors[collection])}`; 198 - } 208 + // For initial load, we need to do deep pagination to get all data for charting 209 + // For load more, we just get the next page 210 + const maxPages = isInitialLoad ? 50 : 1; // Limit for safety 199 211 200 - const response = await fetch(url); 201 - 202 - if (!response.ok) { 203 - console.error(`Error fetching records for ${collection}: ${response.statusText}`); 204 - return []; 212 + while (hasMoreRecords && pageCount < maxPages && !reachedCutoff) { 213 + // Fetch up to 100 records per page 214 + let url = `${endpoint}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(userDid)}&collection=${encodeURIComponent(collection)}&limit=100`; 215 + 216 + // Add cursor if we have one 217 + if (cursor) { 218 + url += `&cursor=${encodeURIComponent(cursor)}`; 219 + } 220 + 221 + console.log(`Fetching ${collection} page ${pageCount + 1}${cursor ? ' with cursor' : ''}`); 222 + 223 + const response = await fetch(url); 224 + 225 + if (!response.ok) { 226 + console.error(`Error fetching records for ${collection}: ${response.statusText}`); 227 + break; 228 + } 229 + 230 + const data = await response.json(); 231 + pageCount++; 232 + 233 + // Process records from this page 234 + if (data.records && data.records.length > 0) { 235 + const processedRecords = data.records.map(record => { 236 + const contentTimestamp = extractTimestamp(record); 237 + const rkey = record.uri.split('/').pop(); 238 + const rkeyTimestamp = tidToTimestamp(rkey); 239 + 240 + return { 241 + ...record, 242 + collection, 243 + collectionType: record.value?.$type || collection, 244 + contentTimestamp, 245 + rkeyTimestamp, 246 + rkey, 247 + }; 248 + }); 249 + 250 + // Check if we've reached older records 251 + if (isInitialLoad) { 252 + // For chart data, determine if any records are beyond our cutoff 253 + const oldestRecordTime = processedRecords.reduce((oldest, record) => { 254 + const timestamp = record.contentTimestamp || record.rkeyTimestamp; 255 + if (!timestamp) return oldest; 256 + 257 + const recordTime = new Date(timestamp).getTime(); 258 + return recordTime < oldest ? recordTime : oldest; 259 + }, Date.now()); 260 + 261 + // If the oldest record on this page is older than our cutoff, we can stop 262 + if (oldestRecordTime < cutoffDate.getTime()) { 263 + reachedCutoff = true; 264 + console.log(`Reached cutoff date for ${collection} on page ${pageCount}`); 265 + 266 + // Filter records from this page to only include those after cutoff 267 + const filteredRecords = processedRecords.filter(record => { 268 + const timestamp = record.contentTimestamp || record.rkeyTimestamp; 269 + if (!timestamp) return false; 270 + return new Date(timestamp) >= cutoffDate; 271 + }); 272 + 273 + collectionRecords.push(...filteredRecords); 274 + } else { 275 + // All records on this page are within our date range 276 + collectionRecords.push(...processedRecords); 277 + } 278 + } else { 279 + // For regular timeline browsing, just add all records 280 + collectionRecords.push(...processedRecords); 281 + } 282 + } 283 + 284 + // Check if there are more pages 285 + if (data.cursor) { 286 + cursor = data.cursor; 287 + } else { 288 + hasMoreRecords = false; 289 + } 290 + 291 + // If we're not doing initial load for chart, break after first page 292 + if (!isInitialLoad) { 293 + break; 294 + } 205 295 } 206 296 207 - const data = await response.json(); 208 - 209 - // Save cursor for next pagination 210 - if (data.cursor) { 211 - newCursors[collection] = data.cursor; 297 + // Save the cursor for this collection for future pagination 298 + if (cursor) { 299 + newCursors[collection] = cursor; 212 300 } else { 213 301 // No more records for this collection 214 302 delete newCursors[collection]; 215 303 } 216 304 217 - // Process and format records 218 - return data.records.map(record => { 219 - const contentTimestamp = extractTimestamp(record); 220 - const rkey = record.uri.split('/').pop(); 221 - const rkeyTimestamp = tidToTimestamp(rkey); 222 - 223 - return { 224 - ...record, 225 - collection, 226 - collectionType: record.value?.$type || collection, 227 - contentTimestamp, 228 - rkeyTimestamp, 229 - // We'll decide which timestamp to use when filtering/sorting 230 - rkey, 231 - }; 232 - }); 233 - }); 305 + // Add this collection's records to our overall array 306 + if (isInitialLoad) { 307 + // For chart initialization, add all records to chart data 308 + allChartRecords = [...allChartRecords, ...collectionRecords]; 309 + } else if (isLoadMore) { 310 + // For load more, only add to both arrays if within display limit 311 + allChartRecords = [...allChartRecords, ...collectionRecords]; 312 + allRecords = [...allRecords, ...collectionRecords]; 313 + } else { 314 + // For regular display, add to both 315 + allChartRecords = [...allChartRecords, ...collectionRecords]; 316 + allRecords = [...allRecords, ...collectionRecords]; 317 + } 318 + } 234 319 235 - // Wait for all fetch operations to complete 236 - const collectionRecords = await Promise.all(fetchPromises); 237 - 238 - // Combine and flatten records from all collections 239 - collectionRecords.forEach(records => { 240 - allRecords = [...allRecords, ...records]; 241 - }); 242 - 243 - // Filter and sort records based on the selected timestamp source 244 - allRecords = allRecords.filter(record => { 320 + // Filter and sort all records based on the selected timestamp source 321 + const filteredChartRecords = allChartRecords.filter(record => { 245 322 if (useRkeyTimestamp) { 246 - // When using rkey timestamps, include all records (all valid TIDs should have timestamps) 247 323 return record.rkeyTimestamp !== null; 248 324 } else { 249 - // When using content timestamps, only include records with valid content timestamps 250 325 return record.contentTimestamp !== null; 251 326 } 252 - }).sort((a, b) => { 253 - // Sort based on the selected timestamp type 327 + }); 328 + 329 + // Sort by timestamp (newest first) 330 + const sortedChartRecords = [...filteredChartRecords].sort((a, b) => { 254 331 const aTime = useRkeyTimestamp ? a.rkeyTimestamp : a.contentTimestamp; 255 332 const bTime = useRkeyTimestamp ? b.rkeyTimestamp : b.contentTimestamp; 256 - 257 - return new Date(bTime) - new Date(aTime); // Newest first 333 + return new Date(bTime) - new Date(aTime); 258 334 }); 259 335 260 - // We'll keep all records for charting purposes, but only show the most recent ones in the timeline 261 - const chartRecords = [...allRecords]; 262 - 263 - // If not loading more, limit to 20 most recent records for initial display in timeline 336 + // For timeline display, limit records to most recent ones 337 + let displayRecords = [...sortedChartRecords]; 264 338 if (!isLoadMore) { 265 - allRecords = allRecords.slice(0, 20); 339 + displayRecords = displayRecords.slice(0, 20); 266 340 } 267 341 268 - // Update state with both the display records and the full set for charting 269 - setRecords(allRecords); 270 - setAllRecordsForChart(prev => isLoadMore ? [...prev, ...chartRecords] : chartRecords); 342 + console.log(`Fetched total of ${sortedChartRecords.length} records for chart, displaying ${displayRecords.length}`); 343 + 344 + // Update state 345 + setRecords(displayRecords); 346 + setAllRecordsForChart(sortedChartRecords); 271 347 setCollectionCursors(newCursors); 272 348 setFetchingMore(false); 349 + 350 + // Turn off chart loading if it was on 351 + if (chartLoading) { 352 + setChartLoading(false); 353 + } 273 354 } catch (err) { 274 355 console.error('Error fetching collection records:', err); 275 356 setError('Failed to fetch records. Please try again.'); 276 357 setFetchingMore(false); 358 + setChartLoading(false); // Make sure we turn off chart loading on error 277 359 } 278 360 }; 279 361 ··· 392 474 <ActivityChart 393 475 records={allRecordsForChart} 394 476 collections={collections} 477 + loading={chartLoading} 395 478 /> 396 479 397 480 <div className="feed-controls">