3 final class DifferentialInlineCommentMailView
4 extends DifferentialMailView
{
11 public function setViewer(PhabricatorUser
$viewer) {
12 $this->viewer
= $viewer;
16 public function getViewer() {
20 public function setInlines($inlines) {
21 $this->inlines
= $inlines;
25 public function getInlines() {
26 return $this->inlines
;
29 public function buildMailSection() {
30 $inlines = $this->getInlines();
32 $comments = mpull($inlines, 'getComment');
33 $comments = mpull($comments, null, 'getPHID');
34 $parents = $this->loadParents($comments);
35 $all_comments = $comments +
$parents;
37 $this->changesets
= $this->loadChangesets($all_comments);
38 $this->authors
= $this->loadAuthors($all_comments);
39 $groups = $this->groupInlines($inlines);
41 $hunk_parser = new DifferentialHunkParser();
44 $spacer_html = phutil_tag('br');
46 $section = new PhabricatorMetaMTAMailSection();
48 $last_group_key = last_key($groups);
49 foreach ($groups as $changeset_id => $group) {
50 $changeset = $this->getChangeset($changeset_id);
55 $is_last_group = ($changeset_id == $last_group_key);
57 $last_inline_key = last_key($group);
58 foreach ($group as $inline_key => $inline) {
59 $comment = $inline->getComment();
60 $parent_phid = $comment->getReplyToCommentPHID();
62 $inline_object = $comment->newInlineCommentObject();
63 $document_engine_key = $inline_object->getDocumentEngineKey();
65 $is_last_inline = ($inline_key == $last_inline_key);
71 $parent = idx($parents, $parent_phid);
73 $context_text = $this->renderInline($parent, false, true);
74 $context_html = $this->renderInline($parent, true, true);
76 } else if ($document_engine_key !== null) {
77 // See T13513. If an inline was left on a rendered document, don't
78 // include the patch context. Document engines currently can not
79 // render to mail targets, and using the line numbers as raw source
80 // lines produces misleading context.
83 $context_text = $this->renderPatch($comment, $patch_text, false);
86 $context_html = $this->renderPatch($comment, $patch_html, true);
88 $patch_text = $this->getPatch($hunk_parser, $comment, false);
89 $context_text = $this->renderPatch($comment, $patch_text, false);
91 $patch_html = $this->getPatch($hunk_parser, $comment, true);
92 $context_html = $this->renderPatch($comment, $patch_html, true);
95 $render_text = $this->renderInline($comment, false, false);
96 $render_html = $this->renderInline($comment, true, false);
98 $section->addPlaintextFragment($context_text);
99 $section->addPlaintextFragment($spacer_text);
100 $section->addPlaintextFragment($render_text);
102 $html_fragment = $this->renderContentBox(
108 $section->addHTMLFragment($html_fragment);
110 if (!$is_last_group ||
!$is_last_inline) {
111 $section->addPlaintextFragment($spacer_text);
112 $section->addHTMLFragment($spacer_html);
120 private function loadChangesets(array $comments) {
126 foreach ($comments as $comment) {
127 $ids[] = $comment->getChangesetID();
130 $changesets = id(new DifferentialChangesetQuery())
131 ->setViewer($this->getViewer())
136 return mpull($changesets, null, 'getID');
139 private function loadParents(array $comments) {
140 $viewer = $this->getViewer();
143 foreach ($comments as $comment) {
144 $parent_phid = $comment->getReplyToCommentPHID();
148 $phids[] = $parent_phid;
155 $parents = id(new DifferentialDiffInlineCommentQuery())
160 return mpull($parents, null, 'getPHID');
163 private function loadAuthors(array $comments) {
164 $viewer = $this->getViewer();
167 foreach ($comments as $comment) {
168 $author_phid = $comment->getAuthorPHID();
172 $phids[] = $author_phid;
179 return $viewer->loadHandles($phids);
182 private function groupInlines(array $inlines) {
183 return DifferentialTransactionComment
::sortAndGroupInlines(
188 private function renderInline(
189 DifferentialTransactionComment
$comment,
193 $changeset = $this->getChangeset($comment->getChangesetID());
198 $content = $comment->getContent();
199 $content = $this->renderRemarkupContent($content, $is_html);
202 $header = $this->renderHeader($comment, $is_html, true);
214 $style[] = 'color: #74777D;';
217 $content = phutil_tag(
220 'style' => implode(' ', $style),
232 $parts = implode('', $parts);
233 $parts = trim($parts);
238 $parts = $this->quoteHTML($parts);
240 $parts = $this->quoteText($parts);
247 private function renderRemarkupContent($content, $is_html) {
248 $viewer = $this->getViewer();
249 $production_uri = PhabricatorEnv
::getProductionURI('/');
252 $mode = PhutilRemarkupEngine
::MODE_HTML_MAIL
;
254 $mode = PhutilRemarkupEngine
::MODE_TEXT
;
258 'style' => 'padding: 0; margin: 8px;',
261 $engine = PhabricatorMarkupEngine
::newMarkupEngine(array())
262 ->setConfig('viewer', $viewer)
263 ->setConfig('uri.base', $production_uri)
264 ->setConfig('default.p.attributes', $attributes)
268 return $engine->markupText($content);
269 } catch (Exception
$ex) {
274 private function getChangeset($id) {
275 return idx($this->changesets
, $id);
278 private function getAuthor($phid) {
279 if (isset($this->authors
[$phid])) {
280 return $this->authors
[$phid];
285 private function quoteText($block) {
286 $block = phutil_split_lines($block);
287 foreach ($block as $key => $line) {
288 $block[$key] = '> '.$line;
291 return implode('', $block);
294 private function quoteHTML($block) {
297 'background: #F7F7F7;',
298 'border-color: #e3e4e8;',
299 'border-style: solid;',
300 'border-width: 0 0 1px 0;',
304 $styles = implode(' ', $styles);
314 private function getPatch(
315 DifferentialHunkParser
$parser,
316 DifferentialTransactionComment
$comment,
319 $changeset = $this->getChangeset($comment->getChangesetID());
320 $is_new = $comment->getIsNewFile();
321 $start = $comment->getLineNumber();
322 $length = $comment->getLineLength();
324 // By default, show one line of context around the target inline.
327 // If the inline is at least 3 lines long, don't show any extra context.
332 // If the inline is more than 7 lines long, only show the first 7 lines.
338 $hunks = $changeset->getHunks();
339 $patch = $parser->makeContextDiff(
345 $patch = phutil_split_lines($patch);
347 // Remove the "@@ -x,y +u,v @@" line.
350 return implode('', $patch);
353 $viewer = $this->getViewer();
354 $engine = new PhabricatorMarkupEngine();
357 $offset_mode = 'new';
359 $offset_mode = 'old';
362 // See PHI894. Use the parse cache since we can end up with a large
363 // rendering cost otherwise when users or bots leave hundreds of inline
364 // comments on diffs with long recipient lists.
365 $cache_key = $changeset->getID();
367 $viewstate = new PhabricatorChangesetViewState();
369 $parser = id(new DifferentialChangesetParser())
370 ->setRenderCacheKey($cache_key)
372 ->setViewstate($viewstate)
373 ->setChangeset($changeset)
374 ->setOffsetMode($offset_mode)
375 ->setMarkupEngine($engine);
377 $parser->setRenderer(new DifferentialChangesetOneUpMailRenderer());
379 return $parser->render(
381 $length +
(2 * $context),
385 private function renderPatch(
386 DifferentialTransactionComment
$comment,
391 if ($patch !== null) {
392 $patch = $this->renderCodeBlock($patch);
396 $header = $this->renderHeader($comment, $is_html, false);
398 if ($patch === null) {
411 $patch = implode('', $patch);
412 $patch = $this->quoteText($patch);
414 $patch = $this->quoteHTML($patch);
420 private function renderHeader(
421 DifferentialTransactionComment
$comment,
425 $changeset = $this->getChangeset($comment->getChangesetID());
426 $path = $changeset->getFilename();
428 // Only show the filename.
429 $path = basename($path);
431 $start = $comment->getLineNumber();
432 $length = $comment->getLineLength();
434 $range = pht('%s-%s', $start, $start +
$length);
439 $header = "{$path}:{$range}";
441 $header = $this->renderHeaderBold($header);
445 $author = $this->getAuthor($comment->getAuthorPHID());
451 $byline = $author->getName();
454 $byline = $this->renderHeaderBold($byline);
457 $header = pht('%s wrote in %s', $byline, $header);
461 $link_href = $this->getInlineURI($comment);
465 'text-decoration: none;',
471 'style' => implode(' ', $link_style),
472 'href' => $link_href,
477 // See PHI920. Add a space after the link so we render this into
480 // View Inline filename.txt
482 // Otherwise, we render "Inlinefilename.txt" and double-clicking
483 // the file name selects the word "Inline" as well.
490 $header = $this->renderHeaderBlock(array($link, $header));
496 private function getInlineURI(DifferentialTransactionComment
$comment) {
497 $changeset = $this->getChangeset($comment->getChangesetID());
502 $diff = $changeset->getDiff();
507 $revision = $diff->getRevision();
512 $link_href = '/'.$revision->getMonogram().'#inline-'.$comment->getID();
513 $link_href = PhabricatorEnv
::getProductionURI($link_href);