Skip to content

Commit 9c5c93d

Browse files
agbaumclaude
andcommitted
Add per-feed article expiry with timer reset
Articles expire based on a per-feed bucket (6h, 18h, 3d, 7d, default 3d), measured from fetchedAt (time first stored). Expiry runs on every refresh and on feed import/add. - Add ExpiryBucket type, EXPIRY_DURATIONS, EXPIRY_LABELS to FeedsContext - Add fetchedAt to Article, expiryBucket to Feed - expireArticles() runs after every refresh and add operation - updateFeedExpiry() and resetArticleExpiry() added to context - FeedSettingsSheet: tap any feed row to set expiry bucket and remove feed - ArticleCard: long press resets the expiry timer for that article - Feed rows show current expiry setting inline Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 74d5581 commit 9c5c93d

8 files changed

Lines changed: 308 additions & 41 deletions

File tree

app.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"expo": {
33
"name": "reader",
44
"slug": "reader",
5-
"version": "1.0.4",
5+
"version": "1.0.5",
66
"orientation": "portrait",
77
"icon": "./assets/images/icon.png",
88
"scheme": "reader",
@@ -21,7 +21,7 @@
2121
"predictiveBackGestureEnabled": false,
2222
"usesCleartextTraffic": true,
2323
"package": "com.akpgreentree.reader",
24-
"versionCode": 8
24+
"versionCode": 9
2525
},
2626
"web": {
2727
"output": "static",

package-lock.json

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/toggle-package-id.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const appJson = JSON.parse(fs.readFileSync(appJsonPath, 'utf8'));
1515
const devValues = {
1616
android: {
1717
package: 'com.akpgreentree.reader.dev',
18-
versionCode: 8,
18+
versionCode: 9,
1919
},
2020
ios: {
2121
bundleIdentifier: 'com.akpgreentree.reader.dev',
@@ -25,7 +25,7 @@ const devValues = {
2525
const prodValues = {
2626
android: {
2727
package: 'com.akpgreentree.reader',
28-
versionCode: 8,
28+
versionCode: 9,
2929
},
3030
ios: {
3131
bundleIdentifier: 'com.akpgreentree.reader',

src/app/(tabs)/feeds.tsx

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { Feather } from "@expo/vector-icons";
22
import * as Haptics from "expo-haptics";
33
import React, { useCallback, useState } from "react";
44
import {
5-
Alert,
65
FlatList,
76
Platform,
87
Pressable,
@@ -13,34 +12,23 @@ import {
1312
import { useSafeAreaInsets } from "react-native-safe-area-context";
1413

1514
import { AddFeedSheet } from "@/components/AddFeedSheet";
15+
import { FeedSettingsSheet } from "@/components/FeedSettingsSheet";
1616
import Colors from "@/constants/colors";
17-
import { Feed, useFeeds } from "@/context/FeedsContext";
17+
import { EXPIRY_LABELS, Feed, useFeeds } from "@/context/FeedsContext";
1818

1919
function FeedRow({
2020
feed,
21-
onRemove,
2221
onRefresh,
22+
onOpenSettings,
2323
}: {
2424
feed: Feed;
25-
onRemove: (id: string) => void;
2625
onRefresh: (id: string) => void;
26+
onOpenSettings: (feed: Feed) => void;
2727
}) {
2828
const { articles } = useFeeds();
2929
const unread = articles.filter((a) => a.feedId === feed.id && !a.isRead).length;
3030
const total = articles.filter((a) => a.feedId === feed.id).length;
3131

32-
const handleLongPress = useCallback(() => {
33-
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
34-
Alert.alert(
35-
`Remove "${feed.title}"?`,
36-
"This will delete all articles from this feed.",
37-
[
38-
{ text: "Cancel", style: "cancel" },
39-
{ text: "Remove", style: "destructive", onPress: () => onRemove(feed.id) },
40-
]
41-
);
42-
}, [feed, onRemove]);
43-
4432
const handleRefresh = useCallback(() => {
4533
Haptics.selectionAsync();
4634
onRefresh(feed.id);
@@ -54,10 +42,11 @@ function FeedRow({
5442
}
5543
})();
5644

45+
const expiryLabel = EXPIRY_LABELS[feed.expiryBucket ?? "3d"];
46+
5747
return (
5848
<Pressable
59-
onLongPress={handleLongPress}
60-
delayLongPress={500}
49+
onPress={() => onOpenSettings(feed)}
6150
style={({ pressed }) => [styles.feedRow, pressed && { opacity: 0.8 }]}
6251
>
6352
<View style={styles.feedIcon}>
@@ -71,7 +60,7 @@ function FeedRow({
7160
{domain}
7261
</Text>
7362
<Text style={styles.feedCount}>
74-
{unread > 0 ? `${unread} unread` : "Up to date"} · {total} total
63+
{unread > 0 ? `${unread} unread` : "Up to date"} · {total} total · expires {expiryLabel}
7564
</Text>
7665
</View>
7766
<Pressable
@@ -86,17 +75,18 @@ function FeedRow({
8675
}
8776

8877
export default function FeedsScreen() {
89-
const { feeds, removeFeed, refreshFeed } = useFeeds();
78+
const { feeds, refreshFeed } = useFeeds();
9079
const insets = useSafeAreaInsets();
9180
const [showAdd, setShowAdd] = useState(false);
81+
const [settingsFeed, setSettingsFeed] = useState<Feed | null>(null);
9282

9383
const topPad = Platform.OS === "web" ? Math.max(insets.top, 67) : insets.top;
9484

9585
const renderItem = useCallback(
9686
({ item }: { item: Feed }) => (
97-
<FeedRow feed={item} onRemove={removeFeed} onRefresh={refreshFeed} />
87+
<FeedRow feed={item} onRefresh={refreshFeed} onOpenSettings={setSettingsFeed} />
9888
),
99-
[removeFeed, refreshFeed]
89+
[refreshFeed]
10090
);
10191

10292
const ListHeader = (
@@ -157,6 +147,7 @@ export default function FeedsScreen() {
157147
]}
158148
/>
159149
<AddFeedSheet visible={showAdd} onClose={() => setShowAdd(false)} />
150+
<FeedSettingsSheet feed={settingsFeed} onClose={() => setSettingsFeed(null)} />
160151
</View>
161152
);
162153
}

src/app/(tabs)/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ function RefreshingBar({ visible }: { visible: boolean }) {
5252
}
5353

5454
export default function TodayScreen() {
55-
const { articles, isRefreshing, refreshFeeds, markAsRead } = useFeeds();
55+
const { articles, isRefreshing, refreshFeeds, markAsRead, resetArticleExpiry } = useFeeds();
5656
const insets = useSafeAreaInsets();
5757
const [sidebarOpen, setSidebarOpen] = useState(false);
5858
const [feedsPanelOpen, setFeedsPanelOpen] = useState(false);
@@ -65,7 +65,7 @@ export default function TodayScreen() {
6565

6666
const renderItem = useCallback(
6767
({ item }: { item: Article }) => (
68-
<ArticleCard article={item} onMarkRead={markAsRead} showFeedName />
68+
<ArticleCard article={item} onMarkRead={markAsRead} onResetExpiry={resetArticleExpiry} showFeedName />
6969
),
7070
[markAsRead]
7171
);

src/components/ArticleCard.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Article } from "@/context/FeedsContext";
1111
interface ArticleCardProps {
1212
article: Article;
1313
onMarkRead: (id: string) => void;
14+
onResetExpiry?: (id: string) => void;
1415
showFeedName?: boolean;
1516
}
1617

@@ -31,6 +32,7 @@ function timeAgo(ts?: number): string {
3132
export function ArticleCard({
3233
article,
3334
onMarkRead,
35+
onResetExpiry,
3436
showFeedName = true,
3537
}: ArticleCardProps) {
3638
const handlePress = useCallback(() => {
@@ -41,9 +43,16 @@ export function ArticleCard({
4143
}
4244
}, [article, onMarkRead]);
4345

46+
const handleLongPress = useCallback(() => {
47+
if (!onResetExpiry) return;
48+
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
49+
onResetExpiry(article.id);
50+
}, [article.id, onResetExpiry]);
51+
4452
return (
4553
<Pressable
4654
onPress={handlePress}
55+
onLongPress={handleLongPress}
4756
style={({ pressed }) => [styles.card, pressed && styles.cardPressed]}
4857
>
4958
<View style={styles.content}>

0 commit comments

Comments
 (0)