app/page/statsShow.jsView |
---|
1 | 1 | const nest = require('depnest') |
2 | | -const { h, resolve, when, Value, Struct, Array: MutantArray, Dict, onceTrue, map, computed, dictToCollection, throttle, watchAll } = require('mutant') |
| 2 | +const { h, resolve, when, Value, Struct, Array: MutantArray, Dict, onceTrue, map, computed, throttle, watchAll } = require('mutant') |
3 | 3 | const pull = require('pull-stream') |
4 | 4 | const marksum = require('markdown-summary') |
5 | 5 | const Chart = require('chart.js') |
6 | 6 | const groupBy = require('lodash/groupBy') |
7 | | -const mergeWith = require('lodash/mergeWith') |
8 | 7 | const flatMap = require('lodash/flatMap') |
9 | 8 | |
10 | 9 | exports.gives = nest('app.page.statsShow') |
11 | 10 | |
12 | 11 | exports.needs = nest({ |
13 | 12 | 'sbot.obs.connection': 'first', |
14 | | - 'history.sync.push': 'first' |
| 13 | + 'history.sync.push': 'first', |
| 14 | + 'message.html.markdown': 'first' |
15 | 15 | }) |
16 | 16 | |
17 | 17 | const COMMENTS = 'comments' |
18 | 18 | const LIKES = 'likes' |
| 19 | +const SHARES = 'shares' |
19 | 20 | const DAY = 24 * 60 * 60 * 1000 |
20 | 21 | |
21 | 22 | exports.create = (api) => { |
22 | 23 | return nest('app.page.statsShow', statsShow) |
24 | 25 | function statsShow (location) { |
25 | 26 | var store = Struct({ |
26 | 27 | blogs: MutantArray([]), |
27 | 28 | comments: Dict(), |
28 | | - likes: Dict() |
| 29 | + likes: Dict(), |
| 30 | + shares: Dict() |
29 | 31 | }) |
30 | | - onceTrue(api.sbot.obs.connection, server => fetchBlogs({ server, store })) |
| 32 | + onceTrue(api.sbot.obs.connection, server => fetchBlogData({ server, store })) |
31 | 33 | |
| 34 | + var foci = Struct({ |
| 35 | + [COMMENTS]: computed([throttle(store.comments, 1000)], (msgs) => { |
| 36 | + return flatMap(msgs, (val, key) => val) |
| 37 | + }), |
| 38 | + [LIKES]: computed([throttle(store.likes, 1000)], (msgs) => { |
| 39 | + return flatMap(msgs, (val, key) => val) |
| 40 | + }), |
| 41 | + [SHARES]: [] |
| 42 | + }) |
| 43 | + |
32 | 44 | var howFarBack = Value(0) |
33 | 45 | |
34 | 46 | var context = Struct({ |
35 | 47 | focus: Value(COMMENTS), |
42 | 54 | lower: endOfDay - (howFarBack + 1) * 30 * DAY |
43 | 55 | } |
44 | 56 | }) |
45 | 57 | }) |
46 | | - console.log(context.range()) |
47 | | - context.range(console.log) |
48 | 58 | |
49 | | - var foci = Struct({ |
50 | | - [COMMENTS]: computed([throttle(store.comments, 1000)], (msgs) => { |
51 | | - return flatMap(msgs, (val, key) => val) |
52 | | - }), |
53 | | - [LIKES]: computed([throttle(store.likes, 1000)], (msgs) => { |
54 | | - return flatMap(msgs, (val, key) => val) |
| 59 | + function totalOnscreenData (focus) { |
| 60 | + return computed([foci[focus], context.range], (msgs, range) => { |
| 61 | + return msgs |
| 62 | + .filter(msg => { |
| 63 | + const ts = msg.value.timestamp |
| 64 | + return ts > range.lower && ts <= range.upper |
| 65 | + }) |
| 66 | + .length |
55 | 67 | }) |
| 68 | + } |
56 | 69 | |
57 | | - }) |
58 | | - |
59 | | - var visibleCommentsCount = computed([foci.comments, context.range], (msgs, range) => { |
60 | | - return msgs |
61 | | - .filter(msg => { |
62 | | - const ts = msg.value.timestamp |
63 | | - return ts > range.lower && ts <= range.upper |
64 | | - }) |
65 | | - .length |
66 | | - }) |
67 | | - |
68 | | - var visibleLikesCount = computed([foci.likes, context.range], (msgs, range) => { |
69 | | - return msgs |
70 | | - .filter(msg => { |
71 | | - const ts = msg.value.timestamp |
72 | | - return ts > range.lower && ts <= range.upper |
73 | | - }) |
74 | | - .length |
75 | | - }) |
76 | | - |
77 | 70 | const canvas = h('canvas', { height: 200, width: 600, style: { height: '200px', width: '600px' } }) |
78 | 71 | |
79 | | - const displayComments = () => context.focus.set(COMMENTS) |
80 | | - const displayLikes = () => context.focus.set(LIKES) |
81 | | - |
82 | 72 | const page = h('Page -statsShow', [ |
83 | 73 | h('Scroller.content', [ |
84 | 74 | h('div.content', [ |
85 | 75 | h('h1', 'Stats'), |
86 | | - h('section.totals', [ |
87 | | - h('div.comments', |
| 76 | + h('section.totals', [COMMENTS, LIKES, SHARES].map(focus => { |
| 77 | + return h('div', |
88 | 78 | { |
89 | | - className: computed(context.focus, focus => focus === COMMENTS ? '-selected' : ''), |
90 | | - 'ev-click': displayComments |
| 79 | + classList: computed(context.focus, f => f === focus ? [focus, '-selected'] : [focus]), |
| 80 | + 'ev-click': () => context.focus.set(focus) |
91 | 81 | }, [ |
92 | | - h('div.count', visibleCommentsCount), |
93 | | - h('strong', 'Comments'), |
| 82 | + h('div.count', totalOnscreenData(focus)), |
| 83 | + h('strong', focus), |
94 | 84 | '(30 days)' |
95 | | - ]), |
96 | | - h('div.likes', |
97 | | - { |
98 | | - className: computed(context.focus, focus => focus === LIKES ? '-selected' : ''), |
99 | | - 'ev-click': displayLikes |
100 | | - }, [ |
101 | | - h('div.count', visibleLikesCount), |
102 | | - h('strong', 'Likes'), |
103 | | - '(30 days)' |
104 | | - ] |
105 | | - ), |
106 | | - h('div.shares', |
107 | | - { |
108 | | - className: when(context.shares, '-selected') |
109 | | - |
110 | | - }, [ |
111 | | - |
112 | | - h('div.count', '--'), |
113 | | - h('strong', 'Shares'), |
114 | | - '(30 days)' |
115 | | - ] |
116 | | - ) |
117 | | - ]), |
| 85 | + ]) |
| 86 | + })), |
118 | 87 | h('section.graph', [ |
119 | 88 | canvas, |
120 | 89 | h('div.changeRange', [ |
121 | 90 | '< ', |
131 | 100 | h('table.blogs', [ |
132 | 101 | h('thead', [ |
133 | 102 | h('tr', [ |
134 | 103 | h('th.details'), |
135 | | - h('th.comment', 'Comments'), |
136 | | - h('th.likes', 'Likes') |
| 104 | + h('th.comments', 'Comments'), |
| 105 | + h('th.likes', 'Likes'), |
| 106 | + h('th.shares', 'Shares') |
137 | 107 | ]) |
138 | 108 | ]), |
139 | | - h('tbody', map(store.blogs, blog => h('tr.blog', { id: blog.key }, [ |
140 | | - h('td.details', [ |
141 | | - h('div.title', {}, getTitle(blog)), |
142 | | - h('a', |
143 | | - { |
144 | | - href: '#', |
145 | | - 'ev-click': viewBlog(blog) |
146 | | - }, |
147 | | - 'View blog' |
148 | | - ) |
149 | | - ]), |
150 | | - h('td.comments', computed(store.comments.get(blog.key), msgs => msgs ? msgs.length : 0)), |
151 | | - h('td.likes', computed(store.likes.get(blog.key), msgs => msgs ? msgs.length : 0)) |
152 | | - |
153 | | - ]))) |
| 109 | + h('tbody', map(store.blogs, BlogRow)) |
154 | 110 | ]) |
155 | 111 | ]) |
156 | 112 | ]) |
157 | 113 | ]) |
158 | 114 | |
159 | | - var chart = new Chart(canvas.getContext('2d'), chartConfig({ context, chartData: [] })) |
160 | | - |
161 | | - |
162 | | - |
163 | | - var lastFocus = context.focus() |
164 | | - const zeroGraphOnFocusChange = (focus) => { |
165 | | - if (focus !== lastFocus) { |
166 | | - chart.data.datasets[0].data = [] |
167 | | - chart.update() |
168 | | - lastFocus = focus |
169 | | - } |
| 115 | + function BlogRow (blog) { |
| 116 | + return h('tr.blog', { id: blog.key }, [ |
| 117 | + h('td.details', [ |
| 118 | + h('div.title', {}, getTitle({ blog, mdRenderer: api.message.html.markdown })), |
| 119 | + h('a', |
| 120 | + { |
| 121 | + href: '#', |
| 122 | + 'ev-click': ev => { |
| 123 | + ev.stopPropagation() |
| 124 | + api.history.sync.push(blog) |
| 125 | + } |
| 126 | + }, |
| 127 | + 'View blog' |
| 128 | + ) |
| 129 | + ]), |
| 130 | + h('td.comments', computed(store.comments.get(blog.key), msgs => msgs ? msgs.length : 0)), |
| 131 | + h('td.likes', computed(store.likes.get(blog.key), msgs => msgs ? msgs.length : 0)), |
| 132 | + h('td.shares', computed(store.shares.get(blog.key), msgs => msgs ? msgs.length : 0)) |
| 133 | + ]) |
170 | 134 | } |
171 | | - const toDay = ts => Math.floor(ts / DAY) |
172 | | - const chartData = computed([context.focus, foci], (focus, foci) => { |
173 | | - zeroGraphOnFocusChange(focus) |
174 | | - const msgs = foci[focus] |
175 | | - const grouped = groupBy(msgs, m => toDay(m.value.timestamp)) |
176 | 135 | |
177 | | - var data = Object.keys(grouped) |
178 | | - .map(day => { |
179 | | - return { |
180 | | - t: day * DAY, |
181 | | - y: grouped[day].length |
182 | | - } |
183 | | - }) |
184 | | - return data |
185 | | - }) |
| 136 | + initialiseChart({ canvas, context, foci }) |
186 | 137 | |
187 | | - chartData(data => { |
188 | | - chart.data.datasets[0].data = data |
189 | | - |
190 | | - chart.update() |
191 | | - }) |
192 | | - |
193 | | - watchAll([chartData, context.range], (data, range) => { |
194 | | - const { lower, upper } = range |
195 | | - const slice = data |
196 | | - .filter(d => d.t > lower && d.t <= upper) |
197 | | - .map(d => d.y) |
198 | | - .sort((a, b) => a < b) |
199 | | - |
200 | | - var h = slice[0] |
201 | | - if (!h || h < 10) h = 10 |
202 | | - else h = h + (5 - h % 5) |
203 | | - |
204 | | - |
205 | | - |
206 | | - chart.options.scales.yAxes[0].ticks.max = h |
207 | | - |
208 | | - chart.update() |
209 | | - }) |
210 | | - |
211 | | - context.range(range => { |
212 | | - const { lower, upper } = range |
213 | | - |
214 | | - chart.options.scales.xAxes[0].time.min = new Date(lower - DAY / 2) |
215 | | - chart.options.scales.xAxes[0].time.max = new Date(upper - DAY / 2) |
216 | | - |
217 | | - |
218 | | - chart.update() |
219 | | - }) |
220 | | - |
221 | 138 | return page |
222 | 139 | } |
| 140 | +} |
223 | 141 | |
224 | | - function viewBlog (blog) { |
225 | | - return () => api.history.sync.push(blog) |
| 142 | +function getTitle ({ blog, mdRenderer }) { |
| 143 | + if (blog.value.content.title) return blog.value.content.title |
| 144 | + else if (blog.value.content.text) { |
| 145 | + var md = mdRenderer(marksum.title(blog.value.content.text)) |
| 146 | + if (md && md.innerText) return md.innerText |
226 | 147 | } |
227 | | -} |
228 | 148 | |
229 | | -function getTitle (blog) { |
230 | | - if (blog.value.content.title) return blog.value.content.title |
231 | | - else if (blog.value.content.text) return marksum.title(blog.value.content.text) |
232 | | - else return blog.key |
| 149 | + return blog.key |
233 | 150 | } |
234 | 151 | |
235 | | -function fetchBlogs ({ server, store }) { |
| 152 | +function fetchBlogData ({ server, store }) { |
236 | 153 | pull( |
237 | 154 | server.blogStats.readBlogs({ reverse: false }), |
238 | 155 | pull.drain(blog => { |
239 | 156 | store.blogs.push(blog) |
241 | 158 | fetchComments({ server, store, blog }) |
242 | 159 | fetchLikes({ server, store, blog }) |
243 | 160 | }) |
244 | 161 | ) |
245 | | -} |
246 | 162 | |
247 | | -function fetchComments ({ server, store, blog }) { |
248 | | - if (!store.comments.has(blog.key)) store.comments.put(blog.key, MutantArray()) |
| 163 | + function fetchComments ({ server, store, blog }) { |
| 164 | + if (!store.comments.has(blog.key)) store.comments.put(blog.key, MutantArray()) |
249 | 165 | |
250 | | - pull( |
251 | | - server.blogStats.readComments(blog), |
252 | | - pull.drain(msg => { |
253 | | - store.comments.get(blog.key).push(msg) |
254 | | - |
255 | | - }) |
256 | | - ) |
| 166 | + pull( |
| 167 | + server.blogStats.readComments(blog), |
| 168 | + pull.drain(msg => { |
| 169 | + store.comments.get(blog.key).push(msg) |
| 170 | + |
| 171 | + }) |
| 172 | + ) |
| 173 | + } |
| 174 | + |
| 175 | + function fetchLikes ({ server, store, blog }) { |
| 176 | + if (!store.likes.has(blog.key)) store.likes.put(blog.key, MutantArray()) |
| 177 | + |
| 178 | + pull( |
| 179 | + server.blogStats.readLikes(blog), |
| 180 | + pull.drain(msg => { |
| 181 | + store.likes.get(blog.key).push(msg) |
| 182 | + |
| 183 | + |
| 184 | + |
| 185 | + }) |
| 186 | + ) |
| 187 | + } |
257 | 188 | } |
258 | 189 | |
259 | | -function fetchLikes ({ server, store, blog }) { |
260 | | - if (!store.likes.has(blog.key)) store.likes.put(blog.key, MutantArray()) |
| 190 | +function initialiseChart ({ canvas, context, foci }) { |
| 191 | + var chart = new Chart(canvas.getContext('2d'), chartConfig({ context })) |
261 | 192 | |
262 | | - pull( |
263 | | - server.blogStats.readLikes(blog), |
264 | | - pull.drain(msg => { |
265 | | - store.likes.get(blog.key).push(msg) |
266 | | - |
267 | | - |
268 | | - |
269 | | - }) |
270 | | - ) |
| 193 | + const chartData = computed([context.focus, foci], (focus, foci) => { |
| 194 | + zeroGraphOnFocusChange(focus) |
| 195 | + const msgs = foci[focus] |
| 196 | + const grouped = groupBy(msgs, m => toDay(m.value.timestamp)) |
| 197 | + |
| 198 | + return Object.keys(grouped) |
| 199 | + .map(day => { |
| 200 | + return { |
| 201 | + t: day * DAY, |
| 202 | + y: grouped[day].length |
| 203 | + } |
| 204 | + }) |
| 205 | + }) |
| 206 | + |
| 207 | + chartData(data => { |
| 208 | + chart.data.datasets[0].data = data |
| 209 | + |
| 210 | + chart.update() |
| 211 | + }) |
| 212 | + |
| 213 | + watchAll([chartData, context.range], (data, range) => { |
| 214 | + const { lower, upper } = range |
| 215 | + const slice = data |
| 216 | + .filter(d => d.t > lower && d.t <= upper) |
| 217 | + .map(d => d.y) |
| 218 | + .sort((a, b) => a < b) |
| 219 | + |
| 220 | + var h = slice[0] |
| 221 | + if (!h || h < 10) h = 10 |
| 222 | + else h = h + (5 - h % 5) |
| 223 | + |
| 224 | + |
| 225 | + |
| 226 | + chart.options.scales.yAxes[0].ticks.max = h |
| 227 | + |
| 228 | + chart.update() |
| 229 | + }) |
| 230 | + |
| 231 | + context.range(range => { |
| 232 | + const { lower, upper } = range |
| 233 | + |
| 234 | + chart.options.scales.xAxes[0].time.min = new Date(lower - DAY / 2) |
| 235 | + chart.options.scales.xAxes[0].time.max = new Date(upper - DAY / 2) |
| 236 | + |
| 237 | + |
| 238 | + chart.update() |
| 239 | + }) |
| 240 | + |
| 241 | + |
| 242 | + |
| 243 | + |
| 244 | + |
| 245 | + var lastFocus = context.focus() |
| 246 | + function zeroGraphOnFocusChange (focus) { |
| 247 | + if (focus !== lastFocus) { |
| 248 | + chart.data.datasets[0].data = [] |
| 249 | + chart.update() |
| 250 | + lastFocus = focus |
| 251 | + } |
| 252 | + } |
| 253 | + function toDay (ts) { return Math.floor(ts / DAY) } |
271 | 254 | } |
272 | 255 | |
273 | 256 | |
274 | 257 | function chartConfig ({ context }) { |