mirror of
https://github.com/aljazceru/hypergolic.git
synced 2025-12-19 06:24:20 +01:00
problem: can't use product groups
This commit is contained in:
@@ -11,16 +11,17 @@
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
import { Terminal } from 'lucide-svelte';
|
||||
import Todo from './Todo.svelte';
|
||||
import { Rocket } from '@/event_helpers/rockets';
|
||||
import { Product, Rocket } from '@/event_helpers/rockets';
|
||||
|
||||
export let product: NDKEvent;
|
||||
export let rocket: NDKEvent;
|
||||
export let product: Product;
|
||||
export let rocket: Rocket;
|
||||
|
||||
let parsedRocket: Rocket = new Rocket(rocket);
|
||||
|
||||
let price: number = 0;
|
||||
let max: number = 0;
|
||||
|
||||
let o = false;
|
||||
|
||||
function publish() {
|
||||
if (!$ndk.signer) {
|
||||
throw new Error('no ndk signer found');
|
||||
@@ -29,20 +30,19 @@
|
||||
if (!author) {
|
||||
throw new Error('no current user');
|
||||
}
|
||||
if (rocket.author.pubkey != author.pubkey) {
|
||||
console.log(rocket.author, author);
|
||||
throw new Error('you are not the creator of this rocket');
|
||||
if (rocket.Event.author.pubkey != author.pubkey) {
|
||||
throw new Error(`${author.pubkey} is not the creator of this rocket`);
|
||||
}
|
||||
let event = parsedRocket.UpsertProduct(product.id, price, max);
|
||||
let event = rocket.UpsertProduct(product.ID(), price, max);
|
||||
event.ndk = $ndk
|
||||
event.publish().then((x) => {
|
||||
console.log(x);
|
||||
goto(`${base}/products`);
|
||||
o = false
|
||||
}).catch(()=>{ console.log("failed to publish", event.rawEvent())});
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root>
|
||||
<Dialog.Root bind:open={o}>
|
||||
<Dialog.Trigger class={buttonVariants({ variant: 'default' })}
|
||||
>Make Available for Purchase</Dialog.Trigger
|
||||
>
|
||||
|
||||
@@ -11,21 +11,21 @@
|
||||
import { requestProvider } from 'webln';
|
||||
import QrCodeSvg from './QrCodeSvg.svelte';
|
||||
import CopyButton from './CopyButton.svelte';
|
||||
import type { RocketProduct } from '@/event_helpers/rockets';
|
||||
import type { Product, Rocket, RocketProduct } from '@/event_helpers/rockets';
|
||||
|
||||
export let product: NDKEvent;
|
||||
export let product: Product;
|
||||
export let rocketProduct: RocketProduct | undefined;
|
||||
export let rocket: NDKEvent;
|
||||
export let rocket: Rocket;
|
||||
|
||||
let invoice: string | null;
|
||||
|
||||
async function zap() {
|
||||
if (rocketProduct) {
|
||||
const z = new NDKZap({ ndk: $ndk, zappedEvent: rocket, zappedUser: rocket.author });
|
||||
const z = new NDKZap({ ndk: $ndk, zappedEvent: rocket.Event, zappedUser: rocket.Event.author });
|
||||
invoice = await z.createZapRequest(
|
||||
rocketProduct.Price * 1000,
|
||||
`Purchase of ${product.getMatchingTags('name')[0][1]} from ${rocket.dTag}`,
|
||||
[['product', product.id]]
|
||||
`Purchase of ${product.Name()} from ${rocket.Event.dTag}`,
|
||||
[['product', product.ID()]]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@
|
||||
<Dialog.Content class="sm:max-w-[425px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title
|
||||
>Buy {product.getMatchingTags('name')[0][1]} from {rocket.dTag} now!</Dialog.Title
|
||||
>Buy {product.Name()} from {rocket.Name()} now!</Dialog.Title
|
||||
>
|
||||
{#if !currentUser}
|
||||
<Alert.Root>
|
||||
@@ -67,7 +67,7 @@
|
||||
>
|
||||
</Alert.Root>
|
||||
{/if}
|
||||
<Dialog.Description>Pay now with Lightning</Dialog.Description>
|
||||
<Dialog.Description>Pay {rocketProduct.Price} sats now with Lightning</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
{#if invoice}
|
||||
<QrCodeSvg content={invoice} />
|
||||
|
||||
@@ -1,57 +1,25 @@
|
||||
<script lang="ts">
|
||||
import * as Card from '$lib/components/ui/card/index.js';
|
||||
import type { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
import { Product, Rocket } from '@/event_helpers/rockets';
|
||||
import AddProductToRocket from './AddProductToRocket.svelte';
|
||||
import PayNow from './PayNow.svelte';
|
||||
import { Rocket } from '@/event_helpers/rockets';
|
||||
|
||||
export let product: NDKEvent;
|
||||
export let rocket: NDKEvent;
|
||||
//$page.url.searchParams.get("tab")
|
||||
|
||||
function validate(event: NDKEvent): boolean {
|
||||
let test = 0;
|
||||
if (
|
||||
event.getMatchingTags('name') &&
|
||||
event.getMatchingTags('name')[0] &&
|
||||
event.getMatchingTags('name')[0][1]
|
||||
) {
|
||||
test++;
|
||||
}
|
||||
if (
|
||||
event.getMatchingTags('description') &&
|
||||
event.getMatchingTags('description')[0] &&
|
||||
event.getMatchingTags('description')[0][1]
|
||||
) {
|
||||
test++;
|
||||
}
|
||||
if (
|
||||
event.getMatchingTags('cover') &&
|
||||
event.getMatchingTags('cover')[0] &&
|
||||
event.getMatchingTags('cover')[0][1]
|
||||
) {
|
||||
test++;
|
||||
}
|
||||
return test == 3;
|
||||
}
|
||||
|
||||
function includedInRocket(rocket:Rocket, product:NDKEvent): boolean {
|
||||
return Boolean(rocket.Products().get(product.id))
|
||||
}
|
||||
export let product: Product;
|
||||
export let rocket: Rocket;
|
||||
</script>
|
||||
|
||||
{#if validate(product)}
|
||||
{#if product.Validate()}
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{product.getMatchingTags('name')[0][1]}</Card.Title>
|
||||
<Card.Description>{product.getMatchingTags('description')[0][1]}</Card.Description>
|
||||
<Card.Title>{product.Group()} {#if product.Option().length > 0}(variant: {product.Option()}){/if}</Card.Title>
|
||||
<Card.Description>{product.Description()}</Card.Description>
|
||||
</Card.Header>
|
||||
|
||||
{#if $$slots.default}
|
||||
<Card.Content>
|
||||
<div class="flex flex-col items-center justify-center gap-2 md:flex-row">
|
||||
<img
|
||||
src={product.getMatchingTags('cover')[0][1]}
|
||||
src={product.CoverImage()}
|
||||
alt="cover"
|
||||
class="aspect-square w-[300px] object-cover"
|
||||
/>
|
||||
@@ -59,17 +27,13 @@
|
||||
</div>
|
||||
</Card.Content>
|
||||
{:else}
|
||||
<img
|
||||
src={product.getMatchingTags('cover')[0][1]}
|
||||
alt="cover"
|
||||
class="aspect-square object-cover"
|
||||
/>
|
||||
<img src={product.CoverImage()} alt="cover" class="aspect-square object-cover" />
|
||||
{/if}
|
||||
<Card.Footer class="flex justify-center pt-2">
|
||||
{#if !includedInRocket(new Rocket(rocket), product)}
|
||||
{#if !rocket.Products().get(product.ID())}
|
||||
<AddProductToRocket {product} {rocket} />
|
||||
{:else}
|
||||
<PayNow {product} rocketProduct={new Rocket(rocket).Products().get(product.id)} {rocket} />
|
||||
<PayNow {product} rocketProduct={rocket.Products().get(product.ID())} {rocket} />
|
||||
{/if}
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
|
||||
@@ -3,30 +3,22 @@
|
||||
import { ndk } from '@/ndk';
|
||||
import type { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
import ProductCard from './ProductCard.svelte';
|
||||
export let productID: string;
|
||||
export let rocket: NDKEvent;
|
||||
let productEvent: NDKEvent | undefined;
|
||||
import { fetchEvent } from '@/event_helpers/products';
|
||||
import { Product, type Rocket } from '@/event_helpers/rockets';
|
||||
export let productID: string | undefined = undefined;
|
||||
export let rocket: Rocket;
|
||||
export let product:Product | undefined = undefined;
|
||||
|
||||
onMount(() => {
|
||||
$ndk.fetchEvent(productID).then((e) => {
|
||||
if (e) {
|
||||
productEvent = e;
|
||||
} else {
|
||||
let _p = $ndk.storeSubscribe([{ids:[productID] }], { subId: productID });
|
||||
_p.subscribe(x=>{
|
||||
if (x[0]) {
|
||||
productEvent = x[0]
|
||||
_p.unsubscribe()
|
||||
if (!product && productID) {
|
||||
fetchEvent(productID, $ndk).then(e => product = new Product(e))
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
{#if productEvent}
|
||||
<ProductCard {rocket} product={productEvent}>
|
||||
{#if product}
|
||||
<ProductCard {rocket} {product}>
|
||||
<slot />
|
||||
</ProductCard>
|
||||
{/if}
|
||||
|
||||
@@ -1,13 +1,48 @@
|
||||
<script lang="ts">
|
||||
import * as Card from '$lib/components/ui/card/index.js';
|
||||
import { Rocket } from '@/event_helpers/rockets';
|
||||
import type { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
import { Product, Rocket } from '@/event_helpers/rockets';
|
||||
import ProductCardFromId from './ProductCardFromID.svelte';
|
||||
import ProductPurchases from './ProductPurchases.svelte';
|
||||
import type { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
import { fetchEvent } from '@/event_helpers/products';
|
||||
import { ndk } from '@/ndk';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import * as Pagination from '@/components/ui/pagination';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-svelte';
|
||||
|
||||
export let rocket: NDKEvent;
|
||||
export let rocket: Rocket;
|
||||
export let unratifiedZaps = 0;
|
||||
|
||||
let products = writable(new Map<string, Product>());
|
||||
|
||||
for (let [id, p] of rocket.Products()) {
|
||||
fetchEvent(id, $ndk).then((e) => {
|
||||
let _p = new Product(e);
|
||||
if (_p.Validate()) {
|
||||
products.update(existing => {
|
||||
existing.set(id, _p)
|
||||
return existing
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let groups = derived(products, ($products) => {
|
||||
let productGroups = new Map<string, Map<string, Product>>();
|
||||
for (let [id, p] of $products) {
|
||||
console.log(p.Group())
|
||||
if (!productGroups.get(p.Group())) {
|
||||
productGroups.set(p.Group(), new Map());
|
||||
}
|
||||
let existing = productGroups.get(p.Group())!;
|
||||
existing.set(id, p);
|
||||
}
|
||||
let productGroupArray = new Map<string, Product[]>()
|
||||
for (let [id, m] of productGroups) {
|
||||
productGroupArray.set(id, Array.from(m, ([_, p]) => {return p}))
|
||||
}
|
||||
return productGroupArray
|
||||
});
|
||||
</script>
|
||||
|
||||
<Card.Root class="sm:col-span-3">
|
||||
@@ -16,13 +51,54 @@
|
||||
<Card.Description></Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content class="grid grid-cols-1 gap-2">
|
||||
{#each new Rocket(rocket).Products() as [id, product] (id)}
|
||||
{#each $groups as [identifier, products] (identifier)}
|
||||
<Pagination.Root count={products.length} perPage={1} siblingCount={1} let:pages let:currentPage>
|
||||
{#if currentPage} <ProductCardFromId {rocket} product={products[currentPage-1]}>
|
||||
|
||||
<ProductPurchases bind:unratifiedZaps {rocket} {products} />
|
||||
</ProductCardFromId>{/if}
|
||||
{#if products.length > 1}
|
||||
<Pagination.Content>
|
||||
<Pagination.Item>
|
||||
<Pagination.PrevButton>
|
||||
<ChevronLeft class="h-4 w-4" />
|
||||
<span class="hidden sm:block">Previous Option</span>
|
||||
</Pagination.PrevButton>
|
||||
</Pagination.Item>
|
||||
{#each pages as page (page.key)}
|
||||
{#if page.type === "ellipsis"}
|
||||
<Pagination.Item>
|
||||
<Pagination.Ellipsis />
|
||||
</Pagination.Item>
|
||||
{:else}
|
||||
<Pagination.Item>
|
||||
<Pagination.Link {page} isActive={currentPage === page.value}>
|
||||
{page.value}
|
||||
</Pagination.Link>
|
||||
</Pagination.Item>
|
||||
{/if}
|
||||
{/each}
|
||||
<Pagination.Item>
|
||||
<Pagination.NextButton>
|
||||
<span class="hidden sm:block">Next Option</span>
|
||||
<ChevronRight class="h-4 w-4" />
|
||||
</Pagination.NextButton>
|
||||
</Pagination.Item>
|
||||
</Pagination.Content>
|
||||
{/if}
|
||||
</Pagination.Root>
|
||||
|
||||
<!-- {#each map as [id, product]} {#if true}
|
||||
|
||||
{/if}{/each} -->
|
||||
{/each}
|
||||
<!-- {#each products as [id, product] (id)}
|
||||
<div>
|
||||
<ProductCardFromId {rocket} productID={product.ID}>
|
||||
<ProductPurchases bind:unratifiedZaps={unratifiedZaps} {rocket} {product} />
|
||||
<ProductCardFromId {rocket} {product}>
|
||||
<ProductPurchases bind:unratifiedZaps {rocket} product={rocket.Products().get(id)} />
|
||||
</ProductCardFromId>
|
||||
</div>
|
||||
{/each}
|
||||
{/each} -->
|
||||
</Card.Content>
|
||||
<Card.Footer></Card.Footer>
|
||||
</Card.Root>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import * as Table from '@/components/ui/table';
|
||||
import { ValidateZapPublisher, ZapPurchase, type RocketProduct } from '@/event_helpers/rockets';
|
||||
import { Product, Rocket, ValidateZapPublisher, ZapPurchase, type RocketProduct } from '@/event_helpers/rockets';
|
||||
import { unixToRelativeTime } from '@/helpers';
|
||||
import { ndk } from '@/ndk';
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
@@ -8,15 +8,16 @@
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
|
||||
export let product: RocketProduct;
|
||||
export let rocket: NDKEvent;
|
||||
export let products: Product[];
|
||||
//export let products: Product[];
|
||||
export let rocket: Rocket;
|
||||
|
||||
export let unratifiedZaps:number = 0; //todo upstream bind this and parse outstanding zaps to merits and satflow component.
|
||||
|
||||
let zaps = $ndk.storeSubscribe(
|
||||
[{ '#a': [`31108:${rocket.author.pubkey}:${rocket.dTag}`], kinds: [9735] }],
|
||||
[{ '#a': [`31108:${rocket.Event.author.pubkey}:${rocket.Event.dTag}`], kinds: [9735] }],
|
||||
{
|
||||
subId: product.ID
|
||||
subId: rocket.Name() + "_zaps"
|
||||
}
|
||||
);
|
||||
|
||||
@@ -24,21 +25,29 @@
|
||||
zaps?.unsubscribe();
|
||||
});
|
||||
|
||||
let productEvent: NDKEvent | undefined;
|
||||
// let productEvent: NDKEvent | undefined;
|
||||
|
||||
onMount(() => {
|
||||
$ndk.fetchEvent(product.ID).then((e) => {
|
||||
if (e) {
|
||||
productEvent = e;
|
||||
// onMount(() => {
|
||||
// $ndk.fetchEvent(product.ID).then((e) => {
|
||||
// if (e) {
|
||||
// productEvent = e;
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
|
||||
function productsInclude(id:string) {
|
||||
let included = false
|
||||
for (let p of products) {
|
||||
if (p.ID() == id) {included = true}
|
||||
}
|
||||
return included
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let validZaps = derived(zaps, ($zaps) => {
|
||||
let zapMap = new Map<string, ZapPurchase>();
|
||||
for (let z of $zaps) {
|
||||
let zapPurchase = new ZapPurchase(z);
|
||||
if (zapPurchase.Valid(rocket) && zapPurchase.ProductID == product.ID) {
|
||||
if (zapPurchase.Valid(rocket.Event) && productsInclude(zapPurchase.ProductID)) {
|
||||
zapMap.set(zapPurchase.ZapReceipt.id, zapPurchase);
|
||||
}
|
||||
}
|
||||
@@ -48,7 +57,7 @@
|
||||
let zapsNotInRocket = derived(validZaps, ($validZaps) => {
|
||||
let zapMap = new Map<string, ZapPurchase>();
|
||||
for (let [id, z] of $validZaps) {
|
||||
if (!z.IncludedInRocketState(rocket)) {
|
||||
if (!z.IncludedInRocketState(rocket.Event)) {
|
||||
zapMap.set(id, z);
|
||||
}
|
||||
}
|
||||
@@ -59,7 +68,7 @@
|
||||
|
||||
zapsNotInRocket.subscribe((z) => {
|
||||
z.forEach((z) => {
|
||||
ValidateZapPublisher(rocket, z.ZapReceipt).then((result) => {
|
||||
ValidateZapPublisher(rocket.Event, z.ZapReceipt).then((result) => {
|
||||
if (result) {
|
||||
validPubkeys.update(existing=>{
|
||||
existing.add(z.ZapReceipt.pubkey);
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
|
||||
import { getMapOfProductsFromRocket } from '@/event_helpers/rockets';
|
||||
import ProductCardFromID from './ProductCardFromID.svelte';
|
||||
import ProductPurchases from './ProductPurchases.svelte';
|
||||
|
||||
export let rocketEvent: NDKEvent;
|
||||
|
||||
$: rocketProducts = getMapOfProductsFromRocket(rocketEvent);
|
||||
</script>
|
||||
|
||||
{#if rocketEvent && rocketProducts.size > 0}
|
||||
{#each rocketProducts as [id, product] (id)}
|
||||
<ProductCardFromID rocket={rocketEvent} productID={product.ID} />
|
||||
<ProductPurchases rocket={rocketEvent} {product} />{/each}
|
||||
{/if}
|
||||
@@ -39,7 +39,7 @@
|
||||
>
|
||||
<MeritsAndSatflow {unratifiedZaps} {rocket} />
|
||||
|
||||
<ProductFomo bind:unratifiedZaps {rocket} />
|
||||
<ProductFomo bind:unratifiedZaps rocket={new Rocket(rocket)} />
|
||||
|
||||
<ProposedProducts {rocket} />
|
||||
|
||||
|
||||
23
src/lib/event_helpers/products.ts
Normal file
23
src/lib/event_helpers/products.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
import type NDKSvelte from "@nostr-dev-kit/ndk-svelte";
|
||||
|
||||
export async function fetchEvent(id:string, ndk:NDKSvelte):Promise<NDKEvent> {
|
||||
return new Promise((resolve)=>{
|
||||
ndk.fetchEvent(id).then((e) => {
|
||||
if (e) {
|
||||
resolve(e)
|
||||
} else {
|
||||
let _p = ndk.storeSubscribe([{ ids: [id] }], { subId: id, closeOnEose: true });
|
||||
_p.subscribe((x) => {
|
||||
if (x[0]) {
|
||||
let e = x[0]
|
||||
_p.unsubscribe();
|
||||
resolve(e)
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -777,3 +777,70 @@ export class BitcoinAssociation {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Product {
|
||||
Event: NDKEvent;
|
||||
Group():string {
|
||||
let s = this.Name()
|
||||
//let regex = /\[\w+\]/i;
|
||||
//if (regex.test(this.Name())) {
|
||||
let g = this.Name().substring(this.Name().indexOf("[")+1, this.Name().lastIndexOf("]"))
|
||||
if (g.length > 0) {
|
||||
s = g
|
||||
}
|
||||
//}
|
||||
return s
|
||||
}
|
||||
Option():string {
|
||||
let result = ""
|
||||
let group = this.Name().substring(this.Name().indexOf("["), this.Name().lastIndexOf("]")+1)
|
||||
if (group.length > 0) {
|
||||
for (let s of this.Name().trim().split(group)) {
|
||||
if (s.trim().length > 0) {
|
||||
result = s.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
ID():string {
|
||||
return this.Event.id
|
||||
}
|
||||
Name():string {
|
||||
return this.Event.getMatchingTags('name')[0][1]
|
||||
}
|
||||
Description():string {
|
||||
return this.Event.getMatchingTags('description')[0][1]
|
||||
}
|
||||
CoverImage():string {
|
||||
return this.Event.getMatchingTags('cover')[0][1]
|
||||
}
|
||||
Validate(): boolean {
|
||||
let test = 0;
|
||||
if (
|
||||
this.Event.getMatchingTags('name') &&
|
||||
this.Event.getMatchingTags('name')[0] &&
|
||||
this.Event.getMatchingTags('name')[0][1]
|
||||
) {
|
||||
test++;
|
||||
}
|
||||
if (
|
||||
this.Event.getMatchingTags('description') &&
|
||||
this.Event.getMatchingTags('description')[0] &&
|
||||
this.Event.getMatchingTags('description')[0][1]
|
||||
) {
|
||||
test++;
|
||||
}
|
||||
if (
|
||||
this.Event.getMatchingTags('cover') &&
|
||||
this.Event.getMatchingTags('cover')[0] &&
|
||||
this.Event.getMatchingTags('cover')[0][1]
|
||||
) {
|
||||
test++;
|
||||
}
|
||||
return test == 3;
|
||||
}
|
||||
constructor(event: NDKEvent) {
|
||||
this.Event = event
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
import { derived } from 'svelte/store';
|
||||
import Heading from '../../components/Heading.svelte';
|
||||
import ProductCard from '../../components/ProductCard.svelte';
|
||||
import { Product, Rocket } from '@/event_helpers/rockets';
|
||||
|
||||
let rockets: NDKEventStore<NDKEvent> | undefined;
|
||||
let products: NDKEventStore<NDKEvent> | undefined;
|
||||
@@ -48,7 +49,7 @@
|
||||
<Heading title={r.dTag} />
|
||||
<div class="grid gap-2" style="grid-template-columns: repeat(auto-fit, 350px);">
|
||||
{#each p as product (product.id)}
|
||||
<ProductCard {product} rocket={r} />
|
||||
<ProductCard product={new Product(product)} rocket={new Rocket(r)} />
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
Reference in New Issue
Block a user