mirror of
https://github.com/dergigi/boris.git
synced 2026-02-17 13:04:59 +01:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27178bc3d1 | ||
|
|
76fefc88ca | ||
|
|
98c006939b | ||
|
|
80ed646dd4 | ||
|
|
7ea868d0b2 | ||
|
|
88e1bc3419 | ||
|
|
4ec34a0379 | ||
|
|
aec2dcb75c | ||
|
|
5bdc435f5d | ||
|
|
db46edd39e | ||
|
|
c9739f804d | ||
|
|
eeb44e344f | ||
|
|
a6674610b8 | ||
|
|
6ae3decafb | ||
|
|
00da638e81 | ||
|
|
f04c0a401e | ||
|
|
f5e9f164f5 | ||
|
|
589ac17114 | ||
|
|
8d3510947c | ||
|
|
08a8f5623a | ||
|
|
e85ccdc7da | ||
|
|
d0e7f146fb | ||
|
|
efdb33eb31 | ||
|
|
0abbe62515 | ||
|
|
ab0972dd29 |
106
CHANGELOG.md
106
CHANGELOG.md
@@ -7,6 +7,112 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.10.30] - 2025-11-01
|
||||
|
||||
### Added
|
||||
|
||||
- Navigate to author's writings page from article author card
|
||||
- Clicking the author card at the end of an article now navigates to `/p/${npub}/writings`
|
||||
- Previously navigated to the author's profile page
|
||||
- Better discovery of author's published articles
|
||||
|
||||
### Fixed
|
||||
|
||||
- Reset scroll to top when navigating to profile pages
|
||||
- Profile pages (including writings page) now scroll to top on navigation
|
||||
- Prevents landing in the middle of the page when navigating from articles
|
||||
- Better user experience when browsing author profiles
|
||||
- Preserve image aspect ratio when full-width images setting is enabled
|
||||
- Images maintain their aspect ratio when full-width setting is active
|
||||
- Prevents image distortion when enlarging small images
|
||||
|
||||
## [0.10.29] - 2025-11-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- Full-width images setting now uses width instead of max-width
|
||||
- Change from --image-max-width CSS variable to --image-width
|
||||
- When enabled, sets images to width: 100% (enlarging small images)
|
||||
- Always constrains with max-width: 100% to prevent overflow
|
||||
- Update mobile responsive styles to respect the setting
|
||||
|
||||
## [0.10.28] - 2025-11-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- Nostr URI processing in markdown links
|
||||
- Prevent double-processing of markdown to avoid nested links
|
||||
- Add HTTP URL detection to prevent processing nostr URIs in URLs
|
||||
- Use parser-based approach to detect markdown link URLs
|
||||
- Prevent nostr URI replacement inside markdown link URLs
|
||||
- Remove unused variables in nostrUriResolver
|
||||
- Improved blog post rendering with proper link handling
|
||||
|
||||
### Removed
|
||||
|
||||
- Debug console.log statements from nostrUriResolver
|
||||
- Cleaned up debug logging used during troubleshooting
|
||||
- Cleaner console output in production
|
||||
|
||||
## [0.10.27] - 2025-10-31
|
||||
|
||||
### Added
|
||||
|
||||
- Refresh button to highlights sidebar header
|
||||
- Users can manually refresh highlights panel
|
||||
- Better control over highlights data updates
|
||||
- Image preloading in BlogPostCard for better caching
|
||||
- Images are preloaded when blog posts are displayed
|
||||
- Improved offline access to article images
|
||||
- Preload logged-in user profile image for offline access
|
||||
- User profile picture is cached for offline viewing
|
||||
- Better user experience when network is unavailable
|
||||
- Development Service Worker for testing image caching
|
||||
- Service Worker enabled in development mode
|
||||
- Improved testing capabilities for offline functionality
|
||||
|
||||
### Fixed
|
||||
|
||||
- Service Worker registration error handling
|
||||
- Better error handling for Service Worker registration failures
|
||||
- More robust development mode Service Worker support
|
||||
- Proper error handling for fetch requests in Service Worker
|
||||
- Article loading race conditions
|
||||
- Resolved race condition when loading articles from cache
|
||||
- Cache is checked synchronously before setting loading state
|
||||
- Articles are populated in cache from explore view
|
||||
- Image caching issues
|
||||
- Images are properly preloaded when loading articles from cache
|
||||
- Removed bulk image preloading to prevent resource exhaustion errors
|
||||
- Avoid redundant image preload when using preview data
|
||||
- Scroll position management
|
||||
- Scroll position is reset when switching articles
|
||||
- Save suppression added when resetting scroll position
|
||||
- Reader content is cleared immediately when article changes
|
||||
- React hook ordering issues
|
||||
- useEffect moved before early return in BlogPostCard
|
||||
- Prevents React hooks dependency violations
|
||||
- TypeScript and linting issues
|
||||
- Cache save logic simplified to avoid TypeScript errors
|
||||
- Unused settings parameter marked as intentionally unused
|
||||
- Articles are saved to localStorage cache after loading from relays
|
||||
- Cache is saved immediately when first event is received
|
||||
|
||||
### Performance
|
||||
|
||||
- Avoid redundant image preload when using preview data
|
||||
- Prevents unnecessary image loading operations
|
||||
- Improved resource utilization
|
||||
|
||||
### Removed
|
||||
|
||||
- Debug console.log statements
|
||||
- Removed all debug console output from article cache and service worker
|
||||
- Removed debug logging from useImageCache hook
|
||||
- Cleaner console output in production
|
||||
- Unused refresh button from highlights panel header
|
||||
- Cleaned up unused UI component
|
||||
|
||||
## [0.10.26] - 2025-10-31
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "boris",
|
||||
"version": "0.10.27",
|
||||
"version": "0.10.31",
|
||||
"description": "A minimal nostr client for bookmark management",
|
||||
"homepage": "https://read.withboris.com/",
|
||||
"type": "module",
|
||||
|
||||
@@ -27,7 +27,7 @@ const AuthorCard: React.FC<AuthorCardProps> = ({ authorPubkey, clickable = true
|
||||
const handleClick = () => {
|
||||
if (clickable) {
|
||||
const npub = nip19.npubEncode(authorPubkey)
|
||||
navigate(`/p/${npub}`)
|
||||
navigate(`/p/${npub}/writings`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -247,10 +247,19 @@ export const BookmarkList: React.FC<BookmarkListProps> = ({
|
||||
/>
|
||||
|
||||
{allIndividualBookmarks.length > 0 && (
|
||||
<BookmarkFilters
|
||||
selectedFilter={selectedFilter}
|
||||
onFilterChange={setSelectedFilter}
|
||||
/>
|
||||
<div className="bookmark-filters-wrapper">
|
||||
<BookmarkFilters
|
||||
selectedFilter={selectedFilter}
|
||||
onFilterChange={setSelectedFilter}
|
||||
/>
|
||||
<CompactButton
|
||||
icon={faPlus}
|
||||
onClick={() => setShowAddModal(true)}
|
||||
title="Add web bookmark"
|
||||
ariaLabel="Add web bookmark"
|
||||
className="bookmark-section-action"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!activeAccount ? (
|
||||
@@ -287,15 +296,6 @@ export const BookmarkList: React.FC<BookmarkListProps> = ({
|
||||
<div key={section.key} className="bookmarks-section">
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<h3 className="bookmarks-section-title" style={{ margin: 0, padding: '1.5rem 0.5rem 0.375rem', flex: 1 }}>{section.title}</h3>
|
||||
{section.key === 'web' && activeAccount && (
|
||||
<CompactButton
|
||||
icon={faPlus}
|
||||
onClick={() => setShowAddModal(true)}
|
||||
title="Add web bookmark"
|
||||
ariaLabel="Add web bookmark"
|
||||
className="bookmark-section-action"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={`bookmarks-grid bookmarks-${viewMode}`}>
|
||||
{section.items.map((individualBookmark, index) => (
|
||||
|
||||
@@ -100,6 +100,17 @@ const Bookmarks: React.FC<BookmarksProps> = ({
|
||||
previousLocationRef.current = location.pathname
|
||||
}
|
||||
}, [location.pathname, showSettings, showMe, showExplore, showProfile])
|
||||
|
||||
// Reset scroll to top when navigating to profile routes
|
||||
useEffect(() => {
|
||||
if (showProfile) {
|
||||
// Reset scroll position when navigating to profile pages
|
||||
// Use requestAnimationFrame to ensure it happens after DOM updates
|
||||
requestAnimationFrame(() => {
|
||||
window.scrollTo({ top: 0, behavior: 'instant' })
|
||||
})
|
||||
}
|
||||
}, [location.pathname, showProfile])
|
||||
|
||||
const activeAccount = Hooks.useActiveAccount()
|
||||
const accountManager = Hooks.useAccountManager()
|
||||
|
||||
@@ -71,8 +71,9 @@ export function useSettings({ relayPool, eventStore, pubkey, accountManager }: U
|
||||
// Set paragraph alignment
|
||||
root.setProperty('--paragraph-alignment', settings.paragraphAlignment || 'justify')
|
||||
|
||||
// Set image max-width based on full-width setting
|
||||
root.setProperty('--image-max-width', settings.fullWidthImages ? 'none' : '100%')
|
||||
// Set image width and max-height based on full-width setting
|
||||
root.setProperty('--image-width', settings.fullWidthImages ? '100%' : 'auto')
|
||||
root.setProperty('--image-max-height', settings.fullWidthImages ? 'none' : '70vh')
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -57,13 +57,14 @@
|
||||
.reader .reader-markdown h1, .reader .reader-markdown h2, .reader .reader-markdown h3, .reader .reader-markdown h4, .reader .reader-markdown h5, .reader .reader-markdown h6 { text-align: left !important; }
|
||||
/* Tame images from external content */
|
||||
.reader .reader-html img, .reader .reader-markdown img {
|
||||
max-width: var(--image-max-width, 100%);
|
||||
max-height: 70vh;
|
||||
width: var(--image-width, auto);
|
||||
max-width: 100%;
|
||||
max-height: var(--image-max-height, 70vh);
|
||||
height: auto;
|
||||
width: auto;
|
||||
display: block;
|
||||
margin: 0.75rem auto;
|
||||
border-radius: 6px;
|
||||
object-fit: contain;
|
||||
}
|
||||
/* Headlines with Tailwind typography */
|
||||
.reader-markdown h1, .reader-html h1 {
|
||||
@@ -192,9 +193,11 @@
|
||||
}
|
||||
|
||||
.reader-markdown img, .reader-html img {
|
||||
width: var(--image-width, auto) !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
max-height: var(--image-max-height, 70vh) !important;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -267,6 +267,42 @@
|
||||
.read-inline-btn:hover { background: rgb(22 163 74); /* green-600 */ }
|
||||
|
||||
/* Bookmark filters */
|
||||
.bookmark-filters-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 1rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
.bookmark-filters-wrapper > .bookmark-filters {
|
||||
padding: 0;
|
||||
border-bottom: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.bookmark-filters-wrapper > .compact-button {
|
||||
background: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
border: none;
|
||||
padding: 0.375rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.875rem;
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.bookmark-filters-wrapper > .compact-button:hover {
|
||||
color: var(--color-text);
|
||||
background: var(--color-bg-elevated);
|
||||
}
|
||||
|
||||
.bookmark-filters {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
|
||||
@@ -107,17 +107,123 @@ export function getNostrUriLabel(encoded: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process markdown to replace nostr URIs while skipping those inside markdown links
|
||||
* This prevents nested markdown link issues when nostr identifiers appear in URLs
|
||||
*/
|
||||
function replaceNostrUrisSafely(
|
||||
markdown: string,
|
||||
getReplacement: (encoded: string) => string
|
||||
): string {
|
||||
// Track positions where we're inside a markdown link URL
|
||||
// Use a parser approach to correctly handle URLs with brackets/parentheses
|
||||
const linkRanges: Array<{ start: number, end: number }> = []
|
||||
|
||||
// Find all markdown link URLs by looking for ]( pattern and tracking to matching )
|
||||
let i = 0
|
||||
while (i < markdown.length) {
|
||||
// Look for ]( pattern that starts a markdown link URL
|
||||
const urlStartMatch = markdown.indexOf('](', i)
|
||||
if (urlStartMatch === -1) break
|
||||
|
||||
const urlStart = urlStartMatch + 2 // Position after "]("
|
||||
|
||||
// Now find the matching closing parenthesis
|
||||
// We need to account for nested parentheses and escaped characters
|
||||
let pos = urlStart
|
||||
let depth = 1 // We're inside one set of parentheses
|
||||
let urlEnd = -1
|
||||
|
||||
while (pos < markdown.length && depth > 0) {
|
||||
const char = markdown[pos]
|
||||
const nextChar = pos + 1 < markdown.length ? markdown[pos + 1] : ''
|
||||
|
||||
// Check for escaped characters
|
||||
if (char === '\\' && nextChar) {
|
||||
pos += 2 // Skip escaped character
|
||||
continue
|
||||
}
|
||||
|
||||
if (char === '(') {
|
||||
depth++
|
||||
} else if (char === ')') {
|
||||
depth--
|
||||
if (depth === 0) {
|
||||
urlEnd = pos
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
pos++
|
||||
}
|
||||
|
||||
if (urlEnd !== -1) {
|
||||
linkRanges.push({
|
||||
start: urlStart,
|
||||
end: urlEnd
|
||||
})
|
||||
|
||||
i = urlEnd + 1
|
||||
} else {
|
||||
// No matching closing paren found, skip this one
|
||||
i = urlStart + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a position is inside any markdown link URL
|
||||
const isInsideLinkUrl = (pos: number): boolean => {
|
||||
return linkRanges.some(range => pos >= range.start && pos < range.end)
|
||||
}
|
||||
|
||||
// Replace nostr URIs, but skip those inside link URLs
|
||||
// Also check if nostr URI is part of any URL pattern (http/https URLs)
|
||||
// Callback params: (match, encoded, type, offset, string)
|
||||
const result = markdown.replace(NOSTR_URI_REGEX, (match, encoded, _type, offset, fullString) => {
|
||||
const matchEnd = offset + match.length
|
||||
|
||||
// Check if this match is inside a markdown link URL
|
||||
// Check both start and end positions to ensure we catch the whole match
|
||||
const startInside = isInsideLinkUrl(offset)
|
||||
const endInside = isInsideLinkUrl(matchEnd - 1) // Check end position
|
||||
|
||||
if (startInside || endInside) {
|
||||
// Don't replace - return original match
|
||||
return match
|
||||
}
|
||||
|
||||
// Also check if the nostr URI is part of an HTTP/HTTPS URL pattern
|
||||
// This catches cases where the source markdown has URLs like https://example.com/naddr1...
|
||||
// before they're formatted as markdown links
|
||||
const contextBefore = fullString.slice(Math.max(0, offset - 200), offset)
|
||||
const contextAfter = fullString.slice(matchEnd, Math.min(fullString.length, matchEnd + 10))
|
||||
|
||||
// Check if we're inside an http/https URL (looking for https?:// pattern before the match)
|
||||
// and the match is followed by valid URL characters (not whitespace or closing paren)
|
||||
const urlPatternBefore = /https?:\/\/[^\s)]*$/i
|
||||
const isInHttpUrl = urlPatternBefore.test(contextBefore)
|
||||
const isValidUrlContinuation = !contextAfter.match(/^[\s)]/) // Not followed by space or closing paren
|
||||
|
||||
if (isInHttpUrl && isValidUrlContinuation) {
|
||||
// Don't replace - return original match
|
||||
return match
|
||||
}
|
||||
|
||||
// encoded is already the NIP-19 identifier without nostr: prefix (from capture group)
|
||||
const replacement = getReplacement(encoded)
|
||||
return replacement
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace nostr: URIs in markdown with proper markdown links
|
||||
* This converts: nostr:npub1... to [label](link)
|
||||
*/
|
||||
export function replaceNostrUrisInMarkdown(markdown: string): string {
|
||||
return markdown.replace(NOSTR_URI_REGEX, (match) => {
|
||||
// Extract just the NIP-19 identifier (without nostr: prefix)
|
||||
const encoded = match.replace(/^nostr:/, '')
|
||||
return replaceNostrUrisSafely(markdown, (encoded) => {
|
||||
const link = createNostrLink(encoded)
|
||||
const label = getNostrUriLabel(encoded)
|
||||
|
||||
return `[${label}](${link})`
|
||||
})
|
||||
}
|
||||
@@ -132,9 +238,7 @@ export function replaceNostrUrisInMarkdownWithTitles(
|
||||
markdown: string,
|
||||
articleTitles: Map<string, string>
|
||||
): string {
|
||||
return markdown.replace(NOSTR_URI_REGEX, (match) => {
|
||||
// Extract just the NIP-19 identifier (without nostr: prefix)
|
||||
const encoded = match.replace(/^nostr:/, '')
|
||||
return replaceNostrUrisSafely(markdown, (encoded) => {
|
||||
const link = createNostrLink(encoded)
|
||||
|
||||
// For articles, use the resolved title if available
|
||||
|
||||
Reference in New Issue
Block a user