git ssb

16+

Dominic / patchbay



Tree: fc27742b00d06e1a68fc1126ff1d8c157eb4b478

Files: fc27742b00d06e1a68fc1126ff1d8c157eb4b478 / app / page / calendar.js

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

Built with git-ssb-web