diff --git a/help/include/reading-conversations.md b/help/include/reading-conversations.md index 5f9b6c6bc4..436178c231 100644 --- a/help/include/reading-conversations.md +++ b/help/include/reading-conversations.md @@ -14,7 +14,8 @@ !!! keyboard_tip "" - Use the N key to go to the next unread topic, or P - to go to the next unread direct message conversation. + Use the N key to go to the next unread topic, or Shift + N + for the next unread [followed](/help/follow-a-topic) topic, or P for the next + unread direct message conversation. {end_tabs} diff --git a/help/keyboard-shortcuts.md b/help/keyboard-shortcuts.md index 9045d88d27..eeb7d80042 100644 --- a/help/keyboard-shortcuts.md +++ b/help/keyboard-shortcuts.md @@ -38,6 +38,8 @@ in the Zulip app to add more to your repertoire as needed. * **Next unread topic**: N +* **Next unread followed topic**: Shift + N + * **Next unread direct message**: P * **Search messages**: / diff --git a/web/src/hotkey.js b/web/src/hotkey.js index e658907e51..2c423346a3 100644 --- a/web/src/hotkey.js +++ b/web/src/hotkey.js @@ -88,6 +88,7 @@ const keydown_shift_mappings = { 38: {name: "up_arrow", message_view_only: false}, // up arrow 40: {name: "down_arrow", message_view_only: false}, // down arrow 72: {name: "view_edit_history", message_view_only: true}, // 'H' + 78: {name: "narrow_to_next_unread_followed_topic", message_view_only: false}, // 'N' }; const keydown_unshift_mappings = { @@ -904,7 +905,10 @@ export function process_hotkey(e, hotkey) { narrow.stream_cycle_forward(); return true; case "n_key": - narrow.narrow_to_next_topic({trigger: "hotkey"}); + narrow.narrow_to_next_topic({trigger: "hotkey", only_followed_topics: false}); + return true; + case "narrow_to_next_unread_followed_topic": + narrow.narrow_to_next_topic({trigger: "hotkey", only_followed_topics: true}); return true; case "p_key": narrow.narrow_to_next_pm_string({trigger: "hotkey"}); diff --git a/web/src/narrow.js b/web/src/narrow.js index ef3515ae49..ea5de0e06e 100644 --- a/web/src/narrow.js +++ b/web/src/narrow.js @@ -770,7 +770,11 @@ export function narrow_to_next_topic(opts = {}) { topic: narrow_state.topic(), }; - const next_narrow = topic_generator.get_next_topic(curr_info.stream, curr_info.topic); + const next_narrow = topic_generator.get_next_topic( + curr_info.stream, + curr_info.topic, + opts.only_followed_topics, + ); if (!next_narrow) { return; diff --git a/web/src/topic_generator.js b/web/src/topic_generator.js index 6d4acc5dd2..3da8f919b2 100644 --- a/web/src/topic_generator.js +++ b/web/src/topic_generator.js @@ -49,7 +49,7 @@ export function next_topic(streams, get_topics, has_unread_messages, curr_stream return undefined; } -export function get_next_topic(curr_stream, curr_topic) { +export function get_next_topic(curr_stream, curr_topic, only_followed_topics) { let my_streams = stream_list_sort.get_streams(); my_streams = my_streams.filter((stream_name) => { @@ -71,11 +71,28 @@ export function get_next_topic(curr_stream, curr_topic) { return topics; } + function get_followed_topics(stream_name) { + const stream_id = stream_data.get_stream_id(stream_name); + let topics = stream_topic_history.get_recent_topic_names(stream_id); + topics = topics.filter((topic) => user_topics.is_topic_followed(stream_id, topic)); + return topics; + } + function has_unread_messages(stream_name, topic) { const stream_id = stream_data.get_stream_id(stream_name); return unread.topic_has_any_unread(stream_id, topic); } + if (only_followed_topics) { + return next_topic( + my_streams, + get_followed_topics, + has_unread_messages, + curr_stream, + curr_topic, + ); + } + return next_topic(my_streams, get_unmuted_topics, has_unread_messages, curr_stream, curr_topic); } diff --git a/web/templates/keyboard_shortcuts.hbs b/web/templates/keyboard_shortcuts.hbs index d645c57db3..41111db0f6 100644 --- a/web/templates/keyboard_shortcuts.hbs +++ b/web/templates/keyboard_shortcuts.hbs @@ -40,6 +40,10 @@ {{t 'Next unread topic' }} N + + {{t 'Next unread followed topic' }} + Shift + N + {{t 'Next unread direct message' }} P diff --git a/web/tests/hotkey.test.js b/web/tests/hotkey.test.js index 02640adb8f..1dd4e6199d 100644 --- a/web/tests/hotkey.test.js +++ b/web/tests/hotkey.test.js @@ -182,6 +182,7 @@ run_test("mappings", () => { assert.equal(map_down(13).name, "enter"); assert.equal(map_down(46).name, "delete"); assert.equal(map_down(13, true).name, "enter"); + assert.equal(map_down(78, true).name, "narrow_to_next_unread_followed_topic"); assert.equal(map_press(47).name, "search"); // slash assert.equal(map_press(106).name, "vim_down"); // j @@ -231,11 +232,15 @@ run_test("mappings", () => { navigator.platform = ""; }); -function process(s) { +function process(s, shiftKey, keydown = false) { const e = { which: s.codePointAt(0), + shiftKey, }; try { + if (keydown) { + return hotkey.process_keydown(e); + } return hotkey.process_keypress(e); } catch (error) /* istanbul ignore next */ { // An exception will be thrown here if a different @@ -247,9 +252,9 @@ function process(s) { } } -function assert_mapping(c, module, func_name, shiftKey) { +function assert_mapping(c, module, func_name, shiftKey, keydown) { stubbing(module, func_name, (stub) => { - assert.ok(process(c, shiftKey)); + assert.ok(process(c, shiftKey, keydown)); assert.equal(stub.num_calls, 1); }); } @@ -442,6 +447,10 @@ run_test("n/p keys", () => { assert_mapping("n", narrow, "narrow_to_next_topic"); }); +run_test("narrow next unread followed topic", () => { + assert_mapping("N", narrow, "narrow_to_next_topic", true, true); +}); + run_test("motion_keys", () => { const codes = { down_arrow: 40, diff --git a/web/tests/topic_generator.test.js b/web/tests/topic_generator.test.js index 1bf05da0b7..b6c6b84a0c 100644 --- a/web/tests/topic_generator.test.js +++ b/web/tests/topic_generator.test.js @@ -89,9 +89,9 @@ run_test("topics", ({override}) => { override(stream_topic_history, "get_recent_topic_names", (stream_id) => { switch (stream_id) { case muted_stream_id: - return ["ms-topic1", "ms-topic2"]; + return ["ms-topic1", "ms-topic2", "followed"]; case devel_stream_id: - return ["muted", "python"]; + return ["muted", "python", "followed"]; } return []; @@ -107,12 +107,26 @@ run_test("topics", ({override}) => { override(user_topics, "is_topic_muted", (_stream_name, topic) => topic === "muted"); + override(user_topics, "is_topic_followed", (_stream_name, topic) => topic === "followed"); + let next_item = tg.get_next_topic("announce", "whatever"); assert.deepEqual(next_item, { stream: "devel", topic: "python", }); + next_item = tg.get_next_topic("devel", "python"); + assert.deepEqual(next_item, { + stream: "devel", + topic: "followed", + }); + + next_item = tg.get_next_topic("muted", "whatever", true); + assert.deepEqual(next_item, { + stream: "muted", + topic: "followed", + }); + next_item = tg.get_next_topic("muted", undefined); assert.deepEqual(next_item, { stream: "muted",