@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
fork

Configure Feed

Select the types of activity you want to include in your feed.

at recaptime-dev/main 574 lines 16 kB view raw
1<?php 2 3/** 4 * @concrete-extensible 5 */ 6class PhabricatorApplicationTransactionView extends AphrontView { 7 8 private $transactions; 9 private $engine; 10 private $showEditActions = true; 11 private $isPreview; 12 private $object; 13 private $objectPHID; 14 private $shouldTerminate = false; 15 private $quoteTargetID; 16 private $quoteRef; 17 private $pager; 18 private $renderAsFeed; 19 private $hideCommentOptions = false; 20 private $viewData = array(); 21 22 public function setRenderAsFeed($feed) { 23 $this->renderAsFeed = $feed; 24 return $this; 25 } 26 27 public function setQuoteRef($quote_ref) { 28 $this->quoteRef = $quote_ref; 29 return $this; 30 } 31 32 public function getQuoteRef() { 33 return $this->quoteRef; 34 } 35 36 public function setQuoteTargetID($quote_target_id) { 37 $this->quoteTargetID = $quote_target_id; 38 return $this; 39 } 40 41 public function getQuoteTargetID() { 42 return $this->quoteTargetID; 43 } 44 45 public function setObject( 46 PhabricatorApplicationTransactionInterface $object) { 47 $this->object = $object; 48 return $this; 49 } 50 51 private function getObject() { 52 return $this->object; 53 } 54 55 public function setObjectPHID($object_phid) { 56 $this->objectPHID = $object_phid; 57 return $this; 58 } 59 60 public function getObjectPHID() { 61 return $this->objectPHID; 62 } 63 64 public function setIsPreview($is_preview) { 65 $this->isPreview = $is_preview; 66 return $this; 67 } 68 69 public function getIsPreview() { 70 return $this->isPreview; 71 } 72 73 public function setShowEditActions($show_edit_actions) { 74 $this->showEditActions = $show_edit_actions; 75 return $this; 76 } 77 78 public function getShowEditActions() { 79 return $this->showEditActions; 80 } 81 82 public function setMarkupEngine(PhabricatorMarkupEngine $engine) { 83 $this->engine = $engine; 84 return $this; 85 } 86 87 /** 88 * @param array<PhabricatorApplicationTransaction> $transactions 89 */ 90 public function setTransactions(array $transactions) { 91 assert_instances_of($transactions, 92 PhabricatorApplicationTransaction::class); 93 $this->transactions = $transactions; 94 return $this; 95 } 96 97 public function getTransactions() { 98 return $this->transactions; 99 } 100 101 public function setShouldTerminate($term) { 102 $this->shouldTerminate = $term; 103 return $this; 104 } 105 106 public function setPager(AphrontCursorPagerView $pager) { 107 $this->pager = $pager; 108 return $this; 109 } 110 111 public function getPager() { 112 return $this->pager; 113 } 114 115 public function setHideCommentOptions($hide_comment_options) { 116 $this->hideCommentOptions = $hide_comment_options; 117 return $this; 118 } 119 120 public function getHideCommentOptions() { 121 return $this->hideCommentOptions; 122 } 123 124 public function setViewData(array $view_data) { 125 $this->viewData = $view_data; 126 return $this; 127 } 128 129 public function getViewData() { 130 return $this->viewData; 131 } 132 133 public function buildEvents($with_hiding = false) { 134 $user = $this->getUser(); 135 136 $xactions = $this->transactions; 137 138 $xactions = $this->filterHiddenTransactions($xactions); 139 $xactions = $this->groupRelatedTransactions($xactions); 140 $groups = $this->groupDisplayTransactions($xactions); 141 142 // If the viewer has interacted with this object, we hide things from 143 // before their most recent interaction by default. This tends to make 144 // very long threads much more manageable, because you don't have to 145 // scroll through a lot of history and can focus on just new stuff. 146 147 $show_group = null; 148 149 if ($with_hiding) { 150 // Find the most recent comment by the viewer. 151 $group_keys = array_keys($groups); 152 $group_keys = array_reverse($group_keys); 153 154 // If we would only hide a small number of transactions, don't hide 155 // anything. Just don't examine the last few keys. Also, we always 156 // want to show the most recent pieces of activity, so don't examine 157 // the first few keys either. 158 $group_keys = array_slice($group_keys, 2, -2); 159 160 $type_comment = PhabricatorTransactions::TYPE_COMMENT; 161 foreach ($group_keys as $group_key) { 162 $group = $groups[$group_key]; 163 foreach ($group as $xaction) { 164 if ($xaction->getAuthorPHID() == $user->getPHID() && 165 $xaction->getTransactionType() == $type_comment) { 166 // This is the most recent group where the user commented. 167 $show_group = $group_key; 168 break 2; 169 } 170 } 171 } 172 } 173 174 $events = array(); 175 $hide_by_default = ($show_group !== null); 176 $set_next_page_id = false; 177 178 foreach ($groups as $group_key => $group) { 179 if ($hide_by_default && ($show_group === $group_key)) { 180 $hide_by_default = false; 181 $set_next_page_id = true; 182 } 183 184 $group_event = null; 185 foreach ($group as $xaction) { 186 $event = $this->renderEvent($xaction, $group); 187 $event->setHideByDefault($hide_by_default); 188 if (!$group_event) { 189 $group_event = $event; 190 } else { 191 $group_event->addEventToGroup($event); 192 } 193 if ($set_next_page_id) { 194 $set_next_page_id = false; 195 $pager = $this->getPager(); 196 if ($pager) { 197 $pager->setNextPageID($xaction->getID()); 198 } 199 } 200 } 201 $events[] = $group_event; 202 203 } 204 205 return $events; 206 } 207 208 public function render() { 209 if (!$this->getObjectPHID()) { 210 throw new PhutilInvalidStateException('setObjectPHID'); 211 } 212 213 $view = $this->buildPHUITimelineView(); 214 215 if ($this->getShowEditActions()) { 216 Javelin::initBehavior('phabricator-transaction-list'); 217 } 218 219 return $view->render(); 220 } 221 222 public function buildPHUITimelineView($with_hiding = true) { 223 if (!$this->getObjectPHID()) { 224 throw new PhutilInvalidStateException('setObjectPHID'); 225 } 226 227 $view = id(new PHUITimelineView()) 228 ->setViewer($this->getViewer()) 229 ->setShouldTerminate($this->shouldTerminate) 230 ->setQuoteTargetID($this->getQuoteTargetID()) 231 ->setQuoteRef($this->getQuoteRef()) 232 ->setViewData($this->getViewData()); 233 234 $events = $this->buildEvents($with_hiding); 235 foreach ($events as $event) { 236 $view->addEvent($event); 237 } 238 239 if ($this->getPager()) { 240 $view->setPager($this->getPager()); 241 } 242 243 return $view; 244 } 245 246 public function isTimelineEmpty() { 247 return !count($this->buildEvents(true)); 248 } 249 250 protected function getOrBuildEngine() { 251 if (!$this->engine) { 252 $field = PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT; 253 254 $engine = id(new PhabricatorMarkupEngine()) 255 ->setViewer($this->getViewer()); 256 257 $object = $this->getObject(); 258 if ($object) { 259 $engine->setContextObject($object); 260 } 261 262 foreach ($this->transactions as $xaction) { 263 if (!$xaction->hasComment()) { 264 continue; 265 } 266 $engine->addObject($xaction->getComment(), $field); 267 } 268 $engine->process(); 269 270 $this->engine = $engine; 271 } 272 273 return $this->engine; 274 } 275 276 private function buildChangeDetailsLink( 277 PhabricatorApplicationTransaction $xaction) { 278 279 return javelin_tag( 280 'a', 281 array( 282 'href' => $xaction->getChangeDetailsURI(), 283 'sigil' => 'workflow', 284 ), 285 pht('(Show Details)')); 286 } 287 288 private function buildExtraInformationLink( 289 PhabricatorApplicationTransaction $xaction) { 290 291 $link = $xaction->renderExtraInformationLink(); 292 if (!$link) { 293 return null; 294 } 295 296 return phutil_tag( 297 'span', 298 array( 299 'class' => 'phui-timeline-extra-information', 300 ), 301 array(" \xC2\xB7 ", $link)); 302 } 303 304 protected function shouldGroupTransactions( 305 PhabricatorApplicationTransaction $u, 306 PhabricatorApplicationTransaction $v) { 307 return false; 308 } 309 310 protected function renderTransactionContent( 311 PhabricatorApplicationTransaction $xaction) { 312 313 $field = PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT; 314 $engine = $this->getOrBuildEngine(); 315 $comment = $xaction->getComment(); 316 317 if ($comment) { 318 if ($comment->getIsRemoved()) { 319 return javelin_tag( 320 'span', 321 array( 322 'class' => 'comment-deleted', 323 'sigil' => 'transaction-comment', 324 'meta' => array('phid' => $comment->getTransactionPHID()), 325 ), 326 pht( 327 'This comment was removed by %s.', 328 $xaction->getHandle($comment->getAuthorPHID())->renderLink())); 329 } else if ($comment->getIsDeleted()) { 330 return javelin_tag( 331 'span', 332 array( 333 'class' => 'comment-deleted', 334 'sigil' => 'transaction-comment', 335 'meta' => array('phid' => $comment->getTransactionPHID()), 336 ), 337 pht('This comment has been deleted.')); 338 } else if ($xaction->hasComment()) { 339 return javelin_tag( 340 'span', 341 array( 342 'class' => 'transaction-comment', 343 'sigil' => 'transaction-comment', 344 'meta' => array('phid' => $comment->getTransactionPHID()), 345 ), 346 $engine->getOutput($comment, $field)); 347 } else { 348 // This is an empty, non-deleted comment. Usually this happens when 349 // rendering previews. 350 return null; 351 } 352 } 353 354 return null; 355 } 356 357 private function filterHiddenTransactions(array $xactions) { 358 foreach ($xactions as $key => $xaction) { 359 if ($xaction->shouldHide()) { 360 unset($xactions[$key]); 361 } 362 } 363 return $xactions; 364 } 365 366 private function groupRelatedTransactions(array $xactions) { 367 $last = null; 368 $last_key = ''; 369 $groups = array(); 370 foreach ($xactions as $key => $xaction) { 371 if ($last && $this->shouldGroupTransactions($last, $xaction)) { 372 $groups[$last_key][] = $xaction; 373 unset($xactions[$key]); 374 } else { 375 $last = $xaction; 376 $last_key = $key; 377 } 378 } 379 380 foreach ($xactions as $key => $xaction) { 381 $xaction->attachTransactionGroup(idx($groups, $key, array())); 382 } 383 384 return $xactions; 385 } 386 387 private function groupDisplayTransactions(array $xactions) { 388 $groups = array(); 389 $group = array(); 390 foreach ($xactions as $xaction) { 391 if ($xaction->shouldDisplayGroupWith($group)) { 392 $group[] = $xaction; 393 } else { 394 if ($group) { 395 $groups[] = $group; 396 } 397 $group = array($xaction); 398 } 399 } 400 401 if ($group) { 402 $groups[] = $group; 403 } 404 405 foreach ($groups as $key => $group) { 406 $results = array(); 407 408 // Sort transactions within the group by action strength, then by 409 // chronological order. This makes sure that multiple actions of the 410 // same type (like a close, then a reopen) render in the order they 411 // were performed. 412 $strength_groups = mgroup($group, 'getActionStrength'); 413 krsort($strength_groups); 414 foreach ($strength_groups as $strength_group) { 415 foreach (msort($strength_group, 'getID') as $xaction) { 416 $results[] = $xaction; 417 } 418 } 419 420 $groups[$key] = $results; 421 } 422 423 return $groups; 424 } 425 426 private function renderEvent( 427 PhabricatorApplicationTransaction $xaction, 428 array $group) { 429 $viewer = $this->getViewer(); 430 431 $event = id(new PHUITimelineEventView()) 432 ->setViewer($viewer) 433 ->setAuthorPHID($xaction->getAuthorPHID()) 434 ->setTransactionPHID($xaction->getPHID()) 435 ->setUserHandle($xaction->getHandle($xaction->getAuthorPHID())) 436 ->setIcon($xaction->getIcon()) 437 ->setColor($xaction->getColor()) 438 ->setHideCommentOptions($this->getHideCommentOptions()) 439 ->setIsSilent($xaction->getIsSilentTransaction()) 440 ->setIsMFA($xaction->getIsMFATransaction()) 441 ->setIsLockOverride($xaction->getIsLockOverrideTransaction()); 442 443 list($token, $token_removed) = $xaction->getToken(); 444 if ($token) { 445 $event->setToken($token, $token_removed); 446 } 447 448 if (!$this->shouldSuppressTitle($xaction, $group)) { 449 if ($this->renderAsFeed) { 450 $title = $xaction->getTitleForFeed(); 451 } else { 452 $title = $xaction->getTitle(); 453 } 454 if ($xaction->hasChangeDetails()) { 455 if (!$this->isPreview) { 456 $details = $this->buildChangeDetailsLink($xaction); 457 $title = array( 458 $title, 459 ' ', 460 $details, 461 ); 462 } 463 } 464 465 if (!$this->isPreview) { 466 $more = $this->buildExtraInformationLink($xaction); 467 if ($more) { 468 $title = array($title, ' ', $more); 469 } 470 } 471 472 $event->setTitle($title); 473 } 474 475 if ($this->isPreview) { 476 $event->setIsPreview(true); 477 } else { 478 $event 479 ->setDateCreated($xaction->getDateCreated()) 480 ->setContentSource($xaction->getContentSource()) 481 ->setAnchor($xaction->getID()); 482 } 483 484 $transaction_type = $xaction->getTransactionType(); 485 $comment_type = PhabricatorTransactions::TYPE_COMMENT; 486 $is_normal_comment = ($transaction_type == $comment_type); 487 488 if ($this->getShowEditActions() && 489 !$this->isPreview && 490 $is_normal_comment) { 491 492 $has_deleted_comment = 493 $xaction->getComment() && 494 $xaction->getComment()->getIsDeleted(); 495 496 $has_removed_comment = 497 $xaction->getComment() && 498 $xaction->getComment()->getIsRemoved(); 499 500 // Make designers happy to make CSS customizations 501 if ($has_removed_comment) { 502 $event->addClass('phui-timeline-shell-removed'); 503 } 504 505 if ($xaction->getCommentVersion() > 1 && !$has_removed_comment) { 506 $event->setIsEdited(true); 507 } 508 509 if (!$has_removed_comment) { 510 $event->setIsNormalComment(true); 511 } 512 513 // If we have a place for quoted text to go and this is a quotable 514 // comment, pass the quote target ID to the event view. 515 if ($this->getQuoteTargetID()) { 516 if ($xaction->hasComment()) { 517 if (!$has_removed_comment && !$has_deleted_comment) { 518 $event->setQuoteTargetID($this->getQuoteTargetID()); 519 $event->setQuoteRef($this->getQuoteRef()); 520 } 521 } 522 } 523 524 $can_edit = PhabricatorPolicyCapability::CAN_EDIT; 525 526 if ($xaction->hasComment() || $has_deleted_comment) { 527 $has_edit_capability = PhabricatorPolicyFilter::hasCapability( 528 $viewer, 529 $xaction, 530 $can_edit); 531 if ($has_edit_capability && !$has_removed_comment) { 532 $event->setIsEditable(true); 533 } 534 535 if ($has_edit_capability || $viewer->getIsAdmin()) { 536 if (!$has_removed_comment) { 537 $event->setIsRemovable(true); 538 } 539 } 540 } 541 542 $can_interact = PhabricatorPolicyFilter::canInteract( 543 $viewer, 544 $xaction->getObject()); 545 $event->setCanInteract($can_interact); 546 } 547 548 $comment = $this->renderTransactionContent($xaction); 549 if ($comment) { 550 $event->appendChild($comment); 551 } 552 553 return $event; 554 } 555 556 private function shouldSuppressTitle( 557 PhabricatorApplicationTransaction $xaction, 558 array $group) { 559 560 // This is a little hard-coded, but we don't have any other reasonable 561 // cases for now. Suppress "commented on" if there are other actions in 562 // the display group. 563 564 if (count($group) > 1) { 565 $type_comment = PhabricatorTransactions::TYPE_COMMENT; 566 if ($xaction->getTransactionType() == $type_comment) { 567 return true; 568 } 569 } 570 571 return false; 572 } 573 574}