Fix/Use the rationale field on Proposals (#406)
* remove freeform from proposal query and replace it with the rationale field * fix: regenerate types and fix proposal names getter Co-authored-by: Botond <fekbot@gmail.com>
This commit is contained in:
parent
4034e14120
commit
a89b2815ee
@ -1,25 +1,40 @@
|
|||||||
|
import { generateProposal } from '../../routes/governance/test-helpers/generate-proposals';
|
||||||
import { getProposalName } from './proposal';
|
import { getProposalName } from './proposal';
|
||||||
|
|
||||||
|
const proposal = generateProposal();
|
||||||
|
|
||||||
it('New market', () => {
|
it('New market', () => {
|
||||||
const name = getProposalName({
|
const name = getProposalName({
|
||||||
__typename: 'NewMarket',
|
...proposal,
|
||||||
decimalPlaces: 1,
|
terms: {
|
||||||
instrument: {
|
...proposal.terms,
|
||||||
__typename: 'InstrumentConfiguration',
|
change: {
|
||||||
name: 'Some market',
|
__typename: 'NewMarket',
|
||||||
|
decimalPlaces: 1,
|
||||||
|
instrument: {
|
||||||
|
__typename: 'InstrumentConfiguration',
|
||||||
|
name: 'Some market',
|
||||||
|
},
|
||||||
|
metadata: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
metadata: [],
|
|
||||||
});
|
});
|
||||||
expect(name).toEqual('New Market: Some market');
|
expect(name).toEqual('New Market: Some market');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('New asset', () => {
|
it('New asset', () => {
|
||||||
const name = getProposalName({
|
const name = getProposalName({
|
||||||
__typename: 'NewAsset',
|
...proposal,
|
||||||
symbol: 'FAKE',
|
terms: {
|
||||||
source: {
|
...proposal.terms,
|
||||||
__typename: 'ERC20',
|
change: {
|
||||||
contractAddress: '0x0',
|
__typename: 'NewAsset',
|
||||||
|
symbol: 'FAKE',
|
||||||
|
source: {
|
||||||
|
__typename: 'ERC20',
|
||||||
|
contractAddress: '0x0',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(name).toEqual('New Asset: FAKE');
|
expect(name).toEqual('New Asset: FAKE');
|
||||||
@ -27,19 +42,31 @@ it('New asset', () => {
|
|||||||
|
|
||||||
it('Update market', () => {
|
it('Update market', () => {
|
||||||
const name = getProposalName({
|
const name = getProposalName({
|
||||||
__typename: 'UpdateMarket',
|
...proposal,
|
||||||
marketId: 'MarketId',
|
terms: {
|
||||||
|
...proposal.terms,
|
||||||
|
change: {
|
||||||
|
__typename: 'UpdateMarket',
|
||||||
|
marketId: 'MarketId',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
expect(name).toEqual('Update Market: MarketId');
|
expect(name).toEqual('Update Market: MarketId');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Update network', () => {
|
it('Update network', () => {
|
||||||
const name = getProposalName({
|
const name = getProposalName({
|
||||||
__typename: 'UpdateNetworkParameter',
|
...proposal,
|
||||||
networkParameter: {
|
terms: {
|
||||||
__typename: 'NetworkParameter',
|
...proposal.terms,
|
||||||
key: 'key',
|
change: {
|
||||||
value: 'value',
|
__typename: 'UpdateNetworkParameter',
|
||||||
|
networkParameter: {
|
||||||
|
__typename: 'NetworkParameter',
|
||||||
|
key: 'key',
|
||||||
|
value: 'value',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(name).toEqual('Update Network: key');
|
expect(name).toEqual('Update Network: key');
|
||||||
@ -47,18 +74,31 @@ it('Update network', () => {
|
|||||||
|
|
||||||
it('Freeform network', () => {
|
it('Freeform network', () => {
|
||||||
const name = getProposalName({
|
const name = getProposalName({
|
||||||
__typename: 'NewFreeform',
|
...proposal,
|
||||||
hash: '0x0',
|
rationale: {
|
||||||
url: 'Earl',
|
...proposal.rationale,
|
||||||
description: 'Something else',
|
hash: '0x0',
|
||||||
|
},
|
||||||
|
terms: {
|
||||||
|
...proposal.terms,
|
||||||
|
change: {
|
||||||
|
__typename: 'NewFreeform',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
expect(name).toEqual('Freeform: 0x0');
|
expect(name).toEqual('Freeform: 0x0');
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Renders unknown proposal if it's a different proposal type", () => {
|
it("Renders unknown proposal if it's a different proposal type", () => {
|
||||||
const name = getProposalName({
|
const name = getProposalName({
|
||||||
// @ts-ignore unknown proposal
|
...proposal,
|
||||||
__typename: 'Foo',
|
terms: {
|
||||||
|
...proposal.terms,
|
||||||
|
change: {
|
||||||
|
// @ts-ignore unknown proposal
|
||||||
|
__typename: 'Foo',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
expect(name).toEqual('Unknown Proposal');
|
expect(name).toEqual('Unknown Proposal');
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import type { Proposals_proposals_terms_change } from '../../routes/governance/proposals/__generated__/Proposals';
|
import type { Proposals_proposals } from '../../routes/governance/proposals/__generated__/Proposals';
|
||||||
|
|
||||||
|
export function getProposalName(proposal: Proposals_proposals) {
|
||||||
|
const { change } = proposal.terms;
|
||||||
|
|
||||||
export function getProposalName(change: Proposals_proposals_terms_change) {
|
|
||||||
if (change.__typename === 'NewAsset') {
|
if (change.__typename === 'NewAsset') {
|
||||||
return `New Asset: ${change.symbol}`;
|
return `New Asset: ${change.symbol}`;
|
||||||
} else if (change.__typename === 'NewMarket') {
|
} else if (change.__typename === 'NewMarket') {
|
||||||
@ -10,7 +12,7 @@ export function getProposalName(change: Proposals_proposals_terms_change) {
|
|||||||
} else if (change.__typename === 'UpdateNetworkParameter') {
|
} else if (change.__typename === 'UpdateNetworkParameter') {
|
||||||
return `Update Network: ${change.networkParameter.key}`;
|
return `Update Network: ${change.networkParameter.key}`;
|
||||||
} else if (change.__typename === 'NewFreeform') {
|
} else if (change.__typename === 'NewFreeform') {
|
||||||
return `Freeform: ${change.hash}`;
|
return `Freeform: ${proposal.rationale.hash}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'Unknown Proposal';
|
return 'Unknown Proposal';
|
||||||
|
@ -9,6 +9,29 @@ import { ProposalState, ProposalRejectionReason, VoteValue } from "@vegaprotocol
|
|||||||
// GraphQL fragment: ProposalFields
|
// GraphQL fragment: ProposalFields
|
||||||
// ====================================================
|
// ====================================================
|
||||||
|
|
||||||
|
export interface ProposalFields_rationale {
|
||||||
|
__typename: "ProposalRationale";
|
||||||
|
/**
|
||||||
|
* Link to a text file describing the proposal in depth.
|
||||||
|
* Optional except for FreeFrom proposal where it's mandatory.
|
||||||
|
* If set, the `url` property must be set.
|
||||||
|
*/
|
||||||
|
url: string | null;
|
||||||
|
/**
|
||||||
|
* Description to show a short title / something in case the link goes offline.
|
||||||
|
* This is to be between 0 and 1024 unicode characters.
|
||||||
|
* This is mandatory for all proposal.
|
||||||
|
*/
|
||||||
|
description: string;
|
||||||
|
/**
|
||||||
|
* Cryptographically secure hash (SHA3-512) of the text pointed by the `url` property
|
||||||
|
* so that viewers can check that the text hasn't been changed over time.
|
||||||
|
* Optional except for FreeFrom proposal where it's mandatory.
|
||||||
|
* If set, the `url` property must be set.
|
||||||
|
*/
|
||||||
|
hash: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ProposalFields_party {
|
export interface ProposalFields_party {
|
||||||
__typename: "Party";
|
__typename: "Party";
|
||||||
/**
|
/**
|
||||||
@ -17,6 +40,10 @@ export interface ProposalFields_party {
|
|||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProposalFields_terms_change_NewFreeform {
|
||||||
|
__typename: "NewFreeform";
|
||||||
|
}
|
||||||
|
|
||||||
export interface ProposalFields_terms_change_NewMarket_instrument {
|
export interface ProposalFields_terms_change_NewMarket_instrument {
|
||||||
__typename: "InstrumentConfiguration";
|
__typename: "InstrumentConfiguration";
|
||||||
/**
|
/**
|
||||||
@ -93,23 +120,7 @@ export interface ProposalFields_terms_change_UpdateNetworkParameter {
|
|||||||
networkParameter: ProposalFields_terms_change_UpdateNetworkParameter_networkParameter;
|
networkParameter: ProposalFields_terms_change_UpdateNetworkParameter_networkParameter;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProposalFields_terms_change_NewFreeform {
|
export type ProposalFields_terms_change = ProposalFields_terms_change_NewFreeform | ProposalFields_terms_change_NewMarket | ProposalFields_terms_change_UpdateMarket | ProposalFields_terms_change_NewAsset | ProposalFields_terms_change_UpdateNetworkParameter;
|
||||||
__typename: "NewFreeform";
|
|
||||||
/**
|
|
||||||
* The URL containing content that describes the proposal
|
|
||||||
*/
|
|
||||||
url: string;
|
|
||||||
/**
|
|
||||||
* A short description of what is being proposed
|
|
||||||
*/
|
|
||||||
description: string;
|
|
||||||
/**
|
|
||||||
* The hash on the content of the URL
|
|
||||||
*/
|
|
||||||
hash: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ProposalFields_terms_change = ProposalFields_terms_change_NewMarket | ProposalFields_terms_change_UpdateMarket | ProposalFields_terms_change_NewAsset | ProposalFields_terms_change_UpdateNetworkParameter | ProposalFields_terms_change_NewFreeform;
|
|
||||||
|
|
||||||
export interface ProposalFields_terms {
|
export interface ProposalFields_terms {
|
||||||
__typename: "ProposalTerms";
|
__typename: "ProposalTerms";
|
||||||
@ -271,6 +282,10 @@ export interface ProposalFields {
|
|||||||
* Error details of the rejectionReason
|
* Error details of the rejectionReason
|
||||||
*/
|
*/
|
||||||
errorDetails: string | null;
|
errorDetails: string | null;
|
||||||
|
/**
|
||||||
|
* Rationale behind the proposal
|
||||||
|
*/
|
||||||
|
rationale: ProposalFields_rationale;
|
||||||
/**
|
/**
|
||||||
* Party that prepared the proposal
|
* Party that prepared the proposal
|
||||||
*/
|
*/
|
||||||
|
@ -19,7 +19,7 @@ export const Proposal = ({ proposal, terms }: ProposalProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Heading title={getProposalName(proposal.terms.change)} />
|
<Heading title={getProposalName(proposal)} />
|
||||||
<ProposalChangeTable proposal={proposal} />
|
<ProposalChangeTable proposal={proposal} />
|
||||||
<VoteDetails proposal={proposal} />
|
<VoteDetails proposal={proposal} />
|
||||||
<ProposalVotesTable proposal={proposal} />
|
<ProposalVotesTable proposal={proposal} />
|
||||||
|
@ -26,7 +26,7 @@ export const ProposalsList = ({ proposals }: ProposalsListProps) => {
|
|||||||
return (
|
return (
|
||||||
<li className="last:mb-0 mb-24" key={proposal.id}>
|
<li className="last:mb-0 mb-24" key={proposal.id}>
|
||||||
<Link to={proposal.id} className="underline">
|
<Link to={proposal.id} className="underline">
|
||||||
<header>{getProposalName(proposal.terms.change)}</header>
|
<header>{getProposalName(proposal)}</header>
|
||||||
</Link>
|
</Link>
|
||||||
<KeyValueTable muted={true}>
|
<KeyValueTable muted={true}>
|
||||||
<KeyValueTableRow>
|
<KeyValueTableRow>
|
||||||
|
@ -8,6 +8,11 @@ export const PROPOSALS_FRAGMENT = gql`
|
|||||||
datetime
|
datetime
|
||||||
rejectionReason
|
rejectionReason
|
||||||
errorDetails
|
errorDetails
|
||||||
|
rationale {
|
||||||
|
url
|
||||||
|
description
|
||||||
|
hash
|
||||||
|
}
|
||||||
party {
|
party {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
@ -43,11 +48,6 @@ export const PROPOSALS_FRAGMENT = gql`
|
|||||||
value
|
value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
... on NewFreeform {
|
|
||||||
url
|
|
||||||
description
|
|
||||||
hash
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
votes {
|
votes {
|
||||||
|
@ -9,6 +9,29 @@ import { ProposalState, ProposalRejectionReason, VoteValue } from "@vegaprotocol
|
|||||||
// GraphQL query operation: Proposal
|
// GraphQL query operation: Proposal
|
||||||
// ====================================================
|
// ====================================================
|
||||||
|
|
||||||
|
export interface Proposal_proposal_rationale {
|
||||||
|
__typename: "ProposalRationale";
|
||||||
|
/**
|
||||||
|
* Link to a text file describing the proposal in depth.
|
||||||
|
* Optional except for FreeFrom proposal where it's mandatory.
|
||||||
|
* If set, the `url` property must be set.
|
||||||
|
*/
|
||||||
|
url: string | null;
|
||||||
|
/**
|
||||||
|
* Description to show a short title / something in case the link goes offline.
|
||||||
|
* This is to be between 0 and 1024 unicode characters.
|
||||||
|
* This is mandatory for all proposal.
|
||||||
|
*/
|
||||||
|
description: string;
|
||||||
|
/**
|
||||||
|
* Cryptographically secure hash (SHA3-512) of the text pointed by the `url` property
|
||||||
|
* so that viewers can check that the text hasn't been changed over time.
|
||||||
|
* Optional except for FreeFrom proposal where it's mandatory.
|
||||||
|
* If set, the `url` property must be set.
|
||||||
|
*/
|
||||||
|
hash: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Proposal_proposal_party {
|
export interface Proposal_proposal_party {
|
||||||
__typename: "Party";
|
__typename: "Party";
|
||||||
/**
|
/**
|
||||||
@ -17,6 +40,10 @@ export interface Proposal_proposal_party {
|
|||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Proposal_proposal_terms_change_NewFreeform {
|
||||||
|
__typename: "NewFreeform";
|
||||||
|
}
|
||||||
|
|
||||||
export interface Proposal_proposal_terms_change_NewMarket_instrument {
|
export interface Proposal_proposal_terms_change_NewMarket_instrument {
|
||||||
__typename: "InstrumentConfiguration";
|
__typename: "InstrumentConfiguration";
|
||||||
/**
|
/**
|
||||||
@ -93,23 +120,7 @@ export interface Proposal_proposal_terms_change_UpdateNetworkParameter {
|
|||||||
networkParameter: Proposal_proposal_terms_change_UpdateNetworkParameter_networkParameter;
|
networkParameter: Proposal_proposal_terms_change_UpdateNetworkParameter_networkParameter;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Proposal_proposal_terms_change_NewFreeform {
|
export type Proposal_proposal_terms_change = Proposal_proposal_terms_change_NewFreeform | Proposal_proposal_terms_change_NewMarket | Proposal_proposal_terms_change_UpdateMarket | Proposal_proposal_terms_change_NewAsset | Proposal_proposal_terms_change_UpdateNetworkParameter;
|
||||||
__typename: "NewFreeform";
|
|
||||||
/**
|
|
||||||
* The URL containing content that describes the proposal
|
|
||||||
*/
|
|
||||||
url: string;
|
|
||||||
/**
|
|
||||||
* A short description of what is being proposed
|
|
||||||
*/
|
|
||||||
description: string;
|
|
||||||
/**
|
|
||||||
* The hash on the content of the URL
|
|
||||||
*/
|
|
||||||
hash: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Proposal_proposal_terms_change = Proposal_proposal_terms_change_NewMarket | Proposal_proposal_terms_change_UpdateMarket | Proposal_proposal_terms_change_NewAsset | Proposal_proposal_terms_change_UpdateNetworkParameter | Proposal_proposal_terms_change_NewFreeform;
|
|
||||||
|
|
||||||
export interface Proposal_proposal_terms {
|
export interface Proposal_proposal_terms {
|
||||||
__typename: "ProposalTerms";
|
__typename: "ProposalTerms";
|
||||||
@ -271,6 +282,10 @@ export interface Proposal_proposal {
|
|||||||
* Error details of the rejectionReason
|
* Error details of the rejectionReason
|
||||||
*/
|
*/
|
||||||
errorDetails: string | null;
|
errorDetails: string | null;
|
||||||
|
/**
|
||||||
|
* Rationale behind the proposal
|
||||||
|
*/
|
||||||
|
rationale: Proposal_proposal_rationale;
|
||||||
/**
|
/**
|
||||||
* Party that prepared the proposal
|
* Party that prepared the proposal
|
||||||
*/
|
*/
|
||||||
|
@ -9,6 +9,29 @@ import { ProposalState, ProposalRejectionReason, VoteValue } from "@vegaprotocol
|
|||||||
// GraphQL query operation: Proposals
|
// GraphQL query operation: Proposals
|
||||||
// ====================================================
|
// ====================================================
|
||||||
|
|
||||||
|
export interface Proposals_proposals_rationale {
|
||||||
|
__typename: "ProposalRationale";
|
||||||
|
/**
|
||||||
|
* Link to a text file describing the proposal in depth.
|
||||||
|
* Optional except for FreeFrom proposal where it's mandatory.
|
||||||
|
* If set, the `url` property must be set.
|
||||||
|
*/
|
||||||
|
url: string | null;
|
||||||
|
/**
|
||||||
|
* Description to show a short title / something in case the link goes offline.
|
||||||
|
* This is to be between 0 and 1024 unicode characters.
|
||||||
|
* This is mandatory for all proposal.
|
||||||
|
*/
|
||||||
|
description: string;
|
||||||
|
/**
|
||||||
|
* Cryptographically secure hash (SHA3-512) of the text pointed by the `url` property
|
||||||
|
* so that viewers can check that the text hasn't been changed over time.
|
||||||
|
* Optional except for FreeFrom proposal where it's mandatory.
|
||||||
|
* If set, the `url` property must be set.
|
||||||
|
*/
|
||||||
|
hash: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Proposals_proposals_party {
|
export interface Proposals_proposals_party {
|
||||||
__typename: "Party";
|
__typename: "Party";
|
||||||
/**
|
/**
|
||||||
@ -17,6 +40,10 @@ export interface Proposals_proposals_party {
|
|||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Proposals_proposals_terms_change_NewFreeform {
|
||||||
|
__typename: "NewFreeform";
|
||||||
|
}
|
||||||
|
|
||||||
export interface Proposals_proposals_terms_change_NewMarket_instrument {
|
export interface Proposals_proposals_terms_change_NewMarket_instrument {
|
||||||
__typename: "InstrumentConfiguration";
|
__typename: "InstrumentConfiguration";
|
||||||
/**
|
/**
|
||||||
@ -93,23 +120,7 @@ export interface Proposals_proposals_terms_change_UpdateNetworkParameter {
|
|||||||
networkParameter: Proposals_proposals_terms_change_UpdateNetworkParameter_networkParameter;
|
networkParameter: Proposals_proposals_terms_change_UpdateNetworkParameter_networkParameter;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Proposals_proposals_terms_change_NewFreeform {
|
export type Proposals_proposals_terms_change = Proposals_proposals_terms_change_NewFreeform | Proposals_proposals_terms_change_NewMarket | Proposals_proposals_terms_change_UpdateMarket | Proposals_proposals_terms_change_NewAsset | Proposals_proposals_terms_change_UpdateNetworkParameter;
|
||||||
__typename: "NewFreeform";
|
|
||||||
/**
|
|
||||||
* The URL containing content that describes the proposal
|
|
||||||
*/
|
|
||||||
url: string;
|
|
||||||
/**
|
|
||||||
* A short description of what is being proposed
|
|
||||||
*/
|
|
||||||
description: string;
|
|
||||||
/**
|
|
||||||
* The hash on the content of the URL
|
|
||||||
*/
|
|
||||||
hash: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Proposals_proposals_terms_change = Proposals_proposals_terms_change_NewMarket | Proposals_proposals_terms_change_UpdateMarket | Proposals_proposals_terms_change_NewAsset | Proposals_proposals_terms_change_UpdateNetworkParameter | Proposals_proposals_terms_change_NewFreeform;
|
|
||||||
|
|
||||||
export interface Proposals_proposals_terms {
|
export interface Proposals_proposals_terms {
|
||||||
__typename: "ProposalTerms";
|
__typename: "ProposalTerms";
|
||||||
@ -271,6 +282,10 @@ export interface Proposals_proposals {
|
|||||||
* Error details of the rejectionReason
|
* Error details of the rejectionReason
|
||||||
*/
|
*/
|
||||||
errorDetails: string | null;
|
errorDetails: string | null;
|
||||||
|
/**
|
||||||
|
* Rationale behind the proposal
|
||||||
|
*/
|
||||||
|
rationale: Proposals_proposals_rationale;
|
||||||
/**
|
/**
|
||||||
* Party that prepared the proposal
|
* Party that prepared the proposal
|
||||||
*/
|
*/
|
||||||
|
@ -24,6 +24,12 @@ export function generateProposal(
|
|||||||
__typename: 'Party',
|
__typename: 'Party',
|
||||||
id: faker.datatype.uuid(),
|
id: faker.datatype.uuid(),
|
||||||
},
|
},
|
||||||
|
rationale: {
|
||||||
|
__typename: 'ProposalRationale',
|
||||||
|
hash: faker.datatype.uuid(),
|
||||||
|
url: faker.internet.url(),
|
||||||
|
description: faker.lorem.words(),
|
||||||
|
},
|
||||||
terms: {
|
terms: {
|
||||||
__typename: 'ProposalTerms',
|
__typename: 'ProposalTerms',
|
||||||
closingDatetime:
|
closingDatetime:
|
||||||
|
@ -46,7 +46,7 @@ export interface Rewards_party_rewardDetails_rewards_epoch {
|
|||||||
export interface Rewards_party_rewardDetails_rewards {
|
export interface Rewards_party_rewardDetails_rewards {
|
||||||
__typename: "Reward";
|
__typename: "Reward";
|
||||||
/**
|
/**
|
||||||
* The asset for which this reward is associated
|
* The asset this reward is paid in
|
||||||
*/
|
*/
|
||||||
asset: Rewards_party_rewardDetails_rewards_asset;
|
asset: Rewards_party_rewardDetails_rewards_asset;
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user