mirror of
https://github.com/dergigi/boris.git
synced 2026-02-23 07:54:59 +01:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19d88c5fba | ||
|
|
461b0936e2 | ||
|
|
e9ee5e87be | ||
|
|
5e66c5ef76 | ||
|
|
307dc3d726 | ||
|
|
e514a5f063 | ||
|
|
880b7974f4 | ||
|
|
47048f435f | ||
|
|
53ad492729 | ||
|
|
eb4da419ae | ||
|
|
c66dfc9e2e | ||
|
|
a31f05d498 | ||
|
|
6548e89c54 | ||
|
|
8a21b46ebd | ||
|
|
bc5fe1ae30 | ||
|
|
b57ea3f640 | ||
|
|
3b55d64468 | ||
|
|
4caf1f0b22 | ||
|
|
1eb9911645 | ||
|
|
38268c453c | ||
|
|
9686b80b09 | ||
|
|
f32dec16fb | ||
|
|
cb444b532f | ||
|
|
962062130a | ||
|
|
e429931139 | ||
|
|
e56d28f82a | ||
|
|
13a30d35c4 | ||
|
|
e3174d8777 | ||
|
|
829a8d5dca | ||
|
|
00978e2e64 | ||
|
|
a5fcf36e83 | ||
|
|
a92a9ee3a3 | ||
|
|
f39e34c699 | ||
|
|
b58f34d587 | ||
|
|
76d1d4544e | ||
|
|
5e56176e2d |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -7,3 +7,7 @@ dist
|
|||||||
# Misc
|
# Misc
|
||||||
*.log
|
*.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# Applesauce Reference
|
||||||
|
applesauce
|
||||||
|
|
||||||
|
|||||||
67
CHANGELOG.md
67
CHANGELOG.md
@@ -5,6 +5,67 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.3.6] - 2025-10-10
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Compact date format for highlights (now, 5m, 3h, 2d, 1mo, 1y)
|
||||||
|
- Ultra-compact date format for bookmarks sidebar
|
||||||
|
- Encode event links as nevent/naddr per NIP-19 for better client compatibility
|
||||||
|
- Render /explore within ThreePaneLayout to keep side panels visible
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Remove incorrect padding-right from highlights container
|
||||||
|
- Reduce font size of highlight metadata for cleaner look
|
||||||
|
- Position highlight FAB button relative to article pane instead of viewport
|
||||||
|
- Adjust relay indicator position for better visual alignment
|
||||||
|
- Ensure highlight metadata elements align on single visual line with consistent line-height
|
||||||
|
- Prevent bookmark icons from being cut off in compact view
|
||||||
|
- Clean up nested borders in bookmark items and sidebar view mode controls
|
||||||
|
- Align highlight metadata elements on single line in sidebar
|
||||||
|
- Change explore header icon from compass to newspaper
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Make connecting notification more subtle with muted blue background
|
||||||
|
- Update Boris pubkey for zap splits to npub19802see0gnk3vjlus0dnmfdagusqrtmsxpl5yfmkwn9uvnfnqylqduhr0x
|
||||||
|
- Update domain references to read.withboris.com (URLs, SEO metadata, and documentation)
|
||||||
|
|
||||||
|
## [0.3.5] - 2025-10-09
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Ensure connecting state shows for minimum 15 seconds to prevent premature offline display
|
||||||
|
- Add Cloudflare Pages routing config for SPA paths
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Extend connecting state duration and remove subtitle text for cleaner UI
|
||||||
|
|
||||||
|
## [0.3.4] - 2025-10-09
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Add p tag (author tag) to highlights of nostr-native content for proper attribution
|
||||||
|
|
||||||
|
## [0.3.3] - 2025-10-09
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Service Worker for robust offline image caching
|
||||||
|
- /explore route to discover blog posts from friends on Nostr
|
||||||
|
- Explore button (newspaper icon) in bookmarks header
|
||||||
|
- "Connecting" status indicator on page load (instead of immediately showing "Offline")
|
||||||
|
- Last fetch time display with relative timestamps in bookmarks list
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Simplify image caching to use Service Worker transparently
|
||||||
|
- Move refresh button from top bar to end of bookmarks list
|
||||||
|
- Make explore page article cards proper links (supports CMD+click to open in new tab)
|
||||||
|
- Reorganize bookmarks UI for better UX
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Improve image cache resilience for offline viewing and hard reloads
|
||||||
|
- Correct TypeScript types for cache stats state
|
||||||
|
- Resolve linter errors for unused parameters
|
||||||
|
- Import useEventModel from applesauce-react/hooks for proper type safety
|
||||||
|
- Import Models from applesauce-core instead of applesauce-react
|
||||||
|
- Use correct useEventModel hook for profile loading in BlogPostCard
|
||||||
|
|
||||||
## [0.3.0] - 2025-10-09
|
## [0.3.0] - 2025-10-09
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -472,6 +533,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Optimize relay usage following applesauce-relay best practices
|
- Optimize relay usage following applesauce-relay best practices
|
||||||
- Use applesauce-react event models for better profile handling
|
- Use applesauce-react event models for better profile handling
|
||||||
|
|
||||||
|
[0.3.6]: https://github.com/dergigi/boris/compare/v0.3.5...v0.3.6
|
||||||
|
[0.3.5]: https://github.com/dergigi/boris/compare/v0.3.4...v0.3.5
|
||||||
|
[0.3.4]: https://github.com/dergigi/boris/compare/v0.3.3...v0.3.4
|
||||||
|
[0.3.3]: https://github.com/dergigi/boris/compare/v0.3.2...v0.3.3
|
||||||
|
[0.3.2]: https://github.com/dergigi/boris/compare/v0.3.1...v0.3.2
|
||||||
|
[0.3.1]: https://github.com/dergigi/boris/compare/v0.3.0...v0.3.1
|
||||||
[0.3.0]: https://github.com/dergigi/boris/compare/v0.2.10...v0.3.0
|
[0.3.0]: https://github.com/dergigi/boris/compare/v0.2.10...v0.3.0
|
||||||
[0.2.10]: https://github.com/dergigi/boris/compare/v0.2.9...v0.2.10
|
[0.2.10]: https://github.com/dergigi/boris/compare/v0.2.9...v0.2.10
|
||||||
[0.2.9]: https://github.com/dergigi/boris/compare/v0.2.8...v0.2.9
|
[0.2.9]: https://github.com/dergigi/boris/compare/v0.2.8...v0.2.9
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Boris turns your Nostr bookmarks into a calm, fast, and focused reading experien
|
|||||||
|
|
||||||
## Live
|
## Live
|
||||||
|
|
||||||
- App: [https://xn--bris-v0b.com/](https://xn--bris-v0b.com/)
|
- App: [https://read.withboris.com/](https://read.withboris.com/)
|
||||||
|
|
||||||
## The Vision
|
## The Vision
|
||||||
|
|
||||||
|
|||||||
@@ -6,18 +6,18 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Boris - Nostr Bookmarks</title>
|
<title>Boris - Nostr Bookmarks</title>
|
||||||
<meta name="description" content="Your reading list for the Nostr world. A minimal nostr client for bookmark management with highlights." />
|
<meta name="description" content="Your reading list for the Nostr world. A minimal nostr client for bookmark management with highlights." />
|
||||||
<link rel="canonical" href="https://xn--bris-v0b.com/" />
|
<link rel="canonical" href="https://read.withboris.com/" />
|
||||||
|
|
||||||
<!-- Open Graph / Social Media -->
|
<!-- Open Graph / Social Media -->
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:url" content="https://xn--bris-v0b.com/" />
|
<meta property="og:url" content="https://read.withboris.com/" />
|
||||||
<meta property="og:title" content="Boris - Nostr Bookmarks" />
|
<meta property="og:title" content="Boris - Nostr Bookmarks" />
|
||||||
<meta property="og:description" content="Your reading list for the Nostr world. A minimal nostr client for bookmark management with highlights." />
|
<meta property="og:description" content="Your reading list for the Nostr world. A minimal nostr client for bookmark management with highlights." />
|
||||||
<meta property="og:site_name" content="Boris" />
|
<meta property="og:site_name" content="Boris" />
|
||||||
|
|
||||||
<!-- Twitter Card -->
|
<!-- Twitter Card -->
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<meta name="twitter:url" content="https://xn--bris-v0b.com/" />
|
<meta name="twitter:url" content="https://read.withboris.com/" />
|
||||||
<meta name="twitter:title" content="Boris - Nostr Bookmarks" />
|
<meta name="twitter:title" content="Boris - Nostr Bookmarks" />
|
||||||
<meta name="twitter:description" content="Your reading list for the Nostr world. A minimal nostr client for bookmark management with highlights." />
|
<meta name="twitter:description" content="Your reading list for the Nostr world. A minimal nostr client for bookmark management with highlights." />
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "boris",
|
"name": "boris",
|
||||||
"version": "0.3.4",
|
"version": "0.3.7",
|
||||||
"description": "A minimal nostr client for bookmark management",
|
"description": "A minimal nostr client for bookmark management",
|
||||||
"homepage": "https://read.withboris.com/",
|
"homepage": "https://read.withboris.com/",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
6
public/_routes.json
Normal file
6
public/_routes.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"include": ["/*"],
|
||||||
|
"exclude": ["/assets/*", "/robots.txt", "/sw.js", "/_headers", "/_redirects"]
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
User-agent: *
|
User-agent: *
|
||||||
Allow: /
|
Allow: /
|
||||||
|
|
||||||
Sitemap: https://xn--bris-v0b.com/sitemap.xml
|
Sitemap: https://read.withboris.com/sitemap.xml
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { registerCommonAccountTypes } from 'applesauce-accounts/accounts'
|
|||||||
import { RelayPool } from 'applesauce-relay'
|
import { RelayPool } from 'applesauce-relay'
|
||||||
import { createAddressLoader } from 'applesauce-loaders/loaders'
|
import { createAddressLoader } from 'applesauce-loaders/loaders'
|
||||||
import Bookmarks from './components/Bookmarks'
|
import Bookmarks from './components/Bookmarks'
|
||||||
import Explore from './components/Explore'
|
|
||||||
import Toast from './components/Toast'
|
import Toast from './components/Toast'
|
||||||
import { useToast } from './hooks/useToast'
|
import { useToast } from './hooks/useToast'
|
||||||
import { RELAYS } from './config/relays'
|
import { RELAYS } from './config/relays'
|
||||||
@@ -28,8 +27,7 @@ function AppRoutes({
|
|||||||
const accountManager = Hooks.useAccountManager()
|
const accountManager = Hooks.useAccountManager()
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
accountManager.setActive(undefined as never)
|
accountManager.clearActive()
|
||||||
localStorage.removeItem('active')
|
|
||||||
showToast('Logged out successfully')
|
showToast('Logged out successfully')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +63,10 @@ function AppRoutes({
|
|||||||
<Route
|
<Route
|
||||||
path="/explore"
|
path="/explore"
|
||||||
element={
|
element={
|
||||||
<Explore relayPool={relayPool} />
|
<Bookmarks
|
||||||
|
relayPool={relayPool}
|
||||||
|
onLogout={handleLogout}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route path="/" element={<Navigate to={`/a/${DEFAULT_ARTICLE}`} replace />} />
|
<Route path="/" element={<Navigate to={`/a/${DEFAULT_ARTICLE}`} replace />} />
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react'
|
|||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { faBookmark, faUserLock, faGlobe } from '@fortawesome/free-solid-svg-icons'
|
import { faBookmark, faUserLock, faGlobe } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { IndividualBookmark } from '../../types/bookmarks'
|
import { IndividualBookmark } from '../../types/bookmarks'
|
||||||
import { formatDate } from '../../utils/bookmarkUtils'
|
import { formatDateCompact } from '../../utils/bookmarkUtils'
|
||||||
import ContentWithResolvedProfiles from '../ContentWithResolvedProfiles'
|
import ContentWithResolvedProfiles from '../ContentWithResolvedProfiles'
|
||||||
import { IconGetter } from './shared'
|
import { IconGetter } from './shared'
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ export const CompactView: React.FC<CompactViewProps> = ({
|
|||||||
<ContentWithResolvedProfiles content={displayText.slice(0, 60) + (displayText.length > 60 ? '…' : '')} />
|
<ContentWithResolvedProfiles content={displayText.slice(0, 60) + (displayText.length > 60 ? '…' : '')} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<span className="bookmark-date-compact">{formatDate(bookmark.created_at)}</span>
|
<span className="bookmark-date-compact">{formatDateCompact(bookmark.created_at)}</span>
|
||||||
{isClickable && (
|
{isClickable && (
|
||||||
<button
|
<button
|
||||||
className="compact-read-btn"
|
className="compact-read-btn"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { useBookmarksUI } from '../hooks/useBookmarksUI'
|
|||||||
import { useRelayStatus } from '../hooks/useRelayStatus'
|
import { useRelayStatus } from '../hooks/useRelayStatus'
|
||||||
import { useOfflineSync } from '../hooks/useOfflineSync'
|
import { useOfflineSync } from '../hooks/useOfflineSync'
|
||||||
import ThreePaneLayout from './ThreePaneLayout'
|
import ThreePaneLayout from './ThreePaneLayout'
|
||||||
|
import Explore from './Explore'
|
||||||
import { classifyHighlights } from '../utils/highlightClassification'
|
import { classifyHighlights } from '../utils/highlightClassification'
|
||||||
|
|
||||||
export type ViewMode = 'compact' | 'cards' | 'large'
|
export type ViewMode = 'compact' | 'cards' | 'large'
|
||||||
@@ -33,6 +34,7 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
|||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const showSettings = location.pathname === '/settings'
|
const showSettings = location.pathname === '/settings'
|
||||||
|
const showExplore = location.pathname === '/explore'
|
||||||
|
|
||||||
// Track previous location for going back from settings
|
// Track previous location for going back from settings
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -179,6 +181,7 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
|||||||
isCollapsed={isCollapsed}
|
isCollapsed={isCollapsed}
|
||||||
isHighlightsCollapsed={isHighlightsCollapsed}
|
isHighlightsCollapsed={isHighlightsCollapsed}
|
||||||
showSettings={showSettings}
|
showSettings={showSettings}
|
||||||
|
showExplore={showExplore}
|
||||||
bookmarks={bookmarks}
|
bookmarks={bookmarks}
|
||||||
bookmarksLoading={bookmarksLoading}
|
bookmarksLoading={bookmarksLoading}
|
||||||
viewMode={viewMode}
|
viewMode={viewMode}
|
||||||
@@ -227,6 +230,9 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
|||||||
highlightButtonRef={highlightButtonRef}
|
highlightButtonRef={highlightButtonRef}
|
||||||
onCreateHighlight={handleCreateHighlight}
|
onCreateHighlight={handleCreateHighlight}
|
||||||
hasActiveAccount={!!(activeAccount && relayPool)}
|
hasActiveAccount={!!(activeAccount && relayPool)}
|
||||||
|
explore={showExplore ? (
|
||||||
|
relayPool ? <Explore relayPool={relayPool} /> : null
|
||||||
|
) : undefined}
|
||||||
toastMessage={toastMessage ?? undefined}
|
toastMessage={toastMessage ?? undefined}
|
||||||
toastType={toastType}
|
toastType={toastType}
|
||||||
onClearToast={clearToast}
|
onClearToast={clearToast}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { faSpinner, faExclamationCircle, faCompass } from '@fortawesome/free-solid-svg-icons'
|
import { faSpinner, faExclamationCircle, faNewspaper } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { Hooks } from 'applesauce-react'
|
import { Hooks } from 'applesauce-react'
|
||||||
import { RelayPool } from 'applesauce-relay'
|
import { RelayPool } from 'applesauce-relay'
|
||||||
import { nip19 } from 'nostr-tools'
|
import { nip19 } from 'nostr-tools'
|
||||||
@@ -105,7 +105,7 @@ const Explore: React.FC<ExploreProps> = ({ relayPool }) => {
|
|||||||
<div className="explore-container">
|
<div className="explore-container">
|
||||||
<div className="explore-header">
|
<div className="explore-header">
|
||||||
<h1>
|
<h1>
|
||||||
<FontAwesomeIcon icon={faCompass} />
|
<FontAwesomeIcon icon={faNewspaper} />
|
||||||
Explore
|
Explore
|
||||||
</h1>
|
</h1>
|
||||||
<p className="explore-subtitle">
|
<p className="explore-subtitle">
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ import React, { useEffect, useRef, useState } from 'react'
|
|||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { faQuoteLeft, faExternalLinkAlt, faPlane, faSpinner, faServer } from '@fortawesome/free-solid-svg-icons'
|
import { faQuoteLeft, faExternalLinkAlt, faPlane, faSpinner, faServer } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { Highlight } from '../types/highlights'
|
import { Highlight } from '../types/highlights'
|
||||||
import { formatDistanceToNow } from 'date-fns'
|
|
||||||
import { useEventModel } from 'applesauce-react/hooks'
|
import { useEventModel } from 'applesauce-react/hooks'
|
||||||
import { Models, IEventStore } from 'applesauce-core'
|
import { Models, IEventStore } from 'applesauce-core'
|
||||||
import { RelayPool } from 'applesauce-relay'
|
import { RelayPool } from 'applesauce-relay'
|
||||||
import { onSyncStateChange, isEventSyncing } from '../services/offlineSyncService'
|
import { onSyncStateChange, isEventSyncing } from '../services/offlineSyncService'
|
||||||
import { RELAYS } from '../config/relays'
|
import { RELAYS } from '../config/relays'
|
||||||
import { areAllRelaysLocal } from '../utils/helpers'
|
import { areAllRelaysLocal } from '../utils/helpers'
|
||||||
|
import { nip19 } from 'nostr-tools'
|
||||||
|
import { formatDateCompact } from '../utils/bookmarkUtils'
|
||||||
|
|
||||||
interface HighlightWithLevel extends Highlight {
|
interface HighlightWithLevel extends Highlight {
|
||||||
level?: 'mine' | 'friends' | 'nostrverse'
|
level?: 'mine' | 'friends' | 'nostrverse'
|
||||||
@@ -102,7 +103,41 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
|
|||||||
|
|
||||||
const getSourceLink = () => {
|
const getSourceLink = () => {
|
||||||
if (highlight.eventReference) {
|
if (highlight.eventReference) {
|
||||||
return `https://search.dergigi.com/e/${highlight.eventReference}`
|
// Check if it's a coordinate string (kind:pubkey:identifier) or a simple event ID
|
||||||
|
if (highlight.eventReference.includes(':')) {
|
||||||
|
// It's an addressable event coordinate, encode as naddr
|
||||||
|
const parts = highlight.eventReference.split(':')
|
||||||
|
if (parts.length === 3) {
|
||||||
|
const [kindStr, pubkey, identifier] = parts
|
||||||
|
const kind = parseInt(kindStr, 10)
|
||||||
|
|
||||||
|
// Get non-local relays for the hint
|
||||||
|
const relayHints = RELAYS.filter(r =>
|
||||||
|
!r.includes('localhost') && !r.includes('127.0.0.1')
|
||||||
|
).slice(0, 3) // Include up to 3 relay hints
|
||||||
|
|
||||||
|
const naddr = nip19.naddrEncode({
|
||||||
|
kind,
|
||||||
|
pubkey,
|
||||||
|
identifier,
|
||||||
|
relays: relayHints
|
||||||
|
})
|
||||||
|
return `https://njump.me/${naddr}`
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// It's a simple event ID, encode as nevent
|
||||||
|
// Get non-local relays for the hint
|
||||||
|
const relayHints = RELAYS.filter(r =>
|
||||||
|
!r.includes('localhost') && !r.includes('127.0.0.1')
|
||||||
|
).slice(0, 3) // Include up to 3 relay hints
|
||||||
|
|
||||||
|
const nevent = nip19.neventEncode({
|
||||||
|
id: highlight.eventReference,
|
||||||
|
relays: relayHints,
|
||||||
|
author: highlight.author
|
||||||
|
})
|
||||||
|
return `https://njump.me/${nevent}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return highlight.urlReference
|
return highlight.urlReference
|
||||||
}
|
}
|
||||||
@@ -248,7 +283,7 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
<span className="highlight-meta-separator">•</span>
|
<span className="highlight-meta-separator">•</span>
|
||||||
<span className="highlight-time">
|
<span className="highlight-time">
|
||||||
{formatDistanceToNow(new Date(highlight.created_at * 1000), { addSuffix: true })}
|
{formatDateCompact(highlight.created_at)}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{sourceLink && (
|
{sourceLink && (
|
||||||
|
|||||||
@@ -32,11 +32,11 @@ export const RelayStatusIndicator: React.FC<RelayStatusIndicatorProps> = ({ rela
|
|||||||
// Connected! Stop showing connecting state
|
// Connected! Stop showing connecting state
|
||||||
setIsConnecting(false)
|
setIsConnecting(false)
|
||||||
} else {
|
} else {
|
||||||
// No connections yet - show connecting for 4 seconds
|
// No connections yet - show connecting for 8 seconds
|
||||||
setIsConnecting(true)
|
setIsConnecting(true)
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
setIsConnecting(false)
|
setIsConnecting(false)
|
||||||
}, 4000)
|
}, 8000)
|
||||||
return () => clearTimeout(timeout)
|
return () => clearTimeout(timeout)
|
||||||
}
|
}
|
||||||
}, [connectedUrls.length])
|
}, [connectedUrls.length])
|
||||||
@@ -58,7 +58,7 @@ export const RelayStatusIndicator: React.FC<RelayStatusIndicatorProps> = ({ rela
|
|||||||
if (!localOnlyMode && !offlineMode && !isConnecting) return null
|
if (!localOnlyMode && !offlineMode && !isConnecting) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relay-status-indicator" title={
|
<div className={`relay-status-indicator ${isConnecting ? 'connecting' : ''}`} title={
|
||||||
isConnecting
|
isConnecting
|
||||||
? 'Connecting to relays...'
|
? 'Connecting to relays...'
|
||||||
: offlineMode
|
: offlineMode
|
||||||
@@ -70,10 +70,7 @@ export const RelayStatusIndicator: React.FC<RelayStatusIndicatorProps> = ({ rela
|
|||||||
</div>
|
</div>
|
||||||
<div className="relay-status-text">
|
<div className="relay-status-text">
|
||||||
{isConnecting ? (
|
{isConnecting ? (
|
||||||
<>
|
<span className="relay-status-title">Connecting</span>
|
||||||
<span className="relay-status-title">Connecting</span>
|
|
||||||
<span className="relay-status-subtitle">Establishing connections...</span>
|
|
||||||
</>
|
|
||||||
) : offlineMode ? (
|
) : offlineMode ? (
|
||||||
<>
|
<>
|
||||||
<span className="relay-status-title">Offline</span>
|
<span className="relay-status-title">Offline</span>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ interface ThreePaneLayoutProps {
|
|||||||
isCollapsed: boolean
|
isCollapsed: boolean
|
||||||
isHighlightsCollapsed: boolean
|
isHighlightsCollapsed: boolean
|
||||||
showSettings: boolean
|
showSettings: boolean
|
||||||
|
showExplore?: boolean
|
||||||
|
|
||||||
// Bookmarks pane
|
// Bookmarks pane
|
||||||
bookmarks: Bookmark[]
|
bookmarks: Bookmark[]
|
||||||
@@ -72,6 +73,9 @@ interface ThreePaneLayoutProps {
|
|||||||
toastMessage?: string
|
toastMessage?: string
|
||||||
toastType?: 'success' | 'error'
|
toastType?: 'success' | 'error'
|
||||||
onClearToast: () => void
|
onClearToast: () => void
|
||||||
|
|
||||||
|
// Optional Explore content
|
||||||
|
explore?: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
|
const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
|
||||||
@@ -105,6 +109,11 @@ const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
|
|||||||
onClose={props.onCloseSettings}
|
onClose={props.onCloseSettings}
|
||||||
relayPool={props.relayPool}
|
relayPool={props.relayPool}
|
||||||
/>
|
/>
|
||||||
|
) : props.showExplore && props.explore ? (
|
||||||
|
// Render Explore inside the main pane to keep side panels
|
||||||
|
<>
|
||||||
|
{props.explore}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<ContentPanel
|
<ContentPanel
|
||||||
loading={props.readerLoading}
|
loading={props.readerLoading}
|
||||||
|
|||||||
@@ -71,15 +71,16 @@ body {
|
|||||||
|
|
||||||
.bookmarks-container .view-mode-controls {
|
.bookmarks-container .view-mode-controls {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
padding: 0.75rem 1rem;
|
padding: 1rem;
|
||||||
border-top: 1px solid #333;
|
border-top: 1px solid #333;
|
||||||
background: #1a1a1a;
|
background: transparent;
|
||||||
border-radius: 0 0 12px 12px;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bookmarks-container .bookmarks-list {
|
.bookmarks-container .bookmarks-list {
|
||||||
padding: 0.25rem;
|
padding: 0.5rem;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@@ -108,13 +109,8 @@ body {
|
|||||||
.view-mode-controls {
|
.view-mode-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
background: #1a1a1a;
|
|
||||||
border: 1px solid #333;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-avatar {
|
.profile-avatar {
|
||||||
@@ -747,11 +743,11 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.individual-bookmark {
|
.individual-bookmark {
|
||||||
background: #2a2a2a;
|
background: transparent;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
border: 1px solid #333;
|
border: 1px solid transparent;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
@@ -759,23 +755,26 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.individual-bookmark:hover {
|
.individual-bookmark:hover {
|
||||||
border-color: #444;
|
border-color: transparent;
|
||||||
background: #2d2d2d;
|
background: #2a2a2a;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Compact view styles */
|
/* Compact view styles */
|
||||||
.individual-bookmark.compact {
|
.individual-bookmark.compact {
|
||||||
padding: 0.3rem 0.25rem;
|
padding: 0.5rem 0.5rem;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-bottom: 1px solid #333;
|
border: none;
|
||||||
|
border-bottom: 1px solid #2a2a2a;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.individual-bookmark.compact:hover {
|
.individual-bookmark.compact:hover {
|
||||||
background: #2a2a2a;
|
background: #252525;
|
||||||
|
border-bottom-color: #333;
|
||||||
transform: none;
|
transform: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
@@ -783,11 +782,11 @@ body {
|
|||||||
.compact-row {
|
.compact-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.5rem;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
justify-content: space-between;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.compact-row.clickable {
|
.compact-row.clickable {
|
||||||
@@ -808,7 +807,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.compact-text {
|
.compact-text {
|
||||||
flex: 1 1 0;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
@@ -816,7 +815,6 @@ body {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
max-width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bookmark-date-compact {
|
.bookmark-date-compact {
|
||||||
@@ -837,10 +835,9 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 26px;
|
width: 24px;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-left: auto;
|
|
||||||
transition: color 0.2s ease;
|
transition: color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1208,12 +1205,22 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.individual-bookmark {
|
.individual-bookmark {
|
||||||
background: #f5f5f5;
|
background: transparent;
|
||||||
border-color: #ddd;
|
border-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.individual-bookmark:hover {
|
.individual-bookmark:hover {
|
||||||
border-color: #646cff;
|
background: #f5f5f5;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.individual-bookmark.compact {
|
||||||
|
border-bottom-color: #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.individual-bookmark.compact:hover {
|
||||||
|
background: #fafafa;
|
||||||
|
border-bottom-color: #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.individual-bookmarks h4 {
|
.individual-bookmarks h4 {
|
||||||
@@ -1279,7 +1286,6 @@ body {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding-right: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlights-container.collapsed {
|
.highlights-container.collapsed {
|
||||||
@@ -1570,7 +1576,7 @@ body {
|
|||||||
|
|
||||||
.highlight-relay-indicator {
|
.highlight-relay-indicator {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -4px;
|
bottom: -2px;
|
||||||
left: 0;
|
left: 0;
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
color: #888;
|
color: #888;
|
||||||
@@ -1635,22 +1641,33 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
font-size: 0.875rem;
|
font-size: 0.8rem;
|
||||||
color: #888;
|
color: #888;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
|
min-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlight-author {
|
.highlight-author {
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 150px;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlight-meta-separator {
|
.highlight-meta-separator {
|
||||||
color: #666;
|
color: #666;
|
||||||
|
flex-shrink: 0;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlight-time {
|
.highlight-time {
|
||||||
color: #888;
|
color: #888;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlight-source {
|
.highlight-source {
|
||||||
@@ -1660,6 +1677,9 @@ body {
|
|||||||
color: #646cff;
|
color: #646cff;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: color 0.2s ease;
|
transition: color 0.2s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlight-source:hover {
|
.highlight-source:hover {
|
||||||
@@ -2494,6 +2514,23 @@ body {
|
|||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.relay-status-indicator.connecting {
|
||||||
|
background: rgba(100, 108, 255, 0.15);
|
||||||
|
border: 1px solid rgba(100, 108, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.relay-status-indicator.connecting:hover {
|
||||||
|
background: rgba(100, 108, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.relay-status-indicator.connecting .relay-status-icon {
|
||||||
|
color: rgba(100, 108, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.relay-status-indicator.connecting .relay-status-title {
|
||||||
|
color: rgba(100, 108, 255, 1);
|
||||||
|
}
|
||||||
|
|
||||||
.relay-status-indicator:hover {
|
.relay-status-indicator:hover {
|
||||||
background: rgba(245, 158, 11, 1);
|
background: rgba(245, 158, 11, 1);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import { areAllRelaysLocal } from '../utils/helpers'
|
|||||||
import { markEventAsOfflineCreated } from './offlineSyncService'
|
import { markEventAsOfflineCreated } from './offlineSyncService'
|
||||||
|
|
||||||
// Boris pubkey for zap splits
|
// Boris pubkey for zap splits
|
||||||
const BORIS_PUBKEY = '6e468422dfb74a5738702a8823b9b28168fc6cfb119d613e49ca0ec5a0bbd0c3'
|
// npub19802see0gnk3vjlus0dnmfdagusqrtmsxpl5yfmkwn9uvnfnqylqduhr0x
|
||||||
|
const BORIS_PUBKEY = '29dea8672f44ed164bfc83db3da5bd472001af70307f42277674cbc64d33013e'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
getHighlightText,
|
getHighlightText,
|
||||||
@@ -75,9 +76,9 @@ export async function createHighlight(
|
|||||||
// Update the alt tag to identify Boris as the creator
|
// Update the alt tag to identify Boris as the creator
|
||||||
const altTagIndex = highlightEvent.tags.findIndex(tag => tag[0] === 'alt')
|
const altTagIndex = highlightEvent.tags.findIndex(tag => tag[0] === 'alt')
|
||||||
if (altTagIndex !== -1) {
|
if (altTagIndex !== -1) {
|
||||||
highlightEvent.tags[altTagIndex] = ['alt', 'Highlight created by Boris. readwithboris.com']
|
highlightEvent.tags[altTagIndex] = ['alt', 'Highlight created by Boris. read.withboris.com']
|
||||||
} else {
|
} else {
|
||||||
highlightEvent.tags.push(['alt', 'Highlight created by Boris. readwithboris.com'])
|
highlightEvent.tags.push(['alt', 'Highlight created by Boris. read.withboris.com'])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add p tag (author tag) for nostr-native content
|
// Add p tag (author tag) for nostr-native content
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { formatDistanceToNow } from 'date-fns'
|
import { formatDistanceToNow, differenceInSeconds, differenceInMinutes, differenceInHours, differenceInDays, differenceInMonths, differenceInYears } from 'date-fns'
|
||||||
import { ParsedContent, ParsedNode } from '../types/bookmarks'
|
import { ParsedContent, ParsedNode } from '../types/bookmarks'
|
||||||
import ResolvedMention from '../components/ResolvedMention'
|
import ResolvedMention from '../components/ResolvedMention'
|
||||||
// Note: ContentWithResolvedProfiles is imported by components directly to keep this file component-only for fast refresh
|
// Note: ContentWithResolvedProfiles is imported by components directly to keep this file component-only for fast refresh
|
||||||
@@ -9,6 +9,26 @@ export const formatDate = (timestamp: number) => {
|
|||||||
return formatDistanceToNow(date, { addSuffix: true })
|
return formatDistanceToNow(date, { addSuffix: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ultra-compact date format for tight spaces (e.g., compact view)
|
||||||
|
export const formatDateCompact = (timestamp: number) => {
|
||||||
|
const date = new Date(timestamp * 1000)
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
const seconds = differenceInSeconds(now, date)
|
||||||
|
const minutes = differenceInMinutes(now, date)
|
||||||
|
const hours = differenceInHours(now, date)
|
||||||
|
const days = differenceInDays(now, date)
|
||||||
|
const months = differenceInMonths(now, date)
|
||||||
|
const years = differenceInYears(now, date)
|
||||||
|
|
||||||
|
if (seconds < 60) return 'now'
|
||||||
|
if (minutes < 60) return `${minutes}m`
|
||||||
|
if (hours < 24) return `${hours}h`
|
||||||
|
if (days < 30) return `${days}d`
|
||||||
|
if (months < 12) return `${months}mo`
|
||||||
|
return `${years}y`
|
||||||
|
}
|
||||||
|
|
||||||
// Component to render content with resolved nprofile names
|
// Component to render content with resolved nprofile names
|
||||||
// Intentionally no exports except components and render helpers
|
// Intentionally no exports except components and render helpers
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user