ac3cdf34b2
User-visible copy now matches the Organization rebrand. Internal
symbols, file names, query keys, routes, and storage keys are
intentionally left alone for this pass — they're still pinned to
"community" / "communities" until a dedicated rename commit.
Touched strings:
- `MobileBottomNav` and `sidebarItems` labels: "Communities" →
"Organize", matching the existing TopNav copy.
- `CommunityDetailPage` hero fallback ("Unnamed Community" →
"Unnamed Organization") and "About this organization" aria-label.
- `CommunityContent` thumbnail fallback name.
- `ExternalContentHeader.CommunityPreview` row label and fallback name.
- `NoteCard` kind-34550 noun "community" → "organization" (used in
feed-card action lines like "created an organization") and the
article switches from "a" to "an".
- `NoteMoreMenu` overflow-menu labels: "Report post to community" →
"Report post to organization", "Remove from community" → "Remove
from organization".
- `BanConfirmDialog` title, description, and success/failure toasts.
- `CommunityContentWarning` reporter pluralization and the
fallback report-type label ("community guidelines" →
"organization guidelines"); reporters are now scoped to founder /
moderators per the commit 4 cleanup, so the wording reflects that.
- `CommunityReportDialog` description copy.
- `CreateGoalDialog` placeholder example.
- `CreateActionDialog` org-scoped description string.
- `CreateCommunityEventDialog` NIP-31 `alt` tag prefix.
- `CommentContext` kind-34550 entries in the action-noun and
rendered-noun maps ("a community" / "community" → "an
organization" / "organization").
- `extraKinds` kind-34550 entry: label, description, and blurb.
- `kindLabels` kinds 4550, 10004, 34550.
- `DiscoverHero` ticker stat copy.
- `GetFeedTool` error message drops "communities" since the
Following feed no longer includes organization activity (removed
in the badge-runtime commit).
100 lines
3.4 KiB
TypeScript
100 lines
3.4 KiB
TypeScript
import { useMemo } from 'react';
|
|
import { Users, Globe } from 'lucide-react';
|
|
import type { NostrEvent } from '@nostrify/nostrify';
|
|
|
|
import { sanitizeUrl } from '@/lib/sanitizeUrl';
|
|
|
|
// --- Helpers ---
|
|
|
|
function getTag(tags: string[][], name: string): string | undefined {
|
|
return tags.find(([n]) => n === name)?.[1];
|
|
}
|
|
|
|
function parseCommunityEvent(event: NostrEvent) {
|
|
const name = getTag(event.tags, 'name') || getTag(event.tags, 'd') || 'Unnamed Organization';
|
|
const description = getTag(event.tags, 'description') || '';
|
|
const image = getTag(event.tags, 'image');
|
|
|
|
// Extract moderators from p tags with "moderator" role
|
|
const moderators = event.tags
|
|
.filter(([n, , , role]) => n === 'p' && role === 'moderator')
|
|
.map(([, pubkey]) => pubkey)
|
|
.filter(Boolean);
|
|
|
|
// Extract relays
|
|
const relays = event.tags
|
|
.filter(([n]) => n === 'relay')
|
|
.map(([, url, marker]) => ({ url, marker }))
|
|
.filter((r) => !!r.url);
|
|
|
|
return { name, description, image, moderators, relays };
|
|
}
|
|
|
|
// --- Main Component ---
|
|
|
|
export function CommunityContent({ event }: { event: NostrEvent }) {
|
|
const { name, description, image } = useMemo(
|
|
() => parseCommunityEvent(event),
|
|
[event],
|
|
);
|
|
|
|
// Extract website URL from description if present
|
|
const descriptionUrl = useMemo(() => {
|
|
const urlMatch = description.match(/https?:\/\/[^\s]+/);
|
|
return sanitizeUrl(urlMatch?.[0]);
|
|
}, [description]);
|
|
|
|
// Description text without trailing URL (if the URL is the last thing)
|
|
const descriptionText = useMemo(() => {
|
|
if (!descriptionUrl) return description;
|
|
return description.replace(new RegExp(`\\s*${descriptionUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*$`), '').trim();
|
|
}, [description, descriptionUrl]);
|
|
|
|
return (
|
|
<div className="mt-3 space-y-5">
|
|
{/* Community hero image */}
|
|
{image ? (
|
|
<div className="relative -mx-4 aspect-[21/9] overflow-hidden">
|
|
<img
|
|
src={image}
|
|
alt={name}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
{/* Gradient overlay for text readability */}
|
|
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent" />
|
|
{/* Community name overlaid on image */}
|
|
<div className="absolute bottom-0 left-0 right-0 px-4 pb-4">
|
|
<h1 className="text-2xl font-bold text-white leading-tight drop-shadow-lg">{name}</h1>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="relative -mx-4 aspect-[21/9] bg-gradient-to-br from-primary/15 via-primary/5 to-transparent flex items-center justify-center">
|
|
<Users className="size-16 text-primary/20" />
|
|
<div className="absolute bottom-0 left-0 right-0 px-4 pb-4">
|
|
<h1 className="text-2xl font-bold leading-tight">{name}</h1>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Description */}
|
|
{descriptionText && (
|
|
<p className="text-sm leading-relaxed text-muted-foreground whitespace-pre-wrap">{descriptionText}</p>
|
|
)}
|
|
|
|
{/* Website link */}
|
|
{descriptionUrl && (
|
|
<a
|
|
href={descriptionUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-flex items-center gap-1.5 text-sm text-primary hover:underline"
|
|
>
|
|
<Globe className="size-3.5" />
|
|
{descriptionUrl.replace(/^https?:\/\//, '').replace(/\/$/, '')}
|
|
</a>
|
|
)}
|
|
|
|
</div>
|
|
);
|
|
}
|