@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
at recaptime-dev/main 450 lines 13 kB view raw
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}