git ssb

10+

Matt McKegg / patchwork



Tree: 16adbf8a92d6cbecee6f7439562e1f486955af19

Files: 16adbf8a92d6cbecee6f7439562e1f486955af19 / main-window.js

6563 bytesRaw
1var combine = require('depject')
2var entry = require('depject/entry')
3var electron = require('electron')
4var h = require('mutant/h')
5var Value = require('mutant/value')
6var when = require('mutant/when')
7var computed = require('mutant/computed')
8var toCollection = require('mutant/dict-to-collection')
9var MutantDict = require('mutant/dict')
10var MutantMap = require('mutant/map')
11var Url = require('url')
12var insertCss = require('insert-css')
13var nest = require('depnest')
14var addSuggest = require('suggest-box')
15
16module.exports = function (config) {
17 var sockets = combine(
18 overrideConfig(config),
19 require('./modules'),
20 require('./plugs'),
21 require('patchcore'),
22 require('./overrides')
23 )
24
25 var api = entry(sockets, nest({
26 'page.html.render': 'first',
27 'keys.sync.id': 'first',
28 'blob.sync.url': 'first',
29 'profile.async.suggest': 'first',
30 'channel.async.suggest': 'first'
31 }))
32
33 var renderPage = api.page.html.render
34 var id = api.keys.sync.id()
35 var getProfileSuggestions = api.profile.async.suggest()
36 var getChannelSuggestions = api.channel.async.suggest()
37
38 var searchTimer = null
39 var searchBox = h('input.search', {
40 type: 'search',
41 placeholder: 'word, @key, #channel',
42 'ev-suggestselect': (ev) => {
43 setView(ev.detail.id)
44 searchBox.value = ev.detail.id
45 }
46 })
47
48 searchBox.oninput = function () {
49 clearTimeout(searchTimer)
50 searchTimer = setTimeout(doSearch, 500)
51 }
52
53 searchBox.onfocus = function () {
54 if (searchBox.value) {
55 doSearch()
56 }
57 }
58
59 var forwardHistory = []
60 var backHistory = []
61
62 var views = MutantDict({
63 // preload tabs (and subscribe to update notifications)
64 '/public': renderPage('/public'),
65 '/private': renderPage('/private'),
66 [id]: renderPage(id),
67 '/mentions': renderPage('/mentions')
68 })
69
70 var lastViewed = {}
71 var defaultViews = views.keys()
72
73 // delete cached view after 30 mins of last seeing
74 setInterval(() => {
75 views.keys().forEach((view) => {
76 if (!defaultViews.includes(view)) {
77 if (lastViewed[view] !== true && Date.now() - lastViewed[view] > (5 * 60e3) && view !== currentView()) {
78 views.delete(view)
79 }
80 }
81 })
82 }, 60e3)
83
84 var canGoForward = Value(false)
85 var canGoBack = Value(false)
86 var currentView = Value('/public')
87
88 var mainElement = h('div.main', MutantMap(toCollection(views), (item) => {
89 return h('div.view', {
90 hidden: computed([item.key, currentView], (a, b) => a !== b)
91 }, [ item.value ])
92 }))
93
94 insertCss(require('./styles'))
95
96 var container = h(`MainWindow -${process.platform}`, {
97 events: {
98 click: catchLinks
99 }
100 }, [
101 h('div.top', [
102 h('span.history', [
103 h('a', {
104 'ev-click': goBack,
105 classList: [ when(canGoBack, '-active') ]
106 }, '<'),
107 h('a', {
108 'ev-click': goForward,
109 classList: [ when(canGoForward, '-active') ]
110 }, '>')
111 ]),
112 h('span.nav', [
113 tab('Public', '/public'),
114 tab('Private', '/private')
115 ]),
116 h('span.appTitle', ['Patchwork']),
117 h('span', [ searchBox ]),
118 h('span.nav', [
119 tab('Profile', id),
120 tab('Mentions', '/mentions')
121 ])
122 ]),
123 mainElement
124 ])
125
126 addSuggest(searchBox, (inputText, cb) => {
127 if (inputText[0] === '@') {
128 cb(null, getProfileSuggestions(inputText.slice(1)), {idOnly: true})
129 } else if (inputText[0] === '#') {
130 cb(null, getChannelSuggestions(inputText.slice(1)))
131 }
132 }, {cls: 'SuggestBox'})
133
134 return container
135
136 // scoped
137
138 function catchLinks (ev) {
139 if (ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.defaultPrevented) {
140 return true
141 }
142
143 var anchor = null
144 for (var n = ev.target; n.parentNode; n = n.parentNode) {
145 if (n.nodeName === 'A') {
146 anchor = n
147 break
148 }
149 }
150 if (!anchor) return true
151
152 var href = anchor.getAttribute('href')
153
154 if (href) {
155 var url = Url.parse(href)
156 if (url.host) {
157 electron.shell.openExternal(href)
158 } else if (href.charAt(0) === '&') {
159 electron.shell.openExternal(api.blob.sync.url(href))
160 } else if (href !== '#') {
161 setView(href)
162 }
163 }
164
165 ev.preventDefault()
166 ev.stopPropagation()
167 }
168
169 function tab (name, view) {
170 var instance = views.get(view)
171 lastViewed[view] = true
172 return h('a', {
173 'ev-click': function (ev) {
174 if (instance.pendingUpdates && instance.pendingUpdates() && instance.reload) {
175 instance.reload()
176 }
177 },
178 href: view,
179 classList: [
180 when(selected(view), '-selected')
181 ]
182 }, [
183 name,
184 when(instance.pendingUpdates, [
185 ' (', instance.pendingUpdates, ')'
186 ])
187 ])
188 }
189
190 function goBack () {
191 if (backHistory.length) {
192 canGoForward.set(true)
193 forwardHistory.push(currentView())
194 currentView.set(backHistory.pop())
195 canGoBack.set(backHistory.length > 0)
196 }
197 }
198
199 function goForward () {
200 if (forwardHistory.length) {
201 backHistory.push(currentView())
202 currentView.set(forwardHistory.pop())
203 canGoForward.set(forwardHistory.length > 0)
204 canGoBack.set(true)
205 }
206 }
207
208 function setView (view) {
209 if (!views.has(view)) {
210 views.put(view, renderPage(view))
211 }
212
213 if (lastViewed[view] !== true) {
214 lastViewed[view] = Date.now()
215 }
216
217 if (currentView() && lastViewed[currentView()] !== true) {
218 lastViewed[currentView()] = Date.now()
219 }
220
221 if (view !== currentView()) {
222 canGoForward.set(false)
223 canGoBack.set(true)
224 forwardHistory.length = 0
225 backHistory.push(currentView())
226 currentView.set(view)
227 }
228 }
229
230 function doSearch () {
231 var value = searchBox.value.trim()
232 if (value.startsWith('/') || value.startsWith('?') || value.startsWith('@') || value.startsWith('#') || value.startsWith('%')) {
233 if (value.startsWith('@') && value.length < 30) {
234 return // probably not a key
235 } else if (value.length > 2) {
236 setView(value)
237 }
238 } else if (value.trim()) {
239 if (value.length > 2) {
240 setView(`?${value.trim()}`)
241 }
242 } else {
243 setView('/public')
244 }
245 }
246
247 function selected (view) {
248 return computed([currentView, view], (currentView, view) => {
249 return currentView === view
250 })
251 }
252}
253
254function overrideConfig (config) {
255 return [{
256 gives: nest('config.sync.load'),
257 create: function (api) {
258 return nest('config.sync.load', () => config)
259 }
260 }]
261}
262

Built with git-ssb-web