@@ -55,14 +58,16 @@ export function DownloadInProgress({
return (
<>
- You are about to start downloading {filesInfo.length} files.
+ You are downloading {pluralize(filesInfo.length, 'file', 'files')}.
>
)
@@ -78,7 +83,8 @@ export function ReadyToDownload({
return (
<>
- You are about to start downloading {filesInfo.length} files.
+ You are about to start downloading{' '}
+ {pluralize(filesInfo.length, 'file', 'files')}.
@@ -106,9 +112,7 @@ export function PasswordEntry({
return (
<>
-
- {errorMessage || 'This download requires a password.'}
-
+
This download requires a password.
+ {errorMessage && (
+
+ {errorMessage}
+
+ )}
>
)
}
diff --git a/src/components/InputLabel.tsx b/src/components/InputLabel.tsx
index 1f383a6..8a5549e 100644
--- a/src/components/InputLabel.tsx
+++ b/src/components/InputLabel.tsx
@@ -2,11 +2,17 @@ import React from 'react'
export default function InputLabel({
children,
+ hasError = false,
}: {
children: React.ReactNode
+ hasError?: boolean
}): JSX.Element {
return (
-
+
{children}
)
diff --git a/src/components/PasswordField.tsx b/src/components/PasswordField.tsx
index 0175104..3223696 100644
--- a/src/components/PasswordField.tsx
+++ b/src/components/PasswordField.tsx
@@ -1,7 +1,12 @@
import React, { useCallback } from 'react'
import InputLabel from './InputLabel'
-export default function PasswordField(props: {
+export default function PasswordField({
+ value,
+ onChange,
+ isRequired = false,
+ isInvalid = false,
+}: {
value: string
onChange: (v: string) => void
isRequired?: boolean
@@ -9,24 +14,24 @@ export default function PasswordField(props: {
}): JSX.Element {
const handleChange = useCallback(
function (e: React.ChangeEvent): void {
- props.onChange(e.target.value)
+ onChange(e.target.value)
},
- [props.onChange],
+ [onChange],
)
return (
-
- {props.isRequired ? 'Password' : 'Password (optional)'}
+
+ {isRequired ? 'Password' : 'Password (optional)'}
diff --git a/src/components/StartButton.tsx b/src/components/StartButton.tsx
index 7beb376..800cb3c 100644
--- a/src/components/StartButton.tsx
+++ b/src/components/StartButton.tsx
@@ -8,7 +8,7 @@ export default function StartButton({
return (
Start
diff --git a/src/components/UnlockButton.tsx b/src/components/UnlockButton.tsx
index 39c54ba..b185c7a 100644
--- a/src/components/UnlockButton.tsx
+++ b/src/components/UnlockButton.tsx
@@ -4,11 +4,11 @@ export default function UnlockButton({
onClick,
}: {
onClick?: React.MouseEventHandler
-}): React.ReactElement {
+}): JSX.Element {
return (
Unlock
diff --git a/src/hooks/useDownloader.ts b/src/hooks/useDownloader.ts
index 0c0b1b2..68db586 100644
--- a/src/hooks/useDownloader.ts
+++ b/src/hooks/useDownloader.ts
@@ -37,7 +37,6 @@ export function useDownloader(uploaderPeerID: string): {
bytesDownloaded: number
} {
const peer = useWebRTC()
- const [password, setPassword] = useState('')
const [dataConnection, setDataConnection] = useState(
null,
)
@@ -50,11 +49,11 @@ export function useDownloader(uploaderPeerID: string): {
((message: z.infer) => void) | null
>(null)
const [isConnected, setIsConnected] = useState(false)
- const [isDownloading, setDownloading] = useState(false)
+ const [isPasswordRequired, setIsPasswordRequired] = useState(false)
+ const [isDownloading, setIsDownloading] = useState(false)
const [isDone, setDone] = useState(false)
const [bytesDownloaded, setBytesDownloaded] = useState(0)
const [errorMessage, setErrorMessage] = useState(null)
- const [isPasswordRequired, setIsPasswordRequired] = useState(false)
useEffect(() => {
const conn = peer.connect(uploaderPeerID, { reliable: true })
@@ -79,6 +78,7 @@ export function useDownloader(uploaderPeerID: string): {
switch (message.type) {
case MessageType.PasswordRequired:
setIsPasswordRequired(true)
+ if (message.errorMessage) setErrorMessage(message.errorMessage)
break
case MessageType.Info:
setFilesInfo(message.files)
@@ -100,7 +100,7 @@ export function useDownloader(uploaderPeerID: string): {
const handleClose = () => {
setDataConnection(null)
setIsConnected(false)
- setDownloading(false)
+ setIsDownloading(false)
}
const handleError = (err: Error) => {
@@ -124,7 +124,7 @@ export function useDownloader(uploaderPeerID: string): {
conn.off('close', handleClose)
peer.off('error', handleError)
}
- }, [peer, password, uploaderPeerID])
+ }, [peer, uploaderPeerID])
const submitPassword = useCallback(
(pass: string) => {
@@ -134,12 +134,12 @@ export function useDownloader(uploaderPeerID: string): {
password: pass,
} as z.infer)
},
- [dataConnection, password],
+ [dataConnection],
)
const startDownload = useCallback(() => {
if (!filesInfo || !dataConnection) return
- setDownloading(true)
+ setIsDownloading(true)
const fileStreamByPath: Record<
string,
@@ -213,8 +213,23 @@ export function useDownloader(uploaderPeerID: string): {
}, [dataConnection, filesInfo])
const stopDownload = useCallback(() => {
- // TODO(@kern): Implement me
- }, [])
+ // TODO(@kern): Continue here with stop / pause logic
+ if (dataConnection) {
+ dataConnection.send({ type: MessageType.Pause } as z.infer<
+ typeof Message
+ >)
+ dataConnection.close()
+ }
+ setIsDownloading(false)
+ setDone(false)
+ setBytesDownloaded(0)
+ setErrorMessage(null)
+ // fileStreams.forEach((stream) => stream.cancel())
+ // fileStreams.length = 0
+ // Object.values(fileStreamByPath).forEach((stream) => stream.cancel())
+ // Object.keys(fileStreamByPath).forEach((key) => delete fileStreamByPath[key])
+ // }, [dataConnection, fileStreams, fileStreamByPath])
+ }, [dataConnection])
return {
filesInfo,
diff --git a/src/hooks/useUploaderConnections.ts b/src/hooks/useUploaderConnections.ts
index 27b944d..3a64610 100644
--- a/src/hooks/useUploaderConnections.ts
+++ b/src/hooks/useUploaderConnections.ts
@@ -57,13 +57,19 @@ export function useUploaderConnections(
const message = decodeMessage(data)
switch (message.type) {
case MessageType.RequestInfo: {
+ const newConnectionState = {
+ browserName: message.browserName,
+ browserVersion: message.browserVersion,
+ osName: message.osName,
+ osVersion: message.osVersion,
+ mobileVendor: message.mobileVendor,
+ mobileModel: message.mobileModel,
+ }
+
if (password) {
- // TODO(@kern): Check password
const request: Message = {
- type: MessageType.Error,
- error: 'Invalid password',
+ type: MessageType.PasswordRequired,
}
-
conn.send(request)
updateConnection((draft) => {
@@ -73,13 +79,8 @@ export function useUploaderConnections(
return {
...draft,
- status: UploaderConnectionStatus.InvalidPassword,
- browserName: message.browserName,
- browserVersion: message.browserVersion,
- osName: message.osName,
- osVersion: message.osVersion,
- mobileVendor: message.mobileVendor,
- mobileModel: message.mobileModel,
+ ...newConnectionState,
+ status: UploaderConnectionStatus.Authenticating,
}
})
@@ -93,13 +94,8 @@ export function useUploaderConnections(
return {
...draft,
+ ...newConnectionState,
status: UploaderConnectionStatus.Paused,
- browserName: message.browserName,
- browserVersion: message.browserVersion,
- osName: message.osName,
- osVersion: message.osVersion,
- mobileVendor: message.mobileVendor,
- mobileModel: message.mobileModel,
}
})
@@ -120,6 +116,57 @@ export function useUploaderConnections(
break
}
+ case MessageType.UsePassword: {
+ const { password: submittedPassword } = message
+ if (submittedPassword === password) {
+ updateConnection((draft) => {
+ if (
+ draft.status !== UploaderConnectionStatus.Authenticating
+ ) {
+ return draft
+ }
+
+ return {
+ ...draft,
+ status: UploaderConnectionStatus.Paused,
+ }
+ })
+
+ const fileInfo = files.map((f) => ({
+ fileName: getFileName(f),
+ size: f.size,
+ type: f.type,
+ }))
+
+ const request: Message = {
+ type: MessageType.Info,
+ files: fileInfo,
+ }
+
+ conn.send(request)
+ } else {
+ updateConnection((draft) => {
+ if (
+ draft.status !== UploaderConnectionStatus.Authenticating
+ ) {
+ return draft
+ }
+
+ return {
+ ...draft,
+ status: UploaderConnectionStatus.InvalidPassword,
+ }
+ })
+
+ const request: Message = {
+ type: MessageType.PasswordRequired,
+ errorMessage: 'Invalid password',
+ }
+ conn.send(request)
+ }
+ break
+ }
+
case MessageType.Start: {
const fileName = message.fileName
let offset = message.offset
diff --git a/src/messages.ts b/src/messages.ts
index 826202a..b7b3033 100644
--- a/src/messages.ts
+++ b/src/messages.ts
@@ -57,6 +57,7 @@ export const ErrorMessage = z.object({
export const PasswordRequiredMessage = z.object({
type: z.literal(MessageType.PasswordRequired),
+ errorMessage: z.string().optional(),
})
export const UsePasswordMessage = z.object({
diff --git a/src/types.ts b/src/types.ts
index 17a5ef2..973331e 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -7,6 +7,7 @@ export enum UploaderConnectionStatus {
Paused = 'PAUSED',
Uploading = 'UPLOADING',
Done = 'DONE',
+ Authenticating = 'AUTHENTICATING',
InvalidPassword = 'INVALID_PASSWORD',
Closed = 'CLOSED',
}
diff --git a/src/utils/pluralize.ts b/src/utils/pluralize.ts
new file mode 100644
index 0000000..9b6b499
--- /dev/null
+++ b/src/utils/pluralize.ts
@@ -0,0 +1,7 @@
+export function pluralize(
+ count: number,
+ singular: string,
+ plural: string,
+): string {
+ return `${count} ${count === 1 ? singular : plural}`
+}