mirror of
https://github.com/dergigi/boris.git
synced 2025-12-18 07:04:19 +01:00
feat: implement dynamic browser title based on content
- Add useDocumentTitle hook to manage document title dynamically - Update useArticleLoader to set title when articles load - Update useExternalUrlLoader to set title for external URLs/videos - Update useEventLoader to set title for events - Reset title to default when navigating away from content - Browser title now shows article/video title instead of always 'Boris'
This commit is contained in:
@@ -14,6 +14,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 { useEventLoader } from '../hooks/useEventLoader'
|
import { useEventLoader } from '../hooks/useEventLoader'
|
||||||
|
import { useDocumentTitle } from '../hooks/useDocumentTitle'
|
||||||
import { Bookmark } from '../types/bookmarks'
|
import { Bookmark } from '../types/bookmarks'
|
||||||
import ThreePaneLayout from './ThreePaneLayout'
|
import ThreePaneLayout from './ThreePaneLayout'
|
||||||
import Explore from './Explore'
|
import Explore from './Explore'
|
||||||
@@ -58,6 +59,12 @@ const Bookmarks: React.FC<BookmarksProps> = ({
|
|||||||
const showSupport = location.pathname === '/support'
|
const showSupport = location.pathname === '/support'
|
||||||
const eventId = eventIdParam
|
const eventId = eventIdParam
|
||||||
|
|
||||||
|
// Manage document title based on current route
|
||||||
|
const isViewingContent = !!(naddr || externalUrl || eventId)
|
||||||
|
useDocumentTitle({
|
||||||
|
title: isViewingContent ? undefined : 'Boris - Read, Highlight, Explore'
|
||||||
|
})
|
||||||
|
|
||||||
// Extract tab from explore routes
|
// Extract tab from explore routes
|
||||||
const exploreTab = location.pathname === '/explore/writings' ? 'writings' : 'highlights'
|
const exploreTab = location.pathname === '/explore/writings' ? 'writings' : 'highlights'
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, Dispatch, SetStateAction } from 'react'
|
import { useEffect, useRef, useState, Dispatch, SetStateAction } from 'react'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import { RelayPool } from 'applesauce-relay'
|
import { RelayPool } from 'applesauce-relay'
|
||||||
import type { IEventStore } from 'applesauce-core'
|
import type { IEventStore } from 'applesauce-core'
|
||||||
@@ -12,6 +12,7 @@ import { ReadableContent } from '../services/readerService'
|
|||||||
import { Highlight } from '../types/highlights'
|
import { Highlight } from '../types/highlights'
|
||||||
import { NostrEvent } from 'nostr-tools'
|
import { NostrEvent } from 'nostr-tools'
|
||||||
import { UserSettings } from '../services/settingsService'
|
import { UserSettings } from '../services/settingsService'
|
||||||
|
import { useDocumentTitle } from './useDocumentTitle'
|
||||||
|
|
||||||
interface PreviewData {
|
interface PreviewData {
|
||||||
title: string
|
title: string
|
||||||
@@ -64,6 +65,10 @@ export function useArticleLoader({
|
|||||||
// Extract preview data from navigation state (from blog post cards)
|
// Extract preview data from navigation state (from blog post cards)
|
||||||
const previewData = (location.state as { previewData?: PreviewData })?.previewData
|
const previewData = (location.state as { previewData?: PreviewData })?.previewData
|
||||||
|
|
||||||
|
// Track the current article title for document title
|
||||||
|
const [currentTitle, setCurrentTitle] = useState<string | undefined>()
|
||||||
|
useDocumentTitle({ title: currentTitle })
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mountedRef.current = true
|
mountedRef.current = true
|
||||||
|
|
||||||
@@ -82,6 +87,7 @@ export function useArticleLoader({
|
|||||||
|
|
||||||
// If we have preview data from navigation, show it immediately (no skeleton!)
|
// If we have preview data from navigation, show it immediately (no skeleton!)
|
||||||
if (previewData) {
|
if (previewData) {
|
||||||
|
setCurrentTitle(previewData.title)
|
||||||
setReaderContent({
|
setReaderContent({
|
||||||
title: previewData.title,
|
title: previewData.title,
|
||||||
markdown: '', // Will be loaded from store or relay
|
markdown: '', // Will be loaded from store or relay
|
||||||
@@ -121,6 +127,7 @@ export function useArticleLoader({
|
|||||||
latestEvent = storedEvent as NostrEvent
|
latestEvent = storedEvent as NostrEvent
|
||||||
firstEmitted = true
|
firstEmitted = true
|
||||||
const title = Helpers.getArticleTitle(storedEvent) || 'Untitled Article'
|
const title = Helpers.getArticleTitle(storedEvent) || 'Untitled Article'
|
||||||
|
setCurrentTitle(title)
|
||||||
const image = Helpers.getArticleImage(storedEvent)
|
const image = Helpers.getArticleImage(storedEvent)
|
||||||
const summary = Helpers.getArticleSummary(storedEvent)
|
const summary = Helpers.getArticleSummary(storedEvent)
|
||||||
const published = Helpers.getArticlePublished(storedEvent)
|
const published = Helpers.getArticlePublished(storedEvent)
|
||||||
@@ -167,6 +174,7 @@ export function useArticleLoader({
|
|||||||
if (!firstEmitted) {
|
if (!firstEmitted) {
|
||||||
firstEmitted = true
|
firstEmitted = true
|
||||||
const title = Helpers.getArticleTitle(evt) || 'Untitled Article'
|
const title = Helpers.getArticleTitle(evt) || 'Untitled Article'
|
||||||
|
setCurrentTitle(title)
|
||||||
const image = Helpers.getArticleImage(evt)
|
const image = Helpers.getArticleImage(evt)
|
||||||
const summary = Helpers.getArticleSummary(evt)
|
const summary = Helpers.getArticleSummary(evt)
|
||||||
const published = Helpers.getArticlePublished(evt)
|
const published = Helpers.getArticlePublished(evt)
|
||||||
@@ -194,6 +202,7 @@ export function useArticleLoader({
|
|||||||
const finalEvent = (events.sort((a, b) => b.created_at - a.created_at)[0]) || latestEvent
|
const finalEvent = (events.sort((a, b) => b.created_at - a.created_at)[0]) || latestEvent
|
||||||
if (finalEvent) {
|
if (finalEvent) {
|
||||||
const title = Helpers.getArticleTitle(finalEvent) || 'Untitled Article'
|
const title = Helpers.getArticleTitle(finalEvent) || 'Untitled Article'
|
||||||
|
setCurrentTitle(title)
|
||||||
const image = Helpers.getArticleImage(finalEvent)
|
const image = Helpers.getArticleImage(finalEvent)
|
||||||
const summary = Helpers.getArticleSummary(finalEvent)
|
const summary = Helpers.getArticleSummary(finalEvent)
|
||||||
const published = Helpers.getArticlePublished(finalEvent)
|
const published = Helpers.getArticlePublished(finalEvent)
|
||||||
@@ -215,6 +224,7 @@ export function useArticleLoader({
|
|||||||
// As a last resort, fall back to the legacy helper (which includes cache)
|
// As a last resort, fall back to the legacy helper (which includes cache)
|
||||||
const article = await fetchArticleByNaddr(relayPool, naddr, false, settingsRef.current)
|
const article = await fetchArticleByNaddr(relayPool, naddr, false, settingsRef.current)
|
||||||
if (!mountedRef.current || currentRequestIdRef.current !== requestId) return
|
if (!mountedRef.current || currentRequestIdRef.current !== requestId) return
|
||||||
|
setCurrentTitle(article.title)
|
||||||
setReaderContent({
|
setReaderContent({
|
||||||
title: article.title,
|
title: article.title,
|
||||||
markdown: article.markdown,
|
markdown: article.markdown,
|
||||||
|
|||||||
35
src/hooks/useDocumentTitle.ts
Normal file
35
src/hooks/useDocumentTitle.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
const DEFAULT_TITLE = 'Boris - Read, Highlight, Explore'
|
||||||
|
|
||||||
|
interface UseDocumentTitleProps {
|
||||||
|
title?: string
|
||||||
|
fallback?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDocumentTitle({ title, fallback }: UseDocumentTitleProps) {
|
||||||
|
const originalTitleRef = useRef<string>(document.title)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Store the original title on first mount
|
||||||
|
if (originalTitleRef.current === DEFAULT_TITLE) {
|
||||||
|
originalTitleRef.current = document.title
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the new title if provided, otherwise use fallback or default
|
||||||
|
const newTitle = title || fallback || DEFAULT_TITLE
|
||||||
|
document.title = newTitle
|
||||||
|
|
||||||
|
// Cleanup: restore original title when component unmounts
|
||||||
|
return () => {
|
||||||
|
document.title = originalTitleRef.current
|
||||||
|
}
|
||||||
|
}, [title, fallback])
|
||||||
|
|
||||||
|
// Return a function to manually reset to default
|
||||||
|
const resetTitle = () => {
|
||||||
|
document.title = DEFAULT_TITLE
|
||||||
|
}
|
||||||
|
|
||||||
|
return { resetTitle }
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import { useEffect, useCallback } from 'react'
|
import { useEffect, useCallback, useState } from 'react'
|
||||||
import { RelayPool } from 'applesauce-relay'
|
import { RelayPool } from 'applesauce-relay'
|
||||||
import { IEventStore } from 'applesauce-core'
|
import { IEventStore } from 'applesauce-core'
|
||||||
import { NostrEvent } from 'nostr-tools'
|
import { NostrEvent } from 'nostr-tools'
|
||||||
import { ReadableContent } from '../services/readerService'
|
import { ReadableContent } from '../services/readerService'
|
||||||
import { eventManager } from '../services/eventManager'
|
import { eventManager } from '../services/eventManager'
|
||||||
import { fetchProfiles } from '../services/profileService'
|
import { fetchProfiles } from '../services/profileService'
|
||||||
|
import { useDocumentTitle } from './useDocumentTitle'
|
||||||
|
|
||||||
interface UseEventLoaderProps {
|
interface UseEventLoaderProps {
|
||||||
eventId?: string
|
eventId?: string
|
||||||
@@ -25,6 +26,9 @@ export function useEventLoader({
|
|||||||
setReaderLoading,
|
setReaderLoading,
|
||||||
setIsCollapsed
|
setIsCollapsed
|
||||||
}: UseEventLoaderProps) {
|
}: UseEventLoaderProps) {
|
||||||
|
// Track the current event title for document title
|
||||||
|
const [currentTitle, setCurrentTitle] = useState<string | undefined>()
|
||||||
|
useDocumentTitle({ title: currentTitle })
|
||||||
const displayEvent = useCallback((event: NostrEvent) => {
|
const displayEvent = useCallback((event: NostrEvent) => {
|
||||||
// Escape HTML in content and convert newlines to breaks for plain text display
|
// Escape HTML in content and convert newlines to breaks for plain text display
|
||||||
const escapedContent = event.content
|
const escapedContent = event.content
|
||||||
@@ -46,6 +50,7 @@ export function useEventLoader({
|
|||||||
title,
|
title,
|
||||||
published: event.created_at
|
published: event.created_at
|
||||||
}
|
}
|
||||||
|
setCurrentTitle(title)
|
||||||
setReaderContent(baseContent)
|
setReaderContent(baseContent)
|
||||||
|
|
||||||
// Background: resolve author profile for kind:1 and update title
|
// Background: resolve author profile for kind:1 and update title
|
||||||
@@ -80,7 +85,9 @@ export function useEventLoader({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (resolved) {
|
if (resolved) {
|
||||||
setReaderContent({ ...baseContent, title: `Note by @${resolved}` })
|
const updatedTitle = `Note by @${resolved}`
|
||||||
|
setCurrentTitle(updatedTitle)
|
||||||
|
setReaderContent({ ...baseContent, title: updatedTitle })
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// ignore profile failures; keep fallback title
|
// ignore profile failures; keep fallback title
|
||||||
@@ -119,6 +126,7 @@ export function useEventLoader({
|
|||||||
html: `<div style="padding: 1rem; color: var(--color-error, red);">Failed to load event: ${err instanceof Error ? err.message : 'Unknown error'}</div>`,
|
html: `<div style="padding: 1rem; color: var(--color-error, red);">Failed to load event: ${err instanceof Error ? err.message : 'Unknown error'}</div>`,
|
||||||
title: 'Error'
|
title: 'Error'
|
||||||
}
|
}
|
||||||
|
setCurrentTitle('Error')
|
||||||
setReaderContent(errorContent)
|
setReaderContent(errorContent)
|
||||||
setReaderLoading(false)
|
setReaderLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useMemo } from 'react'
|
import { useEffect, useRef, useMemo, useState } from 'react'
|
||||||
import { RelayPool } from 'applesauce-relay'
|
import { RelayPool } from 'applesauce-relay'
|
||||||
import { IEventStore } from 'applesauce-core'
|
import { IEventStore } from 'applesauce-core'
|
||||||
import { fetchReadableContent, ReadableContent } from '../services/readerService'
|
import { fetchReadableContent, ReadableContent } from '../services/readerService'
|
||||||
@@ -7,6 +7,7 @@ import { Highlight } from '../types/highlights'
|
|||||||
import { useStoreTimeline } from './useStoreTimeline'
|
import { useStoreTimeline } from './useStoreTimeline'
|
||||||
import { eventToHighlight } from '../services/highlightEventProcessor'
|
import { eventToHighlight } from '../services/highlightEventProcessor'
|
||||||
import { KINDS } from '../config/kinds'
|
import { KINDS } from '../config/kinds'
|
||||||
|
import { useDocumentTitle } from './useDocumentTitle'
|
||||||
|
|
||||||
// Helper to extract filename from URL
|
// Helper to extract filename from URL
|
||||||
function getFilenameFromUrl(url: string): string {
|
function getFilenameFromUrl(url: string): string {
|
||||||
@@ -52,6 +53,10 @@ export function useExternalUrlLoader({
|
|||||||
// Track in-flight request to prevent stale updates when switching quickly
|
// Track in-flight request to prevent stale updates when switching quickly
|
||||||
const currentRequestIdRef = useRef(0)
|
const currentRequestIdRef = useRef(0)
|
||||||
|
|
||||||
|
// Track the current content title for document title
|
||||||
|
const [currentTitle, setCurrentTitle] = useState<string | undefined>()
|
||||||
|
useDocumentTitle({ title: currentTitle })
|
||||||
|
|
||||||
// Load cached URL-specific highlights from event store
|
// Load cached URL-specific highlights from event store
|
||||||
const urlFilter = useMemo(() => {
|
const urlFilter = useMemo(() => {
|
||||||
if (!url) return null
|
if (!url) return null
|
||||||
@@ -88,6 +93,7 @@ export function useExternalUrlLoader({
|
|||||||
if (!mountedRef.current) return
|
if (!mountedRef.current) return
|
||||||
if (currentRequestIdRef.current !== requestId) return
|
if (currentRequestIdRef.current !== requestId) return
|
||||||
|
|
||||||
|
setCurrentTitle(content.title)
|
||||||
setReaderContent(content)
|
setReaderContent(content)
|
||||||
setReaderLoading(false)
|
setReaderLoading(false)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user