Compare commits

...

3 Commits

Author SHA1 Message Date
Gigi
1609416e1e chore: bump version to 0.2.6 2025-10-08 06:40:39 +01:00
Gigi
00d9fbdbab feat: add home button to bookmark bar
- Add home icon button to sidebar header
- Clicking home button navigates to root path '/'
- Position home button before refresh and settings buttons
2025-10-08 06:34:22 +01:00
Gigi
239ab5763d feat: add configurable zap split for highlights on nostr-native content
- Add zapSplitPercentage setting (default 50%) to UserSettings
- Implement NIP-57 Appendix G zap tags for highlight events
- Add zap tags when creating highlights of nostr-native content
- Split zaps between highlighter and article author based on setting
- Add UI slider in settings to configure split percentage
- Include relay URL in zap tags for metadata lookup
- Only add author zap tag if different from highlighter
2025-10-08 06:32:02 +01:00
9 changed files with 152 additions and 7 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "boris",
"version": "0.2.5",
"version": "0.2.6",
"description": "A minimal nostr client for bookmark management",
"homepage": "https://xn--bris-v0b.com/",
"type": "module",

View File

@@ -109,7 +109,8 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
currentArticle,
selectedUrl,
readerContent,
onHighlightCreated: (highlight) => setHighlights(prev => [highlight, ...prev])
onHighlightCreated: (highlight) => setHighlights(prev => [highlight, ...prev]),
settings
})
// Load nostr-native article if naddr is in URL

View File

@@ -23,6 +23,7 @@ const DEFAULT_SETTINGS: UserSettings = {
defaultHighlightVisibilityNostrverse: true,
defaultHighlightVisibilityFriends: true,
defaultHighlightVisibilityMine: true,
zapSplitPercentage: 50,
}
interface SettingsProps {

View File

@@ -107,6 +107,27 @@ const ReadingDisplaySettings: React.FC<ReadingDisplaySettingsProps> = ({ setting
</div>
</div>
<div className="setting-group">
<label className="setting-label">Zap Split for Highlights</label>
<div className="zap-split-container">
<div className="zap-split-labels">
<span className="zap-split-label">You: {settings.zapSplitPercentage ?? 50}%</span>
<span className="zap-split-label">Author: {100 - (settings.zapSplitPercentage ?? 50)}%</span>
</div>
<input
type="range"
min="0"
max="100"
value={settings.zapSplitPercentage ?? 50}
onChange={(e) => onUpdate({ zapSplitPercentage: parseInt(e.target.value) })}
className="zap-split-slider"
/>
<div className="zap-split-description">
When highlighting nostr-native content, zaps will be split between you and the author.
</div>
</div>
</div>
<div className="setting-preview">
<div className="preview-label">Preview</div>
<div

View File

@@ -1,6 +1,7 @@
import React, { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faChevronRight, faRightFromBracket, faRightToBracket, faUserCircle, faGear, faRotate } from '@fortawesome/free-solid-svg-icons'
import { faChevronRight, faRightFromBracket, faRightToBracket, faUserCircle, faGear, faRotate, faHome } from '@fortawesome/free-solid-svg-icons'
import { Hooks } from 'applesauce-react'
import { useEventModel } from 'applesauce-react/hooks'
import { Models } from 'applesauce-core'
@@ -17,6 +18,7 @@ interface SidebarHeaderProps {
const SidebarHeader: React.FC<SidebarHeaderProps> = ({ onToggleCollapse, onLogout, onOpenSettings, onRefresh, isRefreshing }) => {
const [isConnecting, setIsConnecting] = useState(false)
const navigate = useNavigate()
const activeAccount = Hooks.useActiveAccount()
const accountManager = Hooks.useAccountManager()
const profile = useEventModel(Models.ProfileModel, activeAccount ? [activeAccount.pubkey] : null)
@@ -61,6 +63,13 @@ const SidebarHeader: React.FC<SidebarHeaderProps> = ({ onToggleCollapse, onLogou
<FontAwesomeIcon icon={faChevronRight} />
</button>
<div className="sidebar-header-right">
<IconButton
icon={faHome}
onClick={() => navigate('/')}
title="Home"
ariaLabel="Home"
variant="ghost"
/>
{onRefresh && (
<IconButton
icon={faRotate}

View File

@@ -5,6 +5,7 @@ import { Highlight } from '../types/highlights'
import { ReadableContent } from '../services/readerService'
import { createHighlight, eventToHighlight } from '../services/highlightCreationService'
import { HighlightButtonRef } from '../components/HighlightButton'
import { UserSettings } from '../services/settingsService'
interface UseHighlightCreationParams {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -14,6 +15,7 @@ interface UseHighlightCreationParams {
selectedUrl: string | undefined
readerContent: ReadableContent | undefined
onHighlightCreated: (highlight: Highlight) => void
settings?: UserSettings
}
export const useHighlightCreation = ({
@@ -22,7 +24,8 @@ export const useHighlightCreation = ({
currentArticle,
selectedUrl,
readerContent,
onHighlightCreated
onHighlightCreated,
settings
}: UseHighlightCreationParams) => {
const highlightButtonRef = useRef<HighlightButtonRef>(null)
@@ -56,7 +59,9 @@ export const useHighlightCreation = ({
source,
activeAccount,
relayPool,
contentForContext
contentForContext,
undefined,
settings
)
console.log('✅ Highlight created successfully!')
@@ -67,7 +72,7 @@ export const useHighlightCreation = ({
} catch (error) {
console.error('Failed to create highlight:', error)
}
}, [activeAccount, relayPool, currentArticle, selectedUrl, readerContent, onHighlightCreated])
}, [activeAccount, relayPool, currentArticle, selectedUrl, readerContent, onHighlightCreated, settings])
return {
highlightButtonRef,

View File

@@ -2020,6 +2020,74 @@ body {
font-weight: 500;
}
.zap-split-container {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.zap-split-labels {
display: flex;
justify-content: space-between;
align-items: center;
}
.zap-split-label {
font-size: 0.9rem;
color: #ccc;
font-weight: 500;
}
.zap-split-slider {
width: 100%;
height: 8px;
background: linear-gradient(to right, #646cff 0%, #646cff 50%, #f97316 50%, #f97316 100%);
border-radius: 4px;
outline: none;
cursor: pointer;
-webkit-appearance: none;
appearance: none;
}
.zap-split-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
background: white;
border: 2px solid #646cff;
border-radius: 50%;
cursor: pointer;
transition: all 0.2s;
}
.zap-split-slider::-moz-range-thumb {
width: 20px;
height: 20px;
background: white;
border: 2px solid #646cff;
border-radius: 50%;
cursor: pointer;
transition: all 0.2s;
}
.zap-split-slider::-webkit-slider-thumb:hover {
transform: scale(1.1);
box-shadow: 0 0 8px rgba(100, 108, 255, 0.5);
}
.zap-split-slider::-moz-range-thumb:hover {
transform: scale(1.1);
box-shadow: 0 0 8px rgba(100, 108, 255, 0.5);
}
.zap-split-description {
font-size: 0.8rem;
color: #999;
line-height: 1.4;
text-align: left;
}
.settings-footer {
display: flex;
justify-content: flex-start;

View File

@@ -6,6 +6,7 @@ import { NostrEvent } from 'nostr-tools'
import { Helpers } from 'applesauce-core'
import { RELAYS } from '../config/relays'
import { Highlight } from '../types/highlights'
import { UserSettings } from './settingsService'
const {
getHighlightText,
@@ -30,7 +31,8 @@ export async function createHighlight(
account: IAccount,
relayPool: RelayPool,
contentForContext?: string,
comment?: string
comment?: string,
settings?: UserSettings
): Promise<NostrEvent> {
if (!selectedText || !source) {
throw new Error('Missing required data to create highlight')
@@ -72,6 +74,12 @@ export async function createHighlight(
highlightEvent.tags.push(['alt', 'Highlight created by Boris. readwithboris.com'])
}
// Add zap tags for nostr-native content (NIP-57 Appendix G)
if (typeof source === 'object' && 'kind' in source) {
const zapSplitPercentage = settings?.zapSplitPercentage ?? 50
addZapTags(highlightEvent, account.pubkey, source.pubkey, zapSplitPercentage)
}
// Sign the event
const signedEvent = await factory.sign(highlightEvent)
@@ -176,6 +184,36 @@ function extractContext(selectedText: string, articleContent: string): string |
return contextParts.length > 1 ? contextParts.join(' ') : undefined
}
/**
* Adds zap tags to a highlight event for split payments (NIP-57 Appendix G)
* @param event The highlight event to add zap tags to
* @param highlighterPubkey The pubkey of the user creating the highlight
* @param authorPubkey The pubkey of the original article author
* @param highlighterPercentage Percentage (0-100) to give to the highlighter (default 50)
*/
function addZapTags(
event: NostrEvent,
highlighterPubkey: string,
authorPubkey: string,
highlighterPercentage: number = 50
): void {
// Calculate weights based on percentage
// Using simple integer weights where highlighterPercentage:authorPercentage ratio is maintained
const highlighterWeight = Math.round(highlighterPercentage)
const authorWeight = Math.round(100 - highlighterPercentage)
// Use a reliable relay for zap metadata lookup (first non-local relay)
const zapRelay = RELAYS.find(r => !r.includes('localhost')) || RELAYS[0]
// Add zap tag for the highlighter
event.tags.push(['zap', highlighterPubkey, zapRelay, highlighterWeight.toString()])
// Add zap tag for the original author (only if different from highlighter)
if (authorPubkey !== highlighterPubkey) {
event.tags.push(['zap', authorPubkey, zapRelay, authorWeight.toString()])
}
}
/**
* Converts a NostrEvent to a Highlight object for immediate UI display
*/

View File

@@ -35,6 +35,8 @@ export interface UserSettings {
defaultHighlightVisibilityNostrverse?: boolean
defaultHighlightVisibilityFriends?: boolean
defaultHighlightVisibilityMine?: boolean
// Zap split percentage for highlights (0-100, default 50)
zapSplitPercentage?: number
}
export async function loadSettings(