feat(spots): crews + Lab links count as strong signals; add a verify nudge
- useSpotCommunityScore now also reads crews (35006, `a` ref, weight 3) and Lab profile links (35001-35014, `a` ref, weight 2) as effortful signals — a whole crew listing the spot, or a Lab pro linking it, is strong evidence. A crew ref alone reaches "community". (Tours/events use geohash, not a spot coord, so they're not counted.) - Spot page: on an unconfirmed spot, a logged-in non-owner now sees a "Be the first to confirm this spot" nudge with Add-photo + "I've skated here". - Breakdown line/tooltip include crew + Lab counts.
This commit is contained in:
parent
1a9a5ae967
commit
77a3d592ea
4 changed files with 46 additions and 5 deletions
|
|
@ -17,7 +17,7 @@ import type { NostrEvent } from '@nostrify/nostrify';
|
|||
import { useSkateRelays } from '@/hooks/useSkateRelays';
|
||||
import { useBatchTrustScores } from '@/hooks/useBatchTrustScores';
|
||||
import { useBatchActivity } from '@/hooks/useTradeIntegrity';
|
||||
import { SKATE_SPOT_KIND } from '@/lib/types';
|
||||
import { SKATE_SPOT_KIND, SKATE_CREW_KIND, LAB_KINDS } from '@/lib/types';
|
||||
import { getSatoshisFromBolt11 } from '@/lib/bolt11';
|
||||
|
||||
const PHOTO_KIND = 1063; // NIP-94 file metadata (community photo)
|
||||
|
|
@ -38,6 +38,8 @@ export interface SpotCommunityBreakdown {
|
|||
zapSats: number;
|
||||
collections: number;
|
||||
tagSuggestions: number;
|
||||
crews: number;
|
||||
labRefs: number;
|
||||
}
|
||||
|
||||
export type SpotCommunityLevel = 'unverified' | 'seen' | 'community';
|
||||
|
|
@ -55,6 +57,8 @@ const WEIGHT = {
|
|||
photo: 2,
|
||||
status: 2,
|
||||
zap: 3, // real money
|
||||
crew: 3, // a whole crew listing it as home turf
|
||||
labRef: 2, // a Lab pro (builder/coach/…) linking the spot
|
||||
comment: 1,
|
||||
reaction: 1,
|
||||
collection: 1,
|
||||
|
|
@ -75,11 +79,14 @@ interface AccountSignals {
|
|||
zapSats: number;
|
||||
collections: number;
|
||||
tagSuggestions: number;
|
||||
crews: number;
|
||||
labRefs: number;
|
||||
}
|
||||
|
||||
const emptyBreakdown = (): SpotCommunityBreakdown => ({
|
||||
skaters: 0, photos: 0, statusReports: 0, comments: 0,
|
||||
reactions: 0, zaps: 0, zapSats: 0, collections: 0, tagSuggestions: 0,
|
||||
crews: 0, labRefs: 0,
|
||||
});
|
||||
|
||||
function classify(ev: NostrEvent): keyof AccountSignals | null {
|
||||
|
|
@ -91,7 +98,9 @@ function classify(ev: NostrEvent): keyof AccountSignals | null {
|
|||
case REACTION_KIND: return 'reactions';
|
||||
case ZAP_KIND: return 'zaps';
|
||||
case COLLECTION_KIND: return 'collections';
|
||||
default: return null;
|
||||
case SKATE_CREW_KIND: return 'crews';
|
||||
default:
|
||||
return (LAB_KINDS as readonly number[]).includes(ev.kind) ? 'labRefs' : null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -129,6 +138,8 @@ export function useSpotCommunityScore(
|
|||
{ kinds: [ZAP_KIND], '#a': [coord], limit: 100 },
|
||||
{ kinds: [ZAP_KIND], '#e': [spot.id], limit: 100 },
|
||||
{ kinds: [COLLECTION_KIND], '#a': [coord], limit: 100 },
|
||||
{ kinds: [SKATE_CREW_KIND], '#a': [coord], limit: 50 },
|
||||
{ kinds: [...LAB_KINDS], '#a': [coord], limit: 100 },
|
||||
],
|
||||
{ signal },
|
||||
);
|
||||
|
|
@ -147,7 +158,7 @@ export function useSpotCommunityScore(
|
|||
const key = classify(ev);
|
||||
if (!key) continue;
|
||||
const acc = map.get(ev.pubkey) ?? {
|
||||
photos: 0, statusReports: 0, comments: 0, reactions: 0, zaps: 0, zapSats: 0, collections: 0, tagSuggestions: 0,
|
||||
photos: 0, statusReports: 0, comments: 0, reactions: 0, zaps: 0, zapSats: 0, collections: 0, tagSuggestions: 0, crews: 0, labRefs: 0,
|
||||
};
|
||||
acc[key] += 1;
|
||||
if (key === 'zaps') acc.zapSats += zapSatsFrom(ev);
|
||||
|
|
@ -167,6 +178,8 @@ export function useSpotCommunityScore(
|
|||
b.zapSats += a.zapSats;
|
||||
b.collections += a.collections;
|
||||
b.tagSuggestions += a.tagSuggestions;
|
||||
b.crews += a.crews;
|
||||
b.labRefs += a.labRefs;
|
||||
}
|
||||
b.skaters = byAccount.size;
|
||||
return b;
|
||||
|
|
@ -187,7 +200,9 @@ export function useSpotCommunityScore(
|
|||
a.comments * WEIGHT.comment +
|
||||
a.reactions * WEIGHT.reaction +
|
||||
a.collections * WEIGHT.collection +
|
||||
a.tagSuggestions * WEIGHT.tagSuggestion;
|
||||
a.tagSuggestions * WEIGHT.tagSuggestion +
|
||||
a.crews * WEIGHT.crew +
|
||||
a.labRefs * WEIGHT.labRef;
|
||||
const capped = Math.min(raw, PER_ACCOUNT_CAP);
|
||||
// Trust (0..100) and activity both raise the multiplier; unknown → neutral 1.
|
||||
const trustFactor = trust ? 0.4 + 0.6 * ((trust.get(pk) ?? 0) / 100) : 1;
|
||||
|
|
@ -199,7 +214,7 @@ export function useSpotCommunityScore(
|
|||
|
||||
const level: SpotCommunityLevel = useMemo(() => {
|
||||
if (breakdown.skaters === 0) return 'unverified';
|
||||
const strong = breakdown.zaps > 0;
|
||||
const strong = breakdown.zaps > 0 || breakdown.crews > 0;
|
||||
if ((breakdown.skaters >= 2 && score >= COMMUNITY_THRESHOLD) || strong) return 'community';
|
||||
return 'seen';
|
||||
}, [breakdown, score]);
|
||||
|
|
|
|||
|
|
@ -1138,6 +1138,11 @@ spotUi: {
|
|||
breakdownCollections: '{{count}} save(s)',
|
||||
breakdownZaps: '{{count}} zap(s)',
|
||||
breakdownTags: '{{count}} tag suggestion(s)',
|
||||
breakdownCrews: '{{count}} crew(s)',
|
||||
breakdownLab: '{{count}} pro link(s)',
|
||||
verifyNudgeTitle: 'Be the first to confirm this spot',
|
||||
verifyNudgeBody: "A photo or an “I've skated here” is how a spot gets verified — help the next skater out.",
|
||||
verifyNudgeStatus: "I've skated here",
|
||||
noGearListedYet: 'No gear listed yet',
|
||||
noMessagesYet: 'No messages yet in this local channel. Say hi — "anyone skating tonight?"',
|
||||
noPhoto: 'No photo',
|
||||
|
|
|
|||
|
|
@ -1138,6 +1138,11 @@ spotUi: {
|
|||
breakdownCollections: '{{count}} guardado(s)',
|
||||
breakdownZaps: '{{count}} zap(s)',
|
||||
breakdownTags: '{{count}} etiqueta(s) sugerida(s)',
|
||||
breakdownCrews: '{{count}} cuadrilla(s)',
|
||||
breakdownLab: '{{count}} enlace(s) pro',
|
||||
verifyNudgeTitle: 'Sé el primero en confirmar este spot',
|
||||
verifyNudgeBody: 'Una foto o un «he patinado aquí» es como se verifica un spot: ayuda al siguiente skater.',
|
||||
verifyNudgeStatus: 'He patinado aquí',
|
||||
noGearListedYet: 'Aún no hay material publicado',
|
||||
noMessagesYet: 'Aún no hay mensajes en este canal local. Saluda: "¿alguien patina esta noche?"',
|
||||
noPhoto: 'Sin foto',
|
||||
|
|
|
|||
|
|
@ -197,6 +197,8 @@ export default function SpotDetailPage() {
|
|||
if (b.collections) parts.push(t('spotUi.breakdownCollections', { count: b.collections }));
|
||||
if (b.zaps) parts.push(t('spotUi.breakdownZaps', { count: b.zaps }));
|
||||
if (b.tagSuggestions) parts.push(t('spotUi.breakdownTags', { count: b.tagSuggestions }));
|
||||
if (b.crews) parts.push(t('spotUi.breakdownCrews', { count: b.crews }));
|
||||
if (b.labRefs) parts.push(t('spotUi.breakdownLab', { count: b.labRefs }));
|
||||
return parts.join(' · ');
|
||||
}, [community.breakdown, t]);
|
||||
|
||||
|
|
@ -595,6 +597,20 @@ export default function SpotDetailPage() {
|
|||
</p>
|
||||
)}
|
||||
|
||||
{/* Verify nudge — a logged-in non-owner on an unconfirmed spot. */}
|
||||
{!isShop && user && !isOwner && !communityVerified && !staffVerified && (
|
||||
<div className="mt-3 rounded-xl border border-emerald-500/25 bg-emerald-500/[0.06] p-3.5">
|
||||
<p className="text-sm font-bold text-emerald-300">{t('spotUi.verifyNudgeTitle')}</p>
|
||||
<p className="mt-0.5 text-xs text-gray-400">{t('spotUi.verifyNudgeBody')}</p>
|
||||
<div className="mt-2.5 flex flex-wrap items-center gap-2">
|
||||
<AddPhotoToSpot spotPubkey={spot.pubkey} spotIdentifier={spot.identifier} spotTitle={spot.title} />
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => setShowStatusDialog(true)}>
|
||||
{t('spotUi.verifyNudgeStatus')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Reported as not a skate spot — visible to everyone, not just admins. */}
|
||||
{notASpotReporters > 0 && (
|
||||
<div className="mt-3 flex items-start gap-2.5 rounded-xl border border-red-500/40 bg-red-500/10 p-3.5">
|
||||
|
|
|
|||
Loading…
Reference in a new issue