@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 764 lines 19 kB view raw
1<?php 2 3final class PHUITimelineEventView extends AphrontView { 4 5 const DELIMITER = " \xC2\xB7 "; 6 7 private $userHandle; 8 private $title; 9 private $icon; 10 private $color; 11 private $classes = array(); 12 private $contentSource; 13 private $dateCreated; 14 private $anchor; 15 private $isEditable; 16 private $isEdited; 17 private $isRemovable; 18 private $transactionPHID; 19 private $isPreview; 20 private $eventGroup = array(); 21 private $hideByDefault; 22 private $token; 23 private $tokenRemoved; 24 private $quoteTargetID; 25 private $isNormalComment; 26 private $quoteRef; 27 private $reallyMajorEvent; 28 private $hideCommentOptions = false; 29 private $authorPHID; 30 private $badges = array(); 31 private $pinboardItems = array(); 32 private $isSilent; 33 private $isMFA; 34 private $isLockOverride; 35 private $canInteract; 36 37 public function setAuthorPHID($author_phid) { 38 $this->authorPHID = $author_phid; 39 return $this; 40 } 41 42 public function getAuthorPHID() { 43 return $this->authorPHID; 44 } 45 46 public function setQuoteRef($quote_ref) { 47 $this->quoteRef = $quote_ref; 48 return $this; 49 } 50 51 public function getQuoteRef() { 52 return $this->quoteRef; 53 } 54 55 public function setQuoteTargetID($quote_target_id) { 56 $this->quoteTargetID = $quote_target_id; 57 return $this; 58 } 59 60 public function getQuoteTargetID() { 61 return $this->quoteTargetID; 62 } 63 64 public function setIsNormalComment($is_normal_comment) { 65 $this->isNormalComment = $is_normal_comment; 66 return $this; 67 } 68 69 public function getIsNormalComment() { 70 return $this->isNormalComment; 71 } 72 73 public function setHideByDefault($hide_by_default) { 74 $this->hideByDefault = $hide_by_default; 75 return $this; 76 } 77 78 public function getHideByDefault() { 79 return $this->hideByDefault; 80 } 81 82 public function setTransactionPHID($transaction_phid) { 83 $this->transactionPHID = $transaction_phid; 84 return $this; 85 } 86 87 public function getTransactionPHID() { 88 return $this->transactionPHID; 89 } 90 91 public function setIsEdited($is_edited) { 92 $this->isEdited = $is_edited; 93 return $this; 94 } 95 96 public function getIsEdited() { 97 return $this->isEdited; 98 } 99 100 public function setIsPreview($is_preview) { 101 $this->isPreview = $is_preview; 102 return $this; 103 } 104 105 public function getIsPreview() { 106 return $this->isPreview; 107 } 108 109 public function setIsEditable($is_editable) { 110 $this->isEditable = $is_editable; 111 return $this; 112 } 113 114 public function getIsEditable() { 115 return $this->isEditable; 116 } 117 118 public function setCanInteract($can_interact) { 119 $this->canInteract = $can_interact; 120 return $this; 121 } 122 123 public function getCanInteract() { 124 return $this->canInteract; 125 } 126 127 public function setIsRemovable($is_removable) { 128 $this->isRemovable = $is_removable; 129 return $this; 130 } 131 132 public function getIsRemovable() { 133 return $this->isRemovable; 134 } 135 136 public function setDateCreated($date_created) { 137 $this->dateCreated = $date_created; 138 return $this; 139 } 140 141 public function getDateCreated() { 142 return $this->dateCreated; 143 } 144 145 public function setContentSource(PhabricatorContentSource $content_source) { 146 $this->contentSource = $content_source; 147 return $this; 148 } 149 150 public function getContentSource() { 151 return $this->contentSource; 152 } 153 154 public function setUserHandle(PhabricatorObjectHandle $handle) { 155 $this->userHandle = $handle; 156 return $this; 157 } 158 159 public function setAnchor($anchor) { 160 $this->anchor = $anchor; 161 return $this; 162 } 163 164 public function getAnchor() { 165 return $this->anchor; 166 } 167 168 public function setTitle($title) { 169 $this->title = $title; 170 return $this; 171 } 172 173 public function addClass($class) { 174 $this->classes[] = $class; 175 return $this; 176 } 177 178 public function addBadge(PHUIBadgeMiniView $badge) { 179 $this->badges[] = $badge; 180 return $this; 181 } 182 183 public function setIcon($icon) { 184 $this->icon = $icon; 185 return $this; 186 } 187 188 public function setColor($color) { 189 $this->color = $color; 190 return $this; 191 } 192 193 public function setIsSilent($is_silent) { 194 $this->isSilent = $is_silent; 195 return $this; 196 } 197 198 public function getIsSilent() { 199 return $this->isSilent; 200 } 201 202 public function setIsMFA($is_mfa) { 203 $this->isMFA = $is_mfa; 204 return $this; 205 } 206 207 public function getIsMFA() { 208 return $this->isMFA; 209 } 210 211 public function setIsLockOverride($is_override) { 212 $this->isLockOverride = $is_override; 213 return $this; 214 } 215 216 public function getIsLockOverride() { 217 return $this->isLockOverride; 218 } 219 220 public function setReallyMajorEvent($me) { 221 $this->reallyMajorEvent = $me; 222 return $this; 223 } 224 225 public function setHideCommentOptions($hide_comment_options) { 226 $this->hideCommentOptions = $hide_comment_options; 227 return $this; 228 } 229 230 public function getHideCommentOptions() { 231 return $this->hideCommentOptions; 232 } 233 234 public function addPinboardItem(PHUIPinboardItemView $item) { 235 $this->pinboardItems[] = $item; 236 return $this; 237 } 238 239 public function setToken($token, $removed = false) { 240 $this->token = $token; 241 $this->tokenRemoved = $removed; 242 return $this; 243 } 244 245 public function getEventGroup() { 246 return array_merge(array($this), $this->eventGroup); 247 } 248 249 public function addEventToGroup(PHUITimelineEventView $event) { 250 $this->eventGroup[] = $event; 251 return $this; 252 } 253 254 protected function shouldRenderEventTitle() { 255 if ($this->title === null) { 256 return false; 257 } 258 259 return true; 260 } 261 262 protected function renderEventTitle($force_icon, $has_menu, $extra) { 263 $title = $this->title; 264 265 $title_classes = array(); 266 $title_classes[] = 'phui-timeline-title'; 267 268 $icon = null; 269 if ($this->icon || $force_icon) { 270 $title_classes[] = 'phui-timeline-title-with-icon'; 271 } 272 273 if ($has_menu) { 274 $title_classes[] = 'phui-timeline-title-with-menu'; 275 } 276 277 if ($this->icon) { 278 $fill_classes = array(); 279 $fill_classes[] = 'phui-timeline-icon-fill'; 280 if ($this->color) { 281 $fill_classes[] = 'fill-has-color'; 282 $fill_classes[] = 'phui-timeline-icon-fill-'.$this->color; 283 } 284 285 $icon = id(new PHUIIconView()) 286 ->setIcon($this->icon) 287 ->addClass('phui-timeline-icon'); 288 289 $icon = phutil_tag( 290 'span', 291 array( 292 'class' => implode(' ', $fill_classes), 293 ), 294 $icon); 295 } 296 297 $token = null; 298 if ($this->token) { 299 $token = id(new PHUIIconView()) 300 ->addClass('phui-timeline-token') 301 ->setSpriteSheet(PHUIIconView::SPRITE_TOKENS) 302 ->setSpriteIcon($this->token); 303 if ($this->tokenRemoved) { 304 $token->addClass('strikethrough'); 305 } 306 } 307 308 $title = phutil_tag( 309 'div', 310 array( 311 'class' => implode(' ', $title_classes), 312 ), 313 array($icon, $token, $title, $extra)); 314 315 return $title; 316 } 317 318 public function render() { 319 320 $events = $this->getEventGroup(); 321 322 // Move events with icons first. 323 $icon_keys = array(); 324 foreach ($this->getEventGroup() as $key => $event) { 325 if ($event->icon) { 326 $icon_keys[] = $key; 327 } 328 } 329 $events = array_select_keys($events, $icon_keys) + $events; 330 $force_icon = (bool)$icon_keys; 331 332 $menu = null; 333 $items = array(); 334 if (!$this->getIsPreview() && !$this->getHideCommentOptions()) { 335 foreach ($this->getEventGroup() as $event) { 336 $items[] = $event->getMenuItems($this->anchor); 337 } 338 $items = array_mergev($items); 339 } 340 341 if ($items) { 342 $icon = id(new PHUIIconView()) 343 ->setIcon('fa-caret-down'); 344 $aural = javelin_tag( 345 'span', 346 array( 347 'aural' => true, 348 ), 349 pht('Comment Actions')); 350 351 $sigil = 'phui-dropdown-menu'; 352 Javelin::initBehavior('phui-dropdown-menu'); 353 354 $action_list = id(new PhabricatorActionListView()) 355 ->setViewer($this->getUser()); 356 foreach ($items as $item) { 357 $action_list->addAction($item); 358 } 359 360 $menu = javelin_tag( 361 'a', 362 array( 363 'href' => '#', 364 'class' => 'phui-timeline-menu', 365 'sigil' => $sigil, 366 'aria-haspopup' => 'true', 367 'aria-expanded' => 'false', 368 'meta' => $action_list->getDropdownMenuMetadata(), 369 ), 370 array( 371 $aural, 372 $icon, 373 )); 374 375 $has_menu = true; 376 } else { 377 $has_menu = false; 378 } 379 380 // Render "extra" information (timestamp, etc). 381 $extra = $this->renderExtra($events); 382 383 $show_badges = false; 384 385 $group_titles = array(); 386 $group_items = array(); 387 $group_children = array(); 388 foreach ($events as $event) { 389 if ($event->shouldRenderEventTitle()) { 390 391 // Render the group anchor here, outside the title box. If we render 392 // it inside the title box it ends up completely hidden and Chrome 55 393 // refuses to jump to it. See T11997 for discussion. 394 395 if ($extra && $this->anchor) { 396 $group_titles[] = id(new PhabricatorAnchorView()) 397 ->setAnchorName($this->anchor) 398 ->render(); 399 } 400 401 $group_titles[] = $event->renderEventTitle( 402 $force_icon, 403 $has_menu, 404 $extra); 405 406 // Don't render this information more than once. 407 $extra = null; 408 } 409 410 if ($event->hasChildren()) { 411 $group_children[] = $event->renderChildren(); 412 $show_badges = true; 413 } 414 } 415 416 $image_uri = $this->userHandle->getImageURI(); 417 418 $wedge = phutil_tag( 419 'div', 420 array( 421 'class' => 'phui-timeline-wedge', 422 'style' => (nonempty($image_uri)) ? '' : 'display: none;', 423 ), 424 ''); 425 426 $image = null; 427 $badges = null; 428 if ($image_uri) { 429 $image = javelin_tag( 430 ($this->userHandle->getURI()) ? 'a' : 'div', 431 array( 432 'style' => 'background-image: url('.$image_uri.')', 433 'class' => 'phui-timeline-image', 434 'href' => $this->userHandle->getURI(), 435 'aural' => false, 436 ), 437 ''); 438 if ($this->badges && $show_badges) { 439 $flex = new PHUIBadgeBoxView(); 440 $flex->addItems($this->badges); 441 $flex->setCollapsed(true); 442 $badges = phutil_tag( 443 'div', 444 array( 445 'class' => 'phui-timeline-badges', 446 ), 447 $flex); 448 } 449 } 450 451 $content_classes = array(); 452 $content_classes[] = 'phui-timeline-content'; 453 454 $classes = array(); 455 $classes[] = 'phui-timeline-event-view'; 456 if ($group_children) { 457 $classes[] = 'phui-timeline-major-event'; 458 $content = phutil_tag( 459 'div', 460 array( 461 'class' => 'phui-timeline-inner-content', 462 ), 463 array( 464 $group_titles, 465 $menu, 466 phutil_tag( 467 'div', 468 array( 469 'class' => 'phui-timeline-core-content', 470 ), 471 $group_children), 472 )); 473 } else { 474 $classes[] = 'phui-timeline-minor-event'; 475 $content = $group_titles; 476 } 477 478 $content = phutil_tag( 479 'div', 480 array( 481 'class' => 'phui-timeline-group', 482 ), 483 $content); 484 485 // Image Events 486 $pinboard = null; 487 if ($this->pinboardItems) { 488 $pinboard = new PHUIPinboardView(); 489 foreach ($this->pinboardItems as $item) { 490 $pinboard->addItem($item); 491 } 492 } 493 494 $content = phutil_tag( 495 'div', 496 array( 497 'class' => implode(' ', $content_classes), 498 ), 499 array($image, $badges, $wedge, $content, $pinboard)); 500 501 $outer_classes = $this->classes; 502 $outer_classes[] = 'phui-timeline-shell'; 503 $color = null; 504 foreach ($this->getEventGroup() as $event) { 505 if ($event->color) { 506 $color = $event->color; 507 break; 508 } 509 } 510 511 if ($color) { 512 $outer_classes[] = 'phui-timeline-'.$color; 513 } 514 515 $sigils = array(); 516 $meta = null; 517 if ($this->getTransactionPHID()) { 518 $sigils[] = 'transaction'; 519 $meta = array( 520 'phid' => $this->getTransactionPHID(), 521 'anchor' => $this->anchor, 522 ); 523 } 524 525 $major_event = null; 526 if ($this->reallyMajorEvent) { 527 $major_event = phutil_tag( 528 'div', 529 array( 530 'class' => 'phui-timeline-event-view '. 531 'phui-timeline-spacer '. 532 'phui-timeline-spacer-bold', 533 )); 534 } 535 536 $sigils[] = 'anchor-container'; 537 538 return array( 539 javelin_tag( 540 'div', 541 array( 542 'class' => implode(' ', $outer_classes), 543 'sigil' => implode(' ', $sigils), 544 'meta' => $meta, 545 ), 546 phutil_tag( 547 'div', 548 array( 549 'class' => implode(' ', $classes), 550 ), 551 $content)), 552 $major_event, 553 ); 554 } 555 556 private function renderExtra(array $events) { 557 $extra = array(); 558 559 if ($this->getIsPreview()) { 560 $extra[] = pht('PREVIEW'); 561 } else { 562 foreach ($events as $event) { 563 if ($event->getIsEdited()) { 564 $extra[] = pht('Edited'); 565 break; 566 } 567 } 568 569 $source = $this->getContentSource(); 570 $content_source = null; 571 if ($source) { 572 $content_source = id(new PhabricatorContentSourceView()) 573 ->setContentSource($source) 574 ->setViewer($this->getUser()); 575 $content_source = pht('Via %s', $content_source->getSourceName()); 576 } 577 578 $date_created = null; 579 foreach ($events as $event) { 580 if ($event->getDateCreated()) { 581 if ($date_created === null) { 582 $date_created = $event->getDateCreated(); 583 } else { 584 $date_created = min($event->getDateCreated(), $date_created); 585 } 586 } 587 } 588 589 if ($date_created) { 590 $date = phabricator_dual_datetime( 591 $date_created, 592 $this->getUser()); 593 if ($this->anchor) { 594 Javelin::initBehavior('phabricator-watch-anchor'); 595 Javelin::initBehavior('phabricator-tooltips'); 596 597 $date = array( 598 javelin_tag( 599 'a', 600 array( 601 'href' => '#'.$this->anchor, 602 'sigil' => 'has-tooltip', 603 'meta' => array( 604 'tip' => $content_source, 605 ), 606 ), 607 $date), 608 ); 609 } 610 611 $extra[] = $date; 612 } 613 614 // If this edit was applied silently, give user a hint that they should 615 // not expect to have received any mail or notifications. 616 if ($this->getIsSilent()) { 617 $extra[] = id(new PHUIIconView()) 618 ->setIcon('fa-bell-slash', 'white') 619 ->setEmblemColor('red') 620 ->setTooltip(pht('Silent Edit')); 621 } 622 623 // If this edit was applied while the actor was in high-security mode, 624 // provide a hint that it was extra authentic. 625 if ($this->getIsMFA()) { 626 $extra[] = id(new PHUIIconView()) 627 ->setIcon('fa-vcard', 'white') 628 ->setEmblemColor('pink') 629 ->setTooltip(pht('MFA Authenticated')); 630 } 631 632 if ($this->getIsLockOverride()) { 633 $extra[] = id(new PHUIIconView()) 634 ->setIcon('fa-chain-broken', 'white') 635 ->setEmblemColor('violet') 636 ->setTooltip(pht('Lock Overridden')); 637 } 638 } 639 640 $extra = javelin_tag( 641 'span', 642 array( 643 'class' => 'phui-timeline-extra', 644 ), 645 phutil_implode_html( 646 javelin_tag( 647 'span', 648 array( 649 'aural' => false, 650 ), 651 self::DELIMITER), 652 $extra)); 653 654 return $extra; 655 } 656 657 private function getMenuItems($anchor) { 658 $xaction_phid = $this->getTransactionPHID(); 659 660 $can_interact = $this->getCanInteract(); 661 $viewer = $this->getViewer(); 662 $is_admin = $viewer->getIsAdmin(); 663 664 $items = array(); 665 666 if ($this->getIsEditable()) { 667 $items[] = id(new PhabricatorActionView()) 668 ->setIcon('fa-pencil') 669 ->setHref('/transactions/edit/'.$xaction_phid.'/') 670 ->setName(pht('Edit Comment')) 671 ->addSigil('transaction-edit') 672 ->setDisabled(!$can_interact) 673 ->setMetadata( 674 array( 675 'anchor' => $anchor, 676 )); 677 } 678 679 if ($this->getQuoteTargetID()) { 680 $ref = null; 681 if ($this->getQuoteRef()) { 682 $ref = $this->getQuoteRef(); 683 if ($anchor) { 684 $ref = $ref.'#'.$anchor; 685 } 686 } 687 688 $items[] = id(new PhabricatorActionView()) 689 ->setIcon('fa-quote-left') 690 ->setName(pht('Quote Comment')) 691 ->setHref('#') 692 ->addSigil('transaction-quote') 693 ->setMetadata( 694 array( 695 'targetID' => $this->getQuoteTargetID(), 696 'uri' => '/transactions/quote/'.$xaction_phid.'/', 697 'ref' => $ref, 698 )); 699 } 700 701 if ($this->getIsNormalComment()) { 702 $items[] = id(new PhabricatorActionView()) 703 ->setIcon('fa-code') 704 ->setHref('/transactions/raw/'.$xaction_phid.'/') 705 ->setName(pht('View Raw Remarkup')) 706 ->addSigil('transaction-raw') 707 ->setMetadata( 708 array( 709 'anchor' => $anchor, 710 )); 711 712 $content_source = $this->getContentSource(); 713 $source_email = PhabricatorEmailContentSource::SOURCECONST; 714 if ($content_source->getSource() == $source_email) { 715 $source_id = $content_source->getContentSourceParameter('id'); 716 if ($source_id) { 717 $items[] = id(new PhabricatorActionView()) 718 ->setIcon('fa-envelope-o') 719 ->setHref('/transactions/raw/'.$xaction_phid.'/?email') 720 ->setName(pht('View Email Body')) 721 ->addSigil('transaction-raw') 722 ->setMetadata( 723 array( 724 'anchor' => $anchor, 725 )); 726 } 727 } 728 } 729 730 if ($this->getIsEdited()) { 731 $items[] = id(new PhabricatorActionView()) 732 ->setIcon('fa-list') 733 ->setHref('/transactions/history/'.$xaction_phid.'/') 734 ->setName(pht('View Edit History')) 735 ->setWorkflow(true); 736 } 737 738 if ($this->getIsRemovable()) { 739 $items[] = id(new PhabricatorActionView()) 740 ->setType(PhabricatorActionView::TYPE_DIVIDER); 741 742 $remove_item = id(new PhabricatorActionView()) 743 ->setIcon('fa-trash-o') 744 ->setHref('/transactions/remove/'.$xaction_phid.'/') 745 ->setName(pht('Remove Comment')) 746 ->addSigil('transaction-remove') 747 ->setMetadata( 748 array( 749 'anchor' => $anchor, 750 )); 751 752 if (!$is_admin && !$can_interact) { 753 $remove_item->setDisabled(true); 754 } else { 755 $remove_item->setColor(PhabricatorActionView::RED); 756 } 757 758 $items[] = $remove_item; 759 } 760 761 return $items; 762 } 763 764}