├── .gitignore ├── gathering ├── async │ ├── thumbnail.js │ ├── create.js │ ├── title.js │ ├── location.js │ ├── description.js │ ├── endDateTime.js │ ├── startDateTime.js │ ├── hosts.js │ ├── images.js │ ├── attendees.js │ ├── contributors.js │ ├── create.test.js │ ├── hosts.test.js │ ├── images.test.js │ ├── attendees.test.js │ ├── contributors.test.js │ ├── title.test.js │ ├── location.test.js │ ├── description.test.js │ ├── endDateTime.test.js │ └── startDateTime.test.js ├── html │ ├── description.mcss │ ├── thumbnail.js │ ├── location.js │ ├── title.js │ ├── create.js │ ├── attendees.js │ ├── description.js │ ├── images.js │ ├── render.js │ ├── layout │ │ ├── detail.mcss │ │ ├── card.js │ │ ├── detail.js │ │ └── card.mcss │ └── startDateTime.js ├── pull │ ├── find.js │ └── find.test.js ├── styles.js └── obs │ ├── struct.js │ ├── gathering.js │ └── gathering.test.js ├── patch-gatherings.gif ├── index.js ├── test.js ├── package.json ├── util └── testBot.js ├── code-of-conduct.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /gathering/async/thumbnail.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /patch-gatherings.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pietgeursen/patch-gatherings/HEAD/patch-gatherings.gif -------------------------------------------------------------------------------- /gathering/html/description.mcss: -------------------------------------------------------------------------------- 1 | Description { 2 | display: flex 3 | flex-direction: column 4 | textarea { 5 | border: 1px solid gainsboro 6 | border-top-left-radius: 0 7 | border-top-right-radius: 0 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const bulk = require('bulk-require') 2 | 3 | const modules = bulk(__dirname, ['!(node_modules|test.js|util|*.test.js)/**/*.js'], {require: function (module) { 4 | return module.match(/(.*.test.js$)/) ? null : require(module) 5 | }}) 6 | 7 | module.exports = modules 8 | -------------------------------------------------------------------------------- /gathering/async/create.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | 3 | exports.gives = nest('gathering.async.create') 4 | 5 | exports.needs = nest({ 6 | 'sbot.async.publish': 'first' 7 | }) 8 | 9 | exports.create = function (api) { 10 | return nest('gathering.async.create', function (data, cb) { 11 | api.sbot.async.publish({type: 'gathering'}, cb) 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /gathering/async/title.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | 3 | exports.gives = nest('gathering.async.title') 4 | 5 | exports.needs = nest({ 6 | 'sbot.async.publish': 'first' 7 | }) 8 | 9 | exports.create = function (api) { 10 | return nest('gathering.async.title', function ({title, gathering}, cb) { 11 | api.sbot.async.publish({type: 'about', about: gathering, title}, cb) 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /gathering/async/location.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | 3 | exports.gives = nest('gathering.async.location') 4 | 5 | exports.needs = nest({ 6 | 'sbot.async.publish': 'first' 7 | }) 8 | 9 | exports.create = function (api) { 10 | return nest('gathering.async.location', function ({location, gathering}, cb) { 11 | api.sbot.async.publish({type: 'about', about: gathering, location}, cb) 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /gathering/async/description.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | 3 | exports.gives = nest('gathering.async.description') 4 | 5 | exports.needs = nest({ 6 | 'sbot.async.publish': 'first' 7 | }) 8 | 9 | exports.create = function (api) { 10 | return nest('gathering.async.description', function ({description, gathering}, cb) { 11 | api.sbot.async.publish({type: 'about', about: gathering, description}, cb) 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /gathering/async/endDateTime.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | 3 | exports.gives = nest('gathering.async.endDateTime') 4 | 5 | exports.needs = nest({ 6 | 'sbot.async.publish': 'first' 7 | }) 8 | 9 | exports.create = function (api) { 10 | return nest('gathering.async.endDateTime', function ({endDateTime, gathering}, cb) { 11 | api.sbot.async.publish({type: 'about', about: gathering, endDateTime}, cb) 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /gathering/html/thumbnail.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | const { h } = require('mutant') 3 | 4 | exports.needs = nest({ 5 | 'blob.sync.url': 'first' 6 | }) 7 | 8 | exports.gives = nest('gathering.html.thumbnail') 9 | 10 | exports.create = (api) => { 11 | return nest('gathering.html.thumbnail', thumbnail) 12 | function thumbnail ({ thumbnail, msg }) { 13 | return h('div.thumbnail', h('img', {src: thumbnail})) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /gathering/async/startDateTime.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | const spacetime = require('spacetime') 3 | 4 | exports.gives = nest('gathering.async.startDateTime') 5 | 6 | exports.needs = nest({ 7 | 'sbot.async.publish': 'first' 8 | }) 9 | 10 | exports.create = function (api) { 11 | return nest('gathering.async.startDateTime', function ({startDateTime, gathering}, cb) { 12 | api.sbot.async.publish({type: 'about', about: gathering, startDateTime: spacetime(startDateTime)}, cb) 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /gathering/html/location.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | const { h, computed } = require('mutant') 3 | 4 | exports.needs = nest({ 5 | 'message.html.markdown': 'first' 6 | }) 7 | 8 | exports.gives = nest('gathering.html.location') 9 | 10 | exports.create = (api) => { 11 | return nest('gathering.html.location', location) 12 | function location ({location, msg, isEditing, onUpdate}) { 13 | const markdown = api.message.html.markdown 14 | return h('div', {}, computed(location, markdown)) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /gathering/html/title.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | const { h, when } = require('mutant') 3 | 4 | exports.needs = nest({ 5 | 'blob.sync.url': 'first' 6 | }) 7 | 8 | exports.gives = nest( 9 | 'gathering.html.title' 10 | ) 11 | 12 | exports.create = (api) => { 13 | return nest('gathering.html.title', title) 14 | function title ({title, msg, isEditing, onUpdate}) { 15 | return h('section.title', 16 | when(isEditing, 17 | h('input', {'ev-input': e => onUpdate(e.target.value), value: title}), 18 | h('a', {href: msg.key}, title) 19 | ) 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /gathering/async/hosts.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | const pull = require('pull-stream') 3 | 4 | exports.gives = nest('gathering.async.hosts') 5 | 6 | exports.needs = nest({ 7 | 'sbot.async.publish': 'first' 8 | }) 9 | 10 | exports.create = function (api) { 11 | return nest('gathering.async.hosts', function (data, cb) { 12 | pull( 13 | pull.values(data.hosts), 14 | pull.asyncMap((host, cb) => { 15 | api.sbot.async.publish({type: 'about', about: data.gathering, host: {link: host.id, remove: host.remove}}, cb) 16 | }), 17 | pull.collect(cb) 18 | ) 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /gathering/async/images.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | const pull = require('pull-stream') 3 | 4 | exports.gives = nest('gathering.async.images') 5 | 6 | exports.needs = nest({ 7 | 'sbot.async.publish': 'first' 8 | }) 9 | 10 | exports.create = function (api) { 11 | return nest('gathering.async.images', function (data, cb) { 12 | pull( 13 | pull.values(data.images), 14 | pull.asyncMap((image, cb) => { 15 | api.sbot.async.publish({type: 'about', about: data.gathering, image: {link: image.link, remove: image.remove}}, cb) 16 | }), 17 | pull.collect(cb) 18 | ) 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /gathering/html/create.js: -------------------------------------------------------------------------------- 1 | const { h } = require('mutant') 2 | const nest = require('depnest') 3 | 4 | exports.needs = nest({ 5 | 'gathering.async.create': 'first', 6 | 'blob.html.input': 'first', 7 | 'message.html.confirm': 'first' 8 | }) 9 | 10 | exports.gives = nest('gathering.html.create') 11 | 12 | exports.create = function (api) { 13 | return nest({ 'gathering.html.create': create }) 14 | 15 | function create () { 16 | const actions = h('button', {'ev-click': () => api.gathering.async.create()}, 'Create') 17 | const composer = h('div', [ 18 | actions 19 | ]) 20 | return composer 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /gathering/async/attendees.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | const pull = require('pull-stream') 3 | 4 | exports.gives = nest('gathering.async.attendees') 5 | 6 | exports.needs = nest({ 7 | 'sbot.async.publish': 'first' 8 | }) 9 | 10 | exports.create = function (api) { 11 | return nest('gathering.async.attendees', function (data, cb) { 12 | pull( 13 | pull.values(data.attendees), 14 | pull.asyncMap((attendee, cb) => { 15 | api.sbot.async.publish({type: 'about', about: data.gathering, attendee: {link: attendee.id, remove: attendee.remove}}, cb) 16 | }), 17 | pull.collect(cb) 18 | ) 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /gathering/async/contributors.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | const pull = require('pull-stream') 3 | 4 | exports.gives = nest('gathering.async.contributors') 5 | 6 | exports.needs = nest({ 7 | 'sbot.async.publish': 'first' 8 | }) 9 | 10 | exports.create = function (api) { 11 | return nest('gathering.async.contributors', function (data, cb) { 12 | pull( 13 | pull.values(data.contributors), 14 | pull.asyncMap((contributor, cb) => { 15 | api.sbot.async.publish({type: 'about', about: data.gathering, contributor: {link: contributor.id, remove: contributor.remove}}, cb) 16 | }), 17 | pull.collect(cb) 18 | ) 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /gathering/html/attendees.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | const { map } = require('mutant') 3 | 4 | exports.needs = nest({ 5 | 'about.html.link': 'first', 6 | 'about.html.image': 'first', 7 | 'about.obs.name': 'first' 8 | }) 9 | 10 | exports.gives = nest('gathering.html.attendees') 11 | 12 | exports.create = (api) => { 13 | return nest('gathering.html.attendees', attendees) 14 | 15 | function attendees ({ attendees, msg }) { 16 | // TODO handle when hosts / attendees / contributors are not ssb users 17 | 18 | return map(attendees, (attendee) => { 19 | return api.about.html.link(attendee, api.about.html.image(attendee)) 20 | }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const bulk = require('bulk-require') 2 | const combine = require('depject') 3 | const nest = require('depnest') 4 | const {first} = require('depject/apply') 5 | const test = require('pull-test') 6 | 7 | const depTest = { 8 | gives: nest('test'), 9 | needs: nest({ 10 | 'tests': 'reduce' 11 | }), 12 | create: function (api) { 13 | return nest('test', function () { 14 | const tests = api.tests({}) 15 | test(tests) 16 | }) 17 | } 18 | } 19 | 20 | const modules = combine(bulk(__dirname, [ 21 | 'gathering/async/*.js', 22 | 'gathering/obs/*.js', 23 | 'gathering/pull/*.js', 24 | 'util/*.js' 25 | ]), depTest) 26 | 27 | first(modules.test)() 28 | -------------------------------------------------------------------------------- /gathering/pull/find.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | const pull = require('pull-stream') 3 | 4 | exports.gives = nest('gathering.pull.find') 5 | 6 | exports.needs = nest({ 7 | 'sbot.pull.messagesByType': 'first', 8 | 'sbot.pull.links': 'first', 9 | 'sbot.async.get': 'first' 10 | }) 11 | 12 | exports.create = function (api) { 13 | const { messagesByType } = api.sbot.pull 14 | 15 | return nest({'gathering.pull.find': find}) 16 | 17 | function find (opts) { 18 | const _opts = Object.assign({}, {live: true, future: true, past: false}, opts, {type: 'gathering'}) 19 | return pull( 20 | messagesByType(_opts), 21 | pull.filter(gathering => gathering) 22 | ) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /gathering/html/description.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | const { h, computed, when } = require('mutant') 3 | 4 | exports.needs = nest({ 5 | 'message.html.markdown': 'first' 6 | }) 7 | 8 | exports.gives = nest('gathering.html.description') 9 | 10 | exports.create = (api) => { 11 | return nest('gathering.html.description', description) 12 | function description ({description, isEditing, onUpdate}) { 13 | const markdown = api.message.html.markdown 14 | const input = h('textarea', {'ev-input': e => onUpdate(e.target.value), value: description}) 15 | 16 | return h('Description', [ 17 | when(isEditing, 18 | input, 19 | computed(description, markdown) 20 | ) 21 | ]) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /gathering/styles.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { basename } = path 3 | const readDirectory = require('read-directory') 4 | const { each } = require('libnested') 5 | const nest = require('depnest') 6 | 7 | const contents = readDirectory.sync(path.join(__dirname, '..'), { 8 | extensions: false, 9 | filter: '**/*.mcss', 10 | ignore: '**/node_modules/**' 11 | }) 12 | 13 | exports.gives = nest('styles.mcss') 14 | 15 | exports.create = function (api) { 16 | return nest('styles.mcss', mcss) 17 | 18 | function mcss (sofar = {}) { 19 | each(contents, (content, [filename]) => { 20 | const name = 'patch-gatherings-' + basename(filename) 21 | sofar[name] = content 22 | }) 23 | return sofar 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /gathering/async/create.test.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | 3 | exports.gives = nest('tests') 4 | 5 | exports.needs = nest({ 6 | 'gathering.async.create': 'first', 7 | 'sbot.close': 'first', 8 | 'sbot.create': 'first' 9 | }) 10 | 11 | exports.create = function (api) { 12 | return nest('tests', tests) 13 | 14 | function tests (tests) { 15 | tests['create is requireable'] = function (assert, cb) { 16 | assert(api.gathering.async.create) 17 | cb() 18 | } 19 | tests['creates empty gathering without error'] = function (assert, cb) { 20 | api.sbot.create() 21 | api.gathering.async.create({}, function (err) { 22 | assert(!err) 23 | api.sbot.close() 24 | cb() 25 | }) 26 | } 27 | return tests 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /gathering/async/hosts.test.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | 3 | exports.gives = nest('tests') 4 | 5 | exports.needs = nest({ 6 | 'gathering.async.hosts': 'first', 7 | 'sbot.close': 'first', 8 | 'sbot.create': 'first' 9 | }) 10 | 11 | exports.create = function (api) { 12 | return nest('tests', tests) 13 | 14 | function tests (tests) { 15 | tests['hosts is requireable'] = function (assert, cb) { 16 | assert(api.gathering.async.hosts) 17 | cb() 18 | } 19 | tests['creates host without error'] = function (assert, cb) { 20 | api.sbot.create() 21 | api.gathering.async.hosts({hosts: [{link: '123'}], gathering: '456'}, function (err) { 22 | assert(!err) 23 | api.sbot.close() 24 | cb() 25 | }) 26 | } 27 | return tests 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /gathering/async/images.test.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | 3 | exports.gives = nest('tests') 4 | 5 | exports.needs = nest({ 6 | 'gathering.async.images': 'first', 7 | 'sbot.close': 'first', 8 | 'sbot.create': 'first' 9 | }) 10 | 11 | exports.create = function (api) { 12 | return nest('tests', tests) 13 | 14 | function tests (tests) { 15 | tests['images is requireable'] = function (assert, cb) { 16 | assert(api.gathering.async.images) 17 | cb() 18 | } 19 | tests['creates image without error'] = function (assert, cb) { 20 | api.sbot.create() 21 | api.gathering.async.images({images: [{id: '123'}], gathering: '456'}, function (err) { 22 | assert(!err) 23 | api.sbot.close() 24 | cb() 25 | }) 26 | } 27 | return tests 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /gathering/async/attendees.test.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | 3 | exports.gives = nest('tests') 4 | 5 | exports.needs = nest({ 6 | 'gathering.async.attendees': 'first', 7 | 'sbot.close': 'first', 8 | 'sbot.create': 'first' 9 | }) 10 | 11 | exports.create = function (api) { 12 | return nest('tests', tests) 13 | 14 | function tests (tests) { 15 | tests['attendees is requireable'] = function (assert, cb) { 16 | assert(api.gathering.async.attendees) 17 | cb() 18 | } 19 | tests['creates attendee without error'] = function (assert, cb) { 20 | api.sbot.create() 21 | api.gathering.async.attendees({attendees: [{id: '123'}], gathering: '456'}, function (err) { 22 | assert(!err) 23 | api.sbot.close() 24 | cb() 25 | }) 26 | } 27 | return tests 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /gathering/async/contributors.test.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | 3 | exports.gives = nest('tests') 4 | 5 | exports.needs = nest({ 6 | 'gathering.async.contributors': 'first', 7 | 'sbot.close': 'first', 8 | 'sbot.create': 'first' 9 | }) 10 | 11 | exports.create = function (api) { 12 | return nest('tests', tests) 13 | 14 | function tests (tests) { 15 | tests['contributors is requireable'] = function (assert, cb) { 16 | assert(api.gathering.async.contributors) 17 | cb() 18 | } 19 | tests['creates contributor without error'] = function (assert, cb) { 20 | api.sbot.create() 21 | api.gathering.async.contributors({contributors: [{id: '123'}], gathering: '456'}, function (err) { 22 | assert(!err) 23 | api.sbot.close() 24 | cb() 25 | }) 26 | } 27 | return tests 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /gathering/async/title.test.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | 3 | exports.gives = nest('tests') 4 | 5 | exports.needs = nest({ 6 | 'gathering.async.create': 'first', 7 | 'gathering.async.title': 'first', 8 | 'sbot.close': 'first', 9 | 'sbot.create': 'first' 10 | }) 11 | 12 | exports.create = function (api) { 13 | return nest('tests', tests) 14 | 15 | function tests (tests) { 16 | tests['title is requireable'] = function (assert, cb) { 17 | assert(api.gathering.async.title) 18 | cb() 19 | } 20 | tests['can publish a title message without error'] = function (assert, cb) { 21 | const title = 'piet' 22 | const id = 1 23 | api.sbot.create() 24 | api.gathering.async.title({title, gathering: id}, function (err) { 25 | assert(!err) 26 | api.sbot.close() 27 | cb() 28 | }) 29 | } 30 | return tests 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /gathering/async/location.test.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | 3 | exports.gives = nest('tests') 4 | 5 | exports.needs = nest({ 6 | 'gathering.async.create': 'first', 7 | 'gathering.async.location': 'first', 8 | 'sbot.close': 'first', 9 | 'sbot.create': 'first' 10 | }) 11 | 12 | exports.create = function (api) { 13 | return nest('tests', tests) 14 | 15 | function tests (tests) { 16 | tests['location is requireable'] = function (assert, cb) { 17 | assert(api.gathering.async.location) 18 | cb() 19 | } 20 | tests['can publish a location message without error'] = function (assert, cb) { 21 | const location = 'piet' 22 | const link = 1 23 | api.sbot.create() 24 | api.gathering.async.location({location, gathering: link}, function (err) { 25 | assert(!err) 26 | api.sbot.close() 27 | cb() 28 | }) 29 | } 30 | return tests 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /gathering/async/description.test.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | 3 | exports.gives = nest('tests') 4 | 5 | exports.needs = nest({ 6 | 'gathering.async.create': 'first', 7 | 'gathering.async.description': 'first', 8 | 'sbot.close': 'first', 9 | 'sbot.create': 'first' 10 | }) 11 | 12 | exports.create = function (api) { 13 | return nest('tests', tests) 14 | 15 | function tests (tests) { 16 | tests['description is requireable'] = function (assert, cb) { 17 | assert(api.gathering.async.description) 18 | cb() 19 | } 20 | tests['can publish a description message without error'] = function (assert, cb) { 21 | const description = 'piet' 22 | const link = 1 23 | api.sbot.create() 24 | api.gathering.async.description({description, gathering: link}, function (err) { 25 | assert(!err) 26 | api.sbot.close() 27 | cb() 28 | }) 29 | } 30 | return tests 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /gathering/async/endDateTime.test.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | 3 | exports.gives = nest('tests') 4 | 5 | exports.needs = nest({ 6 | 'gathering.async.create': 'first', 7 | 'gathering.async.endDateTime': 'first', 8 | 'sbot.close': 'first', 9 | 'sbot.create': 'first' 10 | }) 11 | 12 | exports.create = function (api) { 13 | return nest('tests', tests) 14 | 15 | function tests (tests) { 16 | tests['endDateTime is requireable'] = function (assert, cb) { 17 | assert(api.gathering.async.endDateTime) 18 | cb() 19 | } 20 | tests['can publish a endDateTime message without error'] = function (assert, cb) { 21 | const endDateTime = 'piet' 22 | const link = 1 23 | api.sbot.create() 24 | api.gathering.async.endDateTime({endDateTime, gathering: link}, function (err) { 25 | assert(!err) 26 | api.sbot.close() 27 | cb() 28 | }) 29 | } 30 | return tests 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /gathering/async/startDateTime.test.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | 3 | exports.gives = nest('tests') 4 | 5 | exports.needs = nest({ 6 | 'gathering.async.create': 'first', 7 | 'gathering.async.startDateTime': 'first', 8 | 'sbot.close': 'first', 9 | 'sbot.create': 'first' 10 | }) 11 | 12 | exports.create = function (api) { 13 | return nest('tests', tests) 14 | 15 | function tests (tests) { 16 | tests['startDateTime is requireable'] = function (assert, cb) { 17 | assert(api.gathering.async.startDateTime) 18 | cb() 19 | } 20 | tests['can publish a startDateTime message without error'] = function (assert, cb) { 21 | const startDateTime = 'piet' 22 | const link = 1 23 | api.sbot.create() 24 | api.gathering.async.startDateTime({startDateTime, gathering: link}, function (err) { 25 | assert(!err) 26 | api.sbot.close() 27 | cb() 28 | }) 29 | } 30 | return tests 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /gathering/html/images.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | const { h, Set, map, forEach, when } = require('mutant') 3 | 4 | exports.needs = nest({ 5 | 'blob.sync.url': 'first', 6 | 'blob.html.input': 'first' 7 | }) 8 | 9 | exports.gives = nest('gathering.html.images') 10 | 11 | exports.create = (api) => { 12 | return nest('gathering.html.images', images) 13 | function images ({images, msg, isEditing, onUpdate}) { 14 | const allImages = Set([]) 15 | // value(images => forEach(images, image => allImages.add(image.link))) // TODO: so that we still publish an image with all the info but just use the link for now. 16 | images(images => forEach(images, image => allImages.add(image))) 17 | 18 | const fileInput = api.blob.html.input(file => { 19 | onUpdate(file) 20 | }) 21 | 22 | return h('section.images', {}, [ 23 | map(allImages, image => h('img', {src: api.blob.sync.url(image)})), 24 | when(isEditing, [h('div', 'Add an image:'), fileInput]) 25 | ]) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "patch-gatherings", 3 | "version": "2.4.7", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node test.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/pietgeursen/patch-gatherings.git" 12 | }, 13 | "keywords": [], 14 | "author": "pietgeursen", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "pull-test": "^1.2.3", 18 | "scuttlebot": "^9.4.6", 19 | "ssb-keys": "^7.0.6" 20 | }, 21 | "dependencies": { 22 | "bulk-require": "^1.0.0", 23 | "cssify": "^1.0.3", 24 | "depject": "^4.1.0", 25 | "depnest": "^1.3.0", 26 | "flatpickr": "^2.6.1", 27 | "insert-css": "^2.0.0", 28 | "libnested": "^1.2.1", 29 | "moment": "^2.18.0", 30 | "mutant": "^3.18.0", 31 | "pull-notify": "^0.1.1", 32 | "pull-scroll": "^1.0.3", 33 | "read-directory": "^2.0.0", 34 | "spacetime": "^1.0.4", 35 | "ssb-ref": "^2.7.1", 36 | "suggest-box": "^2.2.3" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /gathering/pull/find.test.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | const pull = require('pull-stream') 3 | 4 | exports.gives = nest('tests') 5 | 6 | exports.needs = nest({ 7 | 'gathering.pull.find': 'first', 8 | 'sbot.close': 'first', 9 | 'sbot.create': 'first', 10 | 'gathering.async.create': 'first', 11 | 'gathering.async.startDateTime': 'first' 12 | }) 13 | 14 | exports.create = function (api) { 15 | return nest('tests', tests) 16 | 17 | function tests (tests) { 18 | tests['find is requireable'] = function (assert, cb) { 19 | assert(api.gathering.pull.find) 20 | cb() 21 | } 22 | tests['can create and find a gathering'] = function (assert, cb) { 23 | api.sbot.create() 24 | api.gathering.async.create({}, function (err) { 25 | assert(!err) 26 | pull( 27 | api.gathering.pull.find({past: true, future: true}), 28 | pull.drain(function (data) { 29 | assert(data) 30 | api.sbot.close() 31 | cb() 32 | }) 33 | ) 34 | }) 35 | } 36 | return tests 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /util/testBot.js: -------------------------------------------------------------------------------- 1 | const ssbKeys = require('ssb-keys') 2 | const createSbot = require('scuttlebot') 3 | const nest = require('depnest') 4 | 5 | exports.gives = nest({ 6 | 'sbot': ['close', 'create', 'whoami'], 7 | 'sbot.async': ['publish', 'get'], 8 | 'sbot.pull': [ 9 | 'messagesByType', 10 | 'links' 11 | ] 12 | }) 13 | 14 | exports.create = function (api) { 15 | var sbot 16 | return nest({ 17 | 'sbot': { 18 | create: function (name = Math.random().toString()) { 19 | sbot = createSbot({ keys: ssbKeys.generate(), temp: name }) 20 | }, 21 | close: function () { 22 | sbot.close() 23 | }, 24 | whoami: function () { 25 | return sbot.whoami() 26 | }, 27 | 'async.get': function (id, cb) { 28 | sbot.get(id, cb) 29 | }, 30 | 'async.publish': function (content, cb) { 31 | sbot.publish(content, cb) 32 | }, 33 | pull: { 34 | messagesByType: function (opts) { 35 | return sbot.messagesByType(opts) 36 | }, 37 | links: function (opts) { 38 | return sbot.links(opts) 39 | } 40 | } 41 | } 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /gathering/obs/struct.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | const { Value, Set, Struct, forEachPair } = require('mutant') 3 | 4 | exports.needs = nest({ 5 | 'gathering.async': { 6 | 'title': 'first', 7 | 'description': 'first', 8 | 'images': 'first', 9 | 'location': 'first', 10 | 'attendees': 'first', 11 | 'hosts': 'first', 12 | 'startDateTime': 'first' 13 | } 14 | }) 15 | 16 | exports.gives = nest('gathering.obs.struct') 17 | 18 | exports.create = function (api) { 19 | return nest('gathering.obs.struct', function (opts = {}) { 20 | const struct = Struct({ 21 | title: Value(''), 22 | description: Value(''), 23 | thumbnail: Value(''), 24 | startDateTime: Value(''), 25 | endDateTime: Value(''), 26 | location: Value(''), 27 | contributors: Set([]), 28 | hosts: Set([]), 29 | attendees: Set([]), 30 | images: Set([]) 31 | }) 32 | 33 | Object.keys(opts).forEach((k) => { 34 | if (opts[k]) { 35 | struct[k].set(opts[k]) 36 | } 37 | }) 38 | 39 | struct.save = (id) => { 40 | forEachPair(struct, (k, v) => { 41 | if (api.gathering.async[k] && v) { 42 | api.gathering.async[k]({[k]: v, gathering: id}, console.log) 43 | } 44 | }) 45 | } 46 | 47 | return struct 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /gathering/html/render.js: -------------------------------------------------------------------------------- 1 | const h = require('mutant/h') 2 | const Value = require('mutant/value') 3 | const when = require('mutant/when') 4 | const nest = require('depnest') 5 | 6 | exports.needs = nest({ 7 | 'blob.sync.url': 'first', 8 | 'gathering.obs.gathering': 'first', 9 | 'gathering.html': { 10 | 'layout': 'first' 11 | }, 12 | 'gathering.async.attendees': 'first', 13 | 'feed.html.render': 'first', 14 | 'keys.sync.load': 'first', 15 | 'about.html.link': 'first', 16 | 'message.html': { 17 | decorate: 'reduce', 18 | link: 'first', 19 | markdown: 'first' 20 | } 21 | }) 22 | 23 | exports.gives = nest({ 24 | 'message.html': ['render'], 25 | 'gathering.html': ['render'] 26 | }) 27 | 28 | exports.create = function (api) { 29 | return nest({ 30 | 'message.html.render': renderGathering, 31 | 'gathering.html.render': renderGathering 32 | }) 33 | function renderGathering (msg, { pageId } = {}) { 34 | if (!msg.value || (msg.value.content.type !== 'gathering')) return 35 | 36 | const isEditing = Value(false) 37 | const isCard = Value(true) 38 | 39 | if (pageId === msg.key) isCard.set(false) 40 | 41 | const obs = api.gathering.obs.gathering(msg.key) 42 | 43 | const element = h('div', {attributes: {tabindex: '0'}}, 44 | when(isCard, 45 | api.gathering.html.layout(msg, {layout: 'card', isEditing, isCard, obs}), 46 | api.gathering.html.layout(msg, {layout: 'detail', isEditing, isCard, obs}) 47 | )) 48 | 49 | return api.message.html.decorate(element, { msg }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /gathering/html/layout/detail.mcss: -------------------------------------------------------------------------------- 1 | Message -gathering-detail { 2 | div.toggle-layout { 3 | font-size: 1rem 4 | cursor: pointer 5 | position: absolute 6 | top: 2rem 7 | 8 | width: 1rem 9 | display: flex 10 | justify-content: center 11 | 12 | border: 1px solid #fff 13 | 14 | :hover { 15 | border: 1px solid gainsboro 16 | } 17 | } 18 | 19 | section.title { 20 | font-size: 2rem 21 | margin-bottom: 1rem 22 | 23 | input { 24 | font-size: 2rem 25 | border: 1px solid gainsboro 26 | } 27 | } 28 | 29 | section.content { 30 | section { margin-bottom: 1rem } 31 | 32 | section.images { 33 | img { max-width: 100% } 34 | } 35 | 36 | section.description { 37 | min-height: 3rem 38 | } 39 | 40 | section.time { 41 | min-height: 4rem 42 | (input) { 43 | font-size: 1rem 44 | border: 1px solid gainsboro 45 | } 46 | } 47 | 48 | section.attendees { 49 | header { 50 | font-size: .8rem 51 | margin-bottom: .5rem 52 | } 53 | div.people { 54 | display: flex 55 | flex-wrap: wrap 56 | 57 | a { 58 | margin: 0 .2rem .2rem 0 59 | } 60 | 61 | } 62 | } 63 | 64 | section.actions { 65 | display: flex 66 | flex-basis: 100% 67 | justify-content: flex-end 68 | } 69 | } 70 | 71 | section.actions { 72 | display: flex 73 | justify-content: flex-start 74 | 75 | button.attend { 76 | margin-left: 0 77 | } 78 | 79 | button.edit { 80 | margin-left: auto 81 | } 82 | 83 | } 84 | } 85 | 86 | StartDateTime { 87 | div { 88 | input { 89 | border: 1px solid gainsboro 90 | font-size: 1em 91 | width: 16rem 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /gathering/html/startDateTime.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | const moment = require('moment') 3 | const fs = require('fs') 4 | const { h, computed, when } = require('mutant') 5 | const insertCss = require('insert-css') 6 | const Pickr = require('flatpickr') 7 | const stylePath = require.resolve('flatpickr/dist/flatpickr.css') 8 | 9 | const styleCss = fs.readFileSync(stylePath, 'UTF8') 10 | insertCss(styleCss) 11 | 12 | exports.needs = nest({ 13 | }) 14 | 15 | exports.gives = nest('gathering.html.startDateTime') 16 | 17 | exports.create = (api) => { 18 | return nest('gathering.html.startDateTime', startDateTime) 19 | function startDateTime ({startDateTime, msg, isEditing, onUpdate}) { 20 | const input = h('input.date', { 21 | 'ev-change': ({target: {value}}) => { 22 | onUpdate(value * 1000) 23 | } 24 | }) 25 | const div = h('div', input) 26 | let picker 27 | 28 | isEditing(isEditing => { 29 | if (isEditing) { 30 | picker = new Pickr(input, { 31 | enableTime: true, 32 | altInput: true, 33 | dateFormat: 'U' 34 | }) 35 | const t = startDateTime() 36 | if (t) picker.setDate(t.epoch) 37 | } else { 38 | if (picker && picker.destroy) picker.destroy() 39 | } 40 | }) 41 | 42 | startDateTime((t) => { 43 | if (t && t.epoch && picker) picker.setDate(t.epoch) 44 | }) 45 | 46 | return h('StartDateTime', [ 47 | when(isEditing, 48 | div, 49 | [ 50 | h('div', {}, computed(startDateTime, time => { 51 | return time && time.epoch ? moment(time.epoch).format('LT') : '' 52 | })), 53 | h('div', {}, computed(startDateTime, time => { 54 | return time && time.epoch ? moment(time.epoch).format('LL') : '' 55 | })) 56 | ] 57 | ) 58 | ]) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /gathering/html/layout/card.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | const { h, computed } = require('mutant') 3 | const spacetime = require('spacetime') 4 | 5 | exports.needs = nest({ 6 | 'app.sync.goTo': 'first', 7 | 'message.html': { 8 | author: 'first', 9 | backlinks: 'first', 10 | meta: 'map', 11 | action: 'map', 12 | timestamp: 'first' 13 | }, 14 | 'about.html.image': 'first', 15 | 'about.obs.color': 'first', 16 | 'blob.sync.url': 'first', 17 | 'gathering.obs.gathering': 'first', 18 | 'gathering.html': { 19 | 'description': 'first', 20 | 'title': 'first', 21 | 'location': 'first', 22 | 'startDateTime': 'first' 23 | } 24 | }) 25 | 26 | exports.gives = nest('gathering.html.layout') 27 | 28 | exports.create = (api) => { 29 | return nest('gathering.html.layout', gatheringLayout) 30 | 31 | function gatheringLayout (msg, opts) { 32 | const { layout, obs, isCard } = opts 33 | 34 | if (!(layout === undefined || layout === 'card')) return 35 | 36 | const { author, timestamp, meta, backlinks, action } = api.message.html 37 | 38 | const { description, title } = api.gathering.html 39 | 40 | const onClick = () => api.app.sync.goTo(msg.key) 41 | const background = computed(obs.thumbnail, (thumbnail) => `url(${thumbnail})`) 42 | const content = [ 43 | h('.toggle-layout', { 44 | 'ev-click': e => { 45 | e.preventDefault() 46 | isCard.set(false) 47 | } 48 | }, '+'), 49 | h('.details', [ 50 | title({title: obs.title, msg}), 51 | description({description: obs.description}) 52 | ]), 53 | h('.date-splash', 54 | { 55 | style: { 56 | 'background-image': background, 57 | 'background-color': api.about.obs.color(msg.key) 58 | } 59 | }, 60 | [ 61 | h('div', computed(obs.startDateTime, time => { 62 | const t = spacetime(time) 63 | return `${t.format('date')} ${t.format('month-short')}` 64 | })) 65 | ] 66 | ) 67 | ] 68 | 69 | return h('Message -gathering-card', { 70 | 'ev-click': onClick, 71 | attributes: { tabindex: '0' } // needed to be able to navigate and show focus() 72 | }, [ 73 | h('section.avatar', api.about.html.image(msg.value.author)), 74 | h('section.top', [ 75 | // h('div.author', author(msg)), 76 | // h('div.title'), 77 | // h('div.meta', meta(msg)) 78 | ]), 79 | h('section.content', content), 80 | h('section.raw-content'), //, rawMessage), 81 | h('section.bottom', [ 82 | h('div.timestamp', timestamp(msg)), 83 | h('div.actions', action(msg)) 84 | ]), 85 | h('footer.backlinks', backlinks(msg)) 86 | ]) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /gathering/html/layout/detail.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | const { h, when, computed } = require('mutant') 3 | 4 | exports.needs = nest({ 5 | 'about.html.link': 'first', 6 | 'blob.sync.url': 'first', 7 | 'gathering.async': { 8 | 'attendees': 'first' 9 | }, 10 | 'gathering.obs.struct': 'first', 11 | 'keys.sync.load': 'first', 12 | 'message.html': { 13 | 'markdown': 'first' 14 | }, 15 | 'gathering.html': { 16 | 'title': 'first', 17 | 'description': 'first', 18 | 'images': 'first', 19 | 'location': 'first', 20 | 'attendees': 'first', 21 | 'startDateTime': 'first' 22 | } 23 | 24 | }) 25 | 26 | exports.gives = nest('gathering.html.layout') 27 | 28 | exports.create = (api) => { 29 | return nest('gathering.html.layout', gatheringLayout) 30 | 31 | function gatheringLayout (msg, opts) { 32 | if (!(opts.layout === undefined || opts.layout === 'detail')) return 33 | 34 | const { obs, isEditing, isCard } = opts 35 | 36 | const { attendees, title, images, description, startDateTime } = api.gathering.html 37 | const editedGathering = api.gathering.obs.struct() 38 | 39 | const myKey = '@' + api.keys.sync.load().public 40 | 41 | const isAttending = computed(obs.attendees, attendees => attendees.includes(myKey)) 42 | const handleAttend = () => { 43 | if (isAttending()) return 44 | api.gathering.async.attendees({ attendees: [{ id: myKey }], gathering: msg.key }, console.log) 45 | } 46 | const handleUnAttend = () => { 47 | if (!isAttending()) return 48 | api.gathering.async.attendees({ attendees: [{ id: myKey, remove: true }], gathering: msg.key }, console.log) 49 | } 50 | 51 | return h('Message -gathering-detail', [ 52 | h('.toggle-layout', { 53 | 'ev-click': e => { 54 | e.preventDefault() 55 | isCard.set(true) 56 | } 57 | }, '-'), 58 | title({ title: obs.title, msg, isEditing, onUpdate: editedGathering.title.set }), 59 | h('section.content', [ 60 | images({images: obs.images, msg, isEditing, onUpdate: editedGathering.images.add}), 61 | h('section.description', description({description: obs.description, msg, isEditing, onUpdate: editedGathering.description.set})), 62 | h('section.time', startDateTime({startDateTime: obs.startDateTime, msg, isEditing, onUpdate: editedGathering.startDateTime.set})), 63 | h('section.attendees', [ 64 | h('header', 'Attendees'), 65 | h('div.people', attendees({ attendees: obs.attendees, msg })) 66 | ]) 67 | ]), 68 | h('section.actions', [ 69 | h('button.attend', { 'ev-click': handleAttend }, 'Attend'), 70 | h('button.not-going', { 'ev-click': handleUnAttend }, 'Not going'), 71 | h('button.edit', { 'ev-click': () => isEditing.set(!isEditing()) }, when(isEditing, 'Cancel', 'Edit')), 72 | when(isEditing, h('button', {'ev-click': () => save({obs: editedGathering, id: msg.key})}, 'Update')) 73 | ]) 74 | ]) 75 | 76 | function save ({obs, id}) { 77 | obs.save(id) 78 | isEditing.set(false) 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at pietgeursen@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /gathering/html/layout/card.mcss: -------------------------------------------------------------------------------- 1 | Message -gathering-card { 2 | padding: 1rem .5rem 3 | 4 | display: grid 5 | grid-template-columns: 5rem auto 6 | grid-template-rows: 0 auto 7 | 8 | section.avatar { 9 | grid-column: 1 / 2 10 | grid-row: 1 / span 4 11 | a img { 12 | } 13 | } 14 | 15 | section.top { 16 | grid-column: 2 / 3 17 | grid-row: 1 / span 1 18 | 19 | display: flex 20 | align-items: baseline 21 | 22 | div.author { 23 | font-weight: 600 24 | margin-right: .5rem 25 | (a) { color: #222 } 26 | } 27 | 28 | div.title { 29 | flex-grow: 1 30 | font-size: .9rem 31 | $textSubtle 32 | (a) { $textSubtle } 33 | } 34 | 35 | div.meta { 36 | display: flex 37 | justify-content: flex-end 38 | align-items: center 39 | 40 | a { 41 | $textSubtle 42 | margin-left: .5rem 43 | } 44 | } 45 | } 46 | 47 | section.content { 48 | grid-column: 2 / 3 49 | grid-row: 2 / span 1 50 | 51 | margin-bottom: .5rem 52 | 53 | (img) { 54 | max-width: 100% 55 | } 56 | 57 | display: grid 58 | grid-template-columns: auto auto 59 | 60 | color: initial 61 | :hover { text-decoration: none } 62 | 63 | div.details { 64 | border: 1px gainsboro solid 65 | border-right: none 66 | padding: 1rem 67 | 68 | section.title { 69 | font-size: 1.8rem 70 | margin-bottom: 1rem 71 | } 72 | section { 73 | 74 | } 75 | } 76 | 77 | div.date-splash { 78 | min-width: 20rem 79 | min-height: 14rem 80 | background-size: cover 81 | background-position: center 82 | 83 | display: flex 84 | align-items: center 85 | justify-content: center 86 | 87 | div { 88 | color: #fff 89 | font-weight: 600 90 | font-size: 3rem 91 | text-shadow: rgba(0, 0, 0, 0.46) 0 0 10px 92 | } 93 | } 94 | 95 | div.toggle-layout { 96 | position: absolute 97 | top: 3.0rem 98 | right: 1rem 99 | border: none 100 | border-radius: 0 101 | background-color: rgba(255,255,255,0.2) 102 | color: #fff; 103 | font-size: 1rem; 104 | font-weight: 600; 105 | line-height: .8rem 106 | } 107 | } 108 | 109 | section.raw-content { 110 | grid-column: 2 / 3 111 | grid-row: 3 / span 1 112 | 113 | pre { 114 | border: 1px gainsboro solid 115 | padding: .8rem 116 | background-color: #f5f5f5 117 | color: #c121dc 118 | padding: .3rem 119 | white-space: pre-wrap 120 | word-wrap: break-word 121 | 122 | span { 123 | font-weight: 600 124 | } 125 | a { 126 | word-break: break-all 127 | } 128 | } 129 | } 130 | 131 | section.bottom { 132 | grid-column: 2 / 3 133 | grid-row: 4 / span 1 134 | 135 | display: flex 136 | align-items: center 137 | 138 | div.timestamp { 139 | flex-grow: 1 140 | 141 | a { 142 | font-size: .9rem 143 | $textSubtle 144 | } 145 | } 146 | 147 | div.actions { 148 | display: flex 149 | justify-content: flex-end 150 | 151 | font-size: .9rem 152 | a { 153 | margin-left: .5em 154 | } 155 | 156 | a.unlike { 157 | $textSubtle 158 | } 159 | } 160 | } 161 | 162 | footer.backlinks { 163 | grid-row: 5 / span 1 164 | 165 | grid-column: 2 / 3 166 | flex-basis: 100% 167 | } 168 | } 169 | 170 | -------------------------------------------------------------------------------- /gathering/obs/gathering.js: -------------------------------------------------------------------------------- 1 | const spacetime = require('spacetime') 2 | const nest = require('depnest') 3 | const pull = require('pull-stream') 4 | const Notify = require('pull-notify') 5 | const ref = require('ssb-ref') 6 | 7 | exports.needs = nest({ 8 | 'sbot.pull.links': 'first', 9 | 'sbot.async.get': 'first', 10 | 'gathering.obs.struct': 'first', 11 | 'blob.sync.url': 'first' 12 | }) 13 | 14 | exports.gives = nest('gathering.obs.gathering') 15 | 16 | exports.create = function (api) { 17 | return nest('gathering.obs.gathering', function (gatheringId) { 18 | if (!ref.isLink(gatheringId)) throw new Error('an id must be specified') 19 | 20 | const subscription = subscribeToLinks(gatheringId) 21 | const blobToUrl = api.blob.sync.url 22 | 23 | const gathering = api.gathering.obs.struct() 24 | gathering.title.set(gatheringId.substring(0, 10) + '...') 25 | 26 | pull( 27 | subsribeToLinksByKey(subscription, 'location'), 28 | pull.drain(gathering.location.set) 29 | ) 30 | pull( 31 | subsribeToLinksByKey(subscription, 'endDateTime'), 32 | pull.drain(gathering.endDateTime.set) 33 | ) 34 | pull( 35 | subsribeToLinksByKey(subscription, 'startDateTime'), 36 | pull.map(st => { 37 | try { 38 | return spacetime(st) 39 | } catch (e) { 40 | } 41 | }), 42 | pull.drain(gathering.startDateTime.set) 43 | ) 44 | pull( 45 | subsribeToLinksByKey(subscription, 'title'), 46 | pull.drain(gathering.title.set) 47 | ) 48 | pull( 49 | subsribeToLinksByKey(subscription, 'description'), 50 | pull.drain(gathering.description.set) 51 | ) 52 | pull( 53 | subscription(), 54 | pull.filter(msg => msg.content.attendee), 55 | pull.drain((msg) => { 56 | const attendee = msg.content.attendee 57 | attendee.remove ? gathering.attendees.delete(attendee.link) : gathering.attendees.add(attendee.link) 58 | }) 59 | ) 60 | pull( 61 | subscription(), 62 | pull.filter(msg => msg.content.image), 63 | pull.drain((msg) => { 64 | const image = msg.content.image 65 | const link = typeof image === 'object' ? image.link : image 66 | image.remove ? gathering.images.delete(link) : gathering.images.add(link) 67 | }) 68 | ) 69 | pull( 70 | subscription(), 71 | pull.filter(msg => msg.content.image), 72 | pull.filter(msg => !msg.content.image.remove), 73 | pull.map(msg => msg.content.image.link), 74 | pull.map(blobToUrl), 75 | pull.drain((url) => { 76 | gathering.thumbnail.set(url) 77 | }) 78 | ) 79 | return gathering 80 | }) 81 | 82 | function subsribeToLinksByKey (subscription, key) { 83 | var timestamp = 0 84 | return pull( 85 | subscription(), 86 | pull.filter(link => { 87 | return link.content[key] 88 | }), 89 | pull.filter(link => { 90 | if (link.timestamp > timestamp) { 91 | timestamp = link.timestamp 92 | return true 93 | } 94 | return false 95 | }), 96 | pull.map(link => link.content[key]) 97 | ) 98 | } 99 | function subscribeToLinks (id) { 100 | const notify = Notify() 101 | pull( 102 | api.sbot.pull.links({dest: id, live: true}), 103 | pull.filter(data => data.key), 104 | pull.asyncMap(function (data, cb) { 105 | api.sbot.async.get(data.key, cb) 106 | }), 107 | pull.drain(notify) 108 | ) 109 | return notify.listen 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /gathering/obs/gathering.test.js: -------------------------------------------------------------------------------- 1 | const nest = require('depnest') 2 | const pull = require('pull-stream') 3 | 4 | exports.gives = nest({ 5 | 'tests': true, 6 | 'blob.sync.url': true 7 | }) 8 | 9 | exports.needs = nest({ 10 | 'gathering.async.create': 'first', 11 | 'gathering.async.title': 'first', 12 | 'gathering.async.attendees': 'first', 13 | 'gathering.obs.gathering': 'first', 14 | 'gathering.pull.find': 'first', 15 | 'sbot.close': 'first', 16 | 'sbot.whoami': 'first', 17 | 'sbot.create': 'first' 18 | }) 19 | 20 | exports.create = function (api) { 21 | return nest({ 22 | 'tests': tests, 23 | 'blob.sync.url': url => url 24 | }) 25 | 26 | function tests (tests) { 27 | tests['obs.gathering is requireable'] = function (assert, cb) { 28 | assert(api.gathering.obs.gathering) 29 | cb() 30 | } 31 | tests['obs.gathering attendees obs updates when a attendee of a gathering is added '] = function (assert, cb) { 32 | api.sbot.create() 33 | const attendeeId = api.sbot.whoami().id 34 | api.gathering.async.create({}, function (err) { assert(!err) }) 35 | pull( 36 | api.gathering.pull.find({past: true, future: true}), 37 | pull.map(gathering => api.gathering.obs.gathering(gathering.key)), 38 | pull.drain(gathering => { 39 | gathering(val => { 40 | assert(val.attendees.includes(attendeeId)) 41 | api.sbot.close() 42 | cb() 43 | }) 44 | }) 45 | ) 46 | pull( 47 | api.gathering.pull.find({past: true, future: true}), 48 | pull.asyncMap((gathering, cb) => { 49 | api.gathering.async.attendees({attendees: [{id: attendeeId}], gathering: gathering.key}, cb) 50 | }), 51 | pull.drain(attendee => { 52 | }) 53 | ) 54 | } 55 | tests['obs.gathering attendees obs updates when a attendee of a gathering is removed '] = function (assert, cb) { 56 | api.sbot.create() 57 | const attendeeId = api.sbot.whoami().id 58 | api.gathering.async.create({}, function (err) { assert(!err) }) 59 | pull( 60 | api.gathering.pull.find({past: true, future: true}), 61 | pull.map(gathering => api.gathering.obs.gathering(gathering.key)), 62 | pull.drain(gathering => { 63 | gathering.attendees.add(attendeeId) 64 | gathering(val => { 65 | assert(!val.attendees.includes(attendeeId)) 66 | api.sbot.close() 67 | cb() 68 | }) 69 | }) 70 | ) 71 | pull( 72 | api.gathering.pull.find({past: true, future: true}), 73 | pull.asyncMap((gathering, cb) => { 74 | api.gathering.async.attendees({attendees: [{id: attendeeId, remove: true}], gathering: gathering.key}, cb) 75 | }), 76 | pull.drain(attendee => { 77 | }) 78 | ) 79 | } 80 | tests['obs.gathering title obs updates when a title of a gathering is published'] = function (assert, cb) { 81 | const title = 'meow!' 82 | api.sbot.create() 83 | api.gathering.async.create({}, function (err) { assert(!err) }) 84 | pull( 85 | api.gathering.pull.find({past: true, future: true}), 86 | pull.map(gathering => api.gathering.obs.gathering(gathering.key)), 87 | pull.drain(gathering => { 88 | gathering(val => { 89 | assert(val.title === title) 90 | api.sbot.close() 91 | cb() 92 | }) 93 | }) 94 | ) 95 | pull( 96 | api.gathering.pull.find({past: true, future: true}), 97 | pull.asyncMap((gathering, cb) => { 98 | api.gathering.async.title({title, gathering: gathering.key}, cb) 99 | }), 100 | pull.drain(title => { 101 | }) 102 | ) 103 | } 104 | return tests 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [patchcore](https://github.com/ssbc/patchcore) gatherings [depject](https://github.com/depject/depject) plugin for [secure scuttlebutt](https://github.com/ssbc/secure-scuttlebutt) 2 | 3 | `gives` pull-stream sources and async methods for finding and publishing gatherings on secure scuttlebutt 4 | 5 | ![patch-gatherings in patchbay](./patch-gatherings.gif) 6 | 7 | ## Needs 8 | ```js 9 | exports.needs = nest({ 10 | 'sbot.pull.messagesByType': 'first', 11 | 'sbot.pull.links': 'first', 12 | }) 13 | ``` 14 | 15 | ## Gives 16 | ```js 17 | exports.gives = nest({ 18 | 'gatherings.pull': [ 19 | 'find' 20 | ], 21 | 'gatherings.async': [ 22 | 'create', 23 | 'title', 24 | 'description', 25 | 'contributors', 26 | 'startDateTime', 27 | 'endDateTime', 28 | 'location', 29 | 'hosts', 30 | 'attendees', 31 | 'images', 32 | ] 33 | 'gatherings.html': [ 34 | 'attendees', //TODO 35 | 'contributors', //TODO 36 | 'create', 37 | 'description', 38 | 'endDateTime', //TODO 39 | 'hosts', //TODO 40 | 'images', 41 | 'layout': [ 42 | 'default', 43 | 'mini', 44 | ], 45 | 'location', //TODO 46 | 'render', 47 | 'startDateTime', 48 | 'thumbnail', 49 | 'title', 50 | ], 51 | 'gatherings.obs': [ 52 | 'gathering' 53 | ], 54 | 'message.html': [ 55 | 'render' 56 | ], 57 | }) 58 | ``` 59 | 60 | ## How gathering messages work 61 | 62 | A gathering message is extremely simple. It is little more than intent to have a `gathering`. Location, time, description etc are all `about` messages that link to the gathering message. Hopefully we can reuse these about messages to add metadata on completely different things. Examples could be publishing a location message about a pub or a photo. 63 | 64 | ## API 65 | 66 | ### gatherings.pull.find(opts={}, cb) 67 | 68 | Returns a new [pull-stream](https://pull-stream.github.io/) of gatherings. Valid `opts` keys include 69 | 70 | - `past` (default: `false`) - `true`: Get all gatherings whose utcDateTime is from the past 71 | - `future` (default: `true`) - `true`: Get all gatherings whose utcDateTime is in the future 72 | - `timeless` (default: `true`) - `true`: Get all gatherings whose utcDateTime is not set 73 | 74 | ### gatherings.async.create(opts={}, cb) 75 | 76 | Creates a new gathering message and calls cb when done. 77 | 78 | ### gatherings.async.title(opts={}, cb) 79 | 80 | Sets the title of the gathering and calls cb when done. Valid `opts` keys include 81 | 82 | - `gathering` (required) - The id of the gathering to link to. 83 | - `title` (required) - The title of the gathering 84 | 85 | ### gatherings.async.startDateTime(opts={}, cb) 86 | 87 | Sets the utc start dateTime of the gathering and calls cb when done. Valid `opts` keys include 88 | 89 | - `gathering` (required) - The id of the gathering to link to. 90 | - `utcDateTime` (required) - The time of the gathering 91 | 92 | ### gatherings.async.endDateTime(opts={}, cb) 93 | 94 | Sets the utc end dateTime of the gathering and calls cb when done. Valid `opts` keys include 95 | 96 | - `gathering` (required) - The id of the gathering to link to. 97 | - `utcDateTime` (required) - The time of the gathering 98 | 99 | ### gatherings.async.location(opts={}, cb) 100 | 101 | Sets the physical location of the gathering and calls cb when done. Valid `opts` keys include 102 | 103 | - `gathering` (required) - The id of the gathering to link to. 104 | - `location` (required) - The location of the gathering 105 | 106 | ### gatherings.async.description(opts={}, cb) 107 | 108 | Sets the physical location of the gathering and calls cb when done. Valid `opts` keys include 109 | 110 | - `gathering` (required) - The id of the gathering to link to. 111 | - `description` (required) - The description of the gathering 112 | 113 | ### gatherings.async.hosts(opts={}, cb) 114 | 115 | Adds or removes hosts of the gathering and calls cb when done. Valid `opts` keys include 116 | 117 | - `gathering` (required) - The id of the gathering to link to. 118 | - `hosts` (required) - an array of hosts where each host is an object that has keys: 119 | - `id` (required) - The id of the host. 120 | - `remove` (default: false) - Remove this id as a host. 121 | 122 | eg: 123 | ```js 124 | gatherings.async.hosts({ 125 | gathering: '', 126 | hosts: [ 127 | {id: ''}, //adds the host 128 | {id: '', remove: true}, // removes the host 129 | ] 130 | }, err => console.log(err)) 131 | ``` 132 | ### gatherings.async.images(opts={}, cb) 133 | 134 | Adds or removes images of the gathering and calls cb when done. Valid `opts` keys include 135 | 136 | - `gathering` (required) - The id of the gathering to link to. 137 | - `images` (required) - an array of images where each image is an object that has keys: 138 | - `id` (required) - The blob id of the image. 139 | - `remove` (default: false) - Remove this blob as an image. 140 | eg: 141 | ```js 142 | gatherings.async.images({ 143 | gathering: '', 144 | images: [ 145 | {id: ''}, //adds the image 146 | {id: '', remove: true}, // removes the image 147 | ] 148 | }, err => console.log(err)) 149 | ``` 150 | 151 | ### gatherings.async.attendees(opts={}, cb) 152 | 153 | Adds or removes attendees of the gathering and calls cb when done. Valid `opts` keys include 154 | 155 | - `gathering` (required) - The id of the gathering to link to. 156 | - `attendees` (required) - an array of attendees where each host is an object that has the keys: 157 | - `id` (required) - The id of the host. 158 | - `remove` (default: false) - Remove this id as an attendee. 159 | 160 | eg: 161 | ```js 162 | gatherings.async.attendees({ 163 | gathering: '', 164 | attendees: [ 165 | {id: ''}, //adds the attendee 166 | {id: '', remove: true}, // removes the attendee 167 | ] 168 | }, err => console.log(err)) 169 | ``` 170 | ### gatherings.async.contributors(opts={}, cb) 171 | 172 | Adds or removes contributors of the gathering and calls cb when done. Valid `opts` keys include 173 | 174 | - `gathering` (required) - The id of the gathering to link to. 175 | - `contributors` (required) - an array of contributors where each host is an object that has the keys: 176 | - `id` (required) - The id of the host. 177 | - `remove` (default: false) - Remove this id as a contributor. 178 | 179 | eg: 180 | ```js 181 | gatherings.async.contributors({ 182 | gathering: '', 183 | contributors: [ 184 | {id: ''}, //adds the contributor 185 | {id: '', remove: true}, // removes the contributor 186 | ] 187 | }, err => console.log(err)) 188 | ``` 189 | 190 | ## Install 191 | 192 | With [npm](https://npmjs.org/) installed, run 193 | 194 | ``` 195 | $ npm install sbot-gatherings 196 | ``` 197 | 198 | ## Code of conduct 199 | 200 | Please note that this project is released with a [Contributor Code of Conduct](code-of-conduct.md). By participating in this project you agree to abide by its terms. 201 | 202 | ## Prior art 203 | - [schema.org/Event](https://schema.org/Event) 204 | - [linked events](http://linkedevents.org/ontology/) 205 | 206 | ## Acknowledgments 207 | 208 | sbot-gatherings wouldn't be a thing with out the help and encouragement of [mixmix](https://github.com/mixmix) and [ahdinosaur](https://github.com/ahdinosaur). 209 | 210 | ## See Also 211 | [patchbay-gatherings](https://github.com/pietgeursen/patchbay-gatherings) as an example of how to wire patch-gatherings into a client 212 | 213 | 214 | ## License 215 | 216 | ISC 217 | --------------------------------------------------------------------------------