@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 602 lines 15 kB view raw
1<?php 2 3final class PHUIDiffInlineCommentDetailView 4 extends PHUIDiffInlineCommentView { 5 6 private $handles; 7 private $markupEngine; 8 private $editable; 9 private $preview; 10 private $allowReply; 11 private $canMarkDone; 12 private $objectOwnerPHID; 13 14 public function isHidden() { 15 return $this->getInlineComment()->isHidden(); 16 } 17 18 /** 19 * @param array<PhabricatorObjectHandle> $handles 20 */ 21 public function setHandles(array $handles) { 22 assert_instances_of($handles, PhabricatorObjectHandle::class); 23 $this->handles = $handles; 24 return $this; 25 } 26 27 public function setMarkupEngine(PhabricatorMarkupEngine $engine) { 28 $this->markupEngine = $engine; 29 return $this; 30 } 31 32 public function setEditable($editable) { 33 $this->editable = $editable; 34 return $this; 35 } 36 37 public function setPreview($preview) { 38 $this->preview = $preview; 39 return $this; 40 } 41 42 public function setAllowReply($allow_reply) { 43 $this->allowReply = $allow_reply; 44 return $this; 45 } 46 47 public function setCanMarkDone($can_mark_done) { 48 $this->canMarkDone = $can_mark_done; 49 return $this; 50 } 51 52 public function getCanMarkDone() { 53 return $this->canMarkDone; 54 } 55 56 public function setObjectOwnerPHID($phid) { 57 $this->objectOwnerPHID = $phid; 58 return $this; 59 } 60 61 public function getObjectOwnerPHID() { 62 return $this->objectOwnerPHID; 63 } 64 65 public function getAnchorName() { 66 $inline = $this->getInlineComment(); 67 if ($inline->getID()) { 68 return 'inline-'.$inline->getID(); 69 } 70 return null; 71 } 72 73 public function getScaffoldCellID() { 74 $anchor = $this->getAnchorName(); 75 if ($anchor) { 76 return 'anchor-'.$anchor; 77 } 78 return null; 79 } 80 81 public function render() { 82 require_celerity_resource('phui-inline-comment-view-css'); 83 $inline = $this->getInlineComment(); 84 85 $is_synthetic = false; 86 if ($inline->getSyntheticAuthor()) { 87 $is_synthetic = true; 88 } 89 90 $is_preview = $this->preview; 91 92 $metadata = $this->getInlineCommentMetadata(); 93 94 $classes = array( 95 'differential-inline-comment', 96 ); 97 98 $sigil = 'differential-inline-comment'; 99 if ($is_preview) { 100 $sigil = $sigil.' differential-inline-comment-preview'; 101 102 $classes[] = 'inline-comment-preview'; 103 } else { 104 $classes[] = 'inline-comment-element'; 105 } 106 107 $handles = $this->handles; 108 109 $links = array(); 110 111 $draft_text = null; 112 if (!$is_synthetic) { 113 // This display is controlled by CSS 114 $draft_text = id(new PHUITagView()) 115 ->setType(PHUITagView::TYPE_SHADE) 116 ->setName(pht('Unsubmitted')) 117 ->setSlimShady(true) 118 ->setColor(PHUITagView::COLOR_RED) 119 ->addClass('mml inline-draft-text'); 120 } 121 122 $ghost_tag = null; 123 $ghost = $inline->getIsGhost(); 124 $ghost_id = null; 125 if ($ghost) { 126 if ($ghost['new']) { 127 $ghosticon = 'fa-fast-forward'; 128 $reason = pht('View on forward revision'); 129 } else { 130 $ghosticon = 'fa-fast-backward'; 131 $reason = pht('View on previous revision'); 132 } 133 134 $ghost_icon = id(new PHUIIconView()) 135 ->setIcon($ghosticon) 136 ->addSigil('has-tooltip') 137 ->setMetadata( 138 array( 139 'tip' => $reason, 140 'size' => 300, 141 )); 142 $ghost_tag = phutil_tag( 143 'a', 144 array( 145 'class' => 'ghost-icon', 146 'href' => $ghost['href'], 147 'target' => '_blank', 148 ), 149 $ghost_icon); 150 $classes[] = 'inline-comment-ghost'; 151 } 152 153 if ($inline->getReplyToCommentPHID()) { 154 $classes[] = 'inline-comment-is-reply'; 155 } 156 157 $viewer_phid = $this->getUser()->getPHID(); 158 $owner_phid = $this->getObjectOwnerPHID(); 159 160 if ($viewer_phid) { 161 if ($viewer_phid == $owner_phid) { 162 $classes[] = 'viewer-is-object-owner'; 163 } 164 } 165 166 $anchor_name = $this->getAnchorName(); 167 168 $action_buttons = array(); 169 $menu_items = array(); 170 171 if ($this->editable && !$is_preview) { 172 $menu_items[] = array( 173 'label' => pht('Edit Comment'), 174 'icon' => 'fa-pencil', 175 'action' => 'edit', 176 'key' => 'e', 177 ); 178 } else if ($is_preview) { 179 $links[] = javelin_tag( 180 'a', 181 array( 182 'class' => 'inline-button-divider pml msl', 183 'meta' => array( 184 'inlineCommentID' => $inline->getID(), 185 ), 186 'sigil' => 'differential-inline-preview-jump', 187 ), 188 pht('View')); 189 190 $action_buttons[] = id(new PHUIButtonView()) 191 ->setTag('a') 192 ->setTooltip(pht('Delete')) 193 ->setIcon('fa-trash-o') 194 ->addSigil('differential-inline-delete') 195 ->setMustCapture(true) 196 ->setAuralLabel(pht('Delete')); 197 } 198 199 if (!$is_preview && $this->canHide()) { 200 $menu_items[] = array( 201 'label' => pht('Collapse'), 202 'icon' => 'fa-times', 203 'action' => 'collapse', 204 'key' => 'q', 205 ); 206 } 207 208 $can_reply = 209 (!$this->editable) && 210 (!$is_preview) && 211 ($this->allowReply) && 212 213 // NOTE: No product reason why you can't reply to synthetic comments, 214 // but the reply mechanism currently sends the inline comment ID to the 215 // server, not file/line information, and synthetic comments don't have 216 // an inline comment ID. 217 (!$is_synthetic); 218 219 if ($can_reply) { 220 $menu_items[] = array( 221 'label' => pht('Reply to Comment'), 222 'icon' => 'fa-reply', 223 'action' => 'reply', 224 'key' => 'r', 225 ); 226 227 $menu_items[] = array( 228 'label' => pht('Quote Comment'), 229 'icon' => 'fa-quote-left', 230 'action' => 'quote', 231 'key' => 'R', 232 ); 233 } 234 235 if (!$is_preview) { 236 $xaction_phid = $inline->getTransactionPHID(); 237 $storage = $inline->getStorageObject(); 238 239 if ($xaction_phid) { 240 $menu_items[] = array( 241 'label' => pht('View Raw Remarkup'), 242 'icon' => 'fa-code', 243 'action' => 'raw', 244 'uri' => $storage->getRawRemarkupURI(), 245 ); 246 } 247 } 248 249 if ($this->editable && !$is_preview) { 250 $menu_items[] = array( 251 'label' => pht('Delete Comment'), 252 'icon' => 'fa-trash-o', 253 'action' => 'delete', 254 ); 255 } 256 257 $done_button = null; 258 259 $mark_done = $this->getCanMarkDone(); 260 261 // Allow users to mark their own draft inlines as "Done". 262 if ($viewer_phid == $inline->getAuthorPHID()) { 263 if ($inline->isDraft()) { 264 $mark_done = true; 265 } 266 } 267 268 if (!$is_synthetic) { 269 $draft_state = false; 270 switch ($inline->getFixedState()) { 271 case PhabricatorInlineComment::STATE_DRAFT: 272 $is_done = $mark_done; 273 $draft_state = true; 274 break; 275 case PhabricatorInlineComment::STATE_UNDRAFT: 276 $is_done = !$mark_done; 277 $draft_state = true; 278 break; 279 case PhabricatorInlineComment::STATE_DONE: 280 $is_done = true; 281 break; 282 default: 283 case PhabricatorInlineComment::STATE_UNDONE: 284 $is_done = false; 285 break; 286 } 287 288 // If you don't have permission to mark the comment as "Done", you also 289 // can not see the draft state. 290 if (!$mark_done) { 291 $draft_state = false; 292 } 293 294 if ($is_done) { 295 $classes[] = 'inline-is-done'; 296 } 297 298 if ($draft_state) { 299 $classes[] = 'inline-state-is-draft'; 300 } 301 302 if ($mark_done && !$is_preview) { 303 $done_input = javelin_tag( 304 'input', 305 array( 306 'type' => 'checkbox', 307 'checked' => ($is_done ? 'checked' : null), 308 'class' => 'differential-inline-done', 309 'sigil' => 'differential-inline-done', 310 )); 311 $done_button = phutil_tag( 312 'label', 313 array( 314 'class' => 'differential-inline-done-label ', 315 ), 316 array( 317 $done_input, 318 pht('Done'), 319 )); 320 } else { 321 if ($is_done) { 322 $icon = id(new PHUIIconView())->setIcon('fa-check sky msr'); 323 $label = pht('Done'); 324 $class = 'button-done'; 325 } else { 326 $icon = null; 327 $label = pht('Not Done'); 328 $class = 'button-not-done'; 329 } 330 $done_button = phutil_tag( 331 'div', 332 array( 333 'class' => 'done-label '.$class, 334 ), 335 array( 336 $icon, 337 $label, 338 )); 339 } 340 } 341 342 $content = $this->markupEngine->getOutput( 343 $inline, 344 PhabricatorInlineComment::MARKUP_FIELD_BODY); 345 346 if ($is_preview) { 347 $anchor = null; 348 } else { 349 $anchor = phutil_tag( 350 'a', 351 array( 352 'name' => $anchor_name, 353 'id' => $anchor_name, 354 'class' => 'differential-inline-comment-anchor', 355 ), 356 ''); 357 } 358 359 if ($inline->isDraft() && !$is_synthetic) { 360 $classes[] = 'inline-state-is-draft'; 361 } 362 if ($is_synthetic) { 363 $classes[] = 'differential-inline-comment-synthetic'; 364 } 365 $classes = implode(' ', $classes); 366 367 $author_owner = null; 368 if ($is_synthetic) { 369 $author = $inline->getSyntheticAuthor(); 370 } else { 371 $author = $handles[$inline->getAuthorPHID()]->getName(); 372 if ($inline->getAuthorPHID() == $this->objectOwnerPHID) { 373 $author_owner = id(new PHUITagView()) 374 ->setType(PHUITagView::TYPE_SHADE) 375 ->setName(pht('Author')) 376 ->setSlimShady(true) 377 ->setColor(PHUITagView::COLOR_YELLOW) 378 ->addClass('mml'); 379 } 380 } 381 382 $actions = null; 383 if ($action_buttons || $menu_items) { 384 $actions = new PHUIButtonBarView(); 385 $actions->setBorderless(true); 386 $actions->addClass('inline-button-divider'); 387 foreach ($action_buttons as $button) { 388 $actions->addButton($button); 389 } 390 391 if (!$is_preview) { 392 $menu_button = id(new PHUIButtonView()) 393 ->setTag('a') 394 ->setColor(PHUIButtonView::GREY) 395 ->setDropdown(true) 396 ->setAuralLabel(pht('Inline Actions')) 397 ->addSigil('inline-action-dropdown'); 398 399 $actions->addButton($menu_button); 400 } 401 } 402 403 $group_left = phutil_tag( 404 'div', 405 array( 406 'class' => 'inline-head-left', 407 ), 408 array( 409 $author, 410 $author_owner, 411 $draft_text, 412 $ghost_tag, 413 )); 414 415 $group_right = phutil_tag( 416 'div', 417 array( 418 'class' => 'inline-head-right', 419 ), 420 array( 421 $done_button, 422 $links, 423 $actions, 424 )); 425 426 $snippet = id(new PhutilUTF8StringTruncator()) 427 ->setMaximumGlyphs(96) 428 ->truncateString($inline->getContent()); 429 $metadata['snippet'] = pht('%s: %s', $author, $snippet); 430 431 $metadata['menuItems'] = $menu_items; 432 433 $suggestion_content = $this->newSuggestionView($inline); 434 435 $inline_content = phutil_tag( 436 'div', 437 array( 438 'class' => 'phabricator-remarkup', 439 ), 440 $content); 441 442 $markup = javelin_tag( 443 'div', 444 array( 445 'class' => $classes, 446 'sigil' => $sigil, 447 'meta' => $metadata, 448 ), 449 array( 450 javelin_tag( 451 'div', 452 array( 453 'class' => 'differential-inline-comment-head grouped', 454 'sigil' => 'differential-inline-header', 455 ), 456 array( 457 $group_left, 458 $group_right, 459 )), 460 phutil_tag( 461 'div', 462 array( 463 'class' => 'differential-inline-comment-content', 464 ), 465 array( 466 $suggestion_content, 467 $inline_content, 468 )), 469 )); 470 471 $summary = phutil_tag( 472 'div', 473 array( 474 'class' => 'differential-inline-summary', 475 ), 476 array( 477 phutil_tag('strong', array(), pht('%s:', $author)), 478 ' ', 479 $snippet, 480 )); 481 482 return array( 483 $anchor, 484 $markup, 485 $summary, 486 ); 487 } 488 489 private function canHide() { 490 $inline = $this->getInlineComment(); 491 492 if ($inline->isDraft()) { 493 return false; 494 } 495 496 if (!$inline->getID()) { 497 return false; 498 } 499 500 $viewer = $this->getUser(); 501 if (!$viewer->isLoggedIn()) { 502 return false; 503 } 504 505 if (!$inline->supportsHiding()) { 506 return false; 507 } 508 509 return true; 510 } 511 512 private function newSuggestionView(PhabricatorInlineComment $inline) { 513 $content_state = $inline->getContentState(); 514 if (!$content_state->getContentHasSuggestion()) { 515 return null; 516 } 517 518 $context = $inline->getInlineContext(); 519 if (!$context) { 520 return null; 521 } 522 523 $head_lines = $context->getHeadLines(); 524 $head_lines = implode('', $head_lines); 525 526 $tail_lines = $context->getTailLines(); 527 $tail_lines = implode('', $tail_lines); 528 529 $old_lines = $context->getBodyLines(); 530 $old_lines = implode('', $old_lines); 531 $old_lines = $head_lines.$old_lines.$tail_lines; 532 if (strlen($old_lines) && !preg_match('/\n\z/', $old_lines)) { 533 $old_lines .= "\n"; 534 } 535 536 $new_lines = $content_state->getContentSuggestionText(); 537 $new_lines = $head_lines.$new_lines.$tail_lines; 538 if (strlen($new_lines) && !preg_match('/\n\z/', $new_lines)) { 539 $new_lines .= "\n"; 540 } 541 542 if ($old_lines === $new_lines) { 543 return null; 544 } 545 546 $viewer = $this->getViewer(); 547 548 $changeset = id(new PhabricatorDifferenceEngine()) 549 ->generateChangesetFromFileContent($old_lines, $new_lines); 550 551 $changeset->setFilename($context->getFilename()); 552 553 $viewstate = new PhabricatorChangesetViewState(); 554 555 $parser = id(new DifferentialChangesetParser()) 556 ->setViewer($viewer) 557 ->setViewstate($viewstate) 558 ->setChangeset($changeset); 559 560 $fragment = $inline->getInlineCommentCacheFragment(); 561 if ($fragment !== null) { 562 $cache_key = sprintf( 563 '%s.suggestion-view(v1, %s)', 564 $fragment, 565 PhabricatorHash::digestForIndex($new_lines)); 566 $parser->setRenderCacheKey($cache_key); 567 } 568 569 $renderer = new DifferentialChangesetOneUpRenderer(); 570 $renderer->setSimpleMode(true); 571 572 $parser->setRenderer($renderer); 573 574 // See PHI1896. If a user leaves an inline on a very long range with 575 // suggestions at the beginning and end, we'll hide context in the middle 576 // by default. We don't want to do this in the context of an inline 577 // suggestion, so build a mask to force display of all lines. 578 579 // (We don't know exactly how many lines the diff has, we just know that 580 // it can't have more lines than the old file plus the new file, so we're 581 // using that as an upper bound.) 582 583 $min = 0; 584 585 $old_len = count(phutil_split_lines($old_lines)); 586 $new_len = count(phutil_split_lines($new_lines)); 587 $max = ($old_len + $new_len); 588 589 $mask = array_fill($min, ($max - $min), true); 590 591 $diff_view = $parser->render($min, ($max - $min), $mask); 592 593 $view = phutil_tag( 594 'div', 595 array( 596 'class' => 'inline-suggestion-view PhabricatorMonospaced', 597 ), 598 $diff_view); 599 600 return $view; 601 } 602}