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