git ssb

0+

mixmix / patchbay-scry



Tree: fdb2ee6f55290529cd2d95134844044dd149a47c

Files: fdb2ee6f55290529cd2d95134844044dd149a47c / views / show.js

10842 bytesRaw
1const { h, Struct, computed, resolve } = require('mutant')
2const pull = require('pull-stream')
3const printTime = require('../lib/print-time')
4
5module.exports = function ScryShow (opts) {
6 const {
7 poll,
8 myFeedId,
9 scuttle,
10 name = k => k.slice(0, 9),
11 avatar = k => h('img'),
12 testing = false
13 } = opts
14
15 const state = Struct(initialState())
16 fetchState()
17 watchForUpdates(fetchState)
18
19 return h('ScryShow', [
20 h('h1', state.current.title),
21 ScryShowClosesAt(state.current),
22 AuthorActions(),
23 ScryShowResults(),
24 h('div.actions', [
25 PublishBtn()
26 ])
27 ])
28
29 function AuthorActions () {
30 if (poll.value.author !== myFeedId) return
31
32 // TODO hide if already resolved ?
33
34 return h('div.author-actions', [
35 ResolveBtn(),
36 PublishResolveBtn()
37 ])
38
39 function ResolveBtn () {
40 const toggleResolving = () => {
41 const newState = !resolve(state.mode.isResolving)
42 state.mode.isResolving.set(newState)
43 }
44
45 return h('button', { 'ev-click': toggleResolving }, 'Resolve')
46 }
47
48 function PublishResolveBtn () {
49 const publish = () => {
50 const choices = resolve(state.next.resolution)
51 .reduce((acc, choice, i) => {
52 if (choice) acc.push(i)
53 return acc
54 }, [])
55 // const mentions = []
56 scuttle.poll.async.publishResolution({
57 poll: poll,
58 choices
59 }, (err, data) => console.log('resolution:', err, data))
60 }
61 return h('button', { 'ev-click': publish }, 'Publish Resolution')
62 }
63 }
64
65 function PublishBtn () {
66 const publish = () => {
67 state.mode.isPublishing.set(true)
68 const choices = resolve(state.next.position).reduce((acc, el, i) => {
69 if (el) acc.push(i)
70 return acc
71 }, [])
72
73 scuttle.position.async.publishMeetingTime({ poll, choices }, (err, data) => {
74 if (err) throw err
75 console.log(data)
76 })
77 }
78
79 return computed([state.current, state.next, state.mode], (current, next, mode) => {
80 if (validResolution(current.resolution)) return
81 if (!mode.isEditing) return
82 if (mode.isPublishing) return h('button', h('i.fa.fa-spin.fa-pulse'))
83 if (mode.isResolving) return
84
85 const isNewPosition = current.position.join() !== next.position.join()
86 return h('button',
87 {
88 className: isNewPosition ? '-primary' : '',
89 disabled: !isNewPosition,
90 'ev-click': publish
91 }, 'Publish'
92 )
93 })
94 }
95
96 function ScryShowResults () {
97 return computed([state.current, state.next.resolution, state.mode], (current, nextResolution, { isResolving }) => {
98 const { times, rows, resolution } = current
99 const style = {
100 display: 'grid',
101 'grid-template-columns': `minmax(10rem, auto) repeat(${times.length}, 4rem)`
102 }
103
104 const getChosenClass = i => {
105 const relevant = isResolving ? nextResolution : resolution
106 if (!validResolution(relevant)) return ''
107 return relevant[i] ? '-chosen' : '-not-chosen'
108 }
109
110 return [
111 h('ScryShowResults', { style }, [
112 ScryShowTimes(times, getChosenClass),
113 ScryShowResolution(times, resolution),
114 ScryShowSummary(rows, getChosenClass),
115 ScryShowPositions(rows)
116 ])
117 ]
118 })
119 }
120
121 function ScryShowPositions (rows) {
122 return rows.map(({ author, position }) => {
123 if (author !== myFeedId) return OtherPosition(author, position)
124 else return MyPosition(position)
125 })
126
127 function OtherPosition (author, position) {
128 return [
129 h('div.about', [
130 avatar(author),
131 name(author)
132 ]),
133 position.map(pos => pos
134 ? h('div.position.-yes', tick())
135 : h('div.position.-no')
136 )
137 ]
138 }
139
140 function MyPosition (position) {
141 const toggleEditing = () => {
142 const newState = !resolve(state.mode.isEditing)
143 state.mode.isEditing.set(newState)
144 }
145
146 // TODO disable pencil with resolution exists
147 return [
148 h('div.about', [
149 avatar(myFeedId),
150 name(myFeedId),
151 h('i.fa.fa-pencil', { 'ev-click': toggleEditing })
152 ]),
153 computed([state.current.position, state.next.position, state.mode.isEditing], (position, nextPosition, isEditing) => {
154 if (!isEditing) {
155 return position.map(pos => pos
156 ? h('div.position.-yes', tick())
157 : h('div.position.-no')
158 )
159 }
160
161 return nextPosition.map((pos, i) => {
162 return h('div.position.-edit',
163 {
164 'ev-click': () => {
165 const newState = resolve(nextPosition)
166 newState[i] = !pos
167 state.next.position.set(newState)
168 }
169 },
170 pos ? checkedBox() : uncheckedBox()
171 )
172 })
173 })
174 ]
175 }
176 }
177
178 function ScryShowResolution (times, resolution) {
179 return computed([state.mode.isResolving, state.next.resolution], (isResolving, nextResolution) => {
180 if (!isResolving && validResolution(resolution)) {
181 return times.map((_, i) => {
182 const style = { 'grid-column': i + 2 } // grid-columns start at 1 D:
183 const isChoice = Boolean(resolution[i])
184 const className = isChoice ? '-chosen' : ''
185
186 return h('div.resolution', { style, className },
187 isChoice ? star() : ''
188 )
189 })
190 }
191
192 if (isResolving) {
193 const toggleChoice = (i) => {
194 const newState = Array.from(nextResolution)
195 newState[i] = !nextResolution[i]
196 state.next.resolution.set(newState)
197 }
198 return [
199 h('div.resolve-label', 'Final options'),
200 times.map((_, i) => {
201 const isChoice = Boolean(nextResolution[i])
202 const classList = [ '-highlighted', isChoice ? '-chosen' : '' ]
203
204 return h('div.resolution',
205 { classList, 'ev-click': () => toggleChoice(i) },
206 isChoice ? star() : starEmpty()
207 )
208 })
209 ]
210 }
211 })
212 }
213
214 function ScryShowSummary (rows, getChosenClass) {
215 if (!rows.length) return
216
217 const participants = rows.filter(r => r.position[0] !== null).length
218
219 const counts = rows[0].position.map((_, i) => {
220 return rows.reduce((acc, row) => {
221 if (row.position[i] === true) acc += 1
222 return acc
223 }, 0)
224 })
225 return [
226 h('div.participants', participants === 1
227 ? `${participants} participant`
228 : `${participants} participants`
229 ),
230 counts.map((n, i) => {
231 return h('div.count', { className: getChosenClass(i) },
232 `${n}${tick()}`
233 )
234 })
235 ]
236 }
237
238 function fetchState () {
239 scuttle.poll.async.get(poll.key, (err, doc) => {
240 if (err) return console.error(err)
241
242 const { title, closesAt, positions } = doc
243 const times = doc.results.map(result => result.choice)
244 const results = times.map(t => doc.results.find(result => result.choice === t))
245 // this ensures results Array is in same order as a times Array
246
247 const rows = positions
248 .reduce((acc, pos) => {
249 if (!acc.includes(pos.value.author)) acc.push(pos.value.author)
250 return acc
251 }, [])
252 .map(author => {
253 const position = times.map((time, i) => {
254 return results[i].voters.hasOwnProperty(author)
255 })
256 return { author, position }
257 })
258
259 const myRow = rows.find(r => r.author === myFeedId)
260 const myPosition = myRow ? myRow.position : Array(times.length).fill(null)
261
262 var resolution = Array(times.length).fill(null)
263 if (doc.resolution) {
264 resolution = resolution.map((_, i) => doc.resolution.choices.includes(i))
265 }
266 var nextResolution = resolution.map(el => el || false)
267
268 var isEditing = false
269 if (!myRow && !validResolution(resolution)) {
270 rows.push({ author: myFeedId, position: myPosition })
271 isEditing = true
272 }
273
274 state.current.set({
275 title,
276 closesAt,
277 times,
278 rows,
279 position: myPosition,
280 resolution
281 })
282 state.next.set({
283 position: Array.from(myPosition),
284 resolution: nextResolution
285 })
286 state.mode.set({
287 isEditing,
288 isPublishing: false,
289 isResolving: false
290 })
291 })
292 }
293
294 function watchForUpdates (cb) {
295 // TODO check if isEditing before calling cb
296 // start a loop to trigger cb after finished editing
297 pull(
298 scuttle.poll.pull.updates(poll.key),
299 pull.filter(m => !m.sync),
300 pull.drain(m => {
301 cb()
302 })
303 )
304 }
305
306 function tick () { return '✔' }
307 function checkedBox () { return testing ? '☑' : h('i.fa.fa-check-square-o') }
308 function uncheckedBox () { return testing ? '☐' : h('i.fa.fa-square-o') }
309 function star () { return testing ? '★' : h('i.fa.fa-star') }
310 function starEmpty () { return testing ? '☐' : h('i.fa.fa-star-o') }
311}
312
313function initialState () {
314 return {
315 current: Struct({
316 title: '',
317 times: [],
318 closesAt: undefined,
319 rows: [],
320 position: [],
321 resolution: []
322 }),
323 next: Struct({
324 position: [],
325 resolution: []
326 }),
327 mode: Struct({
328 isEditing: false,
329 isPublishing: false,
330 isResolving: false
331 })
332 }
333}
334
335function validResolution (arr) {
336 // valid as in not a dummy resolution that's a placeholder
337 return arr.every(el => el !== null)
338}
339
340// component
341
342function ScryShowClosesAt ({ closesAt, resolution }) {
343 return h('div.closes-at', computed([closesAt, resolution], (t, resolution) => {
344 if (!t) return
345 if (validResolution(resolution)) return
346
347 const distance = t - new Date()
348 if (distance < 0) return 'This scry has closed, but a resolution has yet to be declared.'
349
350 const hours = Math.floor(distance / (60 * 60e3))
351 const days = Math.floor(hours / 24)
352 return `This scry closes in ${days} days, ${hours % 24} hours`
353 }))
354}
355
356// component: show-time
357
358function ScryShowTimes (times, getChosenClass) {
359 return times.map((time, i) => {
360 const style = { 'grid-column': i + 2 } // grid-columns start at 1 D:
361
362 return h('ScryShowTime', { style, className: getChosenClass(i) }, [
363 h('div.month', month(time)),
364 h('div.date', time.getDate()),
365 h('div.day', day(time)),
366 h('div.time', printTime(time))
367 ])
368 })
369}
370
371function month (date) {
372 const months = ['Jan', 'Feb', 'March', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec']
373
374 return months[date.getMonth()]
375}
376
377function day (date) {
378 const days = ['Sun', 'Mon', 'Tues', 'Wed', 'Thu', 'Fri', 'Sat']
379
380 return days[date.getDay()]
381}
382

Built with git-ssb-web