git ssb

1+

Daan Patchwork / patchwork



Tree: 362cac1fe4ece2414ba2a455ab52e09dbd540562

Files: 362cac1fe4ece2414ba2a455ab52e09dbd540562 / lib / main-window.js

13330 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')
19const fullscreen = require('./fullscreen.js')
20
21const localTimezone = moment.tz.guess()
22moment.tz.setDefault(localTimezone)
23
24module.exports = function (config) {
25 const sockets = combine(
26 overrideConfig(config),
27 addCommand('app.navigate', navigate),
28 require('./depject'),
29 require('patch-settings')
30 )
31
32 const api = entry(sockets, nest({
33 'config.sync.load': 'first',
34 'keys.sync.id': 'first',
35 'sbot.obs.connection': 'first',
36 'sbot.async.get': 'first',
37 'blob.sync.url': 'first',
38 'page.html.render': 'first',
39 'app.html.search': 'first',
40 'app.html.channels': 'first',
41 'app.views': '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 dracula: 'dracula'
162 }
163
164 const syntaxTheme = syntaxThemeOptions[themeName] || syntaxThemeOptions.light
165 return requireStyle(`highlight.js/styles/${syntaxTheme}.css`)
166 })
167 })
168 )
169
170 document.head.appendChild(
171 h('style', {
172 innerHTML: requireStyle('noto-color-emoji')
173 })
174 )
175
176 document.head.appendChild(
177 h('style', {
178 innerHTML: computed(api.settings.obs.get('patchwork.fontSize'), size => {
179 if (size) {
180 return 'html, body {font-size: ' + size + ';}'
181 }
182 })
183 })
184 )
185
186 document.head.appendChild(
187 h('style', {
188 innerHTML: computed(api.settings.obs.get('patchwork.fontFamily'), family => {
189 if (family) {
190 return 'body, input, select { font-family: ' + family + ';}'
191 }
192 })
193 })
194 )
195
196 const container = h(`MainWindow -${process.platform}`, {
197 classList: [when(fullscreen(), '-fullscreen')],
198 'ev-dragover': preventDefault,
199 'ev-drop': preventDefault,
200 'ev-dragstart': preventDefaultUnlessImage
201 }, [
202 h('div.top', [
203 h('span.history', [
204 h('a', {
205 'ev-click': views.goBack,
206 classList: [when(views.canGoBack, '-active')]
207 }),
208 h('a', {
209 'ev-click': views.goForward,
210 classList: [when(views.canGoForward, '-active')]
211 })
212 ]),
213 h('span.nav', [
214 tab(i18n('Public'), '/public'),
215 tab(i18n('Private'), '/private'),
216 dropTab(i18n('More'), [
217 getSubscribedChannelMenu,
218 subMenu(i18n('Participating'), [
219 [i18n('All Threads'), '/participating'],
220 [i18n('Threads Started By You'), '/your-posts']
221 ]),
222 subMenu(i18n('Gatherings'), [
223 [i18n('All'), '/gatherings'],
224 [i18n('Attending'), '/attending-gatherings']
225 ]),
226 [i18n('Tags'), `/tags/all/${encodeURIComponent(id)}`],
227 [i18n('Extended Network'), '/all'],
228 { separator: true },
229 [i18n('Settings'), '/settings']
230 ])
231 ]),
232 h('span.appTitle', [
233 h('span.title', i18n('Patchwork')),
234 api.app.html.progressNotifier()
235 ]),
236 h('span', [api.app.html.search(api.app.navigate)]),
237 h('span.nav', [
238 tab(i18n('Profile'), id),
239 computed(includeParticipating, (includeParticipating) => {
240 if (includeParticipating) return tab(i18n('Participating'), '/participating')
241 }),
242 tab(i18n('Mentions'), '/mentions')
243 ])
244 ]),
245 when(latestUpdate,
246 h('div.info', [
247 h('a.message -update', { href: 'https://github.com/ssbc/patchwork/releases/latest' }, [
248 h('strong', ['Patchwork ', latestUpdate, i18n(' has been released.')]), i18n(' Click here to download and view more info!'),
249 h('a.ignore', { 'ev-click': latestUpdate.ignore }, 'X')
250 ])
251 ])
252 ),
253 views.html
254 ])
255
256 const previewElement = api.app.linkPreview(container, 500)
257
258 catchLinks(container, (href, external, anchor) => {
259 if (!href) return
260 if (external) {
261 electron.shell.openExternal(href)
262 } else {
263 api.app.navigate(href, anchor)
264 }
265 })
266 return [container, previewElement]
267
268 // scoped
269
270 function subMenu (label, items) {
271 return function () {
272 return {
273 label,
274 submenu: items.map(item => {
275 return {
276 label: item[0],
277 click () {
278 navigate(item[1])
279 }
280 }
281 })
282 }
283 }
284 }
285
286 function getSubscribedChannelMenu () {
287 const channels = Array.from(subscribedChannels()).sort(localeCompare)
288
289 if (channels.length) {
290 return {
291 label: i18n('Channels'),
292 submenu: [
293 {
294 label: i18n('Browse Recently Active'),
295 click () {
296 navigate('/channels')
297 }
298 },
299 { type: 'separator' }
300 ].concat(channels.map(channel => {
301 return {
302 label: `#${channel}`,
303 click () {
304 navigate(`#${channel}`)
305 }
306 }
307 }))
308 }
309 } else {
310 return {
311 label: i18n('Browse Channels'),
312 click () {
313 navigate('/channels')
314 }
315 }
316 }
317 }
318
319 function dropTab (title, items) {
320 const element = h('a -drop', {
321 'ev-click': () => {
322 const rects = element.getBoundingClientRect()
323 const factor = electron.remote.getCurrentWindow().webContents.zoomFactor
324 const menu = electron.remote.Menu.buildFromTemplate(items.map(item => {
325 if (typeof item === 'function') {
326 return item()
327 } else if (item.separator) {
328 return { type: 'separator' }
329 } else {
330 return {
331 label: item[0],
332 click () {
333 navigate(item[1])
334 }
335 }
336 }
337 }))
338 menu.popup({
339 window: electron.remote.getCurrentWindow(),
340 x: Math.round(rects.left * factor),
341 y: Math.round(rects.bottom * factor) + 4
342 })
343 }
344 }, title)
345 return element
346 }
347
348 function navigate (href, anchor) {
349 if (typeof href !== 'string') return false
350 getExternalHandler(href, (err, handler) => {
351 if (!err && handler) {
352 handler(href)
353 } else {
354 if (href.startsWith('ssb:')) {
355 try {
356 href = ssbUri.toSigilLink(href)
357 } catch (e) {
358 // error can be safely ignored
359 // it just means this isn't an SSB URI
360 }
361 }
362
363 // no external handler found, use page.html.render
364 previewElement.cancel()
365 views.setView(href, anchor)
366 }
367 })
368 }
369
370 function getExternalHandler (href, cb) {
371 const link = ref.parseLink(href)
372 if (link && ref.isMsg(link.link)) {
373 const params = { id: link.link }
374 if (link.query && link.query.unbox) {
375 params.private = true
376 params.unbox = link.query.unbox
377 }
378 api.sbot.async.get(params, function (err, value) {
379 if (err) return cb(err)
380 cb(null, api.app.sync.externalHandler({ key: link.link, value, query: link.query }))
381 })
382 } else if (link && ref.isBlob(link.link)) {
383 cb(null, function (href) {
384 electron.shell.openExternal(api.blob.sync.url(href))
385 })
386 } else {
387 cb()
388 }
389 }
390
391 function tab (name, view) {
392 const instance = views.get(view)
393 return h('a', {
394 'ev-click': function () {
395 const instance = views.get(view)
396 const isSelected = views.currentView() === view
397 const needsRefresh = instance && instance.pendingUpdates && instance.pendingUpdates()
398
399 // refresh if tab is clicked when there are pending items or the page is already selected
400 if ((needsRefresh || isSelected) && instance.reload) {
401 instance.reload()
402 }
403 },
404 href: view,
405 classList: [
406 when(selected(view), '-selected')
407 ]
408 }, [
409 name,
410 instance
411 ? when(instance.pendingUpdates, [' (', instance.pendingUpdates, ')'])
412 : null
413 ])
414 }
415
416 function selected (view) {
417 return computed([views.currentView, view], (currentView, view) => {
418 return currentView === view
419 })
420 }
421
422 function getMessageText (id, cb) {
423 api.sbot.async.get(id, (err, value) => {
424 if (err) return cb(err)
425 if (value.content.type === 'gathering') {
426 api.about.async.latestValues(id, ['title', 'description'], (err, values) => {
427 if (err) return cb(err)
428 const text = `# ${values.title}\n\n${values.description}`
429 cb(null, text)
430 })
431 } else {
432 cb(null, value.content.text)
433 }
434 })
435 }
436}
437
438function overrideConfig (config) {
439 return {
440 'patchwork/config': {
441 gives: nest('config.sync.load'),
442 create: function () {
443 return nest('config.sync.load', () => config)
444 }
445 }
446 }
447}
448
449function addCommand (id, cb) {
450 return {
451 [`patchwork/command/${id}`]: {
452 gives: nest(id),
453 create: function () {
454 return nest(id, cb)
455 }
456 }
457 }
458}
459
460function localeCompare (a, b) {
461 return a.localeCompare(b)
462}
463
464function preventDefault (ev) {
465 ev.preventDefault()
466}
467
468function preventDefaultUnlessImage (ev) {
469 if (ev.target.nodeName !== 'IMG') {
470 ev.preventDefault()
471 }
472}
473

Built with git-ssb-web