git ssb

0+

alanz / patchwork



forked from Matt McKegg / patchwork

Tree: ff719d8d11108e133fbe0b77d5e7229f2412dd3e

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

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

Built with git-ssb-web