Repository navigation
Add Read Later feature - #113
Conversation
Co-authored-by: WCY-dt <55525165+WCY-dt@users.noreply.github.com>
Co-authored-by: WCY-dt <55525165+WCY-dt@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR implements a comprehensive "Read Later" feature for MrRSS, mirroring the existing Favorites functionality. The feature allows users to mark articles for reading later, with automatic state management where adding an article to read later marks it as unread, and reading an article removes it from the read later list.
Key Changes:
- Added
is_read_latercolumn to the database with appropriate indexing and migration support - Implemented backend API endpoints and business logic for read later operations
- Created frontend UI components, keyboard shortcuts (l, 4), and visual indicators (blue highlight, clock icon)
- Integrated read later into rules engine and filter system
- Added full internationalization support (English and Chinese)
Reviewed changes
Copilot reviewed 32 out of 32 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| main.go | Added /api/articles/toggle-read-later endpoint registration |
| internal/rules/engine.go | Added is_read_later condition and read_later/remove_read_later actions to rules engine |
| internal/models/models.go | Added IsReadLater field to Article model |
| internal/handlers/article/article_filters.go | Added is_read_later filter condition support |
| internal/handlers/article/article_basic.go | Implemented HandleToggleReadLater HTTP handler |
| internal/database/db.go | Added database migration for is_read_later column and created index before schema init |
| internal/database/cleanup_db.go | Updated cleanup logic to preserve read later articles alongside favorites |
| internal/database/article_db.go | Implemented ToggleReadLater, SetArticleReadLater, and updated MarkArticleRead to auto-clear read later status (with critical bug) |
| frontend/src/types/models.ts | Added is_read_later field to Article interface and RuleAction types |
| frontend/src/stores/app.ts | Added readLater to Filter type |
| frontend/src/i18n/types.ts | Added read later translation keys |
| frontend/src/i18n/locales/zh.ts | Added Chinese translations for read later feature |
| frontend/src/i18n/locales/en.ts | Added English translations for read later feature |
| frontend/src/composables/ui/useKeyboardShortcuts.ts | Added keyboard shortcuts (l, 4) and toggleCurrentArticleReadLater function (with error handling bug) |
| frontend/src/composables/rules/useRuleOptions.ts | Added read later to rule field and action options |
| frontend/src/composables/filter/useFilterFields.ts | Added is_read_later to filter fields |
| frontend/src/composables/article/useArticleDetail.ts | Added toggleReadLater function with optimistic updates (with error handling bug) |
| frontend/src/composables/article/useArticleActions.ts | Added read later context menu action (with error handling bug) |
| frontend/src/components/sidebar/SidebarNavItem.vue | Added PhClockCountdown icon import and readLater icon type |
| frontend/src/components/sidebar/Sidebar.vue | Added "Read Later" navigation item to sidebar |
| frontend/src/components/modals/settings/shortcuts/ShortcutsTab.vue | Added read later keyboard shortcuts to settings UI |
| frontend/src/components/common/ContextMenu.vue | Added ph-clock-countdown icon to icon map |
| frontend/src/components/article/ArticleToolbar.vue | Added read later toggle button to article toolbar |
| frontend/src/components/article/ArticleItem.vue | Added blue highlight styling and clock icon indicator for read later articles |
| frontend/src/components/article/ArticleDetailToolbar.vue | Added read later toggle button to detail toolbar |
| frontend/src/components/article/ArticleDetail.vue | Wired up read later toggle to detail view |
| README_zh.md | Updated build instructions to use Makefile or -skipbindings flag |
| README.md | Updated build instructions to use Makefile or -skipbindings flag |
| Makefile | Added -skipbindings flag to wails build command |
| CONTRIBUTING.md | Updated build instructions to use Makefile or -skipbindings flag |
| AGENTS.md | Updated build instructions and documented -skipbindings requirement |
| .github/copilot-instructions.md | Added build command reference with -skipbindings flag |
| isRead := 0 | ||
| if read { | ||
| isRead = 1 | ||
| // When marking as read, also remove from read later | ||
| _, err := db.Exec("UPDATE articles SET is_read = 1, is_read_later = 0 WHERE id = ?", id) | ||
| return err | ||
| } | ||
| _, err := db.Exec("UPDATE articles SET is_read = ? WHERE id = ?", isRead, id) |
There was a problem hiding this comment.
The MarkArticleRead function has a logic flaw. When read is false (marking as unread), the code still uses isRead = 0 but then updates with the parameterized isRead variable. This is redundant. More importantly, the code path for marking as read (lines 134-138) returns early, so the code on lines 140-141 is only executed when marking as unread. This means the unread path doesn't go through, making it unreachable.
Suggested fix:
func (db *DB) MarkArticleRead(id int64, read bool) error {
db.WaitForReady()
if read {
// When marking as read, also remove from read later
_, err := db.Exec("UPDATE articles SET is_read = 1, is_read_later = 0 WHERE id = ?", id)
return err
}
// When marking as unread, only update is_read
_, err := db.Exec("UPDATE articles SET is_read = 0 WHERE id = ?", id)
return err
}| isRead := 0 | |
| if read { | |
| isRead = 1 | |
| // When marking as read, also remove from read later | |
| _, err := db.Exec("UPDATE articles SET is_read = 1, is_read_later = 0 WHERE id = ?", id) | |
| return err | |
| } | |
| _, err := db.Exec("UPDATE articles SET is_read = ? WHERE id = ?", isRead, id) | |
| if read { | |
| // When marking as read, also remove from read later | |
| _, err := db.Exec("UPDATE articles SET is_read = 1, is_read_later = 0 WHERE id = ?", id) | |
| return err | |
| } | |
| // When marking as unread, only update is_read | |
| _, err := db.Exec("UPDATE articles SET is_read = 0 WHERE id = ?", id) |
| function toggleCurrentArticleReadLater(): void { | ||
| const article = store.articles.find((a) => a.id === store.currentArticleId); | ||
| if (!article) return; | ||
|
|
||
| const newState = !article.is_read_later; | ||
| article.is_read_later = newState; | ||
| // When adding to read later, also mark as unread | ||
| if (newState) { | ||
| article.is_read = false; | ||
| } | ||
| fetch(`/api/articles/toggle-read-later?id=${article.id}`, { method: 'POST' }) | ||
| .then(() => store.fetchUnreadCounts()) | ||
| .catch((e) => { | ||
| console.error('Error toggling read later:', e); | ||
| article.is_read_later = !newState; | ||
| }); | ||
| } |
There was a problem hiding this comment.
When an error occurs after optimistically updating both is_read_later and is_read, only is_read_later is reverted (line 155). If newState was true, we also set is_read = false (line 149), but this change isn't reverted on error, leaving the UI in an inconsistent state.
Suggested fix:
function toggleCurrentArticleReadLater(): void {
const article = store.articles.find((a) => a.id === store.currentArticleId);
if (!article) return;
const newState = !article.is_read_later;
const oldReadState = article.is_read; // Save the old read state
article.is_read_later = newState;
// When adding to read later, also mark as unread
if (newState) {
article.is_read = false;
}
fetch(`/api/articles/toggle-read-later?id=${article.id}`, { method: 'POST' })
.then(() => store.fetchUnreadCounts())
.catch((e) => {
console.error('Error toggling read later:', e);
article.is_read_later = !newState;
article.is_read = oldReadState; // Restore the old read state
});
}| async function toggleReadLater() { | ||
| if (!article.value) return; | ||
| const newState = !article.value.is_read_later; | ||
| article.value.is_read_later = newState; | ||
| // When adding to read later, also mark as unread | ||
| if (newState) { | ||
| article.value.is_read = false; | ||
| } | ||
| try { | ||
| await fetch(`/api/articles/toggle-read-later?id=${article.value.id}`, { method: 'POST' }); | ||
| store.fetchUnreadCounts(); | ||
| } catch (e) { | ||
| console.error('Error toggling read later:', e); | ||
| // Revert on error | ||
| article.value.is_read_later = !newState; | ||
| } | ||
| } |
There was a problem hiding this comment.
The error handling in toggleReadLater has the same issue as in useKeyboardShortcuts.ts. When an error occurs after optimistically updating both is_read_later and is_read, only is_read_later is reverted (line 117). If newState was true, we also set is_read = false (line 109), but this change isn't reverted on error.
Suggested fix:
async function toggleReadLater() {
if (!article.value) return;
const newState = !article.value.is_read_later;
const oldReadState = article.value.is_read; // Save the old read state
article.value.is_read_later = newState;
// When adding to read later, also mark as unread
if (newState) {
article.value.is_read = false;
}
try {
await fetch(`/api/articles/toggle-read-later?id=${article.value.id}`, { method: 'POST' });
store.fetchUnreadCounts();
} catch (e) {
console.error('Error toggling read later:', e);
// Revert on error
article.value.is_read_later = !newState;
article.value.is_read = oldReadState; // Restore the old read state
}
}| } else if (action === 'toggleReadLater') { | ||
| const newState = !article.is_read_later; | ||
| article.is_read_later = newState; | ||
| // When adding to read later, also mark as unread | ||
| if (newState) { | ||
| article.is_read = false; | ||
| } | ||
| try { | ||
| await fetch(`/api/articles/toggle-read-later?id=${article.id}`, { method: 'POST' }); | ||
| // Update unread counts after toggling read later status | ||
| if (onReadStatusChange) { | ||
| onReadStatusChange(); | ||
| } | ||
| } catch (e) { | ||
| console.error('Error toggling read later:', e); | ||
| // Revert the state change on error | ||
| article.is_read_later = !newState; | ||
| window.showToast(t('errorSavingSettings'), 'error'); | ||
| } |
There was a problem hiding this comment.
The error handling in the toggleReadLater action has the same issue. When an error occurs after optimistically updating both is_read_later and is_read, only is_read_later is reverted (line 122). If newState was true, we also set is_read = false (line 111), but this change isn't reverted on error.
Suggested fix:
} else if (action === 'toggleReadLater') {
const newState = !article.is_read_later;
const oldReadState = article.is_read; // Save the old read state
article.is_read_later = newState;
// When adding to read later, also mark as unread
if (newState) {
article.is_read = false;
}
try {
await fetch(`/api/articles/toggle-read-later?id=${article.id}`, { method: 'POST' });
// Update unread counts after toggling read later status
if (onReadStatusChange) {
onReadStatusChange();
}
} catch (e) {
console.error('Error toggling read later:', e);
// Revert the state change on error
article.is_read_later = !newState;
article.is_read = oldReadState; // Restore the old read state
window.showToast(t('errorSavingSettings'), 'error');
}
}
Description
Implements "Read Later" (稍后阅读) functionality, mirroring the existing Favorites feature. Articles marked for read later are automatically set to unread; reading them removes them from the list.
Type of Change
Related Issues
Fixes #113
Changes Made
Backend (Go):
is_read_latercolumn with index and composite index for query optimizationToggleReadLaterandSetArticleReadLateroperationsGetArticlessupportsreadLaterfilter/api/articles/toggle-read-laterendpointMarkArticleReadto auto-remove from read lateris_read_latercondition +read_later/remove_read_lateractionsFrontend (Vue.js):
PhClockCountdowniconl(toggle),4(navigate to view)is_read_laterfield optionScreenshots
Before
N/A - new feature
After
N/A - desktop application
Testing
go test ./...)npm run test)Test Configuration
Test Steps
lkey4keyChecklist
Additional Notes
Auto-behavior summary:
Breaking Changes
None. Database migration handles existing installations automatically.
Warning
Firewall rules blocked me from connecting to one or more addresses (expand for details)
I tried to connect to the following addresses, but was blocked by firewall rules:
nonexistent-feed-url-12345.com/tmp/go-build2096446897/b293/discovery.test /tmp/go-build2096446897/b293/discovery.test -test.testlogfile=/tmp/go-build2096446897/b293/testlog.txt -test.paniconexit0 -test.timeout=10m0s(dns block)If you need me to access, download, or install something from one of these locations, you can either:
Original prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.