git ssb

16+

Dominic / patchbay



Tree: 9ed5958b4528f84958df37e404fc8ec072d3632b

Files: 9ed5958b4528f84958df37e404fc8ec072d3632b / app / page / calendar.js

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

Built with git-ssb-web