diff --git a/src/applications/differential/mail/DifferentialInlineCommentMailView.php b/src/applications/differential/mail/DifferentialInlineCommentMailView.php index 5fc13c34c5..554bb66a63 100644 --- a/src/applications/differential/mail/DifferentialInlineCommentMailView.php +++ b/src/applications/differential/mail/DifferentialInlineCommentMailView.php @@ -1,523 +1,525 @@ viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function setInlines($inlines) { $this->inlines = $inlines; return $this; } public function getInlines() { return $this->inlines; } public function buildMailSection() { $inlines = $this->getInlines(); $comments = mpull($inlines, 'getComment'); $comments = mpull($comments, null, 'getPHID'); $parents = $this->loadParents($comments); $all_comments = $comments + $parents; $this->changesets = $this->loadChangesets($all_comments); $this->authors = $this->loadAuthors($all_comments); $groups = $this->groupInlines($inlines); $hunk_parser = new DifferentialHunkParser(); $spacer_text = null; $spacer_html = phutil_tag('br'); $section = new PhabricatorMetaMTAMailSection(); $last_group_key = last_key($groups); foreach ($groups as $changeset_id => $group) { $changeset = $this->getChangeset($changeset_id); if (!$changeset) { continue; } $is_last_group = ($changeset_id == $last_group_key); $last_inline_key = last_key($group); foreach ($group as $inline_key => $inline) { $comment = $inline->getComment(); $parent_phid = $comment->getReplyToCommentPHID(); $is_last_inline = ($inline_key == $last_inline_key); $context_text = null; $context_html = null; if ($parent_phid) { $parent = idx($parents, $parent_phid); if ($parent) { $context_text = $this->renderInline($parent, false, true); $context_html = $this->renderInline($parent, true, true); } } else { $patch_text = $this->getPatch($hunk_parser, $comment, false); $context_text = $this->renderPatch($comment, $patch_text, false); $patch_html = $this->getPatch($hunk_parser, $comment, true); $context_html = $this->renderPatch($comment, $patch_html, true); } $render_text = $this->renderInline($comment, false, false); $render_html = $this->renderInline($comment, true, false); $section->addPlaintextFragment($context_text); $section->addPlaintextFragment($spacer_text); $section->addPlaintextFragment($render_text); $style = array( 'border: 1px solid #C7CCD9;', 'border-radius: 3px;', ); $html_fragment = phutil_tag( 'div', array( 'style' => implode(' ', $style), ), array( $context_html, $render_html, )); $section->addHTMLFragment($html_fragment); if (!$is_last_group || !$is_last_inline) { $section->addPlaintextFragment($spacer_text); $section->addHTMLFragment($spacer_html); } } } return $section; } private function loadChangesets(array $comments) { if (!$comments) { return array(); } $ids = array(); foreach ($comments as $comment) { $ids[] = $comment->getChangesetID(); } $changesets = id(new DifferentialChangesetQuery()) ->setViewer($this->getViewer()) ->withIDs($ids) ->needHunks(true) ->execute(); return mpull($changesets, null, 'getID'); } private function loadParents(array $comments) { $viewer = $this->getViewer(); $phids = array(); foreach ($comments as $comment) { $parent_phid = $comment->getReplyToCommentPHID(); if (!$parent_phid) { continue; } $phids[] = $parent_phid; } if (!$phids) { return array(); } $parents = id(new DifferentialDiffInlineCommentQuery()) ->setViewer($viewer) ->withPHIDs($phids) ->execute(); return mpull($parents, null, 'getPHID'); } private function loadAuthors(array $comments) { $viewer = $this->getViewer(); $phids = array(); foreach ($comments as $comment) { $author_phid = $comment->getAuthorPHID(); if (!$author_phid) { continue; } $phids[] = $author_phid; } if (!$phids) { return array(); } return $viewer->loadHandles($phids); } private function groupInlines(array $inlines) { return DifferentialTransactionComment::sortAndGroupInlines( $inlines, $this->changesets); } private function renderInline( DifferentialTransactionComment $comment, $is_html, $is_quote) { $changeset = $this->getChangeset($comment->getChangesetID()); if (!$changeset) { return null; } $content = $comment->getContent(); $content = $this->renderRemarkupContent($content, $is_html); if ($is_quote) { $header = $this->renderHeader($comment, $is_html, true); } else { $header = null; } if ($is_html) { $style = array( 'margin: 8px 0;', 'padding: 0 12px;', ); if ($is_quote) { $style[] = 'color: #74777D;'; } $content = phutil_tag( 'div', array( 'style' => implode(' ', $style), ), $content); } $parts = array( $header, "\n", $content, ); if (!$is_html) { $parts = implode('', $parts); $parts = trim($parts); } if ($is_quote) { if ($is_html) { $parts = $this->quoteHTML($parts); } else { $parts = $this->quoteText($parts); } } return $parts; } private function renderRemarkupContent($content, $is_html) { $viewer = $this->getViewer(); $production_uri = PhabricatorEnv::getProductionURI('/'); if ($is_html) { $mode = PhutilRemarkupEngine::MODE_HTML_MAIL; } else { $mode = PhutilRemarkupEngine::MODE_TEXT; } $attributes = array( 'style' => 'padding: 0; margin: 8px;', ); $engine = PhabricatorMarkupEngine::newMarkupEngine(array()) ->setConfig('viewer', $viewer) ->setConfig('uri.base', $production_uri) ->setConfig('default.p.attributes', $attributes) ->setMode($mode); try { return $engine->markupText($content); } catch (Exception $ex) { return $content; } } private function getChangeset($id) { return idx($this->changesets, $id); } private function getAuthor($phid) { if (isset($this->authors[$phid])) { return $this->authors[$phid]; } return null; } private function quoteText($block) { $block = phutil_split_lines($block); foreach ($block as $key => $line) { $block[$key] = '> '.$line; } return implode('', $block); } private function quoteHTML($block) { $styles = array( 'padding: 0;', 'background: #F7F7F7;', 'border-color: #e3e4e8;', 'border-style: solid;', 'border-width: 0 0 1px 0;', 'margin: 0;', ); $styles = implode(' ', $styles); return phutil_tag( 'div', array( 'style' => $styles, ), $block); } private function getPatch( DifferentialHunkParser $parser, DifferentialTransactionComment $comment, $is_html) { $changeset = $this->getChangeset($comment->getChangesetID()); $is_new = $comment->getIsNewFile(); $start = $comment->getLineNumber(); $length = $comment->getLineLength(); // By default, show one line of context around the target inline. $context = 1; // If the inline is at least 3 lines long, don't show any extra context. if ($length >= 2) { $context = 0; } // If the inline is more than 7 lines long, only show the first 7 lines. if ($length >= 6) { $length = 6; } if (!$is_html) { $hunks = $changeset->getHunks(); $patch = $parser->makeContextDiff( $hunks, $is_new, $start, $length, $context); $patch = phutil_split_lines($patch); // Remove the "@@ -x,y +u,v @@" line. array_shift($patch); return implode('', $patch); } $viewer = $this->getViewer(); $engine = new PhabricatorMarkupEngine(); if ($is_new) { $offset_mode = 'new'; } else { $offset_mode = 'old'; } $parser = id(new DifferentialChangesetParser()) ->setUser($viewer) ->setChangeset($changeset) ->setOffsetMode($offset_mode) ->setMarkupEngine($engine); $parser->setRenderer(new DifferentialChangesetOneUpMailRenderer()); return $parser->render( $start - $context, $length + 1 + (2 * $context), array()); } private function renderPatch( DifferentialTransactionComment $comment, $patch, $is_html) { if ($is_html) { $style = array( 'font: 11px/15px "Menlo", "Consolas", "Monaco", monospace;', + 'white-space: pre-wrap;', + 'clear: both;', 'padding: 4px 0;', 'margin: 0;', ); $style = implode(' ', $style); $patch = phutil_tag( - 'pre', + 'div', array( 'style' => $style, ), $patch); } $header = $this->renderHeader($comment, $is_html, false); $patch = array( $header, "\n", $patch, ); if (!$is_html) { $patch = implode('', $patch); $patch = $this->quoteText($patch); } else { $patch = $this->quoteHTML($patch); } return $patch; } private function renderHeader( DifferentialTransactionComment $comment, $is_html, $with_author) { $changeset = $this->getChangeset($comment->getChangesetID()); $path = $changeset->getFilename(); // Only show the filename. $path = basename($path); $start = $comment->getLineNumber(); $length = $comment->getLineLength(); if ($length) { $range = pht('%s-%s', $start, $start + $length); } else { $range = $start; } $header = "{$path}:{$range}"; if ($is_html) { $header = phutil_tag( 'span', array( 'style' => 'color: #4b4d51; font-weight: bold;', ), $header); } if ($with_author) { $author = $this->getAuthor($comment->getAuthorPHID()); } else { $author = null; } if ($author) { $byline = $author->getName(); if ($is_html) { $byline = phutil_tag( 'span', array( 'style' => 'color: #4b4d51; font-weight: bold;', ), $byline); } $header = pht('%s wrote in %s', $byline, $header); } if ($is_html) { $link_href = $this->getInlineURI($comment); if ($link_href) { $link_style = array( 'float: right;', 'text-decoration: none;', ); $link = phutil_tag( 'a', array( 'style' => implode(' ', $link_style), 'href' => $link_href, ), pht('View Inline')); } else { $link = null; } $style = array( 'color: #74777d;', 'background: #eff2f4;', 'padding: 6px 8px;', 'overflow: hidden;', ); $header = phutil_tag( 'div', array( 'style' => implode(' ', $style), ), array( $link, $header, )); } return $header; } private function getInlineURI(DifferentialTransactionComment $comment) { $changeset = $this->getChangeset($comment->getChangesetID()); if (!$changeset) { return null; } $diff = $changeset->getDiff(); if (!$diff) { return null; } $revision = $diff->getRevision(); if (!$revision) { return null; } $link_href = '/'.$revision->getMonogram().'#inline-'.$comment->getID(); $link_href = PhabricatorEnv::getProductionURI($link_href); return $link_href; } } diff --git a/src/applications/differential/render/DifferentialChangesetOneUpMailRenderer.php b/src/applications/differential/render/DifferentialChangesetOneUpMailRenderer.php index 4c34ebace5..042047b4a8 100644 --- a/src/applications/differential/render/DifferentialChangesetOneUpMailRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetOneUpMailRenderer.php @@ -1,100 +1,141 @@ buildPrimitives($range_start, $range_len); return $this->renderPrimitives($primitives, $rows); } protected function renderPrimitives(array $primitives, $rows) { $out = array(); + foreach ($primitives as $k => $p) { $type = $p['type']; switch ($type) { case 'old': case 'new': case 'old-file': case 'new-file': $is_old = ($type == 'old' || $type == 'old-file'); if ($is_old) { if ($p['htype']) { $style = 'background: #ffd0d0;'; } else { $style = null; } } else { if ($p['htype']) { $style = 'background: #d0ffd0;'; } else { $style = null; } } - $style = "padding: 0 8px; margin: 0 4px; {$style}"; - - $out[] = phutil_tag( - 'div', - array( - 'style' => $style, - ), - $p['render']); + $out[] = array( + 'style' => $style, + 'render' => $p['render'], + 'text' => (string)$p['render'], + ); break; default: break; } } + // Remove all leading and trailing empty lines, since these just look kind + // of weird in mail. + foreach ($out as $key => $line) { + if (!strlen(trim($line['text']))) { + unset($out[$key]); + } else { + break; + } + } + + $keys = array_reverse(array_keys($out)); + foreach ($keys as $key) { + $line = $out[$key]; + if (!strlen(trim($line['text']))) { + unset($out[$key]); + } else { + break; + } + } + + // If the user has commented on an empty line in the middle of a bunch of + // other empty lines, emit an explicit marker instead of just rendering + // nothing. + if (!$out) { + $out[] = array( + 'style' => 'color: #888888;', + 'render' => pht('(Empty.)'), + ); + } + + $render = array(); + foreach ($out as $line) { + $style = $line['style']; + $style = "padding: 0 8px; margin: 0 4px; {$style}"; + + $render[] = phutil_tag( + 'div', + array( + 'style' => $style, + ), + $line['render']); + } + $style_map = id(new PhabricatorDefaultSyntaxStyle()) ->getRemarkupStyleMap(); $styled_body = id(new PhutilPygmentizeParser()) ->setMap($style_map) - ->parse((string)hsprintf('%s', $out)); + ->parse((string)hsprintf('%s', $render)); return phutil_safe_html($styled_body); } }