diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 2020a8bfd9..218487e23a 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4807,6 +4807,7 @@ phutil_register_library_map(array( 'PhabricatorPolicyInterface', 'PhabricatorMarkupInterface', 'PhabricatorApplicationTransactionInterface', + 'PhabricatorSubscribableInterface', ), 'PhabricatorCalendarEventDeleteController' => 'PhabricatorCalendarController', 'PhabricatorCalendarEventEditController' => 'PhabricatorCalendarController', diff --git a/src/applications/conpherence/query/ConpherenceFulltextQuery.php b/src/applications/conpherence/query/ConpherenceFulltextQuery.php index 244791076c..cd1e202ebd 100644 --- a/src/applications/conpherence/query/ConpherenceFulltextQuery.php +++ b/src/applications/conpherence/query/ConpherenceFulltextQuery.php @@ -4,6 +4,7 @@ final class ConpherenceFulltextQuery extends PhabricatorOffsetPagedQuery { private $threadPHIDs; + private $previousTransactionPHIDs; private $fulltext; public function withThreadPHIDs(array $phids) { @@ -11,6 +12,11 @@ final class ConpherenceFulltextQuery return $this; } + public function withPreviousTransactionPHIDs(array $phids) { + $this->previousTransactionPHIDs = $phids; + return $this; + } + public function withFulltext($fulltext) { $this->fulltext = $fulltext; return $this; @@ -42,6 +48,13 @@ final class ConpherenceFulltextQuery $this->threadPHIDs); } + if ($this->previousTransactionPHIDs !== null) { + $where[] = qsprintf( + $conn_r, + 'i.previousTransactionPHID IN (%Ls)', + $this->previousTransactionPHIDs); + } + if (strlen($this->fulltext)) { $where[] = qsprintf( $conn_r, diff --git a/src/applications/conpherence/query/ConpherenceThreadSearchEngine.php b/src/applications/conpherence/query/ConpherenceThreadSearchEngine.php index e57e73c1d2..e6c6855ca3 100644 --- a/src/applications/conpherence/query/ConpherenceThreadSearchEngine.php +++ b/src/applications/conpherence/query/ConpherenceThreadSearchEngine.php @@ -149,6 +149,27 @@ final class ConpherenceThreadSearchEngine $viewer, $conpherences); + $fulltext = $query->getParameter('fulltext'); + if (strlen($fulltext) && $conpherences) { + $context = $this->loadContextMessages($conpherences, $fulltext); + + $author_phids = array(); + foreach ($context as $messages) { + foreach ($messages as $group) { + foreach ($group as $message) { + $xaction = $message['xaction']; + if ($xaction) { + $author_phids[] = $xaction->getAuthorPHID(); + } + } + } + } + + $handles = $viewer->loadHandles($author_phids); + } else { + $context = array(); + } + $list = new PHUIObjectItemListView(); $list->setUser($viewer); foreach ($conpherences as $conpherence) { @@ -181,6 +202,47 @@ final class ConpherenceThreadSearchEngine phabricator_datetime($conpherence->getDateModified(), $viewer)), )); + $messages = idx($context, $conpherence->getPHID()); + if ($messages) { + + // TODO: This is egregiously under-designed. + + foreach ($messages as $group) { + $rows = array(); + $rowc = array(); + foreach ($group as $message) { + $xaction = $message['xaction']; + if (!$xaction) { + continue; + } + + $rowc[] = ($message['match'] ? 'highlighted' : null); + $rows[] = array( + $handles->renderHandle($xaction->getAuthorPHID()), + $xaction->getComment()->getContent(), + phabricator_datetime($xaction->getDateCreated(), $viewer), + ); + } + $table = id(new AphrontTableView($rows)) + ->setHeaders( + array( + pht('User'), + pht('Message'), + pht('At'), + )) + ->setRowClasses($rowc) + ->setColumnClasses( + array( + '', + 'wide', + )); + $box = id(new PHUIBoxView()) + ->appendChild($table) + ->addMargin(PHUI::MARGIN_SMALL); + $item->appendChild($box); + } + } + $list->addItem($item); } @@ -195,4 +257,209 @@ final class ConpherenceThreadSearchEngine ); } + private function loadContextMessages(array $threads, $fulltext) { + $phids = mpull($threads, 'getPHID'); + + // We want to load a few messages for each thread in the result list, to + // show some of the actual content hits to help the user find what they + // are looking for. + + // This method is trying to batch this lookup in most cases, so we do + // between one and "a handful" of queries instead of one per thread in + // most cases. To do this: + // + // - Load a big block of results for all of the threads. + // - If we didn't get a full block back, we have everything that matches + // the query. Sort it out and exit. + // - Otherwise, some threads had a ton of hits, so we might not be + // getting everything we want (we could be getting back 1,000 hits for + // the first thread). Remove any threads which we have enough results + // for and try again. + // - Repeat until we have everything or every thread has enough results. + // + // In the worst case, we could end up degrading to one query per thread, + // but this is incredibly unlikely on real data. + + // Size of the result blocks we're going to load. + $limit = 1000; + + // Number of messages we want for each thread. + $want = 3; + + $need = $phids; + $hits = array(); + while ($need) { + $rows = id(new ConpherenceFulltextQuery()) + ->withThreadPHIDs($need) + ->withFulltext($fulltext) + ->setLimit($limit) + ->execute(); + + foreach ($rows as $row) { + $hits[$row['threadPHID']][] = $row; + } + + if (count($rows) < $limit) { + break; + } + + foreach ($need as $key => $phid) { + if (count($hits[$phid]) >= $want) { + unset($need[$key]); + } + } + } + + // Now that we have all the fulltext matches, throw away any extras that we + // aren't going to render so we don't need to do lookups on them. + foreach ($hits as $phid => $rows) { + if (count($rows) > $want) { + $hits[$phid] = array_slice($rows, 0, $want); + } + } + + // For each fulltext match, we want to render a message before and after + // the match to give it some context. We already know the transactions + // before each match because the rows have a "previousTransactionPHID", + // but we need to do one more query to figure out the transactions after + // each match. + + // Collect the transactions we want to find the next transactions for. + $after = array(); + foreach ($hits as $phid => $rows) { + foreach ($rows as $row) { + $after[] = $row['transactionPHID']; + } + } + + // Look up the next transactions. + if ($after) { + $after_rows = id(new ConpherenceFulltextQuery()) + ->withPreviousTransactionPHIDs($after) + ->execute(); + } else { + $after_rows = array(); + } + + // Build maps from PHIDs to the previous and next PHIDs. + $prev_map = array(); + $next_map = array(); + foreach ($after_rows as $row) { + $next_map[$row['previousTransactionPHID']] = $row['transactionPHID']; + } + + foreach ($hits as $phid => $rows) { + foreach ($rows as $row) { + $prev = $row['previousTransactionPHID']; + if ($prev) { + $prev_map[$row['transactionPHID']] = $prev; + $next_map[$prev] = $row['transactionPHID']; + } + } + } + + // Now we're going to collect the actual transaction PHIDs, in order, that + // we want to show for each thread. + $groups = array(); + foreach ($hits as $thread_phid => $rows) { + $rows = ipull($rows, null, 'transactionPHID'); + foreach ($rows as $phid => $row) { + unset($rows[$phid]); + + $group = array(); + + // Walk backward, finding all the previous results. We can just keep + // going until we run out of results because we've only loaded things + // that we want to show. + $prev = $phid; + while (true) { + if (!isset($prev_map[$prev])) { + // No previous transaction, so we're done. + break; + } + + $prev = $prev_map[$prev]; + + if (isset($rows[$prev])) { + $match = true; + unset($rows[$prev]); + } else { + $match = false; + } + + $group[] = array( + 'phid' => $prev, + 'match' => $match, + ); + } + + if (count($group) > 1) { + $group = array_reverse($group); + } + + $group[] = array( + 'phid' => $phid, + 'match' => true, + ); + + $next = $phid; + while (true) { + if (!isset($next_map[$next])) { + break; + } + + $next = $next_map[$next]; + + if (isset($rows[$next])) { + $match = true; + unset($rows[$next]); + } else { + $match = false; + } + + $group[] = array( + 'phid' => $next, + 'match' => $match, + ); + } + + $groups[$thread_phid][] = $group; + } + } + + // Load all the actual transactions we need. + $xaction_phids = array(); + foreach ($groups as $thread_phid => $group) { + foreach ($group as $list) { + foreach ($list as $item) { + $xaction_phids[] = $item['phid']; + } + } + } + + if ($xaction_phids) { + $xactions = id(new ConpherenceTransactionQuery()) + ->setViewer($this->requireViewer()) + ->withPHIDs($xaction_phids) + ->needComments(true) + ->execute(); + $xactions = mpull($xactions, null, 'getPHID'); + } else { + $xactions = array(); + } + + foreach ($groups as $thread_phid => $group) { + foreach ($group as $key => $list) { + foreach ($list as $lkey => $item) { + $xaction = idx($xactions, $item['phid']); + $groups[$thread_phid][$key][$lkey]['xaction'] = $xaction; + } + } + } + + // TODO: Sort the groups chronologically? + + return $groups; + } + }