git ssb

16+

Dominic / patchbay



Tree: 535b9a31c844497006ff5f0bf0dc391d3df69ae0

Files: 535b9a31c844497006ff5f0bf0dc391d3df69ae0 / app / page / posts.js

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

Built with git-ssb-web