diff --git a/README.md b/README.md index c95a4c2..6f3283f 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Set `HN_REDUCED_MOTION=1` to disable visual animations; ANSI and monochrome pale ## Read a story -Use `j/k` or the arrow keys to select a story. Press `1`, `2`, or `3` for Top, New, or Best; `4` opens Saved. Use `n/p` to change pages — or keep holding `j` past the last story to continue on the next page's first story, and `k` at the top to go back a page. +Use `j/k` or the arrow keys to select a story. Press `1`, `2`, or `3` for Top, New, or Best; `4` opens Saved. Use `n/p` to change pages — or press `j` past the last story to continue on the next page's first story, and `k` at the top to go back a page (further `j/k` wait while the page loads). Press `o` or Enter to open the discussion and its article. Press `c` to read the discussion without opening a new article pane. If a companion pane is already open, it follows the selected story. `O` always opens the story in your default browser. diff --git a/src/app.rs b/src/app.rs index e3d39c8..c000e6d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -209,14 +209,18 @@ impl App { /// Move to next story. At the last story of the page, continue on the next /// page instead (the first story once it loads). Returns true when the - /// page advanced and the caller should request the new page. + /// page advanced and the caller should request the new page. While a page + /// or feed switch is still in flight, j/k are ignored so repeated presses + /// cannot stack multiple page advances. pub fn next_story(&mut self) -> bool { - if self.stories.is_empty() { + if self.stories.is_empty() || self.showing_stale_stories() { return false; } if self.selected_index < self.stories.len() - 1 { self.move_selection(self.selected_index + 1); false + } else if self.is_loading() { + false } else { let page = self.current_page; self.next_page(); @@ -225,12 +229,17 @@ impl App { } /// Move to previous story. At the first story of the page, go back to the - /// previous page (restoring the position left there). Returns true when - /// the page changed and the caller should request it. + /// previous page. Returns true when the page changed and the caller should + /// request it. Blocked while a page/feed switch is in flight, like next_story. pub fn prev_story(&mut self) -> bool { + if self.showing_stale_stories() { + return false; + } if self.selected_index > 0 { self.move_selection(self.selected_index - 1); false + } else if self.is_loading() { + false } else { let page = self.current_page; self.prev_page(); diff --git a/src/ui/ui_tests.rs b/src/ui/ui_tests.rs index 9c035f6..722475f 100644 --- a/src/ui/ui_tests.rs +++ b/src/ui/ui_tests.rs @@ -819,3 +819,32 @@ fn j_mid_list_returns_false_and_keeps_selection_on_page() { assert_eq!(app.selected_index, 1); assert_eq!(app.current_page, 1); } + +#[test] +fn jk_do_not_stack_while_a_page_switch_is_in_flight() { + let mut app = feed(); + app.selected_index = app.stories.len() - 1; + assert!(app.next_story(), "first j advances to page 2"); + assert_eq!(app.current_page, 2); + // Page 2 has not loaded: the displayed list is still page 1 (stale), so + // further j/k must be ignored instead of stacking another page advance. + assert!(app.showing_stale_stories()); + assert!(!app.next_story(), "j while fetching must not advance to page 3"); + assert_eq!(app.current_page, 2); + assert!(!app.prev_story(), "k while a switch is in flight must not move"); + assert_eq!(app.current_page, 2); + assert_eq!(app.selected_index, app.stories.len() - 1); +} + +#[test] +fn page_advance_blocked_during_same_page_refresh_but_selection_still_moves() { + let mut app = feed(); + app.selected_index = app.stories.len() - 1; + app.set_story_loading(true); + assert!(!app.next_story(), "j at the end during a refresh must not advance"); + assert_eq!(app.current_page, 1); + let mut app = feed(); + app.set_story_loading(true); + assert!(!app.next_story(), "mid-list j still works during a refresh"); + assert_eq!(app.selected_index, 1); +}