Files: 03ce3474ce130d4761d364412f79db838997ddb9 / about / html / edit.js
6650 bytesRaw
1 | const nest = require('depnest') |
2 | const dataurl = require('dataurl-') |
3 | const hyperfile = require('hyperfile') |
4 | const hypercrop = require('hypercrop') |
5 | const { |
6 | h, Value, Dict, Struct, |
7 | map, computed, when, dictToCollection |
8 | } = require('mutant') |
9 | const pull = require('pull-stream') |
10 | |
11 | exports.gives = nest('about.html.edit') |
12 | |
13 | exports.needs = nest({ |
14 | 'about.obs': { |
15 | name: 'first', |
16 | imageUrl: 'first', |
17 | description: 'first', |
18 | latestValue: 'first', |
19 | groupedValues: 'first' |
20 | }, |
21 | 'app.html.modal': 'first', |
22 | 'blob.sync.url': 'first', |
23 | 'keys.sync.id': 'first', |
24 | 'message.html.confirm': 'first', |
25 | 'message.html.markdown': 'first', |
26 | sbot: { |
27 | 'async.addBlob': 'first', |
28 | 'pull.links': 'first' |
29 | } |
30 | }) |
31 | |
32 | exports.create = function (api) { |
33 | return nest({ |
34 | 'about.html.edit': edit |
35 | }) |
36 | |
37 | // TODO refactor this to use obs better |
38 | function edit (id) { |
39 | // TODO - get this to wait till the connection is present ! |
40 | |
41 | var isMe = api.keys.sync.id() === id |
42 | |
43 | var avatar = Struct({ |
44 | current: api.about.obs.imageUrl(id), |
45 | new: Dict() |
46 | }) |
47 | |
48 | const links = api.sbot.pull.links |
49 | |
50 | var name = Struct({ |
51 | current: api.about.obs.name(id), |
52 | new: Value() |
53 | }) |
54 | |
55 | const images = computed(api.about.obs.groupedValues(id, 'image'), Object.keys) |
56 | |
57 | var namesRecord = Dict() |
58 | // TODO constrain query to one name per peer? |
59 | pull( |
60 | links({dest: id, rel: 'about', values: true}), |
61 | pull.map(e => e.value.content.name), |
62 | pull.filter(Boolean), |
63 | pull.drain(name => { |
64 | var n = namesRecord.get(name) || 0 |
65 | namesRecord.put(name, n + 1) |
66 | }) |
67 | ) |
68 | var names = dictToCollection(namesRecord) |
69 | |
70 | var publicWebHosting = Struct({ |
71 | current: api.about.obs.latestValue(id, 'publicWebHosting'), |
72 | new: Value(api.about.obs.latestValue(id, 'publicWebHosting')()) |
73 | }) |
74 | |
75 | var isPossibleUpdate = computed([name.new, avatar.new, publicWebHosting.new], (name, avatar, publicWebHostingValue) => { |
76 | return name || avatar.link || (isMe && publicWebHostingValue !== publicWebHosting.current()) |
77 | }) |
78 | |
79 | var avatarSrc = computed([avatar], avatar => { |
80 | if (avatar.new.link) return api.blob.sync.url(avatar.new.link) |
81 | return avatar.current |
82 | }) |
83 | |
84 | var displayedName = computed([name], name => { |
85 | if (name.new) return name.new |
86 | else return name.current |
87 | }) |
88 | |
89 | const modalContent = Value() |
90 | const isOpen = Value(false) |
91 | const modal = api.app.html.modal(modalContent, { isOpen }) |
92 | |
93 | return h('AboutEditor', [ |
94 | modal, |
95 | h('section.avatar', [ |
96 | h('section', [ |
97 | h('img', { src: avatarSrc }) |
98 | ]), |
99 | h('footer', displayedName) |
100 | ]), |
101 | h('section.description', computed(api.about.obs.description(id), (descr) => { |
102 | if (descr == null) return '' // TODO: should be in patchcore, I think... |
103 | return api.message.html.markdown(descr) |
104 | })), |
105 | h('section.aliases', [ |
106 | h('header', 'Aliases'), |
107 | h('section.avatars', [ |
108 | h('header', 'Avatars'), |
109 | map(images, image => h('img', { |
110 | 'src': api.blob.sync.url(image), |
111 | 'ev-click': () => avatar.new.set({ link: image }) |
112 | })), |
113 | h('div.file-upload', [ |
114 | hyperfile.asDataURL(dataUrlCallback) |
115 | ]) |
116 | ]), |
117 | h('section.names', [ |
118 | h('header', 'Names'), |
119 | h('section', [ |
120 | map(names, n => h('div', { 'ev-click': () => name.new.set(n.key()) }, [ |
121 | h('div.name', n.key), |
122 | h('div.count', n.value) |
123 | ])), |
124 | h('input', { |
125 | placeholder: ' + another name', |
126 | 'ev-keyup': e => name.new.set(e.target.value) |
127 | }) |
128 | ]) |
129 | ]), |
130 | isMe |
131 | ? h('section.viewer', [ |
132 | h('header', 'Public viewers'), |
133 | h('section', [ |
134 | h('span', 'Show my posts on public viewers'), |
135 | h('input', { |
136 | type: 'checkbox', |
137 | checked: publicWebHosting.current, |
138 | 'ev-change': e => publicWebHosting.new.set(e.target.checked) |
139 | }) |
140 | ]) |
141 | ]) : '', |
142 | when(isPossibleUpdate, h('section.action', [ |
143 | h('button.cancel', { 'ev-click': clearNewSelections }, 'cancel'), |
144 | h('button.confirm', { 'ev-click': handleUpdateClick }, 'confirm changes') |
145 | ])) |
146 | ]) |
147 | ]) |
148 | |
149 | function dataUrlCallback (data) { |
150 | const cropEl = Crop(data, (err, cropData) => { |
151 | if (err) throw err |
152 | if (!cropData) return isOpen.set(false) |
153 | |
154 | var _data = dataurl.parse(cropData) |
155 | api.sbot.async.addBlob(pull.once(_data.data), (err, hash) => { |
156 | if (err) throw err // TODO check if this is safely caught by error catcher |
157 | |
158 | avatar.new.set({ |
159 | link: hash, |
160 | size: _data.data.length, |
161 | type: _data.mimetype, |
162 | width: 512, |
163 | height: 512 |
164 | }) |
165 | }) |
166 | isOpen.set(false) |
167 | }) |
168 | |
169 | modalContent.set(cropEl) |
170 | isOpen.set(true) |
171 | } |
172 | |
173 | function Crop (data, cb) { |
174 | var img = h('img', { src: data }) |
175 | |
176 | var crop = Value() |
177 | |
178 | waitForImg() |
179 | |
180 | return h('div.cropper', [ |
181 | crop, |
182 | h('div.background') |
183 | ]) |
184 | |
185 | function waitForImg () { |
186 | // WEIRDNESS - if you invoke hypecrop before img is ready, |
187 | // the canvas instantiates and draws nothing |
188 | |
189 | if (!img.height && !img.width) { |
190 | return window.setTimeout(waitForImg, 100) |
191 | } |
192 | |
193 | var canvas = hypercrop(img) |
194 | crop.set( |
195 | h('PatchProfileCrop', [ |
196 | h('header', 'click and drag to crop your image'), |
197 | canvas, |
198 | h('section.actions', [ |
199 | h('button', { 'ev-click': () => cb() }, 'Cancel'), |
200 | h('button -primary', { 'ev-click': () => cb(null, canvas.selection.toDataURL()) }, 'Okay') |
201 | ]) |
202 | ]) |
203 | ) |
204 | } |
205 | } |
206 | |
207 | function clearNewSelections () { |
208 | name.new.set(null) |
209 | avatar.new.set({}) |
210 | publicWebHosting.new.set(publicWebHosting.current()) |
211 | } |
212 | |
213 | function handleUpdateClick () { |
214 | const newName = name.new() |
215 | const newAvatar = avatar.new() |
216 | |
217 | const msg = { |
218 | type: 'about', |
219 | about: id |
220 | } |
221 | |
222 | if (newName) msg.name = newName |
223 | if (newAvatar.link) msg.image = newAvatar |
224 | if (publicWebHosting.new() !== publicWebHosting.current()) msg.publicWebHosting = publicWebHosting.new() |
225 | |
226 | api.message.html.confirm(msg, (err, data) => { |
227 | if (err) return console.error(err) |
228 | |
229 | clearNewSelections() |
230 | |
231 | // TODO - update aliases displayed |
232 | }) |
233 | } |
234 | } |
235 | } |
236 |
Built with git-ssb-web