refactor: update branding assets and convert settings to accordion UI
- Replace SVG favicons with PNG icons from new smeshicon assets - Add theme-aware Icon component using smeshiconlight/dark PNGs - Refactor Settings page to use collapsible accordion sections - Add Radix UI accordion component with animations - Update QrCode component to use new PNG icon - Remove old favicon.svg and nostr.json files - Add new logo assets in resources/ and src/assets/ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,91 +1,586 @@
|
||||
import AboutInfoDialog from '@/components/AboutInfoDialog'
|
||||
import Donation from '@/components/Donation'
|
||||
import Emoji from '@/components/Emoji'
|
||||
import EmojiPackList from '@/components/EmojiPackList'
|
||||
import EmojiPickerDialog from '@/components/EmojiPickerDialog'
|
||||
import FavoriteRelaysSetting from '@/components/FavoriteRelaysSetting'
|
||||
import MailboxSetting from '@/components/MailboxSetting'
|
||||
import NoteList from '@/components/NoteList'
|
||||
import Tabs from '@/components/Tabs'
|
||||
import {
|
||||
toAppearanceSettings,
|
||||
toEmojiPackSettings,
|
||||
toGeneralSettings,
|
||||
toPostSettings,
|
||||
toRelaySettings,
|
||||
toSystemSettings,
|
||||
toWallet
|
||||
} from '@/lib/link'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useSecondaryPage } from '@/PageManager'
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger
|
||||
} from '@/components/ui/accordion'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Tabs as RadixTabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import {
|
||||
BIG_RELAY_URLS,
|
||||
DEFAULT_FAVICON_URL_TEMPLATE,
|
||||
MEDIA_AUTO_LOAD_POLICY,
|
||||
NSFW_DISPLAY_POLICY,
|
||||
PRIMARY_COLORS,
|
||||
TPrimaryColor
|
||||
} from '@/constants'
|
||||
import { LocalizedLanguageNames, TLanguage } from '@/i18n'
|
||||
import { cn, isSupportCheckConnectionType } from '@/lib/utils'
|
||||
import MediaUploadServiceSetting from '@/pages/secondary/PostSettingsPage/MediaUploadServiceSetting'
|
||||
import DefaultZapAmountInput from '@/pages/secondary/WalletPage/DefaultZapAmountInput'
|
||||
import DefaultZapCommentInput from '@/pages/secondary/WalletPage/DefaultZapCommentInput'
|
||||
import LightningAddressInput from '@/pages/secondary/WalletPage/LightningAddressInput'
|
||||
import QuickZapSwitch from '@/pages/secondary/WalletPage/QuickZapSwitch'
|
||||
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||
import { useTheme } from '@/providers/ThemeProvider'
|
||||
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
|
||||
import { useUserTrust } from '@/providers/UserTrustProvider'
|
||||
import { useZap } from '@/providers/ZapProvider'
|
||||
import storage from '@/services/local-storage.service'
|
||||
import { TMediaAutoLoadPolicy, TNsfwDisplayPolicy } from '@/types'
|
||||
import { disconnect, launchModal } from '@getalby/bitcoin-connect-react'
|
||||
import {
|
||||
Check,
|
||||
ChevronRight,
|
||||
Cog,
|
||||
Columns2,
|
||||
Copy,
|
||||
Info,
|
||||
KeyRound,
|
||||
LayoutList,
|
||||
List,
|
||||
Monitor,
|
||||
Moon,
|
||||
Palette,
|
||||
PanelLeft,
|
||||
PencilLine,
|
||||
RotateCcw,
|
||||
Server,
|
||||
Settings2,
|
||||
Smile,
|
||||
Sun,
|
||||
Wallet
|
||||
} from 'lucide-react'
|
||||
import { forwardRef, HTMLProps, useState } from 'react'
|
||||
import { kinds } from 'nostr-tools'
|
||||
import { forwardRef, HTMLProps, useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type TEmojiTab = 'my-packs' | 'explore'
|
||||
|
||||
const THEMES = [
|
||||
{ key: 'system', label: 'System', icon: <Monitor className="size-5" /> },
|
||||
{ key: 'light', label: 'Light', icon: <Sun className="size-5" /> },
|
||||
{ key: 'dark', label: 'Dark', icon: <Moon className="size-5" /> },
|
||||
{ key: 'pure-black', label: 'Pure Black', icon: <Moon className="size-5 fill-current" /> }
|
||||
] as const
|
||||
|
||||
const LAYOUTS = [
|
||||
{ key: false, label: 'Two-column', icon: <Columns2 className="size-5" /> },
|
||||
{ key: true, label: 'Single-column', icon: <PanelLeft className="size-5" /> }
|
||||
] as const
|
||||
|
||||
const NOTIFICATION_STYLES = [
|
||||
{ key: 'detailed', label: 'Detailed', icon: <LayoutList className="size-5" /> },
|
||||
{ key: 'compact', label: 'Compact', icon: <List className="size-5" /> }
|
||||
] as const
|
||||
|
||||
export default function Settings() {
|
||||
const { t } = useTranslation()
|
||||
const { t, i18n } = useTranslation()
|
||||
const { pubkey, nsec, ncryptsec } = useNostr()
|
||||
const { push } = useSecondaryPage()
|
||||
const { isSmallScreen } = useScreenSize()
|
||||
const [copiedNsec, setCopiedNsec] = useState(false)
|
||||
const [copiedNcryptsec, setCopiedNcryptsec] = useState(false)
|
||||
const [openSection, setOpenSection] = useState<string>('')
|
||||
const accordionRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// General settings
|
||||
const [language, setLanguage] = useState<TLanguage>(i18n.language as TLanguage)
|
||||
const {
|
||||
autoplay,
|
||||
setAutoplay,
|
||||
nsfwDisplayPolicy,
|
||||
setNsfwDisplayPolicy,
|
||||
hideContentMentioningMutedUsers,
|
||||
setHideContentMentioningMutedUsers,
|
||||
mediaAutoLoadPolicy,
|
||||
setMediaAutoLoadPolicy,
|
||||
faviconUrlTemplate,
|
||||
setFaviconUrlTemplate
|
||||
} = useContentPolicy()
|
||||
const {
|
||||
hideUntrustedNotes,
|
||||
updateHideUntrustedNotes,
|
||||
hideUntrustedInteractions,
|
||||
updateHideUntrustedInteractions,
|
||||
hideUntrustedNotifications,
|
||||
updateHideUntrustedNotifications
|
||||
} = useUserTrust()
|
||||
const {
|
||||
quickReaction,
|
||||
updateQuickReaction,
|
||||
quickReactionEmoji,
|
||||
updateQuickReactionEmoji,
|
||||
enableSingleColumnLayout,
|
||||
updateEnableSingleColumnLayout,
|
||||
notificationListStyle,
|
||||
updateNotificationListStyle
|
||||
} = useUserPreferences()
|
||||
|
||||
// Appearance settings
|
||||
const { themeSetting, setThemeSetting, primaryColor, setPrimaryColor } = useTheme()
|
||||
|
||||
// Wallet settings
|
||||
const { isWalletConnected, walletInfo } = useZap()
|
||||
|
||||
// Relay settings
|
||||
const [relayTabValue, setRelayTabValue] = useState('favorite-relays')
|
||||
|
||||
// Emoji settings
|
||||
const [emojiTab, setEmojiTab] = useState<TEmojiTab>('my-packs')
|
||||
|
||||
// System settings
|
||||
const [filterOutOnionRelays, setFilterOutOnionRelays] = useState(storage.getFilterOutOnionRelays())
|
||||
|
||||
const handleLanguageChange = (value: TLanguage) => {
|
||||
i18n.changeLanguage(value)
|
||||
setLanguage(value)
|
||||
}
|
||||
|
||||
const handleAccordionChange = useCallback((value: string) => {
|
||||
setOpenSection(value)
|
||||
if (value) {
|
||||
// Scroll the opened section into view
|
||||
setTimeout(() => {
|
||||
const item = accordionRef.current?.querySelector(`[data-state="open"]`)
|
||||
if (item) {
|
||||
const rect = item.getBoundingClientRect()
|
||||
const scrollContainer = accordionRef.current?.closest('[data-radix-scroll-area-viewport]') || window
|
||||
if (scrollContainer === window) {
|
||||
const scrollTop = window.scrollY + rect.top - 16
|
||||
window.scrollTo({ top: scrollTop, behavior: 'smooth' })
|
||||
} else {
|
||||
const containerRect = (scrollContainer as HTMLElement).getBoundingClientRect()
|
||||
const scrollTop = (scrollContainer as HTMLElement).scrollTop + rect.top - containerRect.top - 16
|
||||
;(scrollContainer as HTMLElement).scrollTo({ top: scrollTop, behavior: 'smooth' })
|
||||
}
|
||||
}
|
||||
}, 50)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SettingItem className="clickable" onClick={() => push(toGeneralSettings())}>
|
||||
<div className="flex items-center gap-4">
|
||||
<Settings2 />
|
||||
<div>{t('General')}</div>
|
||||
</div>
|
||||
<ChevronRight />
|
||||
</SettingItem>
|
||||
<SettingItem className="clickable" onClick={() => push(toAppearanceSettings())}>
|
||||
<div className="flex items-center gap-4">
|
||||
<Palette />
|
||||
<div>{t('Appearance')}</div>
|
||||
</div>
|
||||
<ChevronRight />
|
||||
</SettingItem>
|
||||
<SettingItem className="clickable" onClick={() => push(toRelaySettings())}>
|
||||
<div className="flex items-center gap-4">
|
||||
<Server />
|
||||
<div>{t('Relays')}</div>
|
||||
</div>
|
||||
<ChevronRight />
|
||||
</SettingItem>
|
||||
{!!pubkey && (
|
||||
<SettingItem className="clickable" onClick={() => push(toWallet())}>
|
||||
<div className="flex items-center gap-4">
|
||||
<Wallet />
|
||||
<div>{t('Wallet')}</div>
|
||||
</div>
|
||||
<ChevronRight />
|
||||
</SettingItem>
|
||||
)}
|
||||
{!!pubkey && (
|
||||
<SettingItem className="clickable" onClick={() => push(toPostSettings())}>
|
||||
<div className="flex items-center gap-4">
|
||||
<PencilLine />
|
||||
<div>{t('Post settings')}</div>
|
||||
</div>
|
||||
<ChevronRight />
|
||||
</SettingItem>
|
||||
)}
|
||||
{!!pubkey && (
|
||||
<SettingItem className="clickable" onClick={() => push(toEmojiPackSettings())}>
|
||||
<div className="flex items-center gap-4">
|
||||
<Smile />
|
||||
<div>{t('Emoji Packs')}</div>
|
||||
</div>
|
||||
<ChevronRight />
|
||||
</SettingItem>
|
||||
)}
|
||||
<div ref={accordionRef}>
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
value={openSection}
|
||||
onValueChange={handleAccordionChange}
|
||||
className="w-full"
|
||||
>
|
||||
{/* General */}
|
||||
<AccordionItem value="general">
|
||||
<AccordionTrigger className="px-4 hover:no-underline">
|
||||
<div className="flex items-center gap-4">
|
||||
<Settings2 className="size-4" />
|
||||
<span>{t('General')}</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 space-y-4">
|
||||
<SettingItem>
|
||||
<Label htmlFor="languages" className="text-base font-normal">
|
||||
{t('Languages')}
|
||||
</Label>
|
||||
<Select defaultValue="en" value={language} onValueChange={handleLanguageChange}>
|
||||
<SelectTrigger id="languages" className="w-48">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(LocalizedLanguageNames).map(([key, value]) => (
|
||||
<SelectItem key={key} value={key}>
|
||||
{value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SettingItem>
|
||||
<SettingItem>
|
||||
<Label htmlFor="media-auto-load-policy" className="text-base font-normal">
|
||||
{t('Auto-load media')}
|
||||
</Label>
|
||||
<Select
|
||||
defaultValue="wifi-only"
|
||||
value={mediaAutoLoadPolicy}
|
||||
onValueChange={(value: TMediaAutoLoadPolicy) => setMediaAutoLoadPolicy(value)}
|
||||
>
|
||||
<SelectTrigger id="media-auto-load-policy" className="w-48">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={MEDIA_AUTO_LOAD_POLICY.ALWAYS}>{t('Always')}</SelectItem>
|
||||
{isSupportCheckConnectionType() && (
|
||||
<SelectItem value={MEDIA_AUTO_LOAD_POLICY.WIFI_ONLY}>{t('Wi-Fi only')}</SelectItem>
|
||||
)}
|
||||
<SelectItem value={MEDIA_AUTO_LOAD_POLICY.NEVER}>{t('Never')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SettingItem>
|
||||
<SettingItem>
|
||||
<Label htmlFor="autoplay" className="text-base font-normal">
|
||||
<div>{t('Autoplay')}</div>
|
||||
<div className="text-muted-foreground">{t('Enable video autoplay on this device')}</div>
|
||||
</Label>
|
||||
<Switch id="autoplay" checked={autoplay} onCheckedChange={setAutoplay} />
|
||||
</SettingItem>
|
||||
<SettingItem>
|
||||
<Label htmlFor="hide-untrusted-notes" className="text-base font-normal">
|
||||
{t('Hide untrusted notes')}
|
||||
</Label>
|
||||
<Switch
|
||||
id="hide-untrusted-notes"
|
||||
checked={hideUntrustedNotes}
|
||||
onCheckedChange={updateHideUntrustedNotes}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem>
|
||||
<Label htmlFor="hide-untrusted-interactions" className="text-base font-normal">
|
||||
{t('Hide untrusted interactions')}
|
||||
</Label>
|
||||
<Switch
|
||||
id="hide-untrusted-interactions"
|
||||
checked={hideUntrustedInteractions}
|
||||
onCheckedChange={updateHideUntrustedInteractions}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem>
|
||||
<Label htmlFor="hide-untrusted-notifications" className="text-base font-normal">
|
||||
{t('Hide untrusted notifications')}
|
||||
</Label>
|
||||
<Switch
|
||||
id="hide-untrusted-notifications"
|
||||
checked={hideUntrustedNotifications}
|
||||
onCheckedChange={updateHideUntrustedNotifications}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem>
|
||||
<Label htmlFor="hide-content-mentioning-muted-users" className="text-base font-normal">
|
||||
{t('Hide content mentioning muted users')}
|
||||
</Label>
|
||||
<Switch
|
||||
id="hide-content-mentioning-muted-users"
|
||||
checked={hideContentMentioningMutedUsers}
|
||||
onCheckedChange={setHideContentMentioningMutedUsers}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem>
|
||||
<Label htmlFor="nsfw-display-policy" className="text-base font-normal">
|
||||
{t('NSFW content display')}
|
||||
</Label>
|
||||
<Select
|
||||
value={nsfwDisplayPolicy}
|
||||
onValueChange={(value: TNsfwDisplayPolicy) => setNsfwDisplayPolicy(value)}
|
||||
>
|
||||
<SelectTrigger id="nsfw-display-policy" className="w-48">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={NSFW_DISPLAY_POLICY.HIDE}>{t('Hide completely')}</SelectItem>
|
||||
<SelectItem value={NSFW_DISPLAY_POLICY.HIDE_CONTENT}>{t('Show but hide content')}</SelectItem>
|
||||
<SelectItem value={NSFW_DISPLAY_POLICY.SHOW}>{t('Show directly')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SettingItem>
|
||||
<SettingItem>
|
||||
<Label htmlFor="quick-reaction" className="text-base font-normal">
|
||||
<div>{t('Quick reaction')}</div>
|
||||
<div className="text-muted-foreground">
|
||||
{t('If enabled, you can react with a single click. Click and hold for more options')}
|
||||
</div>
|
||||
</Label>
|
||||
<Switch id="quick-reaction" checked={quickReaction} onCheckedChange={updateQuickReaction} />
|
||||
</SettingItem>
|
||||
{quickReaction && (
|
||||
<SettingItem>
|
||||
<Label htmlFor="quick-reaction-emoji" className="text-base font-normal">
|
||||
{t('Quick reaction emoji')}
|
||||
</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => updateQuickReactionEmoji('+')}
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<RotateCcw />
|
||||
</Button>
|
||||
<EmojiPickerDialog
|
||||
onEmojiClick={(emoji) => {
|
||||
if (!emoji) return
|
||||
updateQuickReactionEmoji(emoji)
|
||||
}}
|
||||
>
|
||||
<Button variant="ghost" size="icon" className="border">
|
||||
<Emoji emoji={quickReactionEmoji} />
|
||||
</Button>
|
||||
</EmojiPickerDialog>
|
||||
</div>
|
||||
</SettingItem>
|
||||
)}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
{/* Appearance */}
|
||||
<AccordionItem value="appearance">
|
||||
<AccordionTrigger className="px-4 hover:no-underline">
|
||||
<div className="flex items-center gap-4">
|
||||
<Palette className="size-4" />
|
||||
<span>{t('Appearance')}</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 space-y-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-base">{t('Theme')}</Label>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 w-full">
|
||||
{THEMES.map(({ key, label, icon }) => (
|
||||
<OptionButton
|
||||
key={key}
|
||||
isSelected={themeSetting === key}
|
||||
icon={icon}
|
||||
label={t(label)}
|
||||
onClick={() => setThemeSetting(key)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{!isSmallScreen && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-base">{t('Layout')}</Label>
|
||||
<div className="grid grid-cols-2 gap-4 w-full">
|
||||
{LAYOUTS.map(({ key, label, icon }) => (
|
||||
<OptionButton
|
||||
key={key.toString()}
|
||||
isSelected={enableSingleColumnLayout === key}
|
||||
icon={icon}
|
||||
label={t(label)}
|
||||
onClick={() => updateEnableSingleColumnLayout(key)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-base">{t('Notification list style')}</Label>
|
||||
<div className="grid grid-cols-2 gap-4 w-full">
|
||||
{NOTIFICATION_STYLES.map(({ key, label, icon }) => (
|
||||
<OptionButton
|
||||
key={key}
|
||||
isSelected={notificationListStyle === key}
|
||||
icon={icon}
|
||||
label={t(label)}
|
||||
onClick={() => updateNotificationListStyle(key)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-base">{t('Primary color')}</Label>
|
||||
<div className="grid grid-cols-4 gap-4 w-full">
|
||||
{Object.entries(PRIMARY_COLORS).map(([key, config]) => (
|
||||
<OptionButton
|
||||
key={key}
|
||||
isSelected={primaryColor === key}
|
||||
icon={
|
||||
<div
|
||||
className="size-8 rounded-full shadow-md"
|
||||
style={{ backgroundColor: `hsl(${config.light.primary})` }}
|
||||
/>
|
||||
}
|
||||
label={t(config.name)}
|
||||
onClick={() => setPrimaryColor(key as TPrimaryColor)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
{/* Relays */}
|
||||
<AccordionItem value="relays">
|
||||
<AccordionTrigger className="px-4 hover:no-underline">
|
||||
<div className="flex items-center gap-4">
|
||||
<Server className="size-4" />
|
||||
<span>{t('Relays')}</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4">
|
||||
<RadixTabs value={relayTabValue} onValueChange={setRelayTabValue} className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="favorite-relays">{t('Favorite Relays')}</TabsTrigger>
|
||||
<TabsTrigger value="mailbox">{t('Read & Write Relays')}</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="favorite-relays">
|
||||
<FavoriteRelaysSetting />
|
||||
</TabsContent>
|
||||
<TabsContent value="mailbox">
|
||||
<MailboxSetting />
|
||||
</TabsContent>
|
||||
</RadixTabs>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
{/* Wallet */}
|
||||
{!!pubkey && (
|
||||
<AccordionItem value="wallet">
|
||||
<AccordionTrigger className="px-4 hover:no-underline">
|
||||
<div className="flex items-center gap-4">
|
||||
<Wallet className="size-4" />
|
||||
<span>{t('Wallet')}</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 space-y-4">
|
||||
{isWalletConnected ? (
|
||||
<>
|
||||
<div>
|
||||
{walletInfo?.node.alias && (
|
||||
<div className="mb-2">
|
||||
{t('Connected to')} <strong>{walletInfo.node.alias}</strong>
|
||||
</div>
|
||||
)}
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive">{t('Disconnect Wallet')}</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t('Are you absolutely sure?')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t('You will not be able to send zaps to others.')}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t('Cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction variant="destructive" onClick={() => disconnect()}>
|
||||
{t('Disconnect')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
<DefaultZapAmountInput />
|
||||
<DefaultZapCommentInput />
|
||||
<QuickZapSwitch />
|
||||
<LightningAddressInput />
|
||||
</>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button className="bg-foreground hover:bg-foreground/90" onClick={() => launchModal()}>
|
||||
{t('Connect Wallet')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
)}
|
||||
|
||||
{/* Post Settings */}
|
||||
{!!pubkey && (
|
||||
<AccordionItem value="posts">
|
||||
<AccordionTrigger className="px-4 hover:no-underline">
|
||||
<div className="flex items-center gap-4">
|
||||
<PencilLine className="size-4" />
|
||||
<span>{t('Post settings')}</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4">
|
||||
<MediaUploadServiceSetting />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
)}
|
||||
|
||||
{/* Emoji Packs */}
|
||||
{!!pubkey && (
|
||||
<AccordionItem value="emoji-packs">
|
||||
<AccordionTrigger className="px-4 hover:no-underline">
|
||||
<div className="flex items-center gap-4">
|
||||
<Smile className="size-4" />
|
||||
<span>{t('Emoji Packs')}</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4">
|
||||
<Tabs
|
||||
value={emojiTab}
|
||||
tabs={[
|
||||
{ value: 'my-packs', label: 'My Packs' },
|
||||
{ value: 'explore', label: 'Explore' }
|
||||
]}
|
||||
onTabChange={(tab) => setEmojiTab(tab as TEmojiTab)}
|
||||
/>
|
||||
{emojiTab === 'my-packs' ? (
|
||||
<EmojiPackList />
|
||||
) : (
|
||||
<NoteList
|
||||
showKinds={[kinds.Emojisets]}
|
||||
subRequests={[{ urls: BIG_RELAY_URLS, filter: {} }]}
|
||||
hideUntrustedNotes={hideUntrustedNotes}
|
||||
/>
|
||||
)}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
)}
|
||||
|
||||
{/* System */}
|
||||
<AccordionItem value="system">
|
||||
<AccordionTrigger className="px-4 hover:no-underline">
|
||||
<div className="flex items-center gap-4">
|
||||
<Cog className="size-4" />
|
||||
<span>{t('System')}</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="favicon-url" className="text-base font-normal">
|
||||
{t('Favicon URL')}
|
||||
</Label>
|
||||
<Input
|
||||
id="favicon-url"
|
||||
type="text"
|
||||
value={faviconUrlTemplate}
|
||||
onChange={(e) => setFaviconUrlTemplate(e.target.value)}
|
||||
placeholder={DEFAULT_FAVICON_URL_TEMPLATE}
|
||||
/>
|
||||
</div>
|
||||
<SettingItem>
|
||||
<Label htmlFor="filter-out-onion-relays" className="text-base font-normal">
|
||||
{t('Filter out onion relays')}
|
||||
</Label>
|
||||
<Switch
|
||||
id="filter-out-onion-relays"
|
||||
checked={filterOutOnionRelays}
|
||||
onCheckedChange={(checked) => {
|
||||
storage.setFilterOutOnionRelays(checked)
|
||||
setFilterOutOnionRelays(checked)
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
{/* Non-accordion items */}
|
||||
{!!nsec && (
|
||||
<SettingItem
|
||||
className="clickable"
|
||||
@@ -118,13 +613,6 @@ export default function Settings() {
|
||||
{copiedNcryptsec ? <Check /> : <Copy />}
|
||||
</SettingItem>
|
||||
)}
|
||||
<SettingItem className="clickable" onClick={() => push(toSystemSettings())}>
|
||||
<div className="flex items-center gap-4">
|
||||
<Cog />
|
||||
<div>{t('System')}</div>
|
||||
</div>
|
||||
<ChevronRight />
|
||||
</SettingItem>
|
||||
<AboutInfoDialog>
|
||||
<SettingItem className="clickable">
|
||||
<div className="flex items-center gap-4">
|
||||
@@ -135,7 +623,6 @@ export default function Settings() {
|
||||
<div className="text-muted-foreground">
|
||||
v{import.meta.env.APP_VERSION} ({import.meta.env.GIT_COMMIT})
|
||||
</div>
|
||||
<ChevronRight />
|
||||
</div>
|
||||
</SettingItem>
|
||||
</AboutInfoDialog>
|
||||
@@ -151,7 +638,7 @@ const SettingItem = forwardRef<HTMLDivElement, HTMLProps<HTMLDivElement>>(
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex justify-between select-none items-center px-4 py-2 h-[52px] rounded-lg [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
'flex justify-between select-none items-center px-4 min-h-9 [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -163,3 +650,28 @@ const SettingItem = forwardRef<HTMLDivElement, HTMLProps<HTMLDivElement>>(
|
||||
}
|
||||
)
|
||||
SettingItem.displayName = 'SettingItem'
|
||||
|
||||
const OptionButton = ({
|
||||
isSelected,
|
||||
onClick,
|
||||
icon,
|
||||
label
|
||||
}: {
|
||||
isSelected: boolean
|
||||
onClick: () => void
|
||||
icon: React.ReactNode
|
||||
label: string
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
'flex flex-col items-center gap-2 py-4 rounded-lg border-2 transition-all',
|
||||
isSelected ? 'border-primary' : 'border-border hover:border-muted-foreground/40'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-center w-8 h-8">{icon}</div>
|
||||
<span className="text-xs font-medium">{label}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user