mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 22:14:26 +01:00
Make invoice payments view modular per payment handler (#991)
This commit is contained in:
committed by
Nicolas Dorier
parent
7ea3312534
commit
664b920a39
@@ -72,7 +72,8 @@ namespace BTCPayServer.Controllers
|
|||||||
StatusException = invoice.ExceptionStatus,
|
StatusException = invoice.ExceptionStatus,
|
||||||
Events = invoice.Events,
|
Events = invoice.Events,
|
||||||
PosData = PosDataParser.ParsePosData(invoice.PosData),
|
PosData = PosDataParser.ParsePosData(invoice.PosData),
|
||||||
StatusMessage = StatusMessage
|
StatusMessage = StatusMessage,
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
model.Addresses = invoice.HistoricalAddresses.Select(h =>
|
model.Addresses = invoice.HistoricalAddresses.Select(h =>
|
||||||
@@ -85,20 +86,21 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
var details = InvoicePopulatePayments(invoice);
|
var details = InvoicePopulatePayments(invoice);
|
||||||
model.CryptoPayments = details.CryptoPayments;
|
model.CryptoPayments = details.CryptoPayments;
|
||||||
model.OnChainPayments = details.OnChainPayments;
|
model.Payments = details.Payments;
|
||||||
model.OffChainPayments = details.OffChainPayments;
|
|
||||||
|
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
private InvoiceDetailsModel InvoicePopulatePayments(InvoiceEntity invoice)
|
private InvoiceDetailsModel InvoicePopulatePayments(InvoiceEntity invoice)
|
||||||
{
|
{
|
||||||
var model = new InvoiceDetailsModel();
|
var model = new InvoiceDetailsModel();
|
||||||
|
model.Payments = invoice.GetPayments();
|
||||||
foreach (var data in invoice.GetPaymentMethods())
|
foreach (var data in invoice.GetPaymentMethods())
|
||||||
{
|
{
|
||||||
var accounting = data.Calculate();
|
var accounting = data.Calculate();
|
||||||
var paymentMethodId = data.GetId();
|
var paymentMethodId = data.GetId();
|
||||||
var cryptoPayment = new InvoiceDetailsModel.CryptoPayment();
|
var cryptoPayment = new InvoiceDetailsModel.CryptoPayment();
|
||||||
|
|
||||||
|
cryptoPayment.PaymentMethodId = paymentMethodId;
|
||||||
cryptoPayment.PaymentMethod = paymentMethodId.ToPrettyString();
|
cryptoPayment.PaymentMethod = paymentMethodId.ToPrettyString();
|
||||||
cryptoPayment.Due = _CurrencyNameTable.DisplayFormatCurrency(accounting.Due.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
|
cryptoPayment.Due = _CurrencyNameTable.DisplayFormatCurrency(accounting.Due.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
|
||||||
cryptoPayment.Paid = _CurrencyNameTable.DisplayFormatCurrency(accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
|
cryptoPayment.Paid = _CurrencyNameTable.DisplayFormatCurrency(accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
|
||||||
@@ -108,43 +110,6 @@ namespace BTCPayServer.Controllers
|
|||||||
cryptoPayment.Rate = ExchangeRate(data);
|
cryptoPayment.Rate = ExchangeRate(data);
|
||||||
model.CryptoPayments.Add(cryptoPayment);
|
model.CryptoPayments.Add(cryptoPayment);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var payment in invoice.GetPayments())
|
|
||||||
{
|
|
||||||
var paymentData = payment.GetCryptoPaymentData();
|
|
||||||
//TODO: abstract
|
|
||||||
if (paymentData is Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData)
|
|
||||||
{
|
|
||||||
var m = new InvoiceDetailsModel.Payment();
|
|
||||||
m.Crypto = payment.GetPaymentMethodId().CryptoCode;
|
|
||||||
m.DepositAddress = onChainPaymentData.GetDestination();
|
|
||||||
|
|
||||||
int confirmationCount = onChainPaymentData.ConfirmationCount;
|
|
||||||
if (confirmationCount >= payment.Network.MaxTrackedConfirmation)
|
|
||||||
{
|
|
||||||
m.Confirmations = "At least " + (payment.Network.MaxTrackedConfirmation);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
m.TransactionId = onChainPaymentData.Outpoint.Hash.ToString();
|
|
||||||
m.ReceivedTime = payment.ReceivedTime;
|
|
||||||
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, payment.Network.BlockExplorerLink, m.TransactionId);
|
|
||||||
m.Replaced = !payment.Accounted;
|
|
||||||
model.OnChainPayments.Add(m);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var lightningPaymentData = (LightningLikePaymentData)paymentData;
|
|
||||||
model.OffChainPayments.Add(new InvoiceDetailsModel.OffChainPayment()
|
|
||||||
{
|
|
||||||
Crypto = payment.Network.CryptoCode,
|
|
||||||
BOLT11 = lightningPaymentData.BOLT11
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,32 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace BTCPayServer.Models.InvoicingModels
|
namespace BTCPayServer.Models.InvoicingModels
|
||||||
{
|
{
|
||||||
|
public class OnchainPaymentViewModel
|
||||||
|
{
|
||||||
|
public string Crypto { get; set; }
|
||||||
|
public string Confirmations { get; set; }
|
||||||
|
public BitcoinAddress DepositAddress { get; set; }
|
||||||
|
public string Amount { get; set; }
|
||||||
|
public string TransactionId { get; set; }
|
||||||
|
public DateTimeOffset ReceivedTime { get; set; }
|
||||||
|
public string TransactionLink { get; set; }
|
||||||
|
|
||||||
|
public bool Replaced { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OffChainPaymentViewModel
|
||||||
|
{
|
||||||
|
public string Crypto { get; set; }
|
||||||
|
public string BOLT11 { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public class InvoiceDetailsModel
|
public class InvoiceDetailsModel
|
||||||
{
|
{
|
||||||
public class CryptoPayment
|
public class CryptoPayment
|
||||||
@@ -19,6 +40,8 @@ namespace BTCPayServer.Models.InvoicingModels
|
|||||||
public string Rate { get; internal set; }
|
public string Rate { get; internal set; }
|
||||||
public string PaymentUrl { get; internal set; }
|
public string PaymentUrl { get; internal set; }
|
||||||
public string Overpaid { get; set; }
|
public string Overpaid { get; set; }
|
||||||
|
[JsonIgnore]
|
||||||
|
public PaymentMethodId PaymentMethodId { get; set; }
|
||||||
}
|
}
|
||||||
public class AddressModel
|
public class AddressModel
|
||||||
{
|
{
|
||||||
@@ -26,39 +49,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
|||||||
public string Destination { get; set; }
|
public string Destination { get; set; }
|
||||||
public bool Current { get; set; }
|
public bool Current { get; set; }
|
||||||
}
|
}
|
||||||
public class Payment
|
|
||||||
{
|
|
||||||
public string Crypto { get; set; }
|
|
||||||
public string Confirmations
|
|
||||||
{
|
|
||||||
get; set;
|
|
||||||
}
|
|
||||||
public BitcoinAddress DepositAddress
|
|
||||||
{
|
|
||||||
get; set;
|
|
||||||
}
|
|
||||||
public string Amount
|
|
||||||
{
|
|
||||||
get; set;
|
|
||||||
}
|
|
||||||
public string TransactionId
|
|
||||||
{
|
|
||||||
get; set;
|
|
||||||
}
|
|
||||||
public DateTimeOffset ReceivedTime
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
internal set;
|
|
||||||
}
|
|
||||||
public string TransactionLink
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Replaced { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public string StatusMessage
|
public string StatusMessage
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
@@ -73,14 +64,6 @@ namespace BTCPayServer.Models.InvoicingModels
|
|||||||
get; set;
|
get; set;
|
||||||
} = new List<CryptoPayment>();
|
} = new List<CryptoPayment>();
|
||||||
|
|
||||||
public List<Payment> OnChainPayments { get; set; } = new List<Payment>();
|
|
||||||
public List<OffChainPayment> OffChainPayments { get; set; } = new List<OffChainPayment>();
|
|
||||||
public class OffChainPayment
|
|
||||||
{
|
|
||||||
public string Crypto { get; set; }
|
|
||||||
public string BOLT11 { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public string State
|
public string State
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
@@ -145,5 +128,6 @@ namespace BTCPayServer.Models.InvoicingModels
|
|||||||
public List<Data.InvoiceEventData> Events { get; internal set; }
|
public List<Data.InvoiceEventData> Events { get; internal set; }
|
||||||
public string NotificationEmail { get; internal set; }
|
public string NotificationEmail { get; internal set; }
|
||||||
public Dictionary<string, object> PosData { get; set; }
|
public Dictionary<string, object> PosData { get; set; }
|
||||||
|
public List<PaymentEntity> Payments { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,5 +56,6 @@ namespace BTCPayServer.Payments
|
|||||||
return null;
|
return null;
|
||||||
return string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, txId);
|
return string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, txId);
|
||||||
}
|
}
|
||||||
|
public override string InvoiceViewPaymentPartialName { get; } = "ViewBitcoinLikePaymentData";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,5 +39,6 @@ namespace BTCPayServer.Payments
|
|||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
public override string InvoiceViewPaymentPartialName { get; } = "ViewLightningLikePaymentData";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,5 +61,6 @@ namespace BTCPayServer.Payments
|
|||||||
public abstract ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, JToken value);
|
public abstract ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, JToken value);
|
||||||
|
|
||||||
public abstract string GetTransactionLink(BTCPayNetworkBase network, string txId);
|
public abstract string GetTransactionLink(BTCPayNetworkBase network, string txId);
|
||||||
|
public abstract string InvoiceViewPaymentPartialName { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,100 +1,49 @@
|
|||||||
@model InvoiceDetailsModel
|
@model InvoiceDetailsModel
|
||||||
|
@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12 invoice-payments">
|
<div class="col-md-12 invoice-payments">
|
||||||
<h3>Paid summary</h3>
|
<h3>Paid summary</h3>
|
||||||
<table class="table table-sm table-responsive-md">
|
<table class="table table-sm table-responsive-md">
|
||||||
<thead class="thead-inverse">
|
<thead class="thead-inverse">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Payment method</th>
|
<th>Payment method</th>
|
||||||
<th>Address</th>
|
<th>Address</th>
|
||||||
<th class="text-right">Rate</th>
|
<th class="text-right">Rate</th>
|
||||||
<th class="text-right">Paid</th>
|
<th class="text-right">Paid</th>
|
||||||
<th class="text-right">Due</th>
|
<th class="text-right">Due</th>
|
||||||
@if (Model.StatusException == InvoiceExceptionStatus.PaidOver)
|
@if (Model.StatusException == InvoiceExceptionStatus.PaidOver)
|
||||||
{
|
{
|
||||||
<th class="text-right">Overpaid</th>
|
<th class="text-right">Overpaid</th>
|
||||||
}
|
}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach (var payment in Model.CryptoPayments)
|
@foreach (var payment in Model.CryptoPayments)
|
||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<td>@payment.PaymentMethod</td>
|
<td>@payment.PaymentMethod</td>
|
||||||
<td title="@payment.Address">
|
<td title="@payment.Address">
|
||||||
<span class="text-truncate d-block" style="max-width: 400px">@payment.Address</span>
|
<span class="text-truncate d-block" style="max-width: 400px">@payment.Address</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right">@payment.Rate</td>
|
<td class="text-right">@payment.Rate</td>
|
||||||
<td class="text-right">@payment.Paid</td>
|
<td class="text-right">@payment.Paid</td>
|
||||||
<td class="text-right">@payment.Due</td>
|
<td class="text-right">@payment.Due</td>
|
||||||
@if (Model.StatusException == InvoiceExceptionStatus.PaidOver)
|
@if (Model.StatusException == InvoiceExceptionStatus.PaidOver)
|
||||||
{
|
{
|
||||||
<td class="text-right">@payment.Overpaid</td>
|
<td class="text-right">@payment.Overpaid</td>
|
||||||
}
|
}
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@if (Model.OnChainPayments.Count > 0)
|
@{
|
||||||
{
|
var grouped = Model.Payments.GroupBy(payment => payment.GetPaymentMethodId().PaymentType);
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-12 invoice-payments">
|
|
||||||
<h3>On-Chain payments</h3>
|
|
||||||
<table class="table table-sm table-responsive-lg">
|
|
||||||
<thead class="thead-inverse">
|
|
||||||
<tr>
|
|
||||||
<th>Crypto</th>
|
|
||||||
<th>Deposit address</th>
|
|
||||||
<th>Transaction Id</th>
|
|
||||||
<th class="text-right">Confirmations</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@foreach (var payment in Model.OnChainPayments)
|
|
||||||
{
|
|
||||||
<tr class="@(payment.Replaced ? "linethrough" : "")" >
|
|
||||||
<td>@payment.Crypto</td>
|
|
||||||
<td>@payment.DepositAddress</td>
|
|
||||||
<td>
|
|
||||||
<div class="wraptextAuto">
|
|
||||||
<a href="@payment.TransactionLink" target="_blank">
|
|
||||||
@payment.TransactionId
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="text-right">@payment.Confirmations</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
@if (Model.OffChainPayments.Count > 0)
|
@foreach (var paymentGroup in grouped)
|
||||||
{
|
{
|
||||||
<div class="row">
|
<partial name="@paymentGroup.Key.InvoiceViewPaymentPartialName" model="@paymentGroup.ToList()" />
|
||||||
<div class="col-md-12 invoice-payments">
|
|
||||||
<h3>Off-Chain payments</h3>
|
|
||||||
<table class="table table-sm table-responsive-md">
|
|
||||||
<thead class="thead-inverse">
|
|
||||||
<tr>
|
|
||||||
<th class="firstCol">Crypto</th>
|
|
||||||
<th>BOLT11</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@foreach (var payment in Model.OffChainPayments)
|
|
||||||
{
|
|
||||||
<tr>
|
|
||||||
<td>@payment.Crypto</td>
|
|
||||||
<td><div class="wraptextAuto">@payment.BOLT11</div></td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
|||||||
66
BTCPayServer/Views/Shared/ViewBitcoinLikePaymentData.cshtml
Normal file
66
BTCPayServer/Views/Shared/ViewBitcoinLikePaymentData.cshtml
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
@using System.Globalization
|
||||||
|
@using BTCPayServer.Payments
|
||||||
|
@using BTCPayServer.Payments.Bitcoin
|
||||||
|
@model IEnumerable<BTCPayServer.Services.Invoices.PaymentEntity>
|
||||||
|
|
||||||
|
@{
|
||||||
|
var onchainPayments = Model.Where(entity => entity.GetPaymentMethodId().PaymentType == BitcoinPaymentType.Instance).Select(payment =>
|
||||||
|
{
|
||||||
|
var m = new OnchainPaymentViewModel();
|
||||||
|
var onChainPaymentData = payment.GetCryptoPaymentData() as BitcoinLikePaymentData;
|
||||||
|
m.Crypto = payment.GetPaymentMethodId().CryptoCode;
|
||||||
|
m.DepositAddress = onChainPaymentData.GetDestination();
|
||||||
|
|
||||||
|
int confirmationCount = onChainPaymentData.ConfirmationCount;
|
||||||
|
if (confirmationCount >= payment.Network.MaxTrackedConfirmation)
|
||||||
|
{
|
||||||
|
m.Confirmations = "At least " + (payment.Network.MaxTrackedConfirmation);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
m.TransactionId = onChainPaymentData.Outpoint.Hash.ToString();
|
||||||
|
m.ReceivedTime = payment.ReceivedTime;
|
||||||
|
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, payment.Network.BlockExplorerLink, m.TransactionId);
|
||||||
|
m.Replaced = !payment.Accounted;
|
||||||
|
return m;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (onchainPayments.Any())
|
||||||
|
{
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12 invoice-payments">
|
||||||
|
<h3>On-Chain payments</h3>
|
||||||
|
<table class="table table-sm table-responsive-lg">
|
||||||
|
<thead class="thead-inverse">
|
||||||
|
<tr>
|
||||||
|
<th>Crypto</th>
|
||||||
|
<th>Deposit address</th>
|
||||||
|
<th>Transaction Id</th>
|
||||||
|
<th class="text-right">Confirmations</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var payment in onchainPayments)
|
||||||
|
{
|
||||||
|
<tr class="@(payment.Replaced ? "linethrough" : "")" >
|
||||||
|
<td>@payment.Crypto</td>
|
||||||
|
<td>@payment.DepositAddress</td>
|
||||||
|
<td>
|
||||||
|
<div class="wraptextAuto">
|
||||||
|
<a href="@payment.TransactionLink" target="_blank">
|
||||||
|
@payment.TransactionId
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-right">@payment.Confirmations</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
@using BTCPayServer.Payments
|
||||||
|
@using BTCPayServer.Payments.Lightning
|
||||||
|
@model IEnumerable<BTCPayServer.Services.Invoices.PaymentEntity>
|
||||||
|
|
||||||
|
@{
|
||||||
|
var offchainPayments = Model.Where(entity => entity.GetPaymentMethodId().PaymentType == LightningPaymentType.Instance).Select(payment =>
|
||||||
|
{
|
||||||
|
var offChainPaymentData = payment.GetCryptoPaymentData() as LightningLikePaymentData;
|
||||||
|
return new OffChainPaymentViewModel()
|
||||||
|
{
|
||||||
|
Crypto = payment.Network.CryptoCode,
|
||||||
|
BOLT11 = offChainPaymentData.BOLT11
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@if (offchainPayments.Any())
|
||||||
|
{
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12 invoice-payments">
|
||||||
|
<h3>Off-Chain payments</h3>
|
||||||
|
<table class="table table-sm table-responsive-md">
|
||||||
|
<thead class="thead-inverse">
|
||||||
|
<tr>
|
||||||
|
<th class="firstCol">Crypto</th>
|
||||||
|
<th>BOLT11</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var payment in offchainPayments)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>@payment.Crypto</td>
|
||||||
|
<td><div class="wraptextAuto">@payment.BOLT11</div></td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user