Files: ff719d8d11108e133fbe0b77d5e7229f2412dd3e / modules / page / html / render / search.js
6837 bytesRaw
1 | const { h, Struct, Value, when, computed } = require('mutant') |
2 | const pull = require('pull-stream') |
3 | const Scroller = require('../../../../lib/pull-scroll') |
4 | const TextNodeSearcher = require('text-node-searcher') |
5 | const whitespace = /\s+/ |
6 | const pullAbortable = require('pull-abortable') |
7 | var nest = require('depnest') |
8 | var Next = require('pull-next') |
9 | var defer = require('pull-defer') |
10 | var pullCat = require('pull-cat') |
11 | |
12 | exports.needs = nest({ |
13 | 'sbot.pull.log': 'first', |
14 | 'keys.sync.id': 'first', |
15 | 'message.html.render': 'first' |
16 | }) |
17 | |
18 | exports.gives = nest('page.html.render') |
19 | |
20 | exports.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 | |
133 | function 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 | |
145 | function 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 | |
156 | function createOrRegExp (ary) { |
157 | return new RegExp(ary.map(function (e) { |
158 | return '\\b' + e + '\\b' |
159 | }).join('|'), 'i') |
160 | } |
161 | |
162 | function 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 | |
171 | function 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 | |
188 | function 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 | |
228 | function 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 | |
239 | function clone (obj) { |
240 | var _obj = {} |
241 | for (var k in obj) _obj[k] = obj[k] |
242 | return _obj |
243 | } |
244 | |
245 | function RemoveHook (fn) { |
246 | return function (element) { |
247 | return fn |
248 | } |
249 | } |
250 | |
251 | function pauseAfter (stream, delay) { |
252 | if (!delay) { |
253 | return stream |
254 | } else { |
255 | return pullCat([ |
256 | stream, |
257 | wait(delay) |
258 | ]) |
259 | } |
260 | } |
261 | |
262 | function 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