Skip to content

Commit

Permalink
feat(web): removal-and-challenge-justifications-display
Browse files Browse the repository at this point in the history
  • Loading branch information
Harman-singh-waraich committed Jun 19, 2024
1 parent 3ba2584 commit 4c83305
Show file tree
Hide file tree
Showing 18 changed files with 382 additions and 23 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;
56 changes: 56 additions & 0 deletions web/src/components/HistoryDisplay/Party/JustificationDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
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";

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

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;
51 changes: 51 additions & 0 deletions web/src/components/HistoryDisplay/Party/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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);
return (
<Container>
<label>by</label>
<AliasDisplay address={request?.challenger?.id ?? ""} />
<label>-</label>
<StyledLabel onClick={toggleModal}>
<StyledDoc /> Justification
</StyledLabel>
{isOpen && <JustificationModal {...{ request, toggleModal, isRemoval }} />}
</Container>
);
};

export default Party;
27 changes: 18 additions & 9 deletions web/src/components/HistoryDisplay/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Card, CustomTimeline, _TimelineItem1 } from "@kleros/ui-components-library";
import React from "react";
import styled, { Theme, useTheme } from "styled-components";
import styled, { css, Theme, useTheme } from "styled-components";
import Header from "./Header";
import { useItemRequests } from "queries/useRequestsQuery";
import { RequestDetailsFragment, Ruling, Status } from "src/graphql/graphql";
import { formatDate } from "utils/date";
import { shortenAddress } from "utils/shortenAddress";
import CheckIcon from "assets/svgs/icons/check-circle-outline.svg";
import ClosedIcon from "assets/svgs/icons/close-circle.svg";
import { HistorySkeletonCard } from "../StyledSkeleton";
import Party from "./Party";
import { landscapeStyle } from "styles/landscapeStyle";

const Container = styled(Card)`
display: flex;
Expand All @@ -23,6 +24,14 @@ const Container = styled(Card)`
const StyledTimeline = styled(CustomTimeline)`
width: 100%;
margin-bottom: 30px;
.party-wrapper {
max-height: none;
${landscapeStyle(
() => css`
max-height: 32px;
`
)}
}
`;

interface IHistory {
Expand Down Expand Up @@ -51,7 +60,7 @@ const History: React.FC<IHistory> = ({ itemId, isItem }) => {
const constructItemsFromRequest = (
request: RequestDetailsFragment,
theme: Theme,
isItem?: Boolean
isItem?: boolean
): _TimelineItem1[] => {
const historyItems: _TimelineItem1[] = [];

Expand All @@ -66,10 +75,10 @@ const constructItemsFromRequest = (

if (request.disputed && request.challenger) {
historyItems.push({
title: `${isItem ? "Submission" : "Registration"} Challenged`,
title: `${isItem ? "Submission" : "Registration"} Challenged - Case ${request.disputeID}`,
variant: theme.secondaryPurple,
party: `- Case ${request.disputeID} by ${shortenAddress(request.challenger.id)}`,
subtitle: formatDate(request.submissionTime),
party: <Party {...{ request }} />,
subtitle: formatDate(request.challengeTime),
rightSided: true,
});
}
Expand All @@ -92,14 +101,14 @@ const constructItemsFromRequest = (
variant: theme.primaryBlue,
subtitle: formatDate(request.submissionTime),
rightSided: true,
party: `- By ${shortenAddress(request.requester.id)}`,
party: <Party {...{ request }} isRemoval />,
});

if (request.disputed && request.challenger) {
historyItems.push({
title: "Removal Challenged",
title: `Removal Challenged - Case ${request.disputeID}`,
variant: theme.secondaryPurple,
party: `- Case #${request.disputeID} by ${shortenAddress(request.challenger.id)}`,
party: <Party {...{ request }} />,
subtitle: formatDate(request.submissionTime),
rightSided: true,
});
Expand Down
Loading

0 comments on commit 4c83305

Please sign in to comment.