diff --git a/src/components/BookmarkList.tsx b/src/components/BookmarkList.tsx index d6f76d21..3e96f31b 100644 --- a/src/components/BookmarkList.tsx +++ b/src/components/BookmarkList.tsx @@ -16,6 +16,7 @@ interface BookmarkListProps { viewMode: ViewMode onViewModeChange: (mode: ViewMode) => void selectedUrl?: string + onOpenSettings: () => void } export const BookmarkList: React.FC = ({ @@ -26,7 +27,8 @@ export const BookmarkList: React.FC = ({ onLogout, viewMode, onViewModeChange, - selectedUrl + selectedUrl, + onOpenSettings }) => { if (isCollapsed) { // Check if the selected URL is in bookmarks @@ -57,6 +59,7 @@ export const BookmarkList: React.FC = ({ onLogout={onLogout} viewMode={viewMode} onViewModeChange={onViewModeChange} + onOpenSettings={onOpenSettings} /> {bookmarks.length === 0 ? ( diff --git a/src/components/Bookmarks.tsx b/src/components/Bookmarks.tsx index 061805d7..74b79164 100644 --- a/src/components/Bookmarks.tsx +++ b/src/components/Bookmarks.tsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from 'react' -import { Hooks } from 'applesauce-react' +import { Hooks, useEventStore } from 'applesauce-react' import { RelayPool } from 'applesauce-relay' +import { EventFactory } from 'applesauce-factory' import { Bookmark } from '../types/bookmarks' import { Highlight } from '../types/highlights' import { BookmarkList } from './BookmarkList' @@ -9,6 +10,8 @@ import { fetchHighlights } from '../services/highlightService' import ContentPanel from './ContentPanel' import { HighlightsPanel } from './HighlightsPanel' import { fetchReadableContent, ReadableContent } from '../services/readerService' +import Settings from './Settings' +import { UserSettings, loadSettings, saveSettings } from '../services/settingsService' export type ViewMode = 'compact' | 'cards' | 'large' @@ -17,6 +20,11 @@ interface BookmarksProps { onLogout: () => void } +const RELAY_URLS = [ + 'wss://relay.damus.io', 'wss://nos.lol', 'wss://relay.nostr.band', + 'wss://relay.dergigi.com', 'wss://wot.dergigi.com' +] + const Bookmarks: React.FC = ({ relayPool, onLogout }) => { const [bookmarks, setBookmarks] = useState([]) const [loading, setLoading] = useState(true) @@ -30,8 +38,12 @@ const Bookmarks: React.FC = ({ relayPool, onLogout }) => { const [viewMode, setViewMode] = useState('compact') const [showUnderlines, setShowUnderlines] = useState(true) const [selectedHighlightId, setSelectedHighlightId] = useState(undefined) + const [showSettings, setShowSettings] = useState(false) + const [settings, setSettings] = useState({}) + const [isSavingSettings, setIsSavingSettings] = useState(false) const activeAccount = Hooks.useActiveAccount() const accountManager = Hooks.useAccountManager() + const eventStore = useEventStore() useEffect(() => { console.log('Bookmarks useEffect triggered') @@ -41,10 +53,18 @@ const Bookmarks: React.FC = ({ relayPool, onLogout }) => { console.log('Starting to fetch bookmarks and highlights...') handleFetchBookmarks() handleFetchHighlights() + handleLoadSettings() } else { console.log('Not fetching bookmarks - missing dependencies') } - }, [relayPool, activeAccount?.pubkey]) // Only depend on pubkey, not the entire activeAccount object + }, [relayPool, activeAccount?.pubkey]) + + useEffect(() => { + if (settings.defaultViewMode) setViewMode(settings.defaultViewMode) + if (settings.showUnderlines !== undefined) setShowUnderlines(settings.showUnderlines) + if (settings.sidebarCollapsed !== undefined) setIsCollapsed(settings.sidebarCollapsed) + if (settings.highlightsCollapsed !== undefined) setIsHighlightsCollapsed(settings.highlightsCollapsed) + }, [settings]) const handleFetchBookmarks = async () => { console.log('🔍 fetchBookmarks called, loading:', loading) @@ -80,6 +100,35 @@ const Bookmarks: React.FC = ({ relayPool, onLogout }) => { } } + const handleLoadSettings = async () => { + if (!relayPool || !activeAccount) return + try { + const loadedSettings = await loadSettings(relayPool, eventStore, activeAccount.pubkey, RELAY_URLS) + if (loadedSettings) { + setSettings(loadedSettings) + } + } catch (err) { + console.error('Failed to load settings:', err) + } + } + + const handleSaveSettings = async (newSettings: UserSettings) => { + if (!relayPool || !activeAccount) return + setIsSavingSettings(true) + try { + const fullAccount = accountManager.getActive() + if (!fullAccount) throw new Error('No active account') + const factory = new EventFactory({ signer: fullAccount }) + await saveSettings(relayPool, eventStore, factory, newSettings, RELAY_URLS) + setSettings(newSettings) + setShowSettings(false) + } catch (err) { + console.error('Failed to save settings:', err) + } finally { + setIsSavingSettings(false) + } + } + const handleSelectUrl = async (url: string) => { setSelectedUrl(url) setReaderLoading(true) @@ -105,47 +154,58 @@ const Bookmarks: React.FC = ({ relayPool, onLogout }) => { } return ( -
-
- setIsCollapsed(!isCollapsed)} - onLogout={onLogout} - viewMode={viewMode} - onViewModeChange={setViewMode} - selectedUrl={selectedUrl} - /> + <> +
+
+ setIsCollapsed(!isCollapsed)} + onLogout={onLogout} + viewMode={viewMode} + onViewModeChange={setViewMode} + selectedUrl={selectedUrl} + onOpenSettings={() => setShowSettings(true)} + /> +
+
+ +
+
+ setIsHighlightsCollapsed(!isHighlightsCollapsed)} + onSelectUrl={handleSelectUrl} + selectedUrl={selectedUrl} + onToggleUnderlines={setShowUnderlines} + selectedHighlightId={selectedHighlightId} + onRefresh={handleFetchHighlights} + onHighlightClick={setSelectedHighlightId} + /> +
-
- setShowSettings(false)} + isSaving={isSavingSettings} /> -
-
- setIsHighlightsCollapsed(!isHighlightsCollapsed)} - onSelectUrl={handleSelectUrl} - selectedUrl={selectedUrl} - onToggleUnderlines={setShowUnderlines} - selectedHighlightId={selectedHighlightId} - onRefresh={handleFetchHighlights} - onHighlightClick={setSelectedHighlightId} - /> -
-
+ )} + ) } diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx new file mode 100644 index 00000000..84216406 --- /dev/null +++ b/src/components/Settings.tsx @@ -0,0 +1,109 @@ +import React, { useState, useEffect } from 'react' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faTimes, faSave } from '@fortawesome/free-solid-svg-icons' +import { UserSettings } from '../services/settingsService' +import IconButton from './IconButton' + +interface SettingsProps { + settings: UserSettings + onSave: (settings: UserSettings) => Promise + onClose: () => void + isSaving: boolean +} + +const Settings: React.FC = ({ settings, onSave, onClose, isSaving }) => { + const [localSettings, setLocalSettings] = useState(settings) + + useEffect(() => { + setLocalSettings(settings) + }, [settings]) + + const handleSave = async () => { + await onSave(localSettings) + } + + return ( +
+
+
+

Settings

+ +
+ +
+
+ + +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ +
+ +
+
+
+ ) +} + +export default Settings diff --git a/src/components/SidebarHeader.tsx b/src/components/SidebarHeader.tsx index ebfa894e..cbde6547 100644 --- a/src/components/SidebarHeader.tsx +++ b/src/components/SidebarHeader.tsx @@ -12,9 +12,10 @@ interface SidebarHeaderProps { onLogout: () => void viewMode: ViewMode onViewModeChange: (mode: ViewMode) => void + onOpenSettings: () => void } -const SidebarHeader: React.FC = ({ onToggleCollapse, onLogout, viewMode, onViewModeChange }) => { +const SidebarHeader: React.FC = ({ onToggleCollapse, onLogout, viewMode, onViewModeChange, onOpenSettings }) => { const activeAccount = Hooks.useActiveAccount() const profile = useEventModel(Models.ProfileModel, activeAccount ? [activeAccount.pubkey] : null) @@ -52,7 +53,7 @@ const SidebarHeader: React.FC = ({ onToggleCollapse, onLogou
console.log('Settings clicked')} + onClick={onOpenSettings} title="Settings" ariaLabel="Settings" variant="ghost" diff --git a/src/index.css b/src/index.css index 3d1bb1e7..3b8bed9d 100644 --- a/src/index.css +++ b/src/index.css @@ -1420,3 +1420,122 @@ body { border-color: rgba(100, 108, 255, 0.4); } } + +/* Settings Panel */ +.settings-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.settings-panel { + background: #1a1a1a; + border: 1px solid #333; + border-radius: 8px; + width: 90%; + max-width: 500px; + max-height: 80vh; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.settings-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.5rem; + border-bottom: 1px solid #333; +} + +.settings-header h2 { + margin: 0; + font-size: 1.5rem; +} + +.settings-content { + padding: 1.5rem; + overflow-y: auto; + flex: 1; +} + +.setting-group { + margin-bottom: 1.5rem; +} + +.setting-group label { + display: block; + margin-bottom: 0.5rem; + color: #ccc; + font-weight: 500; +} + +.setting-select { + width: 100%; + padding: 0.5rem; + background: #2a2a2a; + border: 1px solid #444; + border-radius: 4px; + color: #fff; + font-size: 1rem; +} + +.setting-select:focus { + outline: none; + border-color: #646cff; +} + +.checkbox-label { + display: flex; + align-items: center; + gap: 0.75rem; + cursor: pointer; + user-select: none; +} + +.setting-checkbox { + width: 20px; + height: 20px; + cursor: pointer; +} + +.checkbox-label span { + color: #ccc; +} + +.settings-footer { + padding: 1.5rem; + border-top: 1px solid #333; + display: flex; + justify-content: flex-end; +} + +.settings-footer .btn-primary { + background: #646cff; + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: 4px; + font-size: 1rem; + cursor: pointer; + transition: background-color 0.2s; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.settings-footer .btn-primary:hover:not(:disabled) { + background: #535bf2; +} + +.settings-footer .btn-primary:disabled { + opacity: 0.6; + cursor: not-allowed; +} diff --git a/src/services/settingsService.ts b/src/services/settingsService.ts new file mode 100644 index 00000000..7ef410b0 --- /dev/null +++ b/src/services/settingsService.ts @@ -0,0 +1,96 @@ +import { EventStore } from 'applesauce-core' +import { APP_DATA_KIND, getAppDataContent } from 'applesauce-core/helpers/app-data' +import { AppDataBlueprint } from 'applesauce-factory/blueprints' +import { EventFactory } from 'applesauce-factory' +import { RelayPool, onlyEvents, mapEventsToStore } from 'applesauce-relay' +import { NostrEvent } from 'nostr-tools' +import { Account } from 'applesauce-accounts' + +const SETTINGS_IDENTIFIER = 'com.dergigi.boris.user-settings' + +export interface UserSettings { + defaultViewMode?: 'compact' | 'cards' | 'large' + showUnderlines?: boolean + sidebarCollapsed?: boolean + highlightsCollapsed?: boolean +} + +export async function loadSettings( + relayPool: RelayPool, + eventStore: EventStore, + pubkey: string, + relays: string[] +): Promise { + return new Promise((resolve) => { + let hasResolved = false + const timeout = setTimeout(() => { + if (!hasResolved) { + hasResolved = true + resolve(null) + } + }, 5000) + + const sub = relayPool + .subscription(relays, { + kinds: [APP_DATA_KIND], + authors: [pubkey], + '#d': [SETTINGS_IDENTIFIER] + }) + .pipe(onlyEvents(), mapEventsToStore(eventStore)) + .subscribe({ + complete: () => { + clearTimeout(timeout) + if (!hasResolved) { + hasResolved = true + const event = eventStore.replaceable(APP_DATA_KIND, pubkey, SETTINGS_IDENTIFIER).value + if (event) { + const content = getAppDataContent(event) + resolve(content || null) + } else { + resolve(null) + } + } + }, + error: () => { + clearTimeout(timeout) + if (!hasResolved) { + hasResolved = true + resolve(null) + } + } + }) + + setTimeout(() => { + sub.unsubscribe() + }, 5000) + }) +} + +export async function saveSettings( + relayPool: RelayPool, + eventStore: EventStore, + factory: EventFactory, + settings: UserSettings, + relays: string[] +): Promise { + const draft = await factory.create(AppDataBlueprint, SETTINGS_IDENTIFIER, settings, false) + const signed = await factory.sign(draft) + + eventStore.add(signed) + await relayPool.publish(relays, signed) +} + +export function watchSettings( + eventStore: EventStore, + pubkey: string, + callback: (settings: UserSettings | null) => void +) { + return eventStore.replaceable(APP_DATA_KIND, pubkey, SETTINGS_IDENTIFIER).subscribe((event: NostrEvent | undefined) => { + if (event) { + const content = getAppDataContent(event) + callback(content || null) + } else { + callback(null) + } + }) +}