Files: 43af34efb145e2705ff168ae3e6f3cc6832314ac / revs-view.js
6338 bytesRaw
1 | require('setimmediate') |
2 | const h = require('mutant/html-element') |
3 | const Value = require('mutant/value') |
4 | const MutantArray = require('mutant/array') |
5 | const MutantMap = require('mutant/map') |
6 | const computed = require('mutant/computed') |
7 | const when = require('mutant/when') |
8 | const send = require('mutant/send') |
9 | |
10 | const pull = require('pull-stream') |
11 | const htime = require('human-time') |
12 | const ssbAvatar = require('ssb-avatar') |
13 | const memo = require('asyncmemo') |
14 | const lru = require('hashlru') |
15 | |
16 | const DB = require('./db') |
17 | const UpdateStream = require('./update-stream') |
18 | const {isDraft, arr} = require('./util') |
19 | |
20 | function markHeads(entries) { |
21 | const o = {} |
22 | entries.forEach( e => { |
23 | e.isHead = true |
24 | o[e.key] = e |
25 | }) |
26 | entries.forEach( e => { |
27 | let revBranch = e.value.content && e.value.content.revisionBranch |
28 | if (revBranch && o[revBranch]) o[revBranch].isHead = false |
29 | }) |
30 | } |
31 | |
32 | module.exports = function(ssb, drafts, me, blobsRoot, trusted_keys) { |
33 | const db = DB(ssb, drafts) |
34 | const updateStream = UpdateStream([]) |
35 | |
36 | let getAvatar = memo({cache: lru(50)}, function (id, cb) { |
37 | ssbAvatar(ssb, me, id, (err, about)=>{ |
38 | if (err) return cb(err) |
39 | let name = about.name |
40 | if (!/^@/.test(name)) name = '@' + name |
41 | let imageUrl = about.image ? `${blobsRoot}/${about.image}` : null |
42 | cb(null, {name, imageUrl}) |
43 | }) |
44 | }) |
45 | |
46 | let selection = Value() |
47 | let root = Value() |
48 | let currRoot = null |
49 | let ready = Value(false) |
50 | |
51 | function discardDraft(node) { |
52 | drafts.remove(node.key, err => { |
53 | if (err) return console.error('Unable to delete draft', node.key, err) |
54 | let hash = document.location.hash |
55 | if (!isDraft(hash.substr(1))) { |
56 | if (hash.indexOf(':') !== -1) { |
57 | document.location.hash = hash.split(':')[0] |
58 | } |
59 | selection.set('latest') |
60 | } else { |
61 | document.location.hash = '' |
62 | } |
63 | }) |
64 | } |
65 | |
66 | function html(entry) { |
67 | |
68 | function _click(handler, args) { |
69 | return { 'ev-click': send( e => handler.apply(e, args) ) } |
70 | } |
71 | |
72 | let feedId = isDraft(entry.key) ? me : entry.value.author |
73 | let authorAvatarUrl = Value(null, {defaultValue: ""}) |
74 | let authorName = Value(null, {defaultValue: feedId.substr(0,6)}) |
75 | getAvatar(feedId, (err, avatar) =>{ |
76 | if (err) return console.error(err) |
77 | authorAvatarUrl.set(avatar.imageUrl || "") |
78 | authorName.set(avatar.name) |
79 | }) |
80 | |
81 | return h(`div.rev${isDraft(entry.key) ? '.draft' : ''}${entry.isHead ? '.head' : ''}`, { |
82 | classList: computed([selection], sel => sel === entry.key ? ['selected'] : []), |
83 | 'ev-click': e => { |
84 | document.location.hash = `#${root()}:${entry.key}` |
85 | } |
86 | }, [ |
87 | h('div.avatar', { |
88 | style: { |
89 | 'background-image': computed([authorAvatarUrl], u => `url("${u}")`) |
90 | } |
91 | }, [ |
92 | ...(trusted_keys.includes(feedId) ? [h('span.trusted')] : []) |
93 | ]), |
94 | h('span.author', authorName), |
95 | h('span.timestamp', htime(new Date(entry.value.timestamp))), |
96 | h('span.node', |
97 | ((entry.value.content && entry.value.content.revisionBranch) ? |
98 | entry.value.content.revisionBranch.substr(0,6) + ' → ' : '⤜') + |
99 | entry.key.substr(0,6) |
100 | ), |
101 | ...(isDraft(entry.key) ? [h('span', {title: 'draft'}, '✎')] : []), |
102 | h('span.buttons', [ |
103 | ...(isDraft(entry.key) ? [h('button.discard', _click(discardDraft, [entry]), 'discard' )] : []) |
104 | ]) |
105 | ]) |
106 | } |
107 | |
108 | let mutantArray = MutantArray() |
109 | let selectedLatest = false |
110 | |
111 | function streamRevisions(id, syncCb) { |
112 | //console.log('streaming revisions of', id) |
113 | let drain |
114 | let entries |
115 | let synced = false |
116 | pull( |
117 | db.revisions(id, { |
118 | live: true, |
119 | sync: true |
120 | }), |
121 | updateStream({ |
122 | live: true, |
123 | sync: true, |
124 | allowUntrusted: true, |
125 | allRevisions: true, |
126 | bufferUntilSync: true |
127 | }), |
128 | drain = pull.drain( kv =>{ |
129 | if (kv.sync) { |
130 | synced = true |
131 | markHeads(entries) |
132 | mutantArray.set(entries) |
133 | return syncCb(null, entries) |
134 | } |
135 | entries = kv.revisions |
136 | if (synced) { |
137 | markHeads(entries) |
138 | mutantArray.set(entries) |
139 | if (selectedLatest) { |
140 | selection.set('latest') |
141 | } |
142 | } |
143 | }, err =>{ |
144 | if (err) console.error('Revisions stream ends with error', err) |
145 | }) |
146 | ) |
147 | return drain.abort |
148 | } |
149 | |
150 | let containerEl = h('revs', MutantMap(mutantArray, html)) |
151 | let abort |
152 | |
153 | selection( id => { |
154 | selectedLatest = false |
155 | if (id === 'latest') { |
156 | if (mutantArray.getLength() > 0) { |
157 | selection.set(mutantArray.get(0).key) |
158 | } else selection.set(null) |
159 | selectedLatest = true |
160 | } |
161 | console.log('rev selected', id) |
162 | }) |
163 | |
164 | root( id => { |
165 | if (currRoot === id) { |
166 | return ready.set(true) |
167 | } |
168 | //console.log('NEW rev root', id) |
169 | currRoot = id |
170 | |
171 | if (abort) abort() |
172 | abort = null |
173 | selection.set(null) |
174 | ready.set(false) |
175 | entries = [] |
176 | mutantArray.clear() |
177 | if (!id) return |
178 | |
179 | abort = streamRevisions(id, err => { |
180 | if (err) console.error('streaming revisions failed with error', err) |
181 | else console.log('revisions synced') |
182 | ready.set(true) |
183 | }) |
184 | }) |
185 | |
186 | containerEl.selection = selection |
187 | containerEl.root = root |
188 | containerEl.ready = ready |
189 | |
190 | return containerEl |
191 | } |
192 | |
193 | module.exports.css = ()=> ` |
194 | .rev { |
195 | cursor: alias; |
196 | position: relative; |
197 | font-size: 11px; |
198 | color: #6b6969; |
199 | background-color: #eee; |
200 | margin: 1px 1px 0 1px; |
201 | display: flex; |
202 | flex-direction: column; |
203 | align-items: flex-start; |
204 | flex-wrap: wrap; |
205 | max-height: 32px; |
206 | align-content: flex-start; |
207 | } |
208 | .rev.head { |
209 | border-width: 2px; |
210 | border-style: dotted; |
211 | border-color: rgba(0,0,0,0.1); |
212 | background: #e0e0ff; |
213 | margin-right: -1.2em; |
214 | } |
215 | .rev.head .node::after { |
216 | content: "⚈"; |
217 | margin-left: .5em; |
218 | } |
219 | .rev.selected { |
220 | color: #111110; |
221 | background: #b39254; |
222 | } |
223 | .rev .node { |
224 | position: absolute; |
225 | right: 1em; |
226 | top: .5em; |
227 | order: 3; |
228 | font-family: monospace; |
229 | font-size: 12px; |
230 | } |
231 | .rev .avatar { |
232 | margin: 0 8px; |
233 | height: 32px; |
234 | width: 32px; |
235 | border-radius: 3px; |
236 | background-size: cover; |
237 | } |
238 | .rev .author, .rev .timestamp { |
239 | width: 80px; |
240 | white-space: nowrap; |
241 | } |
242 | .rev .author { |
243 | padding-top: 3px; |
244 | } |
245 | ` |
246 |
Built with git-ssb-web