From bc0fa7f52839d3bdd85b60660e851488117bc11c Mon Sep 17 00:00:00 2001 From: codytseng Date: Tue, 8 Apr 2025 14:23:16 +0800 Subject: [PATCH] feat: support change media upload service --- src/App.tsx | 7 +- src/components/PostEditor/Uploader.tsx | 33 ++------ src/constants.ts | 11 +++ src/lib/link.ts | 9 +- src/lib/url.ts | 7 +- .../MediaUploadServiceSetting.tsx | 35 ++++++++ .../secondary/PostSettingsPage/index.tsx | 18 ++++ src/pages/secondary/SettingsPage/index.tsx | 10 ++- src/providers/MediaUploadServiceProvider.tsx | 84 +++++++++++++++++++ src/routes.tsx | 10 ++- src/services/local-storage.service.ts | 15 +++- 11 files changed, 198 insertions(+), 41 deletions(-) create mode 100644 src/pages/secondary/PostSettingsPage/MediaUploadServiceSetting.tsx create mode 100644 src/pages/secondary/PostSettingsPage/index.tsx create mode 100644 src/providers/MediaUploadServiceProvider.tsx diff --git a/src/App.tsx b/src/App.tsx index bdbfb3dc..9cf19a12 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 { - - + + + + diff --git a/src/components/PostEditor/Uploader.tsx b/src/components/PostEditor/Uploader.tsx index 93d81d82..30e2ae21 100644 --- a/src/components/PostEditor/Uploader.tsx +++ b/src/components/PostEditor/Uploader.tsx @@ -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(null) const handleFileChange = async (event: React.ChangeEvent) => { 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())}> +
+ +
{t('Posts')}
+
+ +
{!!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()