diff --git a/src/components/RelaySetsSetting/TemporaryRelaySet.tsx b/src/components/RelaySetsSetting/TemporaryRelaySet.tsx index 5e03a22..7796dae 100644 --- a/src/components/RelaySetsSetting/TemporaryRelaySet.tsx +++ b/src/components/RelaySetsSetting/TemporaryRelaySet.tsx @@ -1,17 +1,15 @@ import { Button } from '@/components/ui/button' import { useFetchRelayInfos } from '@/hooks' -import { simplifyUrl } from '@/lib/url' import { useFeed } from '@/providers/FeedProvider' -import { useRelaySets } from '@/providers/RelaySetsProvider' import client from '@/services/client.service' -import { Save, SearchCheck } from 'lucide-react' +import { ListPlus, SearchCheck } from 'lucide-react' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' +import SaveRelayDropdownMenu from '../SaveRelayDropdownMenu' export default function TemporaryRelaySet() { const { t } = useTranslation() - const { temporaryRelayUrls, switchFeed } = useFeed() - const { addRelaySet } = useRelaySets() + const { temporaryRelayUrls } = useFeed() const [relays, setRelays] = useState< { url: string @@ -42,38 +40,35 @@ export default function TemporaryRelaySet() { return null } - const handleSave = () => { - const relaySetName = - temporaryRelayUrls.length === 1 ? simplifyUrl(temporaryRelayUrls[0]) : 'Temporary' - const id = addRelaySet(relaySetName, temporaryRelayUrls) - switchFeed('relays', { activeRelaySetId: id }) - } - return ( -
-
-
Temporary
- -
- {relays.map((relay, index) => ( -
-
- {relay.isConnected ? ( -
- ) : ( -
- )} -
{relay.url}
- {relayInfos[index]?.supported_nips?.includes(50) && ( -
- -
- )} -
+
+
+
+
Temporary
- ))} + {relays.map((relay, index) => ( +
+
+ {relay.isConnected ? ( +
+ ) : ( +
+ )} +
{relay.url}
+ {relayInfos[index]?.supported_nips?.includes(50) && ( +
+ +
+ )} +
+
+ ))} +
+ + +
) } diff --git a/src/components/SaveRelayDropdownMenu/index.tsx b/src/components/SaveRelayDropdownMenu/index.tsx new file mode 100644 index 0000000..6b3d765 --- /dev/null +++ b/src/components/SaveRelayDropdownMenu/index.tsx @@ -0,0 +1,87 @@ +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu' +import { normalizeUrl } from '@/lib/url' +import { useRelaySets } from '@/providers/RelaySetsProvider' +import { TRelaySet } from '@/types' +import { Check, FolderPlus, Plus } from 'lucide-react' +import { ReactNode, useMemo } from 'react' +import { useTranslation } from 'react-i18next' + +export default function SaveRelayDropdownMenu({ + children, + urls, + asChild = false +}: { + children: ReactNode + urls: string[] + asChild?: boolean +}) { + const { t } = useTranslation() + const { relaySets } = useRelaySets() + const normalizedUrls = useMemo(() => urls.map((url) => normalizeUrl(url)), [urls]) + return ( + + {children} + + {t('Save to')} ... + + {relaySets.map((set) => ( + + ))} + + + + + ) +} + +function RelaySetItem({ set, urls }: { set: TRelaySet; urls: string[] }) { + const { updateRelaySet } = useRelaySets() + const saved = urls.every((url) => set.relayUrls.includes(url)) + + const handleClick = () => { + if (saved) { + updateRelaySet({ + ...set, + relayUrls: set.relayUrls.filter((u) => !urls.includes(u)) + }) + } else { + updateRelaySet({ + ...set, + relayUrls: Array.from(new Set([...set.relayUrls, ...urls])) + }) + } + } + + return ( + + {saved ? : } + {set.name} + + ) +} + +function SaveToNewSet({ urls }: { urls: string[] }) { + const { t } = useTranslation() + const { addRelaySet } = useRelaySets() + + const handleSave = () => { + const newSetName = prompt(t('Enter a name for the new relay set')) + if (newSetName) { + addRelaySet(newSetName, urls) + } + } + + return ( + + + {t('Save to a new relay set')} + + ) +} diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 275cf86..2a2b310 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -142,6 +142,9 @@ export default { 'Enter the password to decrypt your ncryptsec': 'Enter the password to decrypt your ncryptsec', Back: 'Back', 'optional: encrypt nsec': 'optional: encrypt nsec', - password: 'password' + password: 'password', + 'Save to': 'Save to', + 'Enter a name for the new relay set': 'Enter a name for the new relay set', + 'Save to a new relay set': 'Save to a new relay set' } } diff --git a/src/i18n/zh.ts b/src/i18n/zh.ts index 0f05bf7..c9ca2ff 100644 --- a/src/i18n/zh.ts +++ b/src/i18n/zh.ts @@ -143,6 +143,9 @@ export default { Back: '返回', 'password (optional): encrypt nsec': '密码 (可选): 加密 nsec', 'optional: encrypt nsec': '可选: 加密 nsec', - password: '密码' + password: '密码', + 'Save to': '保存到', + 'Enter a name for the new relay set': '输入新服务器组的名称', + 'Save to a new relay set': '保存到新服务器组' } } diff --git a/src/pages/primary/NoteListPage/index.tsx b/src/pages/primary/NoteListPage/index.tsx index ca6ca5a..f340da2 100644 --- a/src/pages/primary/NoteListPage/index.tsx +++ b/src/pages/primary/NoteListPage/index.tsx @@ -5,6 +5,9 @@ import { useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' import FeedButton from './FeedButton' import SearchButton from './SearchButton' +import SaveRelayDropdownMenu from '@/components/SaveRelayDropdownMenu' +import { Button } from '@/components/ui/button' +import { ListPlus } from 'lucide-react' export default function NoteListPage() { const { t } = useTranslation() @@ -21,7 +24,9 @@ export default function NoteListPage() { } + titlebar={ + + } displayScrollToTopButton > {isReady ? ( @@ -33,11 +38,20 @@ export default function NoteListPage() { ) } -function NoteListPageTitlebar() { +function NoteListPageTitlebar({ temporaryRelayUrls = [] }: { temporaryRelayUrls?: string[] }) { return (
- +
+ + {temporaryRelayUrls.length > 0 && ( + + + + )} +
) } diff --git a/src/pages/secondary/NoteListPage/index.tsx b/src/pages/secondary/NoteListPage/index.tsx index e783c2f..3d8a7a9 100644 --- a/src/pages/secondary/NoteListPage/index.tsx +++ b/src/pages/secondary/NoteListPage/index.tsx @@ -1,9 +1,12 @@ import NoteList from '@/components/NoteList' +import SaveRelayDropdownMenu from '@/components/SaveRelayDropdownMenu' +import { Button } from '@/components/ui/button' import { SEARCHABLE_RELAY_URLS } from '@/constants' import { useFetchRelayInfos, useSearchParams } from '@/hooks' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import { isWebsocketUrl, simplifyUrl } from '@/lib/url' import { useFeed } from '@/providers/FeedProvider' +import { ListPlus } from 'lucide-react' import { Filter } from 'nostr-tools' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' @@ -17,33 +20,54 @@ export default function NoteListPage({ index }: { index?: number }) { const { title = '', filter, - urls + urls, + type } = useMemo<{ title?: string filter?: Filter urls: string[] + type?: 'search' | 'hashtag' | 'relay' }>(() => { const hashtag = searchParams.get('t') if (hashtag) { - return { title: `# ${hashtag}`, filter: { '#t': [hashtag] }, urls: relayUrls } + return { + title: `# ${hashtag}`, + filter: { '#t': [hashtag] }, + urls: relayUrls, + type: 'hashtag' + } } const search = searchParams.get('s') if (search) { return { title: `${t('Search')}: ${search}`, filter: { search }, - urls: searchableRelayUrls.concat(SEARCHABLE_RELAY_URLS).slice(0, 4) + urls: searchableRelayUrls.concat(SEARCHABLE_RELAY_URLS).slice(0, 4), + type: 'search' } } const relayUrl = searchParams.get('relay') if (relayUrl && isWebsocketUrl(relayUrl)) { - return { title: simplifyUrl(relayUrl), urls: [relayUrl] } + return { title: simplifyUrl(relayUrl), urls: [relayUrl], type: 'relay' } } return { urls: relayUrls } }, [searchParams, relayUrlsString]) return ( - + + + + ) + } + displayScrollToTopButton + > )