refactor: make zap split sliders independent using weights

- Changed from percentage-based to weight-based zap splits
- All three sliders (highlighter, author, Boris) are now independent
- Weights are normalized to calculate actual percentages
- UI shows both weight value and calculated percentage
- Added migration logic for users with old percentage-based settings
- Each slider can be adjusted without affecting the others
- Prevents interdependent slider behavior that was confusing

Breaking change: Settings now use zapSplitHighlighterWeight,
zapSplitAuthorWeight, and zapSplitBorisWeight instead of
zapSplitPercentage and borisSupportPercentage
This commit is contained in:
Gigi
2025-10-08 07:06:42 +01:00
parent 9b97715274
commit f6d2f98eae
4 changed files with 113 additions and 53 deletions

View File

@@ -24,8 +24,9 @@ const DEFAULT_SETTINGS: UserSettings = {
defaultHighlightVisibilityNostrverse: true,
defaultHighlightVisibilityFriends: true,
defaultHighlightVisibilityMine: true,
zapSplitPercentage: 50,
borisSupportPercentage: 2.1,
zapSplitHighlighterWeight: 50,
zapSplitBorisWeight: 2.1,
zapSplitAuthorWeight: 50,
}
interface SettingsProps {
@@ -35,11 +36,32 @@ interface SettingsProps {
}
const Settings: React.FC<SettingsProps> = ({ settings, onSave, onClose }) => {
const [localSettings, setLocalSettings] = useState<UserSettings>(settings)
const [localSettings, setLocalSettings] = useState<UserSettings>(() => {
// Migrate old settings format to new weight-based format
const migrated = { ...settings }
const anySettings = migrated as Record<string, unknown>
if ('zapSplitPercentage' in anySettings && !('zapSplitHighlighterWeight' in migrated)) {
migrated.zapSplitHighlighterWeight = (anySettings.zapSplitPercentage as number) ?? 50
migrated.zapSplitAuthorWeight = 100 - ((anySettings.zapSplitPercentage as number) ?? 50)
}
if ('borisSupportPercentage' in anySettings && !('zapSplitBorisWeight' in migrated)) {
migrated.zapSplitBorisWeight = (anySettings.borisSupportPercentage as number) ?? 2.1
}
return migrated
})
const isInitialMount = useRef(true)
useEffect(() => {
setLocalSettings(settings)
const migrated = { ...settings }
const anySettings = migrated as Record<string, unknown>
if ('zapSplitPercentage' in anySettings && !('zapSplitHighlighterWeight' in migrated)) {
migrated.zapSplitHighlighterWeight = (anySettings.zapSplitPercentage as number) ?? 50
migrated.zapSplitAuthorWeight = 100 - ((anySettings.zapSplitPercentage as number) ?? 50)
}
if ('borisSupportPercentage' in anySettings && !('zapSplitBorisWeight' in migrated)) {
migrated.zapSplitBorisWeight = (anySettings.borisSupportPercentage as number) ?? 2.1
}
setLocalSettings(migrated)
}, [settings])
useEffect(() => {

View File

@@ -7,34 +7,53 @@ interface ZapSettingsProps {
}
const ZapSettings: React.FC<ZapSettingsProps> = ({ settings, onUpdate }) => {
const highlighterPercentage = settings.zapSplitPercentage ?? 50
const borisPercentage = settings.borisSupportPercentage ?? 2.1
const authorPercentage = Math.max(0, 100 - highlighterPercentage - borisPercentage)
const highlighterWeight = settings.zapSplitHighlighterWeight ?? 50
const borisWeight = settings.zapSplitBorisWeight ?? 2.1
const authorWeight = settings.zapSplitAuthorWeight ?? 50
// Calculate actual percentages from weights
const totalWeight = highlighterWeight + borisWeight + authorWeight
const highlighterPercentage = totalWeight > 0 ? (highlighterWeight / totalWeight) * 100 : 0
const borisPercentage = totalWeight > 0 ? (borisWeight / totalWeight) * 100 : 0
const authorPercentage = totalWeight > 0 ? (authorWeight / totalWeight) * 100 : 0
return (
<div className="settings-section">
<h3 className="section-title">Zap Splits</h3>
<div className="setting-group">
<label className="setting-label">Split Percentage for Highlights</label>
<label className="setting-label">Your Share</label>
<div className="zap-split-container">
<div className="zap-split-labels">
<span className="zap-split-label">You: {highlighterPercentage}%</span>
<span className="zap-split-label">Author(s): {authorPercentage.toFixed(1)}%</span>
<span className="zap-split-label">Boris: {borisPercentage}%</span>
<span className="zap-split-label">Weight: {highlighterWeight}</span>
<span className="zap-split-label">({highlighterPercentage.toFixed(1)}%)</span>
</div>
<input
type="range"
min="0"
max="100"
value={highlighterPercentage}
onChange={(e) => onUpdate({ zapSplitPercentage: parseInt(e.target.value) })}
value={highlighterWeight}
onChange={(e) => onUpdate({ zapSplitHighlighterWeight: parseInt(e.target.value) })}
className="zap-split-slider"
/>
<div className="zap-split-description">
When you highlight nostr-native content, zaps will be split between you (curator) and the author(s).
If the content has multiple authors, their share is divided proportionally.
</div>
</div>
<div className="setting-group">
<label className="setting-label">Author(s) Share</label>
<div className="zap-split-container">
<div className="zap-split-labels">
<span className="zap-split-label">Weight: {authorWeight}</span>
<span className="zap-split-label">({authorPercentage.toFixed(1)}%)</span>
</div>
<input
type="range"
min="0"
max="100"
value={authorWeight}
onChange={(e) => onUpdate({ zapSplitAuthorWeight: parseInt(e.target.value) })}
className="zap-split-slider"
/>
</div>
</div>
@@ -42,22 +61,25 @@ const ZapSettings: React.FC<ZapSettingsProps> = ({ settings, onUpdate }) => {
<label className="setting-label">Support Boris</label>
<div className="zap-split-container">
<div className="zap-split-labels">
<span className="zap-split-label">{borisPercentage.toFixed(1)}%</span>
<span className="zap-split-label">Weight: {borisWeight.toFixed(1)}</span>
<span className="zap-split-label">({borisPercentage.toFixed(1)}%)</span>
</div>
<input
type="range"
min="0"
max="10"
step="0.1"
value={borisPercentage}
onChange={(e) => onUpdate({ borisSupportPercentage: parseFloat(e.target.value) })}
value={borisWeight}
onChange={(e) => onUpdate({ zapSplitBorisWeight: parseFloat(e.target.value) })}
className="zap-split-slider"
/>
<div className="zap-split-description">
Optional: Include a small percentage for Boris development and maintenance.
</div>
</div>
</div>
<div className="zap-split-description">
Weights determine zap splits when highlighting nostr-native content.
If the content has multiple authors, their share is divided proportionally.
</div>
</div>
)
}

View File

@@ -79,9 +79,26 @@ export async function createHighlight(
// Add zap tags for nostr-native content (NIP-57 Appendix G)
if (typeof source === 'object' && 'kind' in source) {
const zapSplitPercentage = settings?.zapSplitPercentage ?? 50
const borisSupportPercentage = settings?.borisSupportPercentage ?? 2.1
addZapTags(highlightEvent, account.pubkey, source, zapSplitPercentage, borisSupportPercentage)
// Migrate old settings format to new weight-based format if needed
let highlighterWeight = settings?.zapSplitHighlighterWeight
let borisWeight = settings?.zapSplitBorisWeight
let authorWeight = settings?.zapSplitAuthorWeight
const anySettings = settings as Record<string, unknown> | undefined
if (!highlighterWeight && anySettings && 'zapSplitPercentage' in anySettings) {
highlighterWeight = anySettings.zapSplitPercentage as number
authorWeight = 100 - (anySettings.zapSplitPercentage as number)
}
if (!borisWeight && anySettings && 'borisSupportPercentage' in anySettings) {
borisWeight = anySettings.borisSupportPercentage as number
}
// Use defaults if still undefined
highlighterWeight = highlighterWeight ?? 50
borisWeight = borisWeight ?? 2.1
authorWeight = authorWeight ?? 50
addZapTags(highlightEvent, account.pubkey, source, highlighterWeight, borisWeight, authorWeight)
}
// Sign the event
@@ -194,37 +211,38 @@ function extractContext(selectedText: string, articleContent: string): string |
* @param event The highlight event to add zap tags to
* @param highlighterPubkey The pubkey of the user creating the highlight
* @param sourceEvent The source event (may contain existing zap tags)
* @param highlighterPercentage Percentage (0-100) to give to the highlighter (default 50)
* @param borisPercentage Percentage (0-100) to give to Boris (default 2.1)
* @param highlighterWeight Weight to give to the highlighter (default 50)
* @param borisWeight Weight to give to Boris (default 2.1)
* @param authorWeight Weight to give to author(s) (default 50)
*/
function addZapTags(
event: NostrEvent,
highlighterPubkey: string,
sourceEvent: NostrEvent,
highlighterPercentage: number = 50,
borisPercentage: number = 2.1
highlighterWeight: number = 50,
borisWeight: number = 2.1,
authorWeight: number = 50
): void {
// Use a reliable relay for zap metadata lookup (first non-local relay)
const zapRelay = RELAYS.find(r => !r.includes('localhost')) || RELAYS[0]
// Calculate author group percentage (what remains after highlighter and Boris)
const authorGroupPercentage = Math.max(0, 100 - highlighterPercentage - borisPercentage)
// Extract existing zap tags from source event (the "author group")
const existingZapTags = sourceEvent.tags.filter(tag => tag[0] === 'zap')
// Add zap tag for the highlighter
event.tags.push(['zap', highlighterPubkey, zapRelay, highlighterPercentage.toString()])
// Add zap tag for Boris (if percentage > 0 and Boris is not the highlighter)
if (borisPercentage > 0 && BORIS_PUBKEY !== highlighterPubkey) {
event.tags.push(['zap', BORIS_PUBKEY, zapRelay, borisPercentage.toFixed(1)])
if (highlighterWeight > 0) {
event.tags.push(['zap', highlighterPubkey, zapRelay, highlighterWeight.toString()])
}
if (existingZapTags.length > 0) {
// Add zap tag for Boris (if weight > 0 and Boris is not the highlighter)
if (borisWeight > 0 && BORIS_PUBKEY !== highlighterPubkey) {
event.tags.push(['zap', BORIS_PUBKEY, zapRelay, borisWeight.toFixed(1)])
}
if (existingZapTags.length > 0 && authorWeight > 0) {
// Calculate total weight from existing zap tags
const totalExistingWeight = existingZapTags.reduce((sum, tag) => {
const weight = parseInt(tag[3] || '1', 10)
const weight = parseFloat(tag[3] || '1')
return sum + weight
}, 0)
@@ -236,25 +254,23 @@ function addZapTags(
// Skip if this is the highlighter or Boris (they already have their shares)
if (authorPubkey === highlighterPubkey || authorPubkey === BORIS_PUBKEY) continue
const originalWeight = parseInt(zapTag[3] || '1', 10)
const originalWeight = parseFloat(zapTag[3] || '1')
const originalRelay = zapTag[2] || zapRelay
// Calculate proportional weight: (original weight / total weight) * author group percentage
const adjustedWeight = (originalWeight / totalExistingWeight) * authorGroupPercentage
// Calculate proportional weight: (original weight / total weight) * author group weight
const adjustedWeight = (originalWeight / totalExistingWeight) * authorWeight
// Only add if weight is greater than 0
if (adjustedWeight > 0) {
event.tags.push(['zap', authorPubkey, originalRelay, adjustedWeight.toFixed(1)])
}
}
} else {
// No existing zap tags, use simple split between highlighter, Boris, and source author
} else if (authorWeight > 0) {
// No existing zap tags, give full author weight to source author
// Add zap tag for the original author (only if different from highlighter and Boris, and if weight > 0)
if (sourceEvent.pubkey !== highlighterPubkey &&
sourceEvent.pubkey !== BORIS_PUBKEY &&
authorGroupPercentage > 0) {
event.tags.push(['zap', sourceEvent.pubkey, zapRelay, authorGroupPercentage.toFixed(1)])
// Add zap tag for the original author (only if different from highlighter and Boris)
if (sourceEvent.pubkey !== highlighterPubkey && sourceEvent.pubkey !== BORIS_PUBKEY) {
event.tags.push(['zap', sourceEvent.pubkey, zapRelay, authorWeight.toFixed(1)])
}
}
}

View File

@@ -35,10 +35,10 @@ export interface UserSettings {
defaultHighlightVisibilityNostrverse?: boolean
defaultHighlightVisibilityFriends?: boolean
defaultHighlightVisibilityMine?: boolean
// Zap split percentage for highlights (0-100, default 50)
zapSplitPercentage?: number
// Boris support percentage (0-100, default 2.1)
borisSupportPercentage?: number
// Zap split weights (treated as relative weights, not strict percentages)
zapSplitHighlighterWeight?: number // default 50
zapSplitBorisWeight?: number // default 2.1
zapSplitAuthorWeight?: number // default 50
}
export async function loadSettings(