POWR/lib/hooks/useFeedHooks.ts

383 lines
11 KiB
TypeScript

// 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';
// POWR official account pubkey
export const POWR_ACCOUNT_PUBKEY = 'npub1p0wer69rpkraqs02l5v8rutagfh6g9wxn2dgytkv44ysz7avt8nsusvpjk';
// Convert POWR account pubkey to hex at the module level
export let POWR_PUBKEY_HEX: string = '';
try {
if (POWR_ACCOUNT_PUBKEY.startsWith('npub')) {
const decoded = nip19.decode(POWR_ACCOUNT_PUBKEY);
POWR_PUBKEY_HEX = decoded.data as string;
} else {
POWR_PUBKEY_HEX = POWR_ACCOUNT_PUBKEY;
}
console.log("Initialized POWR pubkey hex:", POWR_PUBKEY_HEX);
} catch (error) {
console.error('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
*/
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
};
}