git ssb

16+

Dominic / patchbay



Tree: 30af2ef391b12dddb977c5c3dce6ba556f6f91f1

Files: 30af2ef391b12dddb977c5c3dce6ba556f6f91f1 / app / page / posts.js

6834 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 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: {
167 tabindex: '0', // needed to be able to navigate and show focus()
168 'data-id': key // TODO do this with decorators?
169 }
170 }, [
171 h('section.context', [
172 h('div.avatar', root.avatar),
173 h('div.name', root.author),
174 h('div.timestamp', root.timestamp),
175 h('div.counts', [
176 h('div.comments', [ repliesCount, h('i.fa.fa-comment-o') ]),
177 h('div.likes', [ likesCount, h('i.fa.fa-heart-o') ]),
178 h('div.backlinks', [ backlinksCount, h('i.fa.fa-link') ])
179 ]),
180 h('div.participants', map(participants, api.about.html.avatar))
181 ]),
182 h('section.content-preview', { 'ev-click': onClick }, [
183 h('div.root', root.md),
184 h('div.recent', map(recent, msg => {
185 return h('div.msg', [
186 h('div.author', api.about.obs.name(msg.value.author)),
187 ': ',
188 h('div.preview', [
189 api.message.html.markdown(msg.value.content).innerText.slice(0, 120),
190 '...'
191 ])
192 ])
193 }))
194 ])
195 ])
196 }
197}
198
199function getRoot (msg) {
200 return get(msg, 'value.content.root', msg.key)
201}
202
203function isPost (msg) {
204 return get(msg, 'value.content.type') === 'post'
205}
206
207function isLikeOf (key) {
208 return function (msg) {
209 return get(msg, 'value.content.type') === 'vote' &&
210 get(msg, 'value.content.vote.link') === key
211 }
212}
213
214function lastFew (arr) {
215 return arr.reverse().slice(0, 3).reverse()
216}
217
218// copied from app.html.scroller
219function keyscroll (content) {
220 var curMsgEl
221
222 if (!content) return () => {}
223
224 content.addEventListener('click', onActivateChild, false)
225 content.addEventListener('focus', onActivateChild, true)
226
227 function onActivateChild (ev) {
228 for (var el = ev.target; el; el = el.parentNode) {
229 if (el.parentNode === content) {
230 curMsgEl = el
231 return
232 }
233 }
234 }
235
236 return function scroll (d) {
237 selectChild((!curMsgEl || d === 'first') ? content.firstChild
238 : d < 0 ? curMsgEl.previousElementSibling || content.firstChild
239 : d > 0 ? curMsgEl.nextElementSibling || content.lastChild
240 : curMsgEl)
241
242 return curMsgEl
243 }
244
245 function selectChild (el) {
246 if (!el) { return }
247
248 if (!el.scrollIntoViewIfNeeded && !el.scrollIntoView) return
249 ;(el.scrollIntoViewIfNeeded || el.scrollIntoView).call(el)
250 el.focus()
251 curMsgEl = el
252 }
253}
254
255

Built with git-ssb-web