From 5f1bbe0010920b8faaf7c1c9b24afaa10074925d Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Fri, 28 Jun 2024 17:46:49 +0530 Subject: [PATCH 1/5] feat(subgraph): add-additional-fields-in-evidence-entity --- subgraph/core/schema.graphql | 10 ++++++- subgraph/core/src/EvidenceModule.ts | 42 ++++++++++++++++++++++++++++- subgraph/package.json | 2 +- subgraph/utils/index.ts | 26 ++++++++++++++++++ 4 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 subgraph/utils/index.ts diff --git a/subgraph/core/schema.graphql b/subgraph/core/schema.graphql index f1fac66ea..df39fad86 100644 --- a/subgraph/core/schema.graphql +++ b/subgraph/core/schema.graphql @@ -54,6 +54,10 @@ interface Evidence { evidenceGroup: EvidenceGroup! sender: User! timestamp: BigInt! + name: String + description: String + fileURI: String + fileTypeExtension: String } ############ @@ -292,12 +296,16 @@ type ClassicEvidenceGroup implements EvidenceGroup @entity { nextEvidenceIndex: BigInt! } -type ClassicEvidence implements Evidence @entity { +type ClassicEvidence implements Evidence @entity(immutable: true) { id: ID! # classicEvidenceGroup.id-nextEvidenceIndex evidence: String! evidenceGroup: EvidenceGroup! sender: User! timestamp: BigInt! + name: String + description: String + fileURI: String + fileTypeExtension: String } type ClassicContribution implements Contribution @entity { diff --git a/subgraph/core/src/EvidenceModule.ts b/subgraph/core/src/EvidenceModule.ts index bd844f032..4af1cee9a 100644 --- a/subgraph/core/src/EvidenceModule.ts +++ b/subgraph/core/src/EvidenceModule.ts @@ -1,8 +1,10 @@ +import { json, JSONValueKind, log } from "@graphprotocol/graph-ts"; import { Evidence as EvidenceEvent } from "../generated/EvidenceModule/EvidenceModule"; import { ClassicEvidence } from "../generated/schema"; import { ensureClassicEvidenceGroup } from "./entities/ClassicEvidenceGroup"; import { ensureUser } from "./entities/User"; import { ONE } from "./utils"; +import { JSONValueToMaybeString } from "../../utils"; export function handleEvidenceEvent(event: EvidenceEvent): void { const evidenceGroupID = event.params._externalDisputeID.toString(); @@ -10,12 +12,50 @@ export function handleEvidenceEvent(event: EvidenceEvent): void { const evidenceIndex = evidenceGroup.nextEvidenceIndex; evidenceGroup.nextEvidenceIndex = evidenceGroup.nextEvidenceIndex.plus(ONE); evidenceGroup.save(); - const evidence = new ClassicEvidence(`${evidenceGroupID}-${evidenceIndex.toString()}`); + const evidenceId = `${evidenceGroupID}-${evidenceIndex.toString()}`; + const evidence = new ClassicEvidence(evidenceId); const userId = event.params._party.toHexString(); evidence.timestamp = event.block.timestamp; evidence.evidence = event.params._evidence; evidence.evidenceGroup = evidenceGroupID.toString(); evidence.sender = userId; ensureUser(userId); + + let jsonObjValueAndSuccess = json.try_fromString(event.params._evidence); + if (!jsonObjValueAndSuccess.isOk || jsonObjValueAndSuccess.isError) { + log.error(`Error getting json object for evidenceId {}`, [evidenceId]); + evidence.save(); + return; + } + + if (jsonObjValueAndSuccess.value.isNull() || jsonObjValueAndSuccess.value.kind !== JSONValueKind.OBJECT) { + log.error(`Encountered invalid parsed value for evidenceId {}`, [evidenceId]); + evidence.save(); + return; + } + + let jsonObj = jsonObjValueAndSuccess.value.toObject(); + if (!jsonObj) { + log.error(`Error converting json object for evidenceId {}`, [evidenceId]); + evidence.save(); + return; + } + + let name = jsonObj.get("name"); + let description = jsonObj.get("description"); + let fileURI = jsonObj.get("fileURI"); + let fileTypeExtension = jsonObj.get("fileTypeExtension"); + + evidence.name = JSONValueToMaybeString(name); + evidence.description = JSONValueToMaybeString(description); + + if (fileURI) { + evidence.fileURI = JSONValueToMaybeString(fileURI); + } + + if (fileTypeExtension) { + evidence.fileTypeExtension = JSONValueToMaybeString(fileTypeExtension); + } + evidence.save(); } diff --git a/subgraph/package.json b/subgraph/package.json index 529b39724..837a88185 100644 --- a/subgraph/package.json +++ b/subgraph/package.json @@ -1,6 +1,6 @@ { "name": "@kleros/kleros-v2-subgraph", - "version": "0.5.1", + "version": "0.6.2", "license": "MIT", "scripts": { "update:core:arbitrum-sepolia-devnet": "./scripts/update.sh arbitrumSepoliaDevnet arbitrum-sepolia core/subgraph.yaml", diff --git a/subgraph/utils/index.ts b/subgraph/utils/index.ts new file mode 100644 index 000000000..56f65bce5 --- /dev/null +++ b/subgraph/utils/index.ts @@ -0,0 +1,26 @@ +import { JSONValue, JSONValueKind } from "@graphprotocol/graph-ts"; + +export function JSONValueToMaybeString(value: JSONValue | null, _default: string = "-"): string { + // Subgraph considers an empty string to be null and + // the handler crashes when attempting to save the entity. + // This is a security vulnerability because an adversary + // could manually craft an item with missing columns + // and the item would not show up in the UI, passing + // the challenge period unoticed. + // + // We fix this by setting the field manually to a hifen. + if (value == null || value.isNull()) { + return "-"; + } + + switch (value.kind) { + case JSONValueKind.BOOL: + return value.toBool() == true ? "true" : "false"; + case JSONValueKind.STRING: + return value.toString(); + case JSONValueKind.NUMBER: + return value.toBigInt().toHexString(); + default: + return _default; + } +} From 4822433886c5563394bb1907b24e89d66a52fdfd Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Fri, 28 Jun 2024 17:47:45 +0530 Subject: [PATCH 2/5] refactor(contracts): update-evidence-module-natspecs --- contracts/src/arbitration/evidence/EvidenceModule.sol | 2 +- .../src/arbitration/evidence/ModeratedEvidenceModule.sol | 4 ++-- contracts/src/arbitration/interfaces/IEvidence.sol | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/src/arbitration/evidence/EvidenceModule.sol b/contracts/src/arbitration/evidence/EvidenceModule.sol index 49ec09df2..3f7e80c29 100644 --- a/contracts/src/arbitration/evidence/EvidenceModule.sol +++ b/contracts/src/arbitration/evidence/EvidenceModule.sol @@ -64,7 +64,7 @@ contract EvidenceModule is IEvidence, Initializable, UUPSProxiable { /// @dev Submits evidence for a dispute. /// @param _externalDisputeID Unique identifier for this dispute outside Kleros. It's the submitter responsability to submit the right evidence group ID. - /// @param _evidence IPFS path to evidence, example: '/ipfs/Qmarwkf7C9RuzDEJNnarT3WZ7kem5bk8DZAzx78acJjMFH/evidence.json'. + /// @param _evidence Stringified evidence object, example: '{"name" : "Justification", "description" : "Description", "fileURI" : "/ipfs/QmWQV5ZFFhEJiW8Lm7ay2zLxC2XS4wx1b2W7FfdrLMyQQc"}'. function submitEvidence(uint256 _externalDisputeID, string calldata _evidence) external { emit Evidence(_externalDisputeID, msg.sender, _evidence); } diff --git a/contracts/src/arbitration/evidence/ModeratedEvidenceModule.sol b/contracts/src/arbitration/evidence/ModeratedEvidenceModule.sol index 9275b16cf..ded832889 100644 --- a/contracts/src/arbitration/evidence/ModeratedEvidenceModule.sol +++ b/contracts/src/arbitration/evidence/ModeratedEvidenceModule.sol @@ -84,7 +84,7 @@ contract ModeratedEvidenceModule is IArbitrableV2 { /// @param _arbitrator The arbitrator of the contract. /// @param _externalDisputeID Unique identifier for this dispute outside Kleros. It's the submitter responsability to submit the right evidence group ID. /// @param _party The address of the party submiting the evidence. Note that 0x0 refers to evidence not submitted by any party. - /// @param _evidence IPFS path to evidence, example: '/ipfs/Qmarwkf7C9RuzDEJNnarT3WZ7kem5bk8DZAzx78acJjMFH/evidence.json' + /// @param _evidence Stringified evidence object, example: '{"name" : "Justification", "description" : "Description", "fileURI" : "/ipfs/QmWQV5ZFFhEJiW8Lm7ay2zLxC2XS4wx1b2W7FfdrLMyQQc"}'. event ModeratedEvidence( IArbitratorV2 indexed _arbitrator, uint256 indexed _externalDisputeID, @@ -201,7 +201,7 @@ contract ModeratedEvidenceModule is IArbitrableV2 { /// @dev Submits evidence. /// @param _evidenceGroupID Unique identifier of the evidence group the evidence belongs to. It's the submitter responsability to submit the right evidence group ID. - /// @param _evidence IPFS path to evidence, example: '/ipfs/Qmarwkf7C9RuzDEJNnarT3WZ7kem5bk8DZAzx78acJjMFH/evidence.json'. + /// @param _evidence Stringified evidence object, example: '{"name" : "Justification", "description" : "Description", "fileURI" : "/ipfs/QmWQV5ZFFhEJiW8Lm7ay2zLxC2XS4wx1b2W7FfdrLMyQQc"}'. function submitEvidence(uint256 _evidenceGroupID, string calldata _evidence) external payable { // Optimization opportunity: map evidenceID to an incremental index that can be safely assumed to be less than a small uint. bytes32 evidenceID = keccak256(abi.encodePacked(_evidenceGroupID, _evidence)); diff --git a/contracts/src/arbitration/interfaces/IEvidence.sol b/contracts/src/arbitration/interfaces/IEvidence.sol index 9f1699bc9..a43c1af65 100644 --- a/contracts/src/arbitration/interfaces/IEvidence.sol +++ b/contracts/src/arbitration/interfaces/IEvidence.sol @@ -7,6 +7,6 @@ interface IEvidence { /// @dev To be raised when evidence is submitted. Should point to the resource (evidences are not to be stored on chain due to gas considerations). /// @param _externalDisputeID Unique identifier for this dispute outside Kleros. It's the submitter responsability to submit the right external dispute ID. /// @param _party The address of the party submiting the evidence. Note that 0x0 refers to evidence not submitted by any party. - /// @param _evidence IPFS path to evidence, example: '/ipfs/Qmarwkf7C9RuzDEJNnarT3WZ7kem5bk8DZAzx78acJjMFH/evidence.json' + /// @param _evidence Stringified evidence object, example: '{"name" : "Justification", "description" : "Description", "fileURI" : "/ipfs/QmWQV5ZFFhEJiW8Lm7ay2zLxC2XS4wx1b2W7FfdrLMyQQc"}'. event Evidence(uint256 indexed _externalDisputeID, address indexed _party, string _evidence); } From 7cf0b1ead9295026beba26786a826f3d86d04e45 Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Fri, 28 Jun 2024 17:48:36 +0530 Subject: [PATCH 3/5] refactor(web): support-new-evidence-refactors --- web/src/components/EvidenceCard.tsx | 21 +++++------ web/src/hooks/queries/useEvidences.ts | 6 ++- .../Evidence/SubmitEvidenceModal.tsx | 37 +++++++------------ .../Cases/CaseDetails/Evidence/index.tsx | 9 ++++- 4 files changed, 35 insertions(+), 38 deletions(-) diff --git a/web/src/components/EvidenceCard.tsx b/web/src/components/EvidenceCard.tsx index 1d86607ea..4b298fa7d 100644 --- a/web/src/components/EvidenceCard.tsx +++ b/web/src/components/EvidenceCard.tsx @@ -9,11 +9,12 @@ import { Card } from "@kleros/ui-components-library"; import AttachmentIcon from "svgs/icons/attachment.svg"; -import { useIPFSQuery } from "hooks/useIPFSQuery"; import { formatDate } from "utils/date"; import { getIpfsUrl } from "utils/getIpfsUrl"; import { shortenAddress } from "utils/shortenAddress"; +import { type Evidence } from "src/graphql/graphql"; + import { landscapeStyle } from "styles/landscapeStyle"; import { responsiveSize } from "styles/responsiveSize"; @@ -136,24 +137,20 @@ const AttachedFileText: React.FC = () => ( ); -interface IEvidenceCard { - evidence: string; +interface IEvidenceCard extends Pick { sender: string; index: number; - timestamp: string; } -const EvidenceCard: React.FC = ({ evidence, sender, index, timestamp }) => { - const { data } = useIPFSQuery(evidence); - +const EvidenceCard: React.FC = ({ evidence, sender, index, timestamp, name, description, fileURI }) => { return ( #{index}: - {data ? ( + {name && description ? ( <> -

{data.name}

- {data.description} +

{name}

+ {description} ) : (

{evidence}

@@ -165,8 +162,8 @@ const EvidenceCard: React.FC = ({ evidence, sender, index, timest

{shortenAddress(sender)}

{formatDate(Number(timestamp), true)} - {data && typeof data.fileURI !== "undefined" && ( - + {fileURI && ( + diff --git a/web/src/hooks/queries/useEvidences.ts b/web/src/hooks/queries/useEvidences.ts index f22a573e4..1070c6b08 100644 --- a/web/src/hooks/queries/useEvidences.ts +++ b/web/src/hooks/queries/useEvidences.ts @@ -15,6 +15,10 @@ const evidencesQuery = graphql(` id } timestamp + name + description + fileURI + fileTypeExtension } } `); @@ -23,7 +27,7 @@ export const useEvidences = (evidenceGroup?: string) => { const isEnabled = evidenceGroup !== undefined; const { graphqlBatcher } = useGraphqlBatcher(); - return useQuery({ + return useQuery({ queryKey: ["refetchOnBlock", `evidencesQuery${evidenceGroup}`], enabled: isEnabled, queryFn: async () => diff --git a/web/src/pages/Cases/CaseDetails/Evidence/SubmitEvidenceModal.tsx b/web/src/pages/Cases/CaseDetails/Evidence/SubmitEvidenceModal.tsx index 30d86eb31..63984acd0 100644 --- a/web/src/pages/Cases/CaseDetails/Evidence/SubmitEvidenceModal.tsx +++ b/web/src/pages/Cases/CaseDetails/Evidence/SubmitEvidenceModal.tsx @@ -64,23 +64,18 @@ const SubmitEvidenceModal: React.FC<{ const submitEvidence = useCallback(async () => { setIsSending(true); - toast.info("Uploading to IPFS", toastOptions); - const formData = await constructEvidence(message, file); - uploadFormDataToIPFS(formData) - .then(async (res) => { - const response = await res.json(); - if (res.status === 200 && walletClient) { - const cid = response["cids"][0]; - const { request } = await simulateEvidenceModuleSubmitEvidence(wagmiConfig, { - args: [BigInt(evidenceGroup), cid], - }); - await wrapWithToast(async () => await walletClient.writeContract(request), publicClient).then(() => { - setMessage(""); - close(); - }); - } + const evidenceJSON = await constructEvidence(message, file); + + const { request } = await simulateEvidenceModuleSubmitEvidence(wagmiConfig, { + args: [BigInt(evidenceGroup), JSON.stringify(evidenceJSON)], + }); + + if (!walletClient) return; + await wrapWithToast(async () => await walletClient.writeContract(request), publicClient) + .then(() => { + setMessage(""); + close(); }) - .catch() .finally(() => setIsSending(false)); }, [publicClient, wagmiConfig, walletClient, close, evidenceGroup, file, message, setIsSending]); @@ -101,9 +96,10 @@ const SubmitEvidenceModal: React.FC<{ ); }; -const constructEvidence = async (msg: string, file?: File): Promise => { +const constructEvidence = async (msg: string, file?: File) => { let fileURI: string | undefined = undefined; if (file) { + toast.info("Uploading to IPFS", toastOptions); const fileFormData = new FormData(); fileFormData.append("data", file, file.name); fileURI = await uploadFormDataToIPFS(fileFormData).then(async (res) => { @@ -111,12 +107,7 @@ const constructEvidence = async (msg: string, file?: File): Promise => return response["cids"][0]; }); } - const formData = new FormData(); - const evidenceFile = new File([JSON.stringify({ name: "Evidence", description: msg, fileURI })], "evidence.json", { - type: "text/plain", - }); - formData.append("data", evidenceFile, evidenceFile.name); - return formData; + return { name: "Evidence", description: msg, fileURI }; }; export default SubmitEvidenceModal; diff --git a/web/src/pages/Cases/CaseDetails/Evidence/index.tsx b/web/src/pages/Cases/CaseDetails/Evidence/index.tsx index 896d937e5..5f5897e1c 100644 --- a/web/src/pages/Cases/CaseDetails/Evidence/index.tsx +++ b/web/src/pages/Cases/CaseDetails/Evidence/index.tsx @@ -62,8 +62,13 @@ const Evidence: React.FC<{ arbitrable?: `0x${string}` }> = ({ arbitrable }) => { /> {data ? ( - data.evidences.map(({ key, evidence, sender, timestamp }, i) => ( - + data.evidences.map(({ evidence, sender, timestamp, name, description, fileURI }, i) => ( + )) ) : ( From 2d23ed76a4b2fba1d405c7c3b44d0cd481cff14b Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Tue, 2 Jul 2024 17:37:44 +0530 Subject: [PATCH 4/5] feat(subgraph): evidence-fulltext-search --- subgraph/core-neo/subgraph.yaml | 3 +++ subgraph/core-university/subgraph.yaml | 3 +++ subgraph/core/schema.graphql | 12 ++++++++++++ subgraph/core/src/EvidenceModule.ts | 2 ++ subgraph/core/subgraph.yaml | 3 +++ subgraph/package.json | 2 +- 6 files changed, 24 insertions(+), 1 deletion(-) diff --git a/subgraph/core-neo/subgraph.yaml b/subgraph/core-neo/subgraph.yaml index 0b07fb708..e98508cc4 100644 --- a/subgraph/core-neo/subgraph.yaml +++ b/subgraph/core-neo/subgraph.yaml @@ -1,6 +1,9 @@ specVersion: 0.0.4 schema: file: ./schema.graphql +features: + - fullTextSearch + dataSources: - kind: ethereum name: KlerosCore diff --git a/subgraph/core-university/subgraph.yaml b/subgraph/core-university/subgraph.yaml index 2970fb804..03c1f6c72 100644 --- a/subgraph/core-university/subgraph.yaml +++ b/subgraph/core-university/subgraph.yaml @@ -1,6 +1,9 @@ specVersion: 0.0.4 schema: file: ./schema.graphql +features: + - fullTextSearch + dataSources: - kind: ethereum name: KlerosCore diff --git a/subgraph/core/schema.graphql b/subgraph/core/schema.graphql index df39fad86..597914610 100644 --- a/subgraph/core/schema.graphql +++ b/subgraph/core/schema.graphql @@ -52,7 +52,9 @@ interface Evidence { id: ID! evidence: String! evidenceGroup: EvidenceGroup! + evidenceIndex: String! sender: User! + senderAddress: String! timestamp: BigInt! name: String description: String @@ -300,7 +302,9 @@ type ClassicEvidence implements Evidence @entity(immutable: true) { id: ID! # classicEvidenceGroup.id-nextEvidenceIndex evidence: String! evidenceGroup: EvidenceGroup! + evidenceIndex: String! sender: User! + senderAddress: String! timestamp: BigInt! name: String description: String @@ -319,3 +323,11 @@ type ClassicContribution implements Contribution @entity { choice: BigInt! rewardWithdrawn: Boolean! } + +type _Schema_ + @fulltext( + name: "evidenceSearch" + language: en + algorithm: rank + include: [{ entity: "ClassicEvidence", fields: [{ name: "name" }, { name: "description" },{ name: "senderAddress"},{ name: "evidenceIndex"}] }] + ) \ No newline at end of file diff --git a/subgraph/core/src/EvidenceModule.ts b/subgraph/core/src/EvidenceModule.ts index 4af1cee9a..a4dfd8e70 100644 --- a/subgraph/core/src/EvidenceModule.ts +++ b/subgraph/core/src/EvidenceModule.ts @@ -14,11 +14,13 @@ export function handleEvidenceEvent(event: EvidenceEvent): void { evidenceGroup.save(); const evidenceId = `${evidenceGroupID}-${evidenceIndex.toString()}`; const evidence = new ClassicEvidence(evidenceId); + evidence.evidenceIndex = evidenceIndex.plus(ONE).toString(); const userId = event.params._party.toHexString(); evidence.timestamp = event.block.timestamp; evidence.evidence = event.params._evidence; evidence.evidenceGroup = evidenceGroupID.toString(); evidence.sender = userId; + evidence.senderAddress = userId; ensureUser(userId); let jsonObjValueAndSuccess = json.try_fromString(event.params._evidence); diff --git a/subgraph/core/subgraph.yaml b/subgraph/core/subgraph.yaml index 8bd10e01a..f431077d9 100644 --- a/subgraph/core/subgraph.yaml +++ b/subgraph/core/subgraph.yaml @@ -1,6 +1,9 @@ specVersion: 0.0.4 schema: file: ./schema.graphql +features: + - fullTextSearch + dataSources: - kind: ethereum name: KlerosCore diff --git a/subgraph/package.json b/subgraph/package.json index 837a88185..0f62c24d0 100644 --- a/subgraph/package.json +++ b/subgraph/package.json @@ -1,6 +1,6 @@ { "name": "@kleros/kleros-v2-subgraph", - "version": "0.6.2", + "version": "0.7.0", "license": "MIT", "scripts": { "update:core:arbitrum-sepolia-devnet": "./scripts/update.sh arbitrumSepoliaDevnet arbitrum-sepolia core/subgraph.yaml", From 8637c5e67753d7a6efd4f5f8238eb6367c159aef Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Tue, 2 Jul 2024 17:39:19 +0530 Subject: [PATCH 5/5] feat(web): evidence-search --- web/src/assets/svgs/icons/arrow-down.svg | 10 +++ web/src/components/EvidenceCard.tsx | 14 ---- web/src/hooks/queries/useEvidences.ts | 62 ++++++++++----- .../CaseDetails/Evidence/EvidenceSearch.tsx | 69 +++++++++++++++++ .../Cases/CaseDetails/Evidence/index.tsx | 75 ++++++++++++------- 5 files changed, 167 insertions(+), 63 deletions(-) create mode 100644 web/src/assets/svgs/icons/arrow-down.svg create mode 100644 web/src/pages/Cases/CaseDetails/Evidence/EvidenceSearch.tsx diff --git a/web/src/assets/svgs/icons/arrow-down.svg b/web/src/assets/svgs/icons/arrow-down.svg new file mode 100644 index 000000000..97be1a02a --- /dev/null +++ b/web/src/assets/svgs/icons/arrow-down.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/web/src/components/EvidenceCard.tsx b/web/src/components/EvidenceCard.tsx index 4b298fa7d..cf30bb0bc 100644 --- a/web/src/components/EvidenceCard.tsx +++ b/web/src/components/EvidenceCard.tsx @@ -62,20 +62,6 @@ const BottomShade = styled.div` } `; -const StyledA = styled.a` - display: flex; - margin-left: auto; - gap: ${responsiveSize(5, 6)}; - ${landscapeStyle( - () => css` - > svg { - width: 16px; - fill: ${({ theme }) => theme.primaryBlue}; - } - ` - )} -`; - const AccountContainer = styled.div` display: flex; flex-direction: row; diff --git a/web/src/hooks/queries/useEvidences.ts b/web/src/hooks/queries/useEvidences.ts index 1070c6b08..17143657b 100644 --- a/web/src/hooks/queries/useEvidences.ts +++ b/web/src/hooks/queries/useEvidences.ts @@ -3,38 +3,60 @@ import { useQuery } from "@tanstack/react-query"; import { useGraphqlBatcher } from "context/GraphqlBatcher"; import { graphql } from "src/graphql"; -import { EvidencesQuery } from "src/graphql/graphql"; +import { EvidenceDetailsFragment, EvidencesQuery } from "src/graphql/graphql"; export type { EvidencesQuery }; +export const evidenceFragment = graphql(` + fragment EvidenceDetails on ClassicEvidence { + id + evidence + sender { + id + } + timestamp + name + description + fileURI + fileTypeExtension + evidenceIndex + } +`); + const evidencesQuery = graphql(` query Evidences($evidenceGroupID: String) { - evidences(where: { evidenceGroup: $evidenceGroupID }, orderBy: timestamp, orderDirection: desc) { - id - evidence - sender { - id - } - timestamp - name - description - fileURI - fileTypeExtension + evidences(where: { evidenceGroup: $evidenceGroupID }, orderBy: timestamp, orderDirection: asc) { + ...EvidenceDetails } } `); -export const useEvidences = (evidenceGroup?: string) => { +const evidenceSearchQuery = graphql(` + query EvidenceSearch($keywords: String!, $evidenceGroupID: String) { + evidenceSearch(text: $keywords, where: { evidenceGroup: $evidenceGroupID }) { + ...EvidenceDetails + } + } +`); + +export const useEvidences = (evidenceGroup?: string, keywords?: string) => { const isEnabled = evidenceGroup !== undefined; const { graphqlBatcher } = useGraphqlBatcher(); - return useQuery({ - queryKey: ["refetchOnBlock", `evidencesQuery${evidenceGroup}`], + const document = keywords ? evidenceSearchQuery : evidencesQuery; + return useQuery<{ evidences: EvidenceDetailsFragment[] }>({ + queryKey: [ + "refetchOnBlock", + keywords ? `evidenceSearchQuery${evidenceGroup}-${keywords}` : `evidencesQuery${evidenceGroup}`, + ], enabled: isEnabled, - queryFn: async () => - await graphqlBatcher.fetch({ + queryFn: async () => { + const result = await graphqlBatcher.fetch({ id: crypto.randomUUID(), - document: evidencesQuery, - variables: { evidenceGroupID: evidenceGroup?.toString() }, - }), + document: document, + variables: { evidenceGroupID: evidenceGroup?.toString(), keywords: keywords }, + }); + + return keywords ? { evidences: [...result.evidenceSearch] } : result; + }, }); }; diff --git a/web/src/pages/Cases/CaseDetails/Evidence/EvidenceSearch.tsx b/web/src/pages/Cases/CaseDetails/Evidence/EvidenceSearch.tsx new file mode 100644 index 000000000..7aa1a3671 --- /dev/null +++ b/web/src/pages/Cases/CaseDetails/Evidence/EvidenceSearch.tsx @@ -0,0 +1,69 @@ +import React, { useState } from "react"; +import styled from "styled-components"; + +import { useAccount } from "wagmi"; + +import { Button, Searchbar } from "@kleros/ui-components-library"; + +import { isUndefined } from "src/utils"; + +import { responsiveSize } from "styles/responsiveSize"; + +import { EnsureChain } from "components/EnsureChain"; + +import SubmitEvidenceModal from "./SubmitEvidenceModal"; + +const SearchContainer = styled.div` + width: 100%; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: ${responsiveSize(16, 28)}; +`; + +const StyledSearchBar = styled(Searchbar)` + min-width: 220px; + flex: 1; +`; + +const StyledButton = styled(Button)` + align-self: flex-end; +`; + +interface IEvidenceSearch { + search?: string; + setSearch: (search: string) => void; + evidenceGroup?: bigint; +} + +const EvidenceSearch: React.FC = ({ search, setSearch, evidenceGroup }) => { + const [isModalOpen, setIsModalOpen] = useState(false); + const { address } = useAccount(); + + return ( + <> + {!isUndefined(evidenceGroup) && ( + setIsModalOpen(false)} {...{ evidenceGroup }} /> + )} + + + setSearch(e.target.value)} + value={search} + /> + + + setIsModalOpen(true)} + /> + + + + ); +}; + +export default EvidenceSearch; diff --git a/web/src/pages/Cases/CaseDetails/Evidence/index.tsx b/web/src/pages/Cases/CaseDetails/Evidence/index.tsx index 5f5897e1c..8c088edfc 100644 --- a/web/src/pages/Cases/CaseDetails/Evidence/index.tsx +++ b/web/src/pages/Cases/CaseDetails/Evidence/index.tsx @@ -1,23 +1,22 @@ -import React, { useState } from "react"; +import React, { useCallback, useRef, useState } from "react"; import styled from "styled-components"; import { useParams } from "react-router-dom"; -import { useAccount } from "wagmi"; +import { useDebounce } from "react-use"; -import { Button, Searchbar } from "@kleros/ui-components-library"; +import { Button } from "@kleros/ui-components-library"; -import { isUndefined } from "utils/index"; +import DownArrow from "svgs/icons/arrow-down.svg"; import { useEvidenceGroup } from "queries/useEvidenceGroup"; import { useEvidences } from "queries/useEvidences"; import { responsiveSize } from "styles/responsiveSize"; -import { EnsureChain } from "components/EnsureChain"; import EvidenceCard from "components/EvidenceCard"; import { SkeletonEvidenceCard } from "components/StyledSkeleton"; -import SubmitEvidenceModal from "./SubmitEvidenceModal"; +import EvidenceSearch from "./EvidenceSearch"; const Container = styled.div` width: 100%; @@ -29,43 +28,61 @@ const Container = styled.div` padding: ${responsiveSize(16, 32)}; `; -const StyledButton = styled(Button)` - align-self: flex-end; -`; - const StyledLabel = styled.label` display: flex; margin-top: 16px; font-size: 16px; `; +const ScrollButton = styled(Button)` + align-self: flex-end; + background-color: transparent; + padding: 0; + flex-direction: row-reverse; + margin: 0 0 18px; + gap: 8px; + .button-text { + color: ${({ theme }) => theme.primaryBlue}; + font-weight: 400; + } + .button-svg { + margin: 0; + } + :focus, + :hover { + background-color: transparent; + } +`; + const Evidence: React.FC<{ arbitrable?: `0x${string}` }> = ({ arbitrable }) => { - const [isModalOpen, setIsModalOpen] = useState(false); const { id } = useParams(); const { data: evidenceGroup } = useEvidenceGroup(id, arbitrable); - const { data } = useEvidences(evidenceGroup?.toString()); - const { address } = useAccount(); + const ref = useRef(null); + const [search, setSearch] = useState(); + const [debouncedSearch, setDebouncedSearch] = useState(); + + const { data } = useEvidences(evidenceGroup?.toString(), debouncedSearch); + + useDebounce(() => setDebouncedSearch(search), 500, [search]); + + const scrollToLatest = useCallback(() => { + if (!ref.current) return; + const latestEvidence = ref.current.lastElementChild; + + if (!latestEvidence) return; + + latestEvidence.scrollIntoView({ behavior: "smooth" }); + }, [ref]); return ( - - {!isUndefined(evidenceGroup) && ( - setIsModalOpen(false)} {...{ evidenceGroup }} /> - )} - - - setIsModalOpen(true)} - /> - + + + {data ? ( - data.evidences.map(({ evidence, sender, timestamp, name, description, fileURI }, i) => ( + data.evidences.map(({ evidence, sender, timestamp, name, description, fileURI, evidenceIndex }) => (