From f0af8178af0be73625f4b9ddf77c7dedafeb9965 Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Mon, 18 Mar 2019 09:09:24 -0700 Subject: [PATCH] feat: implement "." keyboard shortcut (#1105) fixes #1052 --- src/routes/_actions/showMoreAndScrollToTop.js | 48 +++++++++++++++++++ src/routes/_actions/timeline.js | 4 +- src/routes/_components/NavItem.html | 13 ++--- src/routes/_components/ShortcutHelpInfo.html | 9 ++-- .../_components/status/Notification.html | 9 ++-- src/routes/_components/status/Status.html | 5 +- src/routes/_components/timeline/Timeline.html | 17 +++---- .../computations/timelineComputations.js | 6 +++ .../_utils/createStatusOrNotificationUuid.js | 3 ++ src/routes/_utils/scrollToTop.js | 16 +++++++ tests/spec/024-shortcuts-navigation.js | 17 ++++++- 11 files changed, 116 insertions(+), 31 deletions(-) create mode 100644 src/routes/_actions/showMoreAndScrollToTop.js create mode 100644 src/routes/_utils/createStatusOrNotificationUuid.js create mode 100644 src/routes/_utils/scrollToTop.js diff --git a/src/routes/_actions/showMoreAndScrollToTop.js b/src/routes/_actions/showMoreAndScrollToTop.js new file mode 100644 index 00000000..6d0ed67d --- /dev/null +++ b/src/routes/_actions/showMoreAndScrollToTop.js @@ -0,0 +1,48 @@ +import { showMoreItemsForCurrentTimeline } from './timeline' +import { scrollToTop } from '../_utils/scrollToTop' +import { scheduleIdleTask } from '../_utils/scheduleIdleTask' +import { createStatusOrNotificationUuid } from '../_utils/createStatusOrNotificationUuid' +import { store } from '../_store/store' + +const RETRIES = 5 +const TIMEOUT = 50 + +export function showMoreAndScrollToTop () { + // Similar to Twitter, pressing "." will click the "show more" button and select + // the first toot. + showMoreItemsForCurrentTimeline() + let { + currentInstance, + timelineItemSummaries, + currentTimelineType, + currentTimelineValue + } = store.get() + let firstItemSummary = timelineItemSummaries && timelineItemSummaries[0] + if (!firstItemSummary) { + return + } + let notificationId = currentTimelineType === 'notifications' && firstItemSummary.id + let statusId = currentTimelineType !== 'notifications' && firstItemSummary.id + scrollToTop(/* smooth */ false) + // try 5 times to wait for the element to be rendered and then focus it + let count = 0 + const tryToFocusElement = () => { + let uuid = createStatusOrNotificationUuid( + currentInstance, currentTimelineType, + currentTimelineValue, notificationId, statusId + ) + let element = document.getElementById(uuid) + if (element) { + try { + element.focus({ preventScroll: true }) + } catch (e) { + console.error(e) + } + } else { + if (++count <= RETRIES) { + setTimeout(() => scheduleIdleTask(tryToFocusElement), TIMEOUT) + } + } + } + scheduleIdleTask(tryToFocusElement) +} diff --git a/src/routes/_actions/timeline.js b/src/routes/_actions/timeline.js index 61f1d82f..f0274f52 100644 --- a/src/routes/_actions/timeline.js +++ b/src/routes/_actions/timeline.js @@ -124,7 +124,7 @@ export async function fetchTimelineItemsOnScrollToBottom (instanceName, timeline export async function showMoreItemsForTimeline (instanceName, timelineName) { mark('showMoreItemsForTimeline') - let itemSummariesToAdd = store.getForTimeline(instanceName, timelineName, 'timelineItemSummariesToAdd') + let itemSummariesToAdd = store.getForTimeline(instanceName, timelineName, 'timelineItemSummariesToAdd') || [] itemSummariesToAdd = itemSummariesToAdd.sort(compareTimelineItemSummaries).reverse() addTimelineItemSummaries(instanceName, timelineName, itemSummariesToAdd, false) store.setForTimeline(instanceName, timelineName, { @@ -135,7 +135,7 @@ export async function showMoreItemsForTimeline (instanceName, timelineName) { stop('showMoreItemsForTimeline') } -export async function showMoreItemsForCurrentTimeline () { +export function showMoreItemsForCurrentTimeline () { let { currentInstance, currentTimeline } = store.get() return showMoreItemsForTimeline( currentInstance, diff --git a/src/routes/_components/NavItem.html b/src/routes/_components/NavItem.html index e1c93cc6..066f92d2 100644 --- a/src/routes/_components/NavItem.html +++ b/src/routes/_components/NavItem.html @@ -105,11 +105,10 @@ diff --git a/src/routes/_store/computations/timelineComputations.js b/src/routes/_store/computations/timelineComputations.js index fea2af9c..64a11f18 100644 --- a/src/routes/_store/computations/timelineComputations.js +++ b/src/routes/_store/computations/timelineComputations.js @@ -20,6 +20,12 @@ export function timelineComputations (store) { computeForTimeline(store, 'shouldShowHeader', false) computeForTimeline(store, 'timelineItemSummariesAreStale', false) + store.compute('currentTimelineType', ['currentTimeline'], currentTimeline => ( + currentTimeline && currentTimeline.split('/')[0]) + ) + store.compute('currentTimelineValue', ['currentTimeline'], currentTimeline => ( + currentTimeline && currentTimeline.split('/').slice(-1)[0]) + ) store.compute('firstTimelineItemId', ['timelineItemSummaries'], (timelineItemSummaries) => ( getFirstIdFromItemSummaries(timelineItemSummaries) )) diff --git a/src/routes/_utils/createStatusOrNotificationUuid.js b/src/routes/_utils/createStatusOrNotificationUuid.js new file mode 100644 index 00000000..68d0143d --- /dev/null +++ b/src/routes/_utils/createStatusOrNotificationUuid.js @@ -0,0 +1,3 @@ +export function createStatusOrNotificationUuid (currentInstance, timelineType, timelineValue, notificationId, statusId) { + return `${currentInstance}/${timelineType}/${timelineValue}/${notificationId || ''}/${statusId || ''}` +} diff --git a/src/routes/_utils/scrollToTop.js b/src/routes/_utils/scrollToTop.js new file mode 100644 index 00000000..6f21a98e --- /dev/null +++ b/src/routes/_utils/scrollToTop.js @@ -0,0 +1,16 @@ +import { getScrollContainer } from './scrollContainer' +import { smoothScroll } from './smoothScroll' + +export function scrollToTop (smooth) { + let scroller = getScrollContainer() + let { scrollTop } = scroller + if (scrollTop === 0) { + return false + } + if (smooth) { + smoothScroll(scroller, 0) + } else { + scroller.scrollTop = 0 + } + return true +} diff --git a/tests/spec/024-shortcuts-navigation.js b/tests/spec/024-shortcuts-navigation.js index aab9adab..37c62b84 100644 --- a/tests/spec/024-shortcuts-navigation.js +++ b/tests/spec/024-shortcuts-navigation.js @@ -1,7 +1,9 @@ import { - getUrl, + getNthStatus, + getUrl, isNthStatusActive, modalDialogContents, - notificationsNavButton } from '../utils' + notificationsNavButton, scrollToStatus +} from '../utils' import { loginAsFoobar } from '../roles' fixture`024-shortcuts-navigation.js` @@ -109,3 +111,14 @@ test('Shortcut 6 goes to the settings', async t => { .pressKey('6') .expect(getUrl()).contains('/settings') }) + +test('Shortcut . scrolls to top and focuses', async t => { + await loginAsFoobar(t) + await t + .expect(getUrl()).eql('http://localhost:4002/') + .hover(getNthStatus(1)) + await scrollToStatus(t, 10) + await t + .pressKey('.') + .expect(isNthStatusActive(1)).ok() +})