feat: hide articles from bot accounts by name; add setting (default on)

This commit is contained in:
Gigi
2025-10-21 07:36:00 +02:00
parent 67fec91ab3
commit a5d2ed8b07
6 changed files with 71 additions and 40 deletions

View File

@@ -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(), {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
) )
} }

View File

@@ -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()

View File

@@ -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