Skip to content

v0.4.1: Add app icons, RSSHub integration & reorder features#2

Merged
devohmycode merged 1 commit intomasterfrom
0.4.1
Feb 22, 2026
Merged

v0.4.1: Add app icons, RSSHub integration & reorder features#2
devohmycode merged 1 commit intomasterfrom
0.4.1

Conversation

@devohmycode
Copy link
Copy Markdown
Owner

@devohmycode devohmycode commented Feb 22, 2026

Summary

  • New app icons: Updated icons across all platforms (Android, iOS, desktop, Windows Store)
  • RSSHub integration: Added RSSHub service for discovering RSS feeds from websites, with search and category browsing
  • Reorderable lists: Favorites and Read Later lists now support drag-and-drop reordering with persistent order
  • Expanding panel: New collapsible/expandable panel component for better UI organization
  • ElevenLabs TTS: Added debug logging for troubleshooting text-to-speech requests

Test plan

  • Verify new app icons display correctly on Android, iOS, and desktop
  • Test RSSHub feed discovery with various website URLs
  • Test drag-and-drop reordering in Favorites and Read Later lists
  • Verify reorder persistence across app restarts
  • Test expanding panel collapse/expand behavior
  • Verify TTS still works correctly with added logging

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • RSSHub integration with automatic detection and suggestions for supported websites
    • Configurable RSSHub instance settings
    • Feed renaming capability
    • Drag-and-drop reordering for favorites and read-later items
    • SuperFlux About information panel
    • Enhanced TTS error handling and messaging
  • Bug Fixes

    • Fixed ElevenLabs TTS compatibility by enforcing text length limits

Update app icons (desktop & Android adaptive launcher) and add RSSHub integration and UI improvements. Adds src-tauri Android adaptive icon XML and background color, plus many updated icon assets. Introduces a new ExpandingPanel component and a rsshubService with Settings and AddFeed UI to configure/detect an RSSHub instance. Implements drag-and-drop reordering for Favorites / Read Later in FeedPanel, wires reorder handlers in App, and exposes feed rename input in SourcePanel. Improves TTS error handling/UI in ReaderPanel and adds debug logging for ElevenLabs TTS calls in the Tauri backend.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 22, 2026

📝 Walkthrough

Walkthrough

This PR introduces RSSHub integration with route detection and instance management, implements drag-and-drop reordering for favorites and read-later items, enables feed renaming, enhances TTS with verbose logging and error handling including text truncation for ElevenLabs, and adds a new expanding panel UI component for modal interactions.

Changes

Cohort / File(s) Summary
RSSHub Service & Integration
src/services/rsshubService.ts, src/components/AddFeedModal.tsx, src/components/SettingsModal.tsx, src/components/SourcePanel.tsx
New RSSHub service module with instance management (localStorage-backed) and URL-to-route detection logic covering 18+ host patterns (GitHub, Twitter/X, Bilibili, etc.); AddFeedModal displays RSSHub suggestions when detected; SettingsModal exposes RSSHub instance URL configuration; SourcePanel renders RSSHub badges on qualifying feed items and opens an about panel.
Feed Management (Rename & Reorder)
src/hooks/useFeedStore.ts, src/App.tsx, src/components/FeedPanel.tsx
FeedStore adds renameFeed, getFavoritesOrder, getReadLaterOrder, reorderFavorites, and reorderReadLater methods with localStorage persistence; App.tsx applies two-stage ordering to favorites/read-later views and wires handleReorderItems callback to FeedPanel; FeedPanel implements drag-and-drop reordering UI with new state (canReorder, dragItemId, dropTargetId) and handlers (handleDragStart, handleDragEnd, handleDragOver, handleDrop).
Expanding Panel Component
src/components/ExpandingPanel.tsx, src/index.css
New ExpandingPanel component with animation phases (mounting, expanding, content-in, content-out, closing), computed clip-path circle transitions, keyboard Escape-to-close, overlay click-to-close, and ARIA accessibility (role="dialog", aria-modal, aria-label); extensive CSS added for overlay, panel structure, header/body/close button, and multi-phase transform/opacity transitions; styling for draggable feed cards, drag handles, drop targets, RSSHub badges, and brand name button interactions.
TTS Enhancement
src-tauri/src/lib.rs, src/services/ttsService.ts, src/components/ReaderPanel.tsx
Tauri lib.rs adds verbose runtime logging (voice_id, model, text length, HTTP status, response errors, audio byte length) around ElevenLabs TTS path; ttsService introduces ELEVENLABS_MAX_CHARS limit (~5000) and truncates input text with ellipsis when over limit; ReaderPanel adds ttsError state, clears on article change/TTS stop, captures error messages on TTS failure, displays error styling and warning glyph on TTS button.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Munching carrots while I code,
RSSHub routes find their abode,
Drag-and-drop in grand array,
Expanding panels light the day,
TTS sings, and errors fade away! 🎵

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes in the PR: app icons, RSSHub integration, and reorder features are all substantive additions reflected in the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 0.4.1

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Add RSSHub integration, drag-and-drop reordering, and ExpandingPanel component

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add RSSHub integration for automatic RSS feed detection from websites
• Implement drag-and-drop reordering for Favorites and Read Later lists
• Create ExpandingPanel component for About/Help modal with app info
• Add feed rename functionality and improve TTS error handling
• Enhance ElevenLabs TTS with character limit handling and debug logging
Diagram
flowchart LR
  A["User Input"] -->|detectRSSHubRoute| B["RSSHub Service"]
  B -->|rsshubMatch| C["AddFeedModal"]
  C -->|Suggestion| D["Feed Added"]
  E["Favorites/ReadLater"] -->|Drag-Drop| F["FeedPanel Reorder"]
  F -->|onReorderItems| G["Store Persistence"]
  H["About Button"] -->|Click| I["ExpandingPanel"]
  I -->|Display| J["App Info & Shortcuts"]
  K["TTS Request"] -->|Truncate| L["ElevenLabs Service"]
  L -->|Debug Logs| M["Tauri Backend"]
Loading

Grey Divider

File Changes

1. src/services/rsshubService.ts ✨ Enhancement +169/-0

New RSSHub service with route detection

src/services/rsshubService.ts


2. src/hooks/useFeedStore.ts ✨ Enhancement +39/-0

Add feed rename and reorder persistence

src/hooks/useFeedStore.ts


3. src/services/ttsService.ts ✨ Enhancement +10/-2

Improve TTS with character limit and error handling

src/services/ttsService.ts


View more (12)
4. src/index.css ✨ Enhancement +280/-0

Add styles for RSSHub, drag-drop, and ExpandingPanel

src/index.css


5. src-tauri/src/lib.rs ✨ Enhancement +9/-1

Add debug logging for ElevenLabs TTS requests

src-tauri/src/lib.rs


6. src/components/SourcePanel.tsx ✨ Enhancement +160/-24

Add feed rename input and About panel trigger

src/components/SourcePanel.tsx


7. src/components/FeedPanel.tsx ✨ Enhancement +110/-1

Implement drag-and-drop reordering for items

src/components/FeedPanel.tsx


8. src/components/ExpandingPanel.tsx ✨ Enhancement +164/-0

New animated expanding panel component

src/components/ExpandingPanel.tsx


9. src/components/ReaderPanel.tsx ✨ Enhancement +20/-7

Improve TTS error display and handling

src/components/ReaderPanel.tsx


10. src/App.tsx ✨ Enhancement +39/-2

Wire reorder handlers and feed rename actions

src/App.tsx


11. src/components/AddFeedModal.tsx ✨ Enhancement +20/-0

Add RSSHub suggestion display in feed input

src/components/AddFeedModal.tsx


12. src/components/SettingsModal.tsx ✨ Enhancement +27/-0

Add RSSHub instance URL configuration

src/components/SettingsModal.tsx


13. src-tauri/gen/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml ⚙️ Configuration changes +5/-0

Add Android adaptive icon configuration

src-tauri/gen/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml


14. src-tauri/gen/android/app/src/main/res/values/ic_launcher_background.xml ⚙️ Configuration changes +4/-0

Add Android launcher background color

src-tauri/gen/android/app/src/main/res/values/ic_launcher_background.xml


15. src-tauri/icons/icon.icns Additional files +0/-0

...

src-tauri/icons/icon.icns


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (6) 📘 Rule violations (3) 📎 Requirement gaps (0)

Grey Divider


Action required

1. UI shows raw ttsError 📘 Rule violation ⛨ Security
Description
The UI tooltip displays the raw ttsError string, which can expose internal/third-party error
details directly to end users. This can leak implementation details (e.g., HTTP status/error bodies)
and should be replaced with a generic user-facing message while keeping details in internal logs.
Code

src/components/ReaderPanel.tsx[R837-842]

+                className={`reader-tool-btn tts ${ttsStatus !== 'idle' ? 'active' : ''} ${ttsError ? 'error' : ''}`}
+                title={ttsError ? `Erreur: ${ttsError}` : ttsStatus === 'playing' ? 'Pause' : ttsStatus === 'paused' ? 'Reprendre' : 'Écouter'}
                onClick={handleTts}
              >
-                {ttsStatus === 'playing' ? '⏸' : '▶'}
+                {ttsError ? '⚠' : ttsStatus === 'playing' ? '⏸' : '▶'}
              </button>
Evidence
Secure error handling requires generic user-facing errors and restricting detailed errors to
internal logs (PR Compliance ID 4). The new tooltip uses ttsError directly in the UI, and
ttsError is set from the thrown error message, which can include detailed backend/provider error
text.

Rule 4: Generic: Secure Error Handling
src/components/ReaderPanel.tsx[837-842]
src/components/ReaderPanel.tsx[330-334]
src-tauri/src/lib.rs[496-499]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The UI currently displays raw TTS error details (`ttsError`) to end users (tooltip). This can expose internal/provider details and violates secure error handling.

## Issue Context
`ttsError` is set from `err.message` coming from the TTS invocation path, and backend errors can include HTTP status and response bodies.

## Fix Focus Areas
- src/components/ReaderPanel.tsx[330-335]
- src/components/ReaderPanel.tsx[837-842]
- src-tauri/src/lib.rs[496-499]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Reorder doesn’t re-render 🐞 Bug ✓ Correctness
Description
Saving the new Favorites/Read Later order only writes to localStorage; no React state is updated, so
the UI may not reflect a drag-and-drop reorder until some unrelated re-render occurs.
Code

src/hooks/useFeedStore.ts[R536-550]

+  const getFavoritesOrder = useCallback((): string[] => {
+    return loadFromStorage<string[]>(STORAGE_KEYS.FAVORITES_ORDER, []);
+  }, []);
+
+  const getReadLaterOrder = useCallback((): string[] => {
+    return loadFromStorage<string[]>(STORAGE_KEYS.READLATER_ORDER, []);
+  }, []);
+
+  const reorderFavorites = useCallback((orderedIds: string[]) => {
+    saveToStorage(STORAGE_KEYS.FAVORITES_ORDER, orderedIds);
+  }, []);
+
+  const reorderReadLater = useCallback((orderedIds: string[]) => {
+    saveToStorage(STORAGE_KEYS.READLATER_ORDER, orderedIds);
+  }, []);
Evidence
The reorder APIs only persist to localStorage and don’t update any state inside the store; App’s
reorder handler also doesn’t set any state. Since localStorage mutations do not trigger React
renders, the list order can remain unchanged immediately after drop.

src/hooks/useFeedStore.ts[534-550]
src/App.tsx[282-285]
src/components/FeedPanel.tsx[153-169]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Reordering only writes to localStorage, which doesn’t trigger React re-renders. After a drop, the UI may not update to reflect the new order.

## Issue Context
Favorites/Read Later ordering is read via `getFavoritesOrder/getReadLaterOrder()` but the reorder functions do not update any React state.

## Fix Focus Areas
- src/hooks/useFeedStore.ts[534-585]
- src/App.tsx[121-158]
- src/App.tsx[282-285]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Reorder index wrong 🐞 Bug ✓ Correctness
Description
The drag-and-drop reorder algorithm inserts at the original target index after removing the source
item, which shifts indices; dragging an item downward can place it one position off (often after the
intended target).
Code

src/components/FeedPanel.tsx[R153-166]

+  const handleDrop = useCallback((e: React.DragEvent, targetId: string) => {
+    e.preventDefault();
+    const sourceId = e.dataTransfer.getData('text/plain');
+    if (!sourceId || sourceId === targetId || !onReorderItems) return;
+
+    const ids = items.map(i => i.id);
+    const fromIdx = ids.indexOf(sourceId);
+    const toIdx = ids.indexOf(targetId);
+    if (fromIdx === -1 || toIdx === -1) return;
+
+    ids.splice(fromIdx, 1);
+    ids.splice(toIdx, 0, sourceId);
+    onReorderItems(ids);
+
Evidence
When fromIdx < toIdx, removing the source item shifts the target left by 1, but the code still
inserts at the pre-removal toIdx. This produces incorrect ordering for downward moves.

src/components/FeedPanel.tsx[153-166]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Downward drag-and-drop reorder uses an unadjusted target index after removing the source element, leading to off-by-one placement.

## Issue Context
`ids.splice(fromIdx, 1)` shifts indices; `toIdx` must be adjusted when `fromIdx &lt; toIdx`.

## Fix Focus Areas
- src/components/FeedPanel.tsx[153-169]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (2)
4. ExpandingPanel listener leak 🐞 Bug ⛯ Reliability
Description
ExpandingPanel adds a global keydown listener when opening, but its open-branch cleanup doesn’t
remove it; if the component unmounts while open (or if handler identity changes), Escape listeners
can leak and fire unexpectedly.
Code

src/components/ExpandingPanel.tsx[R39-63]

+  useEffect(() => {
+    if (isOpen) {
+      setPhase('mounting');
+      document.addEventListener('keydown', handleKeyDown);
+
+      const t1 = setTimeout(() => setPhase('expanding'), 30);
+      const t2 = setTimeout(() => setPhase('content-in'), 900);
+
+      return () => {
+        clearTimeout(t1);
+        clearTimeout(t2);
+      };
+    } else {
+      if (phase === 'hidden') return;
+
+      setPhase('content-out');
+
+      const t1 = setTimeout(() => setPhase('closing'), 350);
+      const t2 = setTimeout(() => {
+        setPhase('hidden');
+        document.body.style.overflow = '';
+      }, 1350);
+
+      document.removeEventListener('keydown', handleKeyDown);
+
Evidence
The isOpen branch registers document.addEventListener('keydown', ...) but its returned cleanup
only clears timeouts, not the event listener. App can unmount the whole panel tree when collapsed,
making “unmount while open” realistic.

src/components/ExpandingPanel.tsx[39-50]
src/App.tsx[311-316]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A document-level keydown listener is added when the panel opens, but not removed in the open-branch cleanup, so it can leak on unmount.

## Issue Context
The app conditionally unmounts panel trees when collapsed, so unmount-while-open can occur.

## Fix Focus Areas
- src/components/ExpandingPanel.tsx[39-70]
- src/App.tsx[311-316]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. RSSHub storage can throw 🐞 Bug ⛯ Reliability
Description
rsshubService reads/writes localStorage without try/catch; since isRSSHubUrl() can be called during
render, a localStorage SecurityError/Quota error can crash the UI in restricted environments.
Code

src/services/rsshubService.ts[R4-11]

+export function getRSSHubInstance(): string {
+  return localStorage.getItem(STORAGE_KEY) || DEFAULT_INSTANCE;
+}
+
+export function setRSSHubInstance(url: string) {
+  const trimmed = url.trim().replace(/\/+$/, '');
+  localStorage.setItem(STORAGE_KEY, trimmed || DEFAULT_INSTANCE);
+}
Evidence
rsshubService directly calls localStorage APIs. In contrast, the rest of the codebase uses guarded
storage access (try/catch) to avoid render-path crashes.

src/services/rsshubService.ts[4-11]
src/hooks/useFeedStore.ts[41-57]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Unguarded localStorage access can throw during render paths, crashing the UI.

## Issue Context
Other store code uses guarded access helpers; rsshubService should match that pattern.

## Fix Focus Areas
- src/services/rsshubService.ts[1-12]
- src/hooks/useFeedStore.ts[40-67]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

6. eprintln! logs unstructured details 📘 Rule violation ⛨ Security
Description
New ElevenLabs logging uses unstructured eprintln! statements and logs raw response bodies. This
makes logs harder to audit and risks capturing sensitive/verbose error details from external
services.
Code

src-tauri/src/lib.rs[R494-498]

+    eprintln!("[elevenlabs] Response status: {status}");
    if !status.is_success() {
        let err_body = response.text().await.unwrap_or_default();
+        eprintln!("[elevenlabs] Error body: {err_body}");
        return Err(format!("ElevenLabs HTTP {status}: {err_body}"));
Evidence
The secure logging checklist requires structured logs and forbids logging sensitive data (PR
Compliance ID 5). The added code logs with plain-text eprintln! and includes the raw err_body
from the provider response.

Rule 5: Generic: Secure Logging Practices
src-tauri/src/lib.rs[494-498]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
ElevenLabs-related logs are emitted via `eprintln!` (unstructured) and include raw provider error bodies. This is not aligned with secure logging requirements.

## Issue Context
The new debug logging was added for troubleshooting, but should be structured and should not dump raw external response bodies without redaction/controls.

## Fix Focus Areas
- src-tauri/src/lib.rs[474-475]
- src-tauri/src/lib.rs[488-491]
- src-tauri/src/lib.rs[494-498]
- src-tauri/src/lib.rs[506-506]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. setRSSHubInstance() lacks validation 📘 Rule violation ⛨ Security
Description
The RSSHub instance URL is persisted from a text input without validating scheme/format, allowing
invalid or unsafe values to be stored and used for feed generation. This is missing security-first
input validation for an external endpoint configuration.
Code

src/services/rsshubService.ts[R8-11]

+export function setRSSHubInstance(url: string) {
+  const trimmed = url.trim().replace(/\/+$/, '');
+  localStorage.setItem(STORAGE_KEY, trimmed || DEFAULT_INSTANCE);
+}
Evidence
The input validation checklist requires validating/sanitizing external inputs (PR Compliance ID 6).
The new RSSHub instance setter stores user-provided values after trimming, but does not validate
that it is a valid http(s) URL (or otherwise safe) before persisting and later using it to build
URLs.

Rule 6: Generic: Security-First Input Validation and Data Handling
src/services/rsshubService.ts[8-11]
src/services/rsshubService.ts[151-159]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`setRSSHubInstance()` persists arbitrary user input without validating it as a safe URL, and later concatenates it into generated RSSHub URLs.

## Issue Context
This value is user-configurable via Settings and is used to construct feed URLs. Validation should ensure a valid `http:`/`https:` URL (and ideally store a normalized origin).

## Fix Focus Areas
- src/services/rsshubService.ts[8-11]
- src/services/rsshubService.ts[151-159]
- src/components/SettingsModal.tsx[543-553]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Rename can double-fire 🐞 Bug ⛯ Reliability
Description
Feed renaming triggers onRenameFeed both on Enter and onBlur; removing a focused input after Enter
can also fire blur, potentially causing duplicate rename calls and unnecessary persistence/sync
work.
Code

src/components/SourcePanel.tsx[R286-299]

+            onKeyDown={(e) => {
+              if (e.key === 'Enter' && renameFeedInput.value.trim()) {
+                onRenameFeed(feed.id, renameFeedInput.value.trim());
+                setRenameFeedInput(null);
+              } else if (e.key === 'Escape') {
+                setRenameFeedInput(null);
+              }
+            }}
+            onBlur={() => {
+              if (renameFeedInput.value.trim() && renameFeedInput.value.trim() !== feed.name) {
+                onRenameFeed(feed.id, renameFeedInput.value.trim());
+              }
+              setRenameFeedInput(null);
+            }}
Evidence
Both handlers call onRenameFeed without a guard. When Enter commits and the input is removed, blur
can occur immediately after with the same value in the closure, calling rename twice.

src/components/SourcePanel.tsx[286-299]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Feed rename can trigger duplicate commits via Enter + blur.

## Issue Context
Both `onKeyDown(Enter)` and `onBlur` call `onRenameFeed`.

## Fix Focus Areas
- src/components/SourcePanel.tsx[275-301]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

9. About version outdated 🐞 Bug ✓ Correctness
Description
The new About panel shows a hardcoded version (v0.4.0), which will become incorrect as soon as the
app version changes (this PR is v0.4.1).
Code

src/components/SourcePanel.tsx[R657-658]

+            <div className="panel-about-version">v0.4.0</div>
+            <p className="panel-about-desc">
Evidence
The About modal displays a literal version string instead of reading the app/package version
dynamically.

src/components/SourcePanel.tsx[653-658]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Hardcoded app version in the About panel will drift from the real version.

## Issue Context
The About UI currently renders `v0.4.0` as a literal.

## Fix Focus Areas
- src/components/SourcePanel.tsx[647-723]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment on lines +837 to 842
className={`reader-tool-btn tts ${ttsStatus !== 'idle' ? 'active' : ''} ${ttsError ? 'error' : ''}`}
title={ttsError ? `Erreur: ${ttsError}` : ttsStatus === 'playing' ? 'Pause' : ttsStatus === 'paused' ? 'Reprendre' : 'Écouter'}
onClick={handleTts}
>
{ttsStatus === 'playing' ? '⏸' : '▶'}
{ttsError ? '⚠' : ttsStatus === 'playing' ? '⏸' : '▶'}
</button>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Ui shows raw ttserror 📘 Rule violation ⛨ Security

The UI tooltip displays the raw ttsError string, which can expose internal/third-party error
details directly to end users. This can leak implementation details (e.g., HTTP status/error bodies)
and should be replaced with a generic user-facing message while keeping details in internal logs.
Agent Prompt
## Issue description
The UI currently displays raw TTS error details (`ttsError`) to end users (tooltip). This can expose internal/provider details and violates secure error handling.

## Issue Context
`ttsError` is set from `err.message` coming from the TTS invocation path, and backend errors can include HTTP status and response bodies.

## Fix Focus Areas
- src/components/ReaderPanel.tsx[330-335]
- src/components/ReaderPanel.tsx[837-842]
- src-tauri/src/lib.rs[496-499]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +536 to +550
const getFavoritesOrder = useCallback((): string[] => {
return loadFromStorage<string[]>(STORAGE_KEYS.FAVORITES_ORDER, []);
}, []);

const getReadLaterOrder = useCallback((): string[] => {
return loadFromStorage<string[]>(STORAGE_KEYS.READLATER_ORDER, []);
}, []);

const reorderFavorites = useCallback((orderedIds: string[]) => {
saveToStorage(STORAGE_KEYS.FAVORITES_ORDER, orderedIds);
}, []);

const reorderReadLater = useCallback((orderedIds: string[]) => {
saveToStorage(STORAGE_KEYS.READLATER_ORDER, orderedIds);
}, []);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Reorder doesn’t re-render 🐞 Bug ✓ Correctness

Saving the new Favorites/Read Later order only writes to localStorage; no React state is updated, so
the UI may not reflect a drag-and-drop reorder until some unrelated re-render occurs.
Agent Prompt
## Issue description
Reordering only writes to localStorage, which doesn’t trigger React re-renders. After a drop, the UI may not update to reflect the new order.

## Issue Context
Favorites/Read Later ordering is read via `getFavoritesOrder/getReadLaterOrder()` but the reorder functions do not update any React state.

## Fix Focus Areas
- src/hooks/useFeedStore.ts[534-585]
- src/App.tsx[121-158]
- src/App.tsx[282-285]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +153 to +166
const handleDrop = useCallback((e: React.DragEvent, targetId: string) => {
e.preventDefault();
const sourceId = e.dataTransfer.getData('text/plain');
if (!sourceId || sourceId === targetId || !onReorderItems) return;

const ids = items.map(i => i.id);
const fromIdx = ids.indexOf(sourceId);
const toIdx = ids.indexOf(targetId);
if (fromIdx === -1 || toIdx === -1) return;

ids.splice(fromIdx, 1);
ids.splice(toIdx, 0, sourceId);
onReorderItems(ids);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Reorder index wrong 🐞 Bug ✓ Correctness

The drag-and-drop reorder algorithm inserts at the original target index after removing the source
item, which shifts indices; dragging an item downward can place it one position off (often after the
intended target).
Agent Prompt
## Issue description
Downward drag-and-drop reorder uses an unadjusted target index after removing the source element, leading to off-by-one placement.

## Issue Context
`ids.splice(fromIdx, 1)` shifts indices; `toIdx` must be adjusted when `fromIdx < toIdx`.

## Fix Focus Areas
- src/components/FeedPanel.tsx[153-169]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +39 to +63
useEffect(() => {
if (isOpen) {
setPhase('mounting');
document.addEventListener('keydown', handleKeyDown);

const t1 = setTimeout(() => setPhase('expanding'), 30);
const t2 = setTimeout(() => setPhase('content-in'), 900);

return () => {
clearTimeout(t1);
clearTimeout(t2);
};
} else {
if (phase === 'hidden') return;

setPhase('content-out');

const t1 = setTimeout(() => setPhase('closing'), 350);
const t2 = setTimeout(() => {
setPhase('hidden');
document.body.style.overflow = '';
}, 1350);

document.removeEventListener('keydown', handleKeyDown);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

4. Expandingpanel listener leak 🐞 Bug ⛯ Reliability

ExpandingPanel adds a global keydown listener when opening, but its open-branch cleanup doesn’t
remove it; if the component unmounts while open (or if handler identity changes), Escape listeners
can leak and fire unexpectedly.
Agent Prompt
## Issue description
A document-level keydown listener is added when the panel opens, but not removed in the open-branch cleanup, so it can leak on unmount.

## Issue Context
The app conditionally unmounts panel trees when collapsed, so unmount-while-open can occur.

## Fix Focus Areas
- src/components/ExpandingPanel.tsx[39-70]
- src/App.tsx[311-316]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +4 to +11
export function getRSSHubInstance(): string {
return localStorage.getItem(STORAGE_KEY) || DEFAULT_INSTANCE;
}

export function setRSSHubInstance(url: string) {
const trimmed = url.trim().replace(/\/+$/, '');
localStorage.setItem(STORAGE_KEY, trimmed || DEFAULT_INSTANCE);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

5. Rsshub storage can throw 🐞 Bug ⛯ Reliability

rsshubService reads/writes localStorage without try/catch; since isRSSHubUrl() can be called during
render, a localStorage SecurityError/Quota error can crash the UI in restricted environments.
Agent Prompt
## Issue description
Unguarded localStorage access can throw during render paths, crashing the UI.

## Issue Context
Other store code uses guarded access helpers; rsshubService should match that pattern.

## Fix Focus Areas
- src/services/rsshubService.ts[1-12]
- src/hooks/useFeedStore.ts[40-67]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@devohmycode devohmycode merged commit 9505e46 into master Feb 22, 2026
1 check was pending
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/App.tsx (1)

122-158: ⚠️ Potential issue | 🟠 Major

items memo won't recalculate after a drag-and-drop reorder.

getFavoritesOrder() / getReadLaterOrder() are called inside this memo, but reorderFavorites / reorderReadLater (in useFeedStore) only write to localStorage without updating any React state. Therefore store's reference never changes due to a reorder, the memo's deps remain unchanged, and this computed items list is not re-sorted until the next unrelated state change (e.g., marking an item as read). If FeedPanel re-derives its display from the items prop on each render, the reordering will visually revert.

Root cause and fix are in useFeedStore — see the comment there.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App.tsx` around lines 122 - 158, The computed items memo (items) relies
on getFavoritesOrder/getReadLaterOrder but reorderFavorites/reorderReadLater
only write to localStorage in useFeedStore, so the store reference never changes
and the memo never recalculates after a drag; fix by making reorderFavorites and
reorderReadLater update React state inside useFeedStore (e.g., store an orders
state or a version/timestamp in the store and setState when reordering) and
still persist to localStorage, so getFavoritesOrder/getReadLaterOrder return the
updated in-memory order and items' useMemo sees a changed dependency and
re-sorts immediately.
🧹 Nitpick comments (2)
src/components/SettingsModal.tsx (1)

535-555: Persist RSSHub instance on blur/debounce, not every keystroke.

Saving on each change can temporarily store invalid partial URLs while the user is typing. Consider persisting on blur or after a debounce.

♻️ Example adjustment
 value={rsshubInstance}
-onChange={(e) => {
-  setRsshubInstance(e.target.value);
-  setRSSHubInstanceConfig(e.target.value);
-}}
+onChange={(e) => setRsshubInstance(e.target.value)}
+onBlur={() => setRSSHubInstanceConfig(rsshubInstance)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/SettingsModal.tsx` around lines 535 - 555, The input currently
calls setRsshubInstance and persists on every keystroke via
setRSSHubInstanceConfig in the onChange handler, which can save invalid partial
URLs; change this so onChange only updates local state (setRsshubInstance) and
persist the value on blur (input id "rsshub-instance" onBlur -> call
setRSSHubInstanceConfig) or implement a debounced persister (e.g., use a
debounced callback or useEffect that calls
setRSSHubInstanceConfig(rsshubInstance) after a short delay) so only completed
values are saved; ensure you update the JSX handlers around rsshubInstance,
setRsshubInstance and setRSSHubInstanceConfig accordingly.
src-tauri/src/lib.rs (1)

474-507: Consider gating verbose ElevenLabs logs.

These eprintln! statements are helpful during debugging but can be noisy in release builds and may expose error payloads. Consider guarding them behind a debug flag or feature.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/src/lib.rs` around lines 474 - 507, Replace the raw eprintln! calls
in the ElevenLabs request/response block (the logging lines around the
client.post/send awaiting, the status/error body prints, and the "Audio
received" print near STANDARD.encode(&bytes)) with gated logging: either use
cfg!(debug_assertions) or a Cargo feature (e.g., feature "verbose_elevenlabs")
to conditionally emit those messages, or switch to the log crate (debug! /
trace!) and enable that level only in debug/verbose builds; ensure you update
the ElevenLabs request error mapping and any sensitive error prints to only log
when the gate is enabled.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/AddFeedModal.tsx`:
- Around line 324-341: When the RSSHub suggestion button is clicked it sets the
input and name but does not clear the prior search state, leaving a stale
dropdown; update the onClick handler inside the rsshubMatch block (the
motion.button that calls setInput and setName) to also invoke the app’s
search-reset function — e.g. call the existing search-clearing handler
(resetSearch, clearSearchResults, setSearchOpen(false), or whatever the project
uses) immediately after setInput/setName so prior results and any open dropdown
are cleared when applying the suggestion.

In `@src/components/ExpandingPanel.tsx`:
- Around line 110-116: The ExpandingPanel component declares role="dialog" with
aria-modal="true" but lacks a focus trap; update the component to trap focus
while open by either adding two hidden focusable sentinel elements (start/end
sentinels inside the element referenced by panelRef) that on focus move focus to
the first/last focusable element, or by enhancing the existing handleKeyDown to
intercept Tab and Shift+Tab and cycle focus within the panel's focusable
elements; ensure the logic runs only when the panel is open, uses panelRef to
query tabbable elements, and restores focus when the panel closes.
- Around line 39-50: The cleanup inside the useEffect's isOpen branch currently
clears timeouts but doesn't remove the keydown listener, causing a leak; update
the cleanup returned from that branch in ExpandingPanel's useEffect to call
document.removeEventListener('keydown', handleKeyDown) (alongside
clearTimeout(t1) and clearTimeout(t2)) so the listener is removed when the panel
unmounts or closes; reference the useEffect that sets setPhase('mounting'), adds
document.addEventListener('keydown', handleKeyDown), and creates t1/t2 timeouts
to locate where to add the removeEventListener.
- Around line 57-60: The close timer resets body scroll but opening never locks
it; fix by saving and restoring the body's overflow: in the ExpandingPanel
component add a ref (e.g. prevBodyOverflowRef) to store
document.body.style.overflow before opening, set document.body.style.overflow =
'hidden' when you transition the panel to the open/visible phase (where you call
setPhase('visible') or in the open timeout), and in the closing timeout (where
t2 is created and you setPhase('hidden')) restore document.body.style.overflow =
prevBodyOverflowRef.current instead of setting it to ''. This ensures scroll is
locked on open and restored exactly on close.

In `@src/components/FeedPanel.tsx`:
- Around line 153-165: The handleDrop handler shifts indices when removing the
dragged item and currently inserts at toIdx which causes an off-by-one when
dragging downward; update handleDrop (used with items, ids, fromIdx, toIdx, and
onReorderItems) to decrement the insertion index when fromIdx < toIdx (e.g.,
compute adjustedTo = fromIdx < toIdx ? toIdx - 1 : toIdx) before calling
ids.splice and then call onReorderItems(ids) so the moved item ends up in the
intended position.

In `@src/components/SourcePanel.tsx`:
- Around line 286-299: The onKeyDown/onBlur handlers double-call onRenameFeed
because Enter triggers onRenameFeed then unmount triggers onBlur which sees the
old renameFeedInput and calls onRenameFeed again; modify the handlers in
SourcePanel so that onKeyDown still commits (when e.key === 'Enter' and
renameFeedInput.value.trim()) by calling onRenameFeed(feed.id,
renameFeedInput.value.trim()) and then setRenameFeedInput(null), but change
onBlur to only cancel (call setRenameFeedInput(null) without invoking
onRenameFeed) or alternatively implement a ref flag (e.g., commitInProgressRef)
checked by onBlur to avoid calling onRenameFeed twice; update references to
renameFeedInput, onRenameFeed, setRenameFeedInput, feed.id and feed.name
accordingly.
- Line 657: The displayed hard-coded version string in SourcePanel (the <div
className="panel-about-version"> v0.4.0) is out of sync; import the package.json
version and use it instead so it stays current. Update SourcePanel.tsx to import
the version (e.g., import { version } from '../../package.json') and replace the
hard-coded string in the JSX (the element with className "panel-about-version")
to render the imported version variable.

In `@src/hooks/useFeedStore.ts`:
- Around line 536-550: The reorderFavorites and reorderReadLater functions only
call saveToStorage so they don't update React state and the hook never
re-renders; change the hook to hold favoritesOrder and readLaterOrder in
useState (initialize from loadFromStorage in the hook), update those state
setters inside reorderFavorites/reorderReadLater (and still persist via
saveToStorage), and update getFavoritesOrder/getReadLaterOrder to return the
state values instead of calling loadFromStorage each time so that invoking
reorderFavorites/reorderReadLater triggers a re-render and the UI reflects the
new order immediately.
- Around line 281-286: The renameFeed function currently updates only
name/feedName via setFeeds and setItems; update it to also set updated_at (e.g.,
Date.now() or new Date().toISOString()) on the changed feed objects and on any
item-level objects that need the timestamp, and after state updates call the
sync callbacks (onItemsChanged and/or onFeedAdded or whatever feed-sync callback
exists) so the change will propagate to Supabase/remote providers; locate
renameFeed, setFeeds, setItems, updated_at, and the onItemsChanged/onFeedAdded
handlers and ensure the timestamp and callback invocation are included.

In `@src/services/rsshubService.ts`:
- Around line 8-11: The setRSSHubInstance function currently persists any
trimmed string, which can result in invalid RSSHub links; update
setRSSHubInstance to validate the incoming url: trim and remove trailing
slashes, then attempt to parse it (e.g. via new URL(...) or an equivalent URL
validation) and ensure the protocol is http or https; if validation fails or the
value is empty, store DEFAULT_INSTANCE instead of the invalid value; keep
STORAGE_KEY and localStorage.setItem as the persistence mechanism and ensure the
saved value is the validated/trusted instance string.

---

Outside diff comments:
In `@src/App.tsx`:
- Around line 122-158: The computed items memo (items) relies on
getFavoritesOrder/getReadLaterOrder but reorderFavorites/reorderReadLater only
write to localStorage in useFeedStore, so the store reference never changes and
the memo never recalculates after a drag; fix by making reorderFavorites and
reorderReadLater update React state inside useFeedStore (e.g., store an orders
state or a version/timestamp in the store and setState when reordering) and
still persist to localStorage, so getFavoritesOrder/getReadLaterOrder return the
updated in-memory order and items' useMemo sees a changed dependency and
re-sorts immediately.

---

Nitpick comments:
In `@src-tauri/src/lib.rs`:
- Around line 474-507: Replace the raw eprintln! calls in the ElevenLabs
request/response block (the logging lines around the client.post/send awaiting,
the status/error body prints, and the "Audio received" print near
STANDARD.encode(&bytes)) with gated logging: either use cfg!(debug_assertions)
or a Cargo feature (e.g., feature "verbose_elevenlabs") to conditionally emit
those messages, or switch to the log crate (debug! / trace!) and enable that
level only in debug/verbose builds; ensure you update the ElevenLabs request
error mapping and any sensitive error prints to only log when the gate is
enabled.

In `@src/components/SettingsModal.tsx`:
- Around line 535-555: The input currently calls setRsshubInstance and persists
on every keystroke via setRSSHubInstanceConfig in the onChange handler, which
can save invalid partial URLs; change this so onChange only updates local state
(setRsshubInstance) and persist the value on blur (input id "rsshub-instance"
onBlur -> call setRSSHubInstanceConfig) or implement a debounced persister
(e.g., use a debounced callback or useEffect that calls
setRSSHubInstanceConfig(rsshubInstance) after a short delay) so only completed
values are saved; ensure you update the JSX handlers around rsshubInstance,
setRsshubInstance and setRSSHubInstanceConfig accordingly.

Comment on lines +324 to +341
{rsshubMatch && !resolved.shorthand && (
<motion.button
type="button"
className="rsshub-suggestion"
initial={{ opacity: 0, y: -4 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.15 }}
onClick={() => {
setInput(rsshubMatch.rsshubUrl);
if (!name.trim()) setName(rsshubMatch.label);
}}
>
<svg className="rsshub-logo" viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>
</svg>
<span className="rsshub-suggestion-text">Flux RSSHub disponible — {rsshubMatch.label}</span>
</motion.button>
)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clear search state when applying the RSSHub suggestion.

Clicking the suggestion updates the input but leaves prior search results/search state intact, which can leave a stale dropdown open.

🛠️ Proposed fix
 onClick={() => {
   setInput(rsshubMatch.rsshubUrl);
   if (!name.trim()) setName(rsshubMatch.label);
+  setSearchResults([]);
+  setIsSearching(false);
+  setSearchQuery('');
 }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{rsshubMatch && !resolved.shorthand && (
<motion.button
type="button"
className="rsshub-suggestion"
initial={{ opacity: 0, y: -4 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.15 }}
onClick={() => {
setInput(rsshubMatch.rsshubUrl);
if (!name.trim()) setName(rsshubMatch.label);
}}
>
<svg className="rsshub-logo" viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>
</svg>
<span className="rsshub-suggestion-text">Flux RSSHub disponible — {rsshubMatch.label}</span>
</motion.button>
)}
{rsshubMatch && !resolved.shorthand && (
<motion.button
type="button"
className="rsshub-suggestion"
initial={{ opacity: 0, y: -4 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.15 }}
onClick={() => {
setInput(rsshubMatch.rsshubUrl);
if (!name.trim()) setName(rsshubMatch.label);
setSearchResults([]);
setIsSearching(false);
setSearchQuery('');
}}
>
<svg className="rsshub-logo" viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>
</svg>
<span className="rsshub-suggestion-text">Flux RSSHub disponible — {rsshubMatch.label}</span>
</motion.button>
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/AddFeedModal.tsx` around lines 324 - 341, When the RSSHub
suggestion button is clicked it sets the input and name but does not clear the
prior search state, leaving a stale dropdown; update the onClick handler inside
the rsshubMatch block (the motion.button that calls setInput and setName) to
also invoke the app’s search-reset function — e.g. call the existing
search-clearing handler (resetSearch, clearSearchResults, setSearchOpen(false),
or whatever the project uses) immediately after setInput/setName so prior
results and any open dropdown are cleared when applying the suggestion.

Comment on lines +39 to +50
useEffect(() => {
if (isOpen) {
setPhase('mounting');
document.addEventListener('keydown', handleKeyDown);

const t1 = setTimeout(() => setPhase('expanding'), 30);
const t2 = setTimeout(() => setPhase('content-in'), 900);

return () => {
clearTimeout(t1);
clearTimeout(t2);
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Event listener leak when the panel unmounts while open.

The cleanup returned from the isOpen branch only clears the timeouts. If the component is unmounted while isOpen is true (e.g., a parent conditional removes it), document.removeEventListener('keydown', handleKeyDown) is never called, leaving a dangling listener that will invoke onClose after unmount.

🔒 Proposed fix — add removeEventListener to the cleanup of the open branch
     return () => {
       clearTimeout(t1);
       clearTimeout(t2);
+      document.removeEventListener('keydown', handleKeyDown);
     };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (isOpen) {
setPhase('mounting');
document.addEventListener('keydown', handleKeyDown);
const t1 = setTimeout(() => setPhase('expanding'), 30);
const t2 = setTimeout(() => setPhase('content-in'), 900);
return () => {
clearTimeout(t1);
clearTimeout(t2);
};
useEffect(() => {
if (isOpen) {
setPhase('mounting');
document.addEventListener('keydown', handleKeyDown);
const t1 = setTimeout(() => setPhase('expanding'), 30);
const t2 = setTimeout(() => setPhase('content-in'), 900);
return () => {
clearTimeout(t1);
clearTimeout(t2);
document.removeEventListener('keydown', handleKeyDown);
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ExpandingPanel.tsx` around lines 39 - 50, The cleanup inside
the useEffect's isOpen branch currently clears timeouts but doesn't remove the
keydown listener, causing a leak; update the cleanup returned from that branch
in ExpandingPanel's useEffect to call document.removeEventListener('keydown',
handleKeyDown) (alongside clearTimeout(t1) and clearTimeout(t2)) so the listener
is removed when the panel unmounts or closes; reference the useEffect that sets
setPhase('mounting'), adds document.addEventListener('keydown', handleKeyDown),
and creates t1/t2 timeouts to locate where to add the removeEventListener.

Comment on lines +57 to +60
const t2 = setTimeout(() => {
setPhase('hidden');
document.body.style.overflow = '';
}, 1350);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

document.body.style.overflow = '' is a no-op — the corresponding overflow = 'hidden' on open is missing.

The closing timeout resets body overflow, but nowhere in the opening path is document.body.style.overflow = 'hidden' (or 'clip') set. Either the setter was forgotten — meaning body scroll is never locked when the panel is open — or this reset line is dead code and should be removed.

♻️ Option A — lock body scroll on open, restore on close (typical modal pattern)
     if (isOpen) {
       setPhase('mounting');
       document.addEventListener('keydown', handleKeyDown);
+      document.body.style.overflow = 'hidden';

       const t1 = setTimeout(() => setPhase('expanding'), 30);
       const t2 = setTimeout(() => setPhase('content-in'), 900);

       return () => {
         clearTimeout(t1);
         clearTimeout(t2);
         document.removeEventListener('keydown', handleKeyDown);
+        document.body.style.overflow = '';
       };

Then remove the document.body.style.overflow = '' line from the closing timeout (line 59).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ExpandingPanel.tsx` around lines 57 - 60, The close timer
resets body scroll but opening never locks it; fix by saving and restoring the
body's overflow: in the ExpandingPanel component add a ref (e.g.
prevBodyOverflowRef) to store document.body.style.overflow before opening, set
document.body.style.overflow = 'hidden' when you transition the panel to the
open/visible phase (where you call setPhase('visible') or in the open timeout),
and in the closing timeout (where t2 is created and you setPhase('hidden'))
restore document.body.style.overflow = prevBodyOverflowRef.current instead of
setting it to ''. This ensures scroll is locked on open and restored exactly on
close.

Comment on lines +110 to +116
<div
ref={panelRef}
role="dialog"
aria-modal="true"
aria-label={title || 'Panneau'}
className="expanding-panel"
style={{ clipPath, transition: clipTransition }}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

aria-modal="true" dialog lacks a focus trap.

Declaring aria-modal="true" signals to assistive technologies that interaction is restricted to this dialog, but there is no Tab-key interception or sentinel elements to keep focus inside the panel. Keyboard-only and screen-reader users can Tab out of the dialog while it is open.

Add focus sentinels (hidden focusable elements at the start and end of the panel) that redirect focus back in, or intercept Tab/Shift+Tab in the handleKeyDown callback.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ExpandingPanel.tsx` around lines 110 - 116, The ExpandingPanel
component declares role="dialog" with aria-modal="true" but lacks a focus trap;
update the component to trap focus while open by either adding two hidden
focusable sentinel elements (start/end sentinels inside the element referenced
by panelRef) that on focus move focus to the first/last focusable element, or by
enhancing the existing handleKeyDown to intercept Tab and Shift+Tab and cycle
focus within the panel's focusable elements; ensure the logic runs only when the
panel is open, uses panelRef to query tabbable elements, and restores focus when
the panel closes.

Comment on lines +153 to +165
const handleDrop = useCallback((e: React.DragEvent, targetId: string) => {
e.preventDefault();
const sourceId = e.dataTransfer.getData('text/plain');
if (!sourceId || sourceId === targetId || !onReorderItems) return;

const ids = items.map(i => i.id);
const fromIdx = ids.indexOf(sourceId);
const toIdx = ids.indexOf(targetId);
if (fromIdx === -1 || toIdx === -1) return;

ids.splice(fromIdx, 1);
ids.splice(toIdx, 0, sourceId);
onReorderItems(ids);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix off‑by‑one when dropping an item below its original position.

When dragging downward, removing the source shifts indices; inserting at the original target index places the item after the target (despite the drop‑over border at the top). Adjust the insert index when fromIdx < toIdx.

🐛 Proposed fix
 const fromIdx = ids.indexOf(sourceId);
 const toIdx = ids.indexOf(targetId);
 if (fromIdx === -1 || toIdx === -1) return;

 ids.splice(fromIdx, 1);
-ids.splice(toIdx, 0, sourceId);
+const insertIdx = fromIdx < toIdx ? toIdx - 1 : toIdx;
+ids.splice(insertIdx, 0, sourceId);
 onReorderItems(ids);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/FeedPanel.tsx` around lines 153 - 165, The handleDrop handler
shifts indices when removing the dragged item and currently inserts at toIdx
which causes an off-by-one when dragging downward; update handleDrop (used with
items, ids, fromIdx, toIdx, and onReorderItems) to decrement the insertion index
when fromIdx < toIdx (e.g., compute adjustedTo = fromIdx < toIdx ? toIdx - 1 :
toIdx) before calling ids.splice and then call onReorderItems(ids) so the moved
item ends up in the intended position.

Comment on lines +286 to +299
onKeyDown={(e) => {
if (e.key === 'Enter' && renameFeedInput.value.trim()) {
onRenameFeed(feed.id, renameFeedInput.value.trim());
setRenameFeedInput(null);
} else if (e.key === 'Escape') {
setRenameFeedInput(null);
}
}}
onBlur={() => {
if (renameFeedInput.value.trim() && renameFeedInput.value.trim() !== feed.name) {
onRenameFeed(feed.id, renameFeedInput.value.trim());
}
setRenameFeedInput(null);
}}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Double onRenameFeed call when Enter is pressed.

When Enter is pressed, onKeyDown calls onRenameFeed and schedules setRenameFeedInput(null). React then re-renders, unmounts the input, and fires a synthetic blur before removal. The onBlur closure captures the pre-update renameFeedInput (still non-null), the guard condition value.trim() !== feed.name is true, and onRenameFeed is called a second time. While idempotent today, it is fragile — any future sync callback on rename will fire twice.

Use a ref flag to guard against the double-fire, or simply remove the rename logic from onBlur and commit only on Enter:

🐛 Proposed fix — commit only via Enter, onBlur cancels silently
             onKeyDown={(e) => {
               if (e.key === 'Enter' && renameFeedInput.value.trim()) {
                 onRenameFeed(feed.id, renameFeedInput.value.trim());
                 setRenameFeedInput(null);
               } else if (e.key === 'Escape') {
                 setRenameFeedInput(null);
               }
             }}
-            onBlur={() => {
-              if (renameFeedInput.value.trim() && renameFeedInput.value.trim() !== feed.name) {
-                onRenameFeed(feed.id, renameFeedInput.value.trim());
-              }
-              setRenameFeedInput(null);
-            }}
+            onBlur={() => setRenameFeedInput(null)}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onKeyDown={(e) => {
if (e.key === 'Enter' && renameFeedInput.value.trim()) {
onRenameFeed(feed.id, renameFeedInput.value.trim());
setRenameFeedInput(null);
} else if (e.key === 'Escape') {
setRenameFeedInput(null);
}
}}
onBlur={() => {
if (renameFeedInput.value.trim() && renameFeedInput.value.trim() !== feed.name) {
onRenameFeed(feed.id, renameFeedInput.value.trim());
}
setRenameFeedInput(null);
}}
onKeyDown={(e) => {
if (e.key === 'Enter' && renameFeedInput.value.trim()) {
onRenameFeed(feed.id, renameFeedInput.value.trim());
setRenameFeedInput(null);
} else if (e.key === 'Escape') {
setRenameFeedInput(null);
}
}}
onBlur={() => setRenameFeedInput(null)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/SourcePanel.tsx` around lines 286 - 299, The onKeyDown/onBlur
handlers double-call onRenameFeed because Enter triggers onRenameFeed then
unmount triggers onBlur which sees the old renameFeedInput and calls
onRenameFeed again; modify the handlers in SourcePanel so that onKeyDown still
commits (when e.key === 'Enter' and renameFeedInput.value.trim()) by calling
onRenameFeed(feed.id, renameFeedInput.value.trim()) and then
setRenameFeedInput(null), but change onBlur to only cancel (call
setRenameFeedInput(null) without invoking onRenameFeed) or alternatively
implement a ref flag (e.g., commitInProgressRef) checked by onBlur to avoid
calling onRenameFeed twice; update references to renameFeedInput, onRenameFeed,
setRenameFeedInput, feed.id and feed.name accordingly.

<div className="panel-about-hero">
<div className="panel-about-logo">◈</div>
<div className="panel-about-appname">SuperFlux</div>
<div className="panel-about-version">v0.4.0</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

About panel version v0.4.0 doesn't match the PR's v0.4.1.

Consider importing the version from package.json to keep it in sync automatically:

♻️ Proposed fix — pull version from package.json

At the top of the file:

import { version } from '../../package.json';

Then in the JSX:

-            <div className="panel-about-version">v0.4.0</div>
+            <div className="panel-about-version">v{version}</div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="panel-about-version">v0.4.0</div>
import { version } from '../../package.json';
// ... (other code)
<div className="panel-about-version">v{version}</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/SourcePanel.tsx` at line 657, The displayed hard-coded version
string in SourcePanel (the <div className="panel-about-version"> v0.4.0) is out
of sync; import the package.json version and use it instead so it stays current.
Update SourcePanel.tsx to import the version (e.g., import { version } from
'../../package.json') and replace the hard-coded string in the JSX (the element
with className "panel-about-version") to render the imported version variable.

Comment on lines +281 to +286
// Rename a feed
const renameFeed = useCallback((feedId: string, newName: string) => {
if (!newName.trim()) return;
setFeeds(prev => prev.map(f => f.id === feedId ? { ...f, name: newName.trim() } : f));
setItems(prev => prev.map(i => i.feedId === feedId ? { ...i, feedName: newName.trim() } : i));
}, []);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

renameFeed omits updated_at bump and sync notification.

The feed and its items are updated in place but updated_at is not refreshed, so any sync logic relying on that field to detect changes will miss the rename. Additionally, no onItemsChanged / onFeedAdded callback is triggered, meaning the rename is silently local-only and will not propagate to Supabase or a connected provider.

♻️ Proposed fix
   const renameFeed = useCallback((feedId: string, newName: string) => {
     if (!newName.trim()) return;
-    setFeeds(prev => prev.map(f => f.id === feedId ? { ...f, name: newName.trim() } : f));
-    setItems(prev => prev.map(i => i.feedId === feedId ? { ...i, feedName: newName.trim() } : i));
+    const now = new Date().toISOString();
+    setFeeds(prev => prev.map(f =>
+      f.id === feedId ? { ...f, name: newName.trim(), updated_at: now } : f
+    ));
+    setItems(prev => {
+      const next = prev.map(i =>
+        i.feedId === feedId ? { ...i, feedName: newName.trim(), updated_at: now } : i
+      );
+      const changed = next.filter((item, idx) => item !== prev[idx]);
+      if (changed.length > 0) queueMicrotask(() => cbRef.current?.onItemsChanged?.(changed));
+      return next;
+    });
   }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useFeedStore.ts` around lines 281 - 286, The renameFeed function
currently updates only name/feedName via setFeeds and setItems; update it to
also set updated_at (e.g., Date.now() or new Date().toISOString()) on the
changed feed objects and on any item-level objects that need the timestamp, and
after state updates call the sync callbacks (onItemsChanged and/or onFeedAdded
or whatever feed-sync callback exists) so the change will propagate to
Supabase/remote providers; locate renameFeed, setFeeds, setItems, updated_at,
and the onItemsChanged/onFeedAdded handlers and ensure the timestamp and
callback invocation are included.

Comment on lines +536 to +550
const getFavoritesOrder = useCallback((): string[] => {
return loadFromStorage<string[]>(STORAGE_KEYS.FAVORITES_ORDER, []);
}, []);

const getReadLaterOrder = useCallback((): string[] => {
return loadFromStorage<string[]>(STORAGE_KEYS.READLATER_ORDER, []);
}, []);

const reorderFavorites = useCallback((orderedIds: string[]) => {
saveToStorage(STORAGE_KEYS.FAVORITES_ORDER, orderedIds);
}, []);

const reorderReadLater = useCallback((orderedIds: string[]) => {
saveToStorage(STORAGE_KEYS.READLATER_ORDER, orderedIds);
}, []);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

reorderFavorites / reorderReadLater never update React state — order changes are invisible until the next unrelated re-render.

Both functions only call saveToStorage and return. Because no setState is invoked, useFeedStore does not re-render, the store reference in App.tsx does not change, and the items useMemo (which calls getFavoritesOrder / getReadLaterOrder) is never re-evaluated after a drag-and-drop. The new order is applied the next time any other state change happens to trigger a re-render — until then, the displayed order can visually revert.

Lift the orders into useState so that a reorder triggers a re-render:

🐛 Proposed fix — store orders in React state
+  const [favoritesOrder, setFavoritesOrder] = useState<string[]>(() =>
+    loadFromStorage<string[]>(STORAGE_KEYS.FAVORITES_ORDER, []),
+  );
+  const [readLaterOrder, setReadLaterOrder] = useState<string[]>(() =>
+    loadFromStorage<string[]>(STORAGE_KEYS.READLATER_ORDER, []),
+  );

   const getFavoritesOrder = useCallback((): string[] => {
-    return loadFromStorage<string[]>(STORAGE_KEYS.FAVORITES_ORDER, []);
+    return favoritesOrder;
-  }, []);
+  }, [favoritesOrder]);

   const getReadLaterOrder = useCallback((): string[] => {
-    return loadFromStorage<string[]>(STORAGE_KEYS.READLATER_ORDER, []);
+    return readLaterOrder;
-  }, []);
+  }, [readLaterOrder]);

   const reorderFavorites = useCallback((orderedIds: string[]) => {
     saveToStorage(STORAGE_KEYS.FAVORITES_ORDER, orderedIds);
+    setFavoritesOrder(orderedIds);
   }, []);

   const reorderReadLater = useCallback((orderedIds: string[]) => {
     saveToStorage(STORAGE_KEYS.READLATER_ORDER, orderedIds);
+    setReadLaterOrder(orderedIds);
   }, []);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const getFavoritesOrder = useCallback((): string[] => {
return loadFromStorage<string[]>(STORAGE_KEYS.FAVORITES_ORDER, []);
}, []);
const getReadLaterOrder = useCallback((): string[] => {
return loadFromStorage<string[]>(STORAGE_KEYS.READLATER_ORDER, []);
}, []);
const reorderFavorites = useCallback((orderedIds: string[]) => {
saveToStorage(STORAGE_KEYS.FAVORITES_ORDER, orderedIds);
}, []);
const reorderReadLater = useCallback((orderedIds: string[]) => {
saveToStorage(STORAGE_KEYS.READLATER_ORDER, orderedIds);
}, []);
const [favoritesOrder, setFavoritesOrder] = useState<string[]>(() =>
loadFromStorage<string[]>(STORAGE_KEYS.FAVORITES_ORDER, []),
);
const [readLaterOrder, setReadLaterOrder] = useState<string[]>(() =>
loadFromStorage<string[]>(STORAGE_KEYS.READLATER_ORDER, []),
);
const getFavoritesOrder = useCallback((): string[] => {
return favoritesOrder;
}, [favoritesOrder]);
const getReadLaterOrder = useCallback((): string[] => {
return readLaterOrder;
}, [readLaterOrder]);
const reorderFavorites = useCallback((orderedIds: string[]) => {
saveToStorage(STORAGE_KEYS.FAVORITES_ORDER, orderedIds);
setFavoritesOrder(orderedIds);
}, []);
const reorderReadLater = useCallback((orderedIds: string[]) => {
saveToStorage(STORAGE_KEYS.READLATER_ORDER, orderedIds);
setReadLaterOrder(orderedIds);
}, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useFeedStore.ts` around lines 536 - 550, The reorderFavorites and
reorderReadLater functions only call saveToStorage so they don't update React
state and the hook never re-renders; change the hook to hold favoritesOrder and
readLaterOrder in useState (initialize from loadFromStorage in the hook), update
those state setters inside reorderFavorites/reorderReadLater (and still persist
via saveToStorage), and update getFavoritesOrder/getReadLaterOrder to return the
state values instead of calling loadFromStorage each time so that invoking
reorderFavorites/reorderReadLater triggers a re-render and the UI reflects the
new order immediately.

Comment on lines +8 to +11
export function setRSSHubInstance(url: string) {
const trimmed = url.trim().replace(/\/+$/, '');
localStorage.setItem(STORAGE_KEY, trimmed || DEFAULT_INSTANCE);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate RSSHub instance URLs before persisting.

Persisting invalid values (missing scheme, malformed URL) can generate unusable RSSHub links downstream. Consider validating before saving and falling back to the default.

🛡️ Proposed fix
 export function setRSSHubInstance(url: string) {
   const trimmed = url.trim().replace(/\/+$/, '');
-  localStorage.setItem(STORAGE_KEY, trimmed || DEFAULT_INSTANCE);
+  if (!trimmed) {
+    localStorage.setItem(STORAGE_KEY, DEFAULT_INSTANCE);
+    return;
+  }
+  try {
+    new URL(trimmed);
+    localStorage.setItem(STORAGE_KEY, trimmed);
+  } catch {
+    localStorage.setItem(STORAGE_KEY, DEFAULT_INSTANCE);
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function setRSSHubInstance(url: string) {
const trimmed = url.trim().replace(/\/+$/, '');
localStorage.setItem(STORAGE_KEY, trimmed || DEFAULT_INSTANCE);
}
export function setRSSHubInstance(url: string) {
const trimmed = url.trim().replace(/\/+$/, '');
if (!trimmed) {
localStorage.setItem(STORAGE_KEY, DEFAULT_INSTANCE);
return;
}
try {
new URL(trimmed);
localStorage.setItem(STORAGE_KEY, trimmed);
} catch {
localStorage.setItem(STORAGE_KEY, DEFAULT_INSTANCE);
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/rsshubService.ts` around lines 8 - 11, The setRSSHubInstance
function currently persists any trimmed string, which can result in invalid
RSSHub links; update setRSSHubInstance to validate the incoming url: trim and
remove trailing slashes, then attempt to parse it (e.g. via new URL(...) or an
equivalent URL validation) and ensure the protocol is http or https; if
validation fails or the value is empty, store DEFAULT_INSTANCE instead of the
invalid value; keep STORAGE_KEY and localStorage.setItem as the persistence
mechanism and ensure the saved value is the validated/trusted instance string.

@coderabbitai coderabbitai bot mentioned this pull request Mar 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant