Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
9 changes: 9 additions & 0 deletions web/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import Loader from "components/Loader";
import Layout from "layout/index";

import ErrorFallback from "./components/ErrorFallback";
import AttachmentDisplay from "./pages/AttachmentDisplay";
import { SentryRoutes } from "./utils/sentry";

const App: React.FC = () => {
Expand Down Expand Up @@ -104,6 +105,14 @@ const App: React.FC = () => {
</Suspense>
}
/>
<Route
path="attachment/*"
element={
<Suspense fallback={<Loader width={"48px"} height={"48px"} />}>
<AttachmentDisplay />
</Suspense>
}
/>
<Route path="*" element={<h1>Page not found</h1>} />
</Route>
</SentryRoutes>
Expand Down
4 changes: 2 additions & 2 deletions web/src/components/DisputePreview/Policies.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,13 @@ export const Policies: React.FC<IPolicies> = ({ disputePolicyURI, courtId, attac
<Container>
<StyledP>Policy documents:</StyledP>
{!isUndefined(attachment) && !isUndefined(attachment.uri) ? (
<StyledInternalLink to={`attachment/?url=${getIpfsUrl(attachment.uri)}`}>
<StyledInternalLink to={`/attachment/?title=${"Case Policy"}&&url=${getIpfsUrl(attachment.uri)}`}>
<StyledPaperclipIcon />
{attachment.label ?? "Attachment"}
</StyledInternalLink>
) : null}
{isUndefined(disputePolicyURI) ? null : (
<StyledInternalLink to={`policy/attachment/?url=${getIpfsUrl(disputePolicyURI)}`}>
<StyledInternalLink to={`/attachment/?title=${"Dispute Policy"}&&url=${getIpfsUrl(disputePolicyURI)}`}>
<StyledPolicyIcon />
Dispute Policy
</StyledInternalLink>
Expand Down
38 changes: 30 additions & 8 deletions web/src/components/EnsureAuth.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,37 @@
import React, { useCallback } from "react";
import styled from "styled-components";

import { useAccount } from "wagmi";

import { useAtlasProvider } from "@kleros/kleros-app";
import { Button } from "@kleros/ui-components-library";

import { errorToast, infoToast, successToast } from "utils/wrapWithToast";

const Container = styled.div`
display: flex;
flex-direction: column;
gap: 16px;
justify-content: center;
align-items: center;
`;

const StyledInfo = styled.p`
margin: 0;
padding: 0;
`;

interface IEnsureAuth {
children: React.ReactElement;
message?: string;
buttonText?: string;
className?: string;
}

const EnsureAuth: React.FC<IEnsureAuth> = ({ children, className }) => {
const EnsureAuth: React.FC<IEnsureAuth> = ({ children, message, buttonText, className }) => {
const { address } = useAccount();
const { isVerified, isSigningIn, authoriseUser } = useAtlasProvider();

const handleClick = useCallback(() => {
infoToast(`Signing in User...`);

Expand All @@ -26,13 +45,16 @@ const EnsureAuth: React.FC<IEnsureAuth> = ({ children, className }) => {
return isVerified ? (
children
) : (
<Button
text="Sign In"
onClick={handleClick}
disabled={isSigningIn || !address}
isLoading={isSigningIn}
{...{ className }}
/>
<Container>
{message ? <StyledInfo>{message}</StyledInfo> : null}
<Button
text={buttonText ?? "Sign In"}
onClick={handleClick}
disabled={isSigningIn || !address}
isLoading={isSigningIn}
{...{ className }}
/>
</Container>
);
};

Expand Down
2 changes: 1 addition & 1 deletion web/src/components/EvidenceCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ const EvidenceCard: React.FC<IEvidenceCard> = ({
</BottomLeftContent>
{fileURI && fileURI !== "-" ? (
<FileLinkContainer>
<StyledInternalLink to={`attachment/?url=${getIpfsUrl(fileURI)}`}>
<StyledInternalLink to={`/attachment/?title=${"Evidence File"}&&url=${getIpfsUrl(fileURI)}`}>
<AttachmentIcon />
<AttachedFileText />
</StyledInternalLink>
Expand Down
41 changes: 38 additions & 3 deletions web/src/context/NewDisputeContext.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import React, { createContext, useState, useContext, useMemo, useCallback } from "react";
import React, { createContext, useState, useContext, useMemo, useCallback, useEffect } from "react";

import { useLocation } from "react-router-dom";
import { Address } from "viem";

import { DEFAULT_CHAIN } from "consts/chains";
import { klerosCoreAddress } from "hooks/contracts/generated";
import { useLocalStorage } from "hooks/useLocalStorage";
import { isEmpty, isUndefined } from "utils/index";

export const MIN_DISPUTE_BATCH_SIZE = 2;

export type Answer = {
id: string;
title: string;
Expand Down Expand Up @@ -58,6 +61,10 @@ interface INewDisputeContext {
setIsSubmittingCase: (isSubmittingCase: boolean) => void;
isPolicyUploading: boolean;
setIsPolicyUploading: (isPolicyUploading: boolean) => void;
isBatchCreation: boolean;
setIsBatchCreation: (isBatchCreation: boolean) => void;
batchSize: number;
setBatchSize: (batchSize?: number) => void;
}

const getInitialDisputeData = (): IDisputeData => ({
Expand Down Expand Up @@ -90,13 +97,26 @@ export const NewDisputeProvider: React.FC<{ children: React.ReactNode }> = ({ ch
const [disputeData, setDisputeData] = useLocalStorage<IDisputeData>("disputeData", initialDisputeData);
const [isSubmittingCase, setIsSubmittingCase] = useState<boolean>(false);
const [isPolicyUploading, setIsPolicyUploading] = useState<boolean>(false);
const [isBatchCreation, setIsBatchCreation] = useState<boolean>(false);
const [batchSize, setBatchSize] = useLocalStorage<number>("disputeBatchSize", MIN_DISPUTE_BATCH_SIZE);

const disputeTemplate = useMemo(() => constructDisputeTemplate(disputeData), [disputeData]);
const location = useLocation();

const resetDisputeData = useCallback(() => {
const freshData = getInitialDisputeData();
setDisputeData(freshData);
}, [setDisputeData]);
setBatchSize(MIN_DISPUTE_BATCH_SIZE);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

useEffect(() => {
// Cleanup function to clear local storage when user leaves the route
if (location.pathname.includes("/resolver") || location.pathname.includes("/attachment")) return;

resetDisputeData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [location.pathname]);

const contextValues = useMemo(
() => ({
Expand All @@ -108,8 +128,23 @@ export const NewDisputeProvider: React.FC<{ children: React.ReactNode }> = ({ ch
setIsSubmittingCase,
isPolicyUploading,
setIsPolicyUploading,
isBatchCreation,
setIsBatchCreation,
batchSize,
setBatchSize,
}),
[disputeData, disputeTemplate, resetDisputeData, isSubmittingCase, isPolicyUploading, setDisputeData]
[
disputeData,
disputeTemplate,
resetDisputeData,
isSubmittingCase,
isPolicyUploading,
setDisputeData,
isBatchCreation,
setIsBatchCreation,
batchSize,
setBatchSize,
]
);

return <NewDisputeContext.Provider value={contextValues}>{children}</NewDisputeContext.Provider>;
Expand Down
2 changes: 1 addition & 1 deletion web/src/hooks/queries/usePopulatedDisputeData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { executeActions } from "@kleros/kleros-sdk/src/dataMappings/executeActio
import { DisputeDetails } from "@kleros/kleros-sdk/src/dataMappings/utils/disputeDetailsTypes";
import { populateTemplate } from "@kleros/kleros-sdk/src/dataMappings/utils/populateTemplate";

import { useGraphqlBatcher } from "context/GraphqlBatcher";
import { DEFAULT_CHAIN } from "consts/chains";
import { useGraphqlBatcher } from "context/GraphqlBatcher";
import { debounceErrorToast } from "utils/debounceErrorToast";
import { isUndefined } from "utils/index";

Expand Down
37 changes: 37 additions & 0 deletions web/src/hooks/queries/useRoundDetailsQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useQuery } from "@tanstack/react-query";

import { REFETCH_INTERVAL, STALE_TIME } from "consts/index";
import { useGraphqlBatcher } from "context/GraphqlBatcher";

import { graphql } from "src/graphql";
import { RoundDetailsQuery } from "src/graphql/graphql";
import { isUndefined } from "src/utils";

const roundDetailsQuery = graphql(`
query RoundDetails($roundID: ID!) {
round(id: $roundID) {
court {
id
}
nbVotes
}
}
`);

export const useRoundDetailsQuery = (disputeId?: string, roundIndex?: number) => {
const isEnabled = !isUndefined(disputeId) && !isUndefined(roundIndex);
const { graphqlBatcher } = useGraphqlBatcher();

return useQuery<RoundDetailsQuery>({
queryKey: [`roundDetailsQuery${disputeId}-${roundIndex}`],
enabled: isEnabled,
refetchInterval: REFETCH_INTERVAL,
staleTime: STALE_TIME,
queryFn: async () =>
await graphqlBatcher.fetch({
id: crypto.randomUUID(),
document: roundDetailsQuery,
variables: { roundID: `${disputeId}-${roundIndex}` },
}),
});
};
8 changes: 7 additions & 1 deletion web/src/hooks/useTransactionBatcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,15 @@ const useTransactionBatcher = (
options: TransactionBatcherOptions = { enabled: true }
) => {
const validatedConfigs = configs ?? [];
const totalValue = validatedConfigs.reduce((sum, config) => {
return sum + (config?.value ?? BigInt(0));
}, BigInt(0));

const {
data: batchConfig,
isLoading,
isError,
error,
} = useSimulateTransactionBatcherBatchSend({
query: {
enabled: !isUndefined(configs) && options.enabled,
Expand All @@ -50,6 +55,7 @@ const useTransactionBatcher = (
validatedConfigs.map((config) => config?.value ?? BigInt(0)),
validatedConfigs.map((config) => encodeFunctionData(config)),
],
value: totalValue,
});
const { writeContractAsync } = useWriteTransactionBatcherBatchSend();

Expand All @@ -58,7 +64,7 @@ const useTransactionBatcher = (
[writeContractAsync]
);

return { executeBatch, batchConfig, isError, isLoading };
return { executeBatch, batchConfig, isError, isLoading, error };
};

export default useTransactionBatcher;
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from "react";
import styled from "styled-components";

import { useNavigate, useLocation, useParams } from "react-router-dom";
import { useNavigate } from "react-router-dom";

import { Button } from "@kleros/ui-components-library";

Expand Down Expand Up @@ -64,24 +64,13 @@ const StyledButton = styled(Button)`
}
`;

const Header: React.FC = () => {
const Header: React.FC<{ title: string }> = ({ title }) => {
const navigate = useNavigate();
const { id } = useParams();
const location = useLocation();

const handleReturn = () => {
navigate(-1);
};

let title = "";
if (location.pathname.includes("policy")) {
title = `Policy - Case #${id}`;
} else if (location.pathname.includes("evidence")) {
title = "Attached File";
} else if (location.pathname.includes("attachment")) {
title = `Attachment - Case #${id}`;
}

return (
<Container>
<TitleContainer>
Expand Down
83 changes: 83 additions & 0 deletions web/src/pages/AttachmentDisplay/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import React, { lazy, Suspense } from "react";
import styled from "styled-components";

import { useSearchParams } from "react-router-dom";

import NewTabIcon from "svgs/icons/new-tab.svg";

import { MAX_WIDTH_LANDSCAPE } from "styles/landscapeStyle";

import { ExternalLink } from "components/ExternalLink";
import Loader from "components/Loader";

import Header from "./Header";

const FileViewer = lazy(() => import("components/FileViewer"));

const Container = styled.div`
width: 100%;
background-color: ${({ theme }) => theme.lightBackground};
padding: calc(24px + (136 - 24) * (min(max(100vw, 375px), 1250px) - 375px) / 875);
padding-top: calc(32px + (80 - 32) * (min(max(100vw, 375px), 1250px) - 375px) / 875);
padding-bottom: calc(76px + (96 - 76) * (min(max(100vw, 375px), 1250px) - 375px) / 875);
max-width: ${MAX_WIDTH_LANDSCAPE};
margin: 0 auto;
`;

const AttachmentContainer = styled.div`
width: 100%;
display: flex;
flex-direction: column;
gap: 8px;
`;

const LoaderContainer = styled.div`
width: 100%;
display: flex;
justify-content: center;
`;

const StyledExternalLink = styled(ExternalLink)`
display: flex;
align-items: center;
align-self: flex-end;
gap: 8px;
`;

const StyledNewTabIcon = styled(NewTabIcon)`
path {
fill: ${({ theme }) => theme.primaryBlue};
}
`;

const AttachmentDisplay: React.FC = () => {
const [searchParams] = useSearchParams();

const url = searchParams.get("url");
const title = searchParams.get("title") ?? "Attachment";
return (
<Container>
<AttachmentContainer>
<Header {...{ title }} />
{url ? (
<>
<StyledExternalLink to={url} rel="noreferrer" target="_blank">
Open in new tab <StyledNewTabIcon />
</StyledExternalLink>
<Suspense
fallback={
<LoaderContainer>
<Loader width={"48px"} height={"48px"} />
</LoaderContainer>
}
>
<FileViewer url={url} />
</Suspense>
</>
) : null}
</AttachmentContainer>
</Container>
);
};

export default AttachmentDisplay;
Loading
Loading