) => {
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({
diff --git a/src/constants.ts b/src/constants.ts
index 4505b00c..00db37ac 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -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'
diff --git a/src/lib/link.ts b/src/lib/link.ts
index 76be2d38..0e90d2d3 100644
--- a/src/lib/link.ts
+++ b/src/lib/link.ts
@@ -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'
diff --git a/src/lib/url.ts b/src/lib/url.ts
index b75bd2de..44121258 100644
--- a/src/lib/url.ts
+++ b/src/lib/url.ts
@@ -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 {
diff --git a/src/pages/secondary/PostSettingsPage/MediaUploadServiceSetting.tsx b/src/pages/secondary/PostSettingsPage/MediaUploadServiceSetting.tsx
new file mode 100644
index 00000000..683fcdf8
--- /dev/null
+++ b/src/pages/secondary/PostSettingsPage/MediaUploadServiceSetting.tsx
@@ -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 (
+
+
+
+
+ )
+}
diff --git a/src/pages/secondary/PostSettingsPage/index.tsx b/src/pages/secondary/PostSettingsPage/index.tsx
new file mode 100644
index 00000000..4a164647
--- /dev/null
+++ b/src/pages/secondary/PostSettingsPage/index.tsx
@@ -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 (
+
+
+
+
+
+ )
+})
+PostSettingsPage.displayName = 'PostSettingsPage'
+export default PostSettingsPage
diff --git a/src/pages/secondary/SettingsPage/index.tsx b/src/pages/secondary/SettingsPage/index.tsx
index e2f43925..253d4d7b 100644
--- a/src/pages/secondary/SettingsPage/index.tsx
+++ b/src/pages/secondary/SettingsPage/index.tsx
@@ -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) => {
+ push(toPostSettings())}>
+
+
+
{!!nsec && (
void
+ upload: (file: File) => Promise<{ url: string; tags: string[][] }>
+}
+
+const MediaUploadServiceContext = createContext(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()
+
+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 (
+
+ {children}
+
+ )
+}
diff --git a/src/routes.tsx b/src/routes.tsx
index 1c703afa..aa28e2b8 100644
--- a/src/routes.tsx
+++ b/src/routes.tsx
@@ -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: },
{ path: '/users/:id/following', element: },
{ path: '/users/:id/relays', element: },
- { path: '/relay-settings', element: },
- { path: '/settings', element: },
- { path: '/wallet', element: },
- { path: '/profile-editor', element: },
{ path: '/relays/:url', element: },
+ { path: '/settings', element: },
+ { path: '/settings/relays', element: },
+ { path: '/settings/wallet', element: },
+ { path: '/settings/posts', element: },
+ { path: '/profile-editor', element: },
{ path: '/mutes', element: }
]
diff --git a/src/services/local-storage.service.ts b/src/services/local-storage.service.ts
index 9361ab9e..78e6e56a 100644
--- a/src/services/local-storage.service.ts
+++ b/src/services/local-storage.service.ts
@@ -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 = {}
+ 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()