Commit f30be9436ba56489c01de7d4c5dfef6669137201
WIP scroller with pull-scroll
mix irving committed on 11/19/2017, 4:19:52 AMParent: 0e154203de9454a9f8f6a7ecc08aa6f661819f6d
Files changed
app/html/context.js | changed |
app/html/scroller.js | added |
app/html/scroller.mcss | added |
app/index.js | changed |
package-lock.json | changed |
package.json | changed |
app/html/context.js | ||
---|---|---|
@@ -7,8 +7,9 @@ | ||
7 | 7 | |
8 | 8 | exports.gives = nest('app.html.context') |
9 | 9 | |
10 | 10 | exports.needs = nest({ |
11 | + 'app.html.scroller': 'first', | |
11 | 12 | 'about.html.avatar': 'first', |
12 | 13 | 'about.obs.name': 'first', |
13 | 14 | 'feed.pull.private': 'first', |
14 | 15 | 'feed.pull.rollup': 'first', |
@@ -26,26 +27,26 @@ | ||
26 | 27 | const strings = api.translations.sync.strings() |
27 | 28 | const myKey = api.keys.sync.id() |
28 | 29 | |
29 | 30 | var nearby = api.sbot.obs.localPeers() |
30 | - var recentPeersContacted = Dict() | |
31 | - // TODO - extract as contact.obs.recentPrivate or something | |
31 | + var recentMsgLog = Dict () | |
32 | + function updateRecentMsgLog (msg) { | |
33 | + const { author, timestamp } = msg.value | |
32 | 34 | |
33 | - pull( | |
34 | - next(api.feed.pull.private, {reverse: true, limit: 100, live: false}, ['value', 'timestamp']), | |
35 | - pull.filter(msg => msg.value.content.type === 'post'), // TODO is this the best way to protect against votes? | |
36 | - pull.filter(msg => msg.value.content.recps), | |
37 | - pull.drain(msg => { | |
38 | - msg.value.content.recps | |
39 | - .map(recp => typeof recp === 'object' ? recp.link : recp) | |
40 | - .filter(recp => recp != myKey) | |
41 | - .forEach(recp => { | |
42 | - if (recentPeersContacted.has(recp)) return | |
35 | + if (!recentMsgLog.has(author)) { | |
36 | + recentMsgLog.put(author, msg) | |
37 | + return | |
38 | + } | |
43 | 39 | |
44 | - recentPeersContacted.put(recp, msg) | |
45 | - }) | |
46 | - }) | |
47 | - ) | |
40 | + const currentWinner = recentMsgLog.get(author) | |
41 | + if (timestamp > currentWinner.value.timestamp) { | |
42 | + recentMsgLog.put(author, msg) | |
43 | + } | |
44 | + } | |
45 | + function isLatestMsg (msg) { | |
46 | + const { author, timestamp } = msg.value | |
47 | + return recentMsgLog.get(author).value.timestamp === timestamp | |
48 | + } | |
48 | 49 | |
49 | 50 | return h('Context -feed', [ |
50 | 51 | LevelOneContext(), |
51 | 52 | LevelTwoContext() |
@@ -58,24 +59,25 @@ | ||
58 | 59 | return PAGES_UNDER_DISCOVER.includes(location.page) |
59 | 60 | || get(location, 'value.private') === undefined |
60 | 61 | } |
61 | 62 | |
62 | - return h('div.level.-one', [ | |
63 | + const prepend = [ | |
63 | 64 | // Nearby |
64 | 65 | computed(nearby, n => !isEmpty(n) ? h('header', strings.peopleNearby) : null), |
65 | 66 | map(nearby, feedId => Option({ |
66 | 67 | notifications: Math.random() > 0.7 ? Math.floor(Math.random()*9+1) : 0, // TODO |
67 | 68 | imageEl: api.about.html.avatar(feedId, 'small'), |
68 | 69 | label: api.about.obs.name(feedId), |
69 | 70 | selected: location.feed === feedId, |
70 | - location: computed(recentPeersContacted, recent => { | |
71 | + location: computed(recentMsgLog, recent => { | |
71 | 72 | const lastMsg = recent[feedId] |
72 | 73 | return lastMsg |
73 | 74 | ? Object.assign(lastMsg, { feed: feedId }) |
74 | 75 | : { page: 'threadNew', feed: feedId } |
75 | 76 | }), |
76 | 77 | }), { comparer: (a, b) => a === b }), |
77 | 78 | |
79 | + // --------------------- | |
78 | 80 | computed(nearby, n => !isEmpty(n) ? h('hr') : null), |
79 | 81 | |
80 | 82 | // Discover |
81 | 83 | Option({ |
@@ -83,25 +85,38 @@ | ||
83 | 85 | imageEl: h('i.fa.fa-binoculars'), |
84 | 86 | label: strings.blogIndex.title, |
85 | 87 | selected: isDiscoverContext(location), |
86 | 88 | location: { page: 'blogIndex' }, |
87 | - }), | |
89 | + }) | |
90 | + ] | |
88 | 91 | |
89 | - // Recent Messages | |
90 | - map(dictToCollection(recentPeersContacted), ({ key, value }) => { | |
91 | - const feedId = key() | |
92 | - const lastMsg = value() | |
93 | - if (nearby.has(feedId)) return | |
92 | + const { scroller } = api.app.html.scroller({ | |
93 | + classList: [ 'level', '-one' ], | |
94 | + prepend, | |
95 | + streamBottom: api.feed.pull.private, | |
96 | + filter: pull( | |
97 | + pull.filter(msg => msg.value.content.type === 'post'), // TODO is this the best way to protect against votes? | |
98 | + pull.filter(msg => msg.value.author != myKey), | |
99 | + pull.filter(msg => msg.value.content.recps), | |
100 | + pull.through(updateRecentMsgLog), | |
101 | + pull.filter(isLatestMsg) | |
102 | + //pull.through( // trim exisiting from content up Top case) | |
103 | + ), | |
104 | + renderer: (msg) => { | |
105 | + const { author } = msg.value | |
106 | + if (nearby.has(author)) return | |
94 | 107 | |
95 | 108 | return Option({ |
96 | 109 | notifications: Math.random() > 0.7 ? Math.floor(Math.random()*9+1) : 0, // TODO |
97 | - imageEl: api.about.html.avatar(feedId), | |
98 | - label: api.about.obs.name(feedId), | |
99 | - selected: location.feed === feedId, | |
100 | - location: Object.assign({}, lastMsg, { feed: feedId }) // TODO make obs? | |
110 | + imageEl: api.about.html.avatar(author), | |
111 | + label: api.about.obs.name(author), | |
112 | + selected: location.feed === author, | |
113 | + location: Object.assign({}, msg, { feed: author }) // TODO make obs? | |
101 | 114 | }) |
102 | - }, { comparer: (a, b) => a === b }) | |
103 | - ]) | |
115 | + } | |
116 | + }) | |
117 | + | |
118 | + return scroller | |
104 | 119 | } |
105 | 120 | |
106 | 121 | function LevelTwoContext () { |
107 | 122 | const { key, value, feed: targetUser, page } = location |
app/html/scroller.js | ||
---|---|---|
@@ -1,0 +1,110 @@ | ||
1 | +const nest = require('depnest') | |
2 | +const { h } = require('mutant') | |
3 | +const pull = require('pull-stream') | |
4 | +const pullScroll = require('pull-scroll') | |
5 | +const next = require('pull-next-step') | |
6 | + | |
7 | +exports.gives = nest('app.html.scroller') | |
8 | + | |
9 | +exports.needs = nest({ | |
10 | + 'message.html.render': 'first' | |
11 | +}) | |
12 | + | |
13 | +exports.create = function (api) { | |
14 | + return nest('app.html.scroller', createScroller) | |
15 | + | |
16 | + function createScroller (opts = {}) { | |
17 | + const { | |
18 | + streamTop, | |
19 | + streamBottom, | |
20 | + filter = (msg) => true, | |
21 | + renderer, | |
22 | + classList = [], | |
23 | + content = h('section.content'), | |
24 | + prepend = [], | |
25 | + append = [] | |
26 | + } = opts | |
27 | + | |
28 | + if (!streamTop && !streamBottom) throw new Error('Scroller requires a at least one stream: streamTop || streamBottom') | |
29 | + if (!renderer) throw new Error('Scroller expects a renderer') | |
30 | + | |
31 | + const scroller = h('Scroller', { classList, style: { overflow: 'auto' } }, [ | |
32 | + h('div.wrapper', [ | |
33 | + h('header', prepend), | |
34 | + content, | |
35 | + h('footer', append) | |
36 | + ]) | |
37 | + ]) | |
38 | + // scroller.scroll = keyscroll(content) // used for e.g. reset | |
39 | + | |
40 | + draw() | |
41 | + | |
42 | + return { | |
43 | + scroller, | |
44 | + content, | |
45 | + // reset, | |
46 | + } | |
47 | + | |
48 | + function draw () { | |
49 | + reset() | |
50 | + | |
51 | + if (streamTop) { | |
52 | + pull( | |
53 | + next(streamTop, {old: false, limit: 100}, ['value', 'timestamp']), | |
54 | + filter, | |
55 | + // filterDownThrough(), | |
56 | + pullScroll(scroller, content, renderer, true, false) | |
57 | + ) | |
58 | + } | |
59 | + | |
60 | + if (streamBottom) { | |
61 | + pull( | |
62 | + next(streamBottom, {reverse: true, limit: 100, live: false}, ['value', 'timestamp']), | |
63 | + filter, | |
64 | + // filterUpThrough(), | |
65 | + pullScroll(scroller, content, renderer, false, false) | |
66 | + ) | |
67 | + } | |
68 | + } | |
69 | + | |
70 | + function reset () { | |
71 | + } | |
72 | + | |
73 | + } | |
74 | + | |
75 | +} | |
76 | + | |
77 | +function keyscroll (content) { | |
78 | + var curMsgEl | |
79 | + | |
80 | + if (!content) return () => {} | |
81 | + | |
82 | + content.addEventListener('click', onActivateChild, false) | |
83 | + content.addEventListener('focus', onActivateChild, true) | |
84 | + | |
85 | + function onActivateChild (ev) { | |
86 | + for (var el = ev.target; el; el = el.parentNode) { | |
87 | + if (el.parentNode === content) { | |
88 | + curMsgEl = el | |
89 | + return | |
90 | + } | |
91 | + } | |
92 | + } | |
93 | + | |
94 | + function selectChild (el) { | |
95 | + if (!el) { return } | |
96 | + | |
97 | + ;(el.scrollIntoViewIfNeeded || el.scrollIntoView).call(el) | |
98 | + el.focus() | |
99 | + curMsgEl = el | |
100 | + } | |
101 | + | |
102 | + return function scroll (d) { | |
103 | + selectChild((!curMsgEl || d === 'first') ? content.firstChild | |
104 | + : d < 0 ? curMsgEl.previousElementSibling || content.firstChild | |
105 | + : d > 0 ? curMsgEl.nextElementSibling || content.lastChild | |
106 | + : curMsgEl) | |
107 | + | |
108 | + return curMsgEl | |
109 | + } | |
110 | +} |
app/html/scroller.mcss | ||
---|---|---|
@@ -1,0 +1,36 @@ | ||
1 | +Scroller { | |
2 | + display: flex | |
3 | + flex-direction: column | |
4 | + | |
5 | + overflow: auto | |
6 | + width: 100% | |
7 | + height: 100% | |
8 | + min-height: 0px | |
9 | + | |
10 | + /* div.wrapper { */ | |
11 | + /* align-self: center */ | |
12 | + | |
13 | + /* flex: 1 1 */ | |
14 | + /* $threadWidth */ | |
15 | + /* padding-top: .5rem */ | |
16 | + | |
17 | + /* section.content { */ | |
18 | + /* div { */ | |
19 | + /* border-bottom: solid 1px gainsboro */ | |
20 | + /* } */ | |
21 | + /* } */ | |
22 | + /* } */ | |
23 | +} | |
24 | + | |
25 | +Scroller -errors { | |
26 | + div.wrapper { | |
27 | + width: initial | |
28 | + max-width: 100% | |
29 | + | |
30 | + section.content div { | |
31 | + border: none | |
32 | + } | |
33 | + } | |
34 | +} | |
35 | + | |
36 | + |
app/index.js | ||
---|---|---|
@@ -9,8 +9,9 @@ | ||
9 | 9 | header: require('./html/header'), |
10 | 10 | thread: require('./html/thread'), |
11 | 11 | link: require('./html/link'), |
12 | 12 | blogCard: require('./html/blogCard'), |
13 | + scroller: require('./html/scroller'), | |
13 | 14 | }, |
14 | 15 | page: { |
15 | 16 | blogIndex: require('./page/blogIndex'), |
16 | 17 | blogNew: require('./page/blogNew'), |
package-lock.json | ||
---|---|---|
The diff is too large to show. Use a local git client to view these changes. Old file size: 201691 bytes New file size: 202220 bytes |
Built with git-ssb-web