mirror of
https://github.com/oceanprotocol/market.git
synced 2024-12-02 05:57:29 +01:00
Merge pull request #52 from oceanprotocol/feature/price-refactor
refactor price component
This commit is contained in:
commit
65b7b69aa3
@ -34,6 +34,7 @@
|
|||||||
"axios": "^0.19.2",
|
"axios": "^0.19.2",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"date-fns": "^2.15.0",
|
"date-fns": "^2.15.0",
|
||||||
|
"decimal.js": "^10.2.0",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"ethereum-blockies": "github:MyEtherWallet/blockies",
|
"ethereum-blockies": "github:MyEtherWallet/blockies",
|
||||||
"filesize": "^6.1.0",
|
"filesize": "^6.1.0",
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-bold);
|
||||||
font-size: var(--font-size-large);
|
font-size: var(--font-size-large);
|
||||||
color: var(--brand-grey-dark);
|
color: var(--brand-grey-dark);
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price span:first-child {
|
.price span:first-child {
|
||||||
@ -10,6 +11,11 @@
|
|||||||
font-size: var(--font-size-base);
|
font-size: var(--font-size-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
color: var(--color-secondary);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
|
||||||
.small {
|
.small {
|
||||||
/* lazy making-conversion-smaller-with-same-markup */
|
/* lazy making-conversion-smaller-with-same-markup */
|
||||||
transform: scale(0.8);
|
transform: scale(0.8);
|
||||||
|
@ -1,20 +1,39 @@
|
|||||||
import React, { ReactElement } from 'react'
|
import React, { ReactElement, useState, useEffect } from 'react'
|
||||||
import classNames from 'classnames/bind'
|
import classNames from 'classnames/bind'
|
||||||
import PriceConversion from './Conversion'
|
import PriceConversion from './Conversion'
|
||||||
import styles from './index.module.css'
|
import styles from './index.module.css'
|
||||||
import { formatCurrency } from '@coingecko/cryptoformat'
|
import { formatCurrency } from '@coingecko/cryptoformat'
|
||||||
|
import { useMetadata, useOcean } from '@oceanprotocol/react'
|
||||||
|
import { DDO } from '@oceanprotocol/lib'
|
||||||
|
import Loader from '../Loader'
|
||||||
|
import Tooltip from '../Tooltip'
|
||||||
|
|
||||||
const cx = classNames.bind(styles)
|
const cx = classNames.bind(styles)
|
||||||
|
|
||||||
export default function Price({
|
export default function Price({
|
||||||
price,
|
ddo,
|
||||||
className,
|
className,
|
||||||
small
|
small,
|
||||||
|
setPriceOutside
|
||||||
}: {
|
}: {
|
||||||
price: string // expects price in OCEAN, not wei
|
ddo: DDO
|
||||||
className?: string
|
className?: string
|
||||||
small?: boolean
|
small?: boolean
|
||||||
|
setPriceOutside?: (price: string) => void
|
||||||
}): ReactElement {
|
}): ReactElement {
|
||||||
|
const { chainId } = useOcean()
|
||||||
|
const { getBestPrice } = useMetadata()
|
||||||
|
const [price, setPrice] = useState<string>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function init() {
|
||||||
|
const price = await getBestPrice(ddo.dataToken)
|
||||||
|
setPrice(price)
|
||||||
|
setPriceOutside && price !== '' && setPriceOutside(price)
|
||||||
|
}
|
||||||
|
init()
|
||||||
|
}, [chainId])
|
||||||
|
|
||||||
const styleClasses = cx({
|
const styleClasses = cx({
|
||||||
price: true,
|
price: true,
|
||||||
small: small,
|
small: small,
|
||||||
@ -32,5 +51,14 @@ export default function Price({
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
return <div className={styleClasses}>{displayPrice}</div>
|
return price ? (
|
||||||
|
<div className={styleClasses}>{displayPrice}</div>
|
||||||
|
) : price === '' ? (
|
||||||
|
<div className={styles.empty}>
|
||||||
|
No price found{' '}
|
||||||
|
<Tooltip content="We could not find a pool for this data set, which can have multiple reasons. Is your wallet connected to the correct network?" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Loader message="Retrieving price..." />
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -23,17 +23,6 @@ const AssetTeaser: React.FC<AssetTeaserProps> = ({
|
|||||||
const { description } = metadata.additionalInformation
|
const { description } = metadata.additionalInformation
|
||||||
const isCompute = Boolean(ddo.findServiceByType('compute'))
|
const isCompute = Boolean(ddo.findServiceByType('compute'))
|
||||||
|
|
||||||
const { getBestPrice } = useMetadata(ddo.id)
|
|
||||||
const [price, setPrice] = useState<string>()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function init() {
|
|
||||||
const price = await getBestPrice(ddo.dataToken)
|
|
||||||
setPrice(price)
|
|
||||||
}
|
|
||||||
init()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className={styles.teaser}>
|
<article className={styles.teaser}>
|
||||||
<Link to={`/asset/${ddo.id}`} className={styles.link}>
|
<Link to={`/asset/${ddo.id}`} className={styles.link}>
|
||||||
@ -47,13 +36,7 @@ const AssetTeaser: React.FC<AssetTeaserProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer className={styles.foot}>
|
<footer className={styles.foot}>
|
||||||
{price ? (
|
<Price ddo={ddo} small />
|
||||||
<Price price={price} small />
|
|
||||||
) : price === '' ? (
|
|
||||||
'No price found'
|
|
||||||
) : (
|
|
||||||
<Loader message="Retrieving price..." />
|
|
||||||
)}
|
|
||||||
</footer>
|
</footer>
|
||||||
</Link>
|
</Link>
|
||||||
</article>
|
</article>
|
||||||
|
@ -12,15 +12,20 @@ export declare type Web3Error = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Web3Feedback({
|
export default function Web3Feedback({
|
||||||
isBalanceInsufficient
|
isBalanceSufficient
|
||||||
}: {
|
}: {
|
||||||
isBalanceInsufficient?: boolean
|
isBalanceSufficient?: boolean
|
||||||
}): ReactElement {
|
}): ReactElement {
|
||||||
const { appConfig } = useSiteMetadata()
|
const { appConfig } = useSiteMetadata()
|
||||||
const { account, status, chainId } = useOcean()
|
const { account, status, chainId } = useOcean()
|
||||||
const isOceanConnectionError = status === -1
|
const isOceanConnectionError = status === -1
|
||||||
const correctNetwork = isCorrectNetwork(chainId)
|
const correctNetwork = isCorrectNetwork(chainId)
|
||||||
const showFeedback = !account || isOceanConnectionError || !correctNetwork
|
const showFeedback =
|
||||||
|
!account ||
|
||||||
|
isOceanConnectionError ||
|
||||||
|
!correctNetwork ||
|
||||||
|
isBalanceSufficient === false
|
||||||
|
|
||||||
const desiredNetworkName = appConfig.network.replace(/^\w/, (c: string) =>
|
const desiredNetworkName = appConfig.network.replace(/^\w/, (c: string) =>
|
||||||
c.toUpperCase()
|
c.toUpperCase()
|
||||||
)
|
)
|
||||||
@ -29,7 +34,7 @@ export default function Web3Feedback({
|
|||||||
? 'error'
|
? 'error'
|
||||||
: !correctNetwork
|
: !correctNetwork
|
||||||
? 'warning'
|
? 'warning'
|
||||||
: account && !isBalanceInsufficient
|
: account && isBalanceSufficient
|
||||||
? 'success'
|
? 'success'
|
||||||
: 'warning'
|
: 'warning'
|
||||||
|
|
||||||
@ -40,7 +45,7 @@ export default function Web3Feedback({
|
|||||||
: !correctNetwork
|
: !correctNetwork
|
||||||
? 'Wrong Network'
|
? 'Wrong Network'
|
||||||
: account
|
: account
|
||||||
? isBalanceInsufficient === true
|
? isBalanceSufficient === false
|
||||||
? 'Insufficient balance'
|
? 'Insufficient balance'
|
||||||
: 'Connected to Ocean'
|
: 'Connected to Ocean'
|
||||||
: 'Something went wrong'
|
: 'Something went wrong'
|
||||||
@ -51,7 +56,7 @@ export default function Web3Feedback({
|
|||||||
? 'Please try again.'
|
? 'Please try again.'
|
||||||
: !correctNetwork
|
: !correctNetwork
|
||||||
? `Please connect to ${desiredNetworkName}.`
|
? `Please connect to ${desiredNetworkName}.`
|
||||||
: isBalanceInsufficient === true
|
: isBalanceSufficient === false
|
||||||
? 'You do not have enough OCEAN in your wallet to purchase this asset.'
|
? 'You do not have enough OCEAN in your wallet to purchase this asset.'
|
||||||
: 'Something went wrong.'
|
: 'Something went wrong.'
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React, { useState, useEffect, ReactElement } from 'react'
|
import React, { useState, ReactElement } from 'react'
|
||||||
import { DDO } from '@oceanprotocol/lib'
|
import { DDO } from '@oceanprotocol/lib'
|
||||||
import compareAsBN, { Comparisson } from '../../../utils/compareAsBN'
|
|
||||||
import Loader from '../../atoms/Loader'
|
import Loader from '../../atoms/Loader'
|
||||||
import Web3Feedback from '../../molecules/Wallet/Feedback'
|
import Web3Feedback from '../../molecules/Wallet/Feedback'
|
||||||
import Dropzone from '../../atoms/Dropzone'
|
import Dropzone from '../../atoms/Dropzone'
|
||||||
@ -18,10 +17,12 @@ import Alert from '../../atoms/Alert'
|
|||||||
|
|
||||||
export default function Compute({
|
export default function Compute({
|
||||||
ddo,
|
ddo,
|
||||||
price
|
isBalanceSufficient,
|
||||||
|
setPrice
|
||||||
}: {
|
}: {
|
||||||
ddo: DDO
|
ddo: DDO
|
||||||
price: string // in OCEAN, not wei
|
isBalanceSufficient: boolean
|
||||||
|
setPrice: (price: string) => void
|
||||||
}): ReactElement {
|
}): ReactElement {
|
||||||
const { ocean } = useOcean()
|
const { ocean } = useOcean()
|
||||||
const { compute, isLoading, computeStepText, computeError } = useCompute()
|
const { compute, isLoading, computeStepText, computeError } = useCompute()
|
||||||
@ -29,7 +30,6 @@ export default function Compute({
|
|||||||
|
|
||||||
const [isJobStarting, setIsJobStarting] = useState(false)
|
const [isJobStarting, setIsJobStarting] = useState(false)
|
||||||
const [, setError] = useState('')
|
const [, setError] = useState('')
|
||||||
const [isBalanceSufficient, setIsBalanceSufficient] = useState(false)
|
|
||||||
const [computeType, setComputeType] = useState('')
|
const [computeType, setComputeType] = useState('')
|
||||||
const [computeContainer, setComputeContainer] = useState({
|
const [computeContainer, setComputeContainer] = useState({
|
||||||
entrypoint: '',
|
entrypoint: '',
|
||||||
@ -39,25 +39,13 @@ export default function Compute({
|
|||||||
const [algorithmRawCode, setAlgorithmRawCode] = useState('')
|
const [algorithmRawCode, setAlgorithmRawCode] = useState('')
|
||||||
const [isPublished, setIsPublished] = useState(false)
|
const [isPublished, setIsPublished] = useState(false)
|
||||||
const [file, setFile] = useState(null)
|
const [file, setFile] = useState(null)
|
||||||
const [isTermsAgreed, setIsTermsAgreed] = useState(true)
|
|
||||||
|
|
||||||
const isFree = price === '0'
|
|
||||||
|
|
||||||
const isComputeButtonDisabled =
|
const isComputeButtonDisabled =
|
||||||
isJobStarting ||
|
isJobStarting ||
|
||||||
file === null ||
|
file === null ||
|
||||||
computeType === '' ||
|
computeType === '' ||
|
||||||
!ocean ||
|
!ocean ||
|
||||||
!isBalanceSufficient ||
|
!isBalanceSufficient
|
||||||
!isTermsAgreed
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// setIsBalanceSufficient(
|
|
||||||
// isFree ||
|
|
||||||
// (balance !== null &&
|
|
||||||
// compareAsBN(balance, fromWei(computeService.cost), Comparisson.gte))
|
|
||||||
// )
|
|
||||||
// }, [balance])
|
|
||||||
|
|
||||||
const onDrop = async (files: any) => {
|
const onDrop = async (files: any) => {
|
||||||
setFile(files[0])
|
setFile(files[0])
|
||||||
@ -101,13 +89,7 @@ export default function Compute({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.compute}>
|
<div className={styles.compute}>
|
||||||
{price ? (
|
<Price ddo={ddo} setPriceOutside={setPrice} />
|
||||||
<Price price={price} small />
|
|
||||||
) : price === '' ? (
|
|
||||||
'No price found'
|
|
||||||
) : (
|
|
||||||
<Loader message="Retrieving price..." />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className={styles.info}>
|
<div className={styles.info}>
|
||||||
<div className={styles.selectType}>
|
<div className={styles.selectType}>
|
||||||
@ -133,7 +115,6 @@ export default function Compute({
|
|||||||
Start job
|
Start job
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/* <TermsCheckbox onChange={onCheck} /> */}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer className={styles.feedback}>
|
<footer className={styles.feedback}>
|
||||||
@ -148,7 +129,7 @@ export default function Compute({
|
|||||||
state="success"
|
state="success"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Web3Feedback isBalanceInsufficient={!isBalanceSufficient} />
|
<Web3Feedback isBalanceSufficient={isBalanceSufficient} />
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,16 +12,18 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.consume button {
|
.pricewrapper {
|
||||||
margin-left: calc(var(--spacer) / 4);
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.consume button:first-of-type {
|
.pricewrapper button {
|
||||||
margin-left: 0;
|
margin-top: calc(var(--spacer) / 2);
|
||||||
|
align-self: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price {
|
.pricewrapper > div {
|
||||||
margin-bottom: calc(var(--spacer) / 2);
|
min-height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feedback {
|
.feedback {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import React, { ReactElement } from 'react'
|
import React, { ReactElement } from 'react'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { File as FileMetadata, DDO } from '@oceanprotocol/lib'
|
import { File as FileMetadata, DDO } from '@oceanprotocol/lib'
|
||||||
import compareAsBN, { Comparisson } from '../../../utils/compareAsBN'
|
|
||||||
import Button from '../../atoms/Button'
|
import Button from '../../atoms/Button'
|
||||||
import File from '../../atoms/File'
|
import File from '../../atoms/File'
|
||||||
import Price from '../../atoms/Price'
|
import Price from '../../atoms/Price'
|
||||||
@ -12,21 +11,19 @@ import { useOcean, useConsume } from '@oceanprotocol/react'
|
|||||||
|
|
||||||
export default function Consume({
|
export default function Consume({
|
||||||
ddo,
|
ddo,
|
||||||
price,
|
file,
|
||||||
file
|
isBalanceSufficient,
|
||||||
|
setPrice
|
||||||
}: {
|
}: {
|
||||||
ddo: DDO
|
ddo: DDO
|
||||||
price: string // in OCEAN, not wei
|
|
||||||
file: FileMetadata
|
file: FileMetadata
|
||||||
|
isBalanceSufficient: boolean
|
||||||
|
setPrice: (price: string) => void
|
||||||
}): ReactElement {
|
}): ReactElement {
|
||||||
const accessService = ddo.findServiceByType('access')
|
|
||||||
const { ocean } = useOcean()
|
const { ocean } = useOcean()
|
||||||
const { consumeStepText, consume, consumeError } = useConsume()
|
const { consumeStepText, consume, consumeError } = useConsume()
|
||||||
|
|
||||||
const isFree = price === '0'
|
const isDisabled = !ocean || !isBalanceSufficient
|
||||||
// const isBalanceSufficient =
|
|
||||||
// isFree || compareAsBN(balanceInOcean, fromWei(cost), Comparisson.gte)
|
|
||||||
const isDisabled = !ocean
|
|
||||||
|
|
||||||
if (consumeError) {
|
if (consumeError) {
|
||||||
toast.error(consumeError)
|
toast.error(consumeError)
|
||||||
@ -41,7 +38,7 @@ export default function Consume({
|
|||||||
onClick={() => consume(ddo.id, ddo.dataToken, 'access')}
|
onClick={() => consume(ddo.id, ddo.dataToken, 'access')}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
>
|
>
|
||||||
{isFree ? 'Download' : 'Buy'}
|
Buy
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -52,19 +49,13 @@ export default function Consume({
|
|||||||
<File file={file} />
|
<File file={file} />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.pricewrapper}>
|
<div className={styles.pricewrapper}>
|
||||||
{price ? (
|
<Price ddo={ddo} setPriceOutside={setPrice} />
|
||||||
<Price price={price} small />
|
|
||||||
) : price === '' ? (
|
|
||||||
'No price found'
|
|
||||||
) : (
|
|
||||||
<Loader message="Retrieving price..." />
|
|
||||||
)}
|
|
||||||
<PurchaseButton />
|
<PurchaseButton />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer className={styles.feedback}>
|
<footer className={styles.feedback}>
|
||||||
<Web3Feedback isBalanceInsufficient />
|
<Web3Feedback isBalanceSufficient={isBalanceSufficient} />
|
||||||
</footer>
|
</footer>
|
||||||
</aside>
|
</aside>
|
||||||
)
|
)
|
||||||
|
@ -5,7 +5,8 @@ import Consume from './Consume'
|
|||||||
import { MetadataMarket } from '../../../@types/Metadata'
|
import { MetadataMarket } from '../../../@types/Metadata'
|
||||||
import { DDO } from '@oceanprotocol/lib'
|
import { DDO } from '@oceanprotocol/lib'
|
||||||
import Tabs from '../../atoms/Tabs'
|
import Tabs from '../../atoms/Tabs'
|
||||||
import { useMetadata } from '@oceanprotocol/react'
|
import { useOcean } from '@oceanprotocol/react'
|
||||||
|
import compareAsBN from '../../../utils/compareAsBN'
|
||||||
|
|
||||||
export default function AssetActions({
|
export default function AssetActions({
|
||||||
metadata,
|
metadata,
|
||||||
@ -14,22 +15,37 @@ export default function AssetActions({
|
|||||||
metadata: MetadataMarket
|
metadata: MetadataMarket
|
||||||
ddo: DDO
|
ddo: DDO
|
||||||
}): ReactElement {
|
}): ReactElement {
|
||||||
const { getBestPrice } = useMetadata(ddo.id)
|
const { balance } = useOcean()
|
||||||
const [price, setPrice] = useState<string>()
|
const [price, setPrice] = useState<string>()
|
||||||
|
const [isBalanceSufficient, setIsBalanceSufficient] = useState<boolean>()
|
||||||
useEffect(() => {
|
|
||||||
async function init() {
|
|
||||||
const price = await getBestPrice(ddo.dataToken)
|
|
||||||
setPrice(price)
|
|
||||||
}
|
|
||||||
init()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const isCompute = Boolean(ddo.findServiceByType('compute'))
|
const isCompute = Boolean(ddo.findServiceByType('compute'))
|
||||||
|
|
||||||
|
// Check user balance against price
|
||||||
|
useEffect(() => {
|
||||||
|
if (!price || !balance || !balance.ocean) return
|
||||||
|
|
||||||
|
const isFree = price === '0'
|
||||||
|
setIsBalanceSufficient(isFree ? true : compareAsBN(balance.ocean, price))
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setIsBalanceSufficient(false)
|
||||||
|
}
|
||||||
|
}, [balance, price])
|
||||||
|
|
||||||
const UseContent = isCompute ? (
|
const UseContent = isCompute ? (
|
||||||
<Compute ddo={ddo} price={price} />
|
<Compute
|
||||||
|
ddo={ddo}
|
||||||
|
isBalanceSufficient={isBalanceSufficient}
|
||||||
|
setPrice={setPrice}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Consume ddo={ddo} price={price} file={metadata.main.files[0]} />
|
<Consume
|
||||||
|
ddo={ddo}
|
||||||
|
isBalanceSufficient={isBalanceSufficient}
|
||||||
|
file={metadata.main.files[0]}
|
||||||
|
setPrice={setPrice}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
|
@ -1,21 +1,16 @@
|
|||||||
import BN from 'bn.js'
|
import { Decimal } from 'decimal.js'
|
||||||
|
|
||||||
export enum Comparisson {
|
// Run decimal.js comparison
|
||||||
'lt' = 'lt',
|
// http://mikemcl.github.io/decimal.js/#cmp
|
||||||
'lte' = 'lte',
|
export default function compareAsBN(balance: string, price: string): boolean {
|
||||||
'gt' = 'gt',
|
const aBN = new Decimal(balance)
|
||||||
'gte' = 'gte',
|
const bBN = new Decimal(price)
|
||||||
'eq' = 'eq'
|
const compare = aBN.comparedTo(bBN)
|
||||||
}
|
|
||||||
|
|
||||||
// Run the corresponding bn.js comparisson:
|
switch (compare) {
|
||||||
// https://github.com/indutny/bn.js/#utilities
|
case 1 || 0: // balance is greater or equal to price
|
||||||
export default function compareAsBN(
|
return true
|
||||||
a: string,
|
default:
|
||||||
b: string,
|
return false
|
||||||
comparisson: Comparisson
|
}
|
||||||
) {
|
|
||||||
const aBN = new BN(a)
|
|
||||||
const bBN = new BN(b)
|
|
||||||
return aBN[comparisson](bBN)
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user