@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<?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}