git ssb

16+

Dominic / patchbay



Tree: 920f29c78bb0076a529cda4bbc4f01b19cabd924

Files: 920f29c78bb0076a529cda4bbc4f01b19cabd924 / app / page / posts.js

7154 bytesRaw
1const nest = require('depnest')
2const { h, Value, Array: MutantArray, Struct, computed, when, map } = require('mutant')
3const pull = require('pull-stream')
4const Scroller = require('mutant-scroll')
5const next = require('pull-next-query')
6const merge = require('lodash/merge')
7const get = require('lodash/get')
8const sort = require('ssb-sort')
9
10exports.gives = nest({
11 'app.html.menuItem': true,
12 'app.page.posts': true
13})
14
15exports.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
29exports.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 state = Value(BY_UPDATE)
47 const viewSettings = h('section.viewSettings', [
48 h('button', 'hey!')
49 ])
50
51 return computed(state, state => {
52 var page = byUpdatePage()
53
54 page.title = '/posts'
55 page.scroll = keyscroll(page.querySelector('section.content'))
56 return page
57 })
58
59 function byUpdatePage () {
60 return Scroller({
61 classList: ['Posts'],
62 prepend: [
63 viewSettings,
64 Composer(location)
65 ],
66 streamToTop: createStream({ live: true, old: false }),
67 streamToBottom: createStream({ reverse: true }),
68 updateTop: (soFar, msg) => {
69 const root = getRoot(msg)
70 if (soFar.includes(root)) soFar.delete(root)
71 soFar.insert(root)
72 },
73 updateBottom: (soFar, msg) => {
74 const root = getRoot(msg)
75 if (!soFar.includes(root)) soFar.push(root)
76 },
77 render
78 })
79
80 function createStream (opts) {
81 return api.sbot.pull.stream(server => {
82 // by_update - stream by receive time
83 const defaults = {
84 limit: 50,
85 query: [{
86 $filter: {
87 timestamp: { $gt: 0 },
88 value: {
89 content: {
90 type: 'post',
91 recps: { $not: true }
92 }
93 }
94 }
95 }]
96 }
97 return next(server.query.read, merge({}, defaults, opts), ['timestamp'])
98 })
99 }
100 }
101 }
102
103 function Composer (location) {
104 return api.message.html.compose({
105 location,
106 meta: { type: 'post' },
107 placeholder: 'Write a public message'
108 })
109 }
110
111 // TODO - move out into message.html.render ?
112 function render (key) {
113 const root = Struct({
114 avatar: '',
115 author: '',
116 timestamp: '',
117 md: ''
118 })
119 api.sbot.async.get(key, (err, value) => {
120 if (err) console.error('ThreadCard could not fetch ', key)
121 root.avatar.set(api.about.html.avatar(value.author))
122 root.author.set(api.about.html.link(value.author))
123 root.timestamp.set(api.message.html.timestamp({ key, value }))
124 root.md.set(api.message.html.markdown(value.content))
125 })
126
127 const repliesCount = Value()
128 const recent = MutantArray([])
129 const likesCount = Value()
130 const backlinksCount = Value()
131 const participants = MutantArray([])
132
133 const opts = {
134 query: [{
135 $filter: { dest: key }
136 }],
137 index: 'DTA' // asserted timestamp
138 }
139 pull(
140 api.sbot.pull.stream(server => server.backlinks.read(opts)),
141 pull.collect((err, msgs) => {
142 if (err) console.error(err)
143
144 msgs = sort(msgs)
145
146 const replies = msgs
147 .filter(isPost)
148 .filter(m => getRoot(m) === key)
149
150 repliesCount.set(replies.length)
151 recent.set(lastFew(replies))
152
153 const likes = msgs.filter(isLikeOf(key))
154 likesCount.set(likes.length)
155
156 const backlinks = msgs
157 .filter(isPost)
158 .filter(m => getRoot(m) !== key)
159 backlinksCount.set(backlinks.length)
160
161 const authors = replies
162 .map(m => m.value.author)
163 participants.set(Array.from(new Set(authors)))
164 })
165 )
166
167 const className = computed(root.md, r => r ? '' : '-loading')
168 const onClick = ev => {
169 ev.preventDefault()
170 ev.stopPropagation()
171 api.app.sync.goTo(key)
172 }
173
174 return h('ThreadCard',
175 {
176 className,
177 attributes: {
178 tabindex: '0', // needed to be able to navigate and show focus()
179 'data-id': key // TODO do this with decorators?
180 }
181 }, [
182 h('section.context', [
183 h('div.avatar', root.avatar),
184 h('div.name', root.author),
185 h('div.timestamp', root.timestamp),
186 h('div.counts', [
187 h('div.comments', [ repliesCount, h('i.fa.fa-comment-o') ]),
188 h('div.likes', [ likesCount, h('i.fa.fa-heart-o') ]),
189 h('div.backlinks', [ backlinksCount, h('i.fa.fa-link') ])
190 ]),
191 h('div.participants', map(participants, api.about.html.avatar))
192 ]),
193 h('section.content-preview', { 'ev-click': onClick }, [
194 h('div.root', root.md),
195 h('div.recent', map(recent, msg => {
196 return h('div.msg', [
197 h('div.author', api.about.obs.name(msg.value.author)),
198 ': ',
199 h('div.preview', [
200 api.message.html.markdown(msg.value.content).innerText.slice(0, 120),
201 '...'
202 ])
203 ])
204 }))
205 ])
206 ])
207 }
208}
209
210function getRoot (msg) {
211 return get(msg, 'value.content.root', msg.key)
212}
213
214function isPost (msg) {
215 return get(msg, 'value.content.type') === 'post'
216}
217
218function isLikeOf (key) {
219 return function (msg) {
220 return get(msg, 'value.content.type') === 'vote' &&
221 get(msg, 'value.content.vote.link') === key
222 }
223}
224
225function lastFew (arr) {
226 return arr.reverse().slice(0, 3).reverse()
227}
228
229// copied from app.html.scroller
230function keyscroll (content) {
231 var curMsgEl
232
233 if (!content) return () => {}
234
235 content.addEventListener('click', onActivateChild, false)
236 content.addEventListener('focus', onActivateChild, true)
237
238 function onActivateChild (ev) {
239 for (var el = ev.target; el; el = el.parentNode) {
240 if (el.parentNode === content) {
241 curMsgEl = el
242 return
243 }
244 }
245 }
246
247 return function scroll (d) {
248 selectChild((!curMsgEl || d === 'first') ? content.firstChild
249 : d < 0 ? curMsgEl.previousElementSibling || content.firstChild
250 : d > 0 ? curMsgEl.nextElementSibling || content.lastChild
251 : curMsgEl)
252
253 return curMsgEl
254 }
255
256 function selectChild (el) {
257 if (!el) { return }
258
259 if (!el.scrollIntoViewIfNeeded && !el.scrollIntoView) return
260 ;(el.scrollIntoViewIfNeeded || el.scrollIntoView).call(el)
261 el.focus()
262 curMsgEl = el
263 }
264}
265
266

Built with git-ssb-web