Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/removal challenge justification display #42

Merged
merged 6 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion subgraph/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,9 @@ type Request @entity {
"True if a dispute was raised."
disputed: Boolean!
"ID of the dispute, if any."
disputeID: BigInt!
disputeID: BigInt
"External ID of the dispute, this is always there since it's just requestID. Please use disputed field to check if the dispute was created."
externalDisputeID: BigInt!
"Time when the request was made. Used to track when the challenge period ends."
submissionTime: BigInt!
"True if the request was executed and/or any raised disputes were resolved."
Expand All @@ -172,6 +174,8 @@ type Request @entity {
requester: User!
"The party that challenged the request"
challenger: User
"Time when the request was challenged."
challengeTime: BigInt
"The arbitrator trusted to solve disputes for this request."
arbitrator: Bytes!
"The extra data for the trusted arbitrator of this request."
Expand Down
1 change: 1 addition & 0 deletions subgraph/src/Curate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ export function handleRequestChallenged(event: DisputeRequest): void {

request.disputed = true;
request.challenger = ensureUser(event.transaction.from.toHexString()).id;
request.challengeTime = event.block.timestamp;
Harman-singh-waraich marked this conversation as resolved.
Show resolved Hide resolved
request.disputeID = event.params._arbitrableDisputeID;

request.save();
Expand Down
4 changes: 2 additions & 2 deletions subgraph/src/entities/Request.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { log } from "@graphprotocol/graph-ts";
import { Item, Registry, Request } from "../../generated/schema";
import { Item, Request } from "../../generated/schema";
import { Curate, RequestSubmitted } from "../../generated/templates/Curate/Curate";
import { NONE, ONE, ZERO } from "../utils";
import { ensureCounter } from "./Counters";
Expand Down Expand Up @@ -28,10 +28,10 @@ export function createRequestFromEvent(event: RequestSubmitted): void {
request.resolutionTime = ZERO;
request.disputeOutcome = NONE;
request.resolved = false;
request.disputeID = ZERO;
request.submissionTime = event.block.timestamp;
request.requestType = item.status;
request.creationTx = event.transaction.hash;
request.externalDisputeID = event.params._requestID;

let counter = ensureCounter();
let deposit = item.status.includes("registrationRequested")
Expand Down
2 changes: 1 addition & 1 deletion web/.env.devnet.public
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Do not enter sensitive information here.
export REACT_APP_DEPLOYMENT=devnet
export REACT_APP_ARBSEPOLIA_SUBGRAPH=https://api.studio.thegraph.com/query/71663/curate-test/version/latest
export REACT_APP_ARBSEPOLIA_SUBGRAPH=https://api.studio.thegraph.com/query/61738/curate-v2-devnet/version/latest
export REACT_APP_CORE_SUBGRAPH=https://api.studio.thegraph.com/query/61738/kleros-v2-core-devnet/version/latest
export REACT_APP_STATUS_URL=https://curate-v2-devnet.betteruptime.com/badge
export REACT_APP_GENESIS_BLOCK_ARBSEPOLIA=24725439
2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
},
"dependencies": {
"@filebase/client": "^0.0.5",
"@kleros/ui-components-library": "^2.12.0",
"@kleros/ui-components-library": "^2.13.1",
"@middy/core": "^5.3.5",
"@middy/http-json-body-parser": "^5.3.5",
"@sentry/react": "^7.93.0",
Expand Down
2 changes: 1 addition & 1 deletion web/src/assets/svgs/icons/close-circle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions web/src/components/HistoryDisplay/Party/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from "react";
import styled from "styled-components";

const StyledHeader = styled.h1`
margin: 0;
`;

interface IHeader {
text: string;
}

const Header: React.FC<IHeader> = ({ text }) => {
return <StyledHeader>{text}</StyledHeader>;
};

export default Header;
58 changes: 58 additions & 0 deletions web/src/components/HistoryDisplay/Party/JustificationDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from "react";
import ReactMarkdown from "react-markdown";
import styled from "styled-components";
import { getIpfsUrl } from "utils/getIpfsUrl";
import AttachmentIcon from "svgs/icons/attachment.svg";
import { customScrollbar } from "styles/customScrollbar";

const Container = styled.div`
width: 100%;
display: flex;
flex-direction: column;
`;

const JustificationTitle = styled.h3`
margin: 0px;
font-weight: 600;
`;

const DescriptionContainer = styled.div`
max-height: 400px;
width: 100%;
overflow-y: scroll;
${customScrollbar}
`;

const StyledA = styled.a`
display: flex;
gap: 6px;
> svg {
width: 16px;
fill: ${({ theme }) => theme.primaryBlue};
}
`;

export type Justification = {
name: string;
description: string;
fileURI?: string;
};

const JustificationDetails: React.FC<{ justification: Justification }> = ({ justification }) => {
return (
<Container>
<JustificationTitle>{justification.name}</JustificationTitle>
<DescriptionContainer>
<ReactMarkdown>{justification.description}</ReactMarkdown>
</DescriptionContainer>
{justification?.fileURI && (
<StyledA href={getIpfsUrl(justification.fileURI)}>
<AttachmentIcon />
View attached file
</StyledA>
)}
</Container>
);
};

export default JustificationDetails;
127 changes: 127 additions & 0 deletions web/src/components/HistoryDisplay/Party/JustificationModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import React, { useEffect, useState } from "react";
import styled from "styled-components";
import { Button } from "@kleros/ui-components-library";

import Modal from "components/Modal";
import ActionButton from "components/ActionButton";
import { SkeletonJustificationCard } from "components/StyledSkeleton";
import { mapFromSubgraphStatus } from "components/RegistryCard/StatusBanner";

import { EvidencesQuery, RequestDetailsFragment } from "src/graphql/graphql";
import { isUndefined } from "src/utils";
import { getIpfsUrl } from "utils/getIpfsUrl";
import fetchJsonIpfs from "utils/fetchJsonIpfs";
import { useEvidences } from "queries/useEvidences";

import Header from "./Header";
import JustificationDetails, { Justification } from "./JustificationDetails";

const StyledModal = styled(Modal)`
gap: 30px;
`;

const ButtonsContainer = styled.div`
width: 100%;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
margin-top: 38px;
row-gap: 8px;
`;

const StyledLabel = styled.label`
width: 100%;
`;

const JustificationText = styled.h3`
width: 100%;
margin: 0px;
margin-bottom: 4px;
text-align: center;
`;

interface IJustificationModal {
request: RequestDetailsFragment;
isRemoval: boolean;
toggleModal: () => void;
}

const JustificationModal: React.FC<IJustificationModal> = ({ request, toggleModal, isRemoval }) => {
const { data: evidenceData, isLoading: isLoadingEvidences } = useEvidences(request.externalDisputeID);
const [justification, setJustification] = useState<Justification>();
const [isLoadingJustification, setIsLoadingJustification] = useState(false);

useEffect(() => {
if (isUndefined(evidenceData)) return;
setIsLoadingJustification(true);

const uri = getEvidenceUriForRequest(request, evidenceData.evidences, isRemoval);

if (!uri) {
setIsLoadingJustification(false);
return;
}

fetchJsonIpfs(getIpfsUrl(uri))
.then((res) => {
setJustification(res as Justification);
})
.finally(() => setIsLoadingJustification(false));
}, [evidenceData, isRemoval, request]);
Harman-singh-waraich marked this conversation as resolved.
Show resolved Hide resolved

return (
<StyledModal {...{ toggleModal }}>
<Header text={isRemoval ? "Removal Requested" : "Request Challenged"} />
<JustificationText>Justification</JustificationText>
{isLoadingEvidences || isLoadingJustification ? (
<SkeletonJustificationCard />
) : justification ? (
<JustificationDetails {...{ justification }} />
) : (
<StyledLabel>No Justification provided</StyledLabel>
)}
<ButtonsContainer>
<Button variant="secondary" text="Return" onClick={toggleModal} />
{!request.resolved && (
<ActionButton
isItem
itemId={request.item.itemID}
status={mapFromSubgraphStatus(request.item.status, request.disputed)}
registryAddress={request.item.registryAddress}
/>
)}
</ButtonsContainer>
</StyledModal>
);
};

/**
* @description returns the correct evidence relating to the request
* @need this is needed since the removal request might not have the evidence, same for challenge request.
* to get the correct evidence for the request, we match the timestamp of the request and evidence,
* if both are same , it means the evidence was created in the same block as that request and belongs to the request
* @returns the evidence uri for the request if it exists, otherwise null
*/
const getEvidenceUriForRequest = (
request: RequestDetailsFragment,
evidences: EvidencesQuery["evidences"],
isRemoval: boolean
) => {
if (isRemoval) {
if (evidences.length > 0 && evidences[0].timestamp === request.submissionTime) {
return evidences[0].evidence;
} else {
return null;
}
}

// in case of challenge either the first or the second one can be the challenge evidence,
// in case of registration challenge, the 1st one is the challenge evidence,
// or if the removal request did not have any justification, the 1st one could be the challenge justification
if (evidences.length > 0 && evidences[0].timestamp === request.challengeTime) return evidences[0].evidence;
if (evidences.length > 1 && evidences[1].timestamp === request.challengeTime) return evidences[1].evidence;

return null;
};
Harman-singh-waraich marked this conversation as resolved.
Show resolved Hide resolved

export default JustificationModal;
53 changes: 53 additions & 0 deletions web/src/components/HistoryDisplay/Party/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from "react";
import styled from "styled-components";
import AliasDisplay from "components/RegistryInfo/AliasDisplay";
import { RequestDetailsFragment } from "src/graphql/graphql";
import DocIcon from "svgs/icons/doc.svg";
import { useToggle } from "react-use";
import JustificationModal from "./JustificationModal";

const Container = styled.div`
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
gap: 8px;
`;

const StyledLabel = styled.label`
display: flex;
align-items: center;
gap: 4px;
color: ${({ theme }) => theme.primaryBlue};
cursor: pointer;
`;

const StyledDoc = styled(DocIcon)`
width: 16px;
height: 16px;
fill: ${({ theme }) => theme.primaryBlue};
`;

interface IParty {
request: RequestDetailsFragment;
isRemoval?: boolean;
}

const Party: React.FC<IParty> = ({ request, isRemoval = false }) => {
const [isOpen, toggleModal] = useToggle(false);
const aliasAddress = isRemoval ? request.requester.id : request?.challenger?.id;

return (
<Container>
<label>by</label>
<AliasDisplay address={aliasAddress ?? ""} />
<label>-</label>
<StyledLabel onClick={toggleModal}>
<StyledDoc /> Justification
</StyledLabel>
{isOpen && <JustificationModal {...{ request, toggleModal, isRemoval }} />}
</Container>
);
};

export default Party;
Loading
Loading