patchbay-scry
Version:
figure out a meeting time with friends, (like doodle, but for scuttlebutt)
223 lines (195 loc) • 6.22 kB
JavaScript
const { h, Struct, computed, resolve } = require('mutant')
const pull = require('pull-stream')
const { isGathering } = require('ssb-gathering-schema')
const getContent = require('ssb-msg-content')
const ShowGathering = require('./component/show-gathering')
const ShowClosesAt = require('./component/show-closes-at')
const ShowAuthorActions = require('./component/show-author-actions')
const ShowResults = require('./component/show-results')
const Timezone = require('./component/timezone')
// TODO
// - [x] add mentions to the poll-resolution!
// - (later) add a message renderer for poll-resolution type messages
// - [ ] review the logic, try for refactor
// - this is turning into a state-machine D;
module.exports = function ScryShow (opts) {
const {
poll,
myFeedId,
scuttle,
name = k => k.slice(0, 9),
avatar = k => h('img'),
mdRenderer = (text) => text,
NewGathering,
testing = false
} = opts
const state = LiveState({ scuttle, poll, myFeedId })
const symbols = getSymbols(testing)
return h('ScryShow', [
h('h1', state.current.title),
ShowClosesAt(state.current),
h('div.description', computed((state.current.description), mdRenderer)),
ShowGathering(state.current.gathering),
ShowAuthorActions({ poll, myFeedId, state, scuttle, name, NewGathering }),
h('div.timezone', [
h('label', 'All times are in your current timezone: '),
Timezone()
]),
ShowResults({ state, myFeedId, name, avatar, symbols }),
h('div.actions', [
PublishBtn()
])
])
function PublishBtn () {
const publish = () => {
state.mode.isPublishing.set(true)
const choices = resolve(state.next.position).reduce((acc, el, i) => {
if (el) acc.push(i)
return acc
}, [])
scuttle.position.async.publishMeetingTime({ poll, choices }, (err, data) => {
if (err) throw err
console.log(data)
})
}
return computed([state.current, state.next, state.mode], (current, next, mode) => {
if (validResolution(current.resolution)) return
if (!mode.isEditing) return
if (mode.isPublishing) return h('button', h('i.fa.fa-spin.fa-pulse'))
if (mode.isResolving) return
const isNewPosition = current.position.join() !== next.position.join()
return h('button',
{
className: isNewPosition ? '-primary' : '',
disabled: !isNewPosition,
'ev-click': publish
}, 'Publish'
)
})
}
}
function validResolution (arr) {
// valid as in not a dummy resolution that's a placeholder
return arr.every(el => el !== null)
}
// component
// lib/ helper?
function LiveState ({ scuttle, poll, myFeedId }) {
const state = {
current: Struct({
keu: '',
title: '',
description: '',
times: [],
closesAt: undefined,
rows: [],
position: [],
resolution: [],
gathering: undefined,
backlinks: []
}),
next: Struct({
position: [],
resolution: []
}),
mode: Struct({
isEditing: false,
isPublishing: false,
isResolving: false
})
}
fetchState({ scuttle, poll, myFeedId, state })
// TODO check if isEditing before re-fetching state
// TODO factor out updateStream (see about.pull.updates pattern)
pull(
scuttle.poll.pull.updates(poll.key || poll),
pull.filter(m => !m.sync),
pull.drain(m => {
fetchState({ scuttle, poll, myFeedId, state })
})
)
return state
}
function fetchState ({ scuttle, poll, myFeedId, state }) {
scuttle.poll.async.get(poll.key, (err, doc) => {
if (err) return console.error(err)
const { title, body, closesAt, positions, backlinks } = doc
const times = doc.results.map(result => result.choice)
const results = times.map(t => doc.results.find(result => result.choice === t))
// this ensures results Array is in same order as a times Array
const rows = buildRows({ positions, results, times })
// Array with entries { author: feedId, position: [true, true, false, true, ...] }
const myRow = rows.find(r => r.author === myFeedId)
const myPosition = myRow ? myRow.position : Array(times.length).fill(null)
const resolution = buildResolution({ resolution: doc.resolution, times })
var nextResolution = resolution.map(el => el || false)
// [ false, true, false, false, ... ] where true is a selected time column
var isEditing = false
if (!myRow && !validResolution(resolution) && closesAt > new Date()) {
rows.push({ author: myFeedId, position: myPosition })
isEditing = true
}
state.current.set({
key: poll.key,
title,
description: body,
closesAt,
times,
rows,
position: myPosition,
resolution,
gathering: findGathering(backlinks, poll)
})
state.next.set({
position: Array.from(myPosition),
resolution: nextResolution
})
state.mode.set({
isEditing,
isPublishing: false,
isResolving: false
})
})
}
function buildRows ({ positions, times, results }) {
return positions
.reduce((acc, pos) => {
if (!acc.includes(pos.value.author)) acc.push(pos.value.author)
return acc
}, [])
.map(author => {
const position = times.map((time, i) => {
return results[i].voters.hasOwnProperty(author)
})
return { author, position }
})
}
function buildResolution ({ resolution: res, times }) {
var resolution = Array(times.length).fill(null)
if (res) {
resolution = resolution.map((_, i) => res.choices.includes(i))
}
return resolution
}
function findGathering (backlinks, poll) {
return backlinks
.filter(isGathering)
.find(m => getContent(m).progenitor === poll.key)
}
function getSymbols (testing) {
return testing
? {
tick: () => '✔',
checkedBox: () => '☑',
uncheckedBox: () => '☐',
star: () => '★',
starEmpty: () => '☐'
}
: {
tick: () => '✔',
checkedBox: () => h('i.fa.fa-check-square-o'),
uncheckedBox: () => h('i.fa.fa-square-o'),
star: () => h('i.fa.fa-star'),
starEmpty: () => h('i.fa.fa-star-o')
}
}