@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class ConpherenceThreadSearchEngine
4 extends PhabricatorApplicationSearchEngine {
5
6 public function getResultTypeDescription() {
7 return pht('Conpherence Rooms');
8 }
9
10 public function getApplicationClassName() {
11 return PhabricatorConpherenceApplication::class;
12 }
13
14 public function newQuery() {
15 return id(new ConpherenceThreadQuery())
16 ->needProfileImage(true);
17 }
18
19 protected function buildCustomSearchFields() {
20 return array(
21 id(new PhabricatorUsersSearchField())
22 ->setLabel(pht('Participants'))
23 ->setKey('participants')
24 ->setAliases(array('participant')),
25 id(new PhabricatorSearchDatasourceField())
26 ->setLabel(pht('Rooms'))
27 ->setKey('phids')
28 ->setDescription(pht('Search by room titles.'))
29 ->setDatasource(id(new ConpherenceThreadDatasource())),
30 id(new PhabricatorSearchTextField())
31 ->setLabel(pht('Room Contains Words'))
32 ->setKey('fulltext'),
33 );
34 }
35
36 protected function getDefaultFieldOrder() {
37 return array(
38 'participants',
39 '...',
40 );
41 }
42
43 protected function shouldShowOrderField() {
44 return false;
45 }
46
47 protected function buildQueryFromParameters(array $map) {
48 $query = $this->newQuery();
49 if ($map['participants']) {
50 $query->withParticipantPHIDs($map['participants']);
51 }
52 if ($map['fulltext']) {
53 $query->withFulltext($map['fulltext']);
54 }
55 if ($map['phids']) {
56 $query->withPHIDs($map['phids']);
57 }
58 return $query;
59 }
60
61 protected function getURI($path) {
62 return '/conpherence/search/'.$path;
63 }
64
65 protected function getBuiltinQueryNames() {
66 $names = array();
67
68 $names['all'] = pht('All Rooms');
69
70 if ($this->requireViewer()->isLoggedIn()) {
71 $names['participant'] = pht('Joined Rooms');
72 }
73
74 return $names;
75 }
76
77 public function buildSavedQueryFromBuiltin($query_key) {
78
79 $query = $this->newSavedQuery();
80 $query->setQueryKey($query_key);
81
82 switch ($query_key) {
83 case 'all':
84 return $query;
85 case 'participant':
86 return $query->setParameter(
87 'participants',
88 array($this->requireViewer()->getPHID()));
89 }
90
91 return parent::buildSavedQueryFromBuiltin($query_key);
92 }
93
94 /**
95 * @param array<ConpherenceThread> $conpherences
96 * @param PhabricatorSavedQuery $query
97 * @param array<PhabricatorObjectHandle> $handles
98 */
99 protected function renderResultList(
100 array $conpherences,
101 PhabricatorSavedQuery $query,
102 array $handles) {
103 assert_instances_of($conpherences, ConpherenceThread::class);
104
105 $viewer = $this->requireViewer();
106
107 $policy_objects = ConpherenceThread::loadViewPolicyObjects(
108 $viewer,
109 $conpherences);
110
111 $engines = array();
112
113 $fulltext = $query->getParameter('fulltext');
114 if (phutil_nonempty_string($fulltext) && $conpherences) {
115 $context = $this->loadContextMessages($conpherences, $fulltext);
116
117 $author_phids = array();
118 foreach ($context as $phid => $messages) {
119 $conpherence = $conpherences[$phid];
120
121 $engine = id(new PhabricatorMarkupEngine())
122 ->setViewer($viewer)
123 ->setContextObject($conpherence);
124
125 foreach ($messages as $group) {
126 foreach ($group as $message) {
127 $xaction = $message['xaction'];
128 if ($xaction) {
129 $author_phids[] = $xaction->getAuthorPHID();
130 $engine->addObject(
131 $xaction->getComment(),
132 PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT);
133 }
134 }
135 }
136 $engine->process();
137
138 $engines[$phid] = $engine;
139 }
140
141 $handles = $viewer->loadHandles($author_phids);
142 $handles = iterator_to_array($handles);
143 } else {
144 $context = array();
145 }
146
147 $content = array();
148 $list = new PHUIObjectItemListView();
149 $list->setUser($viewer);
150 foreach ($conpherences as $conpherence_phid => $conpherence) {
151 $created = phabricator_date($conpherence->getDateCreated(), $viewer);
152 $title = $conpherence->getTitle();
153 $monogram = $conpherence->getMonogram();
154
155 $icon_name = $conpherence->getPolicyIconName($policy_objects);
156 $icon = id(new PHUIIconView())
157 ->setIcon($icon_name);
158
159 if (!phutil_nonempty_string($fulltext)) {
160 $item = id(new PHUIObjectItemView())
161 ->setObjectName($conpherence->getMonogram())
162 ->setHeader($title)
163 ->setHref('/'.$conpherence->getMonogram())
164 ->setObject($conpherence)
165 ->setImageURI($conpherence->getProfileImageURI())
166 ->addIcon('none', $created)
167 ->addIcon(
168 'none',
169 pht('Messages: %d', $conpherence->getMessageCount()))
170 ->addAttribute(
171 array(
172 $icon,
173 ' ',
174 pht(
175 'Last updated %s',
176 phabricator_datetime($conpherence->getDateModified(), $viewer)),
177 ));
178 $list->addItem($item);
179 } else {
180 $messages = idx($context, $conpherence_phid);
181 $box = array();
182 $list = null;
183 if ($messages) {
184 foreach ($messages as $group) {
185 $rows = array();
186 foreach ($group as $message) {
187 $xaction = $message['xaction'];
188 if (!$xaction) {
189 continue;
190 }
191
192 $view = id(new ConpherenceTransactionView())
193 ->setUser($viewer)
194 ->setHandles($handles)
195 ->setMarkupEngine($engines[$conpherence_phid])
196 ->setConpherenceThread($conpherence)
197 ->setConpherenceTransaction($xaction)
198 ->setSearchResult(true)
199 ->addClass('conpherence-fulltext-result');
200
201 if ($message['match']) {
202 $view->addClass('conpherence-fulltext-match');
203 }
204
205 $rows[] = $view;
206 }
207 $box[] = id(new PHUIBoxView())
208 ->appendChild($rows)
209 ->addClass('conpherence-fulltext-results');
210 }
211 }
212 $header = id(new PHUIHeaderView())
213 ->setHeader($title)
214 ->setHeaderIcon($icon_name)
215 ->setHref('/'.$monogram);
216
217 $content[] = id(new PHUIObjectBoxView())
218 ->setHeader($header)
219 ->appendChild($box);
220 }
221 }
222
223 if ($list) {
224 $content = $list;
225 } else {
226 $content = id(new PHUIBoxView())
227 ->addClass('conpherence-search-room-results')
228 ->appendChild($content);
229 }
230
231 $result = new PhabricatorApplicationSearchResultView();
232 $result->setContent($content);
233 $result->setNoDataString(pht('No results found.'));
234
235 return $result;
236 }
237
238 private function loadContextMessages(array $threads, $fulltext) {
239 $phids = mpull($threads, 'getPHID');
240
241 // We want to load a few messages for each thread in the result list, to
242 // show some of the actual content hits to help the user find what they
243 // are looking for.
244
245 // This method is trying to batch this lookup in most cases, so we do
246 // between one and "a handful" of queries instead of one per thread in
247 // most cases. To do this:
248 //
249 // - Load a big block of results for all of the threads.
250 // - If we didn't get a full block back, we have everything that matches
251 // the query. Sort it out and exit.
252 // - Otherwise, some threads had a ton of hits, so we might not be
253 // getting everything we want (we could be getting back 1,000 hits for
254 // the first thread). Remove any threads which we have enough results
255 // for and try again.
256 // - Repeat until we have everything or every thread has enough results.
257 //
258 // In the worst case, we could end up degrading to one query per thread,
259 // but this is incredibly unlikely on real data.
260
261 // Size of the result blocks we're going to load.
262 $limit = 1000;
263
264 // Number of messages we want for each thread.
265 $want = 3;
266
267 $need = $phids;
268 $hits = array();
269 while ($need) {
270 $rows = id(new ConpherenceFulltextQuery())
271 ->withThreadPHIDs($need)
272 ->withFulltext($fulltext)
273 ->setLimit($limit)
274 ->execute();
275
276 foreach ($rows as $row) {
277 $hits[$row['threadPHID']][] = $row;
278 }
279
280 if (count($rows) < $limit) {
281 break;
282 }
283
284 foreach ($need as $key => $phid) {
285 if (count($hits[$phid]) >= $want) {
286 unset($need[$key]);
287 }
288 }
289 }
290
291 // Now that we have all the fulltext matches, throw away any extras that we
292 // aren't going to render so we don't need to do lookups on them.
293 foreach ($hits as $phid => $rows) {
294 if (count($rows) > $want) {
295 $hits[$phid] = array_slice($rows, 0, $want);
296 }
297 }
298
299 // For each fulltext match, we want to render a message before and after
300 // the match to give it some context. We already know the transactions
301 // before each match because the rows have a "previousTransactionPHID",
302 // but we need to do one more query to figure out the transactions after
303 // each match.
304
305 // Collect the transactions we want to find the next transactions for.
306 $after = array();
307 foreach ($hits as $phid => $rows) {
308 foreach ($rows as $row) {
309 $after[] = $row['transactionPHID'];
310 }
311 }
312
313 // Look up the next transactions.
314 if ($after) {
315 $after_rows = id(new ConpherenceFulltextQuery())
316 ->withPreviousTransactionPHIDs($after)
317 ->execute();
318 } else {
319 $after_rows = array();
320 }
321
322 // Build maps from PHIDs to the previous and next PHIDs.
323 $prev_map = array();
324 $next_map = array();
325 foreach ($after_rows as $row) {
326 $next_map[$row['previousTransactionPHID']] = $row['transactionPHID'];
327 }
328
329 foreach ($hits as $phid => $rows) {
330 foreach ($rows as $row) {
331 $prev = $row['previousTransactionPHID'];
332 if ($prev) {
333 $prev_map[$row['transactionPHID']] = $prev;
334 $next_map[$prev] = $row['transactionPHID'];
335 }
336 }
337 }
338
339 // Now we're going to collect the actual transaction PHIDs, in order, that
340 // we want to show for each thread.
341 $groups = array();
342 foreach ($hits as $thread_phid => $rows) {
343 $rows = ipull($rows, null, 'transactionPHID');
344 $done = array();
345 foreach ($rows as $phid => $row) {
346 if (isset($done[$phid])) {
347 continue;
348 }
349 $done[$phid] = true;
350
351 $group = array();
352
353 // Walk backward, finding all the previous results. We can just keep
354 // going until we run out of results because we've only loaded things
355 // that we want to show.
356 $prev = $phid;
357 while (true) {
358 if (!isset($prev_map[$prev])) {
359 // No previous transaction, so we're done.
360 break;
361 }
362
363 $prev = $prev_map[$prev];
364
365 if (isset($rows[$prev])) {
366 $match = true;
367 $done[$prev] = true;
368 } else {
369 $match = false;
370 }
371
372 $group[] = array(
373 'phid' => $prev,
374 'match' => $match,
375 );
376 }
377
378 if (count($group) > 1) {
379 $group = array_reverse($group);
380 }
381
382 $group[] = array(
383 'phid' => $phid,
384 'match' => true,
385 );
386
387 $next = $phid;
388 while (true) {
389 if (!isset($next_map[$next])) {
390 break;
391 }
392
393 $next = $next_map[$next];
394
395 if (isset($rows[$next])) {
396 $match = true;
397 $done[$next] = true;
398 } else {
399 $match = false;
400 }
401
402 $group[] = array(
403 'phid' => $next,
404 'match' => $match,
405 );
406 }
407
408 $groups[$thread_phid][] = $group;
409 }
410 }
411
412 // Load all the actual transactions we need.
413 $xaction_phids = array();
414 foreach ($groups as $thread_phid => $group) {
415 foreach ($group as $list) {
416 foreach ($list as $item) {
417 $xaction_phids[] = $item['phid'];
418 }
419 }
420 }
421
422 if ($xaction_phids) {
423 $xactions = id(new ConpherenceTransactionQuery())
424 ->setViewer($this->requireViewer())
425 ->withPHIDs($xaction_phids)
426 ->needComments(true)
427 ->execute();
428 $xactions = mpull($xactions, null, 'getPHID');
429 } else {
430 $xactions = array();
431 }
432
433 foreach ($groups as $thread_phid => $group) {
434 foreach ($group as $key => $list) {
435 foreach ($list as $lkey => $item) {
436 $xaction = idx($xactions, $item['phid']);
437 if ($xaction->shouldHide()) {
438 continue;
439 }
440 $groups[$thread_phid][$key][$lkey]['xaction'] = $xaction;
441 }
442 }
443 }
444
445 // TODO: Sort the groups chronologically?
446
447 return $groups;
448 }
449
450}