git ssb

0+

alanz / patchwork



forked from Matt McKegg / patchwork

Tree: b70759f872a7c4a5c73cc3413f10d221633d860c

Files: b70759f872a7c4a5c73cc3413f10d221633d860c / modules / page / html / render / search.js

6852 bytesRaw
1const { h, Struct, Value, when, computed } = require('mutant')
2const pull = require('pull-stream')
3const Scroller = require('pull-scroll')
4const TextNodeSearcher = require('text-node-searcher')
5const whitespace = /\s+/
6const pullAbortable = require('pull-abortable')
7var nest = require('depnest')
8var Next = require('pull-next')
9var defer = require('pull-defer')
10var pullCat = require('pull-cat')
11
12exports.needs = nest({
13 'sbot.pull.search': 'first',
14 'sbot.pull.log': 'first',
15 'keys.sync.id': 'first',
16 'message.html.render': 'first'
17})
18
19exports.gives = nest('page.html.render')
20
21exports.create = function (api) {
22 return nest('page.html.render', function channel (path) {
23 if (path[0] !== '?') return
24
25 var queryStr = path.substr(1).trim()
26 var query = queryStr.split(whitespace)
27 var matchesQuery = searchFilter(query)
28
29 const search = Struct({
30 isLinear: Value(),
31 linear: Struct({
32 checked: Value(0),
33 isDone: Value(false)
34 }),
35 fulltext: Struct({
36 isDone: Value(false)
37 }),
38 matches: Value(0)
39 })
40
41 const hasNoFulltextMatches = computed([
42 search.fulltext.isDone, search.matches
43 ], (done, matches) => {
44 return done && matches === 0
45 })
46
47 const searchHeader = h('Search', [
48 h('div', {className: 'PageHeading'}, [
49 h('h1', [h('strong', 'Search Results:'), ' ', query.join(' ')]),
50 when(search.isLinear,
51 h('div.meta', ['Searched: ', search.linear.checked]),
52 h('section.details', when(hasNoFulltextMatches, h('div.matches', 'No matches')))
53 )
54 ])
55 ])
56
57 var content = h('section.content')
58 var container = h('Scroller', {
59 style: { overflow: 'auto' }
60 }, [
61 h('div.wrapper', [
62 searchHeader,
63 content,
64 when(search.linear.isDone, null, h('Loading -search'))
65 ])
66 ])
67
68 var realtimeAborter = pullAbortable()
69
70 pull(
71 api.sbot.pull.log({old: false}),
72 pull.filter(matchesQuery),
73 realtimeAborter,
74 Scroller(container, content, renderMsg, true, false)
75 )
76
77 // pull(
78 // nextStepper(api.sbot.pull.search, {query: queryStr, reverse: true, limit: 500, live: false}),
79 // fallback((err) => {
80 // if (err === true) {
81 // search.fulltext.isDone.set(true)
82 // } else if (/^no source/.test(err.message)) {
83 // search.isLinear.set(true)
84 // return pull(
85 // nextStepper(api.sbot.pull.log, {reverse: true, limit: 500, live: false}),
86 // pull.through((msg) => search.linear.checked.set(search.linear.checked() + 1)),
87 // pull.filter(matchesQuery)
88 // )
89 // }
90 // }),
91 // pull.through(() => search.matches.set(search.matches() + 1)),
92 // Scroller(container, content, renderMsg, false, false)
93 // )
94
95 // disable full text for now
96 var aborter = pullAbortable()
97 search.isLinear.set(true)
98 pull(
99 nextStepper(api.sbot.pull.log, {reverse: true, limit: 500, live: false, delay: 100}),
100 pull.through((msg) => search.linear.checked.set(search.linear.checked() + 1)),
101 pull.filter(matchesQuery),
102 pull.through(() => search.matches.set(search.matches() + 1)),
103 aborter,
104 Scroller(container, content, renderMsg, false, false, err => {
105 if (err) console.log(err)
106 search.linear.isDone.set(true)
107 })
108 )
109
110 return h('SplitView', {
111 hooks: [
112 RemoveHook(() => {
113 // terminate search if removed from dom
114 // this is triggered whenever a new search is started
115 realtimeAborter.abort()
116 aborter.abort()
117 })
118 ],
119 uniqueKey: 'search'
120 }, [
121 h('div.main', container)
122 ])
123
124 // scoped
125
126 function renderMsg (msg) {
127 var el = h('div.result', api.message.html.render(msg))
128 highlight(el, createOrRegExp(query))
129 return el
130 }
131 })
132}
133
134function andSearch (terms, inputs) {
135 for (var i = 0; i < terms.length; i++) {
136 var match = false
137 for (var j = 0; j < inputs.length; j++) {
138 if (terms[i].test(inputs[j])) match = true
139 }
140 // if a term was not matched by anything, filter this one
141 if (!match) return false
142 }
143 return true
144}
145
146function searchFilter (terms) {
147 return function (msg) {
148 var c = msg && msg.value && msg.value.content
149 return c && (
150 msg.key === terms[0] || andSearch(terms.map(function (term) {
151 return new RegExp('\\b' + term + '\\b', 'i')
152 }), [c.text, c.name, c.title])
153 )
154 }
155}
156
157function createOrRegExp (ary) {
158 return new RegExp(ary.map(function (e) {
159 return '\\b' + e + '\\b'
160 }).join('|'), 'i')
161}
162
163function highlight (el, query) {
164 if (el) {
165 var searcher = new TextNodeSearcher({container: el})
166 searcher.query = query
167 searcher.highlight()
168 return el
169 }
170}
171
172function fallback (reader) {
173 var fallbackRead
174 return function (read) {
175 return function (abort, cb) {
176 read(abort, function next (end, data) {
177 if (end && reader && (fallbackRead = reader(end))) {
178 reader = null
179 read = fallbackRead
180 read(abort, next)
181 } else {
182 cb(end, data)
183 }
184 })
185 }
186 }
187}
188
189function nextStepper (createStream, opts, property, range) {
190 range = range || (opts.reverse ? 'lt' : 'gt')
191 property = property || 'timestamp'
192
193 var last = null
194 var count = -1
195
196 return Next(function () {
197 if (last) {
198 if (count === 0) return
199 var value = opts[range] = get(last, property)
200 if (value == null) return
201 last = null
202 }
203 var result = defer.source()
204
205 window.requestIdleCallback(() => {
206 result.resolve(pull(
207 createStream(clone(opts)),
208 pull.through(function (msg) {
209 count++
210 if (!msg.sync) {
211 last = msg
212 }
213 }, function (err) {
214 // retry on errors...
215 if (err) {
216 count = -1
217 return count
218 }
219 // end stream if there were no results
220 if (last == null) last = {}
221 })
222 ))
223 })
224
225 return pauseAfter(result, opts.delay)
226 })
227}
228
229function get (obj, path) {
230 if (!obj) return undefined
231 if (typeof path === 'string') return obj[path]
232 if (Array.isArray(path)) {
233 for (var i = 0; obj && i < path.length; i++) {
234 obj = obj[path[i]]
235 }
236 return obj
237 }
238}
239
240function clone (obj) {
241 var _obj = {}
242 for (var k in obj) _obj[k] = obj[k]
243 return _obj
244}
245
246function RemoveHook (fn) {
247 return function (element) {
248 return fn
249 }
250}
251
252function pauseAfter (stream, delay) {
253 if (!delay) {
254 return stream
255 } else {
256 return pullCat([
257 stream,
258 wait(delay)
259 ])
260 }
261}
262
263function wait (delay) {
264 return function (abort, cb) {
265 if (abort) {
266 cb(true)
267 } else {
268 setTimeout(() => {
269 cb(true)
270 }, delay)
271 }
272 }
273}
274

Built with git-ssb-web