fix(ui): remove blocking error screens, show progressive loading with skeletons

- Remove full-screen error messages in Explore and Me
- Show skeletons while loading if no data cached
- Display empty states with 'Pull to refresh!' message
- Allow users to pull-to-refresh to retry on errors
- Keep content visible as data streams in progressively
This commit is contained in:
Gigi
2025-10-15 09:34:46 +02:00
parent 5b2ee94062
commit f16c1720a6
2 changed files with 74 additions and 102 deletions

View File

@@ -316,9 +316,18 @@ const Explore: React.FC<ExploreProps> = ({ relayPool, eventStore, settings, acti
const renderTabContent = () => { const renderTabContent = () => {
switch (activeTab) { switch (activeTab) {
case 'writings': case 'writings':
if (showSkeletons) {
return (
<div className="explore-grid">
{Array.from({ length: 6 }).map((_, i) => (
<BlogPostSkeleton key={i} />
))}
</div>
)
}
return filteredBlogPosts.length === 0 ? ( return filteredBlogPosts.length === 0 ? (
<div className="explore-empty" style={{ gridColumn: '1/-1', textAlign: 'center', color: 'var(--text-secondary)' }}> <div className="explore-empty" style={{ gridColumn: '1/-1', textAlign: 'center', color: 'var(--text-secondary)', padding: '2rem' }}>
<p>No blog posts found yet.</p> <p>No blog posts yet. Pull to refresh!</p>
</div> </div>
) : ( ) : (
<div className="explore-grid"> <div className="explore-grid">
@@ -333,9 +342,18 @@ const Explore: React.FC<ExploreProps> = ({ relayPool, eventStore, settings, acti
) )
case 'highlights': case 'highlights':
if (showSkeletons) {
return (
<div className="explore-grid">
{Array.from({ length: 8 }).map((_, i) => (
<HighlightSkeleton key={i} />
))}
</div>
)
}
return classifiedHighlights.length === 0 ? ( return classifiedHighlights.length === 0 ? (
<div className="explore-empty" style={{ gridColumn: '1/-1', textAlign: 'center', color: 'var(--text-secondary)' }}> <div className="explore-empty" style={{ gridColumn: '1/-1', textAlign: 'center', color: 'var(--text-secondary)', padding: '2rem' }}>
<p>No highlights yet. Your friends should start highlighting content!</p> <p>No highlights yet. Pull to refresh!</p>
</div> </div>
) : ( ) : (
<div className="explore-grid"> <div className="explore-grid">
@@ -355,43 +373,9 @@ const Explore: React.FC<ExploreProps> = ({ relayPool, eventStore, settings, acti
} }
} }
// Only show full loading screen if we don't have any data yet // Show content progressively - no blocking error screens
const hasData = highlights.length > 0 || blogPosts.length > 0 const hasData = highlights.length > 0 || blogPosts.length > 0
const showSkeletons = loading && !hasData
if (loading && !hasData) {
return (
<div className="explore-container" aria-busy="true">
<div className="explore-header">
<h1>
<FontAwesomeIcon icon={faNewspaper} />
Explore
</h1>
</div>
<div className="explore-grid">
{activeTab === 'writings' ? (
Array.from({ length: 6 }).map((_, i) => (
<BlogPostSkeleton key={i} />
))
) : (
Array.from({ length: 8 }).map((_, i) => (
<HighlightSkeleton key={i} />
))
)}
</div>
</div>
)
}
if (error) {
return (
<div className="explore-container">
<div className="explore-error">
<FontAwesomeIcon icon={faExclamationCircle} size="2x" />
<p>{error}</p>
</div>
</div>
)
}
return ( return (
<div className="explore-container"> <div className="explore-container">

View File

@@ -195,56 +195,28 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: pr
.filter(hasContentOrUrl) .filter(hasContentOrUrl)
.sort((a, b) => ((b.added_at || 0) - (a.added_at || 0)) || ((b.created_at || 0) - (a.created_at || 0))) .sort((a, b) => ((b.added_at || 0) - (a.added_at || 0)) || ((b.created_at || 0) - (a.created_at || 0)))
// Only show full loading screen if we don't have any data yet // Show content progressively - no blocking error screens
const hasData = highlights.length > 0 || bookmarks.length > 0 || readArticles.length > 0 || writings.length > 0 const hasData = highlights.length > 0 || bookmarks.length > 0 || readArticles.length > 0 || writings.length > 0
const showSkeletons = loading && !hasData
if (loading && !hasData) {
return (
<div className="explore-container" aria-busy="true">
{viewingPubkey && (
<div className="explore-header">
<AuthorCard authorPubkey={viewingPubkey} />
</div>
)}
<div className="explore-grid">
{activeTab === 'writings' ? (
Array.from({ length: 6 }).map((_, i) => (
<BlogPostSkeleton key={i} />
))
) : activeTab === 'highlights' ? (
Array.from({ length: 8 }).map((_, i) => (
<HighlightSkeleton key={i} />
))
) : (
Array.from({ length: 6 }).map((_, i) => (
<BookmarkSkeleton key={i} viewMode={viewMode} />
))
)}
</div>
</div>
)
}
if (error) {
return (
<div className="explore-container">
<div className="explore-error">
<FontAwesomeIcon icon={faExclamationCircle} size="2x" />
<p>{error}</p>
</div>
</div>
)
}
const renderTabContent = () => { const renderTabContent = () => {
switch (activeTab) { switch (activeTab) {
case 'highlights': case 'highlights':
if (showSkeletons) {
return (
<div className="explore-grid">
{Array.from({ length: 8 }).map((_, i) => (
<HighlightSkeleton key={i} />
))}
</div>
)
}
return highlights.length === 0 ? ( return highlights.length === 0 ? (
<div className="explore-empty"> <div className="explore-empty" style={{ padding: '2rem', textAlign: 'center', color: 'var(--text-secondary)' }}>
<p> <p>
{isOwnProfile {isOwnProfile
? 'No highlights yet. Start highlighting content to see them here!' ? 'No highlights yet. Pull to refresh!'
: 'No highlights yet. You should shame them on nostr!'} : 'No highlights yet. Pull to refresh!'}
</p> </p>
</div> </div>
) : ( ) : (
@@ -261,9 +233,20 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: pr
) )
case 'reading-list': case 'reading-list':
if (showSkeletons) {
return (
<div className="bookmarks-list">
<div className={`bookmarks-grid bookmarks-${viewMode}`}>
{Array.from({ length: 6 }).map((_, i) => (
<BookmarkSkeleton key={i} viewMode={viewMode} />
))}
</div>
</div>
)
}
return allIndividualBookmarks.length === 0 ? ( return allIndividualBookmarks.length === 0 ? (
<div className="explore-empty"> <div className="explore-empty" style={{ padding: '2rem', textAlign: 'center', color: 'var(--text-secondary)' }}>
<p>No bookmarks yet. Bookmark articles to see them here!</p> <p>No bookmarks yet. Pull to refresh!</p>
</div> </div>
) : ( ) : (
<div className="bookmarks-list"> <div className="bookmarks-list">
@@ -312,9 +295,18 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: pr
) )
case 'archive': case 'archive':
if (showSkeletons) {
return (
<div className="explore-grid">
{Array.from({ length: 6 }).map((_, i) => (
<BlogPostSkeleton key={i} />
))}
</div>
)
}
return readArticles.length === 0 ? ( return readArticles.length === 0 ? (
<div className="explore-empty"> <div className="explore-empty" style={{ padding: '2rem', textAlign: 'center', color: 'var(--text-secondary)' }}>
<p>No read articles yet. Mark articles as read to see them here!</p> <p>No read articles yet. Pull to refresh!</p>
</div> </div>
) : ( ) : (
<div className="explore-grid"> <div className="explore-grid">
@@ -329,25 +321,21 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: pr
) )
case 'writings': case 'writings':
if (showSkeletons) {
return (
<div className="explore-grid">
{Array.from({ length: 6 }).map((_, i) => (
<BlogPostSkeleton key={i} />
))}
</div>
)
}
return writings.length === 0 ? ( return writings.length === 0 ? (
<div className="explore-empty"> <div className="explore-empty" style={{ padding: '2rem', textAlign: 'center', color: 'var(--text-secondary)' }}>
<p> <p>
{isOwnProfile {isOwnProfile
? 'No articles written yet. Publish your first article to see it here!' ? 'No articles written yet. Pull to refresh!'
: ( : 'No articles written yet. Pull to refresh!'}
<>
No articles written. You can find other stuff from this user using{' '}
<a
href={viewingPubkey ? getProfileUrl(nip19.npubEncode(viewingPubkey)) : '#'}
target="_blank"
rel="noopener noreferrer"
style={{ color: 'rgb(99 102 241)', textDecoration: 'underline' }}
>
ants
</a>
.
</>
)}
</p> </p>
</div> </div>
) : ( ) : (