From dc14d156b65cba34716ea30c968067a6dac2fd39 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Thu, 30 Jul 2020 17:54:47 +1000 Subject: [PATCH] FEATURE: g,j and g,k to navigate to next and prev topic After visiting a topic list (by tag / category / top level) we track the list Once a list is tracked the combo `g` `j` can be used to go to the next topic in the list and `g` `k` to go to previous topic. This allows you to quickly work through subsets of topics without having to navigate back to the top level lists The shortcut does not work in PM lists yet, or search results, both are under consideration. --- .../controllers/keyboard-shortcuts-help.js | 4 +- .../discourse/app/lib/keyboard-shortcuts.js | 22 ++++++++ .../discourse/app/lib/topic-list-tracker.js | 56 +++++++++++++++++++ .../discourse/app/routes/discovery.js | 4 ++ .../discourse/app/routes/tags-show.js | 4 ++ .../javascripts/discourse/app/routes/topic.js | 6 +- .../modal/keyboard-shortcuts-help.hbs | 2 + config/locales/client.en.yml | 2 + 8 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 app/assets/javascripts/discourse/app/lib/topic-list-tracker.js diff --git a/app/assets/javascripts/discourse/app/controllers/keyboard-shortcuts-help.js b/app/assets/javascripts/discourse/app/controllers/keyboard-shortcuts-help.js index 2199e6b711..7e380bd1fc 100644 --- a/app/assets/javascripts/discourse/app/controllers/keyboard-shortcuts-help.js +++ b/app/assets/javascripts/discourse/app/controllers/keyboard-shortcuts-help.js @@ -69,7 +69,9 @@ export default Controller.extend(ModalFunctionality, { bookmarks: buildShortcut("jump_to.bookmarks", { keys1: ["g", "b"] }), profile: buildShortcut("jump_to.profile", { keys1: ["g", "p"] }), messages: buildShortcut("jump_to.messages", { keys1: ["g", "m"] }), - drafts: buildShortcut("jump_to.drafts", { keys1: ["g", "d"] }) + drafts: buildShortcut("jump_to.drafts", { keys1: ["g", "d"] }), + next: buildShortcut("jump_to.next", { keys1: ["g", "j"] }), + previous: buildShortcut("jump_to.previous", { keys1: ["g", "k"] }) }, navigation: { back: buildShortcut("navigation.back", { keys1: ["u"] }), diff --git a/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js b/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js index 50cc5ee7aa..2095c02a21 100644 --- a/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js +++ b/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js @@ -5,6 +5,10 @@ import { minimumOffset } from "discourse/lib/offset-calculator"; import { ajax } from "discourse/lib/ajax"; import { throttle, schedule } from "@ember/runloop"; import { INPUT_DELAY } from "discourse-common/config/environment"; +import { + nextTopicUrl, + previousTopicUrl +} from "discourse/lib/topic-list-tracker"; const DEFAULT_BINDINGS = { "!": { postAction: "showFlags" }, @@ -36,6 +40,8 @@ const DEFAULT_BINDINGS = { "g m": { path: "/my/messages" }, "g d": { path: "/my/activity/drafts" }, "g s": { handler: "goToFirstSuggestedTopic", anonymous: true }, + "g j": { handler: "goToNextTopic", anonymous: true }, + "g k": { handler: "goToPreviousTopic", anonymous: true }, home: { handler: "goToFirstPost", anonymous: true }, "command+up": { handler: "goToFirstPost", anonymous: true }, j: { handler: "selectDown", anonymous: true }, @@ -228,6 +234,22 @@ export default { return false; }, + goToNextTopic() { + nextTopicUrl().then(url => { + if (url) { + DiscourseURL.routeTo(url); + } + }); + }, + + goToPreviousTopic() { + previousTopicUrl().then(url => { + if (url) { + DiscourseURL.routeTo(url); + } + }); + }, + goToFirstSuggestedTopic() { const $el = $(".suggested-topics a.raw-topic-link:first"); if ($el.length) { diff --git a/app/assets/javascripts/discourse/app/lib/topic-list-tracker.js b/app/assets/javascripts/discourse/app/lib/topic-list-tracker.js new file mode 100644 index 0000000000..c6a5ac30de --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/topic-list-tracker.js @@ -0,0 +1,56 @@ +import { Promise } from "rsvp"; +let model, currentTopicId; + +export function setTopicList(incomingModel) { + model = incomingModel; + currentTopicId = null; +} + +export function nextTopicUrl() { + return urlAt(1); +} + +export function previousTopicUrl() { + return urlAt(-1); +} + +function urlAt(delta) { + if (!model || !model.topics) { + return Promise.resolve(null); + } + + let index = currentIndex(); + if (index === -1) { + index = 0; + } else { + index += delta; + } + + const topic = model.topics[index]; + + if (!topic && index > 0 && model.more_topics_url && model.loadMore) { + return model.loadMore().then(() => urlAt(delta)); + } + + if (topic) { + currentTopicId = topic.id; + return Promise.resolve(topic.lastUnreadUrl); + } + + return Promise.resolve(null); +} + +export function setTopicId(topicId) { + currentTopicId = topicId; +} + +function currentIndex() { + if (currentTopicId && model && model.topics) { + const idx = _.findIndex(model.topics, t => t.id === currentTopicId); + if (idx > -1) { + return idx; + } + } + + return -1; +} diff --git a/app/assets/javascripts/discourse/app/routes/discovery.js b/app/assets/javascripts/discourse/app/routes/discovery.js index 38059e2447..43f030d9ab 100644 --- a/app/assets/javascripts/discourse/app/routes/discovery.js +++ b/app/assets/javascripts/discourse/app/routes/discovery.js @@ -6,6 +6,7 @@ import DiscourseRoute from "discourse/routes/discourse"; import OpenComposer from "discourse/mixins/open-composer"; import { scrollTop } from "discourse/mixins/scroll-top"; import User from "discourse/models/user"; +import { setTopicList } from "discourse/lib/topic-list-tracker"; export default DiscourseRoute.extend(OpenComposer, { queryParams: { @@ -46,6 +47,9 @@ export default DiscourseRoute.extend(OpenComposer, { didTransition() { this.controllerFor("discovery")._showFooter(); this.send("loadingComplete"); + + const model = this.controllerFor("discovery/topics").get("model"); + setTopicList(model); return false; }, diff --git a/app/assets/javascripts/discourse/app/routes/tags-show.js b/app/assets/javascripts/discourse/app/routes/tags-show.js index 74a7ad5efb..1b26424992 100644 --- a/app/assets/javascripts/discourse/app/routes/tags-show.js +++ b/app/assets/javascripts/discourse/app/routes/tags-show.js @@ -11,6 +11,7 @@ import PermissionType from "discourse/models/permission-type"; import Category from "discourse/models/category"; import FilterModeMixin from "discourse/mixins/filter-mode"; import { escapeExpression } from "discourse/lib/utilities"; +import { setTopicList } from "discourse/lib/topic-list-tracker"; export default DiscourseRoute.extend(FilterModeMixin, { navMode: "latest", @@ -99,6 +100,9 @@ export default DiscourseRoute.extend(FilterModeMixin, { staff: list.topic_list.tags[0].staff }); } + + setTopicList(list); + controller.setProperties({ list, canCreateTopic: list.get("can_create_topic"), diff --git a/app/assets/javascripts/discourse/app/routes/topic.js b/app/assets/javascripts/discourse/app/routes/topic.js index 6f7fd410c6..55c8a740d5 100644 --- a/app/assets/javascripts/discourse/app/routes/topic.js +++ b/app/assets/javascripts/discourse/app/routes/topic.js @@ -4,6 +4,7 @@ import { cancel, later, schedule } from "@ember/runloop"; import DiscourseRoute from "discourse/routes/discourse"; import DiscourseURL from "discourse/lib/url"; import { ID_CONSTRAINT } from "discourse/models/topic"; +import { setTopicId } from "discourse/lib/topic-list-tracker"; const SCROLL_DELAY = 500; @@ -200,7 +201,10 @@ const TopicRoute = DiscourseRoute.extend({ }, didTransition() { - this.controllerFor("topic")._showFooter(); + const controller = this.controllerFor("topic"); + controller._showFooter(); + const topicId = controller.get("model.id"); + setTopicId(topicId); return true; }, diff --git a/app/assets/javascripts/discourse/app/templates/modal/keyboard-shortcuts-help.hbs b/app/assets/javascripts/discourse/app/templates/modal/keyboard-shortcuts-help.hbs index 813ab3a1f6..514080379d 100644 --- a/app/assets/javascripts/discourse/app/templates/modal/keyboard-shortcuts-help.hbs +++ b/app/assets/javascripts/discourse/app/templates/modal/keyboard-shortcuts-help.hbs @@ -15,6 +15,8 @@
  • {{html-safe shortcuts.jump_to.messages}}
  • {{/if}}
  • {{html-safe shortcuts.jump_to.drafts}}
  • +
  • {{html-safe shortcuts.jump_to.next}}
  • +
  • {{html-safe shortcuts.jump_to.previous}}
  • diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index ee369171f7..c881feb750 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -3185,6 +3185,8 @@ en: profile: "%{shortcut} Profile" messages: "%{shortcut} Messages" drafts: "%{shortcut} Drafts" + next: "%{shortcut} Next Topic" + previous: "%{shortcut} Previous Topic" navigation: title: "Navigation" jump: "%{shortcut} Go to post #"