3
final class ConpherenceThreadSearchEngine
4
extends PhabricatorApplicationSearchEngine {
6
public function getResultTypeDescription() {
10
public function getApplicationClassName() {
11
return 'PhabricatorConpherenceApplication';
14
public function buildSavedQueryFromRequest(AphrontRequest $request) {
15
$saved = new PhabricatorSavedQuery();
19
$this->readUsersFromRequest($request, 'participants'));
21
$saved->setParameter('fulltext', $request->getStr('fulltext'));
25
$request->getStr('threadType'));
30
public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
31
$query = id(new ConpherenceThreadQuery())
32
->needParticipantCache(true);
34
$participant_phids = $saved->getParameter('participantPHIDs', array());
35
if ($participant_phids && is_array($participant_phids)) {
36
$query->withParticipantPHIDs($participant_phids);
39
$fulltext = $saved->getParameter('fulltext');
40
if (strlen($fulltext)) {
41
$query->withFulltext($fulltext);
44
$thread_type = $saved->getParameter('threadType');
45
if (idx($this->getTypeOptions(), $thread_type)) {
46
switch ($thread_type) {
48
$query->withIsRoom(true);
51
$query->withIsRoom(false);
54
$query->withIsRoom(null);
62
public function buildSearchForm(
63
AphrontFormView $form,
64
PhabricatorSavedQuery $saved) {
66
$participant_phids = $saved->getParameter('participantPHIDs', array());
67
$fulltext = $saved->getParameter('fulltext');
71
id(new AphrontFormTokenizerControl())
72
->setDatasource(new PhabricatorPeopleDatasource())
73
->setName('participants')
74
->setLabel(pht('Participants'))
75
->setValue($participant_phids))
77
id(new AphrontFormTextControl())
79
->setLabel(pht('Contains Words'))
80
->setValue($fulltext))
82
id(new AphrontFormSelectControl())
83
->setLabel(pht('Type'))
84
->setName('threadType')
85
->setOptions($this->getTypeOptions())
86
->setValue($saved->getParameter('threadType')));
89
protected function getURI($path) {
90
return '/conpherence/search/'.$path;
93
protected function getBuiltinQueryNames() {
97
'all' => pht('All Rooms'),
100
if ($this->requireViewer()->isLoggedIn()) {
101
$names['participant'] = pht('Joined Rooms');
102
$names['messages'] = pht('All Messages');
108
public function buildSavedQueryFromBuiltin($query_key) {
110
$query = $this->newSavedQuery();
111
$query->setQueryKey($query_key);
113
switch ($query_key) {
115
$query->setParameter('threadType', 'rooms');
118
$query->setParameter('threadType', 'rooms');
119
return $query->setParameter(
121
array($this->requireViewer()->getPHID()));
123
$query->setParameter('threadType', 'messages');
124
return $query->setParameter(
126
array($this->requireViewer()->getPHID()));
129
return parent::buildSavedQueryFromBuiltin($query_key);
132
protected function getRequiredHandlePHIDsForResultList(
134
PhabricatorSavedQuery $query) {
136
$recent = mpull($conpherences, 'getRecentParticipantPHIDs');
137
return array_unique(array_mergev($recent));
140
protected function renderResultList(
142
PhabricatorSavedQuery $query,
144
assert_instances_of($conpherences, 'ConpherenceThread');
146
$viewer = $this->requireViewer();
148
$policy_objects = ConpherenceThread::loadPolicyObjects(
152
$fulltext = $query->getParameter('fulltext');
153
if (strlen($fulltext) && $conpherences) {
154
$context = $this->loadContextMessages($conpherences, $fulltext);
156
$author_phids = array();
157
foreach ($context as $messages) {
158
foreach ($messages as $group) {
159
foreach ($group as $message) {
160
$xaction = $message['xaction'];
162
$author_phids[] = $xaction->getAuthorPHID();
168
$handles = $viewer->loadHandles($author_phids);
173
$list = new PHUIObjectItemListView();
174
$list->setUser($viewer);
175
foreach ($conpherences as $conpherence) {
176
$created = phabricator_date($conpherence->getDateCreated(), $viewer);
177
$data = $conpherence->getDisplayData($viewer);
178
$title = $data['title'];
180
if ($conpherence->getIsRoom()) {
181
$icon_name = $conpherence->getPolicyIconName($policy_objects);
183
$icon_name = 'fa-envelope-o';
185
$icon = id(new PHUIIconView())
186
->setIconFont($icon_name);
187
$item = id(new PHUIObjectItemView())
188
->setObjectName($conpherence->getMonogram())
190
->setHref('/conpherence/'.$conpherence->getID().'/')
191
->setObject($conpherence)
192
->addIcon('none', $created)
195
pht('Messages: %d', $conpherence->getMessageCount()))
202
phabricator_datetime($conpherence->getDateModified(), $viewer)),
205
$messages = idx($context, $conpherence->getPHID());
208
// TODO: This is egregiously under-designed.
210
foreach ($messages as $group) {
213
foreach ($group as $message) {
214
$xaction = $message['xaction'];
219
$rowc[] = ($message['match'] ? 'highlighted' : null);
221
$handles->renderHandle($xaction->getAuthorPHID()),
222
$xaction->getComment()->getContent(),
223
phabricator_datetime($xaction->getDateCreated(), $viewer),
226
$table = id(new AphrontTableView($rows))
233
->setRowClasses($rowc)
239
$box = id(new PHUIBoxView())
240
->appendChild($table)
241
->addMargin(PHUI::MARGIN_SMALL);
242
$item->appendChild($box);
246
$list->addItem($item);
252
private function getTypeOptions() {
254
'rooms' => pht('Rooms'),
255
'messages' => pht('Messages'),
256
'both' => pht('Both'),
260
private function loadContextMessages(array $threads, $fulltext) {
261
$phids = mpull($threads, 'getPHID');
263
// We want to load a few messages for each thread in the result list, to
264
// show some of the actual content hits to help the user find what they
267
// This method is trying to batch this lookup in most cases, so we do
268
// between one and "a handful" of queries instead of one per thread in
269
// most cases. To do this:
271
// - Load a big block of results for all of the threads.
272
// - If we didn't get a full block back, we have everything that matches
273
// the query. Sort it out and exit.
274
// - Otherwise, some threads had a ton of hits, so we might not be
275
// getting everything we want (we could be getting back 1,000 hits for
276
// the first thread). Remove any threads which we have enough results
277
// for and try again.
278
// - Repeat until we have everything or every thread has enough results.
280
// In the worst case, we could end up degrading to one query per thread,
281
// but this is incredibly unlikely on real data.
283
// Size of the result blocks we're going to load.
286
// Number of messages we want for each thread.
292
$rows = id(new ConpherenceFulltextQuery())
293
->withThreadPHIDs($need)
294
->withFulltext($fulltext)
298
foreach ($rows as $row) {
299
$hits[$row['threadPHID']][] = $row;
302
if (count($rows) < $limit) {
306
foreach ($need as $key => $phid) {
307
if (count($hits[$phid]) >= $want) {
313
// Now that we have all the fulltext matches, throw away any extras that we
314
// aren't going to render so we don't need to do lookups on them.
315
foreach ($hits as $phid => $rows) {
316
if (count($rows) > $want) {
317
$hits[$phid] = array_slice($rows, 0, $want);
321
// For each fulltext match, we want to render a message before and after
322
// the match to give it some context. We already know the transactions
323
// before each match because the rows have a "previousTransactionPHID",
324
// but we need to do one more query to figure out the transactions after
327
// Collect the transactions we want to find the next transactions for.
329
foreach ($hits as $phid => $rows) {
330
foreach ($rows as $row) {
331
$after[] = $row['transactionPHID'];
335
// Look up the next transactions.
337
$after_rows = id(new ConpherenceFulltextQuery())
338
->withPreviousTransactionPHIDs($after)
341
$after_rows = array();
344
// Build maps from PHIDs to the previous and next PHIDs.
347
foreach ($after_rows as $row) {
348
$next_map[$row['previousTransactionPHID']] = $row['transactionPHID'];
351
foreach ($hits as $phid => $rows) {
352
foreach ($rows as $row) {
353
$prev = $row['previousTransactionPHID'];
355
$prev_map[$row['transactionPHID']] = $prev;
356
$next_map[$prev] = $row['transactionPHID'];
361
// Now we're going to collect the actual transaction PHIDs, in order, that
362
// we want to show for each thread.
364
foreach ($hits as $thread_phid => $rows) {
365
$rows = ipull($rows, null, 'transactionPHID');
366
foreach ($rows as $phid => $row) {
371
// Walk backward, finding all the previous results. We can just keep
372
// going until we run out of results because we've only loaded things
373
// that we want to show.
376
if (!isset($prev_map[$prev])) {
377
// No previous transaction, so we're done.
381
$prev = $prev_map[$prev];
383
if (isset($rows[$prev])) {
396
if (count($group) > 1) {
397
$group = array_reverse($group);
407
if (!isset($next_map[$next])) {
411
$next = $next_map[$next];
413
if (isset($rows[$next])) {
426
$groups[$thread_phid][] = $group;
430
// Load all the actual transactions we need.
431
$xaction_phids = array();
432
foreach ($groups as $thread_phid => $group) {
433
foreach ($group as $list) {
434
foreach ($list as $item) {
435
$xaction_phids[] = $item['phid'];
440
if ($xaction_phids) {
441
$xactions = id(new ConpherenceTransactionQuery())
442
->setViewer($this->requireViewer())
443
->withPHIDs($xaction_phids)
446
$xactions = mpull($xactions, null, 'getPHID');
451
foreach ($groups as $thread_phid => $group) {
452
foreach ($group as $key => $list) {
453
foreach ($list as $lkey => $item) {
454
$xaction = idx($xactions, $item['phid']);
455
$groups[$thread_phid][$key][$lkey]['xaction'] = $xaction;
460
// TODO: Sort the groups chronologically?