feat: swap component refactor limits (#7588)
* feat: add limits tab, flag * feat: add unit test * fix: update snapshot
This commit is contained in:
parent
ff6d1cc510
commit
52dc441e31
@ -10,6 +10,7 @@ import { useInfoExploreFlag } from 'featureFlags/flags/infoExplore'
|
||||
import { useInfoLiveViewsFlag } from 'featureFlags/flags/infoLiveViews'
|
||||
import { useInfoPoolPageFlag } from 'featureFlags/flags/infoPoolPage'
|
||||
import { useInfoTDPFlag } from 'featureFlags/flags/infoTDP'
|
||||
import { useLimitsEnabledFlag } from 'featureFlags/flags/limits'
|
||||
import { useMultichainUXFlag } from 'featureFlags/flags/multichainUx'
|
||||
import { useProgressIndicatorV2Flag } from 'featureFlags/flags/progressIndicatorV2'
|
||||
import { useQuickRouteMainnetFlag } from 'featureFlags/flags/quickRouteMainnet'
|
||||
@ -274,6 +275,12 @@ export default function FeatureFlagModal() {
|
||||
featureFlag={FeatureFlag.feesEnabled}
|
||||
label="Enable Swap Fees"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={BaseVariant}
|
||||
value={useLimitsEnabledFlag()}
|
||||
featureFlag={FeatureFlag.limitsEnabled}
|
||||
label="Enable Limits"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={BaseVariant}
|
||||
value={useFallbackProviderEnabledFlag()}
|
||||
|
@ -29,12 +29,18 @@ const StyledTextButton = styled(ButtonText)`
|
||||
color: ${({ theme }) => theme.neutral2};
|
||||
gap: 4px;
|
||||
font-weight: 485;
|
||||
transition-duration: ${({ theme }) => theme.transition.duration.fast};
|
||||
transition-timing-function: ease-in-out;
|
||||
transition-property: opacity, color, background-color;
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
&:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
:hover {
|
||||
opacity: ${({ theme }) => theme.opacity.hover};
|
||||
}
|
||||
`
|
||||
|
||||
export default function SwapBuyFiatButton() {
|
||||
|
37
src/components/swap/SwapHeader.test.tsx
Normal file
37
src/components/swap/SwapHeader.test.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { TEST_ALLOWED_SLIPPAGE, TEST_TRADE_EXACT_INPUT } from 'test-utils/constants'
|
||||
import { render, screen } from 'test-utils/render'
|
||||
|
||||
import SwapHeader, { SwapTab } from './SwapHeader'
|
||||
|
||||
jest.mock('../../featureFlags/flags/limits', () => ({ useLimitsEnabled: () => true }))
|
||||
|
||||
describe('SwapHeader.tsx', () => {
|
||||
it('matches base snapshot', () => {
|
||||
const { asFragment } = render(
|
||||
<SwapHeader
|
||||
trade={TEST_TRADE_EXACT_INPUT}
|
||||
selectedTab={SwapTab.Swap}
|
||||
autoSlippage={TEST_ALLOWED_SLIPPAGE}
|
||||
onClickTab={jest.fn()}
|
||||
/>
|
||||
)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
expect(screen.getByText('Swap')).toBeInTheDocument()
|
||||
expect(screen.getByText('Buy')).toBeInTheDocument()
|
||||
expect(screen.getByText('Limit')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls callback for switching tabs', () => {
|
||||
const onClickTab = jest.fn()
|
||||
render(
|
||||
<SwapHeader
|
||||
trade={TEST_TRADE_EXACT_INPUT}
|
||||
selectedTab={SwapTab.Swap}
|
||||
autoSlippage={TEST_ALLOWED_SLIPPAGE}
|
||||
onClickTab={onClickTab}
|
||||
/>
|
||||
)
|
||||
screen.getByText('Limit').click()
|
||||
expect(onClickTab).toHaveBeenCalledWith(SwapTab.Limit)
|
||||
})
|
||||
})
|
@ -1,8 +1,9 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Percent } from '@uniswap/sdk-core'
|
||||
import { useLimitsEnabled } from 'featureFlags/flags/limits'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
import styled from 'styled-components'
|
||||
import { ThemedText } from 'theme/components'
|
||||
import { ButtonText } from 'theme/components'
|
||||
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import SettingsTab from '../Settings'
|
||||
@ -18,22 +19,49 @@ const HeaderButtonContainer = styled(RowFixed)`
|
||||
gap: 16px;
|
||||
`
|
||||
|
||||
const StyledTextButton = styled(ButtonText)<{ $isActive: boolean }>`
|
||||
color: ${({ theme, $isActive }) => ($isActive ? theme.neutral1 : theme.neutral2)};
|
||||
gap: 4px;
|
||||
font-weight: 485;
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
&:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
`
|
||||
|
||||
export enum SwapTab {
|
||||
Swap = 'swap',
|
||||
Limit = 'limit',
|
||||
}
|
||||
|
||||
export default function SwapHeader({
|
||||
autoSlippage,
|
||||
chainId,
|
||||
trade,
|
||||
selectedTab,
|
||||
onClickTab,
|
||||
}: {
|
||||
autoSlippage: Percent
|
||||
chainId?: number
|
||||
trade?: InterfaceTrade
|
||||
selectedTab: SwapTab
|
||||
onClickTab: (tab: SwapTab) => void
|
||||
}) {
|
||||
const limitsEnabled = useLimitsEnabled()
|
||||
return (
|
||||
<StyledSwapHeader>
|
||||
<HeaderButtonContainer>
|
||||
<ThemedText.SubHeader>
|
||||
<StyledTextButton $isActive={selectedTab === SwapTab.Swap} onClick={() => onClickTab?.(SwapTab.Swap)}>
|
||||
<Trans>Swap</Trans>
|
||||
</ThemedText.SubHeader>
|
||||
</StyledTextButton>
|
||||
<SwapBuyFiatButton />
|
||||
{limitsEnabled && (
|
||||
<StyledTextButton $isActive={selectedTab === SwapTab.Limit} onClick={() => onClickTab?.(SwapTab.Limit)}>
|
||||
<Trans>Limit</Trans>
|
||||
</StyledTextButton>
|
||||
)}
|
||||
</HeaderButtonContainer>
|
||||
<RowFixed>
|
||||
<SettingsTab autoSlippage={autoSlippage} chainId={chainId} trade={trade} />
|
||||
|
@ -120,6 +120,12 @@ exports[`SwapBuyFiatButton.tsx matches base snapshot 1`] = `
|
||||
color: #7D7D7D;
|
||||
gap: 4px;
|
||||
font-weight: 485;
|
||||
-webkit-transition-duration: 125ms;
|
||||
transition-duration: 125ms;
|
||||
-webkit-transition-timing-function: ease-in-out;
|
||||
transition-timing-function: ease-in-out;
|
||||
-webkit-transition-property: opacity,color,background-color;
|
||||
transition-property: opacity,color,background-color;
|
||||
}
|
||||
|
||||
.c4:focus {
|
||||
@ -132,6 +138,10 @@ exports[`SwapBuyFiatButton.tsx matches base snapshot 1`] = `
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.c4:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
<div
|
||||
class="c0"
|
||||
>
|
||||
|
343
src/components/swap/__snapshots__/SwapHeader.test.tsx.snap
Normal file
343
src/components/swap/__snapshots__/SwapHeader.test.tsx.snap
Normal file
@ -0,0 +1,343 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SwapHeader.tsx matches base snapshot 1`] = `
|
||||
<DocumentFragment>
|
||||
.c0 {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.c9 {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
line-height: inherit;
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
font-size: inherit;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
color: white;
|
||||
background-color: primary;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
width: 100%;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
width: -webkit-fit-content;
|
||||
width: -moz-fit-content;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
outline: none;
|
||||
border: none;
|
||||
font-size: inherit;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
-webkit-transition-duration: 125ms;
|
||||
transition-duration: 125ms;
|
||||
-webkit-transition-timing-function: ease-in-out;
|
||||
transition-timing-function: ease-in-out;
|
||||
-webkit-transition-property: opacity,color,background-color;
|
||||
transition-property: opacity,color,background-color;
|
||||
}
|
||||
|
||||
.c6:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.c6:focus {
|
||||
-webkit-text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.c10 {
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
line-height: 24px;
|
||||
font-weight: 535;
|
||||
text-align: center;
|
||||
border-radius: 16px;
|
||||
outline: none;
|
||||
border: 1px solid transparent;
|
||||
color: #222222;
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
-webkit-flex-wrap: nowrap;
|
||||
-ms-flex-wrap: nowrap;
|
||||
flex-wrap: nowrap;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
will-change: transform;
|
||||
-webkit-transition: -webkit-transform 450ms ease;
|
||||
-webkit-transition: transform 450ms ease;
|
||||
transition: transform 450ms ease;
|
||||
-webkit-transform: perspective(1px) translateZ(0);
|
||||
-ms-transform: perspective(1px) translateZ(0);
|
||||
transform: perspective(1px) translateZ(0);
|
||||
}
|
||||
|
||||
.c10:disabled {
|
||||
opacity: 50%;
|
||||
cursor: auto;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.c10 > * {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.c10 > a {
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.c11 {
|
||||
padding: 0;
|
||||
width: -webkit-fit-content;
|
||||
width: -moz-fit-content;
|
||||
width: fit-content;
|
||||
background: none;
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.c11:focus {
|
||||
-webkit-text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.c11:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.c11:active {
|
||||
-webkit-text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.c11:disabled {
|
||||
opacity: 50%;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.c8 {
|
||||
display: inline-block;
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.c17 {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.c17 > * {
|
||||
fill: #7D7D7D;
|
||||
}
|
||||
|
||||
.c15 {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.c15:not([disabled]):hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.c16 {
|
||||
padding: 6px 12px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.c14 {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.c12 {
|
||||
color: #7D7D7D;
|
||||
gap: 4px;
|
||||
font-weight: 485;
|
||||
-webkit-transition-duration: 125ms;
|
||||
transition-duration: 125ms;
|
||||
-webkit-transition-timing-function: ease-in-out;
|
||||
transition-timing-function: ease-in-out;
|
||||
-webkit-transition-property: opacity,color,background-color;
|
||||
transition-property: opacity,color,background-color;
|
||||
}
|
||||
|
||||
.c12:focus {
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.c12:active {
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.c12:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
margin-bottom: 10px;
|
||||
color: #7D7D7D;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
padding: 0 12px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
color: #222222;
|
||||
gap: 4px;
|
||||
font-weight: 485;
|
||||
}
|
||||
|
||||
.c7:focus {
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.c7:active {
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.c13 {
|
||||
color: #7D7D7D;
|
||||
gap: 4px;
|
||||
font-weight: 485;
|
||||
}
|
||||
|
||||
.c13:focus {
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.c13:active {
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
<div
|
||||
class="c0 c1 c2 c3"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c4 c5"
|
||||
>
|
||||
<button
|
||||
class="c6 c7"
|
||||
>
|
||||
Swap
|
||||
</button>
|
||||
<div
|
||||
class="c8"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="c9 c10 c11 c12"
|
||||
data-testid="buy-fiat-button"
|
||||
>
|
||||
Buy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="c6 c13"
|
||||
>
|
||||
Limit
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1 c4"
|
||||
>
|
||||
<div
|
||||
class="c14"
|
||||
>
|
||||
<button
|
||||
aria-label="Transaction Settings"
|
||||
class="c15"
|
||||
data-testid="open-settings-dialog-button"
|
||||
disabled=""
|
||||
id="open-settings-dialog-button"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c16"
|
||||
>
|
||||
<svg
|
||||
class="c17"
|
||||
fill="none"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M20.83 14.6C19.9 14.06 19.33 13.07 19.33 12C19.33 10.93 19.9 9.93999 20.83 9.39999C20.99 9.29999 21.05 9.1 20.95 8.94L19.28 6.06C19.22 5.95 19.11 5.89001 19 5.89001C18.94 5.89001 18.88 5.91 18.83 5.94C18.37 6.2 17.85 6.34 17.33 6.34C16.8 6.34 16.28 6.19999 15.81 5.92999C14.88 5.38999 14.31 4.41 14.31 3.34C14.31 3.15 14.16 3 13.98 3H10.02C9.83999 3 9.69 3.15 9.69 3.34C9.69 4.41 9.12 5.38999 8.19 5.92999C7.72 6.19999 7.20001 6.34 6.67001 6.34C6.15001 6.34 5.63001 6.2 5.17001 5.94C5.01001 5.84 4.81 5.9 4.72 6.06L3.04001 8.94C3.01001 8.99 3 9.05001 3 9.10001C3 9.22001 3.06001 9.32999 3.17001 9.39999C4.10001 9.93999 4.67001 10.92 4.67001 11.99C4.67001 13.07 4.09999 14.06 3.17999 14.6H3.17001C3.01001 14.7 2.94999 14.9 3.04999 15.06L4.72 17.94C4.78 18.05 4.89 18.11 5 18.11C5.06 18.11 5.12001 18.09 5.17001 18.06C6.11001 17.53 7.26 17.53 8.19 18.07C9.11 18.61 9.67999 19.59 9.67999 20.66C9.67999 20.85 9.82999 21 10.02 21H13.98C14.16 21 14.31 20.85 14.31 20.66C14.31 19.59 14.88 18.61 15.81 18.07C16.28 17.8 16.8 17.66 17.33 17.66C17.85 17.66 18.37 17.8 18.83 18.06C18.99 18.16 19.19 18.1 19.28 17.94L20.96 15.06C20.99 15.01 21 14.95 21 14.9C21 14.78 20.94 14.67 20.83 14.6ZM12 15C10.34 15 9 13.66 9 12C9 10.34 10.34 9 12 9C13.66 9 15 10.34 15 12C15 13.66 13.66 15 12 15Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
9
src/featureFlags/flags/limits.ts
Normal file
9
src/featureFlags/flags/limits.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
|
||||
|
||||
export function useLimitsEnabledFlag(): BaseVariant {
|
||||
return useBaseFlag(FeatureFlag.limitsEnabled)
|
||||
}
|
||||
|
||||
export function useLimitsEnabled(): boolean {
|
||||
return useLimitsEnabledFlag() === BaseVariant.Enabled
|
||||
}
|
@ -23,6 +23,7 @@ export enum FeatureFlag {
|
||||
progressIndicatorV2 = 'progress_indicator_v2',
|
||||
feesEnabled = 'fees_enabled',
|
||||
androidGALaunch = 'android_ga_launch',
|
||||
limitsEnabled = 'limits_enabled',
|
||||
}
|
||||
|
||||
interface FeatureFlagsContextType {
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -26,7 +26,7 @@ import PriceImpactModal from 'components/swap/PriceImpactModal'
|
||||
import PriceImpactWarning from 'components/swap/PriceImpactWarning'
|
||||
import { ArrowWrapper, PageWrapper, SwapWrapper } from 'components/swap/styled'
|
||||
import SwapDetailsDropdown from 'components/swap/SwapDetailsDropdown'
|
||||
import SwapHeader from 'components/swap/SwapHeader'
|
||||
import SwapHeader, { SwapTab } from 'components/swap/SwapHeader'
|
||||
import { SwitchLocaleLink } from 'components/SwitchLocaleLink'
|
||||
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
|
||||
import { useConnectionReady } from 'connection/eagerlyConnect'
|
||||
@ -601,8 +601,10 @@ export function Swap({
|
||||
const isDark = useIsDarkMode()
|
||||
const isUniswapXDefaultEnabled = useUniswapXDefaultEnabled()
|
||||
|
||||
const [currentTab, setCurrentTab] = useState<SwapTab>(SwapTab.Swap)
|
||||
|
||||
const swapElement = (
|
||||
<SwapWrapper isDark={isDark} className={className} id="swap-page">
|
||||
<>
|
||||
<TokenSafetyModal
|
||||
isOpen={importTokensNotInDefault.length > 0 && !dismissTokenWarning}
|
||||
tokenAddress={importTokensNotInDefault[0]?.address}
|
||||
@ -611,7 +613,6 @@ export function Swap({
|
||||
onCancel={handleDismissTokenWarning}
|
||||
showCancel={true}
|
||||
/>
|
||||
<SwapHeader trade={trade} autoSlippage={autoSlippage} chainId={chainId} />
|
||||
{trade && showConfirm && (
|
||||
<ConfirmSwapModal
|
||||
trade={trade}
|
||||
@ -832,13 +833,23 @@ export function Swap({
|
||||
</div>
|
||||
</AutoColumn>
|
||||
{!showOptInSmall && !isUniswapXDefaultEnabled && <UniswapXOptIn isSmall={false} swapInfo={swapInfo} />}
|
||||
</SwapWrapper>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{swapElement}
|
||||
<SwapWrapper isDark={isDark} className={className} id="swap-page">
|
||||
<SwapHeader
|
||||
selectedTab={currentTab}
|
||||
onClickTab={(tab) => {
|
||||
setCurrentTab(tab)
|
||||
}}
|
||||
trade={trade}
|
||||
autoSlippage={autoSlippage}
|
||||
chainId={chainId}
|
||||
/>
|
||||
{/* todo: build Limit UI */}
|
||||
{currentTab === SwapTab.Swap ? swapElement : undefined}
|
||||
{showOptInSmall && !isUniswapXDefaultEnabled && <UniswapXOptIn isSmall swapInfo={swapInfo} />}
|
||||
</>
|
||||
</SwapWrapper>
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user