Restore highlighting when jumping to transactions using URI anchors
[phabricator/blender.git] / src / view / phui / PHUITimelineEventView.php
blob83c1889ed79a2aaa8f1d77ff27689171d40bf231
1 <?php
3 final class PHUITimelineEventView extends AphrontView {
5 const DELIMITER = " \xC2\xB7 ";
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;
37 public function setAuthorPHID($author_phid) {
38 $this->authorPHID = $author_phid;
39 return $this;
42 public function getAuthorPHID() {
43 return $this->authorPHID;
46 public function setQuoteRef($quote_ref) {
47 $this->quoteRef = $quote_ref;
48 return $this;
51 public function getQuoteRef() {
52 return $this->quoteRef;
55 public function setQuoteTargetID($quote_target_id) {
56 $this->quoteTargetID = $quote_target_id;
57 return $this;
60 public function getQuoteTargetID() {
61 return $this->quoteTargetID;
64 public function setIsNormalComment($is_normal_comment) {
65 $this->isNormalComment = $is_normal_comment;
66 return $this;
69 public function getIsNormalComment() {
70 return $this->isNormalComment;
73 public function setHideByDefault($hide_by_default) {
74 $this->hideByDefault = $hide_by_default;
75 return $this;
78 public function getHideByDefault() {
79 return $this->hideByDefault;
82 public function setTransactionPHID($transaction_phid) {
83 $this->transactionPHID = $transaction_phid;
84 return $this;
87 public function getTransactionPHID() {
88 return $this->transactionPHID;
91 public function setIsEdited($is_edited) {
92 $this->isEdited = $is_edited;
93 return $this;
96 public function getIsEdited() {
97 return $this->isEdited;
100 public function setIsPreview($is_preview) {
101 $this->isPreview = $is_preview;
102 return $this;
105 public function getIsPreview() {
106 return $this->isPreview;
109 public function setIsEditable($is_editable) {
110 $this->isEditable = $is_editable;
111 return $this;
114 public function getIsEditable() {
115 return $this->isEditable;
118 public function setCanInteract($can_interact) {
119 $this->canInteract = $can_interact;
120 return $this;
123 public function getCanInteract() {
124 return $this->canInteract;
127 public function setIsRemovable($is_removable) {
128 $this->isRemovable = $is_removable;
129 return $this;
132 public function getIsRemovable() {
133 return $this->isRemovable;
136 public function setDateCreated($date_created) {
137 $this->dateCreated = $date_created;
138 return $this;
141 public function getDateCreated() {
142 return $this->dateCreated;
145 public function setContentSource(PhabricatorContentSource $content_source) {
146 $this->contentSource = $content_source;
147 return $this;
150 public function getContentSource() {
151 return $this->contentSource;
154 public function setUserHandle(PhabricatorObjectHandle $handle) {
155 $this->userHandle = $handle;
156 return $this;
159 public function setAnchor($anchor) {
160 $this->anchor = $anchor;
161 return $this;
164 public function getAnchor() {
165 return $this->anchor;
168 public function setTitle($title) {
169 $this->title = $title;
170 return $this;
173 public function addClass($class) {
174 $this->classes[] = $class;
175 return $this;
178 public function addBadge(PHUIBadgeMiniView $badge) {
179 $this->badges[] = $badge;
180 return $this;
183 public function setIcon($icon) {
184 $this->icon = $icon;
185 return $this;
188 public function setColor($color) {
189 $this->color = $color;
190 return $this;
193 public function setIsSilent($is_silent) {
194 $this->isSilent = $is_silent;
195 return $this;
198 public function getIsSilent() {
199 return $this->isSilent;
202 public function setIsMFA($is_mfa) {
203 $this->isMFA = $is_mfa;
204 return $this;
207 public function getIsMFA() {
208 return $this->isMFA;
211 public function setIsLockOverride($is_override) {
212 $this->isLockOverride = $is_override;
213 return $this;
216 public function getIsLockOverride() {
217 return $this->isLockOverride;
220 public function setReallyMajorEvent($me) {
221 $this->reallyMajorEvent = $me;
222 return $this;
225 public function setHideCommentOptions($hide_comment_options) {
226 $this->hideCommentOptions = $hide_comment_options;
227 return $this;
230 public function getHideCommentOptions() {
231 return $this->hideCommentOptions;
234 public function addPinboardItem(PHUIPinboardItemView $item) {
235 $this->pinboardItems[] = $item;
236 return $this;
239 public function setToken($token, $removed = false) {
240 $this->token = $token;
241 $this->tokenRemoved = $removed;
242 return $this;
245 public function getEventGroup() {
246 return array_merge(array($this), $this->eventGroup);
249 public function addEventToGroup(PHUITimelineEventView $event) {
250 $this->eventGroup[] = $event;
251 return $this;
254 protected function shouldRenderEventTitle() {
255 if ($this->title === null) {
256 return false;
259 return true;
262 protected function renderEventTitle($force_icon, $has_menu, $extra) {
263 $title = $this->title;
265 $title_classes = array();
266 $title_classes[] = 'phui-timeline-title';
268 $icon = null;
269 if ($this->icon || $force_icon) {
270 $title_classes[] = 'phui-timeline-title-with-icon';
273 if ($has_menu) {
274 $title_classes[] = 'phui-timeline-title-with-menu';
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;
285 $icon = id(new PHUIIconView())
286 ->setIcon($this->icon)
287 ->addClass('phui-timeline-icon');
289 $icon = phutil_tag(
290 'span',
291 array(
292 'class' => implode(' ', $fill_classes),
294 $icon);
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');
308 $title = phutil_tag(
309 'div',
310 array(
311 'class' => implode(' ', $title_classes),
313 array($icon, $token, $title, $extra));
315 return $title;
318 public function render() {
320 $events = $this->getEventGroup();
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;
329 $events = array_select_keys($events, $icon_keys) + $events;
330 $force_icon = (bool)$icon_keys;
332 $menu = null;
333 $items = array();
334 if (!$this->getIsPreview() && !$this->getHideCommentOptions()) {
335 foreach ($this->getEventGroup() as $event) {
336 $items[] = $event->getMenuItems($this->anchor);
338 $items = array_mergev($items);
341 if ($items) {
342 $icon = id(new PHUIIconView())
343 ->setIcon('fa-caret-down');
344 $aural = javelin_tag(
345 'span',
346 array(
347 'aural' => true,
349 pht('Comment Actions'));
351 if ($items) {
352 $sigil = 'phui-dropdown-menu';
353 Javelin::initBehavior('phui-dropdown-menu');
354 } else {
355 $sigil = null;
358 $action_list = id(new PhabricatorActionListView())
359 ->setUser($this->getUser());
360 foreach ($items as $item) {
361 $action_list->addAction($item);
364 $menu = javelin_tag(
365 $items ? 'a' : 'span',
366 array(
367 'href' => '#',
368 'class' => 'phui-timeline-menu',
369 'sigil' => $sigil,
370 'aria-haspopup' => 'true',
371 'aria-expanded' => 'false',
372 'meta' => $action_list->getDropdownMenuMetadata(),
374 array(
375 $aural,
376 $icon,
379 $has_menu = true;
380 } else {
381 $has_menu = false;
384 // Render "extra" information (timestamp, etc).
385 $extra = $this->renderExtra($events);
387 $show_badges = false;
389 $group_titles = array();
390 $group_items = array();
391 $group_children = array();
392 foreach ($events as $event) {
393 if ($event->shouldRenderEventTitle()) {
395 // Render the group anchor here, outside the title box. If we render
396 // it inside the title box it ends up completely hidden and Chrome 55
397 // refuses to jump to it. See T11997 for discussion.
399 if ($extra && $this->anchor) {
400 $group_titles[] = id(new PhabricatorAnchorView())
401 ->setAnchorName($this->anchor)
402 ->render();
405 $group_titles[] = $event->renderEventTitle(
406 $force_icon,
407 $has_menu,
408 $extra);
410 // Don't render this information more than once.
411 $extra = null;
414 if ($event->hasChildren()) {
415 $group_children[] = $event->renderChildren();
416 $show_badges = true;
420 $image_uri = $this->userHandle->getImageURI();
422 $wedge = phutil_tag(
423 'div',
424 array(
425 'class' => 'phui-timeline-wedge',
426 'style' => (nonempty($image_uri)) ? '' : 'display: none;',
428 '');
430 $image = null;
431 $badges = null;
432 if ($image_uri) {
433 $image = javelin_tag(
434 ($this->userHandle->getURI()) ? 'a' : 'div',
435 array(
436 'style' => 'background-image: url('.$image_uri.')',
437 'class' => 'phui-timeline-image',
438 'href' => $this->userHandle->getURI(),
439 'aural' => false,
441 '');
442 if ($this->badges && $show_badges) {
443 $flex = new PHUIBadgeBoxView();
444 $flex->addItems($this->badges);
445 $flex->setCollapsed(true);
446 $badges = phutil_tag(
447 'div',
448 array(
449 'class' => 'phui-timeline-badges',
451 $flex);
455 $content_classes = array();
456 $content_classes[] = 'phui-timeline-content';
458 $classes = array();
459 $classes[] = 'phui-timeline-event-view';
460 if ($group_children) {
461 $classes[] = 'phui-timeline-major-event';
462 $content = phutil_tag(
463 'div',
464 array(
465 'class' => 'phui-timeline-inner-content',
467 array(
468 $group_titles,
469 $menu,
470 phutil_tag(
471 'div',
472 array(
473 'class' => 'phui-timeline-core-content',
475 $group_children),
477 } else {
478 $classes[] = 'phui-timeline-minor-event';
479 $content = $group_titles;
482 $content = phutil_tag(
483 'div',
484 array(
485 'class' => 'phui-timeline-group',
487 $content);
489 // Image Events
490 $pinboard = null;
491 if ($this->pinboardItems) {
492 $pinboard = new PHUIPinboardView();
493 foreach ($this->pinboardItems as $item) {
494 $pinboard->addItem($item);
498 $content = phutil_tag(
499 'div',
500 array(
501 'class' => implode(' ', $content_classes),
503 array($image, $badges, $wedge, $content, $pinboard));
505 $outer_classes = $this->classes;
506 $outer_classes[] = 'phui-timeline-shell';
507 $color = null;
508 foreach ($this->getEventGroup() as $event) {
509 if ($event->color) {
510 $color = $event->color;
511 break;
515 if ($color) {
516 $outer_classes[] = 'phui-timeline-'.$color;
519 $sigils = array();
520 $meta = null;
521 if ($this->getTransactionPHID()) {
522 $sigils[] = 'transaction';
523 $meta = array(
524 'phid' => $this->getTransactionPHID(),
525 'anchor' => $this->anchor,
529 $major_event = null;
530 if ($this->reallyMajorEvent) {
531 $major_event = phutil_tag(
532 'div',
533 array(
534 'class' => 'phui-timeline-event-view '.
535 'phui-timeline-spacer '.
536 'phui-timeline-spacer-bold',
540 $sigils[] = 'anchor-container';
542 return array(
543 javelin_tag(
544 'div',
545 array(
546 'class' => implode(' ', $outer_classes),
547 'sigil' => implode(' ', $sigils),
548 'meta' => $meta,
550 phutil_tag(
551 'div',
552 array(
553 'class' => implode(' ', $classes),
555 $content)),
556 $major_event,
560 private function renderExtra(array $events) {
561 $extra = array();
563 if ($this->getIsPreview()) {
564 $extra[] = pht('PREVIEW');
565 } else {
566 foreach ($events as $event) {
567 if ($event->getIsEdited()) {
568 $extra[] = pht('Edited');
569 break;
573 $source = $this->getContentSource();
574 $content_source = null;
575 if ($source) {
576 $content_source = id(new PhabricatorContentSourceView())
577 ->setContentSource($source)
578 ->setUser($this->getUser());
579 $content_source = pht('Via %s', $content_source->getSourceName());
582 $date_created = null;
583 foreach ($events as $event) {
584 if ($event->getDateCreated()) {
585 if ($date_created === null) {
586 $date_created = $event->getDateCreated();
587 } else {
588 $date_created = min($event->getDateCreated(), $date_created);
593 if ($date_created) {
594 $date = phabricator_datetime(
595 $date_created,
596 $this->getUser());
597 if ($this->anchor) {
598 Javelin::initBehavior('phabricator-watch-anchor');
599 Javelin::initBehavior('phabricator-tooltips');
601 $date = array(
602 javelin_tag(
603 'a',
604 array(
605 'href' => '#'.$this->anchor,
606 'sigil' => 'has-tooltip',
607 'meta' => array(
608 'tip' => $content_source,
611 $date),
614 $extra[] = $date;
617 // If this edit was applied silently, give user a hint that they should
618 // not expect to have received any mail or notifications.
619 if ($this->getIsSilent()) {
620 $extra[] = id(new PHUIIconView())
621 ->setIcon('fa-bell-slash', 'white')
622 ->setEmblemColor('red')
623 ->setTooltip(pht('Silent Edit'));
626 // If this edit was applied while the actor was in high-security mode,
627 // provide a hint that it was extra authentic.
628 if ($this->getIsMFA()) {
629 $extra[] = id(new PHUIIconView())
630 ->setIcon('fa-vcard', 'white')
631 ->setEmblemColor('pink')
632 ->setTooltip(pht('MFA Authenticated'));
635 if ($this->getIsLockOverride()) {
636 $extra[] = id(new PHUIIconView())
637 ->setIcon('fa-chain-broken', 'white')
638 ->setEmblemColor('violet')
639 ->setTooltip(pht('Lock Overridden'));
643 $extra = javelin_tag(
644 'span',
645 array(
646 'class' => 'phui-timeline-extra',
648 phutil_implode_html(
649 javelin_tag(
650 'span',
651 array(
652 'aural' => false,
654 self::DELIMITER),
655 $extra));
657 return $extra;
660 private function getMenuItems($anchor) {
661 $xaction_phid = $this->getTransactionPHID();
663 $can_interact = $this->getCanInteract();
664 $viewer = $this->getViewer();
665 $is_admin = $viewer->getIsAdmin();
667 $items = array();
669 if ($this->getIsEditable()) {
670 $items[] = id(new PhabricatorActionView())
671 ->setIcon('fa-pencil')
672 ->setHref('/transactions/edit/'.$xaction_phid.'/')
673 ->setName(pht('Edit Comment'))
674 ->addSigil('transaction-edit')
675 ->setDisabled(!$can_interact)
676 ->setMetadata(
677 array(
678 'anchor' => $anchor,
682 if ($this->getQuoteTargetID()) {
683 $ref = null;
684 if ($this->getQuoteRef()) {
685 $ref = $this->getQuoteRef();
686 if ($anchor) {
687 $ref = $ref.'#'.$anchor;
691 $items[] = id(new PhabricatorActionView())
692 ->setIcon('fa-quote-left')
693 ->setName(pht('Quote Comment'))
694 ->setHref('#')
695 ->addSigil('transaction-quote')
696 ->setMetadata(
697 array(
698 'targetID' => $this->getQuoteTargetID(),
699 'uri' => '/transactions/quote/'.$xaction_phid.'/',
700 'ref' => $ref,
704 if ($this->getIsNormalComment()) {
705 $items[] = id(new PhabricatorActionView())
706 ->setIcon('fa-code')
707 ->setHref('/transactions/raw/'.$xaction_phid.'/')
708 ->setName(pht('View Remarkup'))
709 ->addSigil('transaction-raw')
710 ->setMetadata(
711 array(
712 'anchor' => $anchor,
715 $content_source = $this->getContentSource();
716 $source_email = PhabricatorEmailContentSource::SOURCECONST;
717 if ($content_source->getSource() == $source_email) {
718 $source_id = $content_source->getContentSourceParameter('id');
719 if ($source_id) {
720 $items[] = id(new PhabricatorActionView())
721 ->setIcon('fa-envelope-o')
722 ->setHref('/transactions/raw/'.$xaction_phid.'/?email')
723 ->setName(pht('View Email Body'))
724 ->addSigil('transaction-raw')
725 ->setMetadata(
726 array(
727 'anchor' => $anchor,
733 if ($this->getIsEdited()) {
734 $items[] = id(new PhabricatorActionView())
735 ->setIcon('fa-list')
736 ->setHref('/transactions/history/'.$xaction_phid.'/')
737 ->setName(pht('View Edit History'))
738 ->setWorkflow(true);
741 if ($this->getIsRemovable()) {
742 $items[] = id(new PhabricatorActionView())
743 ->setType(PhabricatorActionView::TYPE_DIVIDER);
745 $remove_item = id(new PhabricatorActionView())
746 ->setIcon('fa-trash-o')
747 ->setHref('/transactions/remove/'.$xaction_phid.'/')
748 ->setName(pht('Remove Comment'))
749 ->addSigil('transaction-remove')
750 ->setMetadata(
751 array(
752 'anchor' => $anchor,
755 if (!$is_admin && !$can_interact) {
756 $remove_item->setDisabled(!$is_admin && !$can_interact);
757 } else {
758 $remove_item->setColor(PhabricatorActionView::RED);
761 $items[] = $remove_item;
764 return $items;