mirror of
https://github.com/dergigi/boris.git
synced 2025-12-18 15:14:20 +01:00
feat: hide articles from bot accounts by name; add setting (default on)
This commit is contained in:
@@ -12,12 +12,19 @@ interface BlogPostCardProps {
|
|||||||
href: string
|
href: string
|
||||||
level?: 'mine' | 'friends' | 'nostrverse'
|
level?: 'mine' | 'friends' | 'nostrverse'
|
||||||
readingProgress?: number // 0-1 reading progress (optional)
|
readingProgress?: number // 0-1 reading progress (optional)
|
||||||
|
hideBotByName?: boolean // default true
|
||||||
}
|
}
|
||||||
|
|
||||||
const BlogPostCard: React.FC<BlogPostCardProps> = ({ post, href, level, readingProgress }) => {
|
const BlogPostCard: React.FC<BlogPostCardProps> = ({ post, href, level, readingProgress, hideBotByName = true }) => {
|
||||||
const profile = useEventModel(Models.ProfileModel, [post.author])
|
const profile = useEventModel(Models.ProfileModel, [post.author])
|
||||||
const displayName = profile?.name || profile?.display_name ||
|
const displayName = profile?.name || profile?.display_name ||
|
||||||
`${post.author.slice(0, 8)}...${post.author.slice(-4)}`
|
`${post.author.slice(0, 8)}...${post.author.slice(-4)}`
|
||||||
|
const rawName = (profile?.name || profile?.display_name || '').toLowerCase()
|
||||||
|
|
||||||
|
// Hide bot authors by name/display_name
|
||||||
|
if (hideBotByName && rawName.includes('bot')) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const publishedDate = post.published || post.event.created_at
|
const publishedDate = post.published || post.event.created_at
|
||||||
const formattedDate = formatDistance(new Date(publishedDate * 1000), new Date(), {
|
const formattedDate = formatDistance(new Date(publishedDate * 1000), new Date(), {
|
||||||
|
|||||||
@@ -263,44 +263,44 @@ const Explore: React.FC<ExploreProps> = ({ relayPool, eventStore, settings, acti
|
|||||||
? fetchNostrverseBlogPosts(relayPool, relayUrls, 50, eventStore || undefined).catch(() => [])
|
? fetchNostrverseBlogPosts(relayPool, relayUrls, 50, eventStore || undefined).catch(() => [])
|
||||||
: Promise.resolve([])
|
: Promise.resolve([])
|
||||||
|
|
||||||
// Fire non-blocking fetches and merge as they resolve
|
// Fire non-blocking fetches and merge as they resolve
|
||||||
fetchBlogPostsFromAuthors(relayPool, contactsArray, relayUrls)
|
fetchBlogPostsFromAuthors(relayPool, contactsArray, relayUrls)
|
||||||
.then((friendsPosts) => {
|
.then((friendsPosts) => {
|
||||||
setBlogPosts(prev => {
|
setBlogPosts(prev => {
|
||||||
const merged = dedupeWritingsByReplaceable([...prev, ...friendsPosts])
|
const merged = dedupeWritingsByReplaceable([...prev, ...friendsPosts])
|
||||||
const sorted = merged.sort((a, b) => (b.published || b.event.created_at) - (a.published || a.event.created_at))
|
const sorted = merged.sort((a, b) => (b.published || b.event.created_at) - (a.published || a.event.created_at))
|
||||||
if (activeAccount) setCachedPosts(activeAccount.pubkey, sorted)
|
if (activeAccount) setCachedPosts(activeAccount.pubkey, sorted)
|
||||||
// Pre-cache profiles in background
|
// Pre-cache profiles in background
|
||||||
const authorPubkeys = Array.from(new Set(sorted.map(p => p.author)))
|
const authorPubkeys = Array.from(new Set(sorted.map(p => p.author)))
|
||||||
fetchProfiles(relayPool, eventStore, authorPubkeys, settings).catch(() => {})
|
fetchProfiles(relayPool, eventStore, authorPubkeys, settings).catch(() => {})
|
||||||
return sorted
|
return sorted
|
||||||
})
|
})
|
||||||
|
}).catch(() => {})
|
||||||
|
|
||||||
|
fetchHighlightsFromAuthors(relayPool, contactsArray)
|
||||||
|
.then((friendsHighlights) => {
|
||||||
|
setHighlights(prev => {
|
||||||
|
const merged = dedupeHighlightsById([...prev, ...friendsHighlights])
|
||||||
|
const sorted = merged.sort((a, b) => b.created_at - a.created_at)
|
||||||
|
if (activeAccount) setCachedHighlights(activeAccount.pubkey, sorted)
|
||||||
|
return sorted
|
||||||
|
})
|
||||||
|
}).catch(() => {})
|
||||||
|
|
||||||
|
nostrversePostsPromise.then((nostrversePosts) => {
|
||||||
|
setBlogPosts(prev => dedupeWritingsByReplaceable([...prev, ...nostrversePosts]).sort((a, b) => (b.published || b.event.created_at) - (a.published || a.event.created_at)))
|
||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
|
|
||||||
fetchHighlightsFromAuthors(relayPool, contactsArray)
|
fetchNostrverseHighlights(relayPool, 100, eventStore || undefined)
|
||||||
.then((friendsHighlights) => {
|
.then((nostriverseHighlights) => {
|
||||||
setHighlights(prev => {
|
setHighlights(prev => dedupeHighlightsById([...prev, ...nostriverseHighlights]).sort((a, b) => b.created_at - a.created_at))
|
||||||
const merged = dedupeHighlightsById([...prev, ...friendsHighlights])
|
}).catch(() => {})
|
||||||
const sorted = merged.sort((a, b) => b.created_at - a.created_at)
|
} catch (err) {
|
||||||
if (activeAccount) setCachedHighlights(activeAccount.pubkey, sorted)
|
console.error('Failed to load data:', err)
|
||||||
return sorted
|
// No blocking error - user can pull-to-refresh
|
||||||
})
|
} finally {
|
||||||
}).catch(() => {})
|
// loading is already turned off after seeding
|
||||||
|
}
|
||||||
nostrversePostsPromise.then((nostrversePosts) => {
|
|
||||||
setBlogPosts(prev => dedupeWritingsByReplaceable([...prev, ...nostrversePosts]).sort((a, b) => (b.published || b.event.created_at) - (a.published || a.event.created_at)))
|
|
||||||
}).catch(() => {})
|
|
||||||
|
|
||||||
fetchNostrverseHighlights(relayPool, 100, eventStore || undefined)
|
|
||||||
.then((nostriverseHighlights) => {
|
|
||||||
setHighlights(prev => dedupeHighlightsById([...prev, ...nostriverseHighlights]).sort((a, b) => b.created_at - a.created_at))
|
|
||||||
}).catch(() => {})
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to load data:', err)
|
|
||||||
// No blocking error - user can pull-to-refresh
|
|
||||||
} finally {
|
|
||||||
// loading is already turned off after seeding
|
|
||||||
}
|
|
||||||
}, [relayPool, activeAccount, eventStore, settings, visibility.nostrverse, followedPubkeys])
|
}, [relayPool, activeAccount, eventStore, settings, visibility.nostrverse, followedPubkeys])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -431,6 +431,12 @@ const Explore: React.FC<ExploreProps> = ({ relayPool, eventStore, settings, acti
|
|||||||
const publishedTime = post.published || post.event.created_at
|
const publishedTime = post.published || post.event.created_at
|
||||||
if (publishedTime > maxFutureTime) return false
|
if (publishedTime > maxFutureTime) return false
|
||||||
|
|
||||||
|
// Hide bot authors by profile display name if setting enabled
|
||||||
|
if (settings?.hideBotArticlesByName !== false) {
|
||||||
|
// Profile resolution and filtering is handled in BlogPostCard via ProfileModel
|
||||||
|
// Keep list intact here; individual cards will render null if author is a bot
|
||||||
|
}
|
||||||
|
|
||||||
// Apply visibility filters
|
// Apply visibility filters
|
||||||
const isMine = activeAccount && post.author === activeAccount.pubkey
|
const isMine = activeAccount && post.author === activeAccount.pubkey
|
||||||
const isFriend = followedPubkeys.has(post.author)
|
const isFriend = followedPubkeys.has(post.author)
|
||||||
@@ -498,6 +504,7 @@ const Explore: React.FC<ExploreProps> = ({ relayPool, eventStore, settings, acti
|
|||||||
href={getPostUrl(post)}
|
href={getPostUrl(post)}
|
||||||
level={post.level}
|
level={post.level}
|
||||||
readingProgress={getReadingProgress(post)}
|
readingProgress={getReadingProgress(post)}
|
||||||
|
hideBotByName={settings?.hideBotArticlesByName !== false}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -832,6 +832,7 @@ const Me: React.FC<MeProps> = ({
|
|||||||
post={post}
|
post={post}
|
||||||
href={getPostUrl(post)}
|
href={getPostUrl(post)}
|
||||||
readingProgress={getWritingReadingProgress(post)}
|
readingProgress={getWritingReadingProgress(post)}
|
||||||
|
hideBotByName={settings.hideBotArticlesByName !== false}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -51,6 +51,20 @@ const ExploreSettings: React.FC<ExploreSettingsProps> = ({ settings, onUpdate })
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="setting-group">
|
||||||
|
<label htmlFor="hideBotArticlesByName" className="checkbox-label">
|
||||||
|
<input
|
||||||
|
id="hideBotArticlesByName"
|
||||||
|
type="checkbox"
|
||||||
|
checked={settings.hideBotArticlesByName !== false}
|
||||||
|
onChange={(e) => onUpdate({ hideBotArticlesByName: e.target.checked })}
|
||||||
|
className="setting-checkbox"
|
||||||
|
/>
|
||||||
|
<span>Hide articles from accounts whose name contains "bot"</span>
|
||||||
|
</label>
|
||||||
|
<div className="setting-hint">Examples: Unlocks Bot, Step Counter Bot, Bitcoin Magazine News Bot</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ interface UseSettingsParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useSettings({ relayPool, eventStore, pubkey, accountManager }: UseSettingsParams) {
|
export function useSettings({ relayPool, eventStore, pubkey, accountManager }: UseSettingsParams) {
|
||||||
const [settings, setSettings] = useState<UserSettings>({ renderVideoLinksAsEmbeds: true })
|
const [settings, setSettings] = useState<UserSettings>({ renderVideoLinksAsEmbeds: true, hideBotArticlesByName: true })
|
||||||
const [toastMessage, setToastMessage] = useState<string | null>(null)
|
const [toastMessage, setToastMessage] = useState<string | null>(null)
|
||||||
const [toastType, setToastType] = useState<'success' | 'error'>('success')
|
const [toastType, setToastType] = useState<'success' | 'error'>('success')
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ export function useSettings({ relayPool, eventStore, pubkey, accountManager }: U
|
|||||||
const loadAndWatch = async () => {
|
const loadAndWatch = async () => {
|
||||||
try {
|
try {
|
||||||
const loadedSettings = await loadSettings(relayPool, eventStore, pubkey, RELAYS)
|
const loadedSettings = await loadSettings(relayPool, eventStore, pubkey, RELAYS)
|
||||||
if (loadedSettings) setSettings({ renderVideoLinksAsEmbeds: true, ...loadedSettings })
|
if (loadedSettings) setSettings({ renderVideoLinksAsEmbeds: true, hideBotArticlesByName: true, ...loadedSettings })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load settings:', err)
|
console.error('Failed to load settings:', err)
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ export function useSettings({ relayPool, eventStore, pubkey, accountManager }: U
|
|||||||
loadAndWatch()
|
loadAndWatch()
|
||||||
|
|
||||||
const subscription = watchSettings(eventStore, pubkey, (loadedSettings) => {
|
const subscription = watchSettings(eventStore, pubkey, (loadedSettings) => {
|
||||||
if (loadedSettings) setSettings({ renderVideoLinksAsEmbeds: true, ...loadedSettings })
|
if (loadedSettings) setSettings({ renderVideoLinksAsEmbeds: true, hideBotArticlesByName: true, ...loadedSettings })
|
||||||
})
|
})
|
||||||
|
|
||||||
return () => subscription.unsubscribe()
|
return () => subscription.unsubscribe()
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ export interface UserSettings {
|
|||||||
autoMarkAsReadOnCompletion?: boolean // default: false (opt-in)
|
autoMarkAsReadOnCompletion?: boolean // default: false (opt-in)
|
||||||
// Bookmark filtering
|
// Bookmark filtering
|
||||||
hideBookmarksWithoutCreationDate?: boolean // default: false
|
hideBookmarksWithoutCreationDate?: boolean // default: false
|
||||||
|
// Content filtering
|
||||||
|
hideBotArticlesByName?: boolean // default: true - hide authors whose profile name includes "bot"
|
||||||
// TTS language selection
|
// TTS language selection
|
||||||
ttsUseSystemLanguage?: boolean // default: false
|
ttsUseSystemLanguage?: boolean // default: false
|
||||||
ttsDetectContentLanguage?: boolean // default: true
|
ttsDetectContentLanguage?: boolean // default: true
|
||||||
|
|||||||
Reference in New Issue
Block a user