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