streaming is kinda working

This commit is contained in:
Nolan Lawson 2018-02-11 19:15:21 -08:00
parent 075066ba9a
commit 2a86425c90
9 changed files with 167 additions and 27 deletions

View file

@ -16,17 +16,13 @@ async function removeDuplicates (instanceName, timelineName, updates) {
} }
async function handleFreshChanges (instanceName, timelineName) { async function handleFreshChanges (instanceName, timelineName) {
console.log('handleFreshChanges')
let freshChanges = store.getForTimeline(instanceName, timelineName, 'freshChanges') let freshChanges = store.getForTimeline(instanceName, timelineName, 'freshChanges')
console.log('freshChanges', freshChanges)
if (freshChanges.updates && freshChanges.updates.length) { if (freshChanges.updates && freshChanges.updates.length) {
let updates = freshChanges.updates.slice() let updates = freshChanges.updates.slice()
freshChanges.updates = [] freshChanges.updates = []
store.setForTimeline(instanceName, timelineName, {freshChanges: freshChanges}) store.setForTimeline(instanceName, timelineName, {freshChanges: freshChanges})
console.log('before removing duplicates, updates are ', updates.length)
updates = await removeDuplicates(instanceName, timelineName, updates) updates = await removeDuplicates(instanceName, timelineName, updates)
console.log('after removing duplicates, updates are ', updates.length)
await database.insertTimelineItems(instanceName, timelineName, updates) await database.insertTimelineItems(instanceName, timelineName, updates)
@ -39,7 +35,6 @@ async function handleFreshChanges (instanceName, timelineName) {
} }
function handleStreamMessage (instanceName, timelineName, message) { function handleStreamMessage (instanceName, timelineName, message) {
console.log('handleStreamMessage')
let { event, payload } = message let { event, payload } = message
let key = event === 'update' ? 'updates' : 'deletes' let key = event === 'update' ? 'updates' : 'deletes'
let freshChanges = store.getForTimeline(instanceName, timelineName, 'freshChanges') || {} let freshChanges = store.getForTimeline(instanceName, timelineName, 'freshChanges') || {}
@ -54,7 +49,6 @@ function handleStreamMessage (instanceName, timelineName, message) {
export function createStream (streamingApi, instanceName, accessToken, timelineName) { export function createStream (streamingApi, instanceName, accessToken, timelineName) {
return new TimelineStream(streamingApi, accessToken, timelineName, { return new TimelineStream(streamingApi, accessToken, timelineName, {
onMessage (msg) { onMessage (msg) {
console.log('message', msg)
if (msg.event !== 'update' && msg.event !== 'delete') { if (msg.event !== 'update' && msg.event !== 'delete') {
console.error("don't know how to handle event", msg) console.error("don't know how to handle event", msg)
return return
@ -73,4 +67,4 @@ export function createStream (streamingApi, instanceName, accessToken, timelineN
console.log('reconnected stream for timeline', timelineName) console.log('reconnected stream for timeline', timelineName)
} }
}) })
} }

View file

@ -81,3 +81,15 @@ export async function fetchTimelineItemsOnScrollToBottom () {
await fetchTimelineItemsAndPossiblyFallBack() await fetchTimelineItemsAndPossiblyFallBack()
store.setForTimeline(instanceName, timelineName, { runningUpdate: false }) store.setForTimeline(instanceName, timelineName, { runningUpdate: false })
} }
export async function showMoreItemsForCurrentTimeline() {
let instanceName = store.get('currentInstance')
let timelineName = store.get('currentTimeline')
let itemIdsToAdd = store.get('itemIdsToAdd')
addTimelineItemIds(instanceName, timelineName, itemIdsToAdd)
store.setForTimeline(instanceName, timelineName, {
itemIdsToAdd: [],
shouldShowHeader: false,
showHeader: false
})
}

View file

@ -0,0 +1,25 @@
<div class="more-items-header" role="dialog">
<button class="primary" type="button" on:click="onClick(event)">
Click to show {{count}} more
</button>
</div>
<style>
.more-items-header {
display: flex;
padding: 5px;
align-items: center;
justify-content:center;
}
</style>
<script>
export default {
methods: {
onClick(event) {
let onClick = this.get('onClick')
if (onClick) {
onClick(event)
}
}
}
}
</script>

View file

@ -0,0 +1,11 @@
<MoreHeader count="{{virtualProps.count}}"
onClick="{{virtualProps.onClick}}"
/>
<script>
import MoreHeader from './MoreHeader.html'
export default {
components: {
MoreHeader
}
}
</script>

View file

@ -12,9 +12,13 @@
:makeProps :makeProps
items="{{$timelineItemIds}}" items="{{$timelineItemIds}}"
on:scrollToBottom="onScrollToBottom()" on:scrollToBottom="onScrollToBottom()"
on:scrollToTop="onScrollToTop()"
shown="{{$initialized}}" shown="{{$initialized}}"
footerComponent="{{LoadingFooter}}" footerComponent="{{LoadingFooter}}"
showFooter="{{$initialized && $runningUpdate}}" showFooter="{{$initialized && $runningUpdate}}"
showHeader="{{$showHeader}}"
headerComponent="{{MoreHeaderVirtualWrapper}}"
:headerProps
realm="{{$currentInstance + '/' + timeline}}" realm="{{$currentInstance + '/' + timeline}}"
on:initializedVisibleItems="initialize()" on:initializedVisibleItems="initialize()"
/> />
@ -23,8 +27,12 @@
:makeProps :makeProps
items="{{$timelineItemIds}}" items="{{$timelineItemIds}}"
on:scrollToBottom="onScrollToBottom()" on:scrollToBottom="onScrollToBottom()"
on:scrollToTop="onScrollToTop()"
shown="{{$initialized}}" shown="{{$initialized}}"
footerComponent="{{LoadingFooter}}" footerComponent="{{LoadingFooter}}"
showHeader="{{$showHeader}}"
headerComponent="{{MoreHeaderVirtualWrapper}}"
:headerProps
showFooter="{{$initialized && $runningUpdate}}" showFooter="{{$initialized && $runningUpdate}}"
realm="{{$currentInstance + '/' + timeline}}" realm="{{$currentInstance + '/' + timeline}}"
on:initializedVisibleItems="initialize()" on:initializedVisibleItems="initialize()"
@ -55,13 +63,16 @@
import Status from '../status/Status.html' import Status from '../status/Status.html'
import PseudoVirtualList from '../pseudoVirtualList/PseudoVirtualList.html' import PseudoVirtualList from '../pseudoVirtualList/PseudoVirtualList.html'
import LoadingFooter from './LoadingFooter.html' import LoadingFooter from './LoadingFooter.html'
import MoreHeaderVirtualWrapper from './MoreHeaderVirtualWrapper.html'
import VirtualList from '../virtualList/VirtualList.html' import VirtualList from '../virtualList/VirtualList.html'
import { timelines } from '../../_static/timelines' import { timelines } from '../../_static/timelines'
import { database } from '../../_database/database' import { database } from '../../_database/database'
import { initializeTimeline, fetchTimelineItemsOnScrollToBottom, setupTimeline } from '../../_actions/timeline' import { initializeTimeline, fetchTimelineItemsOnScrollToBottom, setupTimeline } from '../../_actions/timeline'
import LoadingPage from '../LoadingPage.html' import LoadingPage from '../LoadingPage.html'
import { focusWithCapture, blurWithCapture } from '../../_utils/events' import { focusWithCapture, blurWithCapture } from '../../_utils/events'
import { addTimelineItemIds } from '../../_actions/timeline' import { showMoreItemsForCurrentTimeline } from '../../_actions/timeline'
import { virtualListStore } from '../virtualList/virtualListStore' // TODO: hacky, need better way to expose scrollTop
import { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
export default { export default {
oncreate() { oncreate() {
@ -71,15 +82,7 @@
if (this.store.get('initialized')) { if (this.store.get('initialized')) {
this.restoreFocus() this.restoreFocus()
} }
let instanceName = this.store.get('currentInstance') this.setupStreaming()
let timelineName = this.get('timeline')
this.observe('itemIdsToAdd', itemIdsToAdd => {
console.log('itemIdsToAdd', itemIdsToAdd)
if (itemIdsToAdd && itemIdsToAdd.length) {
addTimelineItemIds(instanceName, timelineName, itemIdsToAdd)
this.store.setForTimeline(instanceName, timelineName, {itemIdsToAdd: []})
}
})
}, },
ondestroy() { ondestroy() {
console.log('ondestroy') console.log('ondestroy')
@ -89,6 +92,7 @@
StatusVirtualListItem, StatusVirtualListItem,
NotificationVirtualListItem, NotificationVirtualListItem,
LoadingFooter, LoadingFooter,
MoreHeaderVirtualWrapper,
Status Status
}), }),
computed: { computed: {
@ -139,6 +143,12 @@
$timelines[$currentInstance] && $timelines[$currentInstance] &&
$timelines[$currentInstance][timeline] && $timelines[$currentInstance][timeline] &&
$timelines[$currentInstance][timeline].itemIdsToAdd) || [] $timelines[$currentInstance][timeline].itemIdsToAdd) || []
},
headerProps: (itemIdsToAdd) => {
return {
count: (itemIdsToAdd && itemIdsToAdd.length) || 0,
onClick: showMoreItemsForCurrentTimeline
}
} }
}, },
store: () => store, store: () => store,
@ -168,6 +178,41 @@
} }
fetchTimelineItemsOnScrollToBottom() fetchTimelineItemsOnScrollToBottom()
}, },
onScrollToTop() {
if (this.store.get('shouldShowHeader')) {
this.store.setForCurrentTimeline({
showHeader: true,
shouldShowHeader: false
})
}
},
setupStreaming() {
let instanceName = this.store.get('currentInstance')
let timelineName = this.get('timeline')
let handleItemIdsToAdd = () => {
let itemIdsToAdd = this.get('itemIdsToAdd')
if (!itemIdsToAdd || !itemIdsToAdd.length) {
return
}
let scrollTop = virtualListStore.get('scrollTop')
let shouldShowHeader = this.store.get('shouldShowHeader')
let showHeader = this.store.get('showHeader')
//console.log('handleItemIdsToAdd', (itemIdsToAdd && itemIdsToAdd.length) || 0)
if (scrollTop === 0 && !shouldShowHeader && !showHeader) {
// if the user is scrolled to the top and we're not showing the header, then
// just insert the statuses. this is "chat room mode"
showMoreItemsForCurrentTimeline()
} else {
// user hasn't scrolled to the top, show a header instead
this.store.setForTimeline(instanceName, timelineName, {shouldShowHeader: true})
}
}
this.observe('itemIdsToAdd', itemIdsToAdd => {
if (itemIdsToAdd && itemIdsToAdd.length) {
scheduleIdleTask(handleItemIdsToAdd)
}
})
},
setupFocus() { setupFocus() {
this.onPushState = this.onPushState.bind(this) this.onPushState = this.onPushState.bind(this)
this.store.setForCurrentTimeline({ignoreBlurEvents: false}) this.store.setForCurrentTimeline({ignoreBlurEvents: false})

View file

@ -1,5 +1,5 @@
<!-- TODO: setting height is hacky, just make this element the scroller -->
<div class="virtual-list {{shown ? '' : 'hidden'}}" style="height: {{$height}}px;"> <div class="virtual-list {{shown ? '' : 'hidden'}}" style="height: {{$height}}px;">
<VirtualListHeader component="{{headerComponent}}" virtualProps="{{headerProps}}" shown="{{$showHeader}}"/>
{{#if $visibleItems}} {{#if $visibleItems}}
{{#each $visibleItems as visibleItem @key}} {{#each $visibleItems as visibleItem @key}}
<VirtualListLazyItem :component <VirtualListLazyItem :component
@ -23,25 +23,32 @@
<script> <script>
import VirtualListLazyItem from './VirtualListLazyItem' import VirtualListLazyItem from './VirtualListLazyItem'
import VirtualListFooter from './VirtualListFooter.html' import VirtualListFooter from './VirtualListFooter.html'
import VirtualListHeader from './VirtualListHeader.html'
import { virtualListStore } from './virtualListStore' import { virtualListStore } from './virtualListStore'
import throttle from 'lodash/throttle' import throttle from 'lodash/throttle'
import { mark, stop } from '../../_utils/marks' import { mark, stop } from '../../_utils/marks'
const DISTANCE_FROM_BOTTOM_TO_FIRE = 400 const DISTANCE_FROM_BOTTOM_TO_FIRE = 400
const SCROLL_TO_BOTTOM_DELAY = 1000 const SCROLL_EVENT_THROTTLE = 1000
export default { export default {
oncreate () { oncreate () {
this.observe('showFooter', showFooter => { this.observe('showFooter', showFooter => {
this.store.setForRealm({showFooter: showFooter}) this.store.setForRealm({showFooter: showFooter})
}) })
this.observe('showHeader', showHeader => {
this.store.setForRealm({showHeader: showHeader})
})
this.observe('items', (items) => { this.observe('items', (items) => {
mark('set items') mark('set items')
this.store.setForRealm({items: items}) this.store.setForRealm({items: items})
stop('set items') stop('set items')
this.fireScrollToBottom = throttle(() => { this.fireScrollToBottom = throttle(() => {
this.fire('scrollToBottom') this.fire('scrollToBottom')
}, SCROLL_TO_BOTTOM_DELAY) }, SCROLL_EVENT_THROTTLE)
this.fireScrollToTop = throttle(() => {
this.fire('scrollToTop')
}, SCROLL_EVENT_THROTTLE)
}) })
this.observe('allVisibleItemsHaveHeight', allVisibleItemsHaveHeight => { this.observe('allVisibleItemsHaveHeight', allVisibleItemsHaveHeight => {
@ -62,6 +69,12 @@
this.fireScrollToBottom() this.fireScrollToBottom()
} }
}) })
this.observe('distanceFromTop', (distanceFromTop) => {
if (distanceFromTop === 0) {
this.fireScrollToTop()
}
})
}, },
data: () => ({ data: () => ({
component: null component: null
@ -69,12 +82,14 @@
store: () => virtualListStore, store: () => virtualListStore,
components: { components: {
VirtualListLazyItem, VirtualListLazyItem,
VirtualListFooter VirtualListFooter,
VirtualListHeader
}, },
computed: { computed: {
distanceFromBottom: ($scrollHeight, $scrollTop, $offsetHeight) => { distanceFromBottom: ($scrollHeight, $scrollTop, $offsetHeight) => {
return $scrollHeight - $scrollTop - $offsetHeight return $scrollHeight - $scrollTop - $offsetHeight
}, },
distanceFromTop: ($scrollTop) => $scrollTop,
// TODO: bug in svelte store, shouldn't need to do this // TODO: bug in svelte store, shouldn't need to do this
allVisibleItemsHaveHeight: ($allVisibleItemsHaveHeight) => $allVisibleItemsHaveHeight allVisibleItemsHaveHeight: ($allVisibleItemsHaveHeight) => $allVisibleItemsHaveHeight
} }

View file

@ -0,0 +1,34 @@
<div class="virtual-list-header {{shown ? 'shown' : ''}}"
aria-hidden="{{!shown}}"
ref:node >
<:Component {component} :virtualProps />
</div>
<style>
.virtual-list-header {
position: absolute;
top: 0;
width: 100%;
opacity: 0;
pointer-events: none;
z-index: 10;
}
.virtual-list-header.shown {
opacity: 1;
pointer-events: auto;
}
</style>
<script>
import { virtualListStore } from './virtualListStore'
import { AsyncLayout } from '../../_utils/AsyncLayout'
export default {
oncreate() {
const asyncLayout = new AsyncLayout(() => '__header__')
asyncLayout.observe('__header__', this.refs.node, (rect) => {
asyncLayout.disconnect()
this.store.setForRealm({headerHeight: rect.height})
})
},
store: () => virtualListStore,
}
</script>

View file

@ -14,21 +14,23 @@ const virtualListStore = new VirtualListStore()
virtualListStore.computeForRealm('items', null) virtualListStore.computeForRealm('items', null)
virtualListStore.computeForRealm('showFooter', false) virtualListStore.computeForRealm('showFooter', false)
virtualListStore.computeForRealm('footerHeight', 0) virtualListStore.computeForRealm('footerHeight', 0)
virtualListStore.computeForRealm('showHeader', false)
virtualListStore.computeForRealm('headerHeight', 0)
virtualListStore.computeForRealm('scrollTop', 0) virtualListStore.computeForRealm('scrollTop', 0)
virtualListStore.computeForRealm('scrollHeight', 0) virtualListStore.computeForRealm('scrollHeight', 0)
virtualListStore.computeForRealm('offsetHeight', 0) virtualListStore.computeForRealm('offsetHeight', 0)
virtualListStore.computeForRealm('itemHeights', {}) virtualListStore.computeForRealm('itemHeights', {})
virtualListStore.compute('visibleItems', virtualListStore.compute('visibleItems',
['items', 'scrollTop', 'itemHeights', 'offsetHeight'], ['items', 'scrollTop', 'itemHeights', 'offsetHeight', 'showHeader', 'headerHeight'],
(items, scrollTop, itemHeights, offsetHeight) => { (items, scrollTop, itemHeights, offsetHeight, showHeader, headerHeight) => {
mark('compute visibleItems') mark('compute visibleItems')
if (!items) { if (!items) {
return null return null
} }
let renderBuffer = VIEWPORT_RENDER_FACTOR * offsetHeight let renderBuffer = VIEWPORT_RENDER_FACTOR * offsetHeight
let visibleItems = [] let visibleItems = []
let totalOffset = 0 let totalOffset = showHeader ? headerHeight : 0
let len = items.length let len = items.length
let i = -1 let i = -1
while (++i < len) { while (++i < len) {
@ -57,12 +59,12 @@ virtualListStore.compute('visibleItems',
}) })
virtualListStore.compute('heightWithoutFooter', virtualListStore.compute('heightWithoutFooter',
['items', 'itemHeights'], ['items', 'itemHeights', 'showHeader', 'headerHeight'],
(items, itemHeights) => { (items, itemHeights, showHeader, headerHeight) => {
if (!items) { if (!items) {
return 0 return 0
} }
let sum = 0 let sum = showHeader ? headerHeight : 0
let i = -1 let i = -1
let len = items.length let len = items.length
while (++i < len) { while (++i < len) {

View file

@ -15,6 +15,8 @@ export function timelineComputations (store) {
computeForTimeline(store, 'lastFocusedElementSelector') computeForTimeline(store, 'lastFocusedElementSelector')
computeForTimeline(store, 'ignoreBlurEvents') computeForTimeline(store, 'ignoreBlurEvents')
computeForTimeline(store, 'itemIdsToAdd') computeForTimeline(store, 'itemIdsToAdd')
computeForTimeline(store, 'showHeader')
computeForTimeline(store, 'shouldShowHeader')
store.compute('firstTimelineItemId', ['timelineItemIds'], (timelineItemIds) => timelineItemIds && timelineItemIds.length && timelineItemIds[0]) store.compute('firstTimelineItemId', ['timelineItemIds'], (timelineItemIds) => timelineItemIds && timelineItemIds.length && timelineItemIds[0])
store.compute('lastTimelineItemId', ['timelineItemIds'], (timelineItemIds) => timelineItemIds && timelineItemIds.length && timelineItemIds[timelineItemIds.length - 1]) store.compute('lastTimelineItemId', ['timelineItemIds'], (timelineItemIds) => timelineItemIds && timelineItemIds.length && timelineItemIds[timelineItemIds.length - 1])