feat: 💨

This commit is contained in:
codytseng
2025-08-12 10:50:27 +08:00
parent 9969ab2414
commit c6a5157fe7
10 changed files with 236 additions and 258 deletions

View File

@@ -32,6 +32,14 @@ export default function Image({
const [imageUrl, setImageUrl] = useState(url) const [imageUrl, setImageUrl] = useState(url)
const [tried, setTried] = useState(new Set()) const [tried, setTried] = useState(new Set())
useEffect(() => {
setImageUrl(url)
setIsLoading(true)
setHasError(false)
setDisplayBlurHash(true)
setTried(new Set())
}, [url])
useEffect(() => { useEffect(() => {
if (blurHash) { if (blurHash) {
const { numX, numY } = decodeBlurHashSize(blurHash) const { numX, numY } = decodeBlurHashSize(blurHash)

View File

@@ -19,7 +19,6 @@ import { toast } from 'sonner'
import EmojiPickerDialog from '../EmojiPickerDialog' import EmojiPickerDialog from '../EmojiPickerDialog'
import Mentions from './Mentions' import Mentions from './Mentions'
import PollEditor from './PollEditor' import PollEditor from './PollEditor'
import { usePostEditor } from './PostEditorProvider'
import PostOptions from './PostOptions' import PostOptions from './PostOptions'
import PostTextarea, { TPostTextareaHandle } from './PostTextarea' import PostTextarea, { TPostTextareaHandle } from './PostTextarea'
import SendOnlyToSwitch from './SendOnlyToSwitch' import SendOnlyToSwitch from './SendOnlyToSwitch'
@@ -37,13 +36,12 @@ export default function PostContent({
const { t } = useTranslation() const { t } = useTranslation()
const { pubkey, publish, checkLogin } = useNostr() const { pubkey, publish, checkLogin } = useNostr()
const { addReplies } = useReply() const { addReplies } = useReply()
const { uploadingFiles, setUploadingFiles } = usePostEditor()
const [text, setText] = useState('') const [text, setText] = useState('')
const textareaRef = useRef<TPostTextareaHandle>(null) const textareaRef = useRef<TPostTextareaHandle>(null)
const [posting, setPosting] = useState(false) const [posting, setPosting] = useState(false)
const [uploadProgress, setUploadProgress] = useState<number | null>(null) const [uploadProgresses, setUploadProgresses] = useState<
const [uploadFileName, setUploadFileName] = useState<string | null>(null) { file: File; progress: number; cancel: () => void }[]
const cancelRef = useRef<(() => void) | null>(null) >([])
const [showMoreOptions, setShowMoreOptions] = useState(false) const [showMoreOptions, setShowMoreOptions] = useState(false)
const [addClientTag, setAddClientTag] = useState(false) const [addClientTag, setAddClientTag] = useState(false)
const [specifiedRelayUrls, setSpecifiedRelayUrls] = useState<string[] | undefined>(undefined) const [specifiedRelayUrls, setSpecifiedRelayUrls] = useState<string[] | undefined>(undefined)
@@ -61,7 +59,7 @@ export default function PostContent({
!!pubkey && !!pubkey &&
!!text && !!text &&
!posting && !posting &&
!uploadingFiles && !uploadProgresses.length &&
(!isPoll || pollCreateData.options.filter((option) => !!option.trim()).length >= 2) (!isPoll || pollCreateData.options.filter((option) => !!option.trim()).length >= 2)
useEffect(() => { useEffect(() => {
@@ -161,6 +159,20 @@ export default function PostContent({
setIsPoll((prev) => !prev) setIsPoll((prev) => !prev)
} }
const handleUploadStart = (file: File, cancel: () => void) => {
setUploadProgresses((prev) => [...prev, { file, progress: 0, cancel }])
}
const handleUploadProgress = (file: File, progress: number) => {
setUploadProgresses((prev) =>
prev.map((item) => (item.file === file ? { ...item, progress } : item))
)
}
const handleUploadEnd = (file: File) => {
setUploadProgresses((prev) => prev.filter((item) => item.file !== file))
}
return ( return (
<div className="space-y-2"> <div className="space-y-2">
{parentEvent && ( {parentEvent && (
@@ -178,17 +190,9 @@ export default function PostContent({
parentEvent={parentEvent} parentEvent={parentEvent}
onSubmit={() => post()} onSubmit={() => post()}
className={isPoll ? 'min-h-20' : 'min-h-52'} className={isPoll ? 'min-h-20' : 'min-h-52'}
onUploadStart={(file) => { onUploadStart={handleUploadStart}
setUploadFileName(file.name) onUploadProgress={handleUploadProgress}
setUploadProgress(0) onUploadEnd={handleUploadEnd}
}}
onUploadProgress={(p) => setUploadProgress(p)}
onUploadEnd={() => {
setUploadProgress(null)
setUploadFileName(null)
cancelRef.current = null
}}
onProvideCancel={(cancel) => (cancelRef.current = cancel)}
/> />
{isPoll && ( {isPoll && (
<PollEditor <PollEditor
@@ -197,6 +201,33 @@ export default function PostContent({
setIsPoll={setIsPoll} setIsPoll={setIsPoll}
/> />
)} )}
{uploadProgresses.length > 0 &&
uploadProgresses.map(({ file, progress, cancel }, index) => (
<div key={`${file.name}-${index}`} className="mt-2 flex items-end gap-2">
<div className="min-w-0 flex-1">
<div className="truncate text-xs text-muted-foreground mb-1">
{file.name ?? t('Uploading...')}
</div>
<div className="h-0.5 w-full rounded-full bg-muted overflow-hidden">
<div
className="h-full bg-primary transition-[width] duration-200 ease-out"
style={{ width: `${progress}%` }}
/>
</div>
</div>
<button
type="button"
onClick={() => {
cancel?.()
handleUploadEnd(file)
}}
className="text-muted-foreground hover:text-foreground"
title={t('Cancel')}
>
<X className="h-4 w-4" />
</button>
</div>
))}
{!isPoll && ( {!isPoll && (
<SendOnlyToSwitch <SendOnlyToSwitch
parentEvent={parentEvent} parentEvent={parentEvent}
@@ -204,52 +235,18 @@ export default function PostContent({
setSpecifiedRelayUrls={setSpecifiedRelayUrls} setSpecifiedRelayUrls={setSpecifiedRelayUrls}
/> />
)} )}
{uploadProgress !== null && (
<div className="mt-2 flex items-center gap-2">
<div className="min-w-0 flex-1">
<div className="truncate text-xs text-muted-foreground mb-1">
{uploadFileName ?? t('Uploading...')}
</div>
<div className="h-0.5 w-full rounded-full bg-muted overflow-hidden">
<div
className="h-full bg-primary transition-[width] duration-200 ease-out"
style={{ width: `${uploadProgress}%` }}
/>
</div>
</div>
<button
type="button"
onClick={() => cancelRef.current?.()}
className="p-1 text-muted-foreground hover:text-foreground"
title={t('Cancel')}
>
<X className="h-4 w-4" />
</button>
</div>
)}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<Uploader <Uploader
onUploadSuccess={({ url }) => { onUploadSuccess={({ url }) => {
textareaRef.current?.appendText(url, true) textareaRef.current?.appendText(url, true)
}} }}
onUploadingChange={(uploading) => onUploadStart={handleUploadStart}
setUploadingFiles((prev) => (uploading ? prev + 1 : prev - 1)) onUploadEnd={handleUploadEnd}
} onProgress={handleUploadProgress}
onUploadStart={(file) => {
setUploadFileName(file.name)
setUploadProgress(0)
}}
onUploadEnd={() => {
setUploadProgress(null)
setUploadFileName(null)
cancelRef.current = null
}}
onProgress={(p) => setUploadProgress(p)}
onProvideCancel={(cancel) => (cancelRef.current = cancel)}
accept="image/*,video/*,audio/*" accept="image/*,video/*,audio/*"
> >
<Button variant="ghost" size="icon" disabled={uploadingFiles > 0}> <Button variant="ghost" size="icon">
<ImageUp /> <ImageUp />
</Button> </Button>
</Uploader> </Uploader>

View File

@@ -1,26 +0,0 @@
import { createContext, Dispatch, SetStateAction, useContext, useState } from 'react'
type TPostEditorContext = {
uploadingFiles: number
setUploadingFiles: Dispatch<SetStateAction<number>>
}
const PostEditorContext = createContext<TPostEditorContext | undefined>(undefined)
export const usePostEditor = () => {
const context = useContext(PostEditorContext)
if (!context) {
throw new Error('usePostEditor must be used within a PostEditorProvider')
}
return context
}
export function PostEditorProvider({ children }: { children: React.ReactNode }) {
const [uploadingFiles, setUploadingFiles] = useState(0)
return (
<PostEditorContext.Provider value={{ uploadingFiles, setUploadingFiles }}>
{children}
</PostEditorContext.Provider>
)
}

View File

@@ -1,7 +1,6 @@
import mediaUpload from '@/services/media-upload.service' import mediaUpload from '@/services/media-upload.service'
import { Extension } from '@tiptap/core' import { Extension } from '@tiptap/core'
import { EditorView } from '@tiptap/pm/view' import { EditorView } from '@tiptap/pm/view'
import { Slice } from '@tiptap/pm/model'
import { Plugin, TextSelection } from 'prosemirror-state' import { Plugin, TextSelection } from 'prosemirror-state'
const DRAGOVER_CLASS_LIST = [ const DRAGOVER_CLASS_LIST = [
@@ -13,12 +12,9 @@ const DRAGOVER_CLASS_LIST = [
] ]
export interface ClipboardAndDropHandlerOptions { export interface ClipboardAndDropHandlerOptions {
onUploadStart?: (file: File) => void onUploadStart?: (file: File, cancel: () => void) => void
onUploadSuccess?: (file: File, result: any) => void onUploadEnd?: (file: File) => void
onUploadError?: (file: File, error: any) => void
onUploadEnd?: () => void
onUploadProgress?: (file: File, progress: number) => void onUploadProgress?: (file: File, progress: number) => void
onProvideCancel?: (cancel: () => void) => void
} }
export const ClipboardAndDropHandler = Extension.create<ClipboardAndDropHandlerOptions>({ export const ClipboardAndDropHandler = Extension.create<ClipboardAndDropHandlerOptions>({
@@ -57,7 +53,7 @@ export const ClipboardAndDropHandler = Extension.create<ClipboardAndDropHandlerO
return true return true
} }
}, },
handleDrop(view: EditorView, event: DragEvent, _slice: Slice, _moved: boolean) { handleDrop(view: EditorView, event: DragEvent) {
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
view.dom.classList.remove(...DRAGOVER_CLASS_LIST) view.dom.classList.remove(...DRAGOVER_CLASS_LIST)
@@ -68,7 +64,7 @@ export const ClipboardAndDropHandler = Extension.create<ClipboardAndDropHandlerO
) )
if (!mediaFiles.length) return false if (!mediaFiles.length) return false
uploadFile(view, mediaFiles, options) uploadFiles(view, mediaFiles, options)
return true return true
}, },
handlePaste(view, event) { handlePaste(view, event) {
@@ -82,7 +78,7 @@ export const ClipboardAndDropHandler = Extension.create<ClipboardAndDropHandlerO
) { ) {
const file = item.getAsFile() const file = item.getAsFile()
if (file) { if (file) {
uploadFile(view, [file], options) uploadFiles(view, [file], options)
handled = true handled = true
} }
} else if (item.kind === 'string' && item.type === 'text/plain') { } else if (item.kind === 'string' && item.type === 'text/plain') {
@@ -116,16 +112,22 @@ export const ClipboardAndDropHandler = Extension.create<ClipboardAndDropHandlerO
} }
}) })
async function uploadFile( async function uploadFiles(
view: EditorView, view: EditorView,
files: File[], files: File[],
options: ClipboardAndDropHandlerOptions options: ClipboardAndDropHandlerOptions
) { ) {
const abortControllers = new Map<File, AbortController>()
files.forEach((file) => {
const abortController = new AbortController()
abortControllers.set(file, abortController)
options.onUploadStart?.(file, () => abortController.abort())
})
await new Promise((resolve) => setTimeout(resolve, 10000))
for (const file of files) { for (const file of files) {
const name = file.name const name = file.name
options.onUploadStart?.(file)
const placeholder = `[Uploading "${name}"...]` const placeholder = `[Uploading "${name}"...]`
const uploadingNode = view.state.schema.text(placeholder) const uploadingNode = view.state.schema.text(placeholder)
const hardBreakNode = view.state.schema.nodes.hardBreak.create() const hardBreakNode = view.state.schema.nodes.hardBreak.create()
@@ -133,16 +135,15 @@ async function uploadFile(
tr = tr.insert(tr.selection.from, hardBreakNode) tr = tr.insert(tr.selection.from, hardBreakNode)
view.dispatch(tr) view.dispatch(tr)
const abortController = new AbortController() const abortController = abortControllers.get(file)
options.onProvideCancel?.(() => abortController.abort())
mediaUpload mediaUpload
.upload(file, { .upload(file, {
onProgress: (p) => options.onUploadProgress?.(file, p), onProgress: (p) => options.onUploadProgress?.(file, p),
signal: abortController.signal signal: abortController?.signal
}) })
.then((result) => { .then((result) => {
options.onUploadSuccess?.(file, result) options.onUploadEnd?.(file)
const urlNode = view.state.schema.text(result.url) const urlNode = view.state.schema.text(result.url)
const tr = view.state.tr const tr = view.state.tr
@@ -175,11 +176,10 @@ async function uploadFile(
insertTr.setSelection(TextSelection.near(insertTr.doc.resolve(newPos))) insertTr.setSelection(TextSelection.near(insertTr.doc.resolve(newPos)))
view.dispatch(insertTr) view.dispatch(insertTr)
} }
options.onUploadEnd?.()
}) })
.catch((error) => { .catch((error) => {
console.error('Upload failed:', error) console.error('Upload failed:', error)
options.onUploadError?.(file, error) options.onUploadEnd?.(file)
const tr = view.state.tr const tr = view.state.tr
let didReplace = false let didReplace = false
@@ -200,7 +200,6 @@ async function uploadFile(
if (didReplace) { if (didReplace) {
view.dispatch(tr) view.dispatch(tr)
} }
options.onUploadEnd?.()
throw error throw error
}) })
} }

View File

@@ -13,7 +13,6 @@ import { EditorContent, useEditor } from '@tiptap/react'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { Dispatch, forwardRef, SetStateAction, useImperativeHandle } from 'react' import { Dispatch, forwardRef, SetStateAction, useImperativeHandle } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { usePostEditor } from '../PostEditorProvider'
import { ClipboardAndDropHandler } from './ClipboardAndDropHandler' import { ClipboardAndDropHandler } from './ClipboardAndDropHandler'
import CustomMention from './CustomMention' import CustomMention from './CustomMention'
import Preview from './Preview' import Preview from './Preview'
@@ -33,12 +32,13 @@ const PostTextarea = forwardRef<
parentEvent?: Event parentEvent?: Event
onSubmit?: () => void onSubmit?: () => void
className?: string className?: string
onUploadStart?: (file: File) => void onUploadStart?: (file: File, cancel: () => void) => void
onUploadProgress?: (progress: number, file: File) => void onUploadProgress?: (file: File, progress: number) => void
onUploadEnd?: () => void onUploadEnd?: (file: File) => void
onProvideCancel?: (cancel: () => void) => void
} }
>(({ >(
(
{
text = '', text = '',
setText, setText,
defaultContent, defaultContent,
@@ -47,11 +47,11 @@ const PostTextarea = forwardRef<
className, className,
onUploadStart, onUploadStart,
onUploadProgress, onUploadProgress,
onUploadEnd, onUploadEnd
onProvideCancel },
}, ref) => { ref
) => {
const { t } = useTranslation() const { t } = useTranslation()
const { setUploadingFiles } = usePostEditor()
const editor = useEditor({ const editor = useEditor({
extensions: [ extensions: [
Document, Document,
@@ -60,21 +60,18 @@ const PostTextarea = forwardRef<
History, History,
HardBreak, HardBreak,
Placeholder.configure({ Placeholder.configure({
placeholder: t('Write something...') + ' (' + t('Paste or drop media files to upload') + ')' placeholder:
t('Write something...') + ' (' + t('Paste or drop media files to upload') + ')'
}), }),
CustomMention.configure({ CustomMention.configure({
suggestion suggestion
}), }),
ClipboardAndDropHandler.configure({ ClipboardAndDropHandler.configure({
onUploadStart: (file) => { onUploadStart: (file, cancel) => {
setUploadingFiles((prev) => prev + 1) onUploadStart?.(file, cancel)
onUploadStart?.(file)
}, },
onUploadSuccess: () => setUploadingFiles((prev) => prev - 1), onUploadEnd: (file) => onUploadEnd?.(file),
onUploadError: () => setUploadingFiles((prev) => prev - 1), onUploadProgress: (file, p) => onUploadProgress?.(file, p)
onUploadEnd: () => onUploadEnd?.(),
onUploadProgress: (file, p) => onUploadProgress?.(p, file),
onProvideCancel: (cancel) => onProvideCancel?.(cancel)
}) })
], ],
editorProps: { editorProps: {
@@ -151,6 +148,7 @@ const PostTextarea = forwardRef<
</TabsContent> </TabsContent>
</Tabs> </Tabs>
) )
}) }
)
PostTextarea.displayName = 'PostTextarea' PostTextarea.displayName = 'PostTextarea'
export default PostTextarea export default PostTextarea

View File

@@ -1,62 +1,57 @@
import mediaUpload from '@/services/media-upload.service' import mediaUpload, { UPLOAD_ABORTED_ERROR_MSG } from '@/services/media-upload.service'
import { useRef } from 'react' import { useRef } from 'react'
import { toast } from 'sonner' import { toast } from 'sonner'
export default function Uploader({ export default function Uploader({
children, children,
onUploadSuccess, onUploadSuccess,
onUploadingChange,
onUploadStart, onUploadStart,
onUploadEnd, onUploadEnd,
onProgress, onProgress,
onProvideCancel,
className, className,
accept = 'image/*' accept = 'image/*'
}: { }: {
children: React.ReactNode children: React.ReactNode
onUploadSuccess: ({ url, tags }: { url: string; tags: string[][] }) => void onUploadSuccess: ({ url, tags }: { url: string; tags: string[][] }) => void
onUploadingChange?: (uploading: boolean) => void onUploadStart?: (file: File, cancel: () => void) => void
onUploadStart?: (file: File) => void onUploadEnd?: (file: File) => void
onUploadEnd?: () => void onProgress?: (file: File, progress: number) => void
onProgress?: (progress: number, file: File) => void
onProvideCancel?: (cancel: () => void) => void
className?: string className?: string
accept?: string accept?: string
}) { }) {
const fileInputRef = useRef<HTMLInputElement>(null) const fileInputRef = useRef<HTMLInputElement>(null)
const abortControllerRef = useRef<AbortController | null>(null)
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => { const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
if (!event.target.files) return if (!event.target.files) return
onUploadingChange?.(true) const abortControllerMap = new Map<File, AbortController>()
try {
for (const file of event.target.files) { for (const file of event.target.files) {
abortControllerRef.current = new AbortController() const abortController = new AbortController()
const cancel = () => abortControllerRef.current?.abort() abortControllerMap.set(file, abortController)
onProvideCancel?.(cancel) onUploadStart?.(file, () => abortController.abort())
onUploadStart?.(file) }
for (const file of event.target.files) {
try {
const abortController = abortControllerMap.get(file)
const result = await mediaUpload.upload(file, { const result = await mediaUpload.upload(file, {
onProgress: (p) => onProgress?.(p, file), onProgress: (p) => onProgress?.(file, p),
signal: abortControllerRef.current.signal signal: abortController?.signal
}) })
onUploadSuccess(result) onUploadSuccess(result)
abortControllerRef.current = null onUploadEnd?.(file)
onUploadEnd?.()
}
} catch (error) { } catch (error) {
console.error('Error uploading file', error) console.error('Error uploading file', error)
const message = (error as Error).message const message = (error as Error).message
if (message !== 'Upload aborted') { if (message !== UPLOAD_ABORTED_ERROR_MSG) {
toast.error(`Failed to upload file: ${message}`) toast.error(`Failed to upload file: ${message}`)
} }
if (fileInputRef.current) { if (fileInputRef.current) {
fileInputRef.current.value = '' fileInputRef.current.value = ''
} }
abortControllerRef.current = null onUploadEnd?.(file)
onUploadEnd?.() }
} finally {
onUploadingChange?.(false)
} }
} }

View File

@@ -18,7 +18,6 @@ import postEditor from '@/services/post-editor.service'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { Dispatch, useMemo } from 'react' import { Dispatch, useMemo } from 'react'
import PostContent from './PostContent' import PostContent from './PostContent'
import { PostEditorProvider } from './PostEditorProvider'
import Title from './Title' import Title from './Title'
export default function PostEditor({ export default function PostEditor({
@@ -36,13 +35,11 @@ export default function PostEditor({
const content = useMemo(() => { const content = useMemo(() => {
return ( return (
<PostEditorProvider>
<PostContent <PostContent
defaultContent={defaultContent} defaultContent={defaultContent}
parentEvent={parentEvent} parentEvent={parentEvent}
close={() => setOpen(false)} close={() => setOpen(false)}
/> />
</PostEditorProvider>
) )
}, []) }, [])

View File

@@ -12,7 +12,7 @@ export default function ProfileBanner({
className?: string className?: string
}) { }) {
const defaultBanner = useMemo(() => generateImageByPubkey(pubkey), [pubkey]) const defaultBanner = useMemo(() => generateImageByPubkey(pubkey), [pubkey])
const [bannerUrl, setBannerUrl] = useState(banner || defaultBanner) const [bannerUrl, setBannerUrl] = useState(banner ?? defaultBanner)
useEffect(() => { useEffect(() => {
if (banner) { if (banner) {

View File

@@ -128,7 +128,8 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
<div className="relative bg-cover bg-center rounded-lg mb-2"> <div className="relative bg-cover bg-center rounded-lg mb-2">
<Uploader <Uploader
onUploadSuccess={onBannerUploadSuccess} onUploadSuccess={onBannerUploadSuccess}
onUploadingChange={(uploading) => setTimeout(() => setUploadingBanner(uploading), 50)} onUploadStart={() => setUploadingBanner(true)}
onUploadEnd={() => setUploadingBanner(false)}
className="w-full relative cursor-pointer" className="w-full relative cursor-pointer"
> >
<ProfileBanner <ProfileBanner
@@ -146,7 +147,8 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
</Uploader> </Uploader>
<Uploader <Uploader
onUploadSuccess={onAvatarUploadSuccess} onUploadSuccess={onAvatarUploadSuccess}
onUploadingChange={(uploading) => setTimeout(() => setUploadingAvatar(uploading), 50)} onUploadStart={() => setUploadingAvatar(true)}
onUploadEnd={() => setUploadingAvatar(false)}
className="w-24 h-24 absolute bottom-0 left-4 translate-y-1/2 border-4 border-background cursor-pointer rounded-full" className="w-24 h-24 absolute bottom-0 left-4 translate-y-1/2 border-4 border-background cursor-pointer rounded-full"
> >
<Avatar className="w-full h-full"> <Avatar className="w-full h-full">

View File

@@ -10,6 +10,8 @@ type UploadOptions = {
signal?: AbortSignal signal?: AbortSignal
} }
export const UPLOAD_ABORTED_ERROR_MSG = 'Upload aborted'
class MediaUploadService { class MediaUploadService {
static instance: MediaUploadService static instance: MediaUploadService
@@ -55,7 +57,7 @@ class MediaUploadService {
} }
if (options?.signal?.aborted) { if (options?.signal?.aborted) {
throw new Error('Upload aborted') throw new Error(UPLOAD_ABORTED_ERROR_MSG)
} }
options?.onProgress?.(0) options?.onProgress?.(0)
@@ -115,6 +117,9 @@ class MediaUploadService {
} }
private async uploadByNip96(service: string, file: File, options?: UploadOptions) { private async uploadByNip96(service: string, file: File, options?: UploadOptions) {
if (options?.signal?.aborted) {
throw new Error(UPLOAD_ABORTED_ERROR_MSG)
}
let uploadUrl = this.nip96ServiceUploadUrlMap.get(service) let uploadUrl = this.nip96ServiceUploadUrlMap.get(service)
if (!uploadUrl) { if (!uploadUrl) {
const response = await fetch(`${service}/.well-known/nostr/nip96.json`) const response = await fetch(`${service}/.well-known/nostr/nip96.json`)
@@ -133,6 +138,9 @@ class MediaUploadService {
this.nip96ServiceUploadUrlMap.set(service, uploadUrl) this.nip96ServiceUploadUrlMap.set(service, uploadUrl)
} }
if (options?.signal?.aborted) {
throw new Error(UPLOAD_ABORTED_ERROR_MSG)
}
const formData = new FormData() const formData = new FormData()
formData.append('file', file) formData.append('file', file)
@@ -148,10 +156,10 @@ class MediaUploadService {
const handleAbort = () => { const handleAbort = () => {
try { try {
xhr.abort() xhr.abort()
} catch (_) { } catch {
// ignore // ignore
} }
reject(new Error('Upload aborted')) reject(new Error(UPLOAD_ABORTED_ERROR_MSG))
} }
if (options?.signal) { if (options?.signal) {
if (options.signal.aborted) { if (options.signal.aborted) {