diff --git a/package.json b/package.json index 1e07b3c..91d9d39 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "url": "https://github.com/kern/filepizza/issues" }, "dependencies": { + "@tanstack/react-query": "^5.55.2", "autoprefixer": "^10.4.20", "debug": "^4.3.6", "express": "^4.19.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0f443df..4160c1e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + '@tanstack/react-query': + specifier: ^5.55.2 + version: 5.55.2(react@18.3.1) autoprefixer: specifier: ^10.4.20 version: 10.4.20(postcss@8.4.44) @@ -2314,6 +2317,19 @@ packages: tslib: 2.7.0 dev: false + /@tanstack/query-core@5.55.2: + resolution: {integrity: sha512-WfuUx06sw/V3R2tRRQf4KZj0g4Q5EHrTdwJmCUovMFuHWVzDvLd5U+B7d5Pk8ni5w5SGpUuzGuQv3nN8xo1QbQ==} + dev: false + + /@tanstack/react-query@5.55.2(react@18.3.1): + resolution: {integrity: sha512-PkbEmO64nphWPhufkE9aJ+iEScg8tNNCykYlH7vDwb2R6G8uJC+HTJgXc2n8cRaJe6ETVIeQLFFESVd+2N/tww==} + peerDependencies: + react: ^18 || ^19 + dependencies: + '@tanstack/query-core': 5.55.2 + react: 18.3.1 + dev: false + /@types/body-parser@1.19.5: resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} dependencies: diff --git a/src/components/Uploader.tsx b/src/components/Uploader.tsx index d1d3155..2805954 100644 --- a/src/components/Uploader.tsx +++ b/src/components/Uploader.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react' import { UploadedFile } from '../types' import { useWebRTC } from './WebRTCProvider' -import useFetch from 'use-http' +import { useQuery } from '@tanstack/react-query' import Peer, { DataConnection } from 'peerjs' import { decodeMessage, Message, MessageType } from '../messages' import QRCode from 'react-qr-code' @@ -11,6 +11,7 @@ import Loading from './Loading' import ProgressBar from './ProgressBar' import useClipboard from '../hooks/useClipboard' import InputLabel from './InputLabel' +import { useUploaderChannelRenewal } from '../hooks/useUploaderChannelRenewal' enum UploaderConnectionStatus { Pending = 'PENDING', @@ -32,68 +33,62 @@ type UploaderConnection = { mobileModel?: string uploadingFullPath?: string uploadingOffset?: number + completedFiles: number + totalFiles: number + currentFileProgress: number } // TODO(@kern): Use better values -const RENEW_INTERVAL = 5000 // 20 minutes const MAX_CHUNK_SIZE = 10 * 1024 * 1024 // 10 Mi const QR_CODE_SIZE = 128 +function generateURL(slug: string): string { + const hostPrefix = + window.location.protocol + + '//' + + window.location.hostname + + (['80', '443'].includes(window.location.port) + ? '' + : ':' + window.location.port) + return `${hostPrefix}/download/${slug}` +} + function useUploaderChannel(uploaderPeerID: string): { loading: boolean error: Error | null - longSlug: string - shortSlug: string + longSlug: string | undefined + shortSlug: string | undefined + longURL: string | undefined + shortURL: string | undefined } { - const { loading, error, data } = useFetch( - '/api/create', - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ uploaderPeerID }), + const { isLoading, error, data } = useQuery({ + queryKey: ['uploaderChannel', uploaderPeerID], + queryFn: async () => { + const response = await fetch('/api/create', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ uploaderPeerID }), + }) + if (!response.ok) { + throw new Error('Network response was not ok') + } + return response.json() }, - [uploaderPeerID], - ) + }) - if (!data) { - return { loading, error, longSlug: null, shortSlug: null } - } + const longURL = data?.longSlug ? generateURL(data.longSlug) : undefined + const shortURL = data?.shortSlug ? generateURL(data.shortSlug) : undefined return { - loading: false, - error: null, - longSlug: data.longSlug, - shortSlug: data.shortSlug, + loading: isLoading, + error: error as Error | null, + longSlug: data?.longSlug, + shortSlug: data?.shortSlug, + longURL, + shortURL, } } -function useUploaderChannelRenewal(shortSlug: string): void { - const { post } = useFetch('/api/renew') - - useEffect(() => { - let timeout = null - - const run = (): void => { - timeout = setTimeout(() => { - post({ slug: shortSlug }) - .then(() => { - run() - }) - .catch((err) => { - console.error(err) - run() - }) - }, RENEW_INTERVAL) - } - - run() - - return () => { - clearTimeout(timeout) - } - }, [shortSlug]) -} - function validateOffset( files: UploadedFile[], fullPath: string, @@ -117,10 +112,13 @@ function useUploaderConnections( useEffect(() => { peer.on('connection', (conn: DataConnection) => { - let sendChunkTimeout: number | null = null + let sendChunkTimeout: NodeJS.Timeout | null = null const newConn = { status: UploaderConnectionStatus.Pending, dataConnection: conn, + completedFiles: 0, + totalFiles: files.length, + currentFileProgress: 0, } setConnections((conns) => [...conns, newConn]) @@ -211,6 +209,7 @@ function useUploaderConnections( draft.status = UploaderConnectionStatus.Uploading draft.uploadingFullPath = fullPath draft.uploadingOffset = offset + draft.currentFileProgress = offset / file.size }) const sendNextChunk = () => { @@ -229,11 +228,14 @@ function useUploaderConnections( updateConnection((draft) => { offset = end draft.uploadingOffset = end + draft.currentFileProgress = end / file.size if (final) { draft.status = UploaderConnectionStatus.Paused + draft.completedFiles += 1 + draft.currentFileProgress = 0 } else { - sendChunkTimeout = window.setTimeout(() => { + sendChunkTimeout = setTimeout(() => { sendNextChunk() }, 0) } @@ -300,32 +302,43 @@ function useUploaderConnections( return connections } +function CopyableInput({ label, value }: { label: string; value: string }) { + const { hasCopied, onCopy } = useClipboard(value) + + return ( +