feat: support change media upload service
This commit is contained in:
@@ -7,6 +7,7 @@ import { PageManager } from './PageManager'
|
||||
import { FavoriteRelaysProvider } from './providers/FavoriteRelaysProvider'
|
||||
import { FeedProvider } from './providers/FeedProvider'
|
||||
import { FollowListProvider } from './providers/FollowListProvider'
|
||||
import { MediaUploadServiceProvider } from './providers/MediaUploadServiceProvider'
|
||||
import { MuteListProvider } from './providers/MuteListProvider'
|
||||
import { NostrProvider } from './providers/NostrProvider'
|
||||
import { NoteStatsProvider } from './providers/NoteStatsProvider'
|
||||
@@ -24,8 +25,10 @@ export default function App(): JSX.Element {
|
||||
<MuteListProvider>
|
||||
<FeedProvider>
|
||||
<NoteStatsProvider>
|
||||
<PageManager />
|
||||
<Toaster />
|
||||
<MediaUploadServiceProvider>
|
||||
<PageManager />
|
||||
<Toaster />
|
||||
</MediaUploadServiceProvider>
|
||||
</NoteStatsProvider>
|
||||
</FeedProvider>
|
||||
</MuteListProvider>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useToast } from '@/hooks/use-toast'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { useMediaUploadService } from '@/providers/MediaUploadServiceProvider'
|
||||
import { useRef } from 'react'
|
||||
import { z } from 'zod'
|
||||
|
||||
export default function Uploader({
|
||||
children,
|
||||
@@ -16,41 +15,19 @@ export default function Uploader({
|
||||
className?: string
|
||||
accept?: string
|
||||
}) {
|
||||
const { signHttpAuth } = useNostr()
|
||||
const { toast } = useToast()
|
||||
const { upload } = useMediaUploadService()
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
try {
|
||||
onUploadingChange?.(true)
|
||||
const url = 'https://nostr.build/api/v2/nip96/upload'
|
||||
const auth = await signHttpAuth(url, 'POST')
|
||||
const response = await fetch(url, {
|
||||
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) {
|
||||
onUploadSuccess({ url: imageUrl, tags })
|
||||
} else {
|
||||
throw new Error('No image url found')
|
||||
}
|
||||
const result = await upload(file)
|
||||
console.log('File uploaded successfully', result)
|
||||
onUploadSuccess(result)
|
||||
} catch (error) {
|
||||
console.error('Error uploading file', error)
|
||||
toast({
|
||||
|
||||
@@ -12,6 +12,7 @@ export const StorageKey = {
|
||||
QUICK_ZAP: 'quickZap',
|
||||
LAST_READ_NOTIFICATION_TIME_MAP: 'lastReadNotificationTimeMap',
|
||||
ACCOUNT_FEED_INFO_MAP: 'accountFeedInfoMap',
|
||||
MEDIA_UPLOAD_SERVICE: 'mediaUploadService',
|
||||
ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap', // deprecated
|
||||
ACCOUNT_FOLLOW_LIST_EVENT_MAP: 'accountFollowListEventMap', // deprecated
|
||||
ACCOUNT_MUTE_LIST_EVENT_MAP: 'accountMuteListEventMap', // deprecated
|
||||
@@ -54,3 +55,13 @@ export const DEFAULT_FAVORITE_RELAYS = [
|
||||
'wss://news.utxo.one/',
|
||||
'wss://algo.utxo.one'
|
||||
]
|
||||
|
||||
export const NIP_96_SERVICE = [
|
||||
'https://mockingyou.com',
|
||||
'https://nostpic.com',
|
||||
'https://nostr.build', // default
|
||||
'https://nostrcheck.me',
|
||||
'https://nostrmedia.com',
|
||||
'https://files.sovbit.host'
|
||||
]
|
||||
export const DEFAULT_NIP_96_SERVICE = 'https://nostr.build'
|
||||
|
||||
@@ -35,11 +35,12 @@ export const toOthersRelaySettings = (pubkey: string) => {
|
||||
const npub = nip19.npubEncode(pubkey)
|
||||
return `/users/${npub}/relays`
|
||||
}
|
||||
export const toRelaySettings = (tag?: 'mailbox' | 'favorite-relays') => {
|
||||
return '/relay-settings' + (tag ? '#' + tag : '')
|
||||
}
|
||||
export const toSettings = () => '/settings'
|
||||
export const toWallet = () => '/wallet'
|
||||
export const toRelaySettings = (tag?: 'mailbox' | 'favorite-relays') => {
|
||||
return '/settings/relays' + (tag ? '#' + tag : '')
|
||||
}
|
||||
export const toWallet = () => '/settings/wallet'
|
||||
export const toPostSettings = () => '/settings/posts'
|
||||
export const toProfileEditor = () => '/profile-editor'
|
||||
export const toRelay = (url: string) => `/relays/${encodeURIComponent(url)}`
|
||||
export const toMuteList = () => '/mutes'
|
||||
|
||||
@@ -41,7 +41,12 @@ export function normalizeHttpUrl(url: string): string {
|
||||
}
|
||||
|
||||
export function simplifyUrl(url: string): string {
|
||||
return url.replace('wss://', '').replace('ws://', '').replace(/\/$/, '')
|
||||
return url
|
||||
.replace('wss://', '')
|
||||
.replace('ws://', '')
|
||||
.replace('https://', '')
|
||||
.replace('http://', '')
|
||||
.replace(/\/$/, '')
|
||||
}
|
||||
|
||||
export function isLocalNetworkUrl(urlString: string): boolean {
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
18
src/pages/secondary/PostSettingsPage/index.tsx
Normal file
18
src/pages/secondary/PostSettingsPage/index.tsx
Normal file
@@ -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
|
||||
@@ -3,7 +3,7 @@ import Donation from '@/components/Donation'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select'
|
||||
import { LocalizedLanguageNames, TLanguage } from '@/i18n'
|
||||
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
|
||||
import { toRelaySettings, toWallet } from '@/lib/link'
|
||||
import { toPostSettings, toRelaySettings, toWallet } from '@/lib/link'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useSecondaryPage } from '@/PageManager'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Info,
|
||||
KeyRound,
|
||||
Languages,
|
||||
PencilLine,
|
||||
Server,
|
||||
SunMoon,
|
||||
Wallet
|
||||
@@ -87,6 +88,13 @@ const SettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
|
||||
</div>
|
||||
<ChevronRight />
|
||||
</SettingItem>
|
||||
<SettingItem className="clickable" onClick={() => push(toPostSettings())}>
|
||||
<div className="flex items-center gap-4">
|
||||
<PencilLine />
|
||||
<div>{t('Posts')}</div>
|
||||
</div>
|
||||
<ChevronRight />
|
||||
</SettingItem>
|
||||
{!!nsec && (
|
||||
<SettingItem
|
||||
className="clickable"
|
||||
|
||||
84
src/providers/MediaUploadServiceProvider.tsx
Normal file
84
src/providers/MediaUploadServiceProvider.tsx
Normal file
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import MuteListPage from './pages/secondary/MuteListPage'
|
||||
import NoteListPage from './pages/secondary/NoteListPage'
|
||||
import NotePage from './pages/secondary/NotePage'
|
||||
import OthersRelaySettingsPage from './pages/secondary/OthersRelaySettingsPage'
|
||||
import PostSettingsPage from './pages/secondary/PostSettingsPage'
|
||||
import ProfileEditorPage from './pages/secondary/ProfileEditorPage'
|
||||
import ProfileListPage from './pages/secondary/ProfileListPage'
|
||||
import ProfilePage from './pages/secondary/ProfilePage'
|
||||
@@ -20,11 +21,12 @@ const ROUTES = [
|
||||
{ path: '/users/:id', element: <ProfilePage /> },
|
||||
{ path: '/users/:id/following', element: <FollowingListPage /> },
|
||||
{ path: '/users/:id/relays', element: <OthersRelaySettingsPage /> },
|
||||
{ path: '/relay-settings', element: <RelaySettingsPage /> },
|
||||
{ path: '/settings', element: <SettingsPage /> },
|
||||
{ path: '/wallet', element: <WalletPage /> },
|
||||
{ path: '/profile-editor', element: <ProfileEditorPage /> },
|
||||
{ path: '/relays/:url', element: <RelayPage /> },
|
||||
{ path: '/settings', element: <SettingsPage /> },
|
||||
{ path: '/settings/relays', element: <RelaySettingsPage /> },
|
||||
{ path: '/settings/wallet', element: <WalletPage /> },
|
||||
{ path: '/settings/posts', element: <PostSettingsPage /> },
|
||||
{ path: '/profile-editor', element: <ProfileEditorPage /> },
|
||||
{ path: '/mutes', element: <MuteListPage /> }
|
||||
]
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { StorageKey } from '@/constants'
|
||||
import { DEFAULT_NIP_96_SERVICE, StorageKey } from '@/constants'
|
||||
import { isSameAccount } from '@/lib/account'
|
||||
import { randomString } from '@/lib/random'
|
||||
import {
|
||||
@@ -23,6 +23,7 @@ class LocalStorageService {
|
||||
private defaultZapComment: string = 'Zap!'
|
||||
private quickZap: boolean = false
|
||||
private accountFeedInfoMap: Record<string, TFeedInfo | undefined> = {}
|
||||
private mediaUploadService: string = DEFAULT_NIP_96_SERVICE
|
||||
|
||||
constructor() {
|
||||
if (!LocalStorageService.instance) {
|
||||
@@ -85,6 +86,9 @@ class LocalStorageService {
|
||||
window.localStorage.getItem(StorageKey.ACCOUNT_FEED_INFO_MAP) ?? '{}'
|
||||
this.accountFeedInfoMap = JSON.parse(accountFeedInfoMapStr)
|
||||
|
||||
this.mediaUploadService =
|
||||
window.localStorage.getItem(StorageKey.MEDIA_UPLOAD_SERVICE) ?? DEFAULT_NIP_96_SERVICE
|
||||
|
||||
// Clean up deprecated data
|
||||
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
|
||||
window.localStorage.removeItem(StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP)
|
||||
@@ -222,6 +226,15 @@ class LocalStorageService {
|
||||
JSON.stringify(this.accountFeedInfoMap)
|
||||
)
|
||||
}
|
||||
|
||||
getMediaUploadService() {
|
||||
return this.mediaUploadService
|
||||
}
|
||||
|
||||
setMediaUploadService(service: string) {
|
||||
this.mediaUploadService = service
|
||||
window.localStorage.setItem(StorageKey.MEDIA_UPLOAD_SERVICE, service)
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new LocalStorageService()
|
||||
|
||||
Reference in New Issue
Block a user