diff --git a/src/App.tsx b/src/App.tsx index f819b4f2..817e4e25 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,7 +18,6 @@ import { NostrProvider } from '@/providers/NostrProvider' import { ReplyProvider } from '@/providers/ReplyProvider' import { ScreenSizeProvider } from '@/providers/ScreenSizeProvider' import { ThemeProvider } from '@/providers/ThemeProvider' -import { TranslationServiceProvider } from '@/providers/TranslationServiceProvider' import { UserPreferencesProvider } from '@/providers/UserPreferencesProvider' import { UserTrustProvider } from '@/providers/UserTrustProvider' import { ZapProvider } from '@/providers/ZapProvider' @@ -33,7 +32,6 @@ export default function App(): JSX.Element { - @@ -60,8 +58,7 @@ export default function App(): JSX.Element { - - + diff --git a/src/components/Content/index.tsx b/src/components/Content/index.tsx index 7e0e312e..bf6ed0a8 100644 --- a/src/components/Content/index.tsx +++ b/src/components/Content/index.tsx @@ -1,4 +1,4 @@ -import { useTranslatedEvent, useMediaExtraction } from '@/hooks' +import { useMediaExtraction } from '@/hooks' import { EmbeddedEmojiParser, EmbeddedEventParser, @@ -78,8 +78,7 @@ export default function Content({ className?: string mustLoadMedia?: boolean }) { - const translatedEvent = useTranslatedEvent(event?.id) - const _content = translatedEvent?.content ?? event?.content ?? content + const _content = event?.content ?? content // Use unified media extraction service const extractedMedia = useMediaExtraction(event, _content) diff --git a/src/components/ContentPreview/HighlightPreview.tsx b/src/components/ContentPreview/HighlightPreview.tsx index b1656831..fa3de8a1 100644 --- a/src/components/ContentPreview/HighlightPreview.tsx +++ b/src/components/ContentPreview/HighlightPreview.tsx @@ -1,4 +1,3 @@ -import { useTranslatedEvent } from '@/hooks' import { getEmojiInfosFromEmojiTags } from '@/lib/tag' import { cn } from '@/lib/utils' import { Event } from 'nostr-tools' @@ -14,14 +13,13 @@ export default function HighlightPreview({ className?: string }) { const { t } = useTranslation() - const translatedEvent = useTranslatedEvent(event.id) const emojiInfos = useMemo(() => getEmojiInfosFromEmojiTags(event.tags), [event]) return (
[{t('Highlight')}]{' '} diff --git a/src/components/ContentPreview/NormalContentPreview.tsx b/src/components/ContentPreview/NormalContentPreview.tsx index 55456896..6ce25eea 100644 --- a/src/components/ContentPreview/NormalContentPreview.tsx +++ b/src/components/ContentPreview/NormalContentPreview.tsx @@ -1,4 +1,3 @@ -import { useTranslatedEvent } from '@/hooks' import { getEmojiInfosFromEmojiTags } from '@/lib/tag' import { Event } from 'nostr-tools' import { useMemo } from 'react' @@ -11,12 +10,11 @@ export default function NormalContentPreview({ event: Event className?: string }) { - const translatedEvent = useTranslatedEvent(event?.id) const emojiInfos = useMemo(() => getEmojiInfosFromEmojiTags(event?.tags), [event]) return ( diff --git a/src/components/ContentPreview/PollPreview.tsx b/src/components/ContentPreview/PollPreview.tsx index f3b72827..d457183f 100644 --- a/src/components/ContentPreview/PollPreview.tsx +++ b/src/components/ContentPreview/PollPreview.tsx @@ -1,5 +1,4 @@ import { POLL_TYPE } from '@/constants' -import { useTranslatedEvent } from '@/hooks' import { getPollMetadataFromEvent } from '@/lib/event-metadata' import { getEmojiInfosFromEmojiTags } from '@/lib/tag' import { cn } from '@/lib/utils' @@ -10,13 +9,9 @@ import Content from './Content' export default function PollPreview({ event, className }: { event: Event; className?: string }) { const { t } = useTranslation() - const translatedEvent = useTranslatedEvent(event.id) const emojiInfos = useMemo(() => getEmojiInfosFromEmojiTags(event.tags), [event]) - const poll = useMemo( - () => getPollMetadataFromEvent(translatedEvent ?? event), - [event, translatedEvent] - ) - const content = (translatedEvent?.content ?? event.content)?.trim() + const poll = useMemo(() => getPollMetadataFromEvent(event), [event]) + const content = event.content?.trim() return (
diff --git a/src/components/Note/Poll.tsx b/src/components/Note/Poll.tsx index 1feb994d..59dab008 100644 --- a/src/components/Note/Poll.tsx +++ b/src/components/Note/Poll.tsx @@ -1,6 +1,5 @@ import { Button } from '@/components/ui/button' import { BIG_RELAY_URLS, POLL_TYPE } from '@/constants' -import { useTranslatedEvent } from '@/hooks' import { useFetchPollResults } from '@/hooks/useFetchPollResults' import { createPollResponseDraftEvent } from '@/lib/draft-event' import { getPollMetadataFromEvent } from '@/lib/event-metadata' @@ -18,16 +17,12 @@ import { showPublishingFeedback, showSimplePublishSuccess } from '@/lib/publishi export default function Poll({ event, className }: { event: Event; className?: string }) { const { t } = useTranslation() - const translatedEvent = useTranslatedEvent(event.id) const { pubkey, publish, startLogin } = useNostr() const [isVoting, setIsVoting] = useState(false) const [selectedOptionIds, setSelectedOptionIds] = useState([]) const pollResults = useFetchPollResults(event.id) const [isLoadingResults, setIsLoadingResults] = useState(false) - const poll = useMemo( - () => getPollMetadataFromEvent(translatedEvent ?? event), - [event, translatedEvent] - ) + const poll = useMemo(() => getPollMetadataFromEvent(event), [event]) const votedOptionIds = useMemo(() => { if (!pollResults || !pubkey) return [] return Object.entries(pollResults.results) diff --git a/src/components/Note/index.tsx b/src/components/Note/index.tsx index 90c68508..c51b35eb 100644 --- a/src/components/Note/index.tsx +++ b/src/components/Note/index.tsx @@ -17,7 +17,6 @@ import { FormattedTimestamp } from '../FormattedTimestamp' import Nip05 from '../Nip05' import NoteOptions from '../NoteOptions' import ParentNotePreview from '../ParentNotePreview' -import TranslateButton from '../TranslateButton' import UserAvatar from '../UserAvatar' import Username from '../Username' import { MessageSquare } from 'lucide-react' @@ -266,7 +265,6 @@ export default function Note({ )} - {size === 'normal' && ( { - const detected = detectLanguage(about) - if (!detected) return false - if (detected === 'und') return true - return !i18n.language.startsWith(detected) - }, [about, i18n.language]) - const [translatedAbout, setTranslatedAbout] = useState(null) - const [translating, setTranslating] = useState(false) - const aboutNodes = useMemo(() => { - if (!about) return null + const aboutNodes = parseContent(about ?? '', [ + EmbeddedWebsocketUrlParser, + EmbeddedUrlParser, + EmbeddedHashtagParser, + EmbeddedMentionParser + ]).map((node, index) => { + if (node.type === 'url') { + return + } + if (node.type === 'websocket-url') { + return + } + if (node.type === 'hashtag') { + return + } + if (node.type === 'mention') { + return + } + return node.data + }) - const nodes = parseContent(translatedAbout ?? about, [ - EmbeddedWebsocketUrlParser, - EmbeddedUrlParser, - EmbeddedHashtagParser, - EmbeddedMentionParser - ]) - return nodes.map((node, index) => { - if (node.type === 'url') { - return - } - if (node.type === 'websocket-url') { - return - } - if (node.type === 'hashtag') { - return - } - if (node.type === 'mention') { - return - } - return node.data - }) - }, [about, translatedAbout]) - - const handleTranslate = async () => { - if (translating || translatedAbout) return - setTranslating(true) - translateText(about ?? '') - .then((translated) => { - setTranslatedAbout(translated) - }) - .catch((error) => { - toast.error( - 'Translation failed: ' + - (error.message || 'An error occurred while translating the about') - ) - }) - .finally(() => { - setTranslating(false) - }) - } - - const handleShowOriginal = () => { - setTranslatedAbout(null) - } - - return ( -
-
{aboutNodes}
- {needTranslation && ( -
- {translating ? ( -
{t('Translating...')}
- ) : translatedAbout === null ? ( - - ) : ( - - )} -
- )} -
- ) + return
{aboutNodes}
} diff --git a/src/components/RelayInfo/RelayReviewCard.tsx b/src/components/RelayInfo/RelayReviewCard.tsx index 2ba837d2..7441bcfa 100644 --- a/src/components/RelayInfo/RelayReviewCard.tsx +++ b/src/components/RelayInfo/RelayReviewCard.tsx @@ -9,7 +9,6 @@ import ContentPreview from '../ContentPreview' import { FormattedTimestamp } from '../FormattedTimestamp' import Nip05 from '../Nip05' import Stars from '../Stars' -import TranslateButton from '../TranslateButton' import { SimpleUserAvatar } from '../UserAvatar' import { SimpleUsername } from '../Username' @@ -53,9 +52,6 @@ export default function RelayReviewCard({
-
- -
diff --git a/src/components/ReplyNote/index.tsx b/src/components/ReplyNote/index.tsx index 9eee6f23..c0be38ef 100644 --- a/src/components/ReplyNote/index.tsx +++ b/src/components/ReplyNote/index.tsx @@ -17,7 +17,6 @@ import Nip05 from '../Nip05' import NoteOptions from '../NoteOptions' import NoteStats from '../NoteStats' import ParentNotePreview from '../ParentNotePreview' -import TranslateButton from '../TranslateButton' import UserAvatar from '../UserAvatar' import Username from '../Username' @@ -94,7 +93,6 @@ export default function ReplyNote({
-
diff --git a/src/components/TranslateButton/index.tsx b/src/components/TranslateButton/index.tsx deleted file mode 100644 index ae3f015c..00000000 --- a/src/components/TranslateButton/index.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { ExtendedKind } from '@/constants' -import { useTranslatedEvent } from '@/hooks' -import { toTranslation } from '@/lib/link' -import { cn, detectLanguage } from '@/lib/utils' -import { useSecondaryPage } from '@/PageManager' -import { useTranslationService } from '@/providers/TranslationServiceProvider' -import { Languages, Loader } from 'lucide-react' -import { Event, kinds } from 'nostr-tools' -import { useMemo, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { toast } from 'sonner' - -export default function TranslateButton({ - event, - className -}: { - event: Event - className?: string -}) { - const { i18n } = useTranslation() - const { push } = useSecondaryPage() - const { translateEvent, showOriginalEvent } = useTranslationService() - const [translating, setTranslating] = useState(false) - const translatedEvent = useTranslatedEvent(event.id) - const supported = useMemo( - () => - [ - kinds.ShortTextNote, - kinds.Highlights, - ExtendedKind.COMMENT, - ExtendedKind.PICTURE, - ExtendedKind.POLL, - ExtendedKind.RELAY_REVIEW - ].includes(event.kind), - [event] - ) - - const needTranslation = useMemo(() => { - const detected = detectLanguage(event.content) - if (!detected) return false - if (detected === 'und') return true - return !i18n.language.startsWith(detected) - }, [event, i18n.language]) - - if (!supported || !needTranslation) { - return null - } - - const handleTranslate = async () => { - if (translating) return - - setTranslating(true) - await translateEvent(event) - .catch((error) => { - toast.error( - 'Translation failed: ' + (error.message || 'An error occurred while translating the note') - ) - if (error.message === 'Insufficient balance.') { - push(toTranslation()) - } - }) - .finally(() => { - setTranslating(false) - }) - } - - const showOriginal = () => { - showOriginalEvent(event.id) - } - - return ( - - ) -} diff --git a/src/constants.ts b/src/constants.ts index 1b3878e3..b321e647 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -37,7 +37,6 @@ export const StorageKey = { AUTOPLAY: 'autoplay', HIDE_UNTRUSTED_INTERACTIONS: 'hideUntrustedInteractions', HIDE_UNTRUSTED_NOTIFICATIONS: 'hideUntrustedNotifications', - TRANSLATION_SERVICE_CONFIG_MAP: 'translationServiceConfigMap', MEDIA_UPLOAD_SERVICE_CONFIG_MAP: 'mediaUploadServiceConfigMap', HIDE_UNTRUSTED_NOTES: 'hideUntrustedNotes', DEFAULT_SHOW_NSFW: 'defaultShowNsfw', diff --git a/src/hooks/index.tsx b/src/hooks/index.tsx index 227d80af..652e395d 100644 --- a/src/hooks/index.tsx +++ b/src/hooks/index.tsx @@ -6,5 +6,4 @@ export * from './useFetchRelayInfo' export * from './useFetchRelayInfos' export * from './useFetchRelayList' export * from './useSearchProfiles' -export * from './useTranslatedEvent' export * from './useMediaExtraction' diff --git a/src/hooks/useTranslatedEvent.tsx b/src/hooks/useTranslatedEvent.tsx deleted file mode 100644 index 0bcf6b80..00000000 --- a/src/hooks/useTranslatedEvent.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { useTranslationService } from '@/providers/TranslationServiceProvider' -import { Event } from 'nostr-tools' -import { useEffect, useMemo, useState } from 'react' - -export function useTranslatedEvent(eventId?: string) { - const { translatedEventIdSet, getTranslatedEvent } = useTranslationService() - const translated = useMemo(() => { - return eventId ? translatedEventIdSet.has(eventId) : false - }, [eventId, translatedEventIdSet]) - const [translatedEvent, setTranslatedEvent] = useState(null) - - useEffect(() => { - if (translated && eventId) { - setTranslatedEvent(getTranslatedEvent(eventId)) - } else { - setTranslatedEvent(null) - } - }, [translated, eventId]) - - return translatedEvent -} diff --git a/src/pages/secondary/TranslationPage/JumbleTranslate/AccountInfo.tsx b/src/pages/secondary/TranslationPage/JumbleTranslate/AccountInfo.tsx deleted file mode 100644 index b733baa6..00000000 --- a/src/pages/secondary/TranslationPage/JumbleTranslate/AccountInfo.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { JUMBLE_API_BASE_URL } from '@/constants' -import { useNostr } from '@/providers/NostrProvider' -import { Check, Copy, Eye, EyeOff } from 'lucide-react' -import { useState } from 'react' -import { useTranslation } from 'react-i18next' -import { useJumbleTranslateAccount } from './JumbleTranslateAccountProvider' -import RegenerateApiKeyButton from './RegenerateApiKeyButton' -import TopUp from './TopUp' - -export function AccountInfo() { - const { t } = useTranslation() - const { pubkey, startLogin } = useNostr() - const { account } = useJumbleTranslateAccount() - const [showApiKey, setShowApiKey] = useState(false) - const [copied, setCopied] = useState(false) - - if (!pubkey) { - return ( -
- -
- ) - } - - return ( -
- {/* Balance display in characters */} -
-

{t('Balance')}

-
-

{account?.balance.toLocaleString() ?? '0'}

-

{t('characters')}

-
-
- - {/* API Key section with visibility toggle and copy functionality */} -
-

API key

-
- - - - -
-

- {t('jumbleTranslateApiKeyDescription', { - serviceUrl: new URL('/v1/translation', JUMBLE_API_BASE_URL).toString() - })} -

-
- -
-
- ) -} diff --git a/src/pages/secondary/TranslationPage/JumbleTranslate/JumbleTranslateAccountProvider.tsx b/src/pages/secondary/TranslationPage/JumbleTranslate/JumbleTranslateAccountProvider.tsx deleted file mode 100644 index 78c7f777..00000000 --- a/src/pages/secondary/TranslationPage/JumbleTranslate/JumbleTranslateAccountProvider.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { useNostr } from '@/providers/NostrProvider' -import { useTranslationService } from '@/providers/TranslationServiceProvider' -import { TTranslationAccount } from '@/types' -import { createContext, useContext, useEffect, useState } from 'react' -import { toast } from 'sonner' - -type TJumbleTranslateAccountContext = { - account: TTranslationAccount | null - getAccount: () => Promise - regenerateApiKey: () => Promise -} - -export const JumbleTranslateAccountContext = createContext< - TJumbleTranslateAccountContext | undefined ->(undefined) - -export const useJumbleTranslateAccount = () => { - const context = useContext(JumbleTranslateAccountContext) - if (!context) { - throw new Error( - 'useJumbleTranslateAccount must be used within a JumbleTranslateAccountProvider' - ) - } - return context -} - -export function JumbleTranslateAccountProvider({ children }: { children: React.ReactNode }) { - const { pubkey } = useNostr() - const { getAccount: _getAccount, regenerateApiKey: _regenerateApiKey } = useTranslationService() - const [account, setAccount] = useState(null) - - useEffect(() => { - setAccount(null) - if (!pubkey) return - - setTimeout(() => { - getAccount() - }, 100) - }, [pubkey]) - - const regenerateApiKey = async (): Promise => { - try { - if (!account) { - await getAccount() - } - const newApiKey = await _regenerateApiKey() - if (newApiKey) { - setAccount((prev) => { - if (!prev) return prev - return { - ...prev, - api_key: newApiKey - } - }) - } - } catch (error) { - toast.error( - 'Failed to regenerate Jumble translation API key: ' + - (error instanceof Error - ? error.message - : 'An error occurred while regenerating the API key') - ) - setAccount(null) - } - } - - const getAccount = async (): Promise => { - try { - const data = await _getAccount() - if (data) { - setAccount(data) - } - } catch (error) { - toast.error( - 'Failed to fetch Jumble translation account: ' + - (error instanceof Error ? error.message : 'An error occurred while fetching the account') - ) - setAccount(null) - } - } - - return ( - - {children} - - ) -} diff --git a/src/pages/secondary/TranslationPage/JumbleTranslate/RegenerateApiKeyButton.tsx b/src/pages/secondary/TranslationPage/JumbleTranslate/RegenerateApiKeyButton.tsx deleted file mode 100644 index 2dc68b53..00000000 --- a/src/pages/secondary/TranslationPage/JumbleTranslate/RegenerateApiKeyButton.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { Button } from '@/components/ui/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger -} from '@/components/ui/dialog' -import { Loader, RotateCcw } from 'lucide-react' -import { useState } from 'react' -import { useTranslation } from 'react-i18next' -import { useJumbleTranslateAccount } from './JumbleTranslateAccountProvider' - -export default function RegenerateApiKeyButton() { - const { t } = useTranslation() - const { account, regenerateApiKey } = useJumbleTranslateAccount() - const [resettingApiKey, setResettingApiKey] = useState(false) - const [showResetDialog, setShowResetDialog] = useState(false) - - const handleRegenerateApiKey = async () => { - if (resettingApiKey || !account) return - - setResettingApiKey(true) - await regenerateApiKey() - setShowResetDialog(false) - setResettingApiKey(false) - } - - return ( - - - - - - - {t('Reset API key')} - - {t('Are you sure you want to reset your API key? This action cannot be undone.')} -
-
- {t('Warning')}:{' '} - {t( - 'Your current API key will become invalid immediately, and any applications using it will stop working until you update them with the new key.' - )} -
-
- - - - -
-
- ) -} diff --git a/src/pages/secondary/TranslationPage/JumbleTranslate/TopUp.tsx b/src/pages/secondary/TranslationPage/JumbleTranslate/TopUp.tsx deleted file mode 100644 index 581957f1..00000000 --- a/src/pages/secondary/TranslationPage/JumbleTranslate/TopUp.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { cn } from '@/lib/utils' -import { useNostr } from '@/providers/NostrProvider' -import transaction from '@/services/transaction.service' -import { closeModal, launchPaymentModal } from '@getalby/bitcoin-connect-react' -import { Loader } from 'lucide-react' -import { useState } from 'react' -import { toast } from 'sonner' -import { useJumbleTranslateAccount } from './JumbleTranslateAccountProvider' -import { useTranslation } from 'react-i18next' - -export default function TopUp() { - const { t } = useTranslation() - const { pubkey } = useNostr() - const { getAccount } = useJumbleTranslateAccount() - const [topUpLoading, setTopUpLoading] = useState(false) - const [topUpAmount, setTopUpAmount] = useState(1000) - const [selectedAmount, setSelectedAmount] = useState(1000) - - const presetAmounts = [ - { amount: 1_000, text: '1k' }, - { amount: 5_000, text: '5k' }, - { amount: 10_000, text: '10k' }, - { amount: 25_000, text: '25k' }, - { amount: 50_000, text: '50k' }, - { amount: 100_000, text: '100k' } - ] - const charactersPerUnit = 100 // 1 unit = 100 characters - - const calculateCharacters = (amount: number) => { - return amount * charactersPerUnit - } - - const handlePresetClick = (amount: number) => { - setSelectedAmount(amount) - setTopUpAmount(amount) - } - - const handleInputChange = (value: string) => { - const numValue = parseInt(value) || 0 - setTopUpAmount(numValue) - setSelectedAmount(numValue >= 1000 ? numValue : null) - } - - const handleTopUp = async (amount: number | null) => { - if (topUpLoading || !pubkey || !amount || amount < 1000) return - - setTopUpLoading(true) - try { - const { transactionId, invoiceId } = await transaction.createTransaction(pubkey, amount) - - let checkPaymentInterval: ReturnType | undefined = undefined - const { setPaid } = launchPaymentModal({ - invoice: invoiceId, - onCancelled: () => { - clearInterval(checkPaymentInterval) - setTopUpLoading(false) - } - }) - - let failedCount = 0 - checkPaymentInterval = setInterval(async () => { - try { - const { state } = await transaction.checkTransaction(transactionId) - if (state === 'pending') return - - clearInterval(checkPaymentInterval) - setTopUpLoading(false) - - if (state === 'settled') { - setPaid({ preimage: '' }) // Preimage is not returned, but we can assume payment is successful - getAccount() // Refresh account balance - } else { - closeModal() - toast.error('The invoice has expired or the payment was not successful') - } - } catch (err) { - failedCount++ - if (failedCount <= 3) return - - clearInterval(checkPaymentInterval) - setTopUpLoading(false) - toast.error( - 'Top up failed: ' + - (err instanceof Error ? err.message : 'An error occurred while topping up') - ) - } - }, 2000) - } catch (err) { - setTopUpLoading(false) - toast.error( - 'Top up failed: ' + - (err instanceof Error ? err.message : 'An error occurred while topping up') - ) - } - } - - return ( -
-

{t('Top up')}

- - {/* Preset amounts */} -
- {presetAmounts.map(({ amount, text }) => ( - - ))} -
- - {/* Custom amount input */} -
-
- handleInputChange(e.target.value)} - min={1000} - step={1000} - className="w-40" - /> - {t('sats')} -
- {selectedAmount && selectedAmount >= 1000 && ( -

- {t('Will receive: {n} characters', { - n: calculateCharacters(selectedAmount).toLocaleString() - })} -

- )} -
- - -
- ) -} diff --git a/src/pages/secondary/TranslationPage/JumbleTranslate/index.tsx b/src/pages/secondary/TranslationPage/JumbleTranslate/index.tsx deleted file mode 100644 index f85e59c7..00000000 --- a/src/pages/secondary/TranslationPage/JumbleTranslate/index.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { AccountInfo } from './AccountInfo' -import { JumbleTranslateAccountProvider } from './JumbleTranslateAccountProvider' - -export default function JumbleTranslate() { - return ( - - - - ) -} diff --git a/src/pages/secondary/TranslationPage/LibreTranslate/index.tsx b/src/pages/secondary/TranslationPage/LibreTranslate/index.tsx deleted file mode 100644 index c81d6970..00000000 --- a/src/pages/secondary/TranslationPage/LibreTranslate/index.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { useTranslationService } from '@/providers/TranslationServiceProvider' -import { useEffect, useRef, useState } from 'react' -import { useTranslation } from 'react-i18next' - -export default function LibreTranslate() { - const { t } = useTranslation() - const { config, updateConfig } = useTranslationService() - const [server, setServer] = useState( - config.service === 'libre_translate' ? (config.server ?? '') : '' - ) - const [apiKey, setApiKey] = useState( - config.service === 'libre_translate' ? (config.api_key ?? '') : '' - ) - const initialized = useRef(false) - - useEffect(() => { - if (!initialized.current) { - initialized.current = true - return - } - - updateConfig({ - service: 'libre_translate', - server, - api_key: apiKey - }) - }, [server, apiKey]) - - return ( -
-
- - setServer(e.target.value)} - placeholder="Enter server address" - /> -
-
- - setApiKey(e.target.value)} - placeholder="Enter API Key" - /> -
-
- ) -} diff --git a/src/pages/secondary/TranslationPage/index.tsx b/src/pages/secondary/TranslationPage/index.tsx index 21dc6855..b711e1fd 100644 --- a/src/pages/secondary/TranslationPage/index.tsx +++ b/src/pages/secondary/TranslationPage/index.tsx @@ -1,74 +1,23 @@ -import { Label } from '@/components/ui/label' -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue -} from '@/components/ui/select' -import { LocalizedLanguageNames } from '@/i18n' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' -import { useTranslationService } from '@/providers/TranslationServiceProvider' -import { TLanguage } from '@/types' -import { forwardRef, useState } from 'react' +import { forwardRef } from 'react' import { useTranslation } from 'react-i18next' -import JumbleTranslate from './JumbleTranslate' -import LibreTranslate from './LibreTranslate' -const TranslationPage = forwardRef(({ index, hideTitlebar = false }: { index?: number; hideTitlebar?: boolean }, ref) => { - const { t, i18n } = useTranslation() - const { config, updateConfig } = useTranslationService() - const [language, setLanguage] = useState(i18n.language as TLanguage) +const TranslationPage = forwardRef( + ({ index, hideTitlebar = false }: { index?: number; hideTitlebar?: boolean }, ref) => { + const { t } = useTranslation() - const handleLanguageChange = (value: TLanguage) => { - i18n.changeLanguage(value) - setLanguage(value) - } - - return ( - -
-
- - + return ( + +
+

+ {t( + 'To translate notes and other content, use your browser’s built-in translation. For example: right-click the page and choose “Translate to…”, or use the translate icon in the address bar.' + )} +

-
- - -
- {config.service === 'jumble' ? : } -
- - ) -}) + + ) + } +) TranslationPage.displayName = 'TranslationPage' export default TranslationPage diff --git a/src/providers/TranslationServiceProvider.tsx b/src/providers/TranslationServiceProvider.tsx deleted file mode 100644 index 7902ef51..00000000 --- a/src/providers/TranslationServiceProvider.tsx +++ /dev/null @@ -1,235 +0,0 @@ -import { ExtendedKind } from '@/constants' -import { getPollMetadataFromEvent } from '@/lib/event-metadata' -import libreTranslate from '@/services/libre-translate.service' -import storage from '@/services/local-storage.service' -import translation from '@/services/translation.service' -import { TTranslationAccount, TTranslationServiceConfig } from '@/types' -import { Event, kinds } from 'nostr-tools' -import { createContext, useContext, useEffect, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { useNostr } from './NostrProvider' - -const translatedEventCache: Map = new Map() -const translatedTextCache: Map = new Map() - -type TTranslationServiceContext = { - config: TTranslationServiceConfig - translatedEventIdSet: Set - translateText: (text: string) => Promise - translateEvent: (event: Event) => Promise - getTranslatedEvent: (eventId: string) => Event | null - showOriginalEvent: (eventId: string) => void - getAccount: () => Promise - regenerateApiKey: () => Promise - updateConfig: (newConfig: TTranslationServiceConfig) => void -} - -const TranslationServiceContext = createContext(undefined) - -export const useTranslationService = () => { - const context = useContext(TranslationServiceContext) - if (!context) { - throw new Error('useTranslation must be used within a TranslationProvider') - } - return context -} - -export function TranslationServiceProvider({ children }: { children: React.ReactNode }) { - const { i18n } = useTranslation() - const [config, setConfig] = useState({ service: 'jumble' }) - const { pubkey, startLogin } = useNostr() - const [translatedEventIdSet, setTranslatedEventIdSet] = useState>(new Set()) - - useEffect(() => { - translation.changeCurrentPubkey(pubkey) - const config = storage.getTranslationServiceConfig(pubkey) - setConfig(config) - }, [pubkey]) - - const getAccount = async (): Promise => { - if (config.service !== 'jumble') return - if (!pubkey) { - startLogin() - return - } - return await translation.getAccount() - } - - const regenerateApiKey = async (): Promise => { - if (config.service !== 'jumble') return - if (!pubkey) { - startLogin() - return - } - return await translation.regenerateApiKey() - } - - const getTranslatedEvent = (eventId: string): Event | null => { - const target = i18n.language - const cacheKey = target + '_' + eventId - return translatedEventCache.get(cacheKey) ?? null - } - - const translate = async (text: string, target: string): Promise => { - if (config.service === 'jumble') { - return await translation.translate(text, target) - } else { - return await libreTranslate.translate(text, target, config.server, config.api_key) - } - } - - const translateText = async (text: string): Promise => { - if (!text) { - return text - } - - const target = i18n.language - const cacheKey = target + '_' + text - const cache = translatedTextCache.get(cacheKey) - if (cache) { - return cache - } - - const translatedText = await translate(text, target) - translatedTextCache.set(cacheKey, translatedText) - return translatedText - } - - const translateHighlightEvent = async (event: Event): Promise => { - const target = i18n.language - const comment = event.tags.find((tag) => tag[0] === 'comment')?.[1] - - const texts = { - content: event.content, - comment - } - const joinedText = joinTexts(texts) - if (!joinedText) return event - - const translatedText = await translate(joinedText, target) - const translatedTexts = splitTranslatedText(translatedText) - return { - ...event, - content: translatedTexts.content ?? event.content, - tags: event.tags.map((tag) => - tag[0] === 'comment' ? ['comment', translatedTexts.comment ?? tag[1]] : tag - ) - } - } - - const translatePollEvent = async (event: Event): Promise => { - const target = i18n.language - const pollMetadata = getPollMetadataFromEvent(event) - - const texts: Record = { - question: event.content, - ...pollMetadata?.options.reduce( - (acc, option) => { - acc[option.id] = option.label - return acc - }, - {} as Record - ) - } - const joinedText = joinTexts(texts) - if (!joinedText) return event - - const translatedText = await translate(joinedText, target) - const translatedTexts = splitTranslatedText(translatedText) - return { - ...event, - content: translatedTexts.question ?? '', - tags: event.tags.map((tag) => - tag[0] === 'option' ? ['option', tag[1], translatedTexts[tag[1]] ?? tag[2]] : tag - ) - } - } - - const translateEvent = async (event: Event): Promise => { - if (config.service === 'jumble' && !pubkey) { - startLogin() - return - } - - const target = i18n.language - const cacheKey = target + '_' + event.id - const cache = translatedEventCache.get(cacheKey) - if (cache) { - setTranslatedEventIdSet((prev) => new Set(prev.add(event.id))) - return cache - } - - let translatedEvent: Event | undefined - if (event.kind === kinds.Highlights) { - translatedEvent = await translateHighlightEvent(event) - } else if (event.kind === ExtendedKind.POLL) { - translatedEvent = await translatePollEvent(event) - } else { - const translatedText = await translate(event.content, target) - if (!translatedText) { - return - } - translatedEvent = { ...event, content: translatedText } - } - - translatedEventCache.set(cacheKey, translatedEvent) - setTranslatedEventIdSet((prev) => new Set(prev.add(event.id))) - return translatedEvent - } - - const showOriginalEvent = (eventId: string) => { - setTranslatedEventIdSet((prev) => { - const newSet = new Set(prev) - newSet.delete(eventId) - return newSet - }) - } - - const updateConfig = (newConfig: TTranslationServiceConfig) => { - setConfig(newConfig) - storage.setTranslationServiceConfig(newConfig, pubkey) - } - - return ( - - {children} - - ) -} - -function joinTexts(texts: Record): string { - return ( - Object.entries(texts).filter(([, content]) => content && content.trim() !== '') as [ - string, - string - ][] - ) - .map(([key, content]) => `=== ${key} ===\n${content.trim()}\n=== ${key} ===`) - .join('\n\n') -} - -function splitTranslatedText(translated: string) { - const regex = /=== (.+?) ===\n([\s\S]*?)\n=== \1 ===/g - const results: Record = {} - - let match: RegExpExecArray | null - while ((match = regex.exec(translated)) !== null) { - const key = match[1].trim() - const content = match[2].trim() - results[key] = content - } - - return results -} diff --git a/src/services/client.service.ts b/src/services/client.service.ts index d9b0711e..216b2ed0 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -2152,11 +2152,6 @@ class ClientService extends EventTarget { await this.updateReplaceableEventCache(evt) } - /** Fetch profile (kind 0) event; uses replaceable cache and IndexedDB. */ - async fetchProfileEvent(pubkey: string) { - return await this.fetchReplaceableEvent(pubkey, kinds.Metadata) - } - /** * Force-refresh profile (kind 0) and payment info (kind 10133) cache for a pubkey: * clears in-memory cache and IndexedDB so the next fetch loads from relays. diff --git a/src/services/libre-translate.service.ts b/src/services/libre-translate.service.ts deleted file mode 100644 index d17c5a3d..00000000 --- a/src/services/libre-translate.service.ts +++ /dev/null @@ -1,42 +0,0 @@ -class LibreTranslateService { - static instance: LibreTranslateService - - constructor() { - if (!LibreTranslateService.instance) { - LibreTranslateService.instance = this - } - return LibreTranslateService.instance - } - - async translate( - text: string, - target: string, - server?: string, - api_key?: string - ): Promise { - if (!text) { - return text - } - if (!server) { - throw new Error('LibreTranslate server address is not configured') - } - const url = new URL('/translate', server).toString() - const response = await fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ q: text, target, source: 'auto', api_key }) - }) - const data = await response.json() - if (!response.ok) { - throw new Error(data.error ?? 'Failed to translate') - } - const translatedText = data.translatedText - if (!translatedText) { - throw new Error('Translation failed') - } - return translatedText - } -} - -const instance = new LibreTranslateService() -export default instance diff --git a/src/services/local-storage.service.ts b/src/services/local-storage.service.ts index 48d16d0f..098745f7 100644 --- a/src/services/local-storage.service.ts +++ b/src/services/local-storage.service.ts @@ -20,7 +20,6 @@ import { TNotificationStyle, TRelaySet, TThemeSetting, - TTranslationServiceConfig } from '@/types' class LocalStorageService { @@ -43,7 +42,6 @@ class LocalStorageService { private hideUntrustedInteractions: boolean = false private hideUntrustedNotifications: boolean = false private hideUntrustedNotes: boolean = false - private translationServiceConfigMap: Record = {} private mediaUploadServiceConfigMap: Record = {} private defaultShowNsfw: boolean = false private dismissedTooManyRelaysAlert: boolean = false @@ -162,13 +160,6 @@ class LocalStorageService { ? storedHideUntrustedNotes === 'true' : hideUntrustedEvents - const translationServiceConfigMapStr = window.localStorage.getItem( - StorageKey.TRANSLATION_SERVICE_CONFIG_MAP - ) - if (translationServiceConfigMapStr) { - this.translationServiceConfigMap = JSON.parse(translationServiceConfigMapStr) - } - const mediaUploadServiceConfigMapStr = window.localStorage.getItem( StorageKey.MEDIA_UPLOAD_SERVICE_CONFIG_MAP ) @@ -546,18 +537,6 @@ class LocalStorageService { window.localStorage.setItem(StorageKey.HIDE_UNTRUSTED_NOTES, hideUntrustedNotes.toString()) } - getTranslationServiceConfig(pubkey?: string | null) { - return this.translationServiceConfigMap[pubkey ?? '_'] ?? { service: 'jumble' } - } - - setTranslationServiceConfig(config: TTranslationServiceConfig, pubkey?: string | null) { - this.translationServiceConfigMap[pubkey ?? '_'] = config - window.localStorage.setItem( - StorageKey.TRANSLATION_SERVICE_CONFIG_MAP, - JSON.stringify(this.translationServiceConfigMap) - ) - } - getMediaUploadServiceConfig(pubkey?: string | null): TMediaUploadServiceConfig { const defaultConfig = { type: 'nip96', service: this.mediaUploadService } as const if (!pubkey) { diff --git a/src/services/translation.service.ts b/src/services/translation.service.ts deleted file mode 100644 index 52af8e2a..00000000 --- a/src/services/translation.service.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { JUMBLE_API_BASE_URL } from '@/constants' -import client from '@/services/client.service' -import { TTranslationAccount } from '@/types' - -class TranslationService { - static instance: TranslationService - - private apiKeyMap: Record = {} - private currentPubkey: string | null = null - - constructor() { - if (!TranslationService.instance) { - TranslationService.instance = this - } - return TranslationService.instance - } - - async getAccount(): Promise { - if (!this.currentPubkey) { - throw new Error('Please login first') - } - const apiKey = this.apiKeyMap[this.currentPubkey] - const path = '/v1/translation/account' - const method = 'GET' - let auth: string | undefined - if (!apiKey) { - auth = await client.signHttpAuth( - new URL(path, JUMBLE_API_BASE_URL).toString(), - method, - 'Auth to get Jumble translation service account' - ) - } - const act = await this._fetch({ - path, - method, - auth, - retryWhenUnauthorized: !auth - }) - - if (act.api_key && act.pubkey) { - this.apiKeyMap[act.pubkey] = act.api_key - } - - return act - } - - async regenerateApiKey(): Promise { - try { - const data = await this._fetch({ - path: '/v1/translation/regenerate-api-key', - method: 'POST' - }) - if (data.api_key && this.currentPubkey) { - this.apiKeyMap[this.currentPubkey] = data.api_key - } - return data.api_key - } catch (error) { - const errMsg = error instanceof Error ? error.message : '' - throw new Error(errMsg || 'Failed to regenerate API key') - } - } - - async translate(text: string, target: string): Promise { - if (!text) { - return text - } - try { - const data = await this._fetch({ - path: '/v1/translation/translate', - method: 'POST', - body: JSON.stringify({ q: text, target }) - }) - const translatedText = data.translatedText - if (!translatedText) { - throw new Error('Translation failed') - } - return translatedText - } catch (error) { - const errMsg = error instanceof Error ? error.message : '' - throw new Error(errMsg || 'Failed to translate') - } - } - - changeCurrentPubkey(pubkey: string | null): void { - this.currentPubkey = pubkey - } - - private async _fetch({ - path, - method, - body, - auth, - retryWhenUnauthorized = true - }: { - path: string - method: string - body?: string - auth?: string - retryWhenUnauthorized?: boolean - }): Promise { - if (!this.currentPubkey) { - throw new Error('Please login first') - } - const apiKey = this.apiKeyMap[this.currentPubkey] - const hasApiKey = !!apiKey - let _auth: string - if (auth) { - _auth = auth - } else if (hasApiKey) { - _auth = `Bearer ${apiKey}` - } else { - const act = await this.getAccount() - _auth = `Bearer ${act.api_key}` - } - - const url = new URL(path, JUMBLE_API_BASE_URL).toString() - const response = await fetch(url, { - method, - headers: { 'Content-Type': 'application/json', Authorization: _auth }, - body - }) - - const data = await response.json() - if (!response.ok) { - if (data.code === '00403' && hasApiKey && retryWhenUnauthorized) { - this.apiKeyMap[this.currentPubkey] = undefined - return this._fetch({ path, method, body, retryWhenUnauthorized: false }) - } - throw new Error(data.error) - } - return data - } -} - -const instance = new TranslationService() -export default instance diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 5f819d46..b31900a1 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -186,22 +186,6 @@ export type TEmoji = { url: string } -export type TTranslationAccount = { - pubkey: string - api_key: string - balance: number -} - -export type TTranslationServiceConfig = - | { - service: 'jumble' - } - | { - service: 'libre_translate' - server?: string - api_key?: string - } - export type TMediaUploadServiceConfig = | { type: 'nip96'