From c739d9d28ce1554378227d80ef54a523cbea7ad6 Mon Sep 17 00:00:00 2001 From: Cody Tseng Date: Sat, 5 Apr 2025 15:31:34 +0800 Subject: [PATCH] feat: favorite relays (#250) --- src/App.tsx | 6 +- src/components/DrawerMenuItem/index.tsx | 25 ++ .../FavoriteRelaysSetting/AddNewRelay.tsx | 56 +++++ .../FavoriteRelaysSetting/AddNewRelaySet.tsx | 42 ++++ .../FavoriteRelaysSetting/RelayItem.tsx | 19 ++ .../RelaySet.tsx | 136 ++++++----- .../RelayUrl.tsx | 25 +- .../TemporaryRelaySet.tsx | 28 +++ .../FavoriteRelaysSetting/index.tsx | 35 +++ .../provider.tsx | 14 +- src/components/FeedSwitcher/index.tsx | 90 +++---- src/components/Nip22ReplyNoteList/index.tsx | 4 +- src/components/NoteCard/GenericNoteCard.tsx | 4 +- src/components/NoteList/index.tsx | 4 +- src/components/NoteOptions/index.tsx | 2 +- src/components/NoteStats/RepostButton.tsx | 2 +- src/components/NoteStats/SeenOnButton.tsx | 2 +- .../NotificationItem/CommentNotification.tsx | 4 +- .../NotificationItem/ReactionNotification.tsx | 4 +- .../NotificationItem/index.tsx | 4 +- src/components/NotificationList/index.tsx | 6 +- src/components/ProfileOptions/index.tsx | 2 +- src/components/RelayIcon/index.tsx | 5 +- src/components/RelaySetCard/index.tsx | 85 ++----- .../RelaySetsSetting/PullFromRelaysButton.tsx | 174 -------------- .../RelaySetsSetting/PushToRelaysButton.tsx | 45 ---- .../RelaySetsSetting/TemporaryRelaySet.tsx | 69 ------ src/components/RelaySetsSetting/index.tsx | 77 ------ .../SaveRelayDropdownMenu/index.tsx | 141 +++++++++-- src/components/Sidebar/AccountButton.tsx | 2 +- src/components/ui/drawer.tsx | 1 + src/components/ui/dropdown-menu.tsx | 2 +- src/components/ui/popover.tsx | 1 + src/constants.ts | 25 +- src/i18n/locales/ar.ts | 13 +- src/i18n/locales/de.ts | 13 +- src/i18n/locales/en.ts | 13 +- src/i18n/locales/es.ts | 14 +- src/i18n/locales/fr.ts | 13 +- src/i18n/locales/it.ts | 30 +-- src/i18n/locales/ja.ts | 13 +- src/i18n/locales/pl.ts | 13 +- src/i18n/locales/pt-BR.ts | 13 +- src/i18n/locales/pt-PT.ts | 13 +- src/i18n/locales/ru.ts | 13 +- src/i18n/locales/zh.ts | 13 +- src/lib/draft-event.ts | 25 +- src/lib/event.ts | 30 ++- src/lib/link.ts | 2 +- src/pages/primary/MePage/index.tsx | 15 +- src/pages/primary/NoteListPage/FeedButton.tsx | 49 ++-- src/pages/primary/NoteListPage/index.tsx | 14 +- .../secondary/RelaySettingsPage/index.tsx | 16 +- src/pages/secondary/SettingsPage/index.tsx | 10 +- src/providers/FavoriteRelaysProvider.tsx | 226 ++++++++++++++++++ src/providers/FeedProvider.tsx | 101 +++++--- src/providers/NostrProvider/index.tsx | 50 +++- src/providers/NotificationProvider.tsx | 4 +- src/providers/RelaySetsProvider.tsx | 80 ------- src/services/client.service.ts | 4 - src/services/indexed-db.service.ts | 39 ++- src/services/local-storage.service.ts | 80 ++----- src/types.ts | 3 +- 63 files changed, 1081 insertions(+), 982 deletions(-) create mode 100644 src/components/DrawerMenuItem/index.tsx create mode 100644 src/components/FavoriteRelaysSetting/AddNewRelay.tsx create mode 100644 src/components/FavoriteRelaysSetting/AddNewRelaySet.tsx create mode 100644 src/components/FavoriteRelaysSetting/RelayItem.tsx rename src/components/{RelaySetsSetting => FavoriteRelaysSetting}/RelaySet.tsx (59%) rename src/components/{RelaySetsSetting => FavoriteRelaysSetting}/RelayUrl.tsx (80%) create mode 100644 src/components/FavoriteRelaysSetting/TemporaryRelaySet.tsx create mode 100644 src/components/FavoriteRelaysSetting/index.tsx rename src/components/{RelaySetsSetting => FavoriteRelaysSetting}/provider.tsx (71%) delete mode 100644 src/components/RelaySetsSetting/PullFromRelaysButton.tsx delete mode 100644 src/components/RelaySetsSetting/PushToRelaysButton.tsx delete mode 100644 src/components/RelaySetsSetting/TemporaryRelaySet.tsx delete mode 100644 src/components/RelaySetsSetting/index.tsx create mode 100644 src/providers/FavoriteRelaysProvider.tsx delete mode 100644 src/providers/RelaySetsProvider.tsx diff --git a/src/App.tsx b/src/App.tsx index ca959b27..bdbfb3dc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,12 +4,12 @@ import './index.css' import { Toaster } from '@/components/ui/toaster' import { ThemeProvider } from '@/providers/ThemeProvider' import { PageManager } from './PageManager' +import { FavoriteRelaysProvider } from './providers/FavoriteRelaysProvider' import { FeedProvider } from './providers/FeedProvider' import { FollowListProvider } from './providers/FollowListProvider' import { MuteListProvider } from './providers/MuteListProvider' import { NostrProvider } from './providers/NostrProvider' import { NoteStatsProvider } from './providers/NoteStatsProvider' -import { RelaySetsProvider } from './providers/RelaySetsProvider' import { ScreenSizeProvider } from './providers/ScreenSizeProvider' import { ZapProvider } from './providers/ZapProvider' @@ -19,7 +19,7 @@ export default function App(): JSX.Element { - + @@ -30,7 +30,7 @@ export default function App(): JSX.Element { - + diff --git a/src/components/DrawerMenuItem/index.tsx b/src/components/DrawerMenuItem/index.tsx new file mode 100644 index 00000000..341fd3cb --- /dev/null +++ b/src/components/DrawerMenuItem/index.tsx @@ -0,0 +1,25 @@ +import { Button } from '@/components/ui/button' +import { DrawerClose } from '@/components/ui/drawer' +import { cn } from '@/lib/utils' + +export default function DrawerMenuItem({ + children, + className, + onClick +}: { + children: React.ReactNode + className?: string + onClick?: (e: React.MouseEvent) => void +}) { + return ( + + + + ) +} diff --git a/src/components/FavoriteRelaysSetting/AddNewRelay.tsx b/src/components/FavoriteRelaysSetting/AddNewRelay.tsx new file mode 100644 index 00000000..d73ad382 --- /dev/null +++ b/src/components/FavoriteRelaysSetting/AddNewRelay.tsx @@ -0,0 +1,56 @@ +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { normalizeUrl } from '@/lib/url' +import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' + +export default function AddNewRelay() { + const { t } = useTranslation() + const { favoriteRelays, addFavoriteRelays } = useFavoriteRelays() + const [input, setInput] = useState('') + const [errorMsg, setErrorMsg] = useState('') + + const saveRelay = async () => { + if (!input) return + const normalizedUrl = normalizeUrl(input) + if (!normalizedUrl) { + setErrorMsg(t('Invalid URL')) + return + } + if (favoriteRelays.includes(normalizedUrl)) { + setErrorMsg(t('Already saved')) + return + } + await addFavoriteRelays([normalizedUrl]) + setInput('') + } + + const handleNewRelayInputChange = (e: React.ChangeEvent) => { + setInput(e.target.value) + setErrorMsg('') + } + + const handleNewRelayInputKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + event.preventDefault() + saveRelay() + } + } + + return ( +
+
+ + +
+ {errorMsg &&
{errorMsg}
} +
+ ) +} diff --git a/src/components/FavoriteRelaysSetting/AddNewRelaySet.tsx b/src/components/FavoriteRelaysSetting/AddNewRelaySet.tsx new file mode 100644 index 00000000..e09059de --- /dev/null +++ b/src/components/FavoriteRelaysSetting/AddNewRelaySet.tsx @@ -0,0 +1,42 @@ +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' + +export default function AddNewRelaySet() { + const { t } = useTranslation() + const { addRelaySet } = useFavoriteRelays() + const [newRelaySetName, setNewRelaySetName] = useState('') + + const saveRelaySet = () => { + if (!newRelaySetName) return + addRelaySet(newRelaySetName) + setNewRelaySetName('') + } + + const handleNewRelaySetNameChange = (e: React.ChangeEvent) => { + setNewRelaySetName(e.target.value) + } + + const handleNewRelaySetNameKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + event.preventDefault() + saveRelaySet() + } + } + + return ( +
+
+ + +
+
+ ) +} diff --git a/src/components/FavoriteRelaysSetting/RelayItem.tsx b/src/components/FavoriteRelaysSetting/RelayItem.tsx new file mode 100644 index 00000000..324c0e1d --- /dev/null +++ b/src/components/FavoriteRelaysSetting/RelayItem.tsx @@ -0,0 +1,19 @@ +import { toRelay } from '@/lib/link' +import { useSecondaryPage } from '@/PageManager' +import RelayIcon from '../RelayIcon' +import SaveRelayDropdownMenu from '../SaveRelayDropdownMenu' + +export default function RelayItem({ relay }: { relay: string }) { + const { push } = useSecondaryPage() + + return ( +
push(toRelay(relay))} + > + +
{relay}
+ +
+ ) +} diff --git a/src/components/RelaySetsSetting/RelaySet.tsx b/src/components/FavoriteRelaysSetting/RelaySet.tsx similarity index 59% rename from src/components/RelaySetsSetting/RelaySet.tsx rename to src/components/FavoriteRelaysSetting/RelaySet.tsx index b148cdd6..b066ba7d 100644 --- a/src/components/RelaySetsSetting/RelaySet.tsx +++ b/src/components/FavoriteRelaysSetting/RelaySet.tsx @@ -1,4 +1,5 @@ import { Button } from '@/components/ui/button' +import { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer' import { DropdownMenu, DropdownMenuContent, @@ -6,29 +7,35 @@ import { DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { Input } from '@/components/ui/input' -import { useRelaySets } from '@/providers/RelaySetsProvider' +import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' +import { useScreenSize } from '@/providers/ScreenSizeProvider' import { TRelaySet } from '@/types' -import { Check, ChevronDown, Circle, CircleCheck, EllipsisVertical } from 'lucide-react' -import { useMemo, useState } from 'react' +import { + Check, + ChevronDown, + Edit, + EllipsisVertical, + FolderClosed, + Link, + Trash2 +} from 'lucide-react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' +import DrawerMenuItem from '../DrawerMenuItem' import RelayUrls from './RelayUrl' import { useRelaySetsSettingComponent } from './provider' export default function RelaySet({ relaySet }: { relaySet: TRelaySet }) { const { t } = useTranslation() - const { expandedRelaySetId, selectedRelaySetIds } = useRelaySetsSettingComponent() - const isSelected = useMemo( - () => selectedRelaySetIds.includes(relaySet.id), - [selectedRelaySetIds, relaySet.id] - ) + const { expandedRelaySetId } = useRelaySetsSettingComponent() return ( -
+
-
- +
+
+ +
@@ -43,37 +50,10 @@ export default function RelaySet({ relaySet }: { relaySet: TRelaySet }) { ) } -function RelaySetActiveToggle({ relaySetId }: { relaySetId: string }) { - const { selectedRelaySetIds, toggleSelectedRelaySetId } = useRelaySetsSettingComponent() - const isSelected = useMemo( - () => selectedRelaySetIds.includes(relaySetId), - [selectedRelaySetIds, relaySetId] - ) - - const handleClick = () => { - toggleSelectedRelaySetId(relaySetId) - } - - return isSelected ? ( - - ) : ( - - ) -} - function RelaySetName({ relaySet }: { relaySet: TRelaySet }) { const [newSetName, setNewSetName] = useState(relaySet.name) - const { updateRelaySet } = useRelaySets() - const { renamingRelaySetId, setRenamingRelaySetId, toggleSelectedRelaySetId } = - useRelaySetsSettingComponent() + const { updateRelaySet } = useFavoriteRelays() + const { renamingRelaySetId, setRenamingRelaySetId } = useRelaySetsSettingComponent() const saveNewRelaySetName = () => { if (relaySet.name === newSetName) { @@ -108,12 +88,7 @@ function RelaySetName({ relaySet }: { relaySet: TRelaySet }) {
) : ( -
toggleSelectedRelaySetId(relaySet.id)} - > - {relaySet.name} -
+
{relaySet.name}
) } @@ -141,33 +116,70 @@ function RelayUrlsExpandToggle({ function RelaySetOptions({ relaySet }: { relaySet: TRelaySet }) { const { t } = useTranslation() - const { deleteRelaySet } = useRelaySets() + const { isSmallScreen } = useScreenSize() + const { deleteRelaySet } = useFavoriteRelays() const { setRenamingRelaySetId } = useRelaySetsSettingComponent() + const trigger = ( + + ) + + const rename = () => { + setRenamingRelaySetId(relaySet.id) + } + + const copyShareLink = () => { + navigator.clipboard.writeText( + `https://jumble.social/?${relaySet.relayUrls.map((url) => 'r=' + url).join('&')}` + ) + } + + if (isSmallScreen) { + return ( + + {trigger} + +
+ + + {t('Rename')} + + + + {t('Copy share link')} + + deleteRelaySet(relaySet.id)} + > + + {t('Delete')} + +
+
+
+ ) + } + return ( - - - + {trigger} - setRenamingRelaySetId(relaySet.id)}> + + {t('Rename')} - { - navigator.clipboard.writeText( - `https://jumble.social/?${relaySet.relayUrls.map((url) => 'r=' + url).join('&')}` - ) - }} - > + + {t('Copy share link')} deleteRelaySet(relaySet.id)} > + {t('Delete')} diff --git a/src/components/RelaySetsSetting/RelayUrl.tsx b/src/components/FavoriteRelaysSetting/RelayUrl.tsx similarity index 80% rename from src/components/RelaySetsSetting/RelayUrl.tsx rename to src/components/FavoriteRelaysSetting/RelayUrl.tsx index 5f0105ce..b3f21ece 100644 --- a/src/components/RelaySetsSetting/RelayUrl.tsx +++ b/src/components/FavoriteRelaysSetting/RelayUrl.tsx @@ -1,15 +1,15 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' -import { useFetchRelayInfo } from '@/hooks' import { isWebsocketUrl, normalizeUrl } from '@/lib/url' -import { useRelaySets } from '@/providers/RelaySetsProvider' -import { CircleX, SearchCheck } from 'lucide-react' +import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' +import { CircleX } from 'lucide-react' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import RelayIcon from '../RelayIcon' export default function RelayUrls({ relaySetId }: { relaySetId: string }) { const { t } = useTranslation() - const { relaySets, updateRelaySet } = useRelaySets() + const { relaySets, updateRelaySet } = useFavoriteRelays() const [newRelayUrl, setNewRelayUrl] = useState('') const [newRelayUrlError, setNewRelayUrlError] = useState(null) const relaySet = useMemo( @@ -79,20 +79,13 @@ export default function RelayUrls({ relaySetId }: { relaySetId: string }) { } function RelayUrl({ url, onRemove }: { url: string; onRemove: () => void }) { - const { t } = useTranslation() - const { relayInfo } = useFetchRelayInfo(url) - return ( -
-
-
{url}
- {relayInfo?.supported_nips?.includes(50) && ( -
- -
- )} +
+
+ +
{url}
-
+
+
+
+
Temporary
+
+ {temporaryRelayUrls.map((url) => ( +
+ +
{url}
+
+ ))} +
+ +
+ ) +} diff --git a/src/components/FavoriteRelaysSetting/index.tsx b/src/components/FavoriteRelaysSetting/index.tsx new file mode 100644 index 00000000..268277c7 --- /dev/null +++ b/src/components/FavoriteRelaysSetting/index.tsx @@ -0,0 +1,35 @@ +import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' +import { useTranslation } from 'react-i18next' +import AddNewRelay from './AddNewRelay' +import AddNewRelaySet from './AddNewRelaySet' +import { RelaySetsSettingComponentProvider } from './provider' +import RelayItem from './RelayItem' +import RelaySet from './RelaySet' +import TemporaryRelaySet from './TemporaryRelaySet' + +export default function FavoriteRelaysSetting() { + const { t } = useTranslation() + const { relaySets, favoriteRelays } = useFavoriteRelays() + + return ( + +
+ +
+
{t('Relay sets')}
+ {relaySets.map((relaySet) => ( + + ))} +
+ +
+
{t('Relays')}
+ {favoriteRelays.map((relay) => ( + + ))} +
+ +
+
+ ) +} diff --git a/src/components/RelaySetsSetting/provider.tsx b/src/components/FavoriteRelaysSetting/provider.tsx similarity index 71% rename from src/components/RelaySetsSetting/provider.tsx rename to src/components/FavoriteRelaysSetting/provider.tsx index 4a154b71..bd22de96 100644 --- a/src/components/RelaySetsSetting/provider.tsx +++ b/src/components/FavoriteRelaysSetting/provider.tsx @@ -5,8 +5,6 @@ type TRelaySetsSettingComponentContext = { setRenamingRelaySetId: React.Dispatch> expandedRelaySetId: string | null setExpandedRelaySetId: React.Dispatch> - selectedRelaySetIds: string[] - toggleSelectedRelaySetId: (relaySetId: string) => void } export const RelaySetsSettingComponentContext = createContext< @@ -26,7 +24,6 @@ export const useRelaySetsSettingComponent = () => { export function RelaySetsSettingComponentProvider({ children }: { children: React.ReactNode }) { const [renamingRelaySetId, setRenamingRelaySetId] = useState(null) const [expandedRelaySetId, setExpandedRelaySetId] = useState(null) - const [selectedRelaySetIds, setSelectedRelaySetIds] = useState([]) return ( { - setSelectedRelaySetIds((pre) => { - if (pre.includes(relaySetId)) { - return pre.filter((id) => id !== relaySetId) - } - return [...pre, relaySetId] - }) - } + setExpandedRelaySetId }} > {children} diff --git a/src/components/FeedSwitcher/index.tsx b/src/components/FeedSwitcher/index.tsx index 93420f34..c483d799 100644 --- a/src/components/FeedSwitcher/index.tsx +++ b/src/components/FeedSwitcher/index.tsx @@ -1,67 +1,71 @@ import { toRelaySettings } from '@/lib/link' import { simplifyUrl } from '@/lib/url' import { SecondaryPageLink } from '@/PageManager' +import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useFeed } from '@/providers/FeedProvider' import { useNostr } from '@/providers/NostrProvider' -import { useRelaySets } from '@/providers/RelaySetsProvider' -import { Circle, CircleCheck } from 'lucide-react' import { useTranslation } from 'react-i18next' +import RelayIcon from '../RelayIcon' import RelaySetCard from '../RelaySetCard' import SaveRelayDropdownMenu from '../SaveRelayDropdownMenu' +import { UsersRound } from 'lucide-react' export default function FeedSwitcher({ close }: { close?: () => void }) { const { t } = useTranslation() - const { feedType, switchFeed, activeRelaySetId, temporaryRelayUrls } = useFeed() const { pubkey } = useNostr() - const { relaySets } = useRelaySets() + const { relaySets, favoriteRelays } = useFavoriteRelays() + const { feedInfo, switchFeed, temporaryRelayUrls } = useFeed() return (
{pubkey && ( { if (!pubkey) return switchFeed('following', { pubkey }) close?.() }} - /> + > +
+
+ +
+
{t('Following')}
+
+
+ )} + {temporaryRelayUrls.length > 0 && ( + { + switchFeed('temporary') + close?.() + }} + controls={} + > + {temporaryRelayUrls.length === 1 ? simplifyUrl(temporaryRelayUrls[0]) : t('Temporary')} + )}
-
-
{t('relay sets')}
+
close?.()} > {t('edit')}
- {temporaryRelayUrls.length > 0 && ( - { - switchFeed('temporary') - close?.() - }} - controls={} - /> - )} {relaySets .filter((set) => set.relayUrls.length > 0) .map((set) => ( { if (!select) return switchFeed('relays', { activeRelaySetId: set.id }) @@ -69,19 +73,34 @@ export default function FeedSwitcher({ close }: { close?: () => void }) { }} /> ))} + {favoriteRelays.map((relay) => ( + { + switchFeed('relay', { relay }) + close?.() + }} + > +
+ +
{simplifyUrl(relay)}
+
+
+ ))}
) } function FeedSwitcherItem({ - itemName, + children, isActive, temporary = false, onClick, controls }: { - itemName: string + children: React.ReactNode isActive: boolean temporary?: boolean onClick: () => void @@ -93,20 +112,9 @@ function FeedSwitcherItem({ onClick={onClick} >
-
- -
{itemName}
-
+
{children}
{controls}
) } - -function FeedToggle({ isActive }: { isActive: boolean }) { - return isActive ? ( - - ) : ( - - ) -} diff --git a/src/components/Nip22ReplyNoteList/index.tsx b/src/components/Nip22ReplyNoteList/index.tsx index 7fc3600c..5b6e7090 100644 --- a/src/components/Nip22ReplyNoteList/index.tsx +++ b/src/components/Nip22ReplyNoteList/index.tsx @@ -1,5 +1,5 @@ import { Separator } from '@/components/ui/separator' -import { BIG_RELAY_URLS, COMMENT_EVENT_KIND } from '@/constants' +import { BIG_RELAY_URLS, ExtendedKind } from '@/constants' import { isCommentEvent, isProtectedEvent } from '@/lib/event' import { tagNameEquals } from '@/lib/tag' import { cn } from '@/lib/utils' @@ -71,7 +71,7 @@ export default function Nip22ReplyNoteList({ relayUrls.slice(0, 4), { '#E': [event.id], - kinds: [COMMENT_EVENT_KIND], + kinds: [ExtendedKind.COMMENT], limit: LIMIT }, { diff --git a/src/components/NoteCard/GenericNoteCard.tsx b/src/components/NoteCard/GenericNoteCard.tsx index 3899fb1d..14f1631a 100644 --- a/src/components/NoteCard/GenericNoteCard.tsx +++ b/src/components/NoteCard/GenericNoteCard.tsx @@ -1,4 +1,4 @@ -import { GROUP_METADATA_EVENT_KIND } from '@/constants' +import { ExtendedKind } from '@/constants' import { isSupportedKind } from '@/lib/event' import { useMuteList } from '@/providers/MuteListProvider' import { Event, kinds } from 'nostr-tools' @@ -58,7 +58,7 @@ export default function GenericNoteCard({ ) } - if (event.kind === GROUP_METADATA_EVENT_KIND) { + if (event.kind === ExtendedKind.GROUP_METADATA) { return ( listMode === 'pictures', [listMode]) const noteFilter = useMemo(() => { return { - kinds: isPictures ? [PICTURE_EVENT_KIND] : [kinds.ShortTextNote, kinds.Repost], + kinds: isPictures ? [ExtendedKind.PICTURE] : [kinds.ShortTextNote, kinds.Repost], ...filter } }, [JSON.stringify(filter), isPictures]) diff --git a/src/components/NoteOptions/index.tsx b/src/components/NoteOptions/index.tsx index c7f54c76..4dd13cb5 100644 --- a/src/components/NoteOptions/index.tsx +++ b/src/components/NoteOptions/index.tsx @@ -114,7 +114,7 @@ export default function NoteOptions({ event, className }: { event: Event; classN
e.stopPropagation()}> {trigger} - + navigator.clipboard.writeText(getSharableEventId(event))} > diff --git a/src/components/NoteStats/RepostButton.tsx b/src/components/NoteStats/RepostButton.tsx index 2f9b6349..95a6101e 100644 --- a/src/components/NoteStats/RepostButton.tsx +++ b/src/components/NoteStats/RepostButton.tsx @@ -135,7 +135,7 @@ export default function RepostButton({ event }: { event: Event }) { <> {trigger} - + { e.stopPropagation() diff --git a/src/components/NoteStats/SeenOnButton.tsx b/src/components/NoteStats/SeenOnButton.tsx index 2ad4753e..42598a44 100644 --- a/src/components/NoteStats/SeenOnButton.tsx +++ b/src/components/NoteStats/SeenOnButton.tsx @@ -79,7 +79,7 @@ export default function SeenOnButton({ event }: { event: Event }) { return ( {trigger} - + {t('Seen on')} {relays.map((relay) => ( diff --git a/src/components/NotificationList/NotificationItem/CommentNotification.tsx b/src/components/NotificationList/NotificationItem/CommentNotification.tsx index b07e9afc..33e6dd4c 100644 --- a/src/components/NotificationList/NotificationItem/CommentNotification.tsx +++ b/src/components/NotificationList/NotificationItem/CommentNotification.tsx @@ -1,4 +1,4 @@ -import { PICTURE_EVENT_KIND } from '@/constants' +import { ExtendedKind } from '@/constants' import { toNote } from '@/lib/link' import { tagNameEquals } from '@/lib/tag' import { cn } from '@/lib/utils' @@ -24,7 +24,7 @@ export function CommentNotification({ !rootEventId || !rootPubkey || !rootKind || - ![kinds.ShortTextNote, PICTURE_EVENT_KIND].includes(parseInt(rootKind)) + ![kinds.ShortTextNote, ExtendedKind.PICTURE].includes(parseInt(rootKind)) ) { return null } diff --git a/src/components/NotificationList/NotificationItem/ReactionNotification.tsx b/src/components/NotificationList/NotificationItem/ReactionNotification.tsx index 5da89192..9caf9020 100644 --- a/src/components/NotificationList/NotificationItem/ReactionNotification.tsx +++ b/src/components/NotificationList/NotificationItem/ReactionNotification.tsx @@ -1,5 +1,5 @@ import Image from '@/components/Image' -import { PICTURE_EVENT_KIND } from '@/constants' +import { ExtendedKind } from '@/constants' import { useFetchEvent } from '@/hooks' import { toNote } from '@/lib/link' import { extractEmojiFromEventTags, tagNameEquals } from '@/lib/tag' @@ -53,7 +53,7 @@ export function ReactionNotification({ return notification.content }, [notification]) - if (!event || !eventId || ![kinds.ShortTextNote, PICTURE_EVENT_KIND].includes(event.kind)) { + if (!event || !eventId || ![kinds.ShortTextNote, ExtendedKind.PICTURE].includes(event.kind)) { return null } diff --git a/src/components/NotificationList/NotificationItem/index.tsx b/src/components/NotificationList/NotificationItem/index.tsx index 784f948e..cd009425 100644 --- a/src/components/NotificationList/NotificationItem/index.tsx +++ b/src/components/NotificationList/NotificationItem/index.tsx @@ -1,4 +1,4 @@ -import { COMMENT_EVENT_KIND } from '@/constants' +import { ExtendedKind } from '@/constants' import { useMuteList } from '@/providers/MuteListProvider' import { Event, kinds } from 'nostr-tools' import { CommentNotification } from './CommentNotification' @@ -30,7 +30,7 @@ export function NotificationItem({ if (notification.kind === kinds.Zap) { return } - if (notification.kind === COMMENT_EVENT_KIND) { + if (notification.kind === ExtendedKind.COMMENT) { return } return null diff --git a/src/components/NotificationList/index.tsx b/src/components/NotificationList/index.tsx index 0f179a94..365bccbd 100644 --- a/src/components/NotificationList/index.tsx +++ b/src/components/NotificationList/index.tsx @@ -1,6 +1,6 @@ import { Separator } from '@/components/ui/separator' import { Skeleton } from '@/components/ui/skeleton' -import { BIG_RELAY_URLS, COMMENT_EVENT_KIND } from '@/constants' +import { BIG_RELAY_URLS, ExtendedKind } from '@/constants' import { cn } from '@/lib/utils' import { useDeepBrowsing } from '@/providers/DeepBrowsingProvider' import { useNostr } from '@/providers/NostrProvider' @@ -44,13 +44,13 @@ const NotificationList = forwardRef((_, ref) => { const filterKinds = useMemo(() => { switch (notificationType) { case 'mentions': - return [kinds.ShortTextNote, COMMENT_EVENT_KIND] + return [kinds.ShortTextNote, ExtendedKind.COMMENT] case 'reactions': return [kinds.Reaction, kinds.Repost] case 'zaps': return [kinds.Zap] default: - return [kinds.ShortTextNote, kinds.Repost, kinds.Reaction, kinds.Zap, COMMENT_EVENT_KIND] + return [kinds.ShortTextNote, kinds.Repost, kinds.Reaction, kinds.Zap, ExtendedKind.COMMENT] } }, [notificationType]) useImperativeHandle( diff --git a/src/components/ProfileOptions/index.tsx b/src/components/ProfileOptions/index.tsx index e81d17e4..26bdb302 100644 --- a/src/components/ProfileOptions/index.tsx +++ b/src/components/ProfileOptions/index.tsx @@ -25,7 +25,7 @@ export default function ProfileOptions({ pubkey }: { pubkey: string }) { - + navigator.clipboard.writeText('nostr:' + pubkeyToNpub(pubkey))} > diff --git a/src/components/RelayIcon/index.tsx b/src/components/RelayIcon/index.tsx index 1f5eac00..e0df1010 100644 --- a/src/components/RelayIcon/index.tsx +++ b/src/components/RelayIcon/index.tsx @@ -1,11 +1,12 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { useFetchRelayInfo } from '@/hooks' +import { cn } from '@/lib/utils' import { Server } from 'lucide-react' import { useMemo } from 'react' export default function RelayIcon({ url, - className = 'w-6 h-6', + className, iconSize = 14 }: { url?: string @@ -23,7 +24,7 @@ export default function RelayIcon({ }, [url, relayInfo]) return ( - + diff --git a/src/components/RelaySetCard/index.tsx b/src/components/RelaySetCard/index.tsx index 7597b362..54ebdade 100644 --- a/src/components/RelaySetCard/index.tsx +++ b/src/components/RelaySetCard/index.tsx @@ -1,33 +1,31 @@ -import client from '@/services/client.service' import { TRelaySet } from '@/types' -import { ChevronDown, Circle, CircleCheck } from 'lucide-react' -import { useEffect, useState } from 'react' +import { ChevronDown, FolderClosed } from 'lucide-react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' +import RelayIcon from '../RelayIcon' export default function RelaySetCard({ relaySet, select, - onSelectChange, - showConnectionStatus = false + onSelectChange }: { relaySet: TRelaySet select: boolean onSelectChange: (select: boolean) => void - showConnectionStatus?: boolean }) { const { t } = useTranslation() const [expand, setExpand] = useState(false) return (
onSelectChange(!select)} >
-
onSelectChange(!select)} - > - +
+
+ +
{relaySet.name}
@@ -36,21 +34,11 @@ export default function RelaySetCard({
- {expand && ( - - )} + {expand && }
) } -function RelaySetActiveToggle({ select }: { select: boolean }) { - return select ? ( - - ) : ( - - ) -} - function RelayUrlsExpandToggle({ children, expand, @@ -63,7 +51,10 @@ function RelayUrlsExpandToggle({ return (
onExpandChange(!expand)} + onClick={(e) => { + e.stopPropagation() + onExpandChange(!expand) + }} >
{children}
(urls.map((url) => ({ url, isConnected: false })) ?? []) - - useEffect(() => { - if (!showConnectionStatus || urls.length === 0) return - - const interval = setInterval(() => { - const connectionStatusMap = client.listConnectionStatus() - setRelays((pre) => { - return pre.map((relay) => { - const isConnected = connectionStatusMap.get(relay.url) || false - return { ...relay, isConnected } - }) - }) - }, 1000) - - return () => clearInterval(interval) - }, [showConnectionStatus, urls]) - +function RelayUrls({ urls }: { urls: string[] }) { if (!urls) return null return ( -
- {relays.map(({ url, isConnected: isConnected }, index) => ( -
- {showConnectionStatus && - (isConnected ? ( -
- ) : ( -
- ))} -
{url}
+
+ {urls.map((url) => ( +
+ +
{url}
))}
diff --git a/src/components/RelaySetsSetting/PullFromRelaysButton.tsx b/src/components/RelaySetsSetting/PullFromRelaysButton.tsx deleted file mode 100644 index d5b9a100..00000000 --- a/src/components/RelaySetsSetting/PullFromRelaysButton.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import { Button } from '@/components/ui/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger -} from '@/components/ui/dialog' -import { - Drawer, - DrawerContent, - DrawerDescription, - DrawerHeader, - DrawerTitle, - DrawerTrigger -} from '@/components/ui/drawer' -import { BIG_RELAY_URLS } from '@/constants' -import { tagNameEquals } from '@/lib/tag' -import { isWebsocketUrl, simplifyUrl } from '@/lib/url' -import { useNostr } from '@/providers/NostrProvider' -import { useRelaySets } from '@/providers/RelaySetsProvider' -import { useScreenSize } from '@/providers/ScreenSizeProvider' -import client from '@/services/client.service' -import { TRelaySet } from '@/types' -import { CloudDownload } from 'lucide-react' -import { kinds } from 'nostr-tools' -import { useEffect, useState } from 'react' -import { useTranslation } from 'react-i18next' -import RelaySetCard from '../RelaySetCard' - -export default function PullFromRelaysButton() { - const { t } = useTranslation() - const { pubkey } = useNostr() - const { isSmallScreen } = useScreenSize() - const [open, setOpen] = useState(false) - - const trigger = ( - - ) - - if (isSmallScreen) { - return ( - - {trigger} - -
- - {t('Select the relay sets you want to pull')} - - - setOpen(false)} /> -
-
-
- ) - } - - return ( - - {trigger} - - - {t('Select the relay sets you want to pull')} - - - setOpen(false)} /> - - - ) -} - -function RemoteRelaySets({ close }: { close?: () => void }) { - const { t } = useTranslation() - const { pubkey, relayList } = useNostr() - const { mergeRelaySets } = useRelaySets() - const [initialed, setInitialed] = useState(false) - const [relaySets, setRelaySets] = useState([]) - const [selectedRelaySetIds, setSelectedRelaySetIds] = useState([]) - - useEffect(() => { - if (!pubkey) return - - const init = async () => { - setInitialed(false) - const events = await client.fetchEvents( - (relayList?.write ?? []).concat(BIG_RELAY_URLS).slice(0, 4), - { - kinds: [kinds.Relaysets], - authors: [pubkey], - limit: 50 - } - ) - events.sort((a, b) => b.created_at - a.created_at) - - const relaySetIds = new Set() - const relaySets: TRelaySet[] = [] - events.forEach((evt) => { - const id = evt.tags.find(tagNameEquals('d'))?.[1] - if (!id || relaySetIds.has(id)) return - - relaySetIds.add(id) - const relayUrls = evt.tags - .filter(tagNameEquals('relay')) - .map((tag) => tag[1]) - .filter((url) => url && isWebsocketUrl(url)) - if (!relayUrls.length) return - - let title = evt.tags.find(tagNameEquals('title'))?.[1] - if (!title) { - title = relayUrls.length === 1 ? simplifyUrl(relayUrls[0]) : id - } - relaySets.push({ id, name: title, relayUrls }) - }) - - setRelaySets(relaySets) - setInitialed(true) - } - init() - }, [pubkey]) - - if (!pubkey) return null - if (!initialed) return
{t('loading...')}
- if (!relaySets.length) { - return
{t('No relay sets found')}
- } - - return ( -
-
- {relaySets.map((relaySet) => ( - { - if (select) { - setSelectedRelaySetIds([...selectedRelaySetIds, relaySet.id]) - } else { - setSelectedRelaySetIds(selectedRelaySetIds.filter((id) => id !== relaySet.id)) - } - }} - /> - ))} -
-
- - -
-
- ) -} diff --git a/src/components/RelaySetsSetting/PushToRelaysButton.tsx b/src/components/RelaySetsSetting/PushToRelaysButton.tsx deleted file mode 100644 index c24dbaae..00000000 --- a/src/components/RelaySetsSetting/PushToRelaysButton.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Button } from '@/components/ui/button' -import { useToast } from '@/hooks' -import { createRelaySetDraftEvent } from '@/lib/draft-event' -import { useNostr } from '@/providers/NostrProvider' -import { useRelaySets } from '@/providers/RelaySetsProvider' -import { CloudUpload, Loader } from 'lucide-react' -import { useState } from 'react' -import { useTranslation } from 'react-i18next' -import { useRelaySetsSettingComponent } from './provider' - -export default function PushToRelaysButton() { - const { t } = useTranslation() - const { toast } = useToast() - const { pubkey, publish } = useNostr() - const { relaySets } = useRelaySets() - const { selectedRelaySetIds } = useRelaySetsSettingComponent() - const [pushing, setPushing] = useState(false) - - const push = async () => { - const selectedRelaySets = relaySets.filter((r) => selectedRelaySetIds.includes(r.id)) - if (!selectedRelaySets.length) return - - setPushing(true) - const draftEvents = selectedRelaySets.map((relaySet) => createRelaySetDraftEvent(relaySet)) - await Promise.allSettled(draftEvents.map((event) => publish(event))) - toast({ - title: t('Push Successful'), - description: t('Successfully pushed relay sets to relays') - }) - setPushing(false) - } - - return ( - - ) -} diff --git a/src/components/RelaySetsSetting/TemporaryRelaySet.tsx b/src/components/RelaySetsSetting/TemporaryRelaySet.tsx deleted file mode 100644 index 19063076..00000000 --- a/src/components/RelaySetsSetting/TemporaryRelaySet.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { useFetchRelayInfos } from '@/hooks' -import { useFeed } from '@/providers/FeedProvider' -import client from '@/services/client.service' -import { 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 } = useFeed() - const [relays, setRelays] = useState< - { - url: string - isConnected: boolean - }[] - >(temporaryRelayUrls.map((url) => ({ url, isConnected: false }))) - const { relayInfos } = useFetchRelayInfos(relays.map((relay) => relay.url)) - - useEffect(() => { - const interval = setInterval(() => { - const connectionStatusMap = client.listConnectionStatus() - setRelays((pre) => { - return pre.map((relay) => { - const isConnected = connectionStatusMap.get(relay.url) || false - return { ...relay, isConnected } - }) - }) - }, 1000) - - return () => clearInterval(interval) - }, []) - - useEffect(() => { - setRelays(temporaryRelayUrls.map((url) => ({ url, isConnected: false }))) - }, [temporaryRelayUrls]) - - if (!relays.length) { - return null - } - - return ( -
-
-
-
Temporary
-
- {relays.map((relay, index) => ( -
-
- {relay.isConnected ? ( -
- ) : ( -
- )} -
{relay.url}
- {relayInfos[index]?.supported_nips?.includes(50) && ( -
- -
- )} -
-
- ))} -
- -
- ) -} diff --git a/src/components/RelaySetsSetting/index.tsx b/src/components/RelaySetsSetting/index.tsx deleted file mode 100644 index 793ab80e..00000000 --- a/src/components/RelaySetsSetting/index.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Separator } from '@/components/ui/separator' -import { useRelaySets } from '@/providers/RelaySetsProvider' -import { useEffect, useRef, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { RelaySetsSettingComponentProvider } from './provider' -import RelaySet from './RelaySet' -import TemporaryRelaySet from './TemporaryRelaySet' -import PushToRelaysButton from './PushToRelaysButton' -import PullFromRelaysButton from './PullFromRelaysButton' - -export default function RelaySetsSetting() { - const { t } = useTranslation() - const { relaySets, addRelaySet } = useRelaySets() - const [newRelaySetName, setNewRelaySetName] = useState('') - const dummyRef = useRef(null) - - useEffect(() => { - if (dummyRef.current) { - dummyRef.current.focus() - } - }, []) - - const saveRelaySet = () => { - if (!newRelaySetName) return - addRelaySet(newRelaySetName) - setNewRelaySetName('') - } - - const handleNewRelaySetNameChange = (e: React.ChangeEvent) => { - setNewRelaySetName(e.target.value) - } - - const handleNewRelaySetNameKeyDown = (event: React.KeyboardEvent) => { - if (event.key === 'Enter') { - event.preventDefault() - saveRelaySet() - } - } - - return ( - -
-
- - -
-
- - {relaySets.map((relaySet) => ( - - ))} -
- {relaySets.length < 10 && ( - <> - -
-
-
{t('Add a new relay set')}
-
-
- - -
-
- - )} -
- ) -} diff --git a/src/components/SaveRelayDropdownMenu/index.tsx b/src/components/SaveRelayDropdownMenu/index.tsx index 47eaf579..2bf891dd 100644 --- a/src/components/SaveRelayDropdownMenu/index.tsx +++ b/src/components/SaveRelayDropdownMenu/index.tsx @@ -1,4 +1,11 @@ import { Button } from '@/components/ui/button' +import { + Drawer, + DrawerContent, + DrawerHeader, + DrawerOverlay, + DrawerTitle +} from '@/components/ui/drawer' import { DropdownMenu, DropdownMenuContent, @@ -7,12 +14,15 @@ import { DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' +import { Separator } from '@/components/ui/separator' import { normalizeUrl } from '@/lib/url' -import { useRelaySets } from '@/providers/RelaySetsProvider' +import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' +import { useScreenSize } from '@/providers/ScreenSizeProvider' import { TRelaySet } from '@/types' import { Check, FolderPlus, Plus, Star } from 'lucide-react' -import { useMemo } from 'react' +import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import DrawerMenuItem from '../DrawerMenuItem' export default function SaveRelayDropdownMenu({ urls, @@ -22,29 +32,66 @@ export default function SaveRelayDropdownMenu({ atTitlebar?: boolean }) { const { t } = useTranslation() - const { relaySets } = useRelaySets() + const { isSmallScreen } = useScreenSize() + const { favoriteRelays, relaySets } = useFavoriteRelays() const normalizedUrls = useMemo(() => urls.map((url) => normalizeUrl(url)).filter(Boolean), [urls]) - const alreadySaved = useMemo( - () => relaySets.some((set) => normalizedUrls.every((url) => set.relayUrls.includes(url))), - [relaySets, normalizedUrls] + const alreadySaved = useMemo(() => { + return ( + normalizedUrls.every((url) => favoriteRelays.includes(url)) || + relaySets.some((set) => normalizedUrls.every((url) => set.relayUrls.includes(url))) + ) + }, [relaySets, normalizedUrls]) + const [isDrawerOpen, setIsDrawerOpen] = useState(false) + + const trigger = atTitlebar ? ( + + ) : ( + ) + if (isSmallScreen) { + return ( + <> + {trigger} +
e.stopPropagation()}> + + setIsDrawerOpen(false)} /> + + + {t('Save to')} ... + +
+ + {relaySets.map((set) => ( + + ))} + + +
+
+
+
+ + ) + } + return ( - - {atTitlebar ? ( - - ) : ( - - )} - - + {trigger} + e.stopPropagation()}> {t('Save to')} ... + {relaySets.map((set) => ( ))} @@ -55,8 +102,43 @@ export default function SaveRelayDropdownMenu({ ) } +function RelayItem({ urls }: { urls: string[] }) { + const { t } = useTranslation() + const { isSmallScreen } = useScreenSize() + const { favoriteRelays, addFavoriteRelays, deleteFavoriteRelays } = useFavoriteRelays() + const saved = useMemo( + () => urls.every((url) => favoriteRelays.includes(url)), + [favoriteRelays, urls] + ) + + const handleClick = async () => { + if (saved) { + await deleteFavoriteRelays(urls) + } else { + await addFavoriteRelays(urls) + } + } + + if (isSmallScreen) { + return ( + + {saved ? : } + {t('Favorite')} + + ) + } + + return ( + + {saved ? : } + {t('Favorite')} + + ) +} + function RelaySetItem({ set, urls }: { set: TRelaySet; urls: string[] }) { - const { updateRelaySet } = useRelaySets() + const { isSmallScreen } = useScreenSize() + const { updateRelaySet } = useFavoriteRelays() const saved = urls.every((url) => set.relayUrls.includes(url)) const handleClick = () => { @@ -73,6 +155,15 @@ function RelaySetItem({ set, urls }: { set: TRelaySet; urls: string[] }) { } } + if (isSmallScreen) { + return ( + + {saved ? : } + {set.name} + + ) + } + return ( {saved ? : } @@ -83,7 +174,8 @@ function RelaySetItem({ set, urls }: { set: TRelaySet; urls: string[] }) { function SaveToNewSet({ urls }: { urls: string[] }) { const { t } = useTranslation() - const { addRelaySet } = useRelaySets() + const { isSmallScreen } = useScreenSize() + const { addRelaySet } = useFavoriteRelays() const handleSave = () => { const newSetName = prompt(t('Enter a name for the new relay set')) @@ -92,6 +184,15 @@ function SaveToNewSet({ urls }: { urls: string[] }) { } } + if (isSmallScreen) { + return ( + + + {t('Save to a new relay set')} + + ) + } + return ( diff --git a/src/components/Sidebar/AccountButton.tsx b/src/components/Sidebar/AccountButton.tsx index 4f512de0..e8ef0a17 100644 --- a/src/components/Sidebar/AccountButton.tsx +++ b/src/components/Sidebar/AccountButton.tsx @@ -58,7 +58,7 @@ function ProfileButton() {
- + push(toProfile(pubkey))}> {t('Profile')} diff --git a/src/components/ui/drawer.tsx b/src/components/ui/drawer.tsx index f283ea2e..a762ef2e 100644 --- a/src/components/ui/drawer.tsx +++ b/src/components/ui/drawer.tsx @@ -44,6 +44,7 @@ const DrawerContent = React.forwardRef< style={{ paddingBottom: 'env(safe-area-inset-bottom)' }} + onOpenAutoFocus={(e) => e.preventDefault()} {...props} >
diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx index be47874d..37cd1f53 100644 --- a/src/components/ui/dropdown-menu.tsx +++ b/src/components/ui/dropdown-menu.tsx @@ -61,7 +61,7 @@ const DropdownMenuContent = React.forwardRef< ref={ref} sideOffset={sideOffset} className={cn( - 'z-50 min-w-[8rem] overflow-hidden rounded-lg border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', + 'z-50 min-w-52 overflow-hidden rounded-lg border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', className )} collisionPadding={10} diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx index ce5bf71e..89324a9e 100644 --- a/src/components/ui/popover.tsx +++ b/src/components/ui/popover.tsx @@ -23,6 +23,7 @@ const PopoverContent = React.forwardRef< 'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', className )} + onOpenAutoFocus={(e) => e.preventDefault()} {...props} /> diff --git a/src/constants.ts b/src/constants.ts index 70e667a5..4505b00c 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,8 +1,7 @@ export const StorageKey = { + VERSION: 'version', THEME_SETTING: 'themeSetting', RELAY_SETS: 'relaySets', - ACTIVE_RELAY_SET_ID: 'activeRelaySetId', - FEED_TYPE: 'feedType', ACCOUNTS: 'accounts', CURRENT_ACCOUNT: 'currentAccount', ADD_CLIENT_TAG: 'addClientTag', @@ -12,11 +11,14 @@ export const StorageKey = { DEFAULT_ZAP_COMMENT: 'defaultZapComment', QUICK_ZAP: 'quickZap', LAST_READ_NOTIFICATION_TIME_MAP: 'lastReadNotificationTimeMap', + ACCOUNT_FEED_INFO_MAP: 'accountFeedInfoMap', ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap', // deprecated ACCOUNT_FOLLOW_LIST_EVENT_MAP: 'accountFollowListEventMap', // deprecated ACCOUNT_MUTE_LIST_EVENT_MAP: 'accountMuteListEventMap', // deprecated ACCOUNT_MUTE_DECRYPTED_TAGS_MAP: 'accountMuteDecryptedTagsMap', // deprecated - ACCOUNT_PROFILE_EVENT_MAP: 'accountProfileEventMap' // deprecated + ACCOUNT_PROFILE_EVENT_MAP: 'accountProfileEventMap', // deprecated + ACTIVE_RELAY_SET_ID: 'activeRelaySetId', // deprecated + FEED_TYPE: 'feedType' // deprecated } export const BIG_RELAY_URLS = [ @@ -28,10 +30,15 @@ export const BIG_RELAY_URLS = [ export const SEARCHABLE_RELAY_URLS = ['wss://relay.nostr.band/', 'wss://search.nos.today/'] -export const PICTURE_EVENT_KIND = 20 -export const COMMENT_EVENT_KIND = 1111 export const GROUP_METADATA_EVENT_KIND = 39000 +export const ExtendedKind = { + PICTURE: 20, + FAVORITE_RELAYS: 10012, + COMMENT: 1111, + GROUP_METADATA: 39000 +} + export const URL_REGEX = /https?:\/\/[\w\p{L}\p{N}\p{M}&.-/?=#\-@%+_:!~*]+/gu export const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ @@ -39,3 +46,11 @@ export const MONITOR = '9bbbb845e5b6c831c29789900769843ab43bb5047abe697870cb50b6 export const MONITOR_RELAYS = ['wss://relay.nostr.watch/'] export const CODY_PUBKEY = '8125b911ed0e94dbe3008a0be48cfe5cd0c0b05923cfff917ae7e87da8400883' + +export const DEFAULT_FAVORITE_RELAYS = [ + 'wss://nostr.wine/', + 'wss://pyramid.fiatjaf.com/', + 'wss://140.f7z.io/', + 'wss://news.utxo.one/', + 'wss://algo.utxo.one' +] diff --git a/src/i18n/locales/ar.ts b/src/i18n/locales/ar.ts index 587136a7..aeb4fd1b 100644 --- a/src/i18n/locales/ar.ts +++ b/src/i18n/locales/ar.ts @@ -74,7 +74,6 @@ export default { 'Reply to': 'الرد على', Search: 'بحث', 'The relays you are connected to do not support search': 'الريلايات المتصلة لا تدعم البحث', - 'supports search': 'يدعم البحث', 'Show more...': 'عرض المزيد...', 'All users': 'جميع المستخدمين', 'Display replies': 'عرض الردود', @@ -116,14 +115,6 @@ export default { 'R & W': 'قراءة وكتابة', Read: 'قراءة', Write: 'كتابة', - 'Push to relays': 'إرسال إلى الريلايات', - 'Push Successful': 'تم الإرسال بنجاح', - 'Successfully pushed relay sets to relays': 'تم إرسال مجموعات الريلاي إلى الريلايات بنجاح', - 'Pull from relays': 'استلام من الريلايات', - 'Select the relay sets you want to pull': 'اختر مجموعات الريلاي التي تريد استلامها', - 'No relay sets found': 'لم يتم العثور على مجموعات ريلاي', - 'Pull n relay sets': 'سحب {{n}} مجموعات ريلاي', - Pull: 'سحب', 'Select all': 'اختر الكل', 'Relay Sets': 'مجموعات الريلاي', 'Read & Write Relays': 'ريلايات القراءة والكتابة', @@ -210,6 +201,8 @@ export default { 'Seen on': 'شوهد على', 'Temporarily display this reply': 'عرض هذا الرد مؤقتاً', 'Not found the note': 'لم يتم العثور على الملاحظة', - 'no more replies': 'لا توجد مزيد من الردود' + 'no more replies': 'لا توجد مزيد من الردود', + 'Relay sets': 'مجموعات الريلاي', + 'Favorite Relays': 'الريلايات المفضلة' } } diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 4f5f3008..d225a782 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -75,7 +75,6 @@ export default { Search: 'Suchen', 'The relays you are connected to do not support search': 'Die verbundenen Relays unterstützen keine Suche', - 'supports search': 'unterstützt Suche', 'Show more...': 'Mehr anzeigen...', 'All users': 'Alle Benutzer', 'Display replies': 'Antworten anzeigen', @@ -117,14 +116,6 @@ export default { 'R & W': 'R & W', Read: 'Lesen', Write: 'Schreiben', - 'Push to relays': 'An Relays senden', - 'Push Successful': 'Senden erfolgreich', - 'Successfully pushed relay sets to relays': 'Relay-Sets erfolgreich an Relays gesendet', - 'Pull from relays': 'Von Relays abrufen', - 'Select the relay sets you want to pull': 'Wähle die Relay-Sets, die du abrufen möchtest', - 'No relay sets found': 'Keine Relay-Sets gefunden', - 'Pull n relay sets': 'Hole {{n}} Relay-Sets', - Pull: 'Abrufen', 'Select all': 'Alle auswählen', 'Relay Sets': 'Relay-Sets', 'Read & Write Relays': 'Lese- & Schreib-Relays', @@ -214,6 +205,8 @@ export default { 'Seen on': 'Gesehen auf', 'Temporarily display this reply': 'Antwort vorübergehend anzeigen', 'Not found the note': 'Die Notiz wurde nicht gefunden', - 'no more replies': 'keine weiteren Antworten' + 'no more replies': 'keine weiteren Antworten', + 'Relay sets': 'Relay-Sets', + 'Favorite Relays': 'Lieblings-Relays' } } diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 36b3b7e5..62c0f5bb 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -74,7 +74,6 @@ export default { Search: 'Search', 'The relays you are connected to do not support search': 'The relays you are connected to do not support search', - 'supports search': 'supports search', 'Show more...': 'Show more...', 'All users': 'All users', 'Display replies': 'Display replies', @@ -116,14 +115,6 @@ export default { 'R & W': 'R & W', Read: 'Read', Write: 'Write', - 'Push to relays': 'Push to relays', - 'Push Successful': 'Push Successful', - 'Successfully pushed relay sets to relays': 'Successfully pushed relay sets to relays', - 'Pull from relays': 'Pull from relays', - 'Select the relay sets you want to pull': 'Select the relay sets you want to pull', - 'No relay sets found': 'No relay sets found', - 'Pull n relay sets': 'Pull {{n}} relay sets', - Pull: 'Pull', 'Select all': 'Select all', 'Relay Sets': 'Relay Sets', 'Read & Write Relays': 'Read & Write Relays', @@ -210,6 +201,8 @@ export default { 'Seen on': 'Seen on', 'Temporarily display this reply': 'Temporarily display this reply', 'Not found the note': 'Not found the note', - 'no more replies': 'no more replies' + 'no more replies': 'no more replies', + 'Relay sets': 'Relay sets', + 'Favorite Relays': 'Favorite Relays' } } diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index 9dcf09d0..001ed97b 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -75,7 +75,6 @@ export default { Search: 'Buscar', 'The relays you are connected to do not support search': 'Los relés a los que estás conectado no soportan la búsqueda', - 'supports search': 'soporta la búsqueda', 'Show more...': 'Mostrar más...', 'All users': 'Todos los usuarios', 'Display replies': 'Mostrar respuestas', @@ -117,15 +116,6 @@ export default { 'R & W': 'L y E', Read: 'Leer', Write: 'Escribir', - 'Push to relays': 'Enviar a relés', - 'Push Successful': 'Envío exitoso', - 'Successfully pushed relay sets to relays': 'Conjuntos de relés enviados con éxito a relés', - 'Pull from relays': 'Recibir de relés', - 'Select the relay sets you want to pull': - 'Selecciona los conjuntos de relés que deseas recibir', - 'No relay sets found': 'No se encontraron conjuntos de relés', - 'Pull n relay sets': 'Recibir {{n}} conjuntos de relés', - Pull: 'Recibir', 'Select all': 'Seleccionar todo', 'Relay Sets': 'Conjuntos de relés', 'Read & Write Relays': 'Relés de lectura y escritura', @@ -214,6 +204,8 @@ export default { 'Seen on': 'Visto en', 'Temporarily display this reply': 'Mostrar temporalmente esta respuesta', 'Not found the note': 'No se encontró la nota', - 'no more replies': 'no hay más respuestas' + 'no more replies': 'no hay más respuestas', + 'Relay sets': 'Conjuntos de relés', + 'Favorite Relays': 'Relés favoritos' } } diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index c59b66d8..1b9635f1 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -75,7 +75,6 @@ export default { Search: 'Recherche', 'The relays you are connected to do not support search': 'Les relais auxquels vous êtes connecté ne prennent pas en charge la recherche', - 'supports search': 'prend en charge la recherche', 'Show more...': 'Afficher plus...', 'All users': 'Tous les utilisateurs', 'Display replies': 'Afficher les réponses', @@ -117,14 +116,6 @@ export default { 'R & W': 'R & W', Read: 'Lire', Write: 'Écrire', - 'Push to relays': 'Envoyer aux relais', - 'Push Successful': 'Envoi réussi', - 'Successfully pushed relay sets to relays': 'Groupes de relais envoyés avec succès aux relais', - 'Pull from relays': 'Récupérer depuis les relais', - 'Select the relay sets you want to pull': 'Sélectionnez les groupes de relais à récupérer', - 'No relay sets found': 'Aucun groupe de relais trouvé', - 'Pull n relay sets': 'Récupérer {{n}} groupes de relais', - Pull: 'Récupérer', 'Select all': 'Tout sélectionner', 'Relay Sets': 'Groupes de relais', 'Read & Write Relays': 'Relais lecture & écriture', @@ -213,6 +204,8 @@ export default { 'Seen on': 'Vu sur', 'Temporarily display this reply': 'Afficher temporairement cette réponse', 'Not found the note': 'Note introuvable', - 'no more replies': 'aucune autre réponse' + 'no more replies': 'aucune autre réponse', + 'Relay sets': 'Groupes de relais', + 'Favorite Relays': 'Relais favoris' } } diff --git a/src/i18n/locales/it.ts b/src/i18n/locales/it.ts index d98d10b5..733a78bf 100644 --- a/src/i18n/locales/it.ts +++ b/src/i18n/locales/it.ts @@ -50,9 +50,9 @@ export default { 'switch to system theme': 'passa al tema di sistema', Note: 'Nota', note: 'nota', - "username's following": "{{username}} seguiti", - "username's used relays": "{{username}} relays usati", - "username's muted": "{{username}} zittiti", + "username's following": '{{username}} seguiti', + "username's used relays": '{{username}} relays usati', + "username's muted": '{{username}} zittiti', Login: 'Accedi', 'Follows you': 'Ti segue', 'Relay Settings': 'Impostazioni Relay', @@ -74,7 +74,6 @@ export default { Search: 'Ricerca', 'The relays you are connected to do not support search': 'I relays a cui siete collegati non supportano la ricerca.', - 'supports search': 'ricerche supportate', 'Show more...': 'Mostra di più...', 'All users': 'Tutti gli utenti', 'Display replies': 'Visualizza repliche', @@ -92,7 +91,8 @@ export default { 'Add an Account': 'Aggiungi un Account', 'More options': 'Più opzioni', 'Add client tag': 'Aggiungi etichetta del client', - 'Show others this was sent via Jumble': 'Mostra agli altri che questo è stato inviato tramite Jumble', + 'Show others this was sent via Jumble': + 'Mostra agli altri che questo è stato inviato tramite Jumble', 'Are you sure you want to logout?': 'Sei sicuro di volerti scollegare?', 'relay sets': 'set di relay', edit: 'modifica', @@ -116,14 +116,6 @@ export default { 'R & W': 'L & S', Read: 'Leggi', Write: 'Scrivi', - 'Push to relays': 'Invia ai relays', - 'Push Successful': 'Invio riuscito', - 'Successfully pushed relay sets to relays': 'Invio riuscito del set di relay ai relays', - 'Pull from relays': 'Ottieni dai relays', - 'Select the relay sets you want to pull': 'Selezionare i set di relay che si desidera ottenere', - 'No relay sets found': 'Nessun set di relay trovato', - 'Pull n relay sets': 'Ottieni {{n}} set di relay', - Pull: 'Ottieni', 'Select all': 'Seleziona tutto', 'Relay Sets': 'Set di Relay', 'Read & Write Relays': 'Relay Leggi & Scrivi', @@ -133,7 +125,7 @@ export default { 'I relay di scrittura sono utilizzati per pubblicare i tuoi eventi. Gli altri utenti cercheranno i tuoi eventi dai vostri relay di scrittura.', 'read & write relays notice': 'Il numero di server di lettura e scrittura dovrebbe essere mantenuto idealmente tra 2 e 4.', - "Don't have an account yet?": "Non hai ancora un account?", + "Don't have an account yet?": 'Non hai ancora un account?', 'or simply generate a private key': 'o semplicemente genera una chiave privata', 'This is a private key. Do not share it with anyone. Keep it safe and secure. You will not be able to recover it if you lose it.': 'Questa è una chiave privata. Non condividetela con nessuno. Conservatela al sicuro. Non sarà possibile recuperarla in caso di smarrimento.', @@ -144,7 +136,8 @@ export default { 'Nostr Address (NIP-05)': 'Indirizzo Nostr (NIP-05)', 'Invalid NIP-05 address': 'Indirizzo NIP-05 non valido', 'Copy private key': 'Copia la chiave privata', - 'Enter the password to decrypt your ncryptsec': 'Inserisci la password per decriptare la tua ncryptsec', + 'Enter the password to decrypt your ncryptsec': + 'Inserisci la password per decriptare la tua ncryptsec', Back: 'Indietro', 'optional: encrypt nsec': 'opzione: cripta nsec', password: 'password', @@ -205,11 +198,14 @@ export default { 'Earlier notifications': 'Notifiche precedenti', 'Temporarily display this note': 'Visualizza temporaneamente questa nota', buttonFollowing: 'Seguendo', - 'Are you sure you want to unfollow this user?': 'Sei sicuro di voler disiscrivere questo utente?', + 'Are you sure you want to unfollow this user?': + 'Sei sicuro di voler disiscrivere questo utente?', 'Recent Supporters': 'Recenti Sostenitori', 'Seen on': 'Visto su', 'Temporarily display this reply': 'Mostra temporaneamente questa replica', 'Not found the note': 'Non è stata trovata la nota', - 'no more replies': 'niente più repliche' + 'no more replies': 'niente più repliche', + 'Relay sets': 'Set di Relay', + 'Favorite Relays': 'Relay preferiti' } } diff --git a/src/i18n/locales/ja.ts b/src/i18n/locales/ja.ts index 43bdcf7c..b4cd7997 100644 --- a/src/i18n/locales/ja.ts +++ b/src/i18n/locales/ja.ts @@ -75,7 +75,6 @@ export default { Search: '検索', 'The relays you are connected to do not support search': '接続しているリレイは検索をサポートしていません', - 'supports search': '検索対応', 'Show more...': 'さらに表示...', 'All users': '全ユーザー', 'Display replies': '返信を表示', @@ -117,14 +116,6 @@ export default { 'R & W': '読&書', Read: '読む', Write: '書く', - 'Push to relays': 'リレイへプッシュ', - 'Push Successful': 'プッシュ成功', - 'Successfully pushed relay sets to relays': 'リレイセットをリレイへ正常にプッシュしました', - 'Pull from relays': 'リレイからプル', - 'Select the relay sets you want to pull': 'プルするリレイセットを選択', - 'No relay sets found': 'リレイセットが見つかりません', - 'Pull n relay sets': '{{n}} 個のリレイセットをプル', - Pull: 'プル', 'Select all': 'すべて選択', 'Relay Sets': 'リレイセット', 'Read & Write Relays': '読み&書きリレイ', @@ -211,6 +202,8 @@ export default { 'Seen on': '見た', 'Temporarily display this reply': 'この返信を一時的に表示', 'Not found the note': 'ノートが見つかりません', - 'no more replies': 'これ以上の返信はありません' + 'no more replies': 'これ以上の返信はありません', + 'Relay sets': 'リレイセット', + 'Favorite Relays': 'お気に入りのリレイ' } } diff --git a/src/i18n/locales/pl.ts b/src/i18n/locales/pl.ts index c607ac1c..e597e395 100644 --- a/src/i18n/locales/pl.ts +++ b/src/i18n/locales/pl.ts @@ -74,7 +74,6 @@ export default { Search: 'Wyszukiwarka', 'The relays you are connected to do not support search': 'Podłączone transmitery nie obsługują wyszukiwania', - 'supports search': 'Obsługa wyszukiwania', 'Show more...': 'Więcej...', 'All users': 'Wszyscy użytkownicy', 'Display replies': 'Wyświetl komentarze', @@ -116,14 +115,6 @@ export default { 'R & W': 'O & Z', Read: 'Odczyt', Write: 'Zapis', - 'Push to relays': 'Wyślij do transmiterów', - 'Push Successful': 'Wysłano Pomyślnie', - 'Successfully pushed relay sets to relays': 'Pomyślnie wysłano zestaw do transmiterów', - 'Pull from relays': 'Pobierz z transmiterów', - 'Select the relay sets you want to pull': 'Wybierz zestaw transmiterów do pobrania', - 'No relay sets found': 'Nie znaleziono zestawu transmiterów', - 'Pull n relay sets': 'Pobierz {{n}} zestawów transmiterów', - Pull: 'Pobierz', 'Select all': 'Wszystkie', 'Relay Sets': 'Grupy transmiterów', 'Read & Write Relays': 'Transmitery zapisu i odczytu', @@ -212,6 +203,8 @@ export default { 'Seen on': 'Widziany na', 'Temporarily display this reply': 'Tymczasowo wyświetl tę odpowiedź', 'Not found the note': 'Nie znaleziono wpisu', - 'no more replies': 'brak kolejnych odpowiedzi' + 'no more replies': 'brak kolejnych odpowiedzi', + 'Relay sets': 'Zestawy transmiterów', + 'Favorite Relays': 'Ulubione transmitery' } } diff --git a/src/i18n/locales/pt-BR.ts b/src/i18n/locales/pt-BR.ts index 2eea3003..21d5acca 100644 --- a/src/i18n/locales/pt-BR.ts +++ b/src/i18n/locales/pt-BR.ts @@ -74,7 +74,6 @@ export default { Search: 'Pesquisar', 'The relays you are connected to do not support search': 'Os relés aos quais você está conectado não suportam pesquisa', - 'supports search': 'suporta pesquisa', 'Show more...': 'Mostrar mais...', 'All users': 'Todos os usuários', 'Display replies': 'Exibir respostas', @@ -116,14 +115,6 @@ export default { 'R & W': 'Leitura & Escrita', Read: 'Ler', Write: 'Escrever', - 'Push to relays': 'Enviar para relés', - 'Push Successful': 'Envio bem-sucedido', - 'Successfully pushed relay sets to relays': 'Conjuntos de relé enviados com sucesso', - 'Pull from relays': 'Receber de relés', - 'Select the relay sets you want to pull': 'Selecione os conjuntos de relé que deseja receber', - 'No relay sets found': 'Nenhum conjunto de relé encontrado', - 'Pull n relay sets': 'Receber {{n}} conjuntos de relé', - Pull: 'Receber', 'Select all': 'Selecionar todos', 'Relay Sets': 'Conjuntos de relé', 'Read & Write Relays': 'Relés de Leitura & Escrita', @@ -212,6 +203,8 @@ export default { 'Seen on': 'Visto em', 'Temporarily display this reply': 'Exibir temporariamente esta resposta', 'Not found the note': 'Nota não encontrada', - 'no more replies': 'não há mais respostas' + 'no more replies': 'não há mais respostas', + 'Relay sets': 'Conjuntos de relé', + 'Favorite Relays': 'Relés favoritos' } } diff --git a/src/i18n/locales/pt-PT.ts b/src/i18n/locales/pt-PT.ts index 7323b866..2b70ff21 100644 --- a/src/i18n/locales/pt-PT.ts +++ b/src/i18n/locales/pt-PT.ts @@ -75,7 +75,6 @@ export default { Search: 'Pesquisar', 'The relays you are connected to do not support search': 'Os relés aos quais você está conectado não suportam pesquisa', - 'supports search': 'suporta pesquisa', 'Show more...': 'Mostrar mais...', 'All users': 'Todos os usuários', 'Display replies': 'Exibir respostas', @@ -117,14 +116,6 @@ export default { 'R & W': 'Leitura & Escrita', Read: 'Ler', Write: 'Escrever', - 'Push to relays': 'Enviar para relés', - 'Push Successful': 'Envio Bem-sucedido', - 'Successfully pushed relay sets to relays': 'Conjuntos de relé enviados com sucesso', - 'Pull from relays': 'Receber de relés', - 'Select the relay sets you want to pull': 'Selecione os conjuntos de relé que deseja receber', - 'No relay sets found': 'Nenhum conjunto de relé encontrado', - 'Pull n relay sets': 'Receber {{n}} conjuntos de relé', - Pull: 'Receber', 'Select all': 'Selecionar todos', 'Relay Sets': 'Conjuntos de relé', 'Read & Write Relays': 'Relés de Leitura & Escrita', @@ -213,6 +204,8 @@ export default { 'Seen on': 'Visto em', 'Temporarily display this reply': 'Exibir temporariamente esta resposta', 'Not found the note': 'Nota não encontrada', - 'no more replies': 'não há mais respostas' + 'no more replies': 'não há mais respostas', + 'Relay sets': 'Conjuntos de Relé', + 'Favorite Relays': 'Relés Favoritos' } } diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index 9e47f5d0..be42dff8 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -75,7 +75,6 @@ export default { Search: 'Поиск', 'The relays you are connected to do not support search': 'Подключённые ретрансляторы не поддерживают поиск', - 'supports search': 'поддерживает поиск', 'Show more...': 'Показать больше...', 'All users': 'Все пользователи', 'Display replies': 'Показать ответы', @@ -118,14 +117,6 @@ export default { 'R & W': 'Чтение & Запись', Read: 'Читать', Write: 'Писать', - 'Push to relays': 'Отправить на ретрансляторы', - 'Push Successful': 'Отправка успешна', - 'Successfully pushed relay sets to relays': 'Наборы ретрансляторов успешно отправлены', - 'Pull from relays': 'Получить с ретрансляторов', - 'Select the relay sets you want to pull': 'Выберите наборы ретрансляторов для получения', - 'No relay sets found': 'Наборы ретрансляторов не найдены', - 'Pull n relay sets': 'Получить {{n}} наборов ретрансляторов', - Pull: 'Получить', 'Select all': 'Выбрать все', 'Relay Sets': 'Наборы ретрансляторов', 'Read & Write Relays': 'Ретрансляторы для чтения и записи', @@ -214,6 +205,8 @@ export default { 'Seen on': 'Просмотрено на', 'Temporarily display this reply': 'Временно отобразить этот ответ', 'Not found the note': 'Заметка не найдена', - 'no more replies': 'больше нет ответов' + 'no more replies': 'больше нет ответов', + 'Relay sets': 'Наборы ретрансляторов', + 'Favorite Relays': 'Избранные ретрансляторы' } } diff --git a/src/i18n/locales/zh.ts b/src/i18n/locales/zh.ts index 128511d2..4a03450e 100644 --- a/src/i18n/locales/zh.ts +++ b/src/i18n/locales/zh.ts @@ -73,7 +73,6 @@ export default { 'Reply to': '回复', Search: '搜索', 'The relays you are connected to do not support search': '您连接的服务器不支持搜索', - 'supports search': '支持搜索', 'Show more...': '查看更多...', 'All users': '所有用户', 'Display replies': '显示回复', @@ -116,14 +115,6 @@ export default { 'R & W': '读写', Read: '只读', Write: '只写', - 'Push to relays': '保存到服务器', - 'Push Successful': '保存成功', - 'Successfully pushed relay sets to relays': '成功保存到服务器', - 'Pull from relays': '从服务器拉取', - 'Select the relay sets you want to pull': '选择要拉取的服务器组', - 'No relay sets found': '未找到服务器组', - 'Pull n relay sets': '拉取 {{n}} 个服务器组', - Pull: '拉取', 'Select all': '全选', 'Relay Sets': '服务器组', Mailbox: '邮箱', @@ -211,6 +202,8 @@ export default { 'Seen on': '来自', 'Temporarily display this reply': '临时显示此回复', 'Not found the note': '未找到该笔记', - 'no more replies': '没有更多回复了' + 'no more replies': '没有更多回复了', + 'Relay sets': '服务器组', + 'Favorite Relays': '收藏的服务器' } } diff --git a/src/lib/draft-event.ts b/src/lib/draft-event.ts index 52647bfe..2f070431 100644 --- a/src/lib/draft-event.ts +++ b/src/lib/draft-event.ts @@ -1,4 +1,4 @@ -import { COMMENT_EVENT_KIND, PICTURE_EVENT_KIND } from '@/constants' +import { ExtendedKind } from '@/constants' import client from '@/services/client.service' import { TDraftEvent, TMailboxRelay, TRelaySet } from '@/types' import dayjs from 'dayjs' @@ -150,7 +150,7 @@ export async function createPictureNoteDraftEvent( } return { - kind: PICTURE_EVENT_KIND, + kind: ExtendedKind.PICTURE, content, tags, created_at: dayjs().unix() @@ -210,7 +210,7 @@ export async function createCommentDraftEvent( } return { - kind: COMMENT_EVENT_KIND, + kind: ExtendedKind.COMMENT, content, tags, created_at: dayjs().unix() @@ -255,6 +255,25 @@ export function createProfileDraftEvent(content: string, tags: string[][] = []): } } +export function createFavoriteRelaysDraftEvent( + favoriteRelays: string[], + relaySetEvents: Event[] +): TDraftEvent { + const tags: string[][] = [] + favoriteRelays.forEach((url) => { + tags.push(['relay', url]) + }) + relaySetEvents.forEach((event) => { + tags.push(['a', getEventCoordinate(event)]) + }) + return { + kind: ExtendedKind.FAVORITE_RELAYS, + content: '', + tags, + created_at: dayjs().unix() + } +} + function generateImetaTags(imageUrls: string[], pictureInfos: { url: string; tags: string[][] }[]) { return imageUrls.map((imageUrl) => { const pictureInfo = pictureInfos.find((info) => info.url === imageUrl) diff --git a/src/lib/event.ts b/src/lib/event.ts index 72431623..562159d9 100644 --- a/src/lib/event.ts +++ b/src/lib/event.ts @@ -1,6 +1,6 @@ -import { BIG_RELAY_URLS, COMMENT_EVENT_KIND, PICTURE_EVENT_KIND } from '@/constants' +import { BIG_RELAY_URLS, ExtendedKind } from '@/constants' import client from '@/services/client.service' -import { TImageInfo, TRelayList } from '@/types' +import { TImageInfo, TRelayList, TRelaySet } from '@/types' import { LRUCache } from 'lru-cache' import { Event, kinds, nip19 } from 'nostr-tools' import { getAmountFromInvoice, getLightningAddressFromProfile } from './lightning' @@ -47,11 +47,11 @@ export function isReplyNoteEvent(event: Event) { } export function isCommentEvent(event: Event) { - return event.kind === COMMENT_EVENT_KIND + return event.kind === ExtendedKind.COMMENT } export function isPictureEvent(event: Event) { - return event.kind === PICTURE_EVENT_KIND + return event.kind === ExtendedKind.PICTURE } export function isProtectedEvent(event: Event) { @@ -59,7 +59,7 @@ export function isProtectedEvent(event: Event) { } export function isSupportedKind(kind: number) { - return [kinds.ShortTextNote, PICTURE_EVENT_KIND].includes(kind) + return [kinds.ShortTextNote, ExtendedKind.PICTURE].includes(kind) } export function getParentEventTag(event?: Event) { @@ -195,6 +195,22 @@ export function getProfileFromProfileEvent(event: Event) { } } +export function getRelaySetFromRelaySetEvent(event: Event): TRelaySet { + const id = getReplaceableEventIdentifier(event) + const relayUrls = event.tags + .filter(tagNameEquals('relay')) + .map((tag) => tag[1]) + .filter((url) => url && isWebsocketUrl(url)) + .map((url) => normalizeUrl(url)) + + let name = event.tags.find(tagNameEquals('title'))?.[1] + if (!name) { + name = id + } + + return { id, name, relayUrls } +} + export async function extractMentions(content: string, parentEvent?: Event) { const parentEventPubkey = parentEvent ? parentEvent.pubkey : undefined const pubkeys: string[] = [] @@ -485,3 +501,7 @@ export function extractEmbeddedEventIds(event: Event) { export function getLatestEvent(events: Event[]) { return events.sort((a, b) => b.created_at - a.created_at)[0] } + +export function getReplaceableEventIdentifier(event: Event) { + return event.tags.find(tagNameEquals('d'))?.[1] ?? '' +} diff --git a/src/lib/link.ts b/src/lib/link.ts index 95b55c8d..76be2d38 100644 --- a/src/lib/link.ts +++ b/src/lib/link.ts @@ -35,7 +35,7 @@ export const toOthersRelaySettings = (pubkey: string) => { const npub = nip19.npubEncode(pubkey) return `/users/${npub}/relays` } -export const toRelaySettings = (tag?: 'mailbox' | 'relay-sets') => { +export const toRelaySettings = (tag?: 'mailbox' | 'favorite-relays') => { return '/relay-settings' + (tag ? '#' + tag : '') } export const toSettings = () => '/settings' diff --git a/src/pages/primary/MePage/index.tsx b/src/pages/primary/MePage/index.tsx index 8c7e65c7..9006dcb9 100644 --- a/src/pages/primary/MePage/index.tsx +++ b/src/pages/primary/MePage/index.tsx @@ -8,11 +8,19 @@ import { Separator } from '@/components/ui/separator' import { SimpleUserAvatar } from '@/components/UserAvatar' import { SimpleUsername } from '@/components/Username' import PrimaryPageLayout from '@/layouts/PrimaryPageLayout' -import { toProfile, toSettings, toWallet } from '@/lib/link' +import { toProfile, toRelaySettings, toSettings, toWallet } from '@/lib/link' import { cn } from '@/lib/utils' import { useSecondaryPage } from '@/PageManager' import { useNostr } from '@/providers/NostrProvider' -import { ArrowDownUp, ChevronRight, LogOut, Settings, UserRound, Wallet } from 'lucide-react' +import { + ArrowDownUp, + ChevronRight, + LogOut, + Server, + Settings, + UserRound, + Wallet +} from 'lucide-react' import { forwardRef, HTMLProps, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -54,6 +62,9 @@ const MePage = forwardRef((_, ref) => { {t('Profile')} + push(toRelaySettings())}> + {t('Relays')} + push(toWallet())}> {t('Wallet')} diff --git a/src/pages/primary/NoteListPage/FeedButton.tsx b/src/pages/primary/NoteListPage/FeedButton.tsx index 962f9e95..d24a9202 100644 --- a/src/pages/primary/NoteListPage/FeedButton.tsx +++ b/src/pages/primary/NoteListPage/FeedButton.tsx @@ -3,11 +3,11 @@ import { Drawer, DrawerContent } from '@/components/ui/drawer' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { simplifyUrl } from '@/lib/url' import { cn } from '@/lib/utils' +import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useFeed } from '@/providers/FeedProvider' -import { useRelaySets } from '@/providers/RelaySetsProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider' import { ChevronDown, Server, UsersRound } from 'lucide-react' -import { forwardRef, HTMLAttributes, useState } from 'react' +import { forwardRef, HTMLAttributes, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' export default function FeedButton({ className }: { className?: string }) { @@ -20,7 +20,7 @@ export default function FeedButton({ className }: { className?: string }) { setOpen(true)} /> -
+
setOpen(false)} />
@@ -44,21 +44,32 @@ export default function FeedButton({ className }: { className?: string }) { const FeedSwitcherTrigger = forwardRef>( ({ className, ...props }, ref) => { const { t } = useTranslation() - const { feedType, relayUrls, activeRelaySetId } = useFeed() - const { relaySets } = useRelaySets() - const activeRelaySet = activeRelaySetId - ? relaySets.find((set) => set.id === activeRelaySetId) - : undefined - const title = - feedType === 'following' - ? t('Following') - : relayUrls.length > 0 - ? relayUrls.length === 1 - ? simplifyUrl(relayUrls[0]) - : activeRelaySet - ? activeRelaySet.name - : t('Temporary') - : t('Choose a relay set') + const { feedInfo, relayUrls } = useFeed() + const { relaySets } = useFavoriteRelays() + const activeRelaySet = useMemo(() => { + return feedInfo.feedType === 'relays' && feedInfo.id + ? relaySets.find((set) => set.id === feedInfo.id) + : undefined + }, [feedInfo, relaySets]) + const title = useMemo(() => { + if (feedInfo.feedType === 'following') { + return t('Following') + } + if (relayUrls.length === 0) { + return t('Choose a relay') + } + if (feedInfo.feedType === 'relay') { + return simplifyUrl(feedInfo.id ?? '') + } + if (feedInfo.feedType === 'relays') { + return activeRelaySet?.name ?? activeRelaySet?.id + } + if (feedInfo.feedType === 'temporary') { + return relayUrls.length === 1 + ? simplifyUrl(relayUrls[0]) + : (activeRelaySet?.name ?? t('Temporary')) + } + }, [feedInfo, activeRelaySet]) return (
- {feedType === 'following' ? : } + {feedInfo.feedType === 'following' ? : }
{title}
diff --git a/src/pages/primary/NoteListPage/index.tsx b/src/pages/primary/NoteListPage/index.tsx index 9b098809..7a8b482d 100644 --- a/src/pages/primary/NoteListPage/index.tsx +++ b/src/pages/primary/NoteListPage/index.tsx @@ -17,17 +17,17 @@ const NoteListPage = forwardRef((_, ref) => { const { t } = useTranslation() const layoutRef = useRef(null) const { pubkey, checkLogin } = useNostr() - const { feedType, relayUrls, isReady, filter } = useFeed() + const { feedInfo, relayUrls, isReady, filter } = useFeed() useImperativeHandle(ref, () => layoutRef.current) useEffect(() => { if (layoutRef.current) { layoutRef.current.scrollToTop() } - }, [JSON.stringify(relayUrls), feedType]) + }, [JSON.stringify(relayUrls), feedInfo]) let content =
{t('loading...')}
- if (feedType === 'following' && !pubkey) { + if (feedInfo.feedType === 'following' && !pubkey) { content = (