Files: 9bb4dd6572b0113dd462049c31d372b6062f9ac5 / modules / page / html / render / search.js
5189 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 | var nest = require('depnest') |
7 | var Next = require('pull-next') |
8 | |
9 | exports.needs = nest({ |
10 | 'sbot.pull.search': 'first', |
11 | 'sbot.pull.log': 'first', |
12 | 'keys.sync.id': 'first', |
13 | 'message.html.render': 'first' |
14 | }) |
15 | |
16 | exports.gives = nest('page.html.render') |
17 | |
18 | exports.create = function (api) { |
19 | return nest('page.html.render', function channel (path) { |
20 | if (path[0] !== '?') return |
21 | |
22 | var queryStr = path.substr(1).trim() |
23 | var query = queryStr.split(whitespace) |
24 | var matchesQuery = searchFilter(query) |
25 | |
26 | const search = Struct({ |
27 | isLinear: Value(), |
28 | linear: Struct({ |
29 | checked: Value(0) |
30 | }), |
31 | fulltext: Struct({ |
32 | isDone: Value(false) |
33 | }), |
34 | matches: Value(0) |
35 | }) |
36 | |
37 | const hasNoFulltextMatches = computed([ |
38 | search.fulltext.isDone, search.matches |
39 | ], (done, matches) => { |
40 | return done && matches === 0 |
41 | }) |
42 | |
43 | const searchHeader = h('Search', [ |
44 | h('div', {className: 'PageHeading'}, [ |
45 | h('h1', [h('strong', 'Search Results:'), ' ', query.join(' ')]), |
46 | when(search.isLinear, |
47 | h('div.meta', ['Searched: ', search.linear.checked]), |
48 | h('section.details', when(hasNoFulltextMatches, h('div.matches', 'No matches'))) |
49 | ) |
50 | ]), |
51 | when(search.matches, null, h('Loading -large')) |
52 | ]) |
53 | |
54 | var content = h('section.content') |
55 | var container = h('Scroller', { |
56 | style: { overflow: 'auto' } |
57 | }, [ |
58 | h('div.wrapper', [ |
59 | searchHeader, |
60 | content |
61 | ]) |
62 | ]) |
63 | |
64 | pull( |
65 | api.sbot.pull.log({old: false}), |
66 | pull.filter(matchesQuery), |
67 | Scroller(container, content, renderMsg, true, false) |
68 | ) |
69 | |
70 | pull( |
71 | nextStepper(api.sbot.pull.search, {query: queryStr, reverse: true, limit: 500, live: false}), |
72 | fallback((err) => { |
73 | if (err === true) { |
74 | search.fulltext.isDone.set(true) |
75 | } else if (/^no source/.test(err.message)) { |
76 | search.isLinear.set(true) |
77 | return pull( |
78 | nextStepper(api.sbot.pull.log, {reverse: true, limit: 500, live: false}), |
79 | pull.through((msg) => search.linear.checked.set(search.linear.checked() + 1)), |
80 | pull.filter(matchesQuery) |
81 | ) |
82 | } |
83 | }), |
84 | pull.through(() => search.matches.set(search.matches() + 1)), |
85 | Scroller(container, content, renderMsg, false, false) |
86 | ) |
87 | |
88 | return h('div', {className: 'SplitView'}, [ |
89 | h('div.main', container) |
90 | ]) |
91 | |
92 | // scoped |
93 | |
94 | function renderMsg (msg) { |
95 | var el = api.message.html.render(msg) |
96 | highlight(el, createOrRegExp(query)) |
97 | return el |
98 | } |
99 | }) |
100 | } |
101 | |
102 | function andSearch (terms, inputs) { |
103 | for (var i = 0; i < terms.length; i++) { |
104 | var match = false |
105 | for (var j = 0; j < inputs.length; j++) { |
106 | if (terms[i].test(inputs[j])) match = true |
107 | } |
108 | // if a term was not matched by anything, filter this one |
109 | if (!match) return false |
110 | } |
111 | return true |
112 | } |
113 | |
114 | function searchFilter (terms) { |
115 | return function (msg) { |
116 | var c = msg && msg.value && msg.value.content |
117 | return c && ( |
118 | msg.key === terms[0] || andSearch(terms.map(function (term) { |
119 | return new RegExp('\\b' + term + '\\b', 'i') |
120 | }), [c.text, c.name, c.title]) |
121 | ) |
122 | } |
123 | } |
124 | |
125 | function createOrRegExp (ary) { |
126 | return new RegExp(ary.map(function (e) { |
127 | return '\\b' + e + '\\b' |
128 | }).join('|'), 'i') |
129 | } |
130 | |
131 | function highlight (el, query) { |
132 | if (el) { |
133 | var searcher = new TextNodeSearcher({container: el}) |
134 | searcher.query = query |
135 | searcher.highlight() |
136 | return el |
137 | } |
138 | } |
139 | |
140 | function fallback (reader) { |
141 | var fallbackRead |
142 | return function (read) { |
143 | return function (abort, cb) { |
144 | read(abort, function next (end, data) { |
145 | if (end && reader && (fallbackRead = reader(end))) { |
146 | reader = null |
147 | read = fallbackRead |
148 | read(abort, next) |
149 | } else { |
150 | cb(end, data) |
151 | } |
152 | }) |
153 | } |
154 | } |
155 | } |
156 | |
157 | function nextStepper (createStream, opts, property, range) { |
158 | range = range || (opts.reverse ? 'lt' : 'gt') |
159 | property = property || 'timestamp' |
160 | |
161 | var last = null |
162 | var count = -1 |
163 | |
164 | return Next(function () { |
165 | if (last) { |
166 | if (count === 0) return |
167 | var value = opts[range] = get(last, property) |
168 | if (value == null) return |
169 | last = null |
170 | } |
171 | return pull( |
172 | createStream(clone(opts)), |
173 | pull.through(function (msg) { |
174 | count++ |
175 | if (!msg.sync) { |
176 | last = msg |
177 | } |
178 | }, function (err) { |
179 | // retry on errors... |
180 | if (err) { |
181 | count = -1 |
182 | return count |
183 | } |
184 | // end stream if there were no results |
185 | if (last == null) last = {} |
186 | }) |
187 | ) |
188 | }) |
189 | } |
190 | |
191 | function get (obj, path) { |
192 | if (!obj) return undefined |
193 | if (typeof path === 'string') return obj[path] |
194 | if (Array.isArray(path)) { |
195 | for (var i = 0; obj && i < path.length; i++) { |
196 | obj = obj[path[i]] |
197 | } |
198 | return obj |
199 | } |
200 | } |
201 | |
202 | function clone (obj) { |
203 | var _obj = {} |
204 | for (var k in obj) _obj[k] = obj[k] |
205 | return _obj |
206 | } |
207 |
Built with git-ssb-web