git ssb

16+

Dominic / patchbay



Commit 2ad00a30b030cc01724cbb7998058c8880da46c5

/posts - basic working rollups

mix irving committed on 7/1/2018, 9:40:59 PM
Parent: dc06cb1de81eb3375ac9fa5e35df122d752f81d8

Files changed

app/page/posts.jsadded
app/page/posts.mcssadded
package-lock.jsonchanged
package.jsonchanged
router/sync/routes.jschanged
app/page/posts.jsView
@@ -1,0 +1,251 @@
1 +const nest = require('depnest')
2 +const { h, Value, Array: MutantArray, Struct, computed, when, map } = require('mutant')
3 +const pull = require('pull-stream')
4 +const Scroller = require('mutant-scroll')
5 +const next = require('pull-next-query')
6 +const merge = require('lodash/merge')
7 +const get = require('lodash/get')
8 +const sort = require('ssb-sort')
9 +
10 +exports.gives = nest({
11 + 'app.html.menuItem': true,
12 + 'app.page.posts': true
13 +})
14 +
15 +exports.needs = nest({
16 + 'about.obs.name': 'first',
17 + 'about.html.avatar': 'first',
18 + 'about.html.link': 'first',
19 + 'app.sync.goTo': 'first',
20 + // 'feed.pull.public': 'first',
21 + 'sbot.async.get': 'first',
22 + 'sbot.pull.stream': 'first',
23 + 'message.html.compose': 'first',
24 + 'message.html.markdown': 'first',
25 + 'message.html.timestamp': 'first',
26 + 'message.obs.backlinks': 'first'
27 +})
28 +
29 +exports.create = function (api) {
30 + return nest({
31 + 'app.html.menuItem': menuItem,
32 + 'app.page.posts': postsPage
33 + })
34 +
35 + function menuItem () {
36 + return h('a', {
37 + style: { order: 1 },
38 + 'ev-click': () => api.app.sync.goTo({ page: 'posts' })
39 + }, '/posts')
40 + }
41 +
42 + function postsPage (location) {
43 + const BY_UPDATE = 'by_update'
44 + const BY_ROOT = 'by_root'
45 +
46 + const composer = api.message.html.compose({
47 + location,
48 + meta: { type: 'post' },
49 + placeholder: 'Write a public message'
50 + })
51 +
52 + const store = MutantArray([])
53 +
54 + const page = Scroller({
55 + classList: ['Posts'],
56 + prepend: [
57 + composer
58 + ],
59 + streamToTop: createStream({ live: true, old: false }),
60 + streamToBottom: createStream({ reverse: true }),
61 + store,
62 + updateTop: (soFar, msg) => {
63 + const root = getRoot(msg)
64 + if (soFar.includes(root)) soFar.delete(root)
65 + soFar.insert(root)
66 + },
67 + updateBottom: (soFar, msg) => {
68 + const root = getRoot(msg)
69 + if (!soFar.includes(root)) soFar.push(root)
70 + },
71 + render
72 + })
73 +
74 + function createStream (opts) {
75 + return api.sbot.pull.stream(server => {
76 + // by_update - stream by receive time
77 + const defaults = {
78 + limit: 50,
79 + query: [{
80 + $filter: {
81 + timestamp: { $gt: 0 },
82 + value: {
83 + content: {
84 + type: 'post',
85 + recps: { $is: 'undefined' }
86 + }
87 + }
88 + }
89 + }]
90 + }
91 + return next(server.query.read, merge({}, defaults, opts), ['timestamp'])
92 + })
93 + }
94 +
95 + page.title = '/posts'
96 + page.scroll = keyscroll(page.querySelector('section.content'))
97 + return page
98 + }
99 +
100 + // TODO - move out into message.html.render ?
101 + function render (key) {
102 + const root = Struct({
103 + avatar: '',
104 + author: '',
105 + timestamp: '',
106 + md: ''
107 + })
108 + api.sbot.async.get(key, (err, value) => {
109 + if (err) console.error('ThreadCard could not fetch ', key)
110 + root.avatar.set(api.about.html.avatar(value.author))
111 + root.author.set(api.about.html.link(value.author))
112 + root.timestamp.set(api.message.html.timestamp({ key, value }))
113 + root.md.set(api.message.html.markdown(value.content))
114 + })
115 +
116 + const repliesCount = Value()
117 + const recent = MutantArray([])
118 + const likesCount = Value()
119 + const backlinksCount = Value()
120 + const participants = MutantArray([])
121 +
122 + const opts = {
123 + query: [{
124 + $filter: { dest: key }
125 + }],
126 + index: 'DTA' // asserted timestamp
127 + }
128 + pull(
129 + api.sbot.pull.stream(server => server.backlinks.read(opts)),
130 + pull.collect((err, msgs) => {
131 + if (err) console.error(err)
132 +
133 + msgs = sort(msgs)
134 +
135 + const replies = msgs
136 + .filter(isPost)
137 + .filter(m => getRoot(m) === key)
138 +
139 + repliesCount.set(replies.length)
140 + recent.set(lastFew(replies))
141 +
142 + const likes = msgs.filter(isLikeOf(key))
143 + likesCount.set(likes.length)
144 +
145 + const backlinks = msgs
146 + .filter(isPost)
147 + .filter(m => getRoot(m) !== key)
148 + backlinksCount.set(backlinks.length)
149 +
150 + const authors = replies
151 + .map(m => m.value.author)
152 + participants.set(Array.from(new Set(authors)))
153 + })
154 + )
155 +
156 + const className = computed(root.md, r => r ? '' : '-loading')
157 + const onClick = ev => {
158 + ev.preventDefault()
159 + ev.stopPropagation()
160 + api.app.sync.goTo(key)
161 + }
162 +
163 + return h('ThreadCard',
164 + {
165 + className,
166 + attributes: { tabindex: '0' } // needed to be able to navigate and show focus()
167 + }, [
168 + h('section.context', [
169 + h('div.avatar', root.avatar),
170 + h('div.name', root.author),
171 + h('div.timestamp', root.timestamp),
172 + h('div.counts', [
173 + h('div.comments', [ repliesCount, h('i.fa.fa-comment-o') ]),
174 + h('div.likes', [ likesCount, h('i.fa.fa-heart-o') ]),
175 + h('div.backlinks', [ backlinksCount, h('i.fa.fa-link') ])
176 + ]),
177 + h('div.participants', map(participants, api.about.html.avatar))
178 + ]),
179 + h('section.content-preview', { 'ev-click': onClick }, [
180 + h('div.root', root.md),
181 + h('div.recent', map(recent, msg => {
182 + return h('div.msg', [
183 + h('div.author', api.about.obs.name(msg.value.author)),
184 + ': ',
185 + h('div.preview', [
186 + api.message.html.markdown(msg.value.content).innerText.slice(0, 120),
187 + '...'
188 + ])
189 + ])
190 + }))
191 + ])
192 + ])
193 + }
194 +}
195 +
196 +function getRoot (msg) {
197 + return get(msg, 'value.content.root', msg.key)
198 +}
199 +
200 +function isPost (msg) {
201 + return get(msg, 'value.content.type') === 'post'
202 +}
203 +
204 +function isLikeOf (key) {
205 + return function (msg) {
206 + return get(msg, 'value.content.type') === 'vote' &&
207 + get(msg, 'value.content.vote.link') === key
208 + }
209 +}
210 +
211 +function lastFew (arr) {
212 + return arr.reverse().slice(0, 3).reverse()
213 +}
214 +
215 +// copied from app.html.scroller
216 +function keyscroll (content) {
217 + var curMsgEl
218 +
219 + if (!content) return () => {}
220 +
221 + content.addEventListener('click', onActivateChild, false)
222 + content.addEventListener('focus', onActivateChild, true)
223 +
224 + function onActivateChild (ev) {
225 + for (var el = ev.target; el; el = el.parentNode) {
226 + if (el.parentNode === content) {
227 + curMsgEl = el
228 + return
229 + }
230 + }
231 + }
232 +
233 + return function scroll (d) {
234 + selectChild((!curMsgEl || d === 'first') ? content.firstChild
235 + : d < 0 ? curMsgEl.previousElementSibling || content.firstChild
236 + : d > 0 ? curMsgEl.nextElementSibling || content.lastChild
237 + : curMsgEl)
238 +
239 + return curMsgEl
240 + }
241 +
242 + function selectChild (el) {
243 + if (!el) { return }
244 +
245 + if (!el.scrollIntoViewIfNeeded && !el.scrollIntoView) return
246 + ;(el.scrollIntoViewIfNeeded || el.scrollIntoView).call(el)
247 + el.focus()
248 + curMsgEl = el
249 + }
250 +}
251 +
app/page/posts.mcssView
@@ -1,0 +1,109 @@
1 +Posts {
2 + grid-template-columns: auto minmax(800px, 1200px) 1rem auto
3 +
4 + section.content {
5 + div.ThreadCard {
6 + border: none
7 + margin-top: 3rem
8 + }
9 + }
10 +}
11 +
12 +ThreadCard {
13 + -loading {
14 + min-height: 20rem
15 + }
16 +
17 + display: grid
18 + grid-template-columns: 8rem 1fr
19 + grid-gap: 2rem
20 +
21 + section.context {
22 + display: grid
23 + grid-gap: 1rem
24 + align-content: start
25 + justify-items: end
26 +
27 + div.avatar {
28 + height: 4rem
29 + width: 4rem
30 + a {
31 + height: 4rem
32 + width: 4rem
33 + img {
34 + height: 4rem
35 + width: 4rem
36 + }
37 + }
38 + }
39 + div.name {
40 + a {
41 + color: #222
42 + font-weight: 600
43 + text-decoration: none
44 + }
45 + }
46 + div.counts {
47 + display: grid
48 + grid-template-columns: auto auto auto
49 + grid-gap: 1rem
50 +
51 + div {
52 + display: flex
53 + align-items: center
54 + i { margin-left: .3rem }
55 + }
56 + }
57 +
58 + div.participants {
59 + display: flex
60 + justify-content: flex-end
61 + flex-wrap: wrap
62 +
63 + a {
64 + height: 2rem
65 + width: 2rem
66 + margin: 0 0 .5rem .5rem
67 + img {
68 + height: 2rem
69 + width: 2rem
70 + }
71 + }
72 + }
73 + }
74 +
75 + section.content-preview {
76 + cursor: pointer
77 +
78 + div.root {
79 + div.Markdown {
80 + background-color: #000
81 + color: #fff
82 + line-height: 1.4rem
83 + padding: 2rem
84 +
85 + max-height: 20rem
86 + overflow: hidden
87 +
88 + p {
89 + :first-of-type { margin-top: 0 }
90 + }
91 + (img) { max-width: 100% }
92 + }
93 + }
94 +
95 + div.recent {
96 + div.msg {
97 + margin: 1rem 2rem 0 2rem
98 + display: flex
99 +
100 + :last-child { margin-bottom: .5rem }
101 +
102 + div.author {
103 + font-weight: 600
104 + }
105 + div.preview { margin-left: .5rem }
106 + }
107 + }
108 + }
109 +}
package-lock.jsonView
The diff is too large to show. Use a local git client to view these changes.
Old file size: 359889 bytes
New file size: 348561 bytes
package.jsonView
@@ -73,9 +73,9 @@
7373 "pull-stream": "^3.6.8",
7474 "read-directory": "^2.1.1",
7575 "require-style": "^1.0.1",
7676 "scuttle-blog": "^1.0.1",
77- "scuttlebot": "^11.3.0",
77 + "scuttlebot": "^11.3.3",
7878 "setimmediate": "^1.0.5",
7979 "ssb-about": "^0.1.2",
8080 "ssb-backlinks": "^0.7.1",
8181 "ssb-blobs": "^1.1.3",
@@ -90,9 +90,10 @@
9090 "ssb-mentions": "^0.4.1",
9191 "ssb-mutual": "^0.1.0",
9292 "ssb-private": "^0.2.1",
9393 "ssb-query": "^2.1.0",
94- "ssb-search": "^1.0.1",
94 + "ssb-search": "^1.1.1",
95 + "ssb-sort": "^1.1.0",
9596 "ssb-ws": "^1.0.3",
9697 "style-resolve": "^1.0.1",
9798 "suggest-box": "^2.2.3",
9899 "text-node-searcher": "^1.1.1",
router/sync/routes.jsView
@@ -5,8 +5,9 @@
55
66 exports.needs = nest({
77 'app.page': {
88 'errors': 'first',
9 + 'posts': 'first',
910 'public': 'first',
1011 'private': 'first',
1112 'notifications': 'first',
1213 'profile': 'first',
@@ -27,8 +28,9 @@
2728 const pages = api.app.page
2829
2930 // loc = location
3031 const routes = [
32 + [ loc => loc.page === 'posts', pages.posts ],
3133 [ loc => loc.page === 'public', pages.public ],
3234 [ loc => loc.page === 'private', pages.private ],
3335 [ loc => loc.page === 'notifications', pages.notifications ],
3436 [ loc => loc.page === 'errors', pages.errors ],

Built with git-ssb-web