mirror of
https://github.com/HolgerHatGarKeineNode/einundzwanzig-nostr.git
synced 2026-07-07 14:52:01 +00:00
➕ Add Nostr Community Group (NIP-29) as a new benefit for members
- ✨ Introduced support for a Nostr Community Group with relay-based chat and events (NIP-29 standard). - 🌐 Added integration with `group.einundzwanzig.space`, accessible via Flotilla or Nostr clients. - 📋 Implemented "Copy Community Relay URL" functionality. - ✅ Updated tests to validate access for active, paid members.
This commit is contained in:
@@ -116,6 +116,13 @@ new class extends Component
|
||||
Flux::toast('Blossom-Adresse in die Zwischenablage kopiert!');
|
||||
}
|
||||
|
||||
public function copyGroupRelayUrl(): void
|
||||
{
|
||||
$groupRelayUrl = 'wss://group.einundzwanzig.space';
|
||||
$this->js("navigator.clipboard.writeText('{$groupRelayUrl}')");
|
||||
Flux::toast('Community-Relay-Adresse in die Zwischenablage kopiert!');
|
||||
}
|
||||
|
||||
public function handleNostrLoggedIn($signedEvent = null): void
|
||||
{
|
||||
NostrAuth::loginWithSignedEvent($signedEvent);
|
||||
@@ -154,13 +161,14 @@ new class extends Component
|
||||
@if($isActiveMember)
|
||||
<flux:callout variant="success" icon="check-circle" class="mb-6">
|
||||
<flux:callout.heading>Mitgliedschaft aktiv</flux:callout.heading>
|
||||
<flux:callout.text>Alle vier Dienste unten sind für dich freigeschaltet.</flux:callout.text>
|
||||
<flux:callout.text>Alle Dienste unten sind für dich freigeschaltet.</flux:callout.text>
|
||||
</flux:callout>
|
||||
@else
|
||||
<flux:callout variant="warning" icon="lock-closed" class="mb-6">
|
||||
<flux:callout.heading>Dienste gesperrt</flux:callout.heading>
|
||||
<flux:callout.text>
|
||||
Aktiviere deine Mitgliedschaft, um Relay, NIP-05, Watchtower und den Blossom-Medienserver zu nutzen.
|
||||
Aktiviere deine Mitgliedschaft, um Relay, NIP-05, Watchtower, den Blossom-Medienserver und die
|
||||
Nostr-Community-Gruppe zu nutzen.
|
||||
</flux:callout.text>
|
||||
<x-slot name="actions">
|
||||
<flux:button :href="route('association.profile')" size="sm" variant="primary" wire:navigate>
|
||||
@@ -170,12 +178,12 @@ new class extends Component
|
||||
</flux:callout>
|
||||
@endif
|
||||
|
||||
<!-- Benefits Grid - 2 Spalten auf Desktop für ruhigere, scanbare Übersicht -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 items-start">
|
||||
<!-- Benefits Grid - 2 Spalten auf Desktop, gleiche Höhe je Zeile (flex + h-full) für ruhige Optik -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
|
||||
<!-- Benefit 1: Nostr Relay -->
|
||||
<flux:card
|
||||
class="{{ $isActiveMember ? '' : 'opacity-60' }} border-amber-200 dark:border-amber-200/30">
|
||||
class="flex flex-col h-full {{ $isActiveMember ? '' : 'opacity-60' }} border-amber-200 dark:border-amber-200/30">
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="shrink-0 w-10 h-10 rounded-full bg-amber-100 dark:bg-amber-900/60 flex items-center justify-center">
|
||||
@@ -208,7 +216,7 @@ new class extends Component
|
||||
aria-label="Relay-Adresse kopieren"/>
|
||||
</div>
|
||||
|
||||
<flux:accordion class="mt-3">
|
||||
<flux:accordion class="mt-auto pt-3">
|
||||
<flux:accordion.item heading="Anleitung anzeigen">
|
||||
<div class="space-y-2 text-xs text-zinc-600 dark:text-zinc-400 leading-relaxed">
|
||||
<p>
|
||||
@@ -231,7 +239,7 @@ new class extends Component
|
||||
|
||||
<!-- Benefit 2: NIP-05 -->
|
||||
<flux:card
|
||||
class="{{ $isActiveMember ? '' : 'opacity-60' }} border-emerald-200 dark:border-emerald-200/30">
|
||||
class="flex flex-col h-full {{ $isActiveMember ? '' : 'opacity-60' }} border-emerald-200 dark:border-emerald-200/30">
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="shrink-0 w-10 h-10 rounded-full bg-emerald-100 dark:bg-emerald-900/60 flex items-center justify-center">
|
||||
@@ -253,54 +261,69 @@ new class extends Component
|
||||
</div>
|
||||
|
||||
@if($isActiveMember)
|
||||
<div class="mt-4 space-y-3">
|
||||
<flux:field>
|
||||
<flux:label>Dein NIP-05 Handle</flux:label>
|
||||
<flux:input.group>
|
||||
<flux:input wire:model.live.debounce="nip05Handle" placeholder="dein-name"/>
|
||||
<flux:input.group.suffix>@einundzwanzig.space</flux:input.group.suffix>
|
||||
</flux:input.group>
|
||||
<flux:error name="nip05Handle"/>
|
||||
</flux:field>
|
||||
|
||||
<flux:button wire:click="saveNip05Handle" wire:loading.attr="disabled" size="sm" variant="primary">
|
||||
Speichern
|
||||
</flux:button>
|
||||
|
||||
@php($nip05DisplayHandle = $nip05VerifiedHandle ?: $nip05Handle)
|
||||
<div class="mt-4 flex items-center gap-2">
|
||||
<code
|
||||
class="flex-1 text-xs bg-zinc-100 dark:bg-zinc-800 px-3 py-2 rounded text-zinc-700 dark:text-zinc-300 font-mono break-all">
|
||||
@if($nip05DisplayHandle){{ $nip05DisplayHandle }}@einundzwanzig.space
|
||||
@else Noch kein Handle – unten einrichten
|
||||
@endif
|
||||
</code>
|
||||
@if($nip05Verified)
|
||||
<flux:callout variant="success" icon="check-circle">
|
||||
<flux:callout.text>
|
||||
Du hast {{ count($nip05VerifiedHandles) }} aktive Handles für deinen Pubkey.
|
||||
@if($nip05HandleMismatch)
|
||||
Die Synchronisation zu
|
||||
<strong class="break-all">{{ $nip05Handle }}@einundzwanzig.space</strong>
|
||||
läuft automatisch im Hintergrund.
|
||||
@endif
|
||||
</flux:callout.text>
|
||||
</flux:callout>
|
||||
|
||||
<div class="p-3 bg-white/50 dark:bg-zinc-800/50 rounded border border-zinc-200 dark:border-zinc-600">
|
||||
<p class="text-xs font-medium text-zinc-700 dark:text-zinc-300 mb-2">Deine aktivierten Handles:</p>
|
||||
<ul class="space-y-2">
|
||||
@foreach($nip05VerifiedHandles as $handle)
|
||||
<li class="flex items-center gap-2 text-sm" wire:key="handle-{{ $handle }}">
|
||||
<span class="break-all text-zinc-800 dark:text-zinc-200 font-mono">{{ $handle }}@einundzwanzig.space</span>
|
||||
<flux:badge color="green" size="sm">OK</flux:badge>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
<flux:badge color="green" size="sm">OK</flux:badge>
|
||||
@elseif($nip05Handle)
|
||||
<flux:callout variant="secondary" icon="information-circle">
|
||||
<flux:callout.text>
|
||||
Dein Handle <strong class="break-all">{{ $nip05Handle }}@einundzwanzig.space</strong>
|
||||
ist gespeichert, aber noch nicht aktiv. Der Vorstand schaltet es bald frei.
|
||||
</flux:callout.text>
|
||||
</flux:callout>
|
||||
<flux:badge color="amber" size="sm">Wartet</flux:badge>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<flux:accordion class="mt-auto pt-3">
|
||||
<flux:accordion.item heading="NIP-05 einrichten & verwalten">
|
||||
<div class="space-y-3">
|
||||
<flux:field>
|
||||
<flux:label>Dein NIP-05 Handle</flux:label>
|
||||
<flux:input.group>
|
||||
<flux:input wire:model.live.debounce="nip05Handle" placeholder="dein-name"/>
|
||||
<flux:input.group.suffix>@einundzwanzig.space</flux:input.group.suffix>
|
||||
</flux:input.group>
|
||||
<flux:error name="nip05Handle"/>
|
||||
</flux:field>
|
||||
|
||||
<flux:button wire:click="saveNip05Handle" wire:loading.attr="disabled" size="sm" variant="primary">
|
||||
Speichern
|
||||
</flux:button>
|
||||
|
||||
@if($nip05Verified)
|
||||
<flux:callout variant="success" icon="check-circle">
|
||||
<flux:callout.text>
|
||||
Du hast {{ count($nip05VerifiedHandles) }} aktive Handles für deinen Pubkey.
|
||||
@if($nip05HandleMismatch)
|
||||
Die Synchronisation zu
|
||||
<strong class="break-all">{{ $nip05Handle }}@einundzwanzig.space</strong>
|
||||
läuft automatisch im Hintergrund.
|
||||
@endif
|
||||
</flux:callout.text>
|
||||
</flux:callout>
|
||||
|
||||
<div class="p-3 bg-white/50 dark:bg-zinc-800/50 rounded border border-zinc-200 dark:border-zinc-600">
|
||||
<p class="text-xs font-medium text-zinc-700 dark:text-zinc-300 mb-2">Deine aktivierten Handles:</p>
|
||||
<ul class="space-y-2">
|
||||
@foreach($nip05VerifiedHandles as $handle)
|
||||
<li class="flex items-center gap-2 text-sm" wire:key="handle-{{ $handle }}">
|
||||
<span class="break-all text-zinc-800 dark:text-zinc-200 font-mono">{{ $handle }}@einundzwanzig.space</span>
|
||||
<flux:badge color="green" size="sm">OK</flux:badge>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@elseif($nip05Handle)
|
||||
<flux:callout variant="secondary" icon="information-circle">
|
||||
<flux:callout.text>
|
||||
Dein Handle <strong class="break-all">{{ $nip05Handle }}@einundzwanzig.space</strong>
|
||||
ist gespeichert, aber noch nicht aktiv. Der Vorstand schaltet es bald frei.
|
||||
</flux:callout.text>
|
||||
</flux:callout>
|
||||
@endif
|
||||
|
||||
<flux:accordion>
|
||||
<flux:accordion.item heading="Was ist NIP-05 & welche Regeln gelten?">
|
||||
<div class="space-y-2 text-xs text-zinc-600 dark:text-zinc-400 leading-relaxed">
|
||||
<p>
|
||||
<flux:link href="https://nostr.how/en/guides/get-verified#self-hosted" target="_blank">NIP-05</flux:link>
|
||||
@@ -312,15 +335,15 @@ new class extends Component
|
||||
sowie „-" und „_". Großbuchstaben werden automatisch kleingeschrieben.
|
||||
</p>
|
||||
</div>
|
||||
</flux:accordion.item>
|
||||
</flux:accordion>
|
||||
</div>
|
||||
</div>
|
||||
</flux:accordion.item>
|
||||
</flux:accordion>
|
||||
@endif
|
||||
</flux:card>
|
||||
|
||||
<!-- Benefit 3: Lightning Watchtower -->
|
||||
<flux:card
|
||||
class="{{ $isActiveMember ? '' : 'opacity-60' }} border-purple-200 dark:border-purple-200/30">
|
||||
class="flex flex-col h-full {{ $isActiveMember ? '' : 'opacity-60' }} border-purple-200 dark:border-purple-200/30">
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="shrink-0 w-10 h-10 rounded-full bg-purple-100 dark:bg-purple-900/60 flex items-center justify-center">
|
||||
@@ -353,7 +376,7 @@ new class extends Component
|
||||
aria-label="Watchtower-Adresse kopieren"/>
|
||||
</div>
|
||||
|
||||
<flux:accordion class="mt-3">
|
||||
<flux:accordion class="mt-auto pt-3">
|
||||
<flux:accordion.item heading="Anleitung anzeigen">
|
||||
<div class="space-y-2 text-xs text-zinc-600 dark:text-zinc-400 leading-relaxed">
|
||||
<p>
|
||||
@@ -378,7 +401,7 @@ new class extends Component
|
||||
|
||||
<!-- Benefit 4: Blossom Medienserver (NEU) -->
|
||||
<flux:card
|
||||
class="{{ $isActiveMember ? '' : 'opacity-60' }} border-rose-200 dark:border-rose-200/30">
|
||||
class="flex flex-col h-full {{ $isActiveMember ? '' : 'opacity-60' }} border-rose-200 dark:border-rose-200/30">
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="shrink-0 w-10 h-10 rounded-full bg-rose-100 dark:bg-rose-900/60 flex items-center justify-center">
|
||||
@@ -418,7 +441,7 @@ new class extends Component
|
||||
aria-label="Blossom-Adresse kopieren"/>
|
||||
</div>
|
||||
|
||||
<flux:accordion class="mt-3">
|
||||
<flux:accordion class="mt-auto pt-3">
|
||||
<flux:accordion.item heading="Was ist Blossom & wie nutze ich ihn?">
|
||||
<div class="space-y-3 text-xs text-zinc-600 dark:text-zinc-400 leading-relaxed">
|
||||
<p>
|
||||
@@ -455,5 +478,88 @@ new class extends Component
|
||||
@endif
|
||||
</flux:card>
|
||||
|
||||
<!-- Benefit 5: Nostr Community / NIP-29 Gruppe (NEU) -->
|
||||
<flux:card
|
||||
class="flex flex-col h-full {{ $isActiveMember ? '' : 'opacity-60' }} border-sky-200 dark:border-sky-200/30">
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="shrink-0 w-10 h-10 rounded-full bg-sky-100 dark:bg-sky-900/60 flex items-center justify-center">
|
||||
<i class="fa-sharp-duotone fa-solid fa-comments text-sky-600 dark:text-sky-400 text-lg"></i>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<h3 class="text-lg font-semibold text-zinc-800 dark:text-zinc-100">Nostr Community-Gruppe</h3>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<flux:badge color="sky" size="sm">NEU</flux:badge>
|
||||
@if($isActiveMember)
|
||||
<flux:badge color="green" size="sm">Aktiv</flux:badge>
|
||||
@else
|
||||
<flux:badge color="zinc" size="sm" icon="lock-closed">Mitglieder</flux:badge>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
Chatte und poste in der Einundzwanzig-Gruppe auf unserem Community-Relay
|
||||
(<flux:link href="https://github.com/nostr-protocol/nips/blob/master/29.md" target="_blank">NIP-29</flux:link>)
|
||||
– wie Discord, nur auf Nostr.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($isActiveMember)
|
||||
<div class="mt-4 flex flex-col gap-2">
|
||||
<flux:button
|
||||
href="https://app.flotilla.social/spaces/group.einundzwanzig.space"
|
||||
target="_blank"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
icon:trailing="arrow-up-right"
|
||||
class="w-full">
|
||||
Gruppe in Flotilla öffnen
|
||||
</flux:button>
|
||||
<div class="flex items-center gap-2">
|
||||
<code
|
||||
class="flex-1 text-xs bg-zinc-100 dark:bg-zinc-800 px-3 py-2 rounded text-zinc-700 dark:text-zinc-300 font-mono cursor-pointer hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-colors break-all"
|
||||
wire:click="copyGroupRelayUrl"
|
||||
title="Klicken zum Kopieren">
|
||||
wss://group.einundzwanzig.space
|
||||
</code>
|
||||
<flux:button wire:click="copyGroupRelayUrl" size="sm" variant="ghost" icon="clipboard"
|
||||
aria-label="Community-Relay-Adresse kopieren"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<flux:accordion class="mt-auto pt-3">
|
||||
<flux:accordion.item heading="Was ist das & wie trete ich bei?">
|
||||
<div class="space-y-3 text-xs text-zinc-600 dark:text-zinc-400 leading-relaxed">
|
||||
<p>
|
||||
Unser <strong>group.einundzwanzig.space</strong> ist ein Community-Relay nach dem
|
||||
<flux:link href="https://github.com/nostr-protocol/nips/blob/master/29.md" target="_blank">NIP-29</flux:link>-Standard
|
||||
(relay-basierte Gruppen): ein Relay bildet eine Community mit Chat, Threads und Events –
|
||||
nur für Vereinsmitglieder.
|
||||
</p>
|
||||
<div>
|
||||
<p class="font-medium text-zinc-700 dark:text-zinc-300 mb-1">So trittst du bei:</p>
|
||||
<ol class="space-y-1 list-decimal list-inside">
|
||||
<li>Öffne die Gruppe mit einem NIP-29-fähigen Nostr-Client. Am einfachsten über den
|
||||
Button oben (nutzt <flux:link href="https://flotilla.social" target="_blank">Flotilla</flux:link>);
|
||||
es funktionieren aber auch andere Clients wie
|
||||
<flux:link href="https://www.chachi.chat" target="_blank">Chachi</flux:link>.</li>
|
||||
<li>Alternativ das Relay <strong>wss://group.einundzwanzig.space</strong> in deinem
|
||||
Client als Gruppe hinzufügen.</li>
|
||||
<li>Melde dich mit deinem Nostr-Schlüssel an (z.B. via Amber oder nsec-Bunker) –
|
||||
fertig, du kannst sofort lesen und schreiben.</li>
|
||||
</ol>
|
||||
</div>
|
||||
<p>
|
||||
<strong>Automatischer Zugang:</strong> Sobald dein Mitgliedsbeitrag bezahlt ist, wirst du
|
||||
automatisch als Member des Relays eingetragen und erhältst Schreib-Rechte in der Gruppe.
|
||||
</p>
|
||||
</div>
|
||||
</flux:accordion.item>
|
||||
</flux:accordion>
|
||||
@endif
|
||||
</flux:card>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -685,6 +685,19 @@ new class extends Component {
|
||||
<p class="text-xs text-text-tertiary mt-0.5">5 GB Speicher · max. 1 GB pro Datei</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex items-start gap-3">
|
||||
<div
|
||||
class="shrink-0 w-9 h-9 rounded-full bg-sky-100 dark:bg-sky-900/60 flex items-center justify-center">
|
||||
<i class="fa-sharp-duotone fa-solid fa-comments text-sky-600 dark:text-sky-400"></i>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="text-sm font-semibold text-text-primary">Nostr Community-Gruppe</h3>
|
||||
<flux:badge color="sky" size="sm">NEU</flux:badge>
|
||||
</div>
|
||||
<p class="text-xs text-text-secondary">Schreibe in der Einundzwanzig-Gruppe auf unserem Community-Relay (NIP-29).</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<flux:button :href="route('association.benefits')" wire:navigate variant="primary" size="sm"
|
||||
|
||||
@@ -33,3 +33,22 @@ it('copies the blossom url for active members', function () {
|
||||
->call('copyBlossomUrl')
|
||||
->assertHasNoErrors();
|
||||
});
|
||||
|
||||
it('unlocks the nostr community group for active paid members', function () {
|
||||
$pleb = EinundzwanzigPleb::factory()->active()->withPaidCurrentYear()->create();
|
||||
NostrAuth::login($pleb->pubkey);
|
||||
|
||||
Livewire::test('association.benefits')
|
||||
->assertSee('Nostr Community-Gruppe')
|
||||
->assertSee('app.flotilla.social/spaces/group.einundzwanzig.space')
|
||||
->assertSee('wss://group.einundzwanzig.space');
|
||||
});
|
||||
|
||||
it('copies the community relay url for active members', function () {
|
||||
$pleb = EinundzwanzigPleb::factory()->active()->withPaidCurrentYear()->create();
|
||||
NostrAuth::login($pleb->pubkey);
|
||||
|
||||
Livewire::test('association.benefits')
|
||||
->call('copyGroupRelayUrl')
|
||||
->assertHasNoErrors();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user