Skip to content

Commit 37859a5

Browse files
Copilotchiga0
andauthored
Fix PWA: startup flash, Add Feed shortcut no-op, and Share Target 404 (#24)
* Initial plan * fix: resolve PWA bugs - startup flash, Add Feed shortcut, and Share Target Co-authored-by: chiga0 <24784430+chiga0@users.noreply.github.com> * fix: repair flaky E2E tests - favorites regex, history loading, feeds heading Co-authored-by: chiga0 <24784430+chiga0@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: chiga0 <24784430+chiga0@users.noreply.github.com>
1 parent 16562dc commit 37859a5

7 files changed

Lines changed: 103 additions & 15 deletions

File tree

index.html

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,54 @@
7777
})();
7878
</script>
7979

80-
<!-- Critical CSS for loading screen and initial layout -->
80+
<!-- Critical CSS for loading screen - prevents transparent/white flash on PWA launch -->
81+
<style>
82+
#app-loading {
83+
position: fixed;
84+
inset: 0;
85+
z-index: 9999;
86+
display: flex;
87+
align-items: center;
88+
justify-content: center;
89+
background-color: #1f1f23;
90+
color: #e5e5e5;
91+
transition: opacity 0.3s ease;
92+
}
93+
html.light #app-loading {
94+
background-color: #ffffff;
95+
color: #1a1a1a;
96+
}
97+
#app-loading.hide {
98+
opacity: 0;
99+
pointer-events: none;
100+
}
101+
.loading-content {
102+
display: flex;
103+
flex-direction: column;
104+
align-items: center;
105+
gap: 16px;
106+
}
107+
.loading-spinner {
108+
width: 36px;
109+
height: 36px;
110+
border: 3px solid rgba(255, 255, 255, 0.15);
111+
border-top-color: #e5e5e5;
112+
border-radius: 50%;
113+
animation: app-spin 0.75s linear infinite;
114+
}
115+
html.light .loading-spinner {
116+
border-color: rgba(0, 0, 0, 0.1);
117+
border-top-color: #1a1a1a;
118+
}
119+
.loading-text {
120+
font-size: 14px;
121+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
122+
opacity: 0.6;
123+
}
124+
@keyframes app-spin {
125+
to { transform: rotate(360deg); }
126+
}
127+
</style>
81128
</head>
82129

83130
<body>

public/manifest.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,8 @@
9898
}
9999
],
100100
"share_target": {
101-
"action": "/share",
102-
"method": "POST",
103-
"enctype": "multipart/form-data",
101+
"action": "/",
102+
"method": "GET",
104103
"params": {
105104
"title": "title",
106105
"text": "text",

src/components/AddFeedDialog/AddFeedDialog.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Modal dialog for adding new RSS feed subscriptions
44
*/
55

6-
import { useState } from 'react';
6+
import { useState, useEffect } from 'react';
77
import { useTranslation } from 'react-i18next';
88
import { useStore } from '../../hooks/useStore';
99
import { useOfflineDetection } from '../../hooks/useOfflineDetection';
@@ -12,9 +12,10 @@ import { useToast } from '../../hooks/useToast';
1212
interface AddFeedDialogProps {
1313
isOpen: boolean;
1414
onClose: () => void;
15+
initialUrl?: string;
1516
}
1617

17-
export function AddFeedDialog({ isOpen, onClose }: AddFeedDialogProps) {
18+
export function AddFeedDialog({ isOpen, onClose, initialUrl }: AddFeedDialogProps) {
1819
const { t } = useTranslation('feed');
1920
const [url, setUrl] = useState('');
2021
const [categoryId, setCategoryId] = useState<string>('');
@@ -30,6 +31,13 @@ export function AddFeedDialog({ isOpen, onClose }: AddFeedDialogProps) {
3031
loadCategories();
3132
});
3233

34+
// Populate URL field when dialog opens with an initial URL (e.g. from share target)
35+
useEffect(() => {
36+
if (isOpen && initialUrl) {
37+
setUrl(initialUrl);
38+
}
39+
}, [isOpen, initialUrl]);
40+
3341
const handleSubmit = async (e: React.FormEvent) => {
3442
e.preventDefault();
3543
setError('');

src/pages/FeedsPage.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,29 @@ export function FeedsPage() {
3838
const [isRefreshing, setIsRefreshing] = useState(false);
3939
const [articleCounts, setArticleCounts] = useState<Record<string, { total: number; unread: number; starred: number }>>({});
4040
const [isBottomBarVisible, setIsBottomBarVisible] = useState(true);
41+
const [sharedFeedUrl, setSharedFeedUrl] = useState('');
4142
const lastScrollY = useRef(0);
4243

44+
// Handle PWA shortcuts (?action=add-feed) and Web Share Target (?url=... or ?text=...)
45+
useEffect(() => {
46+
const params = new URLSearchParams(window.location.search);
47+
const action = params.get('action');
48+
const sharedUrl = params.get('url') || params.get('text');
49+
50+
if (action === 'add-feed') {
51+
openAddFeedDialog();
52+
} else if (sharedUrl) {
53+
// If the param is plain text with an embedded URL, extract it
54+
let feedUrl = sharedUrl;
55+
if (!/^https?:\/\//i.test(feedUrl)) {
56+
const match = sharedUrl.match(/https?:\/\/[^\s]+/i);
57+
feedUrl = match ? match[0] : sharedUrl;
58+
}
59+
setSharedFeedUrl(feedUrl);
60+
openAddFeedDialog();
61+
}
62+
}, [openAddFeedDialog]);
63+
4364
// Load feeds, categories and start auto-refresh on mount
4465
useEffect(() => {
4566
let cancelled = false;
@@ -327,7 +348,11 @@ export function FeedsPage() {
327348
</div>
328349

329350
{/* Add Feed Dialog */}
330-
<AddFeedDialog isOpen={isAddFeedDialogOpen} onClose={closeAddFeedDialog} />
351+
<AddFeedDialog
352+
isOpen={isAddFeedDialogOpen}
353+
onClose={() => { closeAddFeedDialog(); setSharedFeedUrl(''); }}
354+
initialUrl={sharedFeedUrl}
355+
/>
331356
</div>
332357
);
333358
}

tests/e2e/ci/favorites.spec.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,21 @@ test.describe('Favorites Functionality', () => {
5858
await articleLink.click();
5959
await page.waitForLoadState('networkidle');
6060

61-
// Click the favorite button in the action bar
62-
const favoriteButton = page.locator('button').filter({ hasText: /Favorite||favorite/ }).first();
61+
// If the article is already favorited (from a previous test run), unfavorite it first
62+
// so we can reliably test the favorite→favorited transition
63+
const alreadyFavoritedBtn = page.locator('button').filter({ hasText: /^Favorited$|^$/ }).first();
64+
if (await alreadyFavoritedBtn.isVisible({ timeout: 3_000 }).catch(() => false)) {
65+
await alreadyFavoritedBtn.click();
66+
await expect(page.locator('button').filter({ hasText: /^Favorite$|^$/ }).first()).toBeVisible({ timeout: 10_000 });
67+
}
68+
69+
// Click the favorite button in the action bar (exact match to avoid matching "Favorited")
70+
const favoriteButton = page.locator('button').filter({ hasText: /^Favorite$|^$/ }).first();
6371
await expect(favoriteButton).toBeVisible({ timeout: 10_000 });
6472
await favoriteButton.click();
6573

6674
// Wait for the favorited state to appear (text changes to "Favorited" / "已收藏")
67-
await expect(page.locator('button').filter({ hasText: /Favorited||favorited/ }).first()).toBeVisible({ timeout: 5_000 });
75+
await expect(page.locator('button').filter({ hasText: /^Favorited$|^$/ }).first()).toBeVisible({ timeout: 10_000 });
6876

6977
// Navigate to favorites page
7078
await page.goto('/#/favorites');

tests/e2e/ci/feedSubscription.spec.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,11 +171,10 @@ test.describe('RSS Feed Subscription', () => {
171171
// Navigate back to feeds list
172172
const backToFeedsButton = page.locator('button').filter({ hasText: /Back to Feeds|/ }).first();
173173
await backToFeedsButton.click();
174-
await page.waitForLoadState('networkidle');
175174

176-
// Verify we're back at feeds list
177-
const mainHeading = page.locator('h1').first();
178-
await expect(mainHeading).toBeVisible({ timeout: 10_000 });
175+
// Verify we're back at feeds list (wait for h1 matching Feeds/订阅源 directly)
176+
const mainHeading = page.locator('h1').filter({ hasText: /Feeds|/ }).first();
177+
await expect(mainHeading).toBeVisible({ timeout: 15_000 });
179178
const text = await mainHeading.textContent();
180179
expect(text).toMatch(/Feeds|/);
181180
});

tests/e2e/ci/history.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ test.describe('Reading History', () => {
6464

6565
// Navigate to history page
6666
await page.goto('/#/history');
67+
// Wait for the loading spinner to disappear before checking content
68+
await page.waitForSelector('.animate-spin', { state: 'hidden', timeout: 10_000 }).catch(() => {});
6769
await page.waitForLoadState('networkidle');
6870

6971
// Verify history page shows the read article
@@ -74,7 +76,7 @@ test.describe('Reading History', () => {
7476

7577
// Should have at least one article in history (the one we just read)
7678
const historyArticles = page.locator('a[href*="/articles/"]');
77-
await expect(historyArticles.first()).toBeVisible({ timeout: 10_000 });
79+
await expect(historyArticles.first()).toBeVisible({ timeout: 15_000 });
7880
const count = await historyArticles.count();
7981
expect(count).toBeGreaterThan(0);
8082
});

0 commit comments

Comments
 (0)