Remove product literal strings in "pht()", part 6
[phabricator.git] / src / applications / differential / mail / DifferentialInlineCommentMailView.php
blob44c61611745b0b4c97f1042b051c1aaaa009bd54
1 <?php
3 final class DifferentialInlineCommentMailView
4 extends DifferentialMailView {
6 private $viewer;
7 private $inlines;
8 private $changesets;
9 private $authors;
11 public function setViewer(PhabricatorUser $viewer) {
12 $this->viewer = $viewer;
13 return $this;
16 public function getViewer() {
17 return $this->viewer;
20 public function setInlines($inlines) {
21 $this->inlines = $inlines;
22 return $this;
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();
43 $spacer_text = null;
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);
51 if (!$changeset) {
52 continue;
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);
67 $context_text = null;
68 $context_html = null;
70 if ($parent_phid) {
71 $parent = idx($parents, $parent_phid);
72 if ($parent) {
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.
82 $patch_text = null;
83 $context_text = $this->renderPatch($comment, $patch_text, false);
85 $patch_html = null;
86 $context_html = $this->renderPatch($comment, $patch_html, true);
87 } else {
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(
103 array(
104 $context_html,
105 $render_html,
108 $section->addHTMLFragment($html_fragment);
110 if (!$is_last_group || !$is_last_inline) {
111 $section->addPlaintextFragment($spacer_text);
112 $section->addHTMLFragment($spacer_html);
117 return $section;
120 private function loadChangesets(array $comments) {
121 if (!$comments) {
122 return array();
125 $ids = array();
126 foreach ($comments as $comment) {
127 $ids[] = $comment->getChangesetID();
130 $changesets = id(new DifferentialChangesetQuery())
131 ->setViewer($this->getViewer())
132 ->withIDs($ids)
133 ->needHunks(true)
134 ->execute();
136 return mpull($changesets, null, 'getID');
139 private function loadParents(array $comments) {
140 $viewer = $this->getViewer();
142 $phids = array();
143 foreach ($comments as $comment) {
144 $parent_phid = $comment->getReplyToCommentPHID();
145 if (!$parent_phid) {
146 continue;
148 $phids[] = $parent_phid;
151 if (!$phids) {
152 return array();
155 $parents = id(new DifferentialDiffInlineCommentQuery())
156 ->setViewer($viewer)
157 ->withPHIDs($phids)
158 ->execute();
160 return mpull($parents, null, 'getPHID');
163 private function loadAuthors(array $comments) {
164 $viewer = $this->getViewer();
166 $phids = array();
167 foreach ($comments as $comment) {
168 $author_phid = $comment->getAuthorPHID();
169 if (!$author_phid) {
170 continue;
172 $phids[] = $author_phid;
175 if (!$phids) {
176 return array();
179 return $viewer->loadHandles($phids);
182 private function groupInlines(array $inlines) {
183 return DifferentialTransactionComment::sortAndGroupInlines(
184 $inlines,
185 $this->changesets);
188 private function renderInline(
189 DifferentialTransactionComment $comment,
190 $is_html,
191 $is_quote) {
193 $changeset = $this->getChangeset($comment->getChangesetID());
194 if (!$changeset) {
195 return null;
198 $content = $comment->getContent();
199 $content = $this->renderRemarkupContent($content, $is_html);
201 if ($is_quote) {
202 $header = $this->renderHeader($comment, $is_html, true);
203 } else {
204 $header = null;
207 if ($is_html) {
208 $style = array(
209 'margin: 8px 0;',
210 'padding: 0 12px;',
213 if ($is_quote) {
214 $style[] = 'color: #74777D;';
217 $content = phutil_tag(
218 'div',
219 array(
220 'style' => implode(' ', $style),
222 $content);
225 $parts = array(
226 $header,
227 "\n",
228 $content,
231 if (!$is_html) {
232 $parts = implode('', $parts);
233 $parts = trim($parts);
236 if ($is_quote) {
237 if ($is_html) {
238 $parts = $this->quoteHTML($parts);
239 } else {
240 $parts = $this->quoteText($parts);
244 return $parts;
247 private function renderRemarkupContent($content, $is_html) {
248 $viewer = $this->getViewer();
249 $production_uri = PhabricatorEnv::getProductionURI('/');
251 if ($is_html) {
252 $mode = PhutilRemarkupEngine::MODE_HTML_MAIL;
253 } else {
254 $mode = PhutilRemarkupEngine::MODE_TEXT;
257 $attributes = array(
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)
265 ->setMode($mode);
267 try {
268 return $engine->markupText($content);
269 } catch (Exception $ex) {
270 return $content;
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];
282 return null;
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) {
295 $styles = array(
296 'padding: 0;',
297 'background: #F7F7F7;',
298 'border-color: #e3e4e8;',
299 'border-style: solid;',
300 'border-width: 0 0 1px 0;',
301 'margin: 0;',
304 $styles = implode(' ', $styles);
306 return phutil_tag(
307 'div',
308 array(
309 'style' => $styles,
311 $block);
314 private function getPatch(
315 DifferentialHunkParser $parser,
316 DifferentialTransactionComment $comment,
317 $is_html) {
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.
325 $context = 1;
327 // If the inline is at least 3 lines long, don't show any extra context.
328 if ($length >= 2) {
329 $context = 0;
332 // If the inline is more than 7 lines long, only show the first 7 lines.
333 if ($length >= 6) {
334 $length = 6;
337 if (!$is_html) {
338 $hunks = $changeset->getHunks();
339 $patch = $parser->makeContextDiff(
340 $hunks,
341 $is_new,
342 $start,
343 $length,
344 $context);
345 $patch = phutil_split_lines($patch);
347 // Remove the "@@ -x,y +u,v @@" line.
348 array_shift($patch);
350 return implode('', $patch);
353 $viewer = $this->getViewer();
354 $engine = new PhabricatorMarkupEngine();
356 if ($is_new) {
357 $offset_mode = 'new';
358 } else {
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)
371 ->setViewer($viewer)
372 ->setViewstate($viewstate)
373 ->setChangeset($changeset)
374 ->setOffsetMode($offset_mode)
375 ->setMarkupEngine($engine);
377 $parser->setRenderer(new DifferentialChangesetOneUpMailRenderer());
379 return $parser->render(
380 $start - $context,
381 $length + (2 * $context),
382 array());
385 private function renderPatch(
386 DifferentialTransactionComment $comment,
387 $patch,
388 $is_html) {
390 if ($is_html) {
391 if ($patch !== null) {
392 $patch = $this->renderCodeBlock($patch);
396 $header = $this->renderHeader($comment, $is_html, false);
398 if ($patch === null) {
399 $patch = array(
400 $header,
402 } else {
403 $patch = array(
404 $header,
405 "\n",
406 $patch,
410 if (!$is_html) {
411 $patch = implode('', $patch);
412 $patch = $this->quoteText($patch);
413 } else {
414 $patch = $this->quoteHTML($patch);
417 return $patch;
420 private function renderHeader(
421 DifferentialTransactionComment $comment,
422 $is_html,
423 $with_author) {
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();
433 if ($length) {
434 $range = pht('%s-%s', $start, $start + $length);
435 } else {
436 $range = $start;
439 $header = "{$path}:{$range}";
440 if ($is_html) {
441 $header = $this->renderHeaderBold($header);
444 if ($with_author) {
445 $author = $this->getAuthor($comment->getAuthorPHID());
446 } else {
447 $author = null;
450 if ($author) {
451 $byline = $author->getName();
453 if ($is_html) {
454 $byline = $this->renderHeaderBold($byline);
457 $header = pht('%s wrote in %s', $byline, $header);
460 if ($is_html) {
461 $link_href = $this->getInlineURI($comment);
462 if ($link_href) {
463 $link_style = array(
464 'float: right;',
465 'text-decoration: none;',
468 $link = phutil_tag(
469 'a',
470 array(
471 'style' => implode(' ', $link_style),
472 'href' => $link_href,
474 array(
475 pht('View Inline'),
477 // See PHI920. Add a space after the link so we render this into
478 // the document:
480 // View Inline filename.txt
482 // Otherwise, we render "Inlinefilename.txt" and double-clicking
483 // the file name selects the word "Inline" as well.
484 ' ',
486 } else {
487 $link = null;
490 $header = $this->renderHeaderBlock(array($link, $header));
493 return $header;
496 private function getInlineURI(DifferentialTransactionComment $comment) {
497 $changeset = $this->getChangeset($comment->getChangesetID());
498 if (!$changeset) {
499 return null;
502 $diff = $changeset->getDiff();
503 if (!$diff) {
504 return null;
507 $revision = $diff->getRevision();
508 if (!$revision) {
509 return null;
512 $link_href = '/'.$revision->getMonogram().'#inline-'.$comment->getID();
513 $link_href = PhabricatorEnv::getProductionURI($link_href);
515 return $link_href;