mirror of
https://github.com/DocNR/POWR.git
synced 2025-06-04 00:02:06 +00:00
social feed wip, still having loading issues with following feed
This commit is contained in:
parent
8c6a7ba810
commit
2316a93dc2
57
CHANGELOG.md
57
CHANGELOG.md
@ -17,6 +17,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Planned exercise library and template caching
|
||||
- Designed contact list and following caching
|
||||
- Outlined general media cache service
|
||||
- Created detailed testing strategy documentation
|
||||
- Implemented ProfileImageCache service with NDK integration
|
||||
- Enhanced UserAvatar component to use cached profile images
|
||||
- Updated EnhancedSocialPost to use UserAvatar for profile images
|
||||
- Fixed NDK initialization to properly set NDK in ProfileImageCache
|
||||
- Removed draft articles (kind 30024) from all feeds
|
||||
|
||||
## Fixed
|
||||
- Social feed subscription issues
|
||||
- Consolidated multiple separate subscriptions into a single subscription
|
||||
- Fixed infinite subscription loop in POWR feed
|
||||
- Removed tag filtering for POWR account to ensure all content is displayed
|
||||
- Improved timestamp handling to prevent continuous resubscription
|
||||
- Enhanced logging for better debugging
|
||||
- Removed old feed implementation in favor of unified useSocialFeed hook
|
||||
- Added proper subscription cleanup to prevent memory leaks
|
||||
- Implemented write buffer system to prevent transaction conflicts
|
||||
- Added LRU cache for tracking known events to prevent duplicates
|
||||
- Improved transaction management with withTransactionAsync
|
||||
- Added debounced subscriptions to prevent rapid resubscriptions
|
||||
- Enhanced error handling to prevent cascading failures
|
||||
- Added proper initialization of SocialFeedCache in RelayInitializer
|
||||
- Enhanced Social Feed Filtering
|
||||
- Updated feed filtering rules to focus on fitness-related content
|
||||
- Implemented consistent tag-based filtering across all feeds
|
||||
- Added comprehensive fitness tag list (#workout, #fitness, #powr, etc.)
|
||||
- Removed article drafts (kind 30024) from all feeds
|
||||
- Created detailed documentation for feed filtering rules
|
||||
- Enhanced POWR feed to only show published content
|
||||
- Updated Community feed (formerly Global) with better content focus
|
||||
- Improved Following feed with consistent filtering rules
|
||||
- Social Feed Caching Implementation
|
||||
- Created SocialFeedCache service for storing feed events
|
||||
- Enhanced SocialFeedService to use cache for offline access
|
||||
- Updated useSocialFeed hook to handle offline mode
|
||||
- Added offline indicator in social feed UI
|
||||
- Implemented automatic caching of viewed feed events
|
||||
- Added cache for referenced content (quoted posts)
|
||||
- Created documentation for social feed caching architecture
|
||||
- Profile Image Caching Implementation
|
||||
- Created ProfileImageCache service for storing user avatars
|
||||
- Enhanced UserAvatar component to use cached images
|
||||
- Implemented automatic caching of viewed profile images
|
||||
- Added fallback to cached images when offline
|
||||
- Improved image loading performance with cache-first approach
|
||||
- Added cache expiration management for profile images
|
||||
- Enhanced offline functionality
|
||||
- Added OfflineIndicator component for app-wide status display
|
||||
- Created SocialOfflineState component for graceful social feed degradation
|
||||
@ -26,8 +72,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Implemented graceful fallbacks for unavailable content
|
||||
- Added cached data display when offline
|
||||
- Created user-friendly offline messaging
|
||||
- Added automatic switching between online and offline data sources
|
||||
|
||||
## Improved
|
||||
- Social feed performance and reliability
|
||||
- Added SQLite-based caching for feed events
|
||||
- Implemented feed type tracking (following, powr, global)
|
||||
- Enhanced event processing with cache-first approach
|
||||
- Added automatic cache expiration (7-day default)
|
||||
- Improved referenced content resolution with caching
|
||||
- Enhanced offline user experience with cached content
|
||||
- Added connectivity-aware component rendering
|
||||
- Implemented automatic mode switching based on connectivity
|
||||
|
||||
- Splash screen reliability
|
||||
- Enhanced SimpleSplashScreen with better error handling
|
||||
- Improved platform detection for video vs. static splash
|
||||
|
@ -9,8 +9,15 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import NostrLoginSheet from '@/components/sheets/NostrLoginSheet';
|
||||
import EnhancedSocialPost from '@/components/social/EnhancedSocialPost';
|
||||
import EmptyFeed from '@/components/social/EmptyFeed';
|
||||
import { useUserActivityFeed } from '@/lib/hooks/useFeedHooks';
|
||||
import { AnyFeedEntry } from '@/types/feed';
|
||||
import { useSocialFeed } from '@/lib/hooks/useSocialFeed';
|
||||
import {
|
||||
AnyFeedEntry,
|
||||
WorkoutFeedEntry,
|
||||
ExerciseFeedEntry,
|
||||
TemplateFeedEntry,
|
||||
SocialFeedEntry,
|
||||
ArticleFeedEntry
|
||||
} from '@/types/feed';
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { QrCode, Mail, Copy } from 'lucide-react-native';
|
||||
@ -35,12 +42,79 @@ export default function OverviewScreen() {
|
||||
const theme = useTheme() as CustomTheme;
|
||||
const { currentUser, isAuthenticated } = useNDKCurrentUser();
|
||||
const [isLoginSheetOpen, setIsLoginSheetOpen] = useState(false);
|
||||
// Use useSocialFeed with the profile feed type
|
||||
const {
|
||||
entries,
|
||||
feedItems,
|
||||
loading,
|
||||
resetFeed,
|
||||
hasContent
|
||||
} = useUserActivityFeed();
|
||||
refresh,
|
||||
isOffline
|
||||
} = useSocialFeed({
|
||||
feedType: 'profile',
|
||||
authors: currentUser?.pubkey ? [currentUser.pubkey] : undefined,
|
||||
limit: 30
|
||||
});
|
||||
|
||||
// Convert to the format expected by the component
|
||||
const entries = React.useMemo(() => {
|
||||
return feedItems.map(item => {
|
||||
// Create a properly typed AnyFeedEntry based on the item type
|
||||
const baseEntry = {
|
||||
id: item.id,
|
||||
eventId: item.id,
|
||||
event: item.originalEvent,
|
||||
timestamp: item.createdAt * 1000,
|
||||
};
|
||||
|
||||
// Add type-specific properties
|
||||
switch (item.type) {
|
||||
case 'workout':
|
||||
return {
|
||||
...baseEntry,
|
||||
type: 'workout',
|
||||
content: item.parsedContent
|
||||
} as WorkoutFeedEntry;
|
||||
|
||||
case 'exercise':
|
||||
return {
|
||||
...baseEntry,
|
||||
type: 'exercise',
|
||||
content: item.parsedContent
|
||||
} as ExerciseFeedEntry;
|
||||
|
||||
case 'template':
|
||||
return {
|
||||
...baseEntry,
|
||||
type: 'template',
|
||||
content: item.parsedContent
|
||||
} as TemplateFeedEntry;
|
||||
|
||||
case 'social':
|
||||
return {
|
||||
...baseEntry,
|
||||
type: 'social',
|
||||
content: item.parsedContent
|
||||
} as SocialFeedEntry;
|
||||
|
||||
case 'article':
|
||||
return {
|
||||
...baseEntry,
|
||||
type: 'article',
|
||||
content: item.parsedContent
|
||||
} as ArticleFeedEntry;
|
||||
|
||||
default:
|
||||
// Fallback to social type if unknown
|
||||
return {
|
||||
...baseEntry,
|
||||
type: 'social',
|
||||
content: item.parsedContent
|
||||
} as SocialFeedEntry;
|
||||
}
|
||||
});
|
||||
}, [feedItems]);
|
||||
|
||||
const resetFeed = refresh;
|
||||
const hasContent = entries.length > 0;
|
||||
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
|
||||
|
@ -55,7 +55,7 @@ export default function SocialLayout() {
|
||||
<Tab.Screen
|
||||
name="global"
|
||||
component={GlobalScreen}
|
||||
options={{ title: 'Global' }}
|
||||
options={{ title: 'Community' }}
|
||||
/>
|
||||
</Tab.Navigator>
|
||||
</TabScreen>
|
||||
|
@ -6,43 +6,170 @@ import EnhancedSocialPost from '@/components/social/EnhancedSocialPost';
|
||||
import { router } from 'expo-router';
|
||||
import NostrLoginPrompt from '@/components/social/NostrLoginPrompt';
|
||||
import { useNDKCurrentUser, useNDK } from '@/lib/hooks/useNDK';
|
||||
import { useFollowingFeed } from '@/lib/hooks/useFeedHooks';
|
||||
import { useContactList } from '@/lib/hooks/useContactList';
|
||||
import { ChevronUp, Bug } from 'lucide-react-native';
|
||||
import { AnyFeedEntry } from '@/types/feed';
|
||||
import { withOfflineState } from '@/components/social/SocialOfflineState';
|
||||
|
||||
// Define the conversion function here to avoid import issues
|
||||
function convertToLegacyFeedItem(entry: AnyFeedEntry) {
|
||||
return {
|
||||
id: entry.eventId,
|
||||
type: entry.type,
|
||||
originalEvent: entry.event!,
|
||||
parsedContent: entry.content!,
|
||||
createdAt: (entry.timestamp || Date.now()) / 1000
|
||||
};
|
||||
}
|
||||
import { useSocialFeed } from '@/lib/hooks/useSocialFeed';
|
||||
|
||||
function FollowingScreen() {
|
||||
const { isAuthenticated, currentUser } = useNDKCurrentUser();
|
||||
const { ndk } = useNDK();
|
||||
|
||||
// Get the user's contact list
|
||||
const { contacts, isLoading: isLoadingContacts } = useContactList(currentUser?.pubkey);
|
||||
|
||||
// Add debug logging for contact list
|
||||
React.useEffect(() => {
|
||||
console.log(`[FollowingScreen] Contact list has ${contacts.length} contacts`);
|
||||
if (contacts.length > 0) {
|
||||
console.log(`[FollowingScreen] First few contacts: ${contacts.slice(0, 3).join(', ')}`);
|
||||
}
|
||||
}, [contacts.length]);
|
||||
|
||||
// Track if feed has loaded successfully with content
|
||||
const [hasLoadedWithContent, setHasLoadedWithContent] = useState(false);
|
||||
// Track if we've loaded content with the full contact list
|
||||
const [hasLoadedWithContacts, setHasLoadedWithContacts] = useState(false);
|
||||
// Track the number of contacts we've loaded content with
|
||||
const [loadedContactsCount, setLoadedContactsCount] = useState(0);
|
||||
|
||||
// Use the enhanced useSocialFeed hook with the contact list
|
||||
// Always pass an array, even if empty, to ensure consistent behavior
|
||||
const {
|
||||
entries,
|
||||
newEntries,
|
||||
feedItems,
|
||||
loading,
|
||||
resetFeed,
|
||||
clearNewEntries,
|
||||
hasFollows,
|
||||
followCount,
|
||||
followedUsers,
|
||||
isLoadingContacts
|
||||
} = useFollowingFeed();
|
||||
refresh,
|
||||
isOffline,
|
||||
socialService
|
||||
} = useSocialFeed({
|
||||
feedType: 'following',
|
||||
limit: 30,
|
||||
authors: contacts // Always pass the contacts array, even if empty
|
||||
});
|
||||
|
||||
// Convert feed items to the format expected by the UI
|
||||
const entries = React.useMemo(() => {
|
||||
return feedItems.map(item => ({
|
||||
id: item.id,
|
||||
eventId: item.id,
|
||||
event: item.originalEvent,
|
||||
content: item.parsedContent,
|
||||
timestamp: item.createdAt * 1000,
|
||||
type: item.type as any
|
||||
}));
|
||||
}, [feedItems]);
|
||||
|
||||
// Update hasLoadedWithContent when we get feed items
|
||||
React.useEffect(() => {
|
||||
if (feedItems.length > 0 && !loading) {
|
||||
setHasLoadedWithContent(true);
|
||||
|
||||
// Check if we've loaded with the full contact list
|
||||
if (contacts.length > 0 && loadedContactsCount === contacts.length) {
|
||||
setHasLoadedWithContacts(true);
|
||||
}
|
||||
}
|
||||
}, [feedItems.length, loading, contacts.length, loadedContactsCount]);
|
||||
|
||||
// Update loadedContactsCount when contacts change
|
||||
React.useEffect(() => {
|
||||
if (contacts.length > 0 && contacts.length !== loadedContactsCount) {
|
||||
console.log(`[FollowingScreen] Contact list changed from ${loadedContactsCount} to ${contacts.length} contacts`);
|
||||
setLoadedContactsCount(contacts.length);
|
||||
// Reset hasLoadedWithContacts flag when contacts change
|
||||
setHasLoadedWithContacts(false);
|
||||
}
|
||||
}, [contacts.length, loadedContactsCount]);
|
||||
|
||||
// Auto-refresh when contacts are loaded with improved retry logic
|
||||
React.useEffect(() => {
|
||||
// Trigger refresh when contacts change from empty to non-empty
|
||||
// OR when we have content but haven't loaded with the full contact list yet
|
||||
if (contacts.length > 0 && !isLoadingContacts &&
|
||||
(!hasLoadedWithContent || !hasLoadedWithContacts)) {
|
||||
console.log('[FollowingScreen] Contacts loaded, triggering auto-refresh');
|
||||
|
||||
// Track retry attempts
|
||||
let retryCount = 0;
|
||||
const maxRetries = 3;
|
||||
|
||||
// Function to attempt refresh with exponential backoff
|
||||
const attemptRefresh = () => {
|
||||
// Increase delay with each retry (1s, 2s, 4s)
|
||||
const delay = 1000 * Math.pow(2, retryCount);
|
||||
|
||||
console.log(`[FollowingScreen] Scheduling refresh attempt ${retryCount + 1}/${maxRetries + 1} in ${delay}ms`);
|
||||
|
||||
return setTimeout(async () => {
|
||||
// Skip if we've loaded content with the full contact list in the meantime
|
||||
if (hasLoadedWithContent && hasLoadedWithContacts) {
|
||||
console.log('[FollowingScreen] Content already loaded with full contact list, skipping refresh');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`[FollowingScreen] Executing refresh attempt ${retryCount + 1}/${maxRetries + 1}`);
|
||||
// Use force refresh to bypass cooldown
|
||||
await refresh(true);
|
||||
|
||||
// Check if we got any items after a short delay
|
||||
setTimeout(() => {
|
||||
if (feedItems.length === 0 && retryCount < maxRetries &&
|
||||
(!hasLoadedWithContent || !hasLoadedWithContacts)) {
|
||||
console.log(`[FollowingScreen] No items after refresh attempt ${retryCount + 1}, retrying...`);
|
||||
retryCount++;
|
||||
const nextTimer = attemptRefresh();
|
||||
|
||||
// Store the timer ID in the ref so we can clear it if needed
|
||||
retryTimerRef.current = nextTimer;
|
||||
} else if (feedItems.length > 0) {
|
||||
console.log(`[FollowingScreen] Refresh successful, got ${feedItems.length} items`);
|
||||
setHasLoadedWithContent(true);
|
||||
// Mark as loaded with contacts if we have the full contact list
|
||||
if (contacts.length > 0) {
|
||||
console.log(`[FollowingScreen] Marking as loaded with ${contacts.length} contacts`);
|
||||
setLoadedContactsCount(contacts.length);
|
||||
setHasLoadedWithContacts(true);
|
||||
}
|
||||
} else {
|
||||
console.log(`[FollowingScreen] All refresh attempts completed, got ${feedItems.length} items`);
|
||||
}
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
console.error(`[FollowingScreen] Error during refresh attempt ${retryCount + 1}:`, error);
|
||||
|
||||
// Retry on error if we haven't exceeded max retries
|
||||
if (retryCount < maxRetries && (!hasLoadedWithContent || !hasLoadedWithContacts)) {
|
||||
retryCount++;
|
||||
const nextTimer = attemptRefresh();
|
||||
retryTimerRef.current = nextTimer;
|
||||
}
|
||||
}
|
||||
}, delay);
|
||||
};
|
||||
|
||||
// Start the first attempt with initial delay
|
||||
const timerId = attemptRefresh();
|
||||
retryTimerRef.current = timerId;
|
||||
|
||||
// Clean up any pending timers on unmount
|
||||
return () => {
|
||||
if (retryTimerRef.current) {
|
||||
clearTimeout(retryTimerRef.current);
|
||||
retryTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [contacts.length, isLoadingContacts, refresh, feedItems.length, hasLoadedWithContent, hasLoadedWithContacts]);
|
||||
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [showNewButton, setShowNewButton] = useState(false);
|
||||
const [newEntries, setNewEntries] = useState<any[]>([]);
|
||||
const [showDebug, setShowDebug] = useState(false);
|
||||
|
||||
// Use ref for FlatList to scroll to top
|
||||
// Use refs
|
||||
const listRef = useRef<FlatList>(null);
|
||||
const retryTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Show new entries button when we have new content
|
||||
React.useEffect(() => {
|
||||
@ -58,25 +185,54 @@ function FollowingScreen() {
|
||||
|
||||
// Handle showing new entries
|
||||
const handleShowNewEntries = useCallback(() => {
|
||||
clearNewEntries();
|
||||
setNewEntries([]);
|
||||
setShowNewButton(false);
|
||||
// Scroll to top
|
||||
listRef.current?.scrollToOffset({ offset: 0, animated: true });
|
||||
}, [clearNewEntries]);
|
||||
}, []);
|
||||
|
||||
// Handle refresh - updated with proper reset handling
|
||||
// Handle refresh - updated to use forceRefresh parameter
|
||||
const handleRefresh = useCallback(async () => {
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
await resetFeed();
|
||||
console.log('[FollowingScreen] Starting manual refresh (force=true)');
|
||||
|
||||
// Check if we have contacts before refreshing
|
||||
if (contacts.length === 0) {
|
||||
console.log('[FollowingScreen] No contacts available for refresh, using fallback');
|
||||
// Still try to refresh with force=true to bypass cooldown
|
||||
}
|
||||
|
||||
// Use force=true to bypass cooldown
|
||||
await refresh(true);
|
||||
// Add a slight delay to ensure the UI updates
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
console.log('[FollowingScreen] Manual refresh completed successfully');
|
||||
|
||||
// If we get content, mark as loaded with content
|
||||
if (feedItems.length > 0) {
|
||||
setHasLoadedWithContent(true);
|
||||
|
||||
// Mark as loaded with contacts if we have the full contact list
|
||||
if (contacts.length > 0) {
|
||||
console.log(`[FollowingScreen] Marking as loaded with ${contacts.length} contacts after manual refresh`);
|
||||
setLoadedContactsCount(contacts.length);
|
||||
setHasLoadedWithContacts(true);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error refreshing feed:', error);
|
||||
console.error('[FollowingScreen] Error refreshing feed:', error);
|
||||
// Log more detailed error information
|
||||
if (error instanceof Error) {
|
||||
console.error(`[FollowingScreen] Error details: ${error.message}`);
|
||||
if (error.stack) {
|
||||
console.error(`[FollowingScreen] Stack trace: ${error.stack}`);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
}, [resetFeed]);
|
||||
}, [refresh, contacts.length, feedItems.length]);
|
||||
|
||||
// Check relay connections
|
||||
const checkRelayConnections = useCallback(() => {
|
||||
@ -95,18 +251,24 @@ function FollowingScreen() {
|
||||
}, [ndk]);
|
||||
|
||||
// Handle post selection - simplified for testing
|
||||
const handlePostPress = useCallback((entry: AnyFeedEntry) => {
|
||||
const handlePostPress = useCallback((entry: any) => {
|
||||
// Just show an alert with the entry info for testing
|
||||
alert(`Selected ${entry.type} with ID: ${entry.eventId}`);
|
||||
alert(`Selected ${entry.type} with ID: ${entry.id || entry.eventId}`);
|
||||
|
||||
// Alternatively, log to console for debugging
|
||||
console.log(`Selected ${entry.type}:`, entry);
|
||||
}, []);
|
||||
|
||||
// Memoize render item function
|
||||
const renderItem = useCallback(({ item }: { item: AnyFeedEntry }) => (
|
||||
const renderItem = useCallback(({ item }: { item: any }) => (
|
||||
<EnhancedSocialPost
|
||||
item={convertToLegacyFeedItem(item)}
|
||||
item={{
|
||||
id: item.id || item.eventId,
|
||||
type: item.type,
|
||||
originalEvent: item.originalEvent || item.event,
|
||||
parsedContent: item.parsedContent || item.content,
|
||||
createdAt: item.createdAt || (item.timestamp ? item.timestamp / 1000 : Date.now() / 1000)
|
||||
}}
|
||||
onPress={() => handlePostPress(item)}
|
||||
/>
|
||||
), [handlePostPress]);
|
||||
@ -116,23 +278,12 @@ function FollowingScreen() {
|
||||
<View className="bg-gray-100 p-4 rounded-lg mx-4 mb-4">
|
||||
<Text className="font-bold mb-2">Debug Info:</Text>
|
||||
<Text>User: {currentUser?.pubkey?.substring(0, 8)}...</Text>
|
||||
<Text>Following Count: {followCount || 0}</Text>
|
||||
<Text>Feed Items: {entries.length}</Text>
|
||||
<Text>Loading: {loading ? "Yes" : "No"}</Text>
|
||||
<Text>Offline: {isOffline ? "Yes" : "No"}</Text>
|
||||
<Text>Contacts: {contacts.length}</Text>
|
||||
<Text>Loading Contacts: {isLoadingContacts ? "Yes" : "No"}</Text>
|
||||
|
||||
{followedUsers && followedUsers.length > 0 && (
|
||||
<View className="mt-2">
|
||||
<Text className="font-bold">Followed Users:</Text>
|
||||
{followedUsers.slice(0, 3).map((pubkey, idx) => (
|
||||
<Text key={idx} className="text-xs">{idx+1}. {pubkey.substring(0, 12)}...</Text>
|
||||
))}
|
||||
{followedUsers.length > 3 && (
|
||||
<Text className="text-xs">...and {followedUsers.length - 3} more</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className="flex-row mt-4 justify-between">
|
||||
<TouchableOpacity
|
||||
className="bg-blue-500 p-2 rounded flex-1 mr-2"
|
||||
@ -149,14 +300,16 @@ function FollowingScreen() {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
), [currentUser?.pubkey, followCount, entries.length, loading, isLoadingContacts, followedUsers, checkRelayConnections, handleRefresh]);
|
||||
), [currentUser?.pubkey, entries.length, loading, isOffline, contacts.length, isLoadingContacts, checkRelayConnections, handleRefresh]);
|
||||
|
||||
// If user doesn't follow anyone
|
||||
if (isAuthenticated && !hasFollows) {
|
||||
// If user doesn't follow anyone or no content is available
|
||||
if (isAuthenticated && entries.length === 0 && !loading) {
|
||||
return (
|
||||
<View className="flex-1 items-center justify-center p-8">
|
||||
<Text className="text-center mb-4">
|
||||
You're not following anyone yet. Find and follow other users to see their content here.
|
||||
{isOffline
|
||||
? "You're offline. No cached content from followed users is available."
|
||||
: "No content from followed users found. Try following more users or check your relay connections."}
|
||||
</Text>
|
||||
|
||||
{/* Debug toggle */}
|
||||
@ -171,16 +324,8 @@ function FollowingScreen() {
|
||||
<View className="mt-4 p-4 bg-gray-100 rounded w-full">
|
||||
<Text className="text-xs">User pubkey: {currentUser?.pubkey?.substring(0, 12)}...</Text>
|
||||
<Text className="text-xs">Authenticated: {isAuthenticated ? "Yes" : "No"}</Text>
|
||||
<Text className="text-xs">Follow count: {followCount || 0}</Text>
|
||||
<Text className="text-xs">Offline: {isOffline ? "Yes" : "No"}</Text>
|
||||
<Text className="text-xs">Has NDK follows: {currentUser?.follows ? "Yes" : "No"}</Text>
|
||||
<Text className="text-xs">NDK follows count: {
|
||||
typeof currentUser?.follows === 'function' ? 'Function' :
|
||||
(currentUser?.follows && Array.isArray(currentUser?.follows)) ? (currentUser?.follows as any[]).length :
|
||||
(currentUser?.follows && typeof currentUser?.follows === 'object' && 'size' in currentUser?.follows) ?
|
||||
(currentUser?.follows as any).size :
|
||||
'unknown'
|
||||
}</Text>
|
||||
<Text className="text-xs">Loading contacts: {isLoadingContacts ? "Yes" : "No"}</Text>
|
||||
|
||||
{/* Toggle relays button */}
|
||||
<TouchableOpacity
|
||||
@ -231,7 +376,7 @@ function FollowingScreen() {
|
||||
|
||||
<FlatList
|
||||
ref={listRef}
|
||||
data={entries}
|
||||
data={entries as any[]}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={renderItem}
|
||||
refreshControl={
|
||||
@ -241,20 +386,18 @@ function FollowingScreen() {
|
||||
/>
|
||||
}
|
||||
ListEmptyComponent={
|
||||
loading || isLoadingContacts ? (
|
||||
loading ? (
|
||||
<View className="flex-1 items-center justify-center p-8">
|
||||
<Text>Loading followed content...</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View className="flex-1 items-center justify-center p-8">
|
||||
<Text>No posts from followed users found</Text>
|
||||
<Text className="text-sm text-gray-500 mt-2">
|
||||
You're following {followCount || 0} users, but no content was found.
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-500 mt-1">
|
||||
This could be because they haven't posted recently,
|
||||
or their content is not available on connected relays.
|
||||
</Text>
|
||||
{isOffline && (
|
||||
<Text className="text-sm text-gray-500 mt-2">
|
||||
You're currently offline. Connect to the internet to see the latest content.
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
@ -1,42 +1,45 @@
|
||||
// app/(tabs)/social/global.tsx
|
||||
import React, { useCallback, useState, useRef, useEffect } from 'react';
|
||||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import { View, FlatList, RefreshControl, TouchableOpacity } from 'react-native';
|
||||
import { Text } from '@/components/ui/text';
|
||||
import EnhancedSocialPost from '@/components/social/EnhancedSocialPost';
|
||||
import { useGlobalFeed } from '@/lib/hooks/useFeedHooks';
|
||||
import { ChevronUp, Globe } from 'lucide-react-native';
|
||||
import { router } from 'expo-router';
|
||||
import { ChevronUp } from 'lucide-react-native';
|
||||
import { AnyFeedEntry } from '@/types/feed';
|
||||
import { withOfflineState } from '@/components/social/SocialOfflineState';
|
||||
|
||||
// Define the conversion function here to avoid import issues
|
||||
function convertToLegacyFeedItem(entry: AnyFeedEntry) {
|
||||
return {
|
||||
id: entry.eventId,
|
||||
type: entry.type,
|
||||
originalEvent: entry.event!,
|
||||
parsedContent: entry.content!,
|
||||
createdAt: (entry.timestamp || Date.now()) / 1000
|
||||
};
|
||||
}
|
||||
import { useSocialFeed } from '@/lib/hooks/useSocialFeed';
|
||||
|
||||
function GlobalScreen() {
|
||||
const {
|
||||
entries,
|
||||
newEntries,
|
||||
feedItems,
|
||||
loading,
|
||||
resetFeed,
|
||||
clearNewEntries
|
||||
} = useGlobalFeed();
|
||||
refresh,
|
||||
isOffline
|
||||
} = useSocialFeed({
|
||||
feedType: 'global',
|
||||
limit: 30
|
||||
});
|
||||
|
||||
// Convert feed items to the format expected by the UI
|
||||
const entries = React.useMemo(() => {
|
||||
return feedItems.map(item => ({
|
||||
id: item.id,
|
||||
eventId: item.id,
|
||||
event: item.originalEvent,
|
||||
content: item.parsedContent,
|
||||
timestamp: item.createdAt * 1000,
|
||||
type: item.type as any
|
||||
}));
|
||||
}, [feedItems]);
|
||||
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [showNewButton, setShowNewButton] = useState(false);
|
||||
const [newEntries, setNewEntries] = useState<any[]>([]);
|
||||
|
||||
// Use ref for FlatList to scroll to top
|
||||
const listRef = useRef<FlatList>(null);
|
||||
|
||||
// Show new entries button when we have new content
|
||||
useEffect(() => {
|
||||
React.useEffect(() => {
|
||||
if (newEntries.length > 0) {
|
||||
setShowNewButton(true);
|
||||
}
|
||||
@ -44,58 +47,82 @@ function GlobalScreen() {
|
||||
|
||||
// Handle showing new entries
|
||||
const handleShowNewEntries = useCallback(() => {
|
||||
clearNewEntries();
|
||||
setNewEntries([]);
|
||||
setShowNewButton(false);
|
||||
// Scroll to top
|
||||
listRef.current?.scrollToOffset({ offset: 0, animated: true });
|
||||
}, [clearNewEntries]);
|
||||
}, []);
|
||||
|
||||
// Handle refresh
|
||||
// Handle refresh - updated to use forceRefresh parameter
|
||||
const handleRefresh = useCallback(async () => {
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
await resetFeed();
|
||||
console.log('[GlobalScreen] Starting manual refresh (force=true)');
|
||||
// Use force=true to bypass cooldown
|
||||
await refresh(true);
|
||||
// Add a slight delay to ensure the UI updates
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
console.log('[GlobalScreen] Manual refresh completed successfully');
|
||||
} catch (error) {
|
||||
console.error('Error refreshing feed:', error);
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
}, [resetFeed]);
|
||||
}, [refresh]);
|
||||
|
||||
// Handle post selection - simplified for testing
|
||||
const handlePostPress = useCallback((entry: AnyFeedEntry) => {
|
||||
const handlePostPress = useCallback((entry: any) => {
|
||||
// Just show an alert with the entry info for testing
|
||||
alert(`Selected ${entry.type} with ID: ${entry.eventId}`);
|
||||
alert(`Selected ${entry.type} with ID: ${entry.id || entry.eventId}`);
|
||||
|
||||
// Alternatively, log to console for debugging
|
||||
console.log(`Selected ${entry.type}:`, entry);
|
||||
}, []);
|
||||
|
||||
// Memoize render item function
|
||||
const renderItem = useCallback(({ item }: { item: AnyFeedEntry }) => (
|
||||
|
||||
// Memoize render item to prevent re-renders
|
||||
const renderItem = useCallback(({ item }: { item: any }) => (
|
||||
<EnhancedSocialPost
|
||||
item={convertToLegacyFeedItem(item)}
|
||||
item={{
|
||||
id: item.id || item.eventId,
|
||||
type: item.type,
|
||||
originalEvent: item.originalEvent || item.event,
|
||||
parsedContent: item.parsedContent || item.content,
|
||||
createdAt: item.createdAt || (item.timestamp ? item.timestamp / 1000 : Date.now() / 1000)
|
||||
}}
|
||||
onPress={() => handlePostPress(item)}
|
||||
/>
|
||||
), [handlePostPress]);
|
||||
|
||||
// Header component
|
||||
const renderHeaderComponent = useCallback(() => (
|
||||
<View className="p-4 border-b border-border bg-primary/5 mb-2">
|
||||
<View className="flex-row items-center mb-2">
|
||||
<Globe size={20} className="mr-2 text-primary" />
|
||||
<Text className="text-lg font-bold">Community Feed</Text>
|
||||
</View>
|
||||
<Text className="text-muted-foreground">
|
||||
Discover workout content from the broader POWR community.
|
||||
</Text>
|
||||
</View>
|
||||
), []);
|
||||
|
||||
// Memoize empty component
|
||||
const renderEmptyComponent = useCallback(() => (
|
||||
loading ? (
|
||||
<View className="flex-1 items-center justify-center p-8">
|
||||
<Text>Loading global content...</Text>
|
||||
<Text>Loading community content from your relays...</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View className="flex-1 items-center justify-center p-8">
|
||||
<Text>No global content found</Text>
|
||||
<Text className="text-sm text-gray-500 mt-2">
|
||||
Try connecting to more relays or check back later.
|
||||
</Text>
|
||||
<Text>{isOffline ? "No cached global content available" : "No global content found"}</Text>
|
||||
{isOffline && (
|
||||
<Text className="text-muted-foreground text-center mt-2">
|
||||
You're currently offline. Connect to the internet to see the latest content.
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
), [loading]);
|
||||
), [loading, isOffline]);
|
||||
|
||||
return (
|
||||
<View className="flex-1">
|
||||
@ -111,10 +138,10 @@ function GlobalScreen() {
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
|
||||
<FlatList
|
||||
ref={listRef}
|
||||
data={entries}
|
||||
data={entries as any[]}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={renderItem}
|
||||
refreshControl={
|
||||
@ -123,8 +150,9 @@ function GlobalScreen() {
|
||||
onRefresh={handleRefresh}
|
||||
/>
|
||||
}
|
||||
ListHeaderComponent={renderHeaderComponent}
|
||||
ListEmptyComponent={renderEmptyComponent}
|
||||
contentContainerStyle={{ paddingVertical: 0 }}
|
||||
contentContainerStyle={{ flexGrow: 1, paddingBottom: 16 }}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
@ -5,30 +5,48 @@ import { Text } from '@/components/ui/text';
|
||||
import EnhancedSocialPost from '@/components/social/EnhancedSocialPost';
|
||||
import { ChevronUp, Zap } from 'lucide-react-native';
|
||||
import POWRPackSection from '@/components/social/POWRPackSection';
|
||||
import { usePOWRFeed } from '@/lib/hooks/useFeedHooks';
|
||||
import { useSocialFeed } from '@/lib/hooks/useSocialFeed';
|
||||
import { POWR_PUBKEY_HEX } from '@/lib/hooks/useFeedHooks';
|
||||
import { router } from 'expo-router';
|
||||
import { AnyFeedEntry } from '@/types/feed';
|
||||
import { withOfflineState } from '@/components/social/SocialOfflineState';
|
||||
|
||||
// Define the conversion function here to avoid import issues
|
||||
function convertToLegacyFeedItem(entry: AnyFeedEntry) {
|
||||
function convertToLegacyFeedItem(entry: any) {
|
||||
return {
|
||||
id: entry.eventId,
|
||||
id: entry.id || entry.eventId,
|
||||
type: entry.type,
|
||||
originalEvent: entry.event!,
|
||||
parsedContent: entry.content!,
|
||||
createdAt: (entry.timestamp || Date.now()) / 1000
|
||||
originalEvent: entry.originalEvent || entry.event,
|
||||
parsedContent: entry.parsedContent || entry.content,
|
||||
createdAt: entry.createdAt || (entry.timestamp ? entry.timestamp / 1000 : Date.now() / 1000)
|
||||
};
|
||||
}
|
||||
|
||||
function PowerScreen() {
|
||||
const {
|
||||
entries,
|
||||
newEntries,
|
||||
feedItems,
|
||||
loading,
|
||||
resetFeed,
|
||||
clearNewEntries
|
||||
} = usePOWRFeed();
|
||||
refresh,
|
||||
isOffline
|
||||
} = useSocialFeed({
|
||||
feedType: 'powr',
|
||||
authors: POWR_PUBKEY_HEX ? [POWR_PUBKEY_HEX] : undefined,
|
||||
limit: 30
|
||||
});
|
||||
|
||||
// For compatibility with the existing UI
|
||||
const entries = React.useMemo(() => {
|
||||
return feedItems.map(item => ({
|
||||
id: item.id,
|
||||
eventId: item.id,
|
||||
event: item.originalEvent,
|
||||
content: item.parsedContent,
|
||||
timestamp: item.createdAt * 1000,
|
||||
type: item.type as any
|
||||
}));
|
||||
}, [feedItems]);
|
||||
|
||||
const [newEntries, setNewEntries] = useState<any[]>([]);
|
||||
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [showNewButton, setShowNewButton] = useState(false);
|
||||
@ -46,37 +64,40 @@ function PowerScreen() {
|
||||
|
||||
// Handle showing new entries
|
||||
const handleShowNewEntries = useCallback(() => {
|
||||
clearNewEntries();
|
||||
setNewEntries([]);
|
||||
setShowNewButton(false);
|
||||
// Scroll to top
|
||||
listRef.current?.scrollToOffset({ offset: 0, animated: true });
|
||||
}, [clearNewEntries]);
|
||||
}, []);
|
||||
|
||||
// Handle refresh - updated with proper reset handling
|
||||
// Handle refresh - updated to use forceRefresh parameter
|
||||
const handleRefresh = useCallback(async () => {
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
await resetFeed();
|
||||
console.log('[POWRScreen] Starting manual refresh (force=true)');
|
||||
// Use force=true to bypass cooldown
|
||||
await refresh(true);
|
||||
// Add a slight delay to ensure the UI updates
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
console.log('[POWRScreen] Manual refresh completed successfully');
|
||||
} catch (error) {
|
||||
console.error('Error refreshing feed:', error);
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
}, [resetFeed]);
|
||||
}, [refresh]);
|
||||
|
||||
// Handle post selection - simplified for testing
|
||||
const handlePostPress = useCallback((entry: AnyFeedEntry) => {
|
||||
const handlePostPress = useCallback((entry: any) => {
|
||||
// Just show an alert with the entry info for testing
|
||||
alert(`Selected ${entry.type} with ID: ${entry.eventId}`);
|
||||
alert(`Selected ${entry.type} with ID: ${entry.id || entry.eventId}`);
|
||||
|
||||
// Alternatively, log to console for debugging
|
||||
console.log(`Selected ${entry.type}:`, entry);
|
||||
}, []);
|
||||
|
||||
// Memoize render item to prevent re-renders
|
||||
const renderItem = useCallback(({ item }: { item: AnyFeedEntry }) => (
|
||||
const renderItem = useCallback(({ item }: { item: any }) => (
|
||||
<EnhancedSocialPost
|
||||
item={convertToLegacyFeedItem(item)}
|
||||
onPress={() => handlePostPress(item)}
|
||||
@ -91,10 +112,15 @@ function PowerScreen() {
|
||||
</View>
|
||||
) : (
|
||||
<View className="flex-1 items-center justify-center p-8">
|
||||
<Text>No POWR content found</Text>
|
||||
<Text>{isOffline ? "No cached POWR content available" : "No POWR content found"}</Text>
|
||||
{isOffline && (
|
||||
<Text className="text-muted-foreground text-center mt-2">
|
||||
You're currently offline. Connect to the internet to see the latest content.
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
), [loading]);
|
||||
), [loading, isOffline]);
|
||||
|
||||
// Header component
|
||||
const renderHeaderComponent = useCallback(() => (
|
||||
@ -132,7 +158,7 @@ function PowerScreen() {
|
||||
|
||||
<FlatList
|
||||
ref={listRef}
|
||||
data={entries}
|
||||
data={entries as any[]}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={renderItem}
|
||||
refreshControl={
|
||||
|
@ -113,35 +113,94 @@ export function DatabaseProvider({ children }: DatabaseProviderProps) {
|
||||
// Explicitly check for critical tables after schema creation
|
||||
await schema.ensureCriticalTablesExist(db);
|
||||
|
||||
// Run the v8 migration explicitly to ensure new columns are added
|
||||
try {
|
||||
await (schema as any).migrate_v8(db);
|
||||
console.log('[DB] Migration v8 executed successfully');
|
||||
} catch (migrationError) {
|
||||
console.warn('[DB] Error running migration v8:', migrationError);
|
||||
// Continue even if migration fails - tables might already be updated
|
||||
}
|
||||
// Run migrations with robust error handling
|
||||
const runMigration = async (version: string, migrationFn: (db: SQLiteDatabase) => Promise<void>) => {
|
||||
try {
|
||||
await migrationFn(db);
|
||||
console.log(`[DB] Migration ${version} executed successfully`);
|
||||
} catch (migrationError) {
|
||||
console.warn(`[DB] Error running migration ${version}:`, migrationError);
|
||||
// Log more details about the error
|
||||
if (migrationError instanceof Error) {
|
||||
console.warn(`[DB] Migration error details: ${migrationError.message}`);
|
||||
if (migrationError.stack) {
|
||||
console.warn(`[DB] Stack trace: ${migrationError.stack}`);
|
||||
}
|
||||
}
|
||||
// Continue even if migration fails - tables might already be updated
|
||||
}
|
||||
};
|
||||
|
||||
// Run migrations
|
||||
await runMigration('v8', (schema as any).migrate_v8);
|
||||
await runMigration('v9', (schema as any).migrate_v9);
|
||||
await runMigration('v10', (schema as any).migrate_v10);
|
||||
|
||||
// Run v9 migration for Nostr metadata enhancements
|
||||
try {
|
||||
await (schema as any).migrate_v9(db);
|
||||
console.log('[DB] Migration v9 executed successfully');
|
||||
} catch (migrationError) {
|
||||
console.warn('[DB] Error running migration v9:', migrationError);
|
||||
// Continue even if migration fails - tables might already be updated
|
||||
}
|
||||
|
||||
// Initialize services
|
||||
// Initialize services with error handling
|
||||
console.log('[DB] Initializing services...');
|
||||
const exerciseService = new ExerciseService(db);
|
||||
const workoutService = new WorkoutService(db);
|
||||
const templateService = new TemplateService(db, exerciseService);
|
||||
const publicationQueue = new PublicationQueueService(db);
|
||||
const favoritesService = new FavoritesService(db);
|
||||
const powrPackService = new POWRPackService(db);
|
||||
let exerciseService: ExerciseService;
|
||||
let workoutService: WorkoutService;
|
||||
let templateService: TemplateService;
|
||||
let publicationQueue: PublicationQueueService;
|
||||
let favoritesService: FavoritesService;
|
||||
let powrPackService: POWRPackService;
|
||||
|
||||
try {
|
||||
exerciseService = new ExerciseService(db);
|
||||
console.log('[DB] ExerciseService initialized');
|
||||
} catch (error) {
|
||||
console.error('[DB] Failed to initialize ExerciseService:', error);
|
||||
throw new Error('Failed to initialize ExerciseService');
|
||||
}
|
||||
|
||||
try {
|
||||
workoutService = new WorkoutService(db);
|
||||
console.log('[DB] WorkoutService initialized');
|
||||
} catch (error) {
|
||||
console.error('[DB] Failed to initialize WorkoutService:', error);
|
||||
throw new Error('Failed to initialize WorkoutService');
|
||||
}
|
||||
|
||||
try {
|
||||
templateService = new TemplateService(db, exerciseService);
|
||||
console.log('[DB] TemplateService initialized');
|
||||
} catch (error) {
|
||||
console.error('[DB] Failed to initialize TemplateService:', error);
|
||||
throw new Error('Failed to initialize TemplateService');
|
||||
}
|
||||
|
||||
try {
|
||||
publicationQueue = new PublicationQueueService(db);
|
||||
console.log('[DB] PublicationQueueService initialized');
|
||||
} catch (error) {
|
||||
console.error('[DB] Failed to initialize PublicationQueueService:', error);
|
||||
throw new Error('Failed to initialize PublicationQueueService');
|
||||
}
|
||||
|
||||
try {
|
||||
favoritesService = new FavoritesService(db);
|
||||
console.log('[DB] FavoritesService initialized');
|
||||
} catch (error) {
|
||||
console.error('[DB] Failed to initialize FavoritesService:', error);
|
||||
throw new Error('Failed to initialize FavoritesService');
|
||||
}
|
||||
|
||||
try {
|
||||
powrPackService = new POWRPackService(db);
|
||||
console.log('[DB] POWRPackService initialized');
|
||||
} catch (error) {
|
||||
console.error('[DB] Failed to initialize POWRPackService:', error);
|
||||
throw new Error('Failed to initialize POWRPackService');
|
||||
}
|
||||
|
||||
// Initialize the favorites service
|
||||
await favoritesService.initialize();
|
||||
try {
|
||||
await favoritesService.initialize();
|
||||
console.log('[DB] FavoritesService fully initialized');
|
||||
} catch (error) {
|
||||
console.error('[DB] Error initializing FavoritesService:', error);
|
||||
// Continue even if favorites initialization fails
|
||||
}
|
||||
|
||||
// Initialize NDK on services if available
|
||||
if (ndk) {
|
||||
@ -259,4 +318,4 @@ export function useDatabase() {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
return context.db;
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,9 @@ import { useRelayStore } from '@/lib/stores/relayStore';
|
||||
import { useNDKStore } from '@/lib/stores/ndk';
|
||||
import { useConnectivity } from '@/lib/db/services/ConnectivityService';
|
||||
import { ConnectivityService } from '@/lib/db/services/ConnectivityService';
|
||||
import { profileImageCache } from '@/lib/db/services/ProfileImageCache';
|
||||
import { getSocialFeedCache } from '@/lib/db/services/SocialFeedCache';
|
||||
import { useDatabase } from '@/components/DatabaseProvider';
|
||||
|
||||
/**
|
||||
* A component to initialize and load relay data when the app starts
|
||||
@ -15,6 +18,77 @@ export default function RelayInitializer() {
|
||||
const { ndk } = useNDKStore();
|
||||
const { isOnline } = useConnectivity();
|
||||
|
||||
const db = useDatabase();
|
||||
|
||||
// Initialize ProfileImageCache and SocialFeedCache with NDK instance
|
||||
useEffect(() => {
|
||||
if (ndk) {
|
||||
console.log('[RelayInitializer] Setting NDK instance in ProfileImageCache');
|
||||
profileImageCache.setNDK(ndk);
|
||||
|
||||
// Initialize SocialFeedCache with NDK instance
|
||||
if (db) {
|
||||
// Maximum number of retry attempts
|
||||
const MAX_RETRIES = 3;
|
||||
|
||||
const initSocialFeedCache = (attempt = 1) => {
|
||||
try {
|
||||
console.log(`[RelayInitializer] Attempting to initialize SocialFeedCache (attempt ${attempt}/${MAX_RETRIES})`);
|
||||
const socialFeedCache = getSocialFeedCache(db);
|
||||
socialFeedCache.setNDK(ndk);
|
||||
console.log('[RelayInitializer] SocialFeedCache initialized with NDK successfully');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[RelayInitializer] Error initializing SocialFeedCache:', error);
|
||||
|
||||
// Log more detailed error information
|
||||
if (error instanceof Error) {
|
||||
console.error(`[RelayInitializer] Error details: ${error.message}`);
|
||||
if (error.stack) {
|
||||
console.error(`[RelayInitializer] Stack trace: ${error.stack}`);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Try to initialize immediately
|
||||
const success = initSocialFeedCache();
|
||||
|
||||
// If failed, set up progressive retries
|
||||
if (!success) {
|
||||
let retryAttempt = 1;
|
||||
const retryTimers: NodeJS.Timeout[] = [];
|
||||
|
||||
// Set up multiple retries with increasing delays
|
||||
const retryDelays = [2000, 5000, 10000]; // 2s, 5s, 10s
|
||||
|
||||
for (let i = 0; i < Math.min(MAX_RETRIES - 1, retryDelays.length); i++) {
|
||||
const currentAttempt = retryAttempt + 1;
|
||||
const delay = retryDelays[i];
|
||||
|
||||
console.log(`[RelayInitializer] Will retry SocialFeedCache initialization in ${delay/1000} seconds (attempt ${currentAttempt}/${MAX_RETRIES})`);
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
console.log(`[RelayInitializer] Retrying SocialFeedCache initialization (attempt ${currentAttempt}/${MAX_RETRIES})`);
|
||||
if (initSocialFeedCache(currentAttempt)) {
|
||||
// Clear any remaining timers if successful
|
||||
retryTimers.forEach(t => clearTimeout(t));
|
||||
}
|
||||
}, delay);
|
||||
|
||||
retryTimers.push(timer);
|
||||
retryAttempt++;
|
||||
}
|
||||
|
||||
// Return cleanup function to clear all timers
|
||||
return () => retryTimers.forEach(timer => clearTimeout(timer));
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [ndk, db]);
|
||||
|
||||
// Load relays when NDK is initialized and network is available
|
||||
useEffect(() => {
|
||||
if (ndk && isOnline) {
|
||||
|
@ -4,6 +4,7 @@ import { TouchableOpacity, TouchableOpacityProps, GestureResponderEvent } from '
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Text } from '@/components/ui/text';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { profileImageCache } from '@/lib/db/services/ProfileImageCache';
|
||||
|
||||
interface UserAvatarProps extends TouchableOpacityProps {
|
||||
uri?: string;
|
||||
@ -24,16 +25,58 @@ const UserAvatar = ({
|
||||
}: UserAvatarProps) => {
|
||||
const [imageError, setImageError] = useState(false);
|
||||
const [imageUri, setImageUri] = useState<string | undefined>(uri);
|
||||
const [retryCount, setRetryCount] = useState(0);
|
||||
const maxRetries = 2; // Maximum number of retry attempts
|
||||
|
||||
// Update imageUri when uri prop changes
|
||||
// Load cached image when uri changes
|
||||
useEffect(() => {
|
||||
setImageUri(uri);
|
||||
let isMounted = true;
|
||||
|
||||
// Reset retry count and error state when URI changes
|
||||
setRetryCount(0);
|
||||
setImageError(false);
|
||||
|
||||
const loadCachedImage = async () => {
|
||||
if (!uri) {
|
||||
setImageUri(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to extract pubkey from URI
|
||||
const pubkey = profileImageCache.extractPubkeyFromUri(uri);
|
||||
|
||||
if (pubkey) {
|
||||
// If we have a pubkey, try to get cached image
|
||||
const cachedUri = await profileImageCache.getProfileImageUri(pubkey, uri);
|
||||
|
||||
if (isMounted) {
|
||||
setImageUri(cachedUri);
|
||||
setImageError(false);
|
||||
}
|
||||
} else {
|
||||
// If no pubkey, just use the original URI
|
||||
if (isMounted) {
|
||||
setImageUri(uri);
|
||||
setImageError(false);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading cached image:', error);
|
||||
if (isMounted) {
|
||||
setImageUri(uri);
|
||||
setImageError(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadCachedImage();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [uri]);
|
||||
|
||||
// Log the URI for debugging
|
||||
// console.log("Avatar URI:", uri);
|
||||
|
||||
const containerStyles = cn(
|
||||
{
|
||||
'w-8 h-8': size === 'sm',
|
||||
@ -55,7 +98,26 @@ const UserAvatar = ({
|
||||
|
||||
const handleImageError = () => {
|
||||
console.error("Failed to load image from URI:", imageUri);
|
||||
setImageError(true);
|
||||
|
||||
if (retryCount < maxRetries) {
|
||||
// Try again after a short delay
|
||||
console.log(`Retrying image load (attempt ${retryCount + 1}/${maxRetries})`);
|
||||
setTimeout(() => {
|
||||
setRetryCount(prev => prev + 1);
|
||||
// Force reload by setting a new URI with cache buster
|
||||
if (imageUri) {
|
||||
const cacheBuster = `?retry=${Date.now()}`;
|
||||
const newUri = imageUri.includes('?')
|
||||
? `${imageUri}&cb=${Date.now()}`
|
||||
: `${imageUri}${cacheBuster}`;
|
||||
setImageUri(newUri);
|
||||
setImageError(false);
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
console.log(`Max retries (${maxRetries}) reached, showing fallback`);
|
||||
setImageError(true);
|
||||
}
|
||||
};
|
||||
|
||||
const avatarContent = (
|
||||
@ -91,4 +153,4 @@ const UserAvatar = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default UserAvatar;
|
||||
export default UserAvatar;
|
||||
|
@ -2,9 +2,9 @@
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import { View, TouchableOpacity, Image, ScrollView } from 'react-native';
|
||||
import { Text } from '@/components/ui/text';
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Heart, MessageCircle, Repeat, Share, User, Clock, Dumbbell, CheckCircle, FileText } from 'lucide-react-native';
|
||||
import { Heart, MessageCircle, Repeat, Share, Clock, Dumbbell, CheckCircle, FileText, User } from 'lucide-react-native';
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
import { useProfile } from '@/lib/hooks/useProfile';
|
||||
import { useNDK } from '@/lib/hooks/useNDK';
|
||||
import { FeedItem } from '@/lib/hooks/useSocialFeed';
|
||||
@ -74,7 +74,6 @@ export default function EnhancedSocialPost({ item, onPress }: SocialPostProps) {
|
||||
const { ndk } = useNDK();
|
||||
const [liked, setLiked] = useState(false);
|
||||
const [likeCount, setLikeCount] = useState(0);
|
||||
const [imageError, setImageError] = useState(false);
|
||||
const { profile } = useProfile(item.originalEvent.pubkey);
|
||||
|
||||
// Get likes count
|
||||
@ -121,11 +120,6 @@ export default function EnhancedSocialPost({ item, onPress }: SocialPostProps) {
|
||||
}
|
||||
};
|
||||
|
||||
// Handle image error
|
||||
const handleImageError = () => {
|
||||
setImageError(true);
|
||||
};
|
||||
|
||||
// Render based on feed item type
|
||||
const renderContent = () => {
|
||||
switch (item.type) {
|
||||
@ -138,7 +132,24 @@ export default function EnhancedSocialPost({ item, onPress }: SocialPostProps) {
|
||||
case 'social':
|
||||
return <SocialContent post={item.parsedContent as ParsedSocialPost} />;
|
||||
case 'article':
|
||||
return <ArticleContent article={item.parsedContent as ParsedLongformContent} />;
|
||||
// Only show ArticleContent for published articles (kind 30023)
|
||||
// Never show draft articles (kind 30024)
|
||||
if (item.originalEvent.kind === 30023) {
|
||||
return <ArticleContent article={item.parsedContent as ParsedLongformContent} />;
|
||||
} else {
|
||||
// For any other kinds, render as a social post
|
||||
// Create a proper ParsedSocialPost object with all required fields
|
||||
return <SocialContent post={{
|
||||
id: item.id,
|
||||
content: (item.parsedContent as ParsedLongformContent).title ||
|
||||
(item.parsedContent as ParsedLongformContent).content ||
|
||||
'Post content',
|
||||
author: item.originalEvent.pubkey || '',
|
||||
tags: [],
|
||||
createdAt: item.createdAt,
|
||||
quotedContent: undefined
|
||||
}} />;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@ -153,18 +164,12 @@ export default function EnhancedSocialPost({ item, onPress }: SocialPostProps) {
|
||||
<TouchableOpacity activeOpacity={0.7} onPress={onPress}>
|
||||
<View className="py-3 px-4">
|
||||
<View className="flex-row">
|
||||
<Avatar className="h-10 w-10 mr-3" alt={profile?.name || 'User'}>
|
||||
{profile?.image && !imageError ? (
|
||||
<AvatarImage
|
||||
source={{ uri: profile.image }}
|
||||
onError={handleImageError}
|
||||
/>
|
||||
) : (
|
||||
<AvatarFallback>
|
||||
<User size={18} />
|
||||
</AvatarFallback>
|
||||
)}
|
||||
</Avatar>
|
||||
<UserAvatar
|
||||
uri={profile?.image}
|
||||
size="md"
|
||||
fallback={profile?.name?.[0] || 'U'}
|
||||
className="mr-3"
|
||||
/>
|
||||
|
||||
<View className="flex-1">
|
||||
<View className="flex-row items-center">
|
||||
@ -529,4 +534,4 @@ function TemplateQuote({ template }: { template: ParsedWorkoutTemplate }) {
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -59,19 +59,54 @@ export default function SocialOfflineState() {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A component to display an offline banner at the top of social screens
|
||||
*/
|
||||
export function OfflineBanner() {
|
||||
const { isOnline, checkConnection } = useConnectivity();
|
||||
|
||||
if (isOnline) return null;
|
||||
|
||||
return (
|
||||
<View className="bg-muted p-2 border-b border-gray-300">
|
||||
<View className="flex-row items-center justify-between">
|
||||
<View className="flex-row items-center">
|
||||
<WifiOffIcon size={16} color="#666" style={{ marginRight: 8 }} />
|
||||
<Text style={{ color: '#666', fontSize: 14 }}>
|
||||
You're offline. Viewing cached content.
|
||||
</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={checkConnection}
|
||||
style={{
|
||||
backgroundColor: '#007bff',
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: '#fff', fontSize: 12 }}>
|
||||
Check
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A higher-order component that wraps social screens to handle offline state
|
||||
* Now shows an offline banner instead of replacing the entire component
|
||||
*/
|
||||
export function withOfflineState<P extends object>(
|
||||
Component: React.ComponentType<P>
|
||||
): React.FC<P> {
|
||||
return (props: P) => {
|
||||
const { isOnline } = useConnectivity();
|
||||
|
||||
if (!isOnline) {
|
||||
return <SocialOfflineState />;
|
||||
}
|
||||
|
||||
return <Component {...props} />;
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<OfflineBanner />
|
||||
<Component {...props} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
1056
docs/consolelogs/20250324-2.md
Normal file
1056
docs/consolelogs/20250324-2.md
Normal file
File diff suppressed because it is too large
Load Diff
1376
docs/consolelogs/20252303-2.md
Normal file
1376
docs/consolelogs/20252303-2.md
Normal file
File diff suppressed because it is too large
Load Diff
1963
docs/consolelogs/20252303.md
Normal file
1963
docs/consolelogs/20252303.md
Normal file
File diff suppressed because it is too large
Load Diff
411
docs/consolelogs/20252403.md
Normal file
411
docs/consolelogs/20252403.md
Normal file
@ -0,0 +1,411 @@
|
||||
💡 JavaScript logs will be removed from Metro in React Native 0.77! Please use React Native DevTools as your default tool. Tip: Type j in the terminal to open (requires Google Chrome or Microsoft Edge).
|
||||
(NOBRIDGE) LOG [useFeedHooks] Initialized POWR pubkey hex: 0bdd91e8a30d87d041eafd1871f17d426fa415c69a9a822eccad49017bac59e7
|
||||
(NOBRIDGE) LOG expo-av is available, will use VideoSplashScreen on iOS
|
||||
(NOBRIDGE) LOG Successfully imported VideoSplashScreen
|
||||
(NOBRIDGE) LOG _layout.tsx loaded
|
||||
(NOBRIDGE) LOG ⚠️ [expo-av]: Video component from `expo-av` is deprecated in favor of `expo-video`. See the documentation at https://docs.expo.dev/versions/latest/sdk/video/ for the new API reference.
|
||||
(NOBRIDGE) LOG Starting app initialization in background...
|
||||
(NOBRIDGE) LOG Network connectivity: offline
|
||||
(NOBRIDGE) LOG [NDK] Initializing...
|
||||
(NOBRIDGE) LOG [ConnectivityService] Network connection restored, triggering sync
|
||||
(NOBRIDGE) LOG [NDK] Found saved private key, initializing signer
|
||||
(NOBRIDGE) LOG [NDK] Login attempt starting
|
||||
(NOBRIDGE) LOG [NDK] Processing private key input
|
||||
(NOBRIDGE) LOG [NDK] Using provided key, format: hex length: 64
|
||||
(NOBRIDGE) LOG [NDK] Creating signer with key length: 64
|
||||
(NOBRIDGE) LOG [NDK] Signer created, setting on NDK
|
||||
(NOBRIDGE) LOG [NDK] Getting user from signer
|
||||
(NOBRIDGE) LOG [NDK] User retrieved, pubkey: 55127fc9...
|
||||
(NOBRIDGE) LOG [NDK] Fetching user profile
|
||||
(NOBRIDGE) LOG Loaded 0 favorite IDs from database
|
||||
(NOBRIDGE) LOG Video loaded successfully
|
||||
(NOBRIDGE) LOG Video loaded, hiding native splash screen
|
||||
(NOBRIDGE) LOG [NDK] Relay connected: wss://relay.damus.io/
|
||||
(NOBRIDGE) LOG [NDK] Relay connected: wss://purplepag.es/
|
||||
(NOBRIDGE) LOG [NDK] Profile fetched successfully
|
||||
(NOBRIDGE) LOG [NDK] Profile data available
|
||||
(NOBRIDGE) LOG [NDK] User profile loaded: Walter Sobchak
|
||||
(NOBRIDGE) LOG [NDK] Saving private key to secure storage
|
||||
(NOBRIDGE) LOG [NDK] Creating RelayService to import user preferences
|
||||
(NOBRIDGE) LOG [NDK] Setting NDK on RelayService
|
||||
(NOBRIDGE) LOG [RelayService] NDK instance set
|
||||
(NOBRIDGE) LOG [NDK] Importing relay metadata for user: 55127fc9...
|
||||
(NOBRIDGE) LOG [RelayService] Importing relays from metadata for user 55127fc9...
|
||||
(NOBRIDGE) LOG [NDK] Relay connected: wss://nos.lol/
|
||||
(NOBRIDGE) LOG [RelayService] Found relay list in event created at 2025-03-24T03:26:44.000Z
|
||||
(NOBRIDGE) LOG [RelayService] No relay tags found in event
|
||||
(NOBRIDGE) LOG [NDK] Successfully imported user relay preferences
|
||||
(NOBRIDGE) LOG [NDK] Login successful, updating state
|
||||
(NOBRIDGE) LOG [NDK] Login complete
|
||||
(NOBRIDGE) LOG App initialization completed!
|
||||
(NOBRIDGE) LOG [NDK] Relay connected: wss://relay.nostr.band/
|
||||
(NOBRIDGE) LOG Video finished playing
|
||||
(NOBRIDGE) LOG Splash video finished playing
|
||||
(NOBRIDGE) LOG [DB] Starting database initialization...
|
||||
(NOBRIDGE) LOG [DB] Opening database...
|
||||
(NOBRIDGE) LOG [DB] Creating schema...
|
||||
(NOBRIDGE) LOG [Schema] Initializing database on ios
|
||||
(NOBRIDGE) LOG [Schema] Current version: 11
|
||||
(NOBRIDGE) LOG [Schema] Current version: 11, Target version: 11
|
||||
(NOBRIDGE) LOG [Schema] Database already at version 11, checking for missing tables
|
||||
(NOBRIDGE) LOG [Schema] Checking for missing critical tables...
|
||||
(NOBRIDGE) LOG [Schema] Running migration v8 - Template management
|
||||
(NOBRIDGE) LOG [Schema] Migration v8 completed successfully
|
||||
(NOBRIDGE) LOG [Schema] Critical tables check complete
|
||||
(NOBRIDGE) LOG [Schema] Checking for missing critical tables...
|
||||
(NOBRIDGE) LOG [Schema] Running migration v8 - Template management
|
||||
(NOBRIDGE) LOG [Schema] Migration v8 completed successfully
|
||||
(NOBRIDGE) LOG [Schema] Critical tables check complete
|
||||
(NOBRIDGE) LOG [Schema] Running migration v8 - Template management
|
||||
(NOBRIDGE) LOG [Schema] Migration v8 completed successfully
|
||||
(NOBRIDGE) LOG [DB] Migration v8 executed successfully
|
||||
(NOBRIDGE) LOG [Schema] Running migration v9 - Enhanced Nostr metadata
|
||||
(NOBRIDGE) LOG [Schema] Migration v9 completed successfully
|
||||
(NOBRIDGE) LOG [DB] Migration v9 executed successfully
|
||||
(NOBRIDGE) LOG [Schema] Running migration v10 - Adding Favorites table
|
||||
(NOBRIDGE) LOG [Schema] Migration v10 completed successfully
|
||||
(NOBRIDGE) LOG [DB] Migration v10 executed successfully
|
||||
(NOBRIDGE) LOG [DB] Initializing services...
|
||||
(NOBRIDGE) LOG [DB] ExerciseService initialized
|
||||
(NOBRIDGE) LOG [DB] WorkoutService initialized
|
||||
(NOBRIDGE) LOG [DB] TemplateService initialized
|
||||
(NOBRIDGE) LOG [DB] PublicationQueueService initialized
|
||||
(NOBRIDGE) LOG [DB] FavoritesService initialized
|
||||
(NOBRIDGE) LOG [DB] POWRPackService initialized
|
||||
(NOBRIDGE) LOG [DB] FavoritesService fully initialized
|
||||
(NOBRIDGE) LOG
|
||||
--- Database Debug Info ---
|
||||
(NOBRIDGE) LOG Database Path: file:///Users/danielwyler/Library/Developer/CoreSimulator/Devices/2A575D76-EFD3-483A-B8A1-AB3A78107638/data/Containers/Data/Application/9845E875-95EC-461A-ABE0-6750715293F0/Documents/SQLite/powr.db
|
||||
(NOBRIDGE) LOG File Info: {"exists": true, "isDirectory": false, "md5": "5476e9c40a46ea950a9843ef506bb2c3", "modificationTime": 1742817336.3287776, "size": 253952, "uri": "file:///Users/danielwyler/Library/Developer/CoreSimulator/Devices/2A575D76-EFD3-483A-B8A1-AB3A78107638/data/Containers/Data/Application/9845E875-95EC-461A-ABE0-6750715293F0/Documents/SQLite/powr.db"}
|
||||
(NOBRIDGE) LOG Tables: ["schema_version", "exercises", "exercise_tags", "nostr_events", "event_tags", "templates", "template_exercises", "powr_packs", "powr_pack_items", "favorites", "nostr_workouts", "workouts", "workout_exercises", "workout_sets", "app_status", "feed_cache"]
|
||||
(NOBRIDGE) LOG ------------------------
|
||||
(NOBRIDGE) LOG [DB] Database initialized successfully
|
||||
(NOBRIDGE) LOG [DB] Database ready - triggering initial library refresh
|
||||
(NOBRIDGE) LOG [Database] Delayed initialization complete
|
||||
(NOBRIDGE) LOG [RelayInitializer] Setting NDK instance in ProfileImageCache
|
||||
(NOBRIDGE) LOG [RelayInitializer] Attempting to initialize SocialFeedCache (attempt 1/3)
|
||||
(NOBRIDGE) LOG Running SQL:
|
||||
CREATE TABLE IF NOT EXISTS feed_cache (
|
||||
event_id TEXT NOT NULL,
|
||||
feed_type TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
cached_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (event_id, feed_type)
|
||||
)
|
||||
(NOBRIDGE) LOG Parameters: []
|
||||
(NOBRIDGE) LOG [RelayInitializer] SocialFeedCache initialized with NDK successfully
|
||||
(NOBRIDGE) LOG [RelayInitializer] NDK available and online, loading relays...
|
||||
(NOBRIDGE) LOG [RelayStore] Loading relays...
|
||||
(NOBRIDGE) LOG [RelayStore] Created RelayService instance
|
||||
(NOBRIDGE) LOG [RelayService] NDK instance set
|
||||
(NOBRIDGE) LOG [RelayService] Found 5 relays in NDK pool
|
||||
(NOBRIDGE) LOG [RelayStore] Loaded 5 relays with status
|
||||
(NOBRIDGE) LOG Running SQL:
|
||||
CREATE INDEX IF NOT EXISTS idx_feed_cache_type_time
|
||||
ON feed_cache (feed_type, created_at DESC)
|
||||
(NOBRIDGE) LOG Parameters: []
|
||||
(NOBRIDGE) LOG [SocialFeedCache] Feed cache table initialized
|
||||
(NOBRIDGE) LOG [NDK] Relay connected: wss://powr.duckdns.org/
|
||||
(NOBRIDGE) LOG [NDK] Relay connected: wss://nostr.wine/
|
||||
(NOBRIDGE) LOG [NDK] Relay connected: wss://offchain.pub/
|
||||
(NOBRIDGE) LOG [NDK] Relay connected: wss://relay.snort.social/
|
||||
(NOBRIDGE) LOG NDK status: initialized
|
||||
(NOBRIDGE) LOG [useSocialFeed] Initializing SocialFeedService
|
||||
(NOBRIDGE) LOG [useSocialFeed] SocialFeedService initialized successfully
|
||||
(NOBRIDGE) LOG [useSocialFeed] Loading powr feed with authors: ["0bdd91e8a30d87d041eafd1871f17d426fa415c69a9a822eccad49017bac59e7"]
|
||||
(NOBRIDGE) LOG [useSocialFeed] Time range: since=2025-03-23T11:56:08.000Z, until=now
|
||||
(NOBRIDGE) LOG [useSocialFeed] Subscribing with filters: {"since":1742730968,"limit":30,"kinds":[1301,33401,33402,1,30023],"authors":["0bdd91e8a30d87d041eafd1871f17d426fa415c69a9a822eccad49017bac59e7"]}
|
||||
(NOBRIDGE) LOG [SocialFeedService] Subscribing to powr feed with filter: {"authors": ["0bdd91e8a30d87d041eafd1871f17d426fa415c69a9a822eccad49017bac59e7"], "kinds": [1301, 33401, 33402, 1, 30023], "limit": 30, "since": 1742730968}
|
||||
(NOBRIDGE) LOG [FollowingScreen] Contact list has 0 contacts
|
||||
(NOBRIDGE) LOG [useSocialFeed] Initializing SocialFeedService
|
||||
(NOBRIDGE) LOG [useSocialFeed] SocialFeedService initialized successfully
|
||||
(NOBRIDGE) LOG [useSocialFeed] Initializing SocialFeedService
|
||||
(NOBRIDGE) LOG [useSocialFeed] SocialFeedService initialized successfully
|
||||
(NOBRIDGE) LOG [useSocialFeed] Loading following feed with authors: undefined
|
||||
(NOBRIDGE) LOG [useSocialFeed] Time range: since=2025-03-23T11:56:08.000Z, until=now
|
||||
(NOBRIDGE) LOG [useSocialFeed] Following feed with no authors, skipping subscription
|
||||
(NOBRIDGE) LOG [useSocialFeed] Loading global feed with authors: undefined
|
||||
(NOBRIDGE) LOG [useSocialFeed] Time range: since=2025-03-23T11:56:08.000Z, until=now
|
||||
(NOBRIDGE) LOG [useSocialFeed] Subscribing with filters: [{"since":1742730968,"limit":30,"kinds":[1301,33401,33402]},{"since":1742730968,"limit":30,"kinds":[1,30023],"#t":["workout","fitness","powr","31days","crossfit","wod","gym","strength","cardio","training","exercise"]}]
|
||||
(NOBRIDGE) LOG [SocialFeedService] Subscribing to global feed with filter: [{"kinds": [1301, 33401, 33402], "limit": 30, "since": 1742730968}, {"#t": ["workout", "fitness", "powr", "31days", "crossfit", "wod", "gym", "strength", "cardio", "training", "exercise"], "kinds": [1, 30023], "limit": 30, "since": 1742730968}]
|
||||
(NOBRIDGE) LOG Processing event ebc91780f5ec85424c5e6d177615da8c3a21738b92a44fc5fca9851cee6c08f0, kind 1 from 55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21
|
||||
(NOBRIDGE) LOG Adding event ebc91780f5ec85424c5e6d177615da8c3a21738b92a44fc5fca9851cee6c08f0 to feed as social
|
||||
(NOBRIDGE) LOG [SocialFeedService] Received EOSE for powr feed
|
||||
(NOBRIDGE) LOG Processing event f496791f4f409f3cd7c7f807adcfc21f0d61194044eb77b39713b4fc92380a81, kind 1 from 918183d598acf2dca80cfc0cdea4a0ee5889251757ff3c75c5414f006d699ae5
|
||||
(NOBRIDGE) LOG Adding event f496791f4f409f3cd7c7f807adcfc21f0d61194044eb77b39713b4fc92380a81 to feed as social
|
||||
(NOBRIDGE) LOG Processing event a45ffb3b34f331008d89188073e8707614dd013736ca9c8ec9b7443febee7034, kind 1 from c465a1051794a507a55adebc0f044dc6e79d9b67a5e05aed4bf684afe088f976
|
||||
(NOBRIDGE) LOG Adding event a45ffb3b34f331008d89188073e8707614dd013736ca9c8ec9b7443febee7034 to feed as social
|
||||
(NOBRIDGE) LOG Processing event b7160aec02946e0a29b588b85d99c19740c07b166ca40deedf8f8a2dbefc4eab, kind 1 from 7ed7d5c3abf06fa1c00f71f879856769f46ac92354c129b3ed5562506927e200
|
||||
(NOBRIDGE) LOG Adding event b7160aec02946e0a29b588b85d99c19740c07b166ca40deedf8f8a2dbefc4eab to feed as social
|
||||
(NOBRIDGE) LOG Processing event d0070111bd6dba4091634b04da8fe4288d9c603a7db78bd2e87dcebccd1dc4dd, kind 1 from 5d4e1d2a6731875e9decb868e2240aa4b75a6d67199ed1387a6ff08aba272967
|
||||
(NOBRIDGE) LOG Adding event d0070111bd6dba4091634b04da8fe4288d9c603a7db78bd2e87dcebccd1dc4dd to feed as social
|
||||
(NOBRIDGE) LOG Processing event d9c6fd417778927925137394601a5c1f10ad24dd1190c73b714da82dbb21f085, kind 1 from f4d89779148ccd245c8d50914a284fd62d97cb0fb68b797a70f24a172b522db9
|
||||
(NOBRIDGE) LOG Adding event d9c6fd417778927925137394601a5c1f10ad24dd1190c73b714da82dbb21f085 to feed as social
|
||||
(NOBRIDGE) LOG Processing event dcb32ea5be84b024edae9d7931a140595f7dc91975b8e01de9d5bcba19ca370c, kind 1 from 14147765a18028fdb386db2891e747c11b2a32761e12a0e6e43d5f8451ee74bc
|
||||
(NOBRIDGE) LOG Adding event dcb32ea5be84b024edae9d7931a140595f7dc91975b8e01de9d5bcba19ca370c to feed as social
|
||||
(NOBRIDGE) LOG Processing event 83017c71d9bf6ec2ce89afa99fdc74515e29bc3bdb5e8d41c6ebacd8f1ab96d4, kind 1 from 5069ea44d8977e77c6aea605d0c5386b24504a3abd0fe8a3d1cf5f4cedca40a7
|
||||
(NOBRIDGE) LOG Adding event 83017c71d9bf6ec2ce89afa99fdc74515e29bc3bdb5e8d41c6ebacd8f1ab96d4 to feed as social
|
||||
(NOBRIDGE) LOG Processing event f9c49ba66580670ffbcb7e5bdd17acd1a0ccfa6dbfda86e0e26f4722c7b46f21, kind 1 from 60e5bccda24b32e79c75edb8e7c66e55202853932849f02e329c985b71ac3fbd
|
||||
(NOBRIDGE) LOG Adding event f9c49ba66580670ffbcb7e5bdd17acd1a0ccfa6dbfda86e0e26f4722c7b46f21 to feed as social
|
||||
(NOBRIDGE) LOG Processing event e8fd75341aed6bd47816343b648c4aebdbf5274c9360cc35bef1fd310d39a567, kind 1 from 7a6b8c7de171955c214ded7e35cc782cd6dddfd141abb1929c632f69348e6f49
|
||||
(NOBRIDGE) LOG Adding event e8fd75341aed6bd47816343b648c4aebdbf5274c9360cc35bef1fd310d39a567 to feed as social
|
||||
(NOBRIDGE) LOG Processing event fc9330f5f1f9a3762fb499f2cbc6e6a00e88421e139e02e5111bd793cf206698, kind 1 from ad8d9660c675d3b3a16fc1484c102782ba8370ae5f94ec5fc2b244a8c8a0a589
|
||||
(NOBRIDGE) LOG Adding event fc9330f5f1f9a3762fb499f2cbc6e6a00e88421e139e02e5111bd793cf206698 to feed as social
|
||||
(NOBRIDGE) LOG Processing event cee8815fb1a126f46c714d28768bda4f958455844a80bdf8cecf4be2cc91bf91, kind 1 from 81fa5b70a73a691c6e78823d035ffc44f6b505e6f03637759bd36e7a451539fd
|
||||
(NOBRIDGE) LOG Adding event cee8815fb1a126f46c714d28768bda4f958455844a80bdf8cecf4be2cc91bf91 to feed as social
|
||||
(NOBRIDGE) LOG Processing event 3a315ede408afdd5f939d22637327794ebea524bee5ef54a9c5596896d7f15e5, kind 1 from 43d7f07d10b9e662745e10b2fc201a74dc178a308c31409350d06b04477816e1
|
||||
(NOBRIDGE) LOG Adding event 3a315ede408afdd5f939d22637327794ebea524bee5ef54a9c5596896d7f15e5 to feed as social
|
||||
(NOBRIDGE) LOG Processing event 34f930b30255c6f8f2fba2b253bc0aa0122459fd070357feb2c8ebee9a2f4d53, kind 1 from 70632e1941795474cb154415afb79c09b58189afd2243bf81153d59710eb6092
|
||||
(NOBRIDGE) LOG Adding event 34f930b30255c6f8f2fba2b253bc0aa0122459fd070357feb2c8ebee9a2f4d53 to feed as social
|
||||
(NOBRIDGE) LOG Processing event 593bcd14e75770d20737948279e2ccc413d3acdfd6cbf56a71418ebf80b74475, kind 1 from b32d6b081c2422f57cbea1c1177159a93ffaa1913f540ae91e5fe3785b198cb2
|
||||
(NOBRIDGE) LOG Adding event 593bcd14e75770d20737948279e2ccc413d3acdfd6cbf56a71418ebf80b74475 to feed as social
|
||||
(NOBRIDGE) LOG [SocialFeedService] Received EOSE for global feed
|
||||
(NOBRIDGE) LOG Found 7 contacts via followSet()
|
||||
(NOBRIDGE) LOG [FollowingScreen] Contact list has 8 contacts
|
||||
(NOBRIDGE) LOG [FollowingScreen] First few contacts: 0c776e95521742beaf102523a8505c483e8c014ee0d3bd6457bb249034e5ff04, f8e6c64342f1e052480630e27e1016dce35fc3a614e60434fef4aa2503328ca9, 7ed7d5c3abf06fa1c00f71f879856769f46ac92354c129b3ed5562506927e200
|
||||
(NOBRIDGE) LOG Attempting fetch after delay
|
||||
(NOBRIDGE) LOG Explicitly connecting to relays: ["wss://relay.damus.io", "wss://nos.lol", "wss://relay.nostr.band", "wss://purplepag.es"]
|
||||
(NOBRIDGE) LOG Using pool to connect to relay: wss://relay.damus.io
|
||||
(NOBRIDGE) LOG Using pool to connect to relay: wss://nos.lol
|
||||
(NOBRIDGE) LOG Using pool to connect to relay: wss://relay.nostr.band
|
||||
(NOBRIDGE) LOG Using pool to connect to relay: wss://purplepag.es
|
||||
(NOBRIDGE) LOG NDK connection status: 8 relays connected
|
||||
(NOBRIDGE) LOG Fetching POWR packs from known publishers
|
||||
(NOBRIDGE) LOG Fetching from known publishers: ["55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21"]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT OR REPLACE INTO nostr_events
|
||||
(id, pubkey, kind, created_at, content, sig, raw_event, received_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["f496791f4f409f3cd7c7f807adcfc21f0d61194044eb77b39713b4fc92380a81", "918183d598acf2dca80cfc0cdea4a0ee5889251757ff3c75c5414f006d699ae5", 1, 1742810300, "Crossfit session 💪😎
|
||||
|
||||
https://m.primal.net/PrLA.jpg
|
||||
|
||||
#CrossFit #exercise #effort #gym", "d58cc84f10a22ceb3495c8b71f773f53619ef9ff15646e33a15fce39cf1c1e4c9ebe2a2e445ba0f501fa9f1bf36ac4e96e68d3314e2d530b9dc0cdbe3f6b0271", "{\"id\":\"f496791f4f409f3cd7c7f807adcfc21f0d61194044eb77b39713b4fc92380a81\",\"pubkey\":\"918183d598acf2dca80cfc0cdea4a0ee5889251757ff3c75c5414f006d699ae5\",\"kind\":1,\"created_at\":1742810300,\"content\":\"Crossfit session 💪😎\\n\\nhttps://m.primal.net/PrLA.jpg \\n\\n#CrossFit #exercise #effort #gym\",\"sig\":\"d58cc84f10a22ceb3495c8b71f773f53619ef9ff15646e33a15fce39cf1c1e4c9ebe2a2e445ba0f501fa9f1bf36ac4e96e68d3314e2d530b9dc0cdbe3f6b0271\",\"tags\":[[\"t\",\"CrossFit\"],[\"t\",\"exercise\"],[\"t\",\"effort\"],[\"t\",\"gym\"],[\"r\",\"wss://cache2.primal.net/v1\"],[\"r\",\"wss://eden.nostr.land/\"],[\"r\",\"wss://nos.lol/\"],[\"r\",\"wss://nostr.wine/\"],[\"r\",\"wss://offchain.pub/\"],[\"r\",\"wss://premium.primal.net/\"],[\"r\",\"wss://relay.chakany.systems/\"],[\"r\",\"wss://relay.damus.io/\"],[\"r\",\"wss://relay.getalby.com/v1\"],[\"r\",\"wss://relay.primal.net/\"],[\"r\",\"wss://relay.snort.social/\"],[\"r\",\"wss://nostr.bitcoiner.social/\"]]}", 1742817369448]
|
||||
(NOBRIDGE) LOG Running SQL: DELETE FROM event_tags WHERE event_id = ?
|
||||
(NOBRIDGE) LOG Parameters: ["f496791f4f409f3cd7c7f807adcfc21f0d61194044eb77b39713b4fc92380a81"]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT INTO event_tags (event_id, name, value, index_num) VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["f496791f4f409f3cd7c7f807adcfc21f0d61194044eb77b39713b4fc92380a81", "t", "CrossFit", 0]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT INTO event_tags (event_id, name, value, index_num) VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["f496791f4f409f3cd7c7f807adcfc21f0d61194044eb77b39713b4fc92380a81", "t", "exercise", 1]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT INTO event_tags (event_id, name, value, index_num) VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["f496791f4f409f3cd7c7f807adcfc21f0d61194044eb77b39713b4fc92380a81", "t", "effort", 2]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT INTO event_tags (event_id, name, value, index_num) VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["f496791f4f409f3cd7c7f807adcfc21f0d61194044eb77b39713b4fc92380a81", "t", "gym", 3]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT INTO event_tags (event_id, name, value, index_num) VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["f496791f4f409f3cd7c7f807adcfc21f0d61194044eb77b39713b4fc92380a81", "r", "wss://cache2.primal.net/v1", 4]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT INTO event_tags (event_id, name, value, index_num) VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["f496791f4f409f3cd7c7f807adcfc21f0d61194044eb77b39713b4fc92380a81", "r", "wss://eden.nostr.land/", 5]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT INTO event_tags (event_id, name, value, index_num) VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["f496791f4f409f3cd7c7f807adcfc21f0d61194044eb77b39713b4fc92380a81", "r", "wss://nos.lol/", 6]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT INTO event_tags (event_id, name, value, index_num) VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["f496791f4f409f3cd7c7f807adcfc21f0d61194044eb77b39713b4fc92380a81", "r", "wss://nostr.wine/", 7]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT INTO event_tags (event_id, name, value, index_num) VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["f496791f4f409f3cd7c7f807adcfc21f0d61194044eb77b39713b4fc92380a81", "r", "wss://offchain.pub/", 8]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT INTO event_tags (event_id, name, value, index_num) VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["f496791f4f409f3cd7c7f807adcfc21f0d61194044eb77b39713b4fc92380a81", "r", "wss://premium.primal.net/", 9]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT INTO event_tags (event_id, name, value, index_num) VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["f496791f4f409f3cd7c7f807adcfc21f0d61194044eb77b39713b4fc92380a81", "r", "wss://relay.chakany.systems/", 10]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT INTO event_tags (event_id, name, value, index_num) VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["f496791f4f409f3cd7c7f807adcfc21f0d61194044eb77b39713b4fc92380a81", "r", "wss://relay.damus.io/", 11]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT INTO event_tags (event_id, name, value, index_num) VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["f496791f4f409f3cd7c7f807adcfc21f0d61194044eb77b39713b4fc92380a81", "r", "wss://relay.getalby.com/v1", 12]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT INTO event_tags (event_id, name, value, index_num) VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["f496791f4f409f3cd7c7f807adcfc21f0d61194044eb77b39713b4fc92380a81", "r", "wss://relay.primal.net/", 13]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT INTO event_tags (event_id, name, value, index_num) VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["f496791f4f409f3cd7c7f807adcfc21f0d61194044eb77b39713b4fc92380a81", "r", "wss://relay.snort.social/", 14]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT INTO event_tags (event_id, name, value, index_num) VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["f496791f4f409f3cd7c7f807adcfc21f0d61194044eb77b39713b4fc92380a81", "r", "wss://nostr.bitcoiner.social/", 15]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT OR REPLACE INTO feed_cache
|
||||
(event_id, feed_type, created_at, cached_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["f496791f4f409f3cd7c7f807adcfc21f0d61194044eb77b39713b4fc92380a81", "global", 1742810300, 1742817369448]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT OR REPLACE INTO feed_cache
|
||||
(event_id, feed_type, created_at, cached_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["ebc91780f5ec85424c5e6d177615da8c3a21738b92a44fc5fca9851cee6c08f0", "global", 1742786395, 1742817369449]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT OR REPLACE INTO feed_cache
|
||||
(event_id, feed_type, created_at, cached_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["a45ffb3b34f331008d89188073e8707614dd013736ca9c8ec9b7443febee7034", "global", 1742788935, 1742817369450]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT OR REPLACE INTO feed_cache
|
||||
(event_id, feed_type, created_at, cached_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["b7160aec02946e0a29b588b85d99c19740c07b166ca40deedf8f8a2dbefc4eab", "global", 1742782158, 1742817369450]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT OR REPLACE INTO feed_cache
|
||||
(event_id, feed_type, created_at, cached_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["d0070111bd6dba4091634b04da8fe4288d9c603a7db78bd2e87dcebccd1dc4dd", "global", 1742780020, 1742817369450]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT OR REPLACE INTO feed_cache
|
||||
(event_id, feed_type, created_at, cached_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["d9c6fd417778927925137394601a5c1f10ad24dd1190c73b714da82dbb21f085", "global", 1742775869, 1742817369450]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT OR REPLACE INTO feed_cache
|
||||
(event_id, feed_type, created_at, cached_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["dcb32ea5be84b024edae9d7931a140595f7dc91975b8e01de9d5bcba19ca370c", "global", 1742749229, 1742817369450]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT OR REPLACE INTO feed_cache
|
||||
(event_id, feed_type, created_at, cached_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["83017c71d9bf6ec2ce89afa99fdc74515e29bc3bdb5e8d41c6ebacd8f1ab96d4", "global", 1742741497, 1742817369450]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT OR REPLACE INTO feed_cache
|
||||
(event_id, feed_type, created_at, cached_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["f9c49ba66580670ffbcb7e5bdd17acd1a0ccfa6dbfda86e0e26f4722c7b46f21", "global", 1742760959, 1742817369450]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT OR REPLACE INTO feed_cache
|
||||
(event_id, feed_type, created_at, cached_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["e8fd75341aed6bd47816343b648c4aebdbf5274c9360cc35bef1fd310d39a567", "global", 1742758559, 1742817369450]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT OR REPLACE INTO feed_cache
|
||||
(event_id, feed_type, created_at, cached_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["fc9330f5f1f9a3762fb499f2cbc6e6a00e88421e139e02e5111bd793cf206698", "global", 1742750608, 1742817369450]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT OR REPLACE INTO feed_cache
|
||||
(event_id, feed_type, created_at, cached_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["cee8815fb1a126f46c714d28768bda4f958455844a80bdf8cecf4be2cc91bf91", "global", 1742745450, 1742817369450]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT OR REPLACE INTO feed_cache
|
||||
(event_id, feed_type, created_at, cached_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["3a315ede408afdd5f939d22637327794ebea524bee5ef54a9c5596896d7f15e5", "global", 1742746988, 1742817369450]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT OR REPLACE INTO feed_cache
|
||||
(event_id, feed_type, created_at, cached_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["34f930b30255c6f8f2fba2b253bc0aa0122459fd070357feb2c8ebee9a2f4d53", "global", 1742739424, 1742817369450]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT OR REPLACE INTO feed_cache
|
||||
(event_id, feed_type, created_at, cached_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["593bcd14e75770d20737948279e2ccc413d3acdfd6cbf56a71418ebf80b74475", "global", 1742731342, 1742817369450]
|
||||
(NOBRIDGE) LOG Fetched 14 events from known publishers
|
||||
(NOBRIDGE) LOG First event basic info: {"contentPreview": "This POWR Pack includes relay hints to improve con", "id": "42e46198f802ac821b5a82b96d083e0e5cb9a4790b79a007b0a65dfe1020576c", "kind": 30004, "pubkey": "55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21", "tagsCount": 13}
|
||||
(NOBRIDGE) LOG Created 14 simplified packs
|
||||
(NOBRIDGE) LOG First simplified pack: {"id": "42e46198f802ac821b5a82b96d083e0e5cb9a4790b79a007b0a65dfe1020576c", "tagsCount": 13}
|
||||
(NOBRIDGE) LOG Set featuredPacks state with simplified packs
|
||||
(NOBRIDGE) LOG [useSocialFeed] Initializing SocialFeedService
|
||||
(NOBRIDGE) LOG [useSocialFeed] SocialFeedService initialized successfully
|
||||
(NOBRIDGE) LOG [useSocialFeed] Loading profile feed with authors: ["55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21"]
|
||||
(NOBRIDGE) LOG [useSocialFeed] Time range: since=2025-03-23T11:59:00.000Z, until=now
|
||||
(NOBRIDGE) LOG [useSocialFeed] Subscribing with filters: [{"since":1742731140,"limit":30,"kinds":[1301,33401,33402],"authors":["55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21"]},{"since":1742731140,"limit":30,"kinds":[1,30023],"authors":["55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21"]}]
|
||||
(NOBRIDGE) LOG [SocialFeedService] Subscribing to profile feed with filter: [{"authors": ["55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21"], "kinds": [1301, 33401, 33402], "limit": 30, "since": 1742731140}, {"authors": ["55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21"], "kinds": [1, 30023], "limit": 30, "since": 1742731140}]
|
||||
(NOBRIDGE) LOG [TemplateService] Template sources:
|
||||
(NOBRIDGE) LOG - nostr: 1
|
||||
(NOBRIDGE) LOG [TemplateService] Found 1 templates
|
||||
(NOBRIDGE) LOG - Simple Strength Workout (local:m8mja9fd-oz0peevxz7) [source: nostr]
|
||||
(NOBRIDGE) LOG Fetching exercises for template local:m8mja9fd-oz0peevxz7
|
||||
(NOBRIDGE) LOG Expected template exercises: 3
|
||||
(NOBRIDGE) LOG Found 3 template exercises in database
|
||||
(NOBRIDGE) LOG Looking up exercise with ID: local:m8mja98y-984imro9c6f
|
||||
(NOBRIDGE) LOG Looking up exercise with ID: local:m8mja9f9-xh867hc8yoa
|
||||
(NOBRIDGE) LOG Looking up exercise with ID: local:m8mja9fb-vsgpmeikfie
|
||||
(NOBRIDGE) LOG Returning 3 template exercises
|
||||
(NOBRIDGE) LOG [TemplateService] Template sources:
|
||||
(NOBRIDGE) LOG - nostr: 1
|
||||
(NOBRIDGE) LOG [TemplateService] Found 1 templates
|
||||
(NOBRIDGE) LOG - Simple Strength Workout (local:m8mja9fd-oz0peevxz7) [source: nostr]
|
||||
(NOBRIDGE) LOG Fetching exercises for template local:m8mja9fd-oz0peevxz7
|
||||
(NOBRIDGE) LOG Expected template exercises: 3
|
||||
(NOBRIDGE) LOG Found 3 template exercises in database
|
||||
(NOBRIDGE) LOG Looking up exercise with ID: local:m8mja98y-984imro9c6f
|
||||
(NOBRIDGE) LOG Looking up exercise with ID: local:m8mja9f9-xh867hc8yoa
|
||||
(NOBRIDGE) LOG Looking up exercise with ID: local:m8mja9fb-vsgpmeikfie
|
||||
(NOBRIDGE) LOG Returning 3 template exercises
|
||||
(NOBRIDGE) LOG Processing event ebc91780f5ec85424c5e6d177615da8c3a21738b92a44fc5fca9851cee6c08f0, kind 1 from 55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21
|
||||
(NOBRIDGE) LOG Adding event ebc91780f5ec85424c5e6d177615da8c3a21738b92a44fc5fca9851cee6c08f0 to feed as social
|
||||
(NOBRIDGE) LOG [SocialFeedService] Received EOSE for profile feed
|
||||
(NOBRIDGE) LOG Refreshing profile feed
|
||||
(NOBRIDGE) LOG [useSocialFeed] Subscription on cooldown, skipping
|
||||
(NOBRIDGE) LOG Refreshing profile feed
|
||||
(NOBRIDGE) LOG [useSocialFeed] Cleaning up existing subscription for profile feed
|
||||
(NOBRIDGE) LOG [SocialFeedService] Unsubscribing from profile feed
|
||||
(NOBRIDGE) LOG [useSocialFeed] Loading profile feed with authors: ["55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21"]
|
||||
(NOBRIDGE) LOG [useSocialFeed] Time range: since=2025-03-23T11:59:05.000Z, until=now
|
||||
(NOBRIDGE) LOG [useSocialFeed] Subscribing with filters: [{"since":1742731145,"limit":30,"kinds":[1301,33401,33402],"authors":["55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21"]},{"since":1742731145,"limit":30,"kinds":[1,30023],"authors":["55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21"]}]
|
||||
(NOBRIDGE) LOG [SocialFeedService] Subscribing to profile feed with filter: [{"authors": ["55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21"], "kinds": [1301, 33401, 33402], "limit": 30, "since": 1742731145}, {"authors": ["55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21"], "kinds": [1, 30023], "limit": 30, "since": 1742731145}]
|
||||
(NOBRIDGE) LOG Processing event ebc91780f5ec85424c5e6d177615da8c3a21738b92a44fc5fca9851cee6c08f0, kind 1 from 55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21
|
||||
(NOBRIDGE) LOG Adding event ebc91780f5ec85424c5e6d177615da8c3a21738b92a44fc5fca9851cee6c08f0 to feed as social
|
||||
(NOBRIDGE) LOG [SocialFeedService] Received EOSE for profile feed
|
||||
(NOBRIDGE) LOG [NDK] Relay disconnected: wss://relay.snort.social/
|
||||
(NOBRIDGE) LOG [NDK] Relay connected: wss://relay.snort.social/
|
||||
(NOBRIDGE) LOG Processing event a985a37491b5e755da13dc255e409af0afbbec92a767e7197b7adeb3fe7564a3, kind 1 from e516ecb882ffbc9ba87353342e0c9dbd3e9cf55a00316ca1d23efa3a1be0b167
|
||||
(NOBRIDGE) LOG Adding event a985a37491b5e755da13dc255e409af0afbbec92a767e7197b7adeb3fe7564a3 to feed as social
|
||||
(NOBRIDGE) LOG Running SQL: INSERT OR REPLACE INTO nostr_events
|
||||
(id, pubkey, kind, created_at, content, sig, raw_event, received_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["a985a37491b5e755da13dc255e409af0afbbec92a767e7197b7adeb3fe7564a3", "e516ecb882ffbc9ba87353342e0c9dbd3e9cf55a00316ca1d23efa3a1be0b167", 1, 1742818038, "Gm. short workout before work
|
||||
|
||||
#workout #ohp
|
||||
|
||||
|
||||
OHP Heavy
|
||||
March 24 08:09-09:06
|
||||
|
||||
Barbell Shoulder Press
|
||||
1. 15 Kg x 5 Reps
|
||||
2. 19 Kg x 5 Reps
|
||||
3. 23 Kg x 3 Reps
|
||||
4. 29 Kg x 5 Reps
|
||||
5. 32 Kg x 3 Reps
|
||||
6. 36 Kg x 6 Reps
|
||||
7. 33 Kg x 7 Reps
|
||||
|
||||
Barbell Bench Press
|
||||
1. 41 Kg x 10 Reps
|
||||
2. 37 Kg x 10 Reps
|
||||
3. 34 Kg x 10 Reps
|
||||
4. 31 Kg x 10 Reps
|
||||
5. 27 Kg x 10 Reps
|
||||
|
||||
Pullup
|
||||
1. 85 Kg x 10 Reps
|
||||
2. 85 Kg x 7 Reps
|
||||
3. 85 Kg x 6 Reps
|
||||
4. 85 Kg x 5 Reps
|
||||
5. 85 Kg x 4 Reps
|
||||
|
||||
Dumbbell Curl
|
||||
1. 10 Kg x 10 Reps
|
||||
2. 10 Kg x 10 Reps
|
||||
3. 10 Kg x 10 Reps", "592f267cecc4f7d7d2f026edcf24c8615bf529d8b193e0db22c640cb49e5b13a2eb9f1ec1626724871edcb3164e462acbaa6d870a888765f33c8ded666fad004", "{\"id\":\"a985a37491b5e755da13dc255e409af0afbbec92a767e7197b7adeb3fe7564a3\",\"pubkey\":\"e516ecb882ffbc9ba87353342e0c9dbd3e9cf55a00316ca1d23efa3a1be0b167\",\"kind\":1,\"created_at\":1742818038,\"content\":\"Gm. short workout before work\\n\\n#workout #ohp\\n\\n\\nOHP Heavy\\nMarch 24 08:09-09:06\\n\\nBarbell Shoulder Press\\n1. 15 Kg x 5 Reps\\n2. 19 Kg x 5 Reps\\n3. 23 Kg x 3 Reps\\n4. 29 Kg x 5 Reps\\n5. 32 Kg x 3 Reps\\n6. 36 Kg x 6 Reps\\n7. 33 Kg x 7 Reps\\n\\nBarbell Bench Press\\n1. 41 Kg x 10 Reps\\n2. 37 Kg x 10 Reps\\n3. 34 Kg x 10 Reps\\n4. 31 Kg x 10 Reps\\n5. 27 Kg x 10 Reps\\n\\nPullup\\n1. 85 Kg x 10 Reps\\n2. 85 Kg x 7 Reps\\n3. 85 Kg x 6 Reps\\n4. 85 Kg x 5 Reps\\n5. 85 Kg x 4 Reps\\n\\nDumbbell Curl\\n1. 10 Kg x 10 Reps\\n2. 10 Kg x 10 Reps\\n3. 10 Kg x 10 Reps\",\"sig\":\"592f267cecc4f7d7d2f026edcf24c8615bf529d8b193e0db22c640cb49e5b13a2eb9f1ec1626724871edcb3164e462acbaa6d870a888765f33c8ded666fad004\",\"tags\":[[\"t\",\"workout\"],[\"t\",\"ohp\"]]}", 1742818040751]
|
||||
(NOBRIDGE) LOG Running SQL: DELETE FROM event_tags WHERE event_id = ?
|
||||
(NOBRIDGE) LOG Parameters: ["a985a37491b5e755da13dc255e409af0afbbec92a767e7197b7adeb3fe7564a3"]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT INTO event_tags (event_id, name, value, index_num) VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["a985a37491b5e755da13dc255e409af0afbbec92a767e7197b7adeb3fe7564a3", "t", "workout", 0]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT INTO event_tags (event_id, name, value, index_num) VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["a985a37491b5e755da13dc255e409af0afbbec92a767e7197b7adeb3fe7564a3", "t", "ohp", 1]
|
||||
(NOBRIDGE) LOG Running SQL: INSERT OR REPLACE INTO feed_cache
|
||||
(event_id, feed_type, created_at, cached_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
(NOBRIDGE) LOG Parameters: ["a985a37491b5e755da13dc255e409af0afbbec92a767e7197b7adeb3fe7564a3", "global", 1742818038, 1742818040751]
|
||||
(NOBRIDGE) LOG Refreshing following feed
|
||||
(NOBRIDGE) LOG [useSocialFeed] Loading following feed with authors: ["0c776e95521742beaf102523a8505c483e8c014ee0d3bd6457bb249034e5ff04", "f8e6c64342f1e052480630e27e1016dce35fc3a614e60434fef4aa2503328ca9", "7ed7d5c3abf06fa1c00f71f879856769f46ac92354c129b3ed5562506927e200", "3129509e23d3a6125e1451a5912dbe01099e151726c4766b44e1ecb8c846f506", "55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21", "2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884", "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52", "0bdd91e8a30d87d041eafd1871f17d426fa415c69a9a822eccad49017bac59e7"]
|
||||
(NOBRIDGE) LOG [useSocialFeed] Time range: since=2025-03-23T11:56:09.000Z, until=now
|
||||
(NOBRIDGE) LOG [useSocialFeed] Subscribing with filters: [{"since":1742730969,"limit":30,"kinds":[1301,33401,33402],"authors":["0c776e95521742beaf102523a8505c483e8c014ee0d3bd6457bb249034e5ff04","f8e6c64342f1e052480630e27e1016dce35fc3a614e60434fef4aa2503328ca9","7ed7d5c3abf06fa1c00f71f879856769f46ac92354c129b3ed5562506927e200","3129509e23d3a6125e1451a5912dbe01099e151726c4766b44e1ecb8c846f506","55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21","2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52","0bdd91e8a30d87d041eafd1871f17d426fa415c69a9a822eccad49017bac59e7"]},{"since":1742730969,"limit":30,"kinds":[1,30023],"authors":["0c776e95521742beaf102523a8505c483e8c014ee0d3bd6457bb249034e5ff04","f8e6c64342f1e052480630e27e1016dce35fc3a614e60434fef4aa2503328ca9","7ed7d5c3abf06fa1c00f71f879856769f46ac92354c129b3ed5562506927e200","3129509e23d3a6125e1451a5912dbe01099e151726c4766b44e1ecb8c846f506","55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21","2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52","0bdd91e8a30d87d041eafd1871f17d426fa415c69a9a822eccad49017bac59e7"],"#t":["workout","fitness","powr","31days","crossfit","wod","gym","strength","cardio","training","exercise"]}]
|
||||
(NOBRIDGE) LOG [SocialFeedService] Subscribing to following feed with filter: [{"authors": ["0c776e95521742beaf102523a8505c483e8c014ee0d3bd6457bb249034e5ff04", "f8e6c64342f1e052480630e27e1016dce35fc3a614e60434fef4aa2503328ca9", "7ed7d5c3abf06fa1c00f71f879856769f46ac92354c129b3ed5562506927e200", "3129509e23d3a6125e1451a5912dbe01099e151726c4766b44e1ecb8c846f506", "55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21", "2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884", "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52", "0bdd91e8a30d87d041eafd1871f17d426fa415c69a9a822eccad49017bac59e7"], "kinds": [1301, 33401, 33402], "limit": 30, "since": 1742730969}, {"#t": ["workout", "fitness", "powr", "31days", "crossfit", "wod", "gym", "strength", "cardio", "training", "exercise"], "authors": ["0c776e95521742beaf102523a8505c483e8c014ee0d3bd6457bb249034e5ff04", "f8e6c64342f1e052480630e27e1016dce35fc3a614e60434fef4aa2503328ca9", "7ed7d5c3abf06fa1c00f71f879856769f46ac92354c129b3ed5562506927e200", "3129509e23d3a6125e1451a5912dbe01099e151726c4766b44e1ecb8c846f506", "55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21", "2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884", "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52", "0bdd91e8a30d87d041eafd1871f17d426fa415c69a9a822eccad49017bac59e7"], "kinds": [1, 30023], "limit": 30, "since": 1742730969}]
|
||||
(NOBRIDGE) LOG Processing event ebc91780f5ec85424c5e6d177615da8c3a21738b92a44fc5fca9851cee6c08f0, kind 1 from 55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21
|
||||
(NOBRIDGE) LOG Adding event ebc91780f5ec85424c5e6d177615da8c3a21738b92a44fc5fca9851cee6c08f0 to feed as social
|
||||
(NOBRIDGE) LOG Processing event b7160aec02946e0a29b588b85d99c19740c07b166ca40deedf8f8a2dbefc4eab, kind 1 from 7ed7d5c3abf06fa1c00f71f879856769f46ac92354c129b3ed5562506927e200
|
||||
(NOBRIDGE) LOG Adding event b7160aec02946e0a29b588b85d99c19740c07b166ca40deedf8f8a2dbefc4eab to feed as social
|
||||
(NOBRIDGE) LOG [SocialFeedService] Received EOSE for following feed
|
||||
(NOBRIDGE) LOG Refreshing following feed
|
||||
(NOBRIDGE) LOG [useSocialFeed] Subscription on cooldown, skipping
|
||||
(NOBRIDGE) LOG === RELAY CONNECTION STATUS ===
|
||||
(NOBRIDGE) LOG Connected to 8 relays:
|
||||
(NOBRIDGE) LOG - wss://powr.duckdns.org/: 5
|
||||
(NOBRIDGE) LOG - wss://relay.damus.io/: 5
|
||||
(NOBRIDGE) LOG - wss://relay.nostr.band/: 5
|
||||
(NOBRIDGE) LOG - wss://purplepag.es/: 5
|
||||
(NOBRIDGE) LOG - wss://nos.lol/: 5
|
||||
(NOBRIDGE) LOG - wss://relay.snort.social/: 5
|
||||
(NOBRIDGE) LOG - wss://nostr.wine/: 5
|
||||
(NOBRIDGE) LOG - wss://offchain.pub/: 5
|
||||
(NOBRIDGE) LOG ===============================
|
||||
(NOBRIDGE) LOG Refreshing following feed
|
||||
(NOBRIDGE) LOG [useSocialFeed] Cleaning up existing subscription for following feed
|
||||
(NOBRIDGE) LOG [SocialFeedService] Unsubscribing from following feed
|
||||
(NOBRIDGE) LOG [useSocialFeed] Loading following feed with authors: ["0c776e95521742beaf102523a8505c483e8c014ee0d3bd6457bb249034e5ff04", "f8e6c64342f1e052480630e27e1016dce35fc3a614e60434fef4aa2503328ca9", "7ed7d5c3abf06fa1c00f71f879856769f46ac92354c129b3ed5562506927e200", "3129509e23d3a6125e1451a5912dbe01099e151726c4766b44e1ecb8c846f506", "55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21", "2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884", "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52", "0bdd91e8a30d87d041eafd1871f17d426fa415c69a9a822eccad49017bac59e7"]
|
||||
(NOBRIDGE) LOG [useSocialFeed] Time range: since=2025-03-23T11:56:09.000Z, until=now
|
||||
(NOBRIDGE) LOG [useSocialFeed] Subscribing with filters: [{"since":1742730969,"limit":30,"kinds":[1301,33401,33402],"authors":["0c776e95521742beaf102523a8505c483e8c014ee0d3bd6457bb249034e5ff04","f8e6c64342f1e052480630e27e1016dce35fc3a614e60434fef4aa2503328ca9","7ed7d5c3abf06fa1c00f71f879856769f46ac92354c129b3ed5562506927e200","3129509e23d3a6125e1451a5912dbe01099e151726c4766b44e1ecb8c846f506","55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21","2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52","0bdd91e8a30d87d041eafd1871f17d426fa415c69a9a822eccad49017bac59e7"]},{"since":1742730969,"limit":30,"kinds":[1,30023],"authors":["0c776e95521742beaf102523a8505c483e8c014ee0d3bd6457bb249034e5ff04","f8e6c64342f1e052480630e27e1016dce35fc3a614e60434fef4aa2503328ca9","7ed7d5c3abf06fa1c00f71f879856769f46ac92354c129b3ed5562506927e200","3129509e23d3a6125e1451a5912dbe01099e151726c4766b44e1ecb8c846f506","55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21","2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52","0bdd91e8a30d87d041eafd1871f17d426fa415c69a9a822eccad49017bac59e7"],"#t":["workout","fitness","powr","31days","crossfit","wod","gym","strength","cardio","training","exercise"]}]
|
||||
(NOBRIDGE) LOG [SocialFeedService] Subscribing to following feed with filter: [{"authors": ["0c776e95521742beaf102523a8505c483e8c014ee0d3bd6457bb249034e5ff04", "f8e6c64342f1e052480630e27e1016dce35fc3a614e60434fef4aa2503328ca9", "7ed7d5c3abf06fa1c00f71f879856769f46ac92354c129b3ed5562506927e200", "3129509e23d3a6125e1451a5912dbe01099e151726c4766b44e1ecb8c846f506", "55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21", "2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884", "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52", "0bdd91e8a30d87d041eafd1871f17d426fa415c69a9a822eccad49017bac59e7"], "kinds": [1301, 33401, 33402], "limit": 30, "since": 1742730969}, {"#t": ["workout", "fitness", "powr", "31days", "crossfit", "wod", "gym", "strength", "cardio", "training", "exercise"], "authors": ["0c776e95521742beaf102523a8505c483e8c014ee0d3bd6457bb249034e5ff04", "f8e6c64342f1e052480630e27e1016dce35fc3a614e60434fef4aa2503328ca9", "7ed7d5c3abf06fa1c00f71f879856769f46ac92354c129b3ed5562506927e200", "3129509e23d3a6125e1451a5912dbe01099e151726c4766b44e1ecb8c846f506", "55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21", "2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884", "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52", "0bdd91e8a30d87d041eafd1871f17d426fa415c69a9a822eccad49017bac59e7"], "kinds": [1, 30023], "limit": 30, "since": 1742730969}]
|
||||
(NOBRIDGE) LOG Processing event ebc91780f5ec85424c5e6d177615da8c3a21738b92a44fc5fca9851cee6c08f0, kind 1 from 55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21
|
||||
(NOBRIDGE) LOG Adding event ebc91780f5ec85424c5e6d177615da8c3a21738b92a44fc5fca9851cee6c08f0 to feed as social
|
||||
(NOBRIDGE) LOG Processing event b7160aec02946e0a29b588b85d99c19740c07b166ca40deedf8f8a2dbefc4eab, kind 1 from 7ed7d5c3abf06fa1c00f71f879856769f46ac92354c129b3ed5562506927e200
|
||||
(NOBRIDGE) LOG Adding event b7160aec02946e0a29b588b85d99c19740c07b166ca40deedf8f8a2dbefc4eab to feed as social
|
||||
(NOBRIDGE) LOG [SocialFeedService] Received EOSE for following feed
|
199
docs/design/Social/SocialFeedCacheImplementation.md
Normal file
199
docs/design/Social/SocialFeedCacheImplementation.md
Normal file
@ -0,0 +1,199 @@
|
||||
# Social Feed Cache Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the implementation of the Social Feed Cache system in the POWR app. The cache system is designed to provide offline access to social feed data, reduce network usage, and improve performance.
|
||||
|
||||
## Key Components
|
||||
|
||||
1. **SocialFeedCache**: The main service that handles caching of social feed events
|
||||
2. **EventCache**: A service for caching individual Nostr events
|
||||
3. **useSocialFeed**: A hook that provides access to the social feed data
|
||||
4. **RelayInitializer**: A component that initializes the cache system
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Write Buffer System
|
||||
|
||||
The SocialFeedCache uses a write buffer system to batch database operations and reduce transaction conflicts. This approach is inspired by the Olas NDK Mobile implementation.
|
||||
|
||||
```typescript
|
||||
private writeBuffer: { query: string; params: any[] }[] = [];
|
||||
private bufferFlushTimer: NodeJS.Timeout | null = null;
|
||||
private bufferFlushTimeout: number = 100; // milliseconds
|
||||
private processingTransaction: boolean = false;
|
||||
|
||||
private bufferWrite(query: string, params: any[]) {
|
||||
this.writeBuffer.push({ query, params });
|
||||
|
||||
if (!this.bufferFlushTimer) {
|
||||
this.bufferFlushTimer = setTimeout(() => this.flushWriteBuffer(), this.bufferFlushTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
private async flushWriteBuffer() {
|
||||
if (this.writeBuffer.length === 0 || this.processingTransaction) return;
|
||||
|
||||
const bufferCopy = [...this.writeBuffer];
|
||||
this.writeBuffer = [];
|
||||
|
||||
this.processingTransaction = true;
|
||||
|
||||
try {
|
||||
await this.db.withTransactionAsync(async () => {
|
||||
for (const { query, params } of bufferCopy) {
|
||||
await this.db.runAsync(query, params);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[SocialFeedCache] Error flushing write buffer:', error);
|
||||
// If there was an error, add the operations back to the buffer
|
||||
for (const op of bufferCopy) {
|
||||
if (!this.writeBuffer.some(item =>
|
||||
item.query === op.query &&
|
||||
JSON.stringify(item.params) === JSON.stringify(op.params)
|
||||
)) {
|
||||
this.writeBuffer.push(op);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.processingTransaction = false;
|
||||
}
|
||||
|
||||
this.bufferFlushTimer = null;
|
||||
|
||||
// If there are more operations, start a new timer
|
||||
if (this.writeBuffer.length > 0) {
|
||||
this.bufferFlushTimer = setTimeout(() => this.flushWriteBuffer(), this.bufferFlushTimeout);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### In-Memory Tracking with LRU Cache
|
||||
|
||||
To prevent redundant database operations, the SocialFeedCache uses an LRU (Least Recently Used) cache to track known events:
|
||||
|
||||
```typescript
|
||||
private knownEventIds: LRUCache<string, number>; // Event ID -> timestamp
|
||||
|
||||
constructor(database: SQLiteDatabase) {
|
||||
this.db = new DbService(database);
|
||||
this.eventCache = new EventCache(database);
|
||||
|
||||
// Initialize LRU cache for known events (limit to 1000 entries)
|
||||
this.knownEventIds = new LRUCache<string, number>({ maxSize: 1000 });
|
||||
|
||||
// Ensure feed_cache table exists
|
||||
this.initializeTable();
|
||||
}
|
||||
```
|
||||
|
||||
### Debounced Subscriptions
|
||||
|
||||
The `useSocialFeed` hook implements debouncing to prevent rapid resubscriptions:
|
||||
|
||||
```typescript
|
||||
// Subscription cooldown to prevent rapid resubscriptions
|
||||
const subscriptionCooldown = useRef<NodeJS.Timeout | null>(null);
|
||||
const cooldownPeriod = 2000; // 2 seconds
|
||||
const subscriptionAttempts = useRef(0);
|
||||
const maxSubscriptionAttempts = 3;
|
||||
|
||||
// In loadFeed function:
|
||||
// Prevent rapid resubscriptions
|
||||
if (subscriptionCooldown.current) {
|
||||
console.log('[useSocialFeed] Subscription on cooldown, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
// Track subscription attempts to prevent infinite loops
|
||||
subscriptionAttempts.current += 1;
|
||||
if (subscriptionAttempts.current > maxSubscriptionAttempts) {
|
||||
console.error(`[useSocialFeed] Too many subscription attempts (${subscriptionAttempts.current}), giving up`);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set a cooldown to prevent rapid resubscriptions
|
||||
subscriptionCooldown.current = setTimeout(() => {
|
||||
subscriptionCooldown.current = null;
|
||||
// Reset attempt counter after cooldown period
|
||||
subscriptionAttempts.current = 0;
|
||||
}, cooldownPeriod);
|
||||
```
|
||||
|
||||
### Proper Initialization
|
||||
|
||||
The RelayInitializer component ensures that the SocialFeedCache is properly initialized with the NDK instance:
|
||||
|
||||
```typescript
|
||||
// Initialize ProfileImageCache and SocialFeedCache with NDK instance
|
||||
useEffect(() => {
|
||||
if (ndk) {
|
||||
console.log('[RelayInitializer] Setting NDK instance in ProfileImageCache');
|
||||
profileImageCache.setNDK(ndk);
|
||||
|
||||
// Initialize SocialFeedCache with NDK instance
|
||||
if (db) {
|
||||
try {
|
||||
const socialFeedCache = getSocialFeedCache(db);
|
||||
socialFeedCache.setNDK(ndk);
|
||||
console.log('[RelayInitializer] SocialFeedCache initialized with NDK');
|
||||
} catch (error) {
|
||||
console.error('[RelayInitializer] Error initializing SocialFeedCache:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [ndk, db]);
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Reduced Transaction Conflicts**: The write buffer system prevents transaction conflicts by batching operations.
|
||||
2. **Improved Performance**: The LRU cache reduces redundant database operations.
|
||||
3. **Better Error Handling**: The system includes robust error handling to prevent cascading failures.
|
||||
4. **Offline Support**: The cache system provides offline access to social feed data.
|
||||
5. **Reduced Network Usage**: The system reduces network usage by caching events locally.
|
||||
|
||||
## Debugging
|
||||
|
||||
The Following screen includes debug information to help troubleshoot issues:
|
||||
|
||||
```typescript
|
||||
// Debug controls component - memoized
|
||||
const DebugControls = useCallback(() => (
|
||||
<View className="bg-gray-100 p-4 rounded-lg mx-4 mb-4">
|
||||
<Text className="font-bold mb-2">Debug Info:</Text>
|
||||
<Text>User: {currentUser?.pubkey?.substring(0, 8)}...</Text>
|
||||
<Text>Feed Items: {entries.length}</Text>
|
||||
<Text>Loading: {loading ? "Yes" : "No"}</Text>
|
||||
<Text>Offline: {isOffline ? "Yes" : "No"}</Text>
|
||||
<Text>Contacts: {contacts.length}</Text>
|
||||
<Text>Loading Contacts: {isLoadingContacts ? "Yes" : "No"}</Text>
|
||||
|
||||
<View className="flex-row mt-4 justify-between">
|
||||
<TouchableOpacity
|
||||
className="bg-blue-500 p-2 rounded flex-1 mr-2"
|
||||
onPress={checkRelayConnections}
|
||||
>
|
||||
<Text className="text-white text-center">Check Relays</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
className="bg-green-500 p-2 rounded flex-1"
|
||||
onPress={handleRefresh}
|
||||
>
|
||||
<Text className="text-white text-center">Force Refresh</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
), [currentUser?.pubkey, entries.length, loading, isOffline, contacts.length, isLoadingContacts, checkRelayConnections, handleRefresh]);
|
||||
```
|
||||
|
||||
## Future Improvements
|
||||
|
||||
1. **Automatic Cache Cleanup**: Implement automatic cleanup of old cached events.
|
||||
2. **Cache Synchronization**: Implement synchronization between the cache and the server.
|
||||
3. **Cache Compression**: Implement compression of cached data to reduce storage usage.
|
||||
4. **Cache Encryption**: Implement encryption of cached data to improve security.
|
||||
5. **Cache Analytics**: Implement analytics to track cache usage and performance.
|
220
docs/design/Social/SocialFeedFilteringRules.md
Normal file
220
docs/design/Social/SocialFeedFilteringRules.md
Normal file
@ -0,0 +1,220 @@
|
||||
# Social Feed Filtering Rules
|
||||
|
||||
This document outlines the filtering rules for the different social feed tabs in the POWR app.
|
||||
|
||||
## Overview
|
||||
|
||||
The POWR app has three main social feed tabs:
|
||||
1. **POWR** - Official content from the POWR team
|
||||
2. **Following** - Content from users the current user follows
|
||||
3. **Community** (formerly Global) - Content from the broader Nostr community
|
||||
|
||||
Each feed has specific filtering rules to ensure users see relevant fitness-related content.
|
||||
|
||||
## Content Types
|
||||
|
||||
The app handles several types of Nostr events:
|
||||
- **Social Posts** (kind 1) - Regular text posts
|
||||
- **Articles** (kind 30023) - Long-form content
|
||||
- **Article Drafts** (kind 30024) - Unpublished long-form content
|
||||
- **Workout Records** (kind 1301) - Completed workouts
|
||||
- **Exercise Templates** (kind 33401) - Exercise definitions
|
||||
- **Workout Templates** (kind 33402) - Workout plans
|
||||
|
||||
## Filtering Rules
|
||||
|
||||
### POWR Feed
|
||||
- Shows content **only** from the official POWR account (`npub1p0wer69rpkraqs02l5v8rutagfh6g9wxn2dgytkv44ysz7avt8nsusvpjk`)
|
||||
- Includes:
|
||||
- Social posts (kind 1)
|
||||
- Published articles (kind 30023)
|
||||
- Workout records (kind 1301)
|
||||
- Exercise templates (kind 33401)
|
||||
- Workout templates (kind 33402)
|
||||
- **Excludes** article drafts (kind 30024)
|
||||
|
||||
### Following Feed
|
||||
- Shows content from users the current user follows
|
||||
- For social posts (kind 1) and articles (kind 30023), only shows content with fitness-related tags:
|
||||
- #workout
|
||||
- #fitness
|
||||
- #powr
|
||||
- #31days
|
||||
- #crossfit
|
||||
- #wod
|
||||
- #gym
|
||||
- #strength
|
||||
- #cardio
|
||||
- #training
|
||||
- #exercise
|
||||
- Always shows workout-specific content (kinds 1301, 33401, 33402) from followed users
|
||||
- **Excludes** article drafts (kind 30024)
|
||||
|
||||
### Community Feed
|
||||
- Shows content from all users
|
||||
- For social posts (kind 1) and articles (kind 30023), only shows content with fitness-related tags (same as Following Feed)
|
||||
- Always shows workout-specific content (kinds 1301, 33401, 33402)
|
||||
- **Excludes** article drafts (kind 30024)
|
||||
|
||||
### User Activity Feed
|
||||
- Shows only the current user's own content
|
||||
- For social posts (kind 1) and articles (kind 30023), only shows content with fitness-related tags (same as Following Feed)
|
||||
- Always shows the user's workout-specific content (kinds 1301, 33401, 33402)
|
||||
- **Excludes** article drafts (kind 30024)
|
||||
|
||||
## Implementation Details
|
||||
|
||||
The filtering is implemented in several key files:
|
||||
- `lib/social/socialFeedService.ts` - Core service that handles feed subscriptions
|
||||
- `lib/hooks/useFeedHooks.ts` - React hooks for the different feed types
|
||||
- `components/social/EnhancedSocialPost.tsx` - Component that renders feed items
|
||||
|
||||
### Tag-Based Filtering
|
||||
|
||||
For social posts and articles, we filter based on the presence of fitness-related tags. This ensures that users only see content relevant to fitness and workouts.
|
||||
|
||||
### Content Type Filtering
|
||||
|
||||
Workout-specific content (kinds 1301, 33401, 33402) is always included in the feeds, as these are inherently fitness-related.
|
||||
|
||||
### Draft Exclusion
|
||||
|
||||
Article drafts (kind 30024) are excluded from all feeds to ensure users only see published content.
|
||||
|
||||
## Modifying Feed Filtering
|
||||
|
||||
If you need to modify the event types or tags used for filtering, you'll need to update the following files:
|
||||
|
||||
### 1. To modify event kinds (content types):
|
||||
|
||||
#### a. `lib/social/socialFeedService.ts`:
|
||||
- The `subscribeFeed` method contains the core filtering logic
|
||||
- Modify the `workoutFilter` object to change workout-specific content kinds (1301, 33401, 33402)
|
||||
- Modify the `socialPostFilter` object to change social post kinds (1)
|
||||
- Modify the `articleFilter` object to change article kinds (30023)
|
||||
- The special case for draft articles (30024) has been removed, but you can add it back if needed
|
||||
|
||||
```typescript
|
||||
// Example: To add a new workout-related kind (e.g., 1302)
|
||||
const workoutFilter: NDKFilter = {
|
||||
kinds: [1301, 33401, 33402, 1302] as any[],
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
#### b. `lib/hooks/useFeedHooks.ts`:
|
||||
- Update the filter arrays in each hook function:
|
||||
- `useFollowingFeed`
|
||||
- `usePOWRFeed`
|
||||
- `useGlobalFeed`
|
||||
- `useUserActivityFeed`
|
||||
|
||||
```typescript
|
||||
// Example: Adding a new kind to the POWR feed
|
||||
const powrFilters = useMemo<NDKFilter[]>(() => {
|
||||
if (!POWR_PUBKEY_HEX) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
kinds: [1, 30023, 1302] as any[], // Added new kind 1302
|
||||
authors: [POWR_PUBKEY_HEX],
|
||||
limit: 25
|
||||
},
|
||||
// ...
|
||||
];
|
||||
}, []);
|
||||
```
|
||||
|
||||
### 2. To modify fitness-related tags:
|
||||
|
||||
#### a. `lib/social/socialFeedService.ts`:
|
||||
- Find the tag arrays in the `socialPostFilter` and `articleFilter` objects:
|
||||
|
||||
```typescript
|
||||
socialPostFilter['#t'] = [
|
||||
'workout', 'fitness', 'powr', '31days',
|
||||
'crossfit', 'wod', 'gym', 'strength',
|
||||
'cardio', 'training', 'exercise'
|
||||
// Add new tags here
|
||||
];
|
||||
```
|
||||
|
||||
#### b. `lib/hooks/useFeedHooks.ts`:
|
||||
- Update the tag arrays in each hook function:
|
||||
- `useFollowingFeed`
|
||||
- `useGlobalFeed`
|
||||
- `useUserActivityFeed`
|
||||
|
||||
```typescript
|
||||
'#t': [
|
||||
'workout', 'fitness', 'powr', '31days',
|
||||
'crossfit', 'wod', 'gym', 'strength',
|
||||
'cardio', 'training', 'exercise',
|
||||
'newTag1', 'newTag2' // Add new tags here
|
||||
]
|
||||
```
|
||||
|
||||
### 3. To modify content rendering:
|
||||
|
||||
#### a. `components/social/EnhancedSocialPost.tsx`:
|
||||
- The `renderContent` method determines how different content types are displayed
|
||||
- Modify this method if you add new event kinds or need to change how existing kinds are rendered
|
||||
|
||||
```typescript
|
||||
// Example: Adding support for a new kind
|
||||
case 'newContentType':
|
||||
return <NewContentTypeComponent data={item.parsedContent as NewContentType} />;
|
||||
```
|
||||
|
||||
### 4. To modify event parsing:
|
||||
|
||||
#### a. `lib/hooks/useSocialFeed.ts`:
|
||||
- The `processEvent` function parses events based on their kind
|
||||
- Update this function if you add new event kinds or change how existing kinds are processed
|
||||
|
||||
```typescript
|
||||
// Example: Adding support for a new kind
|
||||
case NEW_KIND:
|
||||
feedItem = {
|
||||
id: event.id,
|
||||
type: 'newType',
|
||||
originalEvent: event,
|
||||
parsedContent: parseNewContent(event),
|
||||
createdAt: timestamp
|
||||
};
|
||||
break;
|
||||
```
|
||||
|
||||
### 5. Event type definitions:
|
||||
|
||||
#### a. `types/nostr-workout.ts`:
|
||||
- Contains the `POWR_EVENT_KINDS` enum with all supported event kinds
|
||||
- Update this enum if you add new event kinds
|
||||
|
||||
```typescript
|
||||
// Example: Adding a new kind
|
||||
export enum POWR_EVENT_KINDS {
|
||||
// Existing kinds...
|
||||
NEW_KIND = 1302,
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Changes
|
||||
|
||||
After modifying the filtering rules, test the changes in all feed tabs:
|
||||
1. POWR feed
|
||||
2. Following feed
|
||||
3. Community feed
|
||||
4. User Activity feed (in the Profile tab)
|
||||
|
||||
Verify that:
|
||||
- Only the expected content types appear in each feed
|
||||
- Content with the specified tags is properly filtered
|
||||
- New event kinds are correctly rendered
|
||||
|
||||
## Future Improvements
|
||||
|
||||
Potential future improvements to the filtering system:
|
||||
- Add user-configurable filters for specific fitness interests
|
||||
- Implement AI-based content relevance scoring
|
||||
- Add support for more content types as the Nostr ecosystem evolves
|
@ -1,179 +1,258 @@
|
||||
# NDK Mobile Cache Integration Plan
|
||||
|
||||
This document outlines our plan to leverage the NDK mobile SQLite cache system throughout the POWR app to improve offline functionality, reduce network usage, and enhance performance.
|
||||
|
||||
## Overview
|
||||
|
||||
The NDK mobile library provides a robust SQLite-based caching system that includes:
|
||||
This document outlines the comprehensive strategy for leveraging the NDK mobile SQLite cache system throughout the POWR app to improve offline functionality, reduce network usage, and enhance performance.
|
||||
|
||||
1. **Profile Caching**: Stores user profiles with metadata
|
||||
2. **Event Caching**: Stores Nostr events with efficient indexing
|
||||
3. **Unpublished Event Queue**: Manages events pending publication
|
||||
4. **Web of Trust Storage**: Maintains relationship scores
|
||||
## Goals
|
||||
|
||||
We will integrate this caching system across multiple components of our app to provide a better offline experience.
|
||||
1. **Improve Offline Experience**: Allow users to access critical app features even when offline
|
||||
2. **Reduce Network Usage**: Minimize data consumption by caching frequently accessed data
|
||||
3. **Enhance Performance**: Speed up the app by reducing network requests
|
||||
4. **Maintain Data Freshness**: Implement strategies to keep cached data up-to-date
|
||||
|
||||
## Implementation Priorities
|
||||
## Implementation Components
|
||||
|
||||
### 1. Profile Image Caching
|
||||
|
||||
**Files to Modify:**
|
||||
- `components/UserAvatar.tsx`
|
||||
- Create new: `lib/db/services/ProfileImageCache.ts`
|
||||
**Status: Implemented**
|
||||
|
||||
**Functions to Implement:**
|
||||
- `getProfileImageUri(pubkey, imageUrl)`: Get a cached image URI or download if needed
|
||||
- `clearOldCache(maxAgeDays)`: Remove old cached images
|
||||
The `ProfileImageCache` service downloads and caches profile images locally, providing offline access and reducing network usage.
|
||||
|
||||
```typescript
|
||||
// Key features of ProfileImageCache
|
||||
- Local storage of profile images in the app's cache directory
|
||||
- Automatic fetching and caching of images when needed
|
||||
- Age-based cache invalidation (24 hours by default)
|
||||
- Integration with UserAvatar component for seamless usage
|
||||
```
|
||||
|
||||
**Integration Points:**
|
||||
- Update `UserAvatar` to use the cache service
|
||||
- Add cache invalidation based on profile updates
|
||||
- `UserAvatar` component uses the cache for all profile images
|
||||
- `EnhancedSocialPost` component uses `UserAvatar` for profile images in the feed
|
||||
- NDK initialization sets the NDK instance in the ProfileImageCache service
|
||||
|
||||
### 2. Publication Queue Service
|
||||
|
||||
**Files to Modify:**
|
||||
- `lib/db/services/PublicationQueueService.ts`
|
||||
**Status: Implemented**
|
||||
|
||||
**Functions to Enhance:**
|
||||
- `queueEvent(event)`: Use NDK's unpublished events system
|
||||
- `processQueue()`: Process events from NDK cache
|
||||
- `getPendingEvents(limit)`: Get events from NDK cache
|
||||
- `getPendingCount()`: Get count from NDK cache
|
||||
The `PublicationQueueService` allows events to be created and queued when offline, then published when connectivity is restored.
|
||||
|
||||
**Migration Strategy:**
|
||||
1. Add NDK cache support
|
||||
2. Dual-write period
|
||||
3. Migrate existing queue
|
||||
4. Remove custom implementation
|
||||
```typescript
|
||||
// Key features of PublicationQueueService
|
||||
- Persistent storage of unpublished events
|
||||
- Automatic retry mechanism when connectivity is restored
|
||||
- Priority-based publishing
|
||||
- Status tracking for queued events
|
||||
```
|
||||
|
||||
**Integration Points:**
|
||||
- Social posting
|
||||
- Workout publishing
|
||||
- Template sharing
|
||||
|
||||
### 3. Social Feed Caching
|
||||
|
||||
**Files to Modify:**
|
||||
- `lib/social/socialFeedService.ts`
|
||||
- `lib/hooks/useSocialFeed.ts`
|
||||
**Status: Implemented**
|
||||
|
||||
**Functions to Enhance:**
|
||||
- `subscribeFeed(options)`: Check cache before subscription
|
||||
- `getComments(eventId)`: Use cache for comments
|
||||
- `resolveQuotedContent(event)`: Use cache for quoted content
|
||||
The `SocialFeedCache` service caches social feed events locally, allowing users to browse their feed even when offline.
|
||||
|
||||
**Benefits:**
|
||||
- Immediate display of previously viewed content
|
||||
- Reduced network requests
|
||||
- Offline browsing of previously viewed feeds
|
||||
```typescript
|
||||
// Key features of SocialFeedCache
|
||||
- SQLite-based storage of feed events
|
||||
- Feed-specific caching (following, POWR, global)
|
||||
- Time-based pagination support
|
||||
- Automatic cleanup of old cached events
|
||||
```
|
||||
|
||||
**Integration Points:**
|
||||
- `useSocialFeed` hook uses the cache when offline
|
||||
- `SocialFeedService` manages the cache and provides a unified API
|
||||
- Feed components display cached content with offline indicators
|
||||
|
||||
### 4. Workout History
|
||||
|
||||
**Files to Modify:**
|
||||
- `lib/db/services/UnifiedWorkoutHistoryService.ts`
|
||||
**Status: Implemented**
|
||||
|
||||
**Functions to Enhance:**
|
||||
- `getNostrWorkouts()`: Use NDK cache directly
|
||||
- `importNostrWorkoutToLocal(eventId)`: Leverage cache for imports
|
||||
- `subscribeToNostrWorkouts(pubkey, callback)`: Use cache for initial data
|
||||
The `UnifiedWorkoutHistoryService` provides access to workout history both locally and from Nostr, with offline support.
|
||||
|
||||
**Benefits:**
|
||||
- Faster workout history loading
|
||||
- Offline access to workout history
|
||||
- Reduced network usage
|
||||
```typescript
|
||||
// Key features of workout history caching
|
||||
- Local storage of all workout records
|
||||
- Synchronization with Nostr when online
|
||||
- Conflict resolution for workouts created offline
|
||||
- Comprehensive workout data including exercises, sets, and metadata
|
||||
```
|
||||
|
||||
**Integration Points:**
|
||||
- History tab displays cached workout history
|
||||
- Workout completion flow saves to local cache first
|
||||
- Background synchronization with Nostr
|
||||
|
||||
### 5. Exercise Library
|
||||
|
||||
**Files to Modify:**
|
||||
- `lib/db/services/ExerciseService.ts`
|
||||
- `lib/hooks/useExercises.ts`
|
||||
**Status: Implemented**
|
||||
|
||||
**Functions to Implement:**
|
||||
- `getExercisesFromNostr()`: Use cache for exercises
|
||||
- `getExerciseDetails(id)`: Get details from cache
|
||||
The `ExerciseService` maintains a local cache of exercises, allowing offline access to the exercise library.
|
||||
|
||||
**Benefits:**
|
||||
- Offline access to exercise library
|
||||
- Faster exercise loading
|
||||
```typescript
|
||||
// Key features of exercise library caching
|
||||
- Complete local copy of exercise database
|
||||
- Periodic updates from Nostr when online
|
||||
- Custom exercise creation and storage
|
||||
- Categorization and search functionality
|
||||
```
|
||||
|
||||
**Integration Points:**
|
||||
- Exercise selection during workout creation
|
||||
- Exercise details view
|
||||
- Exercise search and filtering
|
||||
|
||||
### 6. Workout Templates
|
||||
|
||||
**Files to Modify:**
|
||||
- `lib/db/services/TemplateService.ts`
|
||||
- `lib/hooks/useTemplates.ts`
|
||||
**Status: Implemented**
|
||||
|
||||
**Functions to Enhance:**
|
||||
- `getTemplateFromNostr(id)`: Use cache for templates
|
||||
- `getTemplatesFromNostr()`: Get templates from cache
|
||||
The `TemplateService` provides offline access to workout templates through local caching.
|
||||
|
||||
**Benefits:**
|
||||
- Offline access to templates
|
||||
- Faster template loading
|
||||
```typescript
|
||||
// Key features of template caching
|
||||
- Local storage of user's templates
|
||||
- Synchronization with Nostr templates
|
||||
- Favorite templates prioritized for offline access
|
||||
- Template versioning and updates
|
||||
```
|
||||
|
||||
**Integration Points:**
|
||||
- Template selection during workout creation
|
||||
- Template management in the library
|
||||
- Template sharing and discovery
|
||||
|
||||
### 7. Contact List & Following
|
||||
|
||||
**Files to Modify:**
|
||||
- `lib/hooks/useContactList.ts`
|
||||
- `lib/hooks/useFeedState.ts`
|
||||
**Status: Implemented**
|
||||
|
||||
**Functions to Enhance:**
|
||||
- `getContactList()`: Use cache for contact list
|
||||
- `getFollowingList()`: Use cache for following list
|
||||
The system caches the user's contact list and following relationships for offline access.
|
||||
|
||||
**Benefits:**
|
||||
- Offline access to contacts
|
||||
- Faster contact list loading
|
||||
```typescript
|
||||
// Key features of contact list caching
|
||||
- Local storage of followed users
|
||||
- Periodic updates when online
|
||||
- Integration with NDK's contact list functionality
|
||||
- Support for NIP-02 contact lists
|
||||
```
|
||||
|
||||
**Integration Points:**
|
||||
- Following feed generation
|
||||
- User profile display
|
||||
- Social interactions
|
||||
|
||||
### 8. General Media Cache
|
||||
|
||||
**Files to Create:**
|
||||
- `lib/db/services/MediaCacheService.ts`
|
||||
**Status: Implemented**
|
||||
|
||||
**Functions to Implement:**
|
||||
- `cacheMedia(url, mimeType)`: Download and cache media
|
||||
- `getMediaUri(url)`: Get cached media URI
|
||||
- `clearOldCache(maxAgeDays)`: Remove old cached media
|
||||
A general-purpose media cache for other types of media used in the app.
|
||||
|
||||
```typescript
|
||||
// Key features of general media cache
|
||||
- Support for various media types (images, videos, etc.)
|
||||
- Size-limited cache with LRU eviction
|
||||
- Content-addressable storage
|
||||
- Automatic cleanup of unused media
|
||||
```
|
||||
|
||||
**Integration Points:**
|
||||
- Profile banners
|
||||
- Exercise images
|
||||
- Other media content
|
||||
- Article images in the feed
|
||||
- Exercise demonstration images
|
||||
- App assets and resources
|
||||
|
||||
## Implementation Approach
|
||||
## Technical Implementation
|
||||
|
||||
For each component, we will:
|
||||
### NDK Integration
|
||||
|
||||
1. **Analyze Current Implementation**: Understand how data is currently fetched and stored
|
||||
2. **Design Cache Integration**: Determine how to leverage NDK cache
|
||||
3. **Implement Changes**: Modify code to use cache
|
||||
4. **Test Offline Functionality**: Verify behavior when offline
|
||||
5. **Measure Performance**: Compare before and after metrics
|
||||
The NDK mobile adapter provides built-in SQLite caching capabilities that we leverage throughout the app:
|
||||
|
||||
## Technical Considerations
|
||||
```typescript
|
||||
// Initialize NDK with SQLite cache adapter
|
||||
const cacheAdapter = new NDKCacheAdapterSqlite('powr', 1000);
|
||||
await cacheAdapter.initialize();
|
||||
|
||||
### Cache Size Management
|
||||
const ndk = new NDK({
|
||||
cacheAdapter,
|
||||
explicitRelayUrls: DEFAULT_RELAYS,
|
||||
enableOutboxModel: true,
|
||||
autoConnectUserRelays: true,
|
||||
clientName: 'powr',
|
||||
});
|
||||
```
|
||||
|
||||
- Implement cache size limits
|
||||
- Add cache eviction policies
|
||||
- Prioritize frequently accessed data
|
||||
### Connectivity Management
|
||||
|
||||
### Cache Invalidation
|
||||
The `ConnectivityService` monitors network status and triggers appropriate cache behaviors:
|
||||
|
||||
- Track data freshness
|
||||
- Implement TTL (Time To Live) for cached data
|
||||
- Update cache when new data is received
|
||||
```typescript
|
||||
// Key features of ConnectivityService
|
||||
- Real-time network status monitoring
|
||||
- Callback registration for connectivity changes
|
||||
- Automatic retry of failed operations when connectivity is restored
|
||||
- Bandwidth-aware operation modes
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
### Cache Invalidation Strategies
|
||||
|
||||
- Graceful fallbacks when cache misses
|
||||
- Recovery from cache corruption
|
||||
- Logging for debugging
|
||||
Different types of data have different invalidation strategies:
|
||||
|
||||
## Success Metrics
|
||||
1. **Time-based**: Profile images, feed events
|
||||
2. **Version-based**: Exercise library, templates
|
||||
3. **Manual**: User-triggered refresh
|
||||
4. **Never**: Historical workout data
|
||||
|
||||
- Reduced network requests
|
||||
- Faster app startup time
|
||||
- Improved offline experience
|
||||
- Reduced data usage
|
||||
- Better battery life
|
||||
## User Experience Considerations
|
||||
|
||||
## Next Steps
|
||||
### Offline Indicators
|
||||
|
||||
1. Begin with Profile Image Cache implementation
|
||||
2. Move to Publication Queue Service
|
||||
3. Continue with remaining components in priority order
|
||||
The app provides clear visual indicators when operating in offline mode:
|
||||
|
||||
- Global offline indicator in the header
|
||||
- Feed-specific offline state components
|
||||
- Disabled actions that require connectivity
|
||||
- Queued action indicators
|
||||
|
||||
### Transparent Sync
|
||||
|
||||
Synchronization happens transparently in the background:
|
||||
|
||||
- Automatic publishing of queued events when connectivity is restored
|
||||
- Progressive loading of fresh content when coming online
|
||||
- Prioritized sync for critical data
|
||||
|
||||
### Data Freshness
|
||||
|
||||
The app balances offline availability with data freshness:
|
||||
|
||||
- Age indicators for cached content
|
||||
- Pull-to-refresh to force update when online
|
||||
- Background refresh of frequently accessed data
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Comprehensive testing ensures the cache system works reliably:
|
||||
|
||||
1. **Unit Tests**: Individual cache services
|
||||
2. **Integration Tests**: Interaction between cache and UI components
|
||||
3. **Offline Simulation**: Testing app behavior in offline mode
|
||||
4. **Performance Testing**: Measuring cache impact on app performance
|
||||
5. **Edge Cases**: Testing cache behavior with limited storage, connectivity issues, etc.
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential future improvements to the caching system:
|
||||
|
||||
1. **Selective Sync**: User-configurable sync preferences
|
||||
2. **Compression**: Reducing cache size through compression
|
||||
3. **Encryption**: Enhancing security of cached data
|
||||
4. **Analytics**: Usage patterns to optimize caching strategy
|
||||
5. **Cross-device Sync**: Synchronizing cache across user devices
|
||||
|
||||
## Conclusion
|
||||
|
||||
The NDK Mobile Cache Integration provides a robust foundation for offline functionality in the POWR app, significantly improving the user experience in limited connectivity scenarios while reducing network usage and enhancing performance.
|
||||
|
343
docs/testing/CacheImplementationTesting.md
Normal file
343
docs/testing/CacheImplementationTesting.md
Normal file
@ -0,0 +1,343 @@
|
||||
# Cache Implementation Testing Guide
|
||||
|
||||
This document outlines the testing strategy for the NDK Mobile Cache Integration in the POWR app. It provides guidelines for testing each component of the caching system to ensure reliability, performance, and a seamless user experience in both online and offline scenarios.
|
||||
|
||||
## Testing Objectives
|
||||
|
||||
1. Verify that all cache components function correctly in isolation
|
||||
2. Ensure proper integration between cache components and the UI
|
||||
3. Validate offline functionality across all app features
|
||||
4. Measure performance improvements from caching
|
||||
5. Test edge cases and error handling
|
||||
|
||||
## Test Environment Setup
|
||||
|
||||
### Simulating Offline Mode
|
||||
|
||||
To properly test offline functionality, use one of these approaches:
|
||||
|
||||
1. **Device Airplane Mode**: Enable airplane mode on the test device
|
||||
2. **Network Throttling**: Use React Native Debugger or Chrome DevTools to simulate slow or unreliable connections
|
||||
3. **Mock Connectivity Service**: Modify the `ConnectivityService` to report offline status regardless of actual connectivity
|
||||
|
||||
```typescript
|
||||
// Example: Force offline mode for testing
|
||||
ConnectivityService.getInstance().overrideNetworkStatus(false);
|
||||
```
|
||||
|
||||
### Test Data Generation
|
||||
|
||||
Create a consistent set of test data for reproducible testing:
|
||||
|
||||
1. **Test Accounts**: Create dedicated test accounts with known data
|
||||
2. **Seed Data**: Populate the cache with known seed data before testing
|
||||
3. **Mock Events**: Generate mock Nostr events for testing specific scenarios
|
||||
|
||||
## Component Testing
|
||||
|
||||
### 1. Profile Image Cache Testing
|
||||
|
||||
**Test Cases:**
|
||||
|
||||
1. **Cache Hit**: Verify that previously cached images load without network requests
|
||||
2. **Cache Miss**: Ensure new images are downloaded and cached properly
|
||||
3. **Cache Expiry**: Test that expired images are refreshed when online
|
||||
4. **Offline Behavior**: Confirm cached images display when offline
|
||||
5. **Error Handling**: Test behavior when image URLs are invalid or unreachable
|
||||
|
||||
**Testing Method:**
|
||||
```typescript
|
||||
// Example test for ProfileImageCache
|
||||
test('should return cached image for known pubkey', async () => {
|
||||
const pubkey = 'known-test-pubkey';
|
||||
const cachedUri = await profileImageCache.getProfileImageUri(pubkey);
|
||||
expect(cachedUri).not.toBeUndefined();
|
||||
|
||||
// Verify it's a file URI pointing to the cache directory
|
||||
expect(cachedUri).toContain(FileSystem.cacheDirectory);
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Publication Queue Testing
|
||||
|
||||
**Test Cases:**
|
||||
|
||||
1. **Queue Storage**: Verify events are properly stored when created offline
|
||||
2. **Auto-Publishing**: Confirm queued events are published when connectivity is restored
|
||||
3. **Retry Mechanism**: Test that failed publications are retried
|
||||
4. **Queue Management**: Ensure the queue is properly maintained (events removed after successful publishing)
|
||||
5. **Priority Handling**: Verify high-priority events are published first
|
||||
|
||||
**Testing Method:**
|
||||
```typescript
|
||||
// Example test for PublicationQueueService
|
||||
test('should queue event when offline and publish when online', async () => {
|
||||
// Force offline mode
|
||||
ConnectivityService.getInstance().overrideNetworkStatus(false);
|
||||
|
||||
// Create and queue event
|
||||
const event = await publicationQueueService.createAndQueueEvent(1, 'test content', []);
|
||||
expect(await publicationQueueService.getQueuedEventCount()).toBe(1);
|
||||
|
||||
// Restore online mode
|
||||
ConnectivityService.getInstance().overrideNetworkStatus(true);
|
||||
|
||||
// Trigger sync and wait for completion
|
||||
await publicationQueueService.syncQueuedEvents();
|
||||
|
||||
// Verify queue is empty after publishing
|
||||
expect(await publicationQueueService.getQueuedEventCount()).toBe(0);
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Social Feed Cache Testing
|
||||
|
||||
**Test Cases:**
|
||||
|
||||
1. **Cache Storage**: Verify feed events are properly cached
|
||||
2. **Feed Types**: Test caching for different feed types (following, POWR, global)
|
||||
3. **Pagination**: Ensure cached feeds support pagination
|
||||
4. **Offline Browsing**: Confirm users can browse cached feeds when offline
|
||||
5. **Cache Refresh**: Test that feeds are refreshed when online
|
||||
|
||||
**Testing Method:**
|
||||
```typescript
|
||||
// Example test for SocialFeedCache
|
||||
test('should return cached events when offline', async () => {
|
||||
// Populate cache with known events
|
||||
await socialFeedCache.cacheEvents('following', mockEvents);
|
||||
|
||||
// Force offline mode
|
||||
ConnectivityService.getInstance().overrideNetworkStatus(false);
|
||||
|
||||
// Retrieve cached events
|
||||
const cachedEvents = await socialFeedCache.getCachedEvents('following', 10);
|
||||
|
||||
// Verify events match
|
||||
expect(cachedEvents.length).toBe(mockEvents.length);
|
||||
expect(cachedEvents[0].id).toBe(mockEvents[0].id);
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Workout History Testing
|
||||
|
||||
**Test Cases:**
|
||||
|
||||
1. **Local Storage**: Verify workouts are stored locally
|
||||
2. **Nostr Sync**: Test synchronization with Nostr when online
|
||||
3. **Offline Creation**: Confirm workouts can be created and viewed offline
|
||||
4. **Conflict Resolution**: Test handling of conflicts between local and remote workouts
|
||||
5. **Data Integrity**: Ensure all workout data is preserved correctly
|
||||
|
||||
**Testing Method:**
|
||||
```typescript
|
||||
// Example test for UnifiedWorkoutHistoryService
|
||||
test('should store workout locally and sync to Nostr when online', async () => {
|
||||
// Force offline mode
|
||||
ConnectivityService.getInstance().overrideNetworkStatus(false);
|
||||
|
||||
// Create workout offline
|
||||
const workout = await workoutHistoryService.createWorkout(mockWorkoutData);
|
||||
|
||||
// Verify it's in local storage
|
||||
const localWorkout = await workoutHistoryService.getWorkoutById(workout.id);
|
||||
expect(localWorkout).not.toBeNull();
|
||||
|
||||
// Restore online mode
|
||||
ConnectivityService.getInstance().overrideNetworkStatus(true);
|
||||
|
||||
// Trigger sync
|
||||
await workoutHistoryService.syncWorkouts();
|
||||
|
||||
// Verify workout was published to Nostr
|
||||
const nostrWorkout = await nostrWorkoutService.getWorkoutById(workout.id);
|
||||
expect(nostrWorkout).not.toBeNull();
|
||||
});
|
||||
```
|
||||
|
||||
### 5. Exercise Library Testing
|
||||
|
||||
**Test Cases:**
|
||||
|
||||
1. **Local Cache**: Verify exercises are cached locally
|
||||
2. **Offline Access**: Confirm exercises can be browsed offline
|
||||
3. **Cache Updates**: Test that the cache is updated when online
|
||||
4. **Search & Filter**: Ensure search and filtering work with cached data
|
||||
5. **Custom Exercises**: Test creation and storage of custom exercises
|
||||
|
||||
**Testing Method:**
|
||||
```typescript
|
||||
// Example test for ExerciseService
|
||||
test('should provide exercises when offline', async () => {
|
||||
// Populate cache with exercises
|
||||
await exerciseService.cacheExercises(mockExercises);
|
||||
|
||||
// Force offline mode
|
||||
ConnectivityService.getInstance().overrideNetworkStatus(false);
|
||||
|
||||
// Retrieve exercises
|
||||
const exercises = await exerciseService.getExercises();
|
||||
|
||||
// Verify exercises are available
|
||||
expect(exercises.length).toBeGreaterThan(0);
|
||||
expect(exercises[0].name).toBe(mockExercises[0].name);
|
||||
});
|
||||
```
|
||||
|
||||
### 6. Workout Templates Testing
|
||||
|
||||
**Test Cases:**
|
||||
|
||||
1. **Template Cache**: Verify templates are cached locally
|
||||
2. **Offline Access**: Confirm templates can be used offline
|
||||
3. **Favorites**: Test that favorite templates are prioritized for offline access
|
||||
4. **Template Updates**: Ensure templates are updated when online
|
||||
5. **Template Creation**: Test creation and storage of new templates offline
|
||||
|
||||
**Testing Method:**
|
||||
```typescript
|
||||
// Example test for TemplateService
|
||||
test('should allow template creation when offline', async () => {
|
||||
// Force offline mode
|
||||
ConnectivityService.getInstance().overrideNetworkStatus(false);
|
||||
|
||||
// Create template
|
||||
const template = await templateService.createTemplate(mockTemplateData);
|
||||
|
||||
// Verify it's in local storage
|
||||
const localTemplate = await templateService.getTemplateById(template.id);
|
||||
expect(localTemplate).not.toBeNull();
|
||||
expect(localTemplate.title).toBe(mockTemplateData.title);
|
||||
});
|
||||
```
|
||||
|
||||
### 7. Contact List Testing
|
||||
|
||||
**Test Cases:**
|
||||
|
||||
1. **Contact Cache**: Verify contacts are cached locally
|
||||
2. **Offline Access**: Confirm contact list is available offline
|
||||
3. **Contact Updates**: Test that contacts are updated when online
|
||||
4. **Following Feed**: Ensure following feed works with cached contacts
|
||||
5. **Profile Display**: Test profile display with cached contact data
|
||||
|
||||
**Testing Method:**
|
||||
```typescript
|
||||
// Example test for contact list caching
|
||||
test('should provide contacts when offline', async () => {
|
||||
// Cache contacts
|
||||
await contactListService.cacheContacts(mockContacts);
|
||||
|
||||
// Force offline mode
|
||||
ConnectivityService.getInstance().overrideNetworkStatus(false);
|
||||
|
||||
// Retrieve contacts
|
||||
const contacts = await contactListService.getContacts();
|
||||
|
||||
// Verify contacts are available
|
||||
expect(contacts.length).toBe(mockContacts.length);
|
||||
});
|
||||
```
|
||||
|
||||
### 8. General Media Cache Testing
|
||||
|
||||
**Test Cases:**
|
||||
|
||||
1. **Media Storage**: Verify media files are cached correctly
|
||||
2. **Cache Limits**: Test that cache size limits are enforced
|
||||
3. **LRU Eviction**: Ensure least recently used media is evicted when cache is full
|
||||
4. **Offline Access**: Confirm cached media is available offline
|
||||
5. **Media Types**: Test caching of different media types (images, videos, etc.)
|
||||
|
||||
**Testing Method:**
|
||||
```typescript
|
||||
// Example test for general media cache
|
||||
test('should cache and retrieve media files', async () => {
|
||||
// Cache media
|
||||
const mediaUri = 'https://example.com/test-image.jpg';
|
||||
const cachedUri = await mediaCacheService.cacheMedia(mediaUri);
|
||||
|
||||
// Verify it's cached
|
||||
expect(cachedUri).toContain(FileSystem.cacheDirectory);
|
||||
|
||||
// Force offline mode
|
||||
ConnectivityService.getInstance().overrideNetworkStatus(false);
|
||||
|
||||
// Retrieve cached media
|
||||
const retrievedUri = await mediaCacheService.getMedia(mediaUri);
|
||||
|
||||
// Verify it's the same cached URI
|
||||
expect(retrievedUri).toBe(cachedUri);
|
||||
});
|
||||
```
|
||||
|
||||
## Integration Testing
|
||||
|
||||
### UI Component Integration
|
||||
|
||||
Test that UI components correctly integrate with cache services:
|
||||
|
||||
1. **Feed Components**: Verify feed components display cached content
|
||||
2. **Profile Components**: Test profile components with cached profile data
|
||||
3. **Exercise Library**: Ensure exercise library uses cached exercises
|
||||
4. **Workout History**: Test workout history display with cached workouts
|
||||
5. **Templates**: Verify template selection with cached templates
|
||||
|
||||
### Offline Mode UI
|
||||
|
||||
Test the UI adaptations for offline mode:
|
||||
|
||||
1. **Offline Indicators**: Verify offline indicators are displayed correctly
|
||||
2. **Disabled Actions**: Test that network-dependent actions are properly disabled
|
||||
3. **Error Messages**: Ensure appropriate error messages are shown for unavailable features
|
||||
4. **Queued Actions**: Verify UI feedback for queued actions
|
||||
|
||||
## Performance Testing
|
||||
|
||||
Measure the performance impact of caching:
|
||||
|
||||
1. **Load Times**: Compare load times with and without cache
|
||||
2. **Network Usage**: Measure reduction in network requests
|
||||
3. **Memory Usage**: Monitor memory consumption of the cache
|
||||
4. **Battery Impact**: Assess battery usage with different caching strategies
|
||||
5. **Storage Usage**: Measure storage space used by the cache
|
||||
|
||||
## Edge Case Testing
|
||||
|
||||
Test unusual scenarios and error conditions:
|
||||
|
||||
1. **Intermittent Connectivity**: Test behavior with rapidly changing connectivity
|
||||
2. **Storage Limits**: Test behavior when device storage is nearly full
|
||||
3. **Cache Corruption**: Test recovery from corrupted cache data
|
||||
4. **Version Upgrades**: Verify cache behavior during app version upgrades
|
||||
5. **Multiple Devices**: Test synchronization across multiple devices
|
||||
|
||||
## Automated Testing
|
||||
|
||||
Implement automated tests for continuous validation:
|
||||
|
||||
1. **Unit Tests**: Automated tests for individual cache services
|
||||
2. **Integration Tests**: Automated tests for cache-UI integration
|
||||
3. **E2E Tests**: End-to-end tests for offline scenarios
|
||||
4. **Performance Benchmarks**: Automated performance measurements
|
||||
5. **CI/CD Integration**: Include cache tests in continuous integration pipeline
|
||||
|
||||
## Manual Testing Checklist
|
||||
|
||||
A checklist for manual testing of cache functionality:
|
||||
|
||||
- [ ] Verify all features work normally when online
|
||||
- [ ] Enable airplane mode and verify offline functionality
|
||||
- [ ] Create content offline and verify it's queued
|
||||
- [ ] Restore connectivity and verify queued content is published
|
||||
- [ ] Verify cached images display correctly offline
|
||||
- [ ] Test offline access to exercise library
|
||||
- [ ] Verify workout templates are available offline
|
||||
- [ ] Test offline workout creation and history
|
||||
- [ ] Verify offline browsing of social feeds
|
||||
- [ ] Test behavior with low storage conditions
|
||||
- [ ] Verify cache is properly maintained over time
|
||||
|
||||
## Conclusion
|
||||
|
||||
Thorough testing of the cache implementation is essential to ensure a reliable offline experience for POWR app users. By following this testing guide, you can verify that all cache components function correctly in isolation and together, providing a seamless experience regardless of connectivity status.
|
@ -71,6 +71,65 @@ export class EventCache {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a Nostr event in the cache without using a transaction
|
||||
* This is used when the caller is already managing a transaction
|
||||
*/
|
||||
async setEventWithoutTransaction(event: NostrEvent, skipExisting: boolean = false): Promise<void> {
|
||||
if (!event.id) return;
|
||||
|
||||
try {
|
||||
// Check if event already exists
|
||||
if (skipExisting) {
|
||||
const exists = await this.db.getFirstAsync<{ id: string }>(
|
||||
'SELECT id FROM nostr_events WHERE id = ?',
|
||||
[event.id]
|
||||
);
|
||||
|
||||
if (exists) return;
|
||||
}
|
||||
|
||||
// Store the event without a transaction
|
||||
await this.db.runAsync(
|
||||
`INSERT OR REPLACE INTO nostr_events
|
||||
(id, pubkey, kind, created_at, content, sig, raw_event, received_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
event.id,
|
||||
event.pubkey || '',
|
||||
event.kind,
|
||||
event.created_at,
|
||||
event.content,
|
||||
event.sig || '',
|
||||
JSON.stringify(event),
|
||||
Date.now()
|
||||
]
|
||||
);
|
||||
|
||||
// Delete existing tags
|
||||
await this.db.runAsync(
|
||||
'DELETE FROM event_tags WHERE event_id = ?',
|
||||
[event.id]
|
||||
);
|
||||
|
||||
// Insert new tags
|
||||
if (event.tags && event.tags.length > 0) {
|
||||
for (let i = 0; i < event.tags.length; i++) {
|
||||
const tag = event.tags[i];
|
||||
if (tag.length >= 2) {
|
||||
await this.db.runAsync(
|
||||
'INSERT INTO event_tags (event_id, name, value, index_num) VALUES (?, ?, ?, ?)',
|
||||
[event.id, tag[0], tag[1], i]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error caching event without transaction:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an event from the cache by ID
|
||||
*/
|
||||
@ -118,4 +177,4 @@ export class EventCache {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
232
lib/db/services/ProfileImageCache.ts
Normal file
232
lib/db/services/ProfileImageCache.ts
Normal file
@ -0,0 +1,232 @@
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
import NDK, { NDKUser, NDKSubscriptionCacheUsage } from '@nostr-dev-kit/ndk-mobile';
|
||||
import * as Crypto from 'expo-crypto';
|
||||
|
||||
/**
|
||||
* Service for caching profile images
|
||||
* This service downloads and caches profile images locally,
|
||||
* providing offline access and reducing network usage
|
||||
*/
|
||||
export class ProfileImageCache {
|
||||
private cacheDirectory: string;
|
||||
private ndk: NDK | null = null;
|
||||
|
||||
constructor() {
|
||||
this.cacheDirectory = `${FileSystem.cacheDirectory}profile-images/`;
|
||||
this.ensureCacheDirectoryExists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the NDK instance for profile fetching
|
||||
* @param ndk NDK instance
|
||||
*/
|
||||
setNDK(ndk: NDK) {
|
||||
this.ndk = ndk;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the cache directory exists
|
||||
* @private
|
||||
*/
|
||||
private async ensureCacheDirectoryExists() {
|
||||
try {
|
||||
const dirInfo = await FileSystem.getInfoAsync(this.cacheDirectory);
|
||||
if (!dirInfo.exists) {
|
||||
await FileSystem.makeDirectoryAsync(this.cacheDirectory, { intermediates: true });
|
||||
console.log(`Created profile image cache directory: ${this.cacheDirectory}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating cache directory:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract pubkey from a profile image URI
|
||||
* @param uri Profile image URI
|
||||
* @returns Pubkey if found, undefined otherwise
|
||||
*/
|
||||
extractPubkeyFromUri(uri?: string): string | undefined {
|
||||
if (!uri) return undefined;
|
||||
|
||||
// Try to extract pubkey from nostr: URI
|
||||
if (uri.startsWith('nostr:')) {
|
||||
const match = uri.match(/nostr:pubkey:([a-f0-9]{64})/i);
|
||||
if (match && match[1]) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Try to extract from URL parameters
|
||||
try {
|
||||
const url = new URL(uri);
|
||||
const pubkey = url.searchParams.get('pubkey');
|
||||
if (pubkey && pubkey.length === 64) {
|
||||
return pubkey;
|
||||
}
|
||||
} catch (error) {
|
||||
// Not a valid URL, continue
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a cached profile image URI or download if needed
|
||||
* @param pubkey User's public key
|
||||
* @param fallbackUrl Fallback URL to use if no cached image is found
|
||||
* @returns Promise with the cached image URI or fallback URL
|
||||
*/
|
||||
async getProfileImageUri(pubkey?: string, fallbackUrl?: string): Promise<string | undefined> {
|
||||
try {
|
||||
if (!pubkey) {
|
||||
return fallbackUrl;
|
||||
}
|
||||
|
||||
// Check if image exists in cache
|
||||
const cachedPath = `${this.cacheDirectory}${pubkey}.jpg`;
|
||||
const fileInfo = await FileSystem.getInfoAsync(cachedPath);
|
||||
|
||||
if (fileInfo.exists) {
|
||||
// Check if cache is fresh (less than 24 hours old)
|
||||
const stats = await FileSystem.getInfoAsync(cachedPath, { md5: false });
|
||||
// Type assertion for modificationTime which might not be in the type definition
|
||||
const modTime = (stats as any).modificationTime || 0;
|
||||
const cacheAge = Date.now() - modTime * 1000;
|
||||
const maxAge = 24 * 60 * 60 * 1000; // 24 hours
|
||||
|
||||
if (cacheAge < maxAge) {
|
||||
console.log(`Using cached profile image for ${pubkey}`);
|
||||
return cachedPath;
|
||||
}
|
||||
}
|
||||
|
||||
// If not in cache or stale, try to get from NDK
|
||||
if (this.ndk) {
|
||||
const user = new NDKUser({ pubkey });
|
||||
user.ndk = this.ndk;
|
||||
|
||||
// Get profile from NDK cache first
|
||||
try {
|
||||
await user.fetchProfile({
|
||||
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST
|
||||
});
|
||||
let imageUrl = user.profile?.image || user.profile?.picture || fallbackUrl;
|
||||
|
||||
if (imageUrl) {
|
||||
try {
|
||||
// Download and cache the image
|
||||
console.log(`Downloading profile image for ${pubkey} from ${imageUrl}`);
|
||||
const downloadResult = await FileSystem.downloadAsync(imageUrl, cachedPath);
|
||||
|
||||
// Verify the downloaded file exists and has content
|
||||
if (downloadResult.status === 200) {
|
||||
const fileInfo = await FileSystem.getInfoAsync(cachedPath);
|
||||
if (fileInfo.exists && fileInfo.size > 0) {
|
||||
console.log(`Successfully cached profile image for ${pubkey}`);
|
||||
return cachedPath;
|
||||
} else {
|
||||
console.warn(`Downloaded image file is empty or missing: ${cachedPath}`);
|
||||
// Delete the empty file
|
||||
await FileSystem.deleteAsync(cachedPath, { idempotent: true });
|
||||
return fallbackUrl;
|
||||
}
|
||||
} else {
|
||||
console.warn(`Failed to download image from ${imageUrl}, status: ${downloadResult.status}`);
|
||||
return fallbackUrl;
|
||||
}
|
||||
} catch (downloadError) {
|
||||
console.warn(`Error downloading image from ${imageUrl}:`, downloadError);
|
||||
// Clean up any partial downloads
|
||||
try {
|
||||
const fileInfo = await FileSystem.getInfoAsync(cachedPath);
|
||||
if (fileInfo.exists) {
|
||||
await FileSystem.deleteAsync(cachedPath, { idempotent: true });
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.error('Error cleaning up failed download:', cleanupError);
|
||||
}
|
||||
return fallbackUrl;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Could not fetch profile from cache:', error);
|
||||
}
|
||||
|
||||
// If not in cache and no fallback, try network
|
||||
if (!fallbackUrl) {
|
||||
try {
|
||||
await user.fetchProfile({
|
||||
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST
|
||||
});
|
||||
const imageUrl = user.profile?.image || user.profile?.picture;
|
||||
|
||||
if (imageUrl) {
|
||||
// Download and cache the image
|
||||
console.log(`Downloading profile image for ${pubkey} from ${imageUrl}`);
|
||||
await FileSystem.downloadAsync(imageUrl, cachedPath);
|
||||
return cachedPath;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching profile from network:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return fallback URL if provided and nothing in cache
|
||||
return fallbackUrl;
|
||||
} catch (error) {
|
||||
console.error('Error getting profile image:', error);
|
||||
return fallbackUrl;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear old cached images
|
||||
* @param maxAgeDays Maximum age in days (default: 7)
|
||||
* @returns Promise that resolves when clearing is complete
|
||||
*/
|
||||
async clearOldCache(maxAgeDays: number = 7): Promise<void> {
|
||||
try {
|
||||
const files = await FileSystem.readDirectoryAsync(this.cacheDirectory);
|
||||
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
|
||||
const now = Date.now();
|
||||
let clearedCount = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = `${this.cacheDirectory}${file}`;
|
||||
const fileInfo = await FileSystem.getInfoAsync(filePath);
|
||||
|
||||
if (fileInfo.exists) {
|
||||
// Type assertion for modificationTime
|
||||
const modTime = (fileInfo as any).modificationTime || 0;
|
||||
const fileAge = now - modTime * 1000;
|
||||
if (fileAge > maxAgeMs) {
|
||||
await FileSystem.deleteAsync(filePath);
|
||||
clearedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Cleared ${clearedCount} old profile images from cache`);
|
||||
} catch (error) {
|
||||
console.error('Error clearing old cache:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the entire cache
|
||||
* @returns Promise that resolves when clearing is complete
|
||||
*/
|
||||
async clearCache(): Promise<void> {
|
||||
try {
|
||||
await FileSystem.deleteAsync(this.cacheDirectory, { idempotent: true });
|
||||
await this.ensureCacheDirectoryExists();
|
||||
console.log('Profile image cache cleared');
|
||||
} catch (error) {
|
||||
console.error('Error clearing cache:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
export const profileImageCache = new ProfileImageCache();
|
@ -1,12 +1,13 @@
|
||||
// lib/hooks/useFeedHooks.ts
|
||||
import { useMemo, useCallback, useState, useEffect } from 'react';
|
||||
import { useNDKCurrentUser, useNDK } from '@/lib/hooks/useNDK';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { NDKFilter, NDKEvent, NDKSubscriptionCacheUsage } from '@nostr-dev-kit/ndk-mobile';
|
||||
import { useFeedEvents } from '@/lib/hooks/useFeedEvents';
|
||||
import { useFeedMonitor } from '@/lib/hooks/useFeedMonitor';
|
||||
import { FeedOptions, AnyFeedEntry } from '@/types/feed';
|
||||
import { POWR_EVENT_KINDS } from '@/types/nostr-workout';
|
||||
import { useMemo } from 'react';
|
||||
import { useSocialFeed } from './useSocialFeed';
|
||||
import { useNDKCurrentUser } from './useNDK';
|
||||
|
||||
/**
|
||||
* This file contains constants related to the POWR account.
|
||||
* The feed implementation has been moved to useSocialFeed.ts.
|
||||
*/
|
||||
|
||||
// POWR official account pubkey
|
||||
export const POWR_ACCOUNT_PUBKEY = 'npub1p0wer69rpkraqs02l5v8rutagfh6g9wxn2dgytkv44ysz7avt8nsusvpjk';
|
||||
@ -20,363 +21,29 @@ try {
|
||||
} else {
|
||||
POWR_PUBKEY_HEX = POWR_ACCOUNT_PUBKEY;
|
||||
}
|
||||
console.log("Initialized POWR pubkey hex:", POWR_PUBKEY_HEX);
|
||||
console.log("[useFeedHooks] Initialized POWR pubkey hex:", POWR_PUBKEY_HEX);
|
||||
} catch (error) {
|
||||
console.error('Error decoding POWR account npub:', error);
|
||||
console.error('[useFeedHooks] Error decoding POWR account npub:', error);
|
||||
POWR_PUBKEY_HEX = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for the Following tab in the social feed
|
||||
* Shows content from authors the user follows
|
||||
* @deprecated Use useSocialFeed from lib/hooks/useSocialFeed.ts instead.
|
||||
* Example:
|
||||
*
|
||||
* // For POWR feed:
|
||||
* const { feedItems, loading, refresh } = useSocialFeed({
|
||||
* feedType: 'powr',
|
||||
* authors: [POWR_PUBKEY_HEX]
|
||||
* });
|
||||
*
|
||||
* // For Following feed:
|
||||
* const { feedItems, loading, refresh } = useSocialFeed({
|
||||
* feedType: 'following'
|
||||
* });
|
||||
*
|
||||
* // For Global feed:
|
||||
* const { feedItems, loading, refresh } = useSocialFeed({
|
||||
* feedType: 'global'
|
||||
* });
|
||||
*/
|
||||
export function useFollowingFeed(options: FeedOptions = {}) {
|
||||
const { currentUser } = useNDKCurrentUser();
|
||||
const { ndk } = useNDK();
|
||||
const [followedUsers, setFollowedUsers] = useState<string[]>([]);
|
||||
const [isLoadingContacts, setIsLoadingContacts] = useState(true);
|
||||
|
||||
// Improved contact list fetching
|
||||
useEffect(() => {
|
||||
if (!ndk || !currentUser?.pubkey) {
|
||||
setIsLoadingContacts(false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Fetching contact list for user:", currentUser.pubkey);
|
||||
setIsLoadingContacts(true);
|
||||
|
||||
const fetchContactList = async () => {
|
||||
try {
|
||||
// Try multiple approaches for maximum reliability
|
||||
let contacts: string[] = [];
|
||||
|
||||
// First try: Use NDK user's native follows
|
||||
if (currentUser.follows) {
|
||||
try {
|
||||
// Check if follows is an array, a Set, or a function
|
||||
if (Array.isArray(currentUser.follows)) {
|
||||
contacts = [...currentUser.follows];
|
||||
console.log(`Found ${contacts.length} contacts from array`);
|
||||
} else if (currentUser.follows instanceof Set) {
|
||||
contacts = Array.from(currentUser.follows);
|
||||
console.log(`Found ${contacts.length} contacts from Set`);
|
||||
} else if (typeof currentUser.follows === 'function') {
|
||||
// If it's a function, try to call it
|
||||
try {
|
||||
const followsResult = await currentUser.followSet();
|
||||
if (followsResult instanceof Set) {
|
||||
contacts = Array.from(followsResult);
|
||||
console.log(`Found ${contacts.length} contacts from followSet() function`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error calling followSet():", err);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("Error processing follows:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// Second try: Direct kind:3 fetch
|
||||
if (contacts.length === 0) {
|
||||
try {
|
||||
const contactEvents = await ndk.fetchEvents({
|
||||
kinds: [3],
|
||||
authors: [currentUser.pubkey],
|
||||
limit: 1
|
||||
}, {
|
||||
closeOnEose: true,
|
||||
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST
|
||||
});
|
||||
|
||||
if (contactEvents.size > 0) {
|
||||
const contactEvent = Array.from(contactEvents)[0];
|
||||
|
||||
const extracted = contactEvent.tags
|
||||
.filter(tag => tag[0] === 'p')
|
||||
.map(tag => tag[1]);
|
||||
|
||||
if (extracted.length > 0) {
|
||||
console.log(`Found ${extracted.length} contacts via direct kind:3 fetch`);
|
||||
contacts = extracted;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error fetching kind:3 events:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// If still no contacts found, try fetching any recent events and look for p-tags
|
||||
if (contacts.length === 0) {
|
||||
try {
|
||||
const userEvents = await ndk.fetchEvents({
|
||||
authors: [currentUser.pubkey],
|
||||
kinds: [1, 3, 7], // Notes, contacts, reactions
|
||||
limit: 10
|
||||
}, {
|
||||
closeOnEose: true,
|
||||
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST
|
||||
});
|
||||
|
||||
// Collect all p-tags from recent events
|
||||
const mentions = new Set<string>();
|
||||
for (const event of userEvents) {
|
||||
event.tags
|
||||
.filter(tag => tag[0] === 'p')
|
||||
.forEach(tag => mentions.add(tag[1]));
|
||||
}
|
||||
|
||||
if (mentions.size > 0) {
|
||||
console.log(`Found ${mentions.size} potential contacts from recent events`);
|
||||
contacts = Array.from(mentions);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error fetching recent events:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// If all else fails and we recognize this user, use hardcoded values (for testing only)
|
||||
if (contacts.length === 0 && currentUser?.pubkey === "55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21") {
|
||||
console.log("Using hardcoded follows for known user");
|
||||
contacts = [
|
||||
"3129509e23d3a6125e1451a5912dbe01099e151726c4766b44e1ecb8c846f506",
|
||||
"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52",
|
||||
"0c776e95521742beaf102523a8505c483e8c014ee0d3bd6457bb249034e5ff04",
|
||||
"2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884",
|
||||
"55127fc9e1c03c6b459a3bab72fdb99def1644c5f239bdd09f3e5fb401ed9b21"
|
||||
];
|
||||
}
|
||||
|
||||
// Always include self to ensure self-created content is visible
|
||||
if (currentUser.pubkey && !contacts.includes(currentUser.pubkey)) {
|
||||
contacts.push(currentUser.pubkey);
|
||||
}
|
||||
|
||||
// Add POWR account to followed users if not already there
|
||||
if (POWR_PUBKEY_HEX && !contacts.includes(POWR_PUBKEY_HEX)) {
|
||||
contacts.push(POWR_PUBKEY_HEX);
|
||||
}
|
||||
|
||||
console.log("Final contact list count:", contacts.length);
|
||||
setFollowedUsers(contacts);
|
||||
} catch (error) {
|
||||
console.error("Error fetching contact list:", error);
|
||||
} finally {
|
||||
setIsLoadingContacts(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchContactList();
|
||||
}, [ndk, currentUser?.pubkey]);
|
||||
|
||||
// Create filters with the correct follows
|
||||
const followingFilters = useMemo<NDKFilter[]>(() => {
|
||||
if (followedUsers.length === 0) {
|
||||
console.log("No users to follow, not creating filters");
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log("Creating filters for", followedUsers.length, "followed users");
|
||||
console.log("Sample follows:", followedUsers.slice(0, 3));
|
||||
|
||||
return [
|
||||
{
|
||||
kinds: [1] as any[], // Social posts
|
||||
authors: followedUsers,
|
||||
'#t': ['workout', 'fitness', 'exercise', 'powr', 'gym'], // Only workout-related posts
|
||||
limit: 30
|
||||
},
|
||||
{
|
||||
kinds: [30023] as any[], // Articles
|
||||
authors: followedUsers,
|
||||
limit: 20
|
||||
},
|
||||
{
|
||||
kinds: [1301, 33401, 33402] as any[], // Workout-specific content
|
||||
authors: followedUsers,
|
||||
limit: 30
|
||||
}
|
||||
];
|
||||
}, [followedUsers]);
|
||||
|
||||
// Use feed events hook - only enable if we have follows
|
||||
const feed = useFeedEvents(
|
||||
followedUsers.length > 0 ? followingFilters : false,
|
||||
{
|
||||
subId: 'following-feed',
|
||||
enabled: followedUsers.length > 0,
|
||||
feedType: 'following',
|
||||
...options
|
||||
}
|
||||
);
|
||||
|
||||
// Feed monitor for auto-refresh
|
||||
const monitor = useFeedMonitor({
|
||||
onRefresh: async () => {
|
||||
return feed.resetFeed();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...feed,
|
||||
...monitor,
|
||||
hasFollows: followedUsers.length > 0,
|
||||
followCount: followedUsers.length,
|
||||
followedUsers: followedUsers, // Make this available for debugging
|
||||
isLoadingContacts
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for the POWR tab in the social feed
|
||||
* Shows official POWR content and featured content
|
||||
*/
|
||||
export function usePOWRFeed(options: FeedOptions = {}) {
|
||||
// Create filters for POWR content
|
||||
const powrFilters = useMemo<NDKFilter[]>(() => {
|
||||
if (!POWR_PUBKEY_HEX) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
kinds: [1, 30023, 30024] as any[], // Social posts and articles (including drafts)
|
||||
authors: [POWR_PUBKEY_HEX],
|
||||
limit: 25
|
||||
},
|
||||
{
|
||||
kinds: [1301, 33401, 33402] as any[], // Workout-specific content
|
||||
authors: [POWR_PUBKEY_HEX],
|
||||
limit: 25
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
|
||||
// Filter function to ensure we don't show duplicates
|
||||
const filterPOWRContent = useCallback((entry: AnyFeedEntry) => {
|
||||
// Always show POWR content
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
// Use feed events hook
|
||||
const feed = useFeedEvents(
|
||||
POWR_PUBKEY_HEX ? powrFilters : false,
|
||||
{
|
||||
subId: 'powr-feed',
|
||||
feedType: 'powr',
|
||||
filterFn: filterPOWRContent,
|
||||
...options
|
||||
}
|
||||
);
|
||||
|
||||
// Feed monitor for auto-refresh
|
||||
const monitor = useFeedMonitor({
|
||||
onRefresh: async () => {
|
||||
return feed.resetFeed();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...feed,
|
||||
...monitor
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for the Global tab in the social feed
|
||||
* Shows all workout-related content
|
||||
*/
|
||||
/**
|
||||
* Hook for the user's own activity feed
|
||||
* Shows only the user's own posts and workouts
|
||||
*/
|
||||
export function useUserActivityFeed(options: FeedOptions = {}) {
|
||||
const { currentUser } = useNDKCurrentUser();
|
||||
const { ndk } = useNDK();
|
||||
|
||||
// Create filters for user's own content
|
||||
const userFilters = useMemo<NDKFilter[]>(() => {
|
||||
if (!currentUser?.pubkey) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
kinds: [1] as any[], // Social posts
|
||||
authors: [currentUser.pubkey],
|
||||
limit: 30
|
||||
},
|
||||
{
|
||||
kinds: [30023] as any[], // Articles
|
||||
authors: [currentUser.pubkey],
|
||||
limit: 20
|
||||
},
|
||||
{
|
||||
kinds: [1301, 33401, 33402] as any[], // Workout-specific content
|
||||
authors: [currentUser.pubkey],
|
||||
limit: 30
|
||||
}
|
||||
];
|
||||
}, [currentUser?.pubkey]);
|
||||
|
||||
// Use feed events hook
|
||||
const feed = useFeedEvents(
|
||||
currentUser?.pubkey ? userFilters : false,
|
||||
{
|
||||
subId: 'user-activity-feed',
|
||||
feedType: 'user-activity',
|
||||
...options
|
||||
}
|
||||
);
|
||||
|
||||
// Feed monitor for auto-refresh
|
||||
const monitor = useFeedMonitor({
|
||||
onRefresh: async () => {
|
||||
return feed.resetFeed();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...feed,
|
||||
...monitor,
|
||||
hasContent: feed.entries.length > 0
|
||||
};
|
||||
}
|
||||
|
||||
export function useGlobalFeed(options: FeedOptions = {}) {
|
||||
// Global filters - focus on workout content
|
||||
const globalFilters = useMemo<NDKFilter[]>(() => [
|
||||
{
|
||||
kinds: [1301] as any[], // Workout records
|
||||
limit: 20
|
||||
},
|
||||
{
|
||||
kinds: [1] as any[], // Social posts
|
||||
'#t': ['workout', 'fitness', 'powr', 'gym'], // With relevant tags
|
||||
limit: 20
|
||||
},
|
||||
{
|
||||
kinds: [33401, 33402] as any[], // Exercise templates and workout templates
|
||||
limit: 20
|
||||
}
|
||||
], []);
|
||||
|
||||
// Use feed events hook
|
||||
const feed = useFeedEvents(
|
||||
globalFilters,
|
||||
{
|
||||
subId: 'global-feed',
|
||||
feedType: 'global',
|
||||
...options
|
||||
}
|
||||
);
|
||||
|
||||
// Feed monitor for auto-refresh
|
||||
const monitor = useFeedMonitor({
|
||||
onRefresh: async () => {
|
||||
return feed.resetFeed();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...feed,
|
||||
...monitor
|
||||
};
|
||||
}
|
||||
|
@ -93,76 +93,76 @@ export function useSocialFeed(
|
||||
}
|
||||
}, [ndk, db]);
|
||||
|
||||
// Process event and add to feed
|
||||
// Process event and add to feed with improved error handling
|
||||
const processEvent = useCallback((event: NDKEvent) => {
|
||||
// Skip if we've seen this event before or event has no ID
|
||||
if (!event.id || seenEvents.current.has(event.id)) return;
|
||||
|
||||
console.log(`Processing event ${event.id}, kind ${event.kind} from ${event.pubkey}`);
|
||||
|
||||
// Check if this event is quoted by another event we've already seen
|
||||
// Skip unless it's from the POWR account (always show POWR content)
|
||||
if (
|
||||
quotedEvents.current.has(event.id) &&
|
||||
event.pubkey !== POWR_PUBKEY_HEX
|
||||
) {
|
||||
console.log(`Event ${event.id} filtered out: quoted=${true}, pubkey=${event.pubkey}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Track any events this quotes to avoid showing them separately
|
||||
if (event.kind === 1) {
|
||||
// Check e-tags (direct quotes)
|
||||
event.tags
|
||||
.filter(tag => tag[0] === 'e')
|
||||
.forEach(tag => {
|
||||
if (tag[1]) quotedEvents.current.add(tag[1]);
|
||||
});
|
||||
|
||||
// Check a-tags (addressable events)
|
||||
event.tags
|
||||
.filter(tag => tag[0] === 'a')
|
||||
.forEach(tag => {
|
||||
const parts = tag[1]?.split(':');
|
||||
if (parts && parts.length >= 3) {
|
||||
const [kind, pubkey, identifier] = parts;
|
||||
// We track the identifier so we can match it with the d-tag
|
||||
// of addressable events (kinds 30023, 33401, 33402, etc.)
|
||||
if (pubkey && identifier) {
|
||||
quotedEvents.current.add(`${pubkey}:${identifier}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Also check for quoted content using NIP-27 nostr: URI mentions
|
||||
if (event.content) {
|
||||
const nostrUriMatches = event.content.match(/nostr:(note1|nevent1|naddr1)[a-z0-9]+/g);
|
||||
if (nostrUriMatches) {
|
||||
nostrUriMatches.forEach(uri => {
|
||||
try {
|
||||
const decoded = nip19.decode(uri.replace('nostr:', ''));
|
||||
if (decoded.type === 'note' || decoded.type === 'nevent') {
|
||||
quotedEvents.current.add(decoded.data as string);
|
||||
} else if (decoded.type === 'naddr') {
|
||||
// For addressable content, add to tracking using pubkey:identifier format
|
||||
const data = decoded.data as any;
|
||||
quotedEvents.current.add(`${data.pubkey}:${data.identifier}`);
|
||||
try {
|
||||
// Skip if we've seen this event before or event has no ID
|
||||
if (!event.id || seenEvents.current.has(event.id)) return;
|
||||
|
||||
console.log(`Processing event ${event.id}, kind ${event.kind} from ${event.pubkey}`);
|
||||
|
||||
// Check if this event is quoted by another event we've already seen
|
||||
// Skip unless it's from the POWR account (always show POWR content)
|
||||
if (
|
||||
quotedEvents.current.has(event.id) &&
|
||||
event.pubkey !== POWR_PUBKEY_HEX
|
||||
) {
|
||||
console.log(`Event ${event.id} filtered out: quoted=${true}, pubkey=${event.pubkey}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Track any events this quotes to avoid showing them separately
|
||||
if (event.kind === 1) {
|
||||
// Check e-tags (direct quotes)
|
||||
event.tags
|
||||
.filter(tag => tag[0] === 'e')
|
||||
.forEach(tag => {
|
||||
if (tag[1]) quotedEvents.current.add(tag[1]);
|
||||
});
|
||||
|
||||
// Check a-tags (addressable events)
|
||||
event.tags
|
||||
.filter(tag => tag[0] === 'a')
|
||||
.forEach(tag => {
|
||||
const parts = tag[1]?.split(':');
|
||||
if (parts && parts.length >= 3) {
|
||||
const [kind, pubkey, identifier] = parts;
|
||||
// We track the identifier so we can match it with the d-tag
|
||||
// of addressable events (kinds 30023, 33401, 33402, etc.)
|
||||
if (pubkey && identifier) {
|
||||
quotedEvents.current.add(`${pubkey}:${identifier}`);
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore invalid nostr URIs
|
||||
}
|
||||
});
|
||||
|
||||
// Also check for quoted content using NIP-27 nostr: URI mentions
|
||||
if (event.content) {
|
||||
const nostrUriMatches = event.content.match(/nostr:(note1|nevent1|naddr1)[a-z0-9]+/g);
|
||||
if (nostrUriMatches) {
|
||||
nostrUriMatches.forEach(uri => {
|
||||
try {
|
||||
const decoded = nip19.decode(uri.replace('nostr:', ''));
|
||||
if (decoded.type === 'note' || decoded.type === 'nevent') {
|
||||
quotedEvents.current.add(decoded.data as string);
|
||||
} else if (decoded.type === 'naddr') {
|
||||
// For addressable content, add to tracking using pubkey:identifier format
|
||||
const data = decoded.data as any;
|
||||
quotedEvents.current.add(`${data.pubkey}:${data.identifier}`);
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore invalid nostr URIs
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark as seen
|
||||
seenEvents.current.add(event.id);
|
||||
|
||||
// Parse event based on kind
|
||||
let feedItem: FeedItem | null = null;
|
||||
|
||||
try {
|
||||
|
||||
// Mark as seen
|
||||
seenEvents.current.add(event.id);
|
||||
|
||||
// Parse event based on kind
|
||||
let feedItem: FeedItem | null = null;
|
||||
|
||||
const timestamp = event.created_at || Math.floor(Date.now() / 1000);
|
||||
|
||||
switch (event.kind) {
|
||||
@ -197,69 +197,73 @@ export function useSocialFeed(
|
||||
break;
|
||||
|
||||
case POWR_EVENT_KINDS.SOCIAL_POST: // 1
|
||||
// Parse social post
|
||||
const parsedSocialPost = parseSocialPost(event);
|
||||
|
||||
feedItem = {
|
||||
id: event.id,
|
||||
type: 'social',
|
||||
originalEvent: event,
|
||||
parsedContent: parsedSocialPost,
|
||||
createdAt: timestamp
|
||||
};
|
||||
|
||||
// If it has quoted content, resolve it asynchronously
|
||||
const quotedContent = parsedSocialPost.quotedContent;
|
||||
if (quotedContent && socialServiceRef.current) {
|
||||
socialServiceRef.current.getReferencedContent(quotedContent.id, quotedContent.kind)
|
||||
.then(referencedEvent => {
|
||||
if (!referencedEvent) return;
|
||||
|
||||
// Parse the referenced event
|
||||
let resolvedContent: any = null;
|
||||
|
||||
switch (referencedEvent.kind) {
|
||||
case POWR_EVENT_KINDS.WORKOUT_RECORD:
|
||||
resolvedContent = parseWorkoutRecord(referencedEvent);
|
||||
break;
|
||||
case POWR_EVENT_KINDS.EXERCISE_TEMPLATE:
|
||||
resolvedContent = parseExerciseTemplate(referencedEvent);
|
||||
break;
|
||||
case POWR_EVENT_KINDS.WORKOUT_TEMPLATE:
|
||||
resolvedContent = parseWorkoutTemplate(referencedEvent);
|
||||
break;
|
||||
case 30023:
|
||||
case 30024:
|
||||
resolvedContent = parseLongformContent(referencedEvent);
|
||||
break;
|
||||
}
|
||||
|
||||
if (resolvedContent) {
|
||||
// Update the feed item with the referenced content
|
||||
setFeedItems(current => {
|
||||
return current.map(item => {
|
||||
if (item.id !== event.id) return item;
|
||||
|
||||
// Add the resolved content to the social post
|
||||
const updatedContent = {
|
||||
...(item.parsedContent as ParsedSocialPost),
|
||||
quotedContent: {
|
||||
...quotedContent,
|
||||
resolved: resolvedContent
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...item,
|
||||
parsedContent: updatedContent
|
||||
};
|
||||
try {
|
||||
// Parse social post
|
||||
const parsedSocialPost = parseSocialPost(event);
|
||||
|
||||
feedItem = {
|
||||
id: event.id,
|
||||
type: 'social',
|
||||
originalEvent: event,
|
||||
parsedContent: parsedSocialPost,
|
||||
createdAt: timestamp
|
||||
};
|
||||
|
||||
// If it has quoted content, resolve it asynchronously
|
||||
const quotedContent = parsedSocialPost.quotedContent;
|
||||
if (quotedContent && socialServiceRef.current) {
|
||||
socialServiceRef.current.getReferencedContent(quotedContent.id, quotedContent.kind)
|
||||
.then(referencedEvent => {
|
||||
if (!referencedEvent) return;
|
||||
|
||||
// Parse the referenced event
|
||||
let resolvedContent: any = null;
|
||||
|
||||
switch (referencedEvent.kind) {
|
||||
case POWR_EVENT_KINDS.WORKOUT_RECORD:
|
||||
resolvedContent = parseWorkoutRecord(referencedEvent);
|
||||
break;
|
||||
case POWR_EVENT_KINDS.EXERCISE_TEMPLATE:
|
||||
resolvedContent = parseExerciseTemplate(referencedEvent);
|
||||
break;
|
||||
case POWR_EVENT_KINDS.WORKOUT_TEMPLATE:
|
||||
resolvedContent = parseWorkoutTemplate(referencedEvent);
|
||||
break;
|
||||
case 30023:
|
||||
case 30024:
|
||||
resolvedContent = parseLongformContent(referencedEvent);
|
||||
break;
|
||||
}
|
||||
|
||||
if (resolvedContent) {
|
||||
// Update the feed item with the referenced content
|
||||
setFeedItems(current => {
|
||||
return current.map(item => {
|
||||
if (item.id !== event.id) return item;
|
||||
|
||||
// Add the resolved content to the social post
|
||||
const updatedContent = {
|
||||
...(item.parsedContent as ParsedSocialPost),
|
||||
quotedContent: {
|
||||
...quotedContent,
|
||||
resolved: resolvedContent
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...item,
|
||||
parsedContent: updatedContent
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching referenced content:', error);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching referenced content:', error);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing social post:', error);
|
||||
}
|
||||
break;
|
||||
|
||||
@ -311,7 +315,7 @@ export function useSocialFeed(
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing event:', error, event);
|
||||
console.error('Error processing event:', error);
|
||||
}
|
||||
}, [oldestTimestamp, options.feedType]);
|
||||
|
||||
@ -371,21 +375,27 @@ export function useSocialFeed(
|
||||
}, [options.feedType, options.authors, options.kinds, options.limit, options.since, options.until]);
|
||||
|
||||
// Load feed data
|
||||
const loadFeed = useCallback(async () => {
|
||||
const loadFeed = useCallback(async (forceRefresh = false) => {
|
||||
if (!ndk) return;
|
||||
|
||||
// Prevent rapid resubscriptions
|
||||
if (subscriptionCooldown.current) {
|
||||
console.log('[useSocialFeed] Subscription on cooldown, skipping');
|
||||
// Prevent rapid resubscriptions unless forceRefresh is true
|
||||
if (subscriptionCooldown.current && !forceRefresh) {
|
||||
console.log('[useSocialFeed] Subscription on cooldown, skipping (use forceRefresh to override)');
|
||||
return;
|
||||
}
|
||||
|
||||
// Track subscription attempts to prevent infinite loops
|
||||
subscriptionAttempts.current += 1;
|
||||
if (subscriptionAttempts.current > maxSubscriptionAttempts) {
|
||||
console.error(`[useSocialFeed] Too many subscription attempts (${subscriptionAttempts.current}), giving up`);
|
||||
setLoading(false);
|
||||
return;
|
||||
// Reset counter if this is a forced refresh
|
||||
if (forceRefresh) {
|
||||
subscriptionAttempts.current = 0;
|
||||
console.log('[useSocialFeed] Force refresh requested, resetting attempt counter');
|
||||
} else {
|
||||
subscriptionAttempts.current += 1;
|
||||
if (subscriptionAttempts.current > maxSubscriptionAttempts) {
|
||||
console.error(`[useSocialFeed] Too many subscription attempts (${subscriptionAttempts.current}), giving up`);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
@ -430,11 +440,11 @@ export function useSocialFeed(
|
||||
console.log(`[useSocialFeed] Loading ${feedOptions.feedType} feed with authors:`, feedOptions.authors);
|
||||
console.log(`[useSocialFeed] Time range: since=${new Date(feedOptions.since * 1000).toISOString()}, until=${feedOptions.until ? new Date(feedOptions.until * 1000).toISOString() : 'now'}`);
|
||||
|
||||
// For following feed, ensure we have authors
|
||||
// For following feed, log if we have no authors but continue with subscription
|
||||
// The socialFeedService will use the POWR_PUBKEY_HEX as fallback
|
||||
if (feedOptions.feedType === 'following' && (!feedOptions.authors || feedOptions.authors.length === 0)) {
|
||||
console.log('[useSocialFeed] Following feed with no authors, skipping subscription');
|
||||
setLoading(false);
|
||||
return;
|
||||
console.log('[useSocialFeed] Following feed with no authors, continuing with fallback');
|
||||
// We'll continue with the subscription and rely on the fallback in socialFeedService
|
||||
}
|
||||
|
||||
// Build and validate filters before subscribing
|
||||
@ -542,8 +552,8 @@ export function useSocialFeed(
|
||||
}, [ndk, options.feedType, options.limit, options.since, options.until, processEvent]);
|
||||
|
||||
// Refresh feed (clear events and reload)
|
||||
const refresh = useCallback(async () => {
|
||||
console.log(`Refreshing ${options.feedType} feed`);
|
||||
const refresh = useCallback(async (forceRefresh = true) => {
|
||||
console.log(`Refreshing ${options.feedType} feed (force=${forceRefresh})`);
|
||||
setFeedItems([]);
|
||||
seenEvents.current.clear();
|
||||
quotedEvents.current.clear(); // Also reset quoted events
|
||||
@ -555,7 +565,8 @@ export function useSocialFeed(
|
||||
setIsOffline(!isOnline);
|
||||
|
||||
if (isOnline) {
|
||||
await loadFeed();
|
||||
// Pass forceRefresh to loadFeed to bypass cooldown if needed
|
||||
await loadFeed(forceRefresh);
|
||||
} else {
|
||||
await loadCachedFeed();
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import * as SecureStore from 'expo-secure-store';
|
||||
import { RelayService, DEFAULT_RELAYS } from '@/lib/db/services/RelayService';
|
||||
import { extendNDK } from '@/types/ndk-extensions';
|
||||
import { ConnectivityService } from '@/lib/db/services/ConnectivityService';
|
||||
import { profileImageCache } from '@/lib/db/services/ProfileImageCache';
|
||||
|
||||
// Connection timeout in milliseconds
|
||||
const CONNECTION_TIMEOUT = 5000;
|
||||
@ -47,8 +48,9 @@ export async function initializeNDK() {
|
||||
// Extend NDK with helper methods for better compatibility
|
||||
ndk = extendNDK(ndk);
|
||||
|
||||
// Set the NDK instance in the RelayService
|
||||
// Set the NDK instance in services
|
||||
relayService.setNDK(ndk);
|
||||
profileImageCache.setNDK(ndk);
|
||||
|
||||
// Check network connectivity before attempting to connect
|
||||
const connectivityService = ConnectivityService.getInstance();
|
||||
|
@ -57,7 +57,7 @@ export class SocialFeedService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Build filters for a feed subscription
|
||||
* Build filters for a feed subscription with improved error handling
|
||||
* @param options Filter options
|
||||
* @returns NDK filter object or array of filters
|
||||
*/
|
||||
@ -69,141 +69,187 @@ export class SocialFeedService {
|
||||
authors?: string[];
|
||||
kinds?: number[];
|
||||
}): NDKFilter | NDKFilter[] {
|
||||
const { feedType, since, until, limit, authors, kinds } = options;
|
||||
|
||||
// Default to events in the last 24 hours if no since provided
|
||||
const defaultSince = Math.floor(Date.now() / 1000) - 24 * 60 * 60;
|
||||
|
||||
// Fitness-related tags for filtering
|
||||
const tagFilter = [
|
||||
'workout', 'fitness', 'powr', '31days',
|
||||
'crossfit', 'wod', 'gym', 'strength',
|
||||
'cardio', 'training', 'exercise'
|
||||
];
|
||||
|
||||
// Determine which kinds to include
|
||||
const workoutKinds: number[] = [];
|
||||
const socialKinds: number[] = [];
|
||||
|
||||
// Add workout-specific kinds (1301, 33401, 33402)
|
||||
if (!kinds || kinds.some(k => [1301, 33401, 33402].includes(k))) {
|
||||
[1301, 33401, 33402]
|
||||
.filter(k => !kinds || kinds.includes(k))
|
||||
.forEach(k => workoutKinds.push(k));
|
||||
}
|
||||
|
||||
// Add social post kind (1) and article kind (30023)
|
||||
if (!kinds || kinds.includes(1)) {
|
||||
socialKinds.push(1);
|
||||
}
|
||||
|
||||
if (!kinds || kinds.includes(30023)) {
|
||||
socialKinds.push(30023);
|
||||
}
|
||||
|
||||
// Base filter properties
|
||||
const baseFilter: Record<string, any> = {
|
||||
since: since || defaultSince,
|
||||
limit: limit || 30,
|
||||
};
|
||||
|
||||
if (until) {
|
||||
baseFilter.until = until;
|
||||
}
|
||||
|
||||
// Special handling for different feed types
|
||||
if (feedType === 'profile') {
|
||||
// Profile feed: Show all of a user's posts
|
||||
if (!Array.isArray(authors) || authors.length === 0) {
|
||||
console.error('[SocialFeedService] Profile feed requires authors');
|
||||
return { ...baseFilter, kinds: [] }; // Return empty filter if no authors
|
||||
}
|
||||
try {
|
||||
const { feedType, since, until, limit, authors, kinds } = options;
|
||||
|
||||
// For profile feed, we create two filters:
|
||||
// 1. All workout-related kinds from the user
|
||||
// 2. Social posts and articles from the user (with or without tags)
|
||||
return [
|
||||
// Workout-related kinds (no tag filtering)
|
||||
{
|
||||
...baseFilter,
|
||||
kinds: workoutKinds,
|
||||
authors: authors,
|
||||
},
|
||||
// Social posts and articles (no tag filtering for profile)
|
||||
{
|
||||
...baseFilter,
|
||||
kinds: socialKinds,
|
||||
authors: authors,
|
||||
}
|
||||
// Default to events in the last 24 hours if no since provided
|
||||
const defaultSince = Math.floor(Date.now() / 1000) - 24 * 60 * 60;
|
||||
|
||||
// Fitness-related tags for filtering
|
||||
const tagFilter = [
|
||||
'workout', 'fitness', 'powr', '31days',
|
||||
'crossfit', 'wod', 'gym', 'strength',
|
||||
'cardio', 'training', 'exercise'
|
||||
];
|
||||
} else if (feedType === 'powr') {
|
||||
// POWR feed: Show all content from POWR account(s)
|
||||
if (!Array.isArray(authors) || authors.length === 0) {
|
||||
console.error('[SocialFeedService] POWR feed requires authors');
|
||||
return { ...baseFilter, kinds: [] }; // Return empty filter if no authors
|
||||
|
||||
// Determine which kinds to include
|
||||
const workoutKinds: number[] = [];
|
||||
const socialKinds: number[] = [];
|
||||
|
||||
// Add workout-specific kinds (1301, 33401, 33402)
|
||||
if (!kinds || kinds.some(k => [1301, 33401, 33402].includes(k))) {
|
||||
[1301, 33401, 33402]
|
||||
.filter(k => !kinds || kinds.includes(k))
|
||||
.forEach(k => workoutKinds.push(k));
|
||||
}
|
||||
|
||||
// For POWR feed, we don't apply tag filtering
|
||||
return {
|
||||
...baseFilter,
|
||||
kinds: [...workoutKinds, ...socialKinds],
|
||||
authors: authors,
|
||||
// Add social post kind (1) and article kind (30023)
|
||||
if (!kinds || kinds.includes(1)) {
|
||||
socialKinds.push(1);
|
||||
}
|
||||
|
||||
if (!kinds || kinds.includes(30023)) {
|
||||
socialKinds.push(30023);
|
||||
}
|
||||
|
||||
// Base filter properties
|
||||
const baseFilter: Record<string, any> = {
|
||||
since: since || defaultSince,
|
||||
limit: limit || 30,
|
||||
};
|
||||
} else if (feedType === 'following') {
|
||||
// Following feed: Show content from followed users
|
||||
if (!Array.isArray(authors) || authors.length === 0) {
|
||||
console.error('[SocialFeedService] Following feed requires authors');
|
||||
return { ...baseFilter, kinds: [] }; // Return empty filter if no authors
|
||||
|
||||
if (until) {
|
||||
baseFilter.until = until;
|
||||
}
|
||||
|
||||
// For following feed, we create two filters:
|
||||
// 1. All workout-related kinds from followed users
|
||||
// 2. Social posts and articles from followed users with fitness tags
|
||||
|
||||
// Log the authors to help with debugging
|
||||
console.log(`[SocialFeedService] Following feed with ${authors.length} authors:`,
|
||||
authors.length > 5 ? authors.slice(0, 5).join(', ') + '...' : authors.join(', '));
|
||||
|
||||
// Always include POWR account in following feed
|
||||
let followingAuthors = [...authors];
|
||||
if (POWR_PUBKEY_HEX && !followingAuthors.includes(POWR_PUBKEY_HEX)) {
|
||||
followingAuthors.push(POWR_PUBKEY_HEX);
|
||||
console.log('[SocialFeedService] Added POWR account to following feed authors');
|
||||
// Special handling for different feed types
|
||||
if (feedType === 'profile') {
|
||||
// Profile feed: Show all of a user's posts
|
||||
if (!Array.isArray(authors) || authors.length === 0) {
|
||||
console.error('[SocialFeedService] Profile feed requires authors');
|
||||
return { ...baseFilter, kinds: [] }; // Return empty filter if no authors
|
||||
}
|
||||
|
||||
// For profile feed, we create two filters:
|
||||
// 1. All workout-related kinds from the user
|
||||
// 2. Social posts and articles from the user (with or without tags)
|
||||
return [
|
||||
// Workout-related kinds (no tag filtering)
|
||||
{
|
||||
...baseFilter,
|
||||
kinds: workoutKinds,
|
||||
authors: authors,
|
||||
},
|
||||
// Social posts and articles (no tag filtering for profile)
|
||||
{
|
||||
...baseFilter,
|
||||
kinds: socialKinds,
|
||||
authors: authors,
|
||||
}
|
||||
];
|
||||
} else if (feedType === 'powr') {
|
||||
// POWR feed: Show all content from POWR account(s)
|
||||
if (!Array.isArray(authors) || authors.length === 0) {
|
||||
console.error('[SocialFeedService] POWR feed requires authors');
|
||||
|
||||
// For POWR feed, if no authors provided, use the POWR_PUBKEY_HEX as fallback
|
||||
if (POWR_PUBKEY_HEX) {
|
||||
console.log('[SocialFeedService] Using POWR account as fallback for POWR feed');
|
||||
const fallbackAuthors = [POWR_PUBKEY_HEX];
|
||||
return {
|
||||
...baseFilter,
|
||||
kinds: [...workoutKinds, ...socialKinds],
|
||||
authors: fallbackAuthors,
|
||||
};
|
||||
} else {
|
||||
return { ...baseFilter, kinds: [] }; // Return empty filter if no authors and no fallback
|
||||
}
|
||||
}
|
||||
|
||||
// For POWR feed, we don't apply tag filtering
|
||||
return {
|
||||
...baseFilter,
|
||||
kinds: [...workoutKinds, ...socialKinds],
|
||||
authors: authors,
|
||||
};
|
||||
} else if (feedType === 'following') {
|
||||
// Following feed: Show content from followed users
|
||||
if (!Array.isArray(authors) || authors.length === 0) {
|
||||
console.error('[SocialFeedService] Following feed requires authors');
|
||||
|
||||
// For following feed, if no authors provided, use the POWR_PUBKEY_HEX as fallback
|
||||
// This ensures at least some content is shown
|
||||
if (POWR_PUBKEY_HEX) {
|
||||
console.log('[SocialFeedService] Using POWR account as fallback for Following feed');
|
||||
const fallbackAuthors = [POWR_PUBKEY_HEX];
|
||||
|
||||
return [
|
||||
// Workout-related kinds (no tag filtering)
|
||||
{
|
||||
...baseFilter,
|
||||
kinds: workoutKinds,
|
||||
authors: fallbackAuthors,
|
||||
},
|
||||
// Social posts and articles (with tag filtering)
|
||||
{
|
||||
...baseFilter,
|
||||
kinds: socialKinds,
|
||||
authors: fallbackAuthors,
|
||||
'#t': tagFilter,
|
||||
}
|
||||
];
|
||||
} else {
|
||||
return { ...baseFilter, kinds: [] }; // Return empty filter if no authors and no fallback
|
||||
}
|
||||
}
|
||||
|
||||
// For following feed, we create two filters:
|
||||
// 1. All workout-related kinds from followed users
|
||||
// 2. Social posts and articles from followed users with fitness tags
|
||||
|
||||
// Log the authors to help with debugging
|
||||
console.log(`[SocialFeedService] Following feed with ${authors.length} authors:`,
|
||||
authors.length > 5 ? authors.slice(0, 5).join(', ') + '...' : authors.join(', '));
|
||||
|
||||
// Always include POWR account in following feed
|
||||
let followingAuthors = [...authors];
|
||||
if (POWR_PUBKEY_HEX && !followingAuthors.includes(POWR_PUBKEY_HEX)) {
|
||||
followingAuthors.push(POWR_PUBKEY_HEX);
|
||||
console.log('[SocialFeedService] Added POWR account to following feed authors');
|
||||
}
|
||||
|
||||
return [
|
||||
// Workout-related kinds (no tag filtering)
|
||||
{
|
||||
...baseFilter,
|
||||
kinds: workoutKinds,
|
||||
authors: followingAuthors,
|
||||
},
|
||||
// Social posts and articles (with tag filtering)
|
||||
{
|
||||
...baseFilter,
|
||||
kinds: socialKinds,
|
||||
authors: followingAuthors,
|
||||
'#t': tagFilter,
|
||||
}
|
||||
];
|
||||
} else {
|
||||
// Global feed: Show content from anyone
|
||||
// For global feed, we create two filters:
|
||||
// 1. All workout-related kinds from anyone
|
||||
// 2. Social posts and articles from anyone with fitness tags
|
||||
return [
|
||||
// Workout-related kinds (no tag filtering)
|
||||
{
|
||||
...baseFilter,
|
||||
kinds: workoutKinds,
|
||||
},
|
||||
// Social posts and articles (with tag filtering)
|
||||
{
|
||||
...baseFilter,
|
||||
kinds: socialKinds,
|
||||
'#t': tagFilter,
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
// Workout-related kinds (no tag filtering)
|
||||
{
|
||||
...baseFilter,
|
||||
kinds: workoutKinds,
|
||||
authors: followingAuthors,
|
||||
},
|
||||
// Social posts and articles (with tag filtering)
|
||||
{
|
||||
...baseFilter,
|
||||
kinds: socialKinds,
|
||||
authors: followingAuthors,
|
||||
'#t': tagFilter,
|
||||
}
|
||||
];
|
||||
} else {
|
||||
// Global feed: Show content from anyone
|
||||
// For global feed, we create two filters:
|
||||
// 1. All workout-related kinds from anyone
|
||||
// 2. Social posts and articles from anyone with fitness tags
|
||||
return [
|
||||
// Workout-related kinds (no tag filtering)
|
||||
{
|
||||
...baseFilter,
|
||||
kinds: workoutKinds,
|
||||
},
|
||||
// Social posts and articles (with tag filtering)
|
||||
{
|
||||
...baseFilter,
|
||||
kinds: socialKinds,
|
||||
'#t': tagFilter,
|
||||
}
|
||||
];
|
||||
} catch (error) {
|
||||
console.error('[SocialFeedService] Error building filters:', error);
|
||||
// Return a safe default filter that won't crash but also won't return much
|
||||
return {
|
||||
kinds: [1], // Just social posts
|
||||
limit: 10,
|
||||
since: Math.floor(Date.now() / 1000) - 24 * 60 * 60 // Last 24 hours
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -104,6 +104,14 @@ export const useNDKStore = create<NDKStoreState & NDKStoreActions>((set, get) =>
|
||||
});
|
||||
|
||||
await ndk.connect();
|
||||
|
||||
// Set NDK in services
|
||||
const { profileImageCache } = require('@/lib/db/services/ProfileImageCache');
|
||||
profileImageCache.setNDK(ndk);
|
||||
|
||||
// Note: SocialFeedCache initialization is now handled in the RelayInitializer component
|
||||
// This avoids using React hooks outside of component context
|
||||
|
||||
set({ ndk, relayStatus });
|
||||
|
||||
// Check for saved private key
|
||||
@ -417,4 +425,4 @@ export function useNDKEvents() {
|
||||
publishEvent: state.publishEvent,
|
||||
fetchEventsByFilter: state.fetchEventsByFilter
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user