11 changed files with 196 additions and 39 deletions
@ -0,0 +1,35 @@ |
|||||||
|
import { Label } from '@/components/ui/label' |
||||||
|
import { |
||||||
|
Select, |
||||||
|
SelectContent, |
||||||
|
SelectItem, |
||||||
|
SelectTrigger, |
||||||
|
SelectValue |
||||||
|
} from '@/components/ui/select' |
||||||
|
import { DEFAULT_NIP_96_SERVICE, NIP_96_SERVICE } from '@/constants' |
||||||
|
import { simplifyUrl } from '@/lib/url' |
||||||
|
import { useMediaUploadService } from '@/providers/MediaUploadServiceProvider' |
||||||
|
import { useTranslation } from 'react-i18next' |
||||||
|
|
||||||
|
export default function MediaUploadServiceSetting() { |
||||||
|
const { t } = useTranslation() |
||||||
|
const { service, updateService } = useMediaUploadService() |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="space-y-2"> |
||||||
|
<Label htmlFor="media-upload-service-select">{t('Media upload service')}</Label> |
||||||
|
<Select defaultValue={DEFAULT_NIP_96_SERVICE} value={service} onValueChange={updateService}> |
||||||
|
<SelectTrigger id="media-upload-service-select" className="w-48"> |
||||||
|
<SelectValue /> |
||||||
|
</SelectTrigger> |
||||||
|
<SelectContent> |
||||||
|
{NIP_96_SERVICE.map((url) => ( |
||||||
|
<SelectItem key={url} value={url}> |
||||||
|
{simplifyUrl(url)} |
||||||
|
</SelectItem> |
||||||
|
))} |
||||||
|
</SelectContent> |
||||||
|
</Select> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,18 @@ |
|||||||
|
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' |
||||||
|
import { forwardRef } from 'react' |
||||||
|
import { useTranslation } from 'react-i18next' |
||||||
|
import MediaUploadServiceSetting from './MediaUploadServiceSetting' |
||||||
|
|
||||||
|
const PostSettingsPage = forwardRef(({ index }: { index?: number }, ref) => { |
||||||
|
const { t } = useTranslation() |
||||||
|
|
||||||
|
return ( |
||||||
|
<SecondaryPageLayout ref={ref} index={index} title={t('Wallet')}> |
||||||
|
<div className="px-4 pt-2 space-y-4"> |
||||||
|
<MediaUploadServiceSetting /> |
||||||
|
</div> |
||||||
|
</SecondaryPageLayout> |
||||||
|
) |
||||||
|
}) |
||||||
|
PostSettingsPage.displayName = 'PostSettingsPage' |
||||||
|
export default PostSettingsPage |
||||||
@ -0,0 +1,84 @@ |
|||||||
|
import { simplifyUrl } from '@/lib/url' |
||||||
|
import storage from '@/services/local-storage.service' |
||||||
|
import { createContext, useContext, useState } from 'react' |
||||||
|
import { z } from 'zod' |
||||||
|
import { useNostr } from './NostrProvider' |
||||||
|
|
||||||
|
type TMediaUploadServiceContext = { |
||||||
|
service: string |
||||||
|
updateService: (service: string) => void |
||||||
|
upload: (file: File) => Promise<{ url: string; tags: string[][] }> |
||||||
|
} |
||||||
|
|
||||||
|
const MediaUploadServiceContext = createContext<TMediaUploadServiceContext | undefined>(undefined) |
||||||
|
|
||||||
|
export const useMediaUploadService = () => { |
||||||
|
const context = useContext(MediaUploadServiceContext) |
||||||
|
if (!context) { |
||||||
|
throw new Error('useMediaUploadService must be used within MediaUploadServiceProvider') |
||||||
|
} |
||||||
|
return context |
||||||
|
} |
||||||
|
|
||||||
|
const ServiceUploadUrlMap = new Map<string, string | undefined>() |
||||||
|
|
||||||
|
export function MediaUploadServiceProvider({ children }: { children: React.ReactNode }) { |
||||||
|
const { signHttpAuth } = useNostr() |
||||||
|
const [service, setService] = useState(storage.getMediaUploadService()) |
||||||
|
|
||||||
|
const updateService = (newService: string) => { |
||||||
|
setService(newService) |
||||||
|
storage.setMediaUploadService(newService) |
||||||
|
} |
||||||
|
|
||||||
|
const upload = async (file: File) => { |
||||||
|
let uploadUrl = ServiceUploadUrlMap.get(service) |
||||||
|
if (!uploadUrl) { |
||||||
|
const response = await fetch(`${service}/.well-known/nostr/nip96.json`) |
||||||
|
if (!response.ok) { |
||||||
|
throw new Error( |
||||||
|
`${simplifyUrl(service)} does not work, please try another service in your settings` |
||||||
|
) |
||||||
|
} |
||||||
|
const data = await response.json() |
||||||
|
uploadUrl = data?.api_url |
||||||
|
if (!uploadUrl) { |
||||||
|
throw new Error( |
||||||
|
`${simplifyUrl(service)} does not work, please try another service in your settings` |
||||||
|
) |
||||||
|
} |
||||||
|
ServiceUploadUrlMap.set(service, uploadUrl) |
||||||
|
} |
||||||
|
|
||||||
|
const formData = new FormData() |
||||||
|
formData.append('file', file) |
||||||
|
|
||||||
|
const auth = await signHttpAuth(uploadUrl, 'POST') |
||||||
|
const response = await fetch(uploadUrl, { |
||||||
|
method: 'POST', |
||||||
|
body: formData, |
||||||
|
headers: { |
||||||
|
Authorization: auth |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
if (!response.ok) { |
||||||
|
throw new Error(response.status.toString()) |
||||||
|
} |
||||||
|
|
||||||
|
const data = await response.json() |
||||||
|
const tags = z.array(z.array(z.string())).parse(data.nip94_event?.tags ?? []) |
||||||
|
const imageUrl = tags.find(([tagName]) => tagName === 'url')?.[1] |
||||||
|
if (imageUrl) { |
||||||
|
return { url: imageUrl, tags } |
||||||
|
} else { |
||||||
|
throw new Error('No image url found') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<MediaUploadServiceContext.Provider value={{ service, updateService, upload }}> |
||||||
|
{children} |
||||||
|
</MediaUploadServiceContext.Provider> |
||||||
|
) |
||||||
|
} |
||||||
Loading…
Reference in new issue