Skip to content

Commit

Permalink
Merge pull request #42 from kleros/feat/removal-challenge-justificati…
Browse files Browse the repository at this point in the history
…on-display

Feat/removal challenge justification display
  • Loading branch information
jaybuidl authored Jun 20, 2024
2 parents cd37f92 + 9b02617 commit 18eca3a
Show file tree
Hide file tree
Showing 22 changed files with 476 additions and 72 deletions.
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;
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]);

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;
};

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

0 comments on commit 18eca3a

Please sign in to comment.