Files: b70759f872a7c4a5c73cc3413f10d221633d860c / modules / page / html / render / search.js
6852 bytesRaw
1 | const { h, Struct, Value, when, computed } = require('mutant') |
2 | const pull = require('pull-stream') |
3 | const Scroller = require('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.search': 'first', |
14 | 'sbot.pull.log': 'first', |
15 | 'keys.sync.id': 'first', |
16 | 'message.html.render': 'first' |
17 | }) |
18 | |
19 | exports.gives = nest('page.html.render') |
20 | |
21 | exports.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 | |
134 | function 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 | |
146 | function 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 | |
157 | function createOrRegExp (ary) { |
158 | return new RegExp(ary.map(function (e) { |
159 | return '\\b' + e + '\\b' |
160 | }).join('|'), 'i') |
161 | } |
162 | |
163 | function 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 | |
172 | function 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 | |
189 | function 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 | |
229 | function 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 | |
240 | function clone (obj) { |
241 | var _obj = {} |
242 | for (var k in obj) _obj[k] = obj[k] |
243 | return _obj |
244 | } |
245 | |
246 | function RemoveHook (fn) { |
247 | return function (element) { |
248 | return fn |
249 | } |
250 | } |
251 | |
252 | function pauseAfter (stream, delay) { |
253 | if (!delay) { |
254 | return stream |
255 | } else { |
256 | return pullCat([ |
257 | stream, |
258 | wait(delay) |
259 | ]) |
260 | } |
261 | } |
262 | |
263 | function 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