git ssb

1+

Daan Patchwork / patchwork



Tree: 9b514132e92bd744e5003fc4f189a46ed5cdbf7e

Files: 9b514132e92bd744e5003fc4f189a46ed5cdbf7e / lib / main-window.js

13176 bytesRaw
1const combine = require('depject')
2const entry = require('depject/entry')
3const electron = require('electron')
4const h = require('mutant/h')
5const when = require('mutant/when')
6const onceTrue = require('mutant/once-true')
7const computed = require('mutant/computed')
8const catchLinks = require('./catch-links')
9const themes = require('../styles')
10const nest = require('depnest')
11const LatestUpdate = require('./latest-update')
12const ref = require('ssb-ref')
13const watch = require('mutant/watch')
14const requireStyle = require('require-style')
15const ssbUri = require('ssb-uri')
16const pull = require('pull-stream')
17const moment = require('moment-timezone')
18const fullscreen = require('./fullscreen.js')
19const appViews = require('./app/views.js')
20const getGitHandler = require('./app/sync/external-handler/git')
21
22const localTimezone = moment.tz.guess()
23moment.tz.setDefault(localTimezone)
24
25module.exports = function (config) {
26 const sockets = combine(
27 overrideConfig(config),
28 addCommand('app.navigate', navigate),
29 require('./depject'),
30 require('patch-settings')
31 )
32
33 const api = entry(sockets, nest({
34 'config.sync.load': 'first',
35 'keys.sync.id': 'first',
36 'sbot.obs.connection': 'first',
37 'sbot.async.get': 'first',
38 'blob.sync.url': 'first',
39 'page.html.render': 'first',
40 'app.html.search': 'first',
41 'app.html.channels': 'first',
42 'app.html.progressNotifier': 'first',
43 'about.async.latestValues': 'first',
44 'profile.sheet.edit': 'first',
45 'profile.html.preview': 'first',
46 'app.navigate': 'first',
47 'app.linkPreview': 'first',
48 'channel.obs.subscribed': 'first',
49 'settings.obs.get': 'first',
50 'intl.sync.i18n': 'first',
51 'contact.obs.blocking': 'first'
52 }))
53
54 const i18n = api.intl.sync.i18n
55 const language = api.settings.obs.get('patchwork.lang', '')()
56 moment.locale(language)
57
58 const id = api.keys.sync.id()
59 const latestUpdate = LatestUpdate()
60 const subscribedChannels = api.channel.obs.subscribed(id)
61 const includeParticipating = api.settings.obs.get('patchwork.includeParticipating', false)
62 const autoDeleteBlocked = api.settings.obs.get('patchwork.autoDeleteBlocked', false)
63
64 // prompt to setup profile on first use
65 onceTrue(api.sbot.obs.connection, (ssb) => {
66 ssb.latestSequence(api.keys.sync.id(), (err, key) => {
67 if (err) {
68 // This may throw an error if the feed doesn't have any messages, but
69 // that shouldn't cause any problems so this error can be ignored.
70 }
71
72 if (key == null) {
73 api.profile.sheet.edit({ usePreview: false })
74 }
75 })
76
77 const currentlyDeleting = {}
78
79 watch(api.contact.obs.blocking(id), (blocking) => {
80 if (autoDeleteBlocked() !== true) return
81 if (blocking.length === 0) return
82
83 if (ssb.del == null) return
84
85 blocking
86 .filter(feedId => feedId !== ssb.id)
87 .forEach(feed => {
88 pull(
89 ssb.createUserStream({ id: feed }),
90 pull.asyncMap((msg, cb) => {
91 const key = msg.key
92 if (currentlyDeleting[key] === true) {
93 return cb(null, null) // already deleting
94 }
95
96 currentlyDeleting[key] = true
97
98 ssb.del(key, (err) => {
99 currentlyDeleting[key] = false
100 cb(err, key)
101 })
102 }),
103 pull.filter(),
104 pull.collect((err, keys) => {
105 if (err) {
106 console.error(keys)
107 throw err
108 }
109
110 if (keys.length > 0) {
111 console.log(`deleted ${keys.length} messages from blocked authors`)
112 }
113 })
114 )
115 })
116 })
117 })
118
119 const defaultViews = computed(includeParticipating, (includeParticipating) => {
120 const result = [
121 '/public', '/private', '/mentions'
122 ]
123
124 // allow user to choose in settings whether to show participating tab
125 if (includeParticipating) {
126 result.push('/participating')
127 }
128
129 return result
130 })
131
132 const views = appViews(api.page.html.render, defaultViews)
133
134 const pendingCount = views.get('/mentions').pendingUpdates
135
136 watch(pendingCount, count => {
137 electron.remote.app.badgeCount = count
138 })
139
140 electron.ipcRenderer.on('goForward', views.goForward)
141 electron.ipcRenderer.on('goBack', views.goBack)
142
143 electron.ipcRenderer.on('goToSettings', () => api.app.navigate('/settings'))
144
145 document.head.appendChild(
146 h('style', {
147 innerHTML: computed(api.settings.obs.get('patchwork.theme', 'light'), themeName => {
148 return themes[themeName] || themes.light
149 })
150 })
151 )
152
153 document.head.appendChild(
154 h('style', {
155 innerHTML: computed(api.settings.obs.get('patchwork.theme', 'light'), themeName => {
156 const syntaxThemeOptions = {
157 light: 'github',
158 dark: 'monokai',
159 dracula: 'dracula'
160 }
161
162 const syntaxTheme = syntaxThemeOptions[themeName] || syntaxThemeOptions.light
163 return requireStyle(`highlight.js/styles/${syntaxTheme}.css`)
164 })
165 })
166 )
167
168 document.head.appendChild(
169 h('style', {
170 innerHTML: requireStyle('noto-color-emoji')
171 })
172 )
173
174 document.head.appendChild(
175 h('style', {
176 innerHTML: computed(api.settings.obs.get('patchwork.fontSize'), size => {
177 if (size) {
178 return 'html, body {font-size: ' + size + ';}'
179 }
180 })
181 })
182 )
183
184 document.head.appendChild(
185 h('style', {
186 innerHTML: computed(api.settings.obs.get('patchwork.fontFamily'), family => {
187 if (family) {
188 return 'body, input, select { font-family: ' + family + ';}'
189 }
190 })
191 })
192 )
193
194 const container = h(`MainWindow -${process.platform}`, {
195 classList: [when(fullscreen(), '-fullscreen')],
196 'ev-dragover': preventDefault,
197 'ev-drop': preventDefault,
198 'ev-dragstart': preventDefaultUnlessImage
199 }, [
200 h('div.top', [
201 h('span.history', [
202 h('a', {
203 'ev-click': views.goBack,
204 classList: [when(views.canGoBack, '-active')]
205 }),
206 h('a', {
207 'ev-click': views.goForward,
208 classList: [when(views.canGoForward, '-active')]
209 })
210 ]),
211 h('span.nav', [
212 tab(i18n('Public'), '/public'),
213 tab(i18n('Private'), '/private'),
214 dropTab(i18n('More'), [
215 getSubscribedChannelMenu,
216 subMenu(i18n('Participating'), [
217 [i18n('All Threads'), '/participating'],
218 [i18n('Threads Started By You'), '/your-posts']
219 ]),
220 subMenu(i18n('Gatherings'), [
221 [i18n('All'), '/gatherings'],
222 [i18n('Attending'), '/attending-gatherings']
223 ]),
224 [i18n('Tags'), `/tags/all/${encodeURIComponent(id)}`],
225 [i18n('Extended Network'), '/all'],
226 { separator: true },
227 [i18n('Settings'), '/settings']
228 ])
229 ]),
230 h('span.appTitle', [
231 h('span.title', i18n('Patchwork')),
232 api.app.html.progressNotifier()
233 ]),
234 h('span', [api.app.html.search(api.app.navigate)]),
235 h('span.nav', [
236 tab(i18n('Profile'), id),
237 computed(includeParticipating, (includeParticipating) => {
238 if (includeParticipating) return tab(i18n('Participating'), '/participating')
239 }),
240 tab(i18n('Mentions'), '/mentions')
241 ])
242 ]),
243 when(latestUpdate,
244 h('div.info', [
245 h('a.message -update', { href: 'https://github.com/ssbc/patchwork/releases/latest' }, [
246 h('strong', ['Patchwork ', latestUpdate, i18n(' has been released.')]), i18n(' Click here to download and view more info!'),
247 h('a.ignore', { 'ev-click': latestUpdate.ignore }, 'X')
248 ])
249 ])
250 ),
251 views.html
252 ])
253
254 const previewElement = api.app.linkPreview(container, 500)
255
256 catchLinks(container, (href, external, anchor) => {
257 if (!href) return
258 if (external) {
259 electron.shell.openExternal(href)
260 } else {
261 api.app.navigate(href, anchor)
262 }
263 })
264 return [container, previewElement]
265
266 // scoped
267
268 function subMenu (label, items) {
269 return function () {
270 return {
271 label,
272 submenu: items.map(item => {
273 return {
274 label: item[0],
275 click () {
276 navigate(item[1])
277 }
278 }
279 })
280 }
281 }
282 }
283
284 function getSubscribedChannelMenu () {
285 const channels = Array.from(subscribedChannels()).sort(localeCompare)
286
287 if (channels.length) {
288 return {
289 label: i18n('Channels'),
290 submenu: [
291 {
292 label: i18n('Browse Recently Active'),
293 click () {
294 navigate('/channels')
295 }
296 },
297 { type: 'separator' }
298 ].concat(channels.map(channel => {
299 return {
300 label: `#${channel}`,
301 click () {
302 navigate(`#${channel}`)
303 }
304 }
305 }))
306 }
307 } else {
308 return {
309 label: i18n('Browse Channels'),
310 click () {
311 navigate('/channels')
312 }
313 }
314 }
315 }
316
317 function dropTab (title, items) {
318 const element = h('a -drop', {
319 'ev-click': () => {
320 const rects = element.getBoundingClientRect()
321 const factor = electron.remote.getCurrentWindow().webContents.zoomFactor
322 const menu = electron.remote.Menu.buildFromTemplate(items.map(item => {
323 if (typeof item === 'function') {
324 return item()
325 } else if (item.separator) {
326 return { type: 'separator' }
327 } else {
328 return {
329 label: item[0],
330 click () {
331 navigate(item[1])
332 }
333 }
334 }
335 }))
336 menu.popup({
337 window: electron.remote.getCurrentWindow(),
338 x: Math.round(rects.left * factor),
339 y: Math.round(rects.bottom * factor) + 4
340 })
341 }
342 }, title)
343 return element
344 }
345
346 function navigate (href, anchor) {
347 if (typeof href !== 'string') return false
348 getExternalHandler(href, (err, handler) => {
349 if (!err && handler) {
350 handler(href)
351 } else {
352 if (href.startsWith('ssb:')) {
353 try {
354 href = ssbUri.toSigilLink(href)
355 } catch (e) {
356 // error can be safely ignored
357 // it just means this isn't an SSB URI
358 }
359 }
360
361 // no external handler found, use page.html.render
362 previewElement.cancel()
363 views.setView(href, anchor)
364 }
365 })
366 }
367
368 function getExternalHandler (href, cb) {
369 const link = ref.parseLink(href)
370 if (link && ref.isMsg(link.link)) {
371 const params = { id: link.link }
372 if (link.query && link.query.unbox) {
373 params.private = true
374 params.unbox = link.query.unbox
375 }
376 api.sbot.async.get(params, function (err, value) {
377 if (err) return cb(err)
378 cb(null, getGitHandler({ key: link.link, value, query: link.query }))
379 })
380 } else if (link && ref.isBlob(link.link)) {
381 cb(null, function (href) {
382 electron.shell.openExternal(api.blob.sync.url(href))
383 })
384 } else {
385 cb()
386 }
387 }
388
389 function tab (name, view) {
390 const instance = views.get(view)
391 return h('a', {
392 'ev-click': function () {
393 const instance = views.get(view)
394 const isSelected = views.currentView() === view
395 const needsRefresh = instance && instance.pendingUpdates && instance.pendingUpdates()
396
397 // refresh if tab is clicked when there are pending items or the page is already selected
398 if ((needsRefresh || isSelected) && instance.reload) {
399 instance.reload()
400 }
401 },
402 href: view,
403 classList: [
404 when(selected(view), '-selected')
405 ]
406 }, [
407 name,
408 instance
409 ? when(instance.pendingUpdates, [' (', instance.pendingUpdates, ')'])
410 : null
411 ])
412 }
413
414 function selected (view) {
415 return computed([views.currentView, view], (currentView, view) => {
416 return currentView === view
417 })
418 }
419
420 function getMessageText (id, cb) {
421 api.sbot.async.get(id, (err, value) => {
422 if (err) return cb(err)
423 if (value.content.type === 'gathering') {
424 api.about.async.latestValues(id, ['title', 'description'], (err, values) => {
425 if (err) return cb(err)
426 const text = `# ${values.title}\n\n${values.description}`
427 cb(null, text)
428 })
429 } else {
430 cb(null, value.content.text)
431 }
432 })
433 }
434}
435
436function overrideConfig (config) {
437 return {
438 'patchwork/config': {
439 gives: nest('config.sync.load'),
440 create: function () {
441 return nest('config.sync.load', () => config)
442 }
443 }
444 }
445}
446
447function addCommand (id, cb) {
448 return {
449 [`patchwork/command/${id}`]: {
450 gives: nest(id),
451 create: function () {
452 return nest(id, cb)
453 }
454 }
455 }
456}
457
458function localeCompare (a, b) {
459 return a.localeCompare(b)
460}
461
462function preventDefault (ev) {
463 ev.preventDefault()
464}
465
466function preventDefaultUnlessImage (ev) {
467 if (ev.target.nodeName !== 'IMG') {
468 ev.preventDefault()
469 }
470}
471

Built with git-ssb-web