git ssb

1+

Daan Patchwork / patchwork



Tree: d3826fa5570d6a70af4cd9401fe6f9284ec0ef5c

Files: d3826fa5570d6a70af4cd9401fe6f9284ec0ef5c / lib / main-window.js

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

Built with git-ssb-web