parent
84b20a8fc2
commit
f0af8178af
|
@ -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)
|
||||||
|
}
|
|
@ -124,7 +124,7 @@ export async function fetchTimelineItemsOnScrollToBottom (instanceName, timeline
|
||||||
|
|
||||||
export async function showMoreItemsForTimeline (instanceName, timelineName) {
|
export async function showMoreItemsForTimeline (instanceName, timelineName) {
|
||||||
mark('showMoreItemsForTimeline')
|
mark('showMoreItemsForTimeline')
|
||||||
let itemSummariesToAdd = store.getForTimeline(instanceName, timelineName, 'timelineItemSummariesToAdd')
|
let itemSummariesToAdd = store.getForTimeline(instanceName, timelineName, 'timelineItemSummariesToAdd') || []
|
||||||
itemSummariesToAdd = itemSummariesToAdd.sort(compareTimelineItemSummaries).reverse()
|
itemSummariesToAdd = itemSummariesToAdd.sort(compareTimelineItemSummaries).reverse()
|
||||||
addTimelineItemSummaries(instanceName, timelineName, itemSummariesToAdd, false)
|
addTimelineItemSummaries(instanceName, timelineName, itemSummariesToAdd, false)
|
||||||
store.setForTimeline(instanceName, timelineName, {
|
store.setForTimeline(instanceName, timelineName, {
|
||||||
|
@ -135,7 +135,7 @@ export async function showMoreItemsForTimeline (instanceName, timelineName) {
|
||||||
stop('showMoreItemsForTimeline')
|
stop('showMoreItemsForTimeline')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function showMoreItemsForCurrentTimeline () {
|
export function showMoreItemsForCurrentTimeline () {
|
||||||
let { currentInstance, currentTimeline } = store.get()
|
let { currentInstance, currentTimeline } = store.get()
|
||||||
return showMoreItemsForTimeline(
|
return showMoreItemsForTimeline(
|
||||||
currentInstance,
|
currentInstance,
|
||||||
|
|
|
@ -105,11 +105,10 @@
|
||||||
<script>
|
<script>
|
||||||
import NavItemIcon from './NavItemIcon.html'
|
import NavItemIcon from './NavItemIcon.html'
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import { smoothScroll } from '../_utils/smoothScroll'
|
|
||||||
import { on, emit } from '../_utils/eventBus'
|
import { on, emit } from '../_utils/eventBus'
|
||||||
import { mark, stop } from '../_utils/marks'
|
import { mark, stop } from '../_utils/marks'
|
||||||
import { doubleRAF } from '../_utils/doubleRAF'
|
import { doubleRAF } from '../_utils/doubleRAF'
|
||||||
import { getScrollContainer } from '../_utils/scrollContainer'
|
import { scrollToTop } from '../_utils/scrollToTop'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
oncreate () {
|
oncreate () {
|
||||||
|
@ -167,14 +166,10 @@
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let scroller = getScrollContainer()
|
if (scrollToTop(/* smooth */ true)) {
|
||||||
let { scrollTop } = scroller
|
e.preventDefault()
|
||||||
if (scrollTop === 0) {
|
e.stopPropagation()
|
||||||
return
|
|
||||||
}
|
}
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
smoothScroll(scroller, 0)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
|
|
@ -17,10 +17,13 @@
|
||||||
<li><kbd>Backspace</kbd> to go back, close dialogs</li>
|
<li><kbd>Backspace</kbd> to go back, close dialogs</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<h2>On an active toot</h2>
|
<h2>Timeline</h2>
|
||||||
<div class="hotkey-group">
|
<div class="hotkey-group">
|
||||||
<ul>
|
<ul>
|
||||||
<li><kbd>o</kbd> to open the thread</li>
|
<li><kbd>j</kbd> or <kbd>↓</kbd> to activate the next toot</li>
|
||||||
|
<li><kbd>k</kbd> or <kbd>↑</kbd> to activate the previous toot</li>
|
||||||
|
<li><kbd>.</kbd> to show more and scroll to top</li>
|
||||||
|
<li><kbd>o</kbd> to open</li>
|
||||||
<li><kbd>f</kbd> to favorite</li>
|
<li><kbd>f</kbd> to favorite</li>
|
||||||
<li><kbd>b</kbd> to boost</li>
|
<li><kbd>b</kbd> to boost</li>
|
||||||
<li><kbd>r</kbd> to reply</li>
|
<li><kbd>r</kbd> to reply</li>
|
||||||
|
@ -28,8 +31,6 @@
|
||||||
<li><kbd>p</kbd> to open the author's profile</li>
|
<li><kbd>p</kbd> to open the author's profile</li>
|
||||||
<li><kbd>x</kbd> to show or hide text behind content warning</li>
|
<li><kbd>x</kbd> to show or hide text behind content warning</li>
|
||||||
<li><kbd>y</kbd> to show or hide sensitive media</li>
|
<li><kbd>y</kbd> to show or hide sensitive media</li>
|
||||||
<li><kbd>j</kbd> or <kbd>↓</kbd> to activate the next toot</li>
|
|
||||||
<li><kbd>k</kbd> or <kbd>↑</kbd> to activate the previous toot</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<h2>Media</h2>
|
<h2>Media</h2>
|
||||||
|
|
|
@ -48,6 +48,7 @@
|
||||||
import { classname } from '../../_utils/classname'
|
import { classname } from '../../_utils/classname'
|
||||||
import { applyFocusStylesToParent } from '../../_utils/events'
|
import { applyFocusStylesToParent } from '../../_utils/events'
|
||||||
import noop from 'lodash-es/noop'
|
import noop from 'lodash-es/noop'
|
||||||
|
import { createStatusOrNotificationUuid } from '../../_utils/createStatusOrNotificationUuid'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -65,10 +66,10 @@
|
||||||
notificationId: ({ notification }) => notification.id,
|
notificationId: ({ notification }) => notification.id,
|
||||||
status: ({ notification }) => notification.status,
|
status: ({ notification }) => notification.status,
|
||||||
statusId: ({ status }) => status && status.id,
|
statusId: ({ status }) => status && status.id,
|
||||||
uuid: ({ $currentInstance, timelineType, timelineValue, notificationId, statusId }) => {
|
uuid: ({ $currentInstance, timelineType, timelineValue, notificationId, statusId }) => (
|
||||||
return `${$currentInstance}/${timelineType}/${timelineValue}/${notificationId}/${statusId || ''}`
|
createStatusOrNotificationUuid($currentInstance, timelineType, timelineValue, notificationId, statusId)
|
||||||
},
|
),
|
||||||
elementId: ({ uuid }) => `notification-${uuid}`,
|
elementId: ({ uuid }) => uuid,
|
||||||
shortcutScope: ({ elementId }) => elementId,
|
shortcutScope: ({ elementId }) => elementId,
|
||||||
ariaLabel: ({ status, account, $omitEmojiInDisplayNames }) => (
|
ariaLabel: ({ status, account, $omitEmojiInDisplayNames }) => (
|
||||||
!status && `${getAccountAccessibleName(account, $omitEmojiInDisplayNames)} followed you, @${account.acct}`
|
!status && `${getAccountAccessibleName(account, $omitEmojiInDisplayNames)} followed you, @${account.acct}`
|
||||||
|
|
|
@ -136,6 +136,7 @@
|
||||||
import { composeNewStatusMentioning } from '../../_actions/mention'
|
import { composeNewStatusMentioning } from '../../_actions/mention'
|
||||||
import { applyFocusStylesToParent } from '../../_utils/events'
|
import { applyFocusStylesToParent } from '../../_utils/events'
|
||||||
import noop from 'lodash-es/noop'
|
import noop from 'lodash-es/noop'
|
||||||
|
import { createStatusOrNotificationUuid } from '../../_utils/createStatusOrNotificationUuid'
|
||||||
|
|
||||||
const INPUT_TAGS = new Set(['a', 'button', 'input', 'textarea'])
|
const INPUT_TAGS = new Set(['a', 'button', 'input', 'textarea'])
|
||||||
const isUserInputElement = node => INPUT_TAGS.has(node.localName)
|
const isUserInputElement = node => INPUT_TAGS.has(node.localName)
|
||||||
|
@ -238,9 +239,9 @@
|
||||||
),
|
),
|
||||||
inReplyToId: ({ originalStatus }) => originalStatus.in_reply_to_id,
|
inReplyToId: ({ originalStatus }) => originalStatus.in_reply_to_id,
|
||||||
uuid: ({ $currentInstance, timelineType, timelineValue, notificationId, statusId }) => (
|
uuid: ({ $currentInstance, timelineType, timelineValue, notificationId, statusId }) => (
|
||||||
`${$currentInstance}/${timelineType}/${timelineValue}/${notificationId || ''}/${statusId}`
|
createStatusOrNotificationUuid($currentInstance, timelineType, timelineValue, notificationId, statusId)
|
||||||
),
|
),
|
||||||
elementId: ({ uuid }) => `status-${uuid}`,
|
elementId: ({ uuid }) => uuid,
|
||||||
shortcutScope: ({ elementId }) => elementId,
|
shortcutScope: ({ elementId }) => elementId,
|
||||||
isStatusInOwnThread: ({ timelineType, timelineValue, originalStatusId }) => (
|
isStatusInOwnThread: ({ timelineType, timelineValue, originalStatusId }) => (
|
||||||
(timelineType === 'status' || timelineType === 'reply') && timelineValue === originalStatusId
|
(timelineType === 'status' || timelineType === 'reply') && timelineValue === originalStatusId
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
<div>Error: component failed to load! Try reloading. {error}</div>
|
<div>Error: component failed to load! Try reloading. {error}</div>
|
||||||
{/await}
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
|
<Shortcut scope="global" key="." on:pressed="showMoreAndScrollToTop()" />
|
||||||
<ScrollListShortcuts />
|
<ScrollListShortcuts />
|
||||||
<script>
|
<script>
|
||||||
import { store } from '../../_store/store'
|
import { store } from '../../_store/store'
|
||||||
|
@ -35,6 +36,7 @@
|
||||||
import LoadingFooter from './LoadingFooter.html'
|
import LoadingFooter from './LoadingFooter.html'
|
||||||
import MoreHeaderVirtualWrapper from './MoreHeaderVirtualWrapper.html'
|
import MoreHeaderVirtualWrapper from './MoreHeaderVirtualWrapper.html'
|
||||||
import ScrollListShortcuts from '../shortcut/ScrollListShortcuts.html'
|
import ScrollListShortcuts from '../shortcut/ScrollListShortcuts.html'
|
||||||
|
import Shortcut from '../shortcut/Shortcut.html'
|
||||||
import {
|
import {
|
||||||
importVirtualList,
|
importVirtualList,
|
||||||
importList,
|
importList,
|
||||||
|
@ -56,6 +58,7 @@
|
||||||
import { doubleRAF } from '../../_utils/doubleRAF'
|
import { doubleRAF } from '../../_utils/doubleRAF'
|
||||||
import { observe } from 'svelte-extras'
|
import { observe } from 'svelte-extras'
|
||||||
import { createMakeProps } from '../../_actions/createMakeProps'
|
import { createMakeProps } from '../../_actions/createMakeProps'
|
||||||
|
import { showMoreAndScrollToTop } from '../../_actions/showMoreAndScrollToTop'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
oncreate () {
|
oncreate () {
|
||||||
|
@ -114,12 +117,8 @@
|
||||||
return `Notifications on ${$currentInstance}`
|
return `Notifications on ${$currentInstance}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
timelineType: ({ timeline }) => {
|
timelineType: ({ $currentTimelineType }) => $currentTimelineType,
|
||||||
return timeline.split('/')[0]
|
timelineValue: ({ $currentTimelineValue }) => $currentTimelineValue,
|
||||||
},
|
|
||||||
timelineValue: ({ timeline }) => {
|
|
||||||
return timeline.split('/').slice(-1)[0]
|
|
||||||
},
|
|
||||||
// Scroll to the first item if this is a "status in own thread" timeline.
|
// Scroll to the first item if this is a "status in own thread" timeline.
|
||||||
// Don't scroll to the first item because it obscures the "back" button.
|
// Don't scroll to the first item because it obscures the "back" button.
|
||||||
scrollToItem: ({ timelineType, timelineValue, $firstTimelineItemId }) => (
|
scrollToItem: ({ timelineType, timelineValue, $firstTimelineItemId }) => (
|
||||||
|
@ -293,10 +292,12 @@
|
||||||
// where the scrollable content appears to jump around if we need to scroll it.
|
// where the scrollable content appears to jump around if we need to scroll it.
|
||||||
console.log('timeline preinitialized')
|
console.log('timeline preinitialized')
|
||||||
this.store.set({ timelinePreinitialized: true })
|
this.store.set({ timelinePreinitialized: true })
|
||||||
}
|
},
|
||||||
|
showMoreAndScrollToTop
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
ScrollListShortcuts
|
ScrollListShortcuts,
|
||||||
|
Shortcut
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -20,6 +20,12 @@ export function timelineComputations (store) {
|
||||||
computeForTimeline(store, 'shouldShowHeader', false)
|
computeForTimeline(store, 'shouldShowHeader', false)
|
||||||
computeForTimeline(store, 'timelineItemSummariesAreStale', 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) => (
|
store.compute('firstTimelineItemId', ['timelineItemSummaries'], (timelineItemSummaries) => (
|
||||||
getFirstIdFromItemSummaries(timelineItemSummaries)
|
getFirstIdFromItemSummaries(timelineItemSummaries)
|
||||||
))
|
))
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function createStatusOrNotificationUuid (currentInstance, timelineType, timelineValue, notificationId, statusId) {
|
||||||
|
return `${currentInstance}/${timelineType}/${timelineValue}/${notificationId || ''}/${statusId || ''}`
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
import {
|
import {
|
||||||
getUrl,
|
getNthStatus,
|
||||||
|
getUrl, isNthStatusActive,
|
||||||
modalDialogContents,
|
modalDialogContents,
|
||||||
notificationsNavButton } from '../utils'
|
notificationsNavButton, scrollToStatus
|
||||||
|
} from '../utils'
|
||||||
import { loginAsFoobar } from '../roles'
|
import { loginAsFoobar } from '../roles'
|
||||||
|
|
||||||
fixture`024-shortcuts-navigation.js`
|
fixture`024-shortcuts-navigation.js`
|
||||||
|
@ -109,3 +111,14 @@ test('Shortcut 6 goes to the settings', async t => {
|
||||||
.pressKey('6')
|
.pressKey('6')
|
||||||
.expect(getUrl()).contains('/settings')
|
.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()
|
||||||
|
})
|
||||||
|
|
Loading…
Reference in New Issue