git ssb

16+

Dominic / patchbay



Tree: 5408d3118894220a8d70afa1754390b36514ba45

Files: 5408d3118894220a8d70afa1754390b36514ba45 / app / page / calendar.js

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

Built with git-ssb-web