git ssb

16+

Dominic / patchbay



Tree: e332b352c95bf5beda97668ba19154b3c705093a

Files: e332b352c95bf5beda97668ba19154b3c705093a / app / page / calendar.js

7231 bytesRaw
1const nest = require('depnest')
2const { h, Array: MutantArray, map, Struct, computed, watch, throttle, resolve } = require('mutant')
3const Month = require('marama')
4
5const pull = require('pull-stream')
6const paraMap = require('pull-paramap')
7const { isMsg } = require('ssb-ref')
8
9exports.gives = nest({
10 'app.page.calendar': true,
11 'app.html.menuItem': true
12})
13
14exports.needs = nest({
15 'app.sync.goTo': 'first',
16 'keys.sync.id': 'first',
17 'message.html.render': 'first',
18 'message.sync.unbox': 'first',
19 'sbot.async.get': 'first',
20 'sbot.pull.stream': 'first'
21})
22
23exports.create = (api) => {
24 return nest({
25 'app.html.menuItem': menuItem,
26 'app.page.calendar': calendarPage
27 })
28
29 function menuItem () {
30 return h('a', {
31 'ev-click': () => api.app.sync.goTo({ page: 'calendar' })
32 }, '/calendar')
33 }
34
35 function calendarPage (location) {
36 const d = startOfDay()
37 const state = Struct({
38 today: d,
39 year: d.getFullYear(),
40 events: MutantArray([]),
41 attending: MutantArray([]),
42 range: Struct({
43 gte: d,
44 lt: endOfDay(d)
45 })
46 })
47
48 watch(state.year, year => getGatherings(year, state.events, Query))
49 watchAttending(state.attending, api)
50
51 const page = h('CalendarPage', { title: '/calendar' }, [
52 Calendar(state),
53 Events(state, api)
54 ])
55
56 page.scroll = (i) => scroll(state.range, i)
57
58 return page
59 }
60
61 function Query (opts) {
62 return api.sbot.pull.stream(server => server.query.read(opts))
63 }
64}
65
66function scroll (range, i) {
67 const { gte, lt } = resolve(range)
68
69 if (isMonthInterval(gte, lt)) {
70 range.gte.set(new Date(gte.getFullYear(), gte.getMonth() + i, gte.getDate()))
71 range.lt.set(new Date(lt.getFullYear(), lt.getMonth() + i, lt.getDate()))
72 return
73 }
74
75 if (isWeekInterval(gte, lt)) {
76 range.gte.set(new Date(gte.getFullYear(), gte.getMonth(), gte.getDate() + 7 * i))
77 range.lt.set(new Date(lt.getFullYear(), lt.getMonth(), lt.getDate() + 7 * i))
78 return
79 }
80
81 range.gte.set(new Date(gte.getFullYear(), gte.getMonth(), gte.getDate() + i))
82 range.lt.set(new Date(lt.getFullYear(), lt.getMonth(), lt.getDate() + i))
83
84 function isMonthInterval (gte, lt) {
85 return gte.getDate() === 1 && // 1st of month
86 lt.getDate() === 1 && // to the 1st of the month
87 gte.getMonth() + 1 === lt.getMonth() && // one month gap
88 gte.getFullYear() === lt.getFullYear()
89 }
90
91 function isWeekInterval (gte, lt) {
92 return gte.getDay() === 1 && // from monday
93 lt.getDay() === 1 && // to just inside monday
94 new Date(gte.getFullYear(), gte.getMonth(), gte.getDate() + 7).toISOString() === lt.toISOString()
95 }
96}
97
98function Events (state, api) {
99 return h('CalendarEvents', computed([state.events, state.range], (events, range) => {
100 const keys = events
101 .filter(ev => ev.date >= range.gte && ev.date < range.lt)
102 .sort((a, b) => a.date - b.date)
103 .map(ev => ev.data.key)
104
105 const gatherings = MutantArray([])
106
107 pull(
108 pull.values(keys),
109 pull.asyncMap((key, cb) => {
110 api.sbot.async.get(key, (err, value) => {
111 if (err) return cb(err)
112
113 if (typeof value.content === 'object') cb(null, {key, value})
114 else cb(null, api.message.sync.unbox({key, value}))
115 })
116 }),
117 pull.drain(msg => gatherings.push(msg))
118 )
119
120 return map(gatherings, g => api.message.html.render(g))
121 }))
122}
123
124function watchAttending (attending, api) {
125 const myKey = api.keys.sync.id()
126
127 const query = [{
128 $filter: {
129 value: {
130 author: myKey,
131 content: {
132 type: 'about',
133 about: { $is: 'string' },
134 attendee: { link: myKey }
135 }
136 }
137 }
138 }, {
139 $map: {
140 key: ['value', 'content', 'about'], // gathering
141 rm: ['value', 'content', 'attendee', 'remove']
142 }
143 }]
144
145 const opts = { reverse: false, live: true, query }
146
147 pull(
148 api.sbot.pull.stream(server => server.query.read(opts)),
149 pull.filter(m => !m.sync),
150 pull.filter(Boolean),
151 pull.drain(({ key, rm }) => {
152 var hasKey = attending.includes(key)
153
154 if (!hasKey && !rm) attending.push(key)
155 else if (hasKey && rm) attending.delete(key)
156 })
157 )
158}
159
160
161function getGatherings (year, events, Query) {
162 // gatherings specify times with `about` messages which have a startDateTime
163 // NOTE - this gets a window of about messages around the current year but does not gaurentee
164 // that we got all events in this year (e.g. something booked 6 months agead would be missed)
165 const query = [{
166 $filter: {
167 value: {
168 timestamp: { // ordered by published time
169 $gt: Number(new Date(year - 1, 11, 1)),
170 $lt: Number(new Date(year + 1, 0, 1))
171 },
172 content: {
173 type: 'about',
174 startDateTime: {
175 epoch: {$is: 'number'}
176 }
177 }
178 }
179 }
180 }, {
181 $map: {
182 key: ['value', 'content', 'about'],
183 date: ['value', 'content', 'startDateTime', 'epoch'],
184 ts: ['value', 'timestamp']
185 }
186 }]
187 const opts = { reverse: false, live: true, query }
188
189 var target
190 pull(
191 Query(opts),
192 pull.filter(m => !m.sync),
193 pull.filter(m => m.date > 0 && Number.isInteger(m.date)),
194 pull.map(m => {
195 m.date = new Date(m.date)
196 return m
197 }),
198 pull.drain(({ key, date, ts }) => {
199 target = events.find(ev => ev.data.key === key)
200 if (target && target.data.ts <= ts) events.delete(target)
201 // TODO causally sorted about messages
202 // could do this with a backlinks query, paramap'd
203
204
205 events.push({ date, data: { key, ts } })
206 })
207 )
208}
209
210// Thanks to nomand for the inspiration and code (https://github.com/nomand/Letnice),
211// Calendar takes events of format { date: Date, data: { attending: Boolean, ... } }
212
213const MONTH_NAMES = [ 'Ja', 'Fe', 'Ma', 'Ap', 'Ma', 'Ju', 'Ju', 'Au', 'Se', 'Oc', 'No', 'De' ]
214
215function Calendar (state) {
216 // TODO assert events is an Array of object
217 // of form { date, data }
218
219 return h('Calendar', [
220 h('div.header', [
221 h('div.year', [
222 state.year,
223 h('a', { 'ev-click': () => state.year.set(state.year() - 1) }, '-'),
224 h('a', { 'ev-click': () => state.year.set(state.year() + 1) }, '+')
225 ])
226 ]),
227 h('div.months', computed(throttle(state, 100), ({ today, year, events, attending, range }) => {
228 events = events.map(ev => {
229 ev.data.attending = attending.includes(ev.data.key)
230 return ev
231 })
232
233 return Array(12).fill(0).map((_, i) => {
234 const setMonthRange = (ev) => {
235 onSelect({
236 gte: new Date(year, i, 1),
237 lt: new Date(year, i + 1, 1)
238 })
239 }
240
241 return h('div.month', [
242 h('div.month-name', { 'ev-click': setMonthRange }, MONTH_NAMES[i]),
243 Month({ year, monthIndex: i, events, range, onSelect, styles: {weekFormat: 'columns'} })
244 ])
245 })
246 }))
247 ])
248
249 function onSelect ({ gte, lt }) {
250 state.range.set({ gte, lt })
251 }
252}
253
254function startOfDay (d = new Date()) {
255 return new Date(d.getFullYear(), d.getMonth(), d.getDate())
256}
257
258function endOfDay (d = new Date()) {
259 return new Date(d.getFullYear(), d.getMonth(), d.getDate() + 1)
260}
261

Built with git-ssb-web