The entire skate-spots codebase was accidentally copied into src/src/ and public/public/ instead of src/ and public/ directly. This left the real src/ directory as blank MKStack template stubs, explaining why the landing page showed the default template instead of SkateSpots. Changes made: - Copied all files from src/src/ up to src/ (App.tsx, AppRouter.tsx, main.tsx, all pages, components, hooks, contexts, lib, i18n, test) - Copied all files from public/public/ up to public/ (images, manifest, etc.) - Deleted src/src/ and public/public/ nested directories - Removed HelloWorld.tsx template stub from pages/ - Retained the previously fixed index.css (Tailwind v3 directives) Full skate-spots feature set is now active: - All 30+ routes (map, events, spots, coaches, marketplace, wallet, etc.) - NWCProvider, LanguageProvider, PWAInstallPrompt, AmberCallbackHandler - Skate community relays (goskatespots.nostr1.com, wotr.relatr.xyz, etc.) - Outfit Variable font, Amber pre-check in main.tsx - All hooks (useSkateSpots, useCoaches, useZaps, etc.) - i18n support (en/es) Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
98 lines
No EOL
3.8 KiB
TypeScript
98 lines
No EOL
3.8 KiB
TypeScript
import { NKinds, NostrEvent, NostrFilter } from '@nostrify/nostrify';
|
|
import { useNostr } from '@nostrify/react';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
|
|
export function useComments(root: NostrEvent | URL, limit?: number) {
|
|
const { nostr } = useNostr();
|
|
|
|
return useQuery({
|
|
queryKey: ['nostr', 'comments', root instanceof URL ? root.toString() : root.id, limit],
|
|
queryFn: async (c) => {
|
|
const filter: NostrFilter = { kinds: [1111] };
|
|
|
|
if (root instanceof URL) {
|
|
filter['#I'] = [root.toString()];
|
|
} else if (NKinds.addressable(root.kind)) {
|
|
const d = root.tags.find(([name]) => name === 'd')?.[1] ?? '';
|
|
filter['#A'] = [`${root.kind}:${root.pubkey}:${d}`];
|
|
} else if (NKinds.replaceable(root.kind)) {
|
|
filter['#A'] = [`${root.kind}:${root.pubkey}:`];
|
|
} else {
|
|
filter['#E'] = [root.id];
|
|
}
|
|
|
|
if (typeof limit === 'number') {
|
|
filter.limit = limit;
|
|
}
|
|
|
|
// Query for all kind 1111 comments that reference this addressable event regardless of depth
|
|
const signal = AbortSignal.any([c.signal, AbortSignal.timeout(5000)]);
|
|
const events = await nostr.query([filter], { signal });
|
|
|
|
// Helper function to get tag value
|
|
const getTagValue = (event: NostrEvent, tagName: string): string | undefined => {
|
|
const tag = event.tags.find(([name]) => name === tagName);
|
|
return tag?.[1];
|
|
};
|
|
|
|
// Filter top-level comments (those with lowercase tag matching the root)
|
|
const topLevelComments = events.filter(comment => {
|
|
if (root instanceof URL) {
|
|
return getTagValue(comment, 'i') === root.toString();
|
|
} else if (NKinds.addressable(root.kind)) {
|
|
const d = getTagValue(root, 'd') ?? '';
|
|
return getTagValue(comment, 'a') === `${root.kind}:${root.pubkey}:${d}`;
|
|
} else if (NKinds.replaceable(root.kind)) {
|
|
return getTagValue(comment, 'a') === `${root.kind}:${root.pubkey}:`;
|
|
} else {
|
|
return getTagValue(comment, 'e') === root.id;
|
|
}
|
|
});
|
|
|
|
// Helper function to get all descendants of a comment
|
|
const getDescendants = (parentId: string): NostrEvent[] => {
|
|
const directReplies = events.filter(comment => {
|
|
const eTag = getTagValue(comment, 'e');
|
|
return eTag === parentId;
|
|
});
|
|
|
|
const allDescendants = [...directReplies];
|
|
|
|
// Recursively get descendants of each direct reply
|
|
for (const reply of directReplies) {
|
|
allDescendants.push(...getDescendants(reply.id));
|
|
}
|
|
|
|
return allDescendants;
|
|
};
|
|
|
|
// Create a map of comment ID to its descendants
|
|
const commentDescendants = new Map<string, NostrEvent[]>();
|
|
for (const comment of events) {
|
|
commentDescendants.set(comment.id, getDescendants(comment.id));
|
|
}
|
|
|
|
// Sort top-level comments by creation time (newest first)
|
|
const sortedTopLevel = topLevelComments.sort((a, b) => b.created_at - a.created_at);
|
|
|
|
return {
|
|
allComments: events,
|
|
topLevelComments: sortedTopLevel,
|
|
getDescendants: (commentId: string) => {
|
|
const descendants = commentDescendants.get(commentId) || [];
|
|
// Sort descendants by creation time (oldest first for threaded display)
|
|
return descendants.sort((a, b) => a.created_at - b.created_at);
|
|
},
|
|
getDirectReplies: (commentId: string) => {
|
|
const directReplies = events.filter(comment => {
|
|
const eTag = getTagValue(comment, 'e');
|
|
return eTag === commentId;
|
|
});
|
|
// Sort direct replies by creation time (oldest first for threaded display)
|
|
return directReplies.sort((a, b) => a.created_at - b.created_at);
|
|
}
|
|
};
|
|
},
|
|
enabled: !!root,
|
|
});
|
|
} |