hotkey: Add 'narrow to next unread followed topic' hotkey.

This commit adds a 'Shift + N' keyboard shortcut, which is
used to narrow to the next unread followed topic.

Fixes part of #27323.
This commit is contained in:
Prakhar Pratyush 2023-11-01 17:29:58 +05:30 committed by Tim Abbott
parent 6819ecee92
commit 8e2264b585
8 changed files with 65 additions and 10 deletions

View File

@ -14,7 +14,8 @@
!!! keyboard_tip "" !!! keyboard_tip ""
Use the <kbd>N</kbd> key to go to the next unread topic, or <kbd>P</kbd> Use the <kbd>N</kbd> key to go to the next unread topic, or <kbd>Shift</kbd> + <kbd>N</kbd>
to go to the next unread direct message conversation. for the next unread [followed](/help/follow-a-topic) topic, or <kbd>P</kbd> for the next
unread direct message conversation.
{end_tabs} {end_tabs}

View File

@ -38,6 +38,8 @@ in the Zulip app to add more to your repertoire as needed.
* **Next unread topic**: <kbd>N</kbd> * **Next unread topic**: <kbd>N</kbd>
* **Next unread followed topic**: <kbd>Shift</kbd> + <kbd>N</kbd>
* **Next unread direct message**: <kbd>P</kbd> * **Next unread direct message**: <kbd>P</kbd>
* **Search messages**: <kbd>/</kbd> * **Search messages**: <kbd>/</kbd>

View File

@ -88,6 +88,7 @@ const keydown_shift_mappings = {
38: {name: "up_arrow", message_view_only: false}, // up arrow 38: {name: "up_arrow", message_view_only: false}, // up arrow
40: {name: "down_arrow", message_view_only: false}, // down arrow 40: {name: "down_arrow", message_view_only: false}, // down arrow
72: {name: "view_edit_history", message_view_only: true}, // 'H' 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 = { const keydown_unshift_mappings = {
@ -904,7 +905,10 @@ export function process_hotkey(e, hotkey) {
narrow.stream_cycle_forward(); narrow.stream_cycle_forward();
return true; return true;
case "n_key": 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; return true;
case "p_key": case "p_key":
narrow.narrow_to_next_pm_string({trigger: "hotkey"}); narrow.narrow_to_next_pm_string({trigger: "hotkey"});

View File

@ -770,7 +770,11 @@ export function narrow_to_next_topic(opts = {}) {
topic: narrow_state.topic(), 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) { if (!next_narrow) {
return; return;

View File

@ -49,7 +49,7 @@ export function next_topic(streams, get_topics, has_unread_messages, curr_stream
return undefined; 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(); let my_streams = stream_list_sort.get_streams();
my_streams = my_streams.filter((stream_name) => { my_streams = my_streams.filter((stream_name) => {
@ -71,11 +71,28 @@ export function get_next_topic(curr_stream, curr_topic) {
return topics; 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) { function has_unread_messages(stream_name, topic) {
const stream_id = stream_data.get_stream_id(stream_name); const stream_id = stream_data.get_stream_id(stream_name);
return unread.topic_has_any_unread(stream_id, topic); 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); return next_topic(my_streams, get_unmuted_topics, has_unread_messages, curr_stream, curr_topic);
} }

View File

@ -40,6 +40,10 @@
<td class="definition">{{t 'Next unread topic' }}</td> <td class="definition">{{t 'Next unread topic' }}</td>
<td><span class="hotkey"><kbd>N</kbd></span></td> <td><span class="hotkey"><kbd>N</kbd></span></td>
</tr> </tr>
<tr>
<td class="definition">{{t 'Next unread followed topic' }}</td>
<td><span class="hotkey"><kbd>Shift</kbd> + <kbd>N</kbd></span></td>
</tr>
<tr> <tr>
<td class="definition">{{t 'Next unread direct message' }}</td> <td class="definition">{{t 'Next unread direct message' }}</td>
<td><span class="hotkey"><kbd>P</kbd></span></td> <td><span class="hotkey"><kbd>P</kbd></span></td>

View File

@ -182,6 +182,7 @@ run_test("mappings", () => {
assert.equal(map_down(13).name, "enter"); assert.equal(map_down(13).name, "enter");
assert.equal(map_down(46).name, "delete"); assert.equal(map_down(46).name, "delete");
assert.equal(map_down(13, true).name, "enter"); 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(47).name, "search"); // slash
assert.equal(map_press(106).name, "vim_down"); // j assert.equal(map_press(106).name, "vim_down"); // j
@ -231,11 +232,15 @@ run_test("mappings", () => {
navigator.platform = ""; navigator.platform = "";
}); });
function process(s) { function process(s, shiftKey, keydown = false) {
const e = { const e = {
which: s.codePointAt(0), which: s.codePointAt(0),
shiftKey,
}; };
try { try {
if (keydown) {
return hotkey.process_keydown(e);
}
return hotkey.process_keypress(e); return hotkey.process_keypress(e);
} catch (error) /* istanbul ignore next */ { } catch (error) /* istanbul ignore next */ {
// An exception will be thrown here if a different // 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) => { stubbing(module, func_name, (stub) => {
assert.ok(process(c, shiftKey)); assert.ok(process(c, shiftKey, keydown));
assert.equal(stub.num_calls, 1); assert.equal(stub.num_calls, 1);
}); });
} }
@ -442,6 +447,10 @@ run_test("n/p keys", () => {
assert_mapping("n", narrow, "narrow_to_next_topic"); 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", () => { run_test("motion_keys", () => {
const codes = { const codes = {
down_arrow: 40, down_arrow: 40,

View File

@ -89,9 +89,9 @@ run_test("topics", ({override}) => {
override(stream_topic_history, "get_recent_topic_names", (stream_id) => { override(stream_topic_history, "get_recent_topic_names", (stream_id) => {
switch (stream_id) { switch (stream_id) {
case muted_stream_id: case muted_stream_id:
return ["ms-topic1", "ms-topic2"]; return ["ms-topic1", "ms-topic2", "followed"];
case devel_stream_id: case devel_stream_id:
return ["muted", "python"]; return ["muted", "python", "followed"];
} }
return []; 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_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"); let next_item = tg.get_next_topic("announce", "whatever");
assert.deepEqual(next_item, { assert.deepEqual(next_item, {
stream: "devel", stream: "devel",
topic: "python", 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); next_item = tg.get_next_topic("muted", undefined);
assert.deepEqual(next_item, { assert.deepEqual(next_item, {
stream: "muted", stream: "muted",