Colin Watson has proposed merging lp:~cjwatson/launchpad/bmp-hide-comments into 
lp:launchpad with lp:~cjwatson/launchpad/bmp-remove-old-comment-threading as a 
prerequisite.

Commit message:
Add spam controls to code review comments.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1680746 in Launchpad itself: "No way to remove merge proposal comment 
spam"
  https://bugs.launchpad.net/launchpad/+bug/1680746

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/bmp-hide-comments/+merge/344225
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of 
lp:~cjwatson/launchpad/bmp-hide-comments into lp:launchpad.
=== modified file 'lib/lp/app/javascript/comment.js'
--- lib/lp/app/javascript/comment.js	2017-09-22 19:41:16 +0000
+++ lib/lp/app/javascript/comment.js	2018-04-25 12:03:26 +0000
@@ -72,6 +72,16 @@
         });
     },
 
+    /**
+     * Bind the UI to a new comment.
+     *
+     * @method bind_new_comment
+     */
+    bind_new_comment: function(comment_node) {
+        comment_node.delegate(
+            'click', this.toggle_hidden, 'a.mark-spam', this);
+    },
+
     destructor: function() {
         var comment_list_container = this.get('comment_list_container');
         comment_list_container.detachAll();
@@ -475,7 +485,7 @@
         var comment = Y.Node.create(message_html);
         conversation.appendChild(comment);
         this.reset_contents();
-        this.fire('CodeReviewComment.appended');
+        this.fire('CodeReviewComment.appended', comment);
         Y.lp.anim.green_flash({node: comment}).run();
     },
 

=== modified file 'lib/lp/app/javascript/tests/test_comment.html'
--- lib/lp/app/javascript/tests/test_comment.html	2012-10-26 09:54:28 +0000
+++ lib/lp/app/javascript/tests/test_comment.html	2018-04-25 12:03:26 +0000
@@ -1,6 +1,6 @@
 <!DOCTYPE html>
 <!--
-Copyright 2012 Canonical Ltd.  This software is licensed under the
+Copyright 2012-2018 Canonical Ltd.  This software is licensed under the
 GNU Affero General Public License version 3 (see the file LICENSE).
 -->
 
@@ -32,7 +32,9 @@
       <script type="text/javascript"
         src="../../../../../build/js/lp/app/client.js"></script>
       <script type="text/javascript"
-        src="../../../../../build/js/lp/app/client.js"></script>
+        src="../../../../../build/js/lp/app/anim/anim.js"></script>
+      <script type="text/javascript"
+        src="../../../../../build/js/lp/app/extras/extras.js"></script>
       <!-- The module under test. -->
       <script type="text/javascript" src="../comment.js"></script>
 

=== modified file 'lib/lp/app/javascript/tests/test_comment.js'
--- lib/lp/app/javascript/tests/test_comment.js	2017-09-22 19:41:16 +0000
+++ lib/lp/app/javascript/tests/test_comment.js	2018-04-25 12:03:26 +0000
@@ -1,4 +1,4 @@
-/* Copyright 2012 Canonical Ltd.  This software is licensed under the
+/* Copyright 2012-2018 Canonical Ltd.  This software is licensed under the
  * GNU Affero General Public License version 3 (see the file LICENSE). */
 
 YUI.add('lp.app.comment.test', function (Y) {
@@ -120,6 +120,48 @@
             comment.insert_comment_HTML('<span id="fake"></span>');
             Y.Assert.isTrue(reset_contents_called);
             Y.Assert.isObject(Y.one('#fake'));
+        },
+
+        test_insert_code_review_comment_html: function () {
+            // Test CodeReviewComment.insert_comment_HTML
+            window.LP = {
+                cache: {
+                    context: { self_link: '/mp/1/' }
+                }
+            };
+            Y.one('body').appendChild(
+                Y.Node.create('<div id="conversation"/>'));
+            var mock_client = new Y.lp.testing.helpers.LPClient();
+            mock_client.named_post.args = [];
+            mock_client.get.args = [];
+            var comment = new Y.lp.app.comment.CodeReviewComment({
+                animate: false,
+                lp_client: mock_client
+            });
+            var reset_contents_called = false;
+            comment.reset_contents = function () {
+                reset_contents_called = true;
+            };
+            var new_comment_node = null;
+            comment.on('CodeReviewComment.appended', function (comment_node) {
+                new_comment_node = comment_node;
+            });
+            var green_flash = Y.lp.anim.green_flash;
+            var green_flashed = false;
+            Y.lp.anim.green_flash = function () {
+                return {
+                    run: function () {
+                        green_flashed = true;
+                    }
+                };
+            };
+            comment.insert_comment_HTML('<span id="fake"></span>');
+            Y.Assert.isTrue(reset_contents_called);
+            Y.Assert.isObject(new_comment_node);
+            Y.Assert.isObject(Y.one('#fake'));
+            Y.Assert.areEqual(new_comment_node.get('id'), 'fake');
+            Y.Assert.isTrue(green_flashed);
+            Y.lp.anim.green_flash = green_flash;
         }
     }));
 

=== modified file 'lib/lp/code/browser/branchmergeproposal.py'
--- lib/lp/code/browser/branchmergeproposal.py	2018-04-25 12:03:26 +0000
+++ lib/lp/code/browser/branchmergeproposal.py	2018-04-25 12:03:26 +0000
@@ -123,6 +123,7 @@
 from lp.services.webapp.authorization import check_permission
 from lp.services.webapp.breadcrumb import Breadcrumb
 from lp.services.webapp.escaping import structured
+from lp.services.webapp.interfaces import ILaunchBag
 from lp.services.webapp.menu import NavigationMenu
 
 
@@ -436,9 +437,14 @@
         except ValueError:
             return None
         try:
-            return self.context.getComment(id)
+            comment = self.context.getComment(id)
         except WrongBranchMergeProposal:
             return None
+        user = getUtility(ILaunchBag).user
+        if comment.visible or comment.userCanSetCommentVisibility(user):
+            return comment
+        else:
+            return None
 
     @stepthrough("+preview-diff")
     def traverse_preview_diff(self, id):
@@ -591,6 +597,8 @@
         self.comment_date = None
         self.display_attachments = False
         self.index = None
+        self.visible = True
+        self.show_spam_controls = False
 
     def download(self, request):
         pass
@@ -661,6 +669,8 @@
         source = merge_proposal.merge_source
         if IBranch.providedBy(source):
             source = DecoratedBranch(source)
+        user = getUtility(ILaunchBag).user
+        strip_invisible = not merge_proposal.userCanSetCommentVisibility(user)
         comments = []
         if (getFeatureFlag('code.incremental_diffs.enabled') and
                 merge_proposal.source_branch is not None):
@@ -688,6 +698,8 @@
                 for comment in merge_proposal.all_comments)
             merge_proposal = merge_proposal.supersedes
         comments = sorted(comments, key=operator.attrgetter('date'))
+        if strip_invisible:
+            comments = [c for c in comments if c.visible or c.author == user]
         self._populate_previewdiffs(comments)
         return CodeReviewConversation(comments)
 

=== modified file 'lib/lp/code/browser/codereviewcomment.py'
--- lib/lp/code/browser/codereviewcomment.py	2015-07-09 20:06:17 +0000
+++ lib/lp/code/browser/codereviewcomment.py	2018-04-25 12:03:26 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -50,6 +50,7 @@
     LaunchpadView,
     Link,
     )
+from lp.services.webapp.interfaces import ILaunchBag
 
 
 class ICodeReviewDisplayComment(IComment, ICodeReviewComment):
@@ -85,10 +86,11 @@
 
     @property
     def extra_css_class(self):
+        css_classes = (
+            super(CodeReviewDisplayComment, self).extra_css_class.split())
         if self.from_superseded:
-            return 'from-superseded'
-        else:
-            return ''
+            css_classes.append('from-superseded')
+        return ' '.join(css_classes)
 
     @cachedproperty
     def previewdiff_id(self):
@@ -121,6 +123,11 @@
     def download_url(self):
         return canonical_url(self.comment, view_name='+download')
 
+    @cachedproperty
+    def show_spam_controls(self):
+        user = getUtility(ILaunchBag).user
+        return self.comment.userCanSetCommentVisibility(user)
+
 
 def get_message(display_comment):
     """Adapt an ICodeReviwComment to an IMessage."""

=== modified file 'lib/lp/code/browser/tests/test_branchmergeproposal.py'
--- lib/lp/code/browser/tests/test_branchmergeproposal.py	2018-03-16 21:20:00 +0000
+++ lib/lp/code/browser/tests/test_branchmergeproposal.py	2018-04-25 12:03:26 +0000
@@ -26,6 +26,10 @@
     Tag,
     Within,
     )
+from testscenarios import (
+    load_tests_apply_scenarios,
+    WithScenarios,
+    )
 from testtools.matchers import (
     ContainsDict,
     DocTestMatches,
@@ -77,6 +81,10 @@
     make_merge_proposal_without_reviewers,
     )
 from lp.code.xmlrpc.git import GitAPI
+from lp.coop.answersbugs.visibility import (
+    TestHideMessageControlMixin,
+    TestMessageVisibilityMixin,
+    )
 from lp.registry.enums import (
     PersonVisibility,
     TeamMembershipPolicy,
@@ -1899,6 +1907,64 @@
             git_proposal.merge_source.commit_sha1, view.source_revid)
 
 
+class TestCodeReviewCommentVisibility(
+        WithScenarios, BrowserTestCase, TestMessageVisibilityMixin):
+
+    layer = DatabaseFunctionalLayer
+
+    scenarios = [
+        ('bzr', {'git': False}),
+        ('git', {'git': True}),
+        ]
+
+    def makeHiddenMessage(self, comment_owner=None):
+        """See `TestMessageVisibilityMixin`."""
+        if self.git:
+            self.useFixture(GitHostingFixture())
+        comment = self.factory.makeCodeReviewComment(
+            sender=comment_owner, body=self.comment_text, git=self.git)
+        with admin_logged_in():
+            comment.message.visible = False
+        return comment.branch_merge_proposal
+
+    def getView(self, context, user=None, no_login=False):
+        """See `TestMessageVisibilityMixin`."""
+        return self.getViewBrowser(
+            context=context, user=user, no_login=no_login)
+
+
+class TestCodeReviewCommentHideControls(
+        WithScenarios, BrowserTestCase, TestHideMessageControlMixin):
+
+    layer = DatabaseFunctionalLayer
+
+    scenarios = [
+        ('bzr', {'git': False}),
+        ('git', {'git': True}),
+        ]
+
+    def getContext(self, comment_owner=None):
+        """See `TestHideMessageControlMixin`."""
+        if self.git:
+            self.useFixture(GitHostingFixture())
+        comment = self.factory.makeCodeReviewComment(
+            sender=comment_owner, git=self.git)
+        self.control_text = 'mark-spam-%d' % comment.id
+        return comment.branch_merge_proposal
+
+    def getView(self, context, user=None, no_login=False):
+        """See `TestHideMessageControlMixin`."""
+        return self.getViewBrowser(
+            context=context, user=user, no_login=no_login)
+
+    def test_comment_owner_sees_hide_control(self):
+        user = self.factory.makePerson()
+        context = self.getContext(comment_owner=user)
+        view = self.getView(context=context, user=user)
+        hide_link = find_tag_by_id(view.contents, self.control_text)
+        self.assertIsNot(None, hide_link)
+
+
 class TestCommentAttachmentRendering(TestCaseWithFactory):
     """Test diff attachments are rendered correctly."""
 
@@ -2308,3 +2374,6 @@
 
     def _makeBranchMergeProposal(self, **kwargs):
         return self.factory.makeBranchMergeProposalForGit(**kwargs)
+
+
+load_tests = load_tests_apply_scenarios

=== modified file 'lib/lp/code/browser/tests/test_codereviewcomment.py'
--- lib/lp/code/browser/tests/test_codereviewcomment.py	2017-10-04 01:16:22 +0000
+++ lib/lp/code/browser/tests/test_codereviewcomment.py	2018-04-25 12:03:26 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Unit tests for CodeReviewComments."""
@@ -24,9 +24,12 @@
 from lp.code.interfaces.codereviewinlinecomment import (
     ICodeReviewInlineCommentSet,
     )
+from lp.services.propertycache import clear_property_cache
 from lp.services.webapp import canonical_url
 from lp.testing import (
+    admin_logged_in,
     BrowserTestCase,
+    celebrity_logged_in,
     person_logged_in,
     StormStatementRecorder,
     TestCaseWithFactory,
@@ -52,6 +55,34 @@
 
         verifyObject(ICodeReviewDisplayComment, display_comment)
 
+    def test_extra_css_classes_visibility(self):
+        author = self.factory.makePerson()
+        comment = self.factory.makeCodeReviewComment(sender=author)
+        display_comment = CodeReviewDisplayComment(comment)
+        self.assertEqual('', display_comment.extra_css_class)
+        with person_logged_in(author):
+            display_comment.message.setVisible(False)
+        self.assertEqual(
+            'adminHiddenComment', display_comment.extra_css_class)
+
+    def test_show_spam_controls_permissions(self):
+        # Admins, registry experts, and the author of the comment itself can
+        # hide comments, but other people can't.
+        author = self.factory.makePerson()
+        comment = self.factory.makeCodeReviewComment(sender=author)
+        display_comment = CodeReviewDisplayComment(comment)
+        with person_logged_in(author):
+            self.assertTrue(display_comment.show_spam_controls)
+        clear_property_cache(display_comment)
+        with person_logged_in(self.factory.makePerson()):
+            self.assertFalse(display_comment.show_spam_controls)
+        clear_property_cache(display_comment)
+        with celebrity_logged_in('registry_experts'):
+            self.assertTrue(display_comment.show_spam_controls)
+        clear_property_cache(display_comment)
+        with admin_logged_in():
+            self.assertTrue(display_comment.show_spam_controls)
+
 
 class TestCodeReviewCommentInlineComments(TestCaseWithFactory):
     """Test `CodeReviewDisplayComment` integration with inline-comments."""

=== modified file 'lib/lp/code/interfaces/branchmergeproposal.py'
--- lib/lp/code/interfaces/branchmergeproposal.py	2018-03-16 21:20:00 +0000
+++ lib/lp/code/interfaces/branchmergeproposal.py	2018-04-25 12:03:26 +0000
@@ -376,7 +376,41 @@
     @operation_returns_entry(Interface)
     @export_read_operation()
     def getComment(id):
-        """Return the CodeReviewComment with the specified ID."""
+        """Return the CodeReviewComment with the specified ID.
+
+        :raises WrongBranchMergeProposal: if the comment with this ID is not
+            on this merge proposal.
+        """
+
+    def userCanSetCommentVisibility(user):
+        """Can `user` set code review comment visibility?
+
+        Admins and registry experts can set the visibility of any code
+        review comment.
+
+        Comment authors can also set the visibility of their own comments,
+        but that is not checked here; this method determines whether
+        arbitrary users can set the visibility of comments they did not make
+        themselves.
+        """
+
+    @operation_parameters(
+        # Really comment_id, but this lets us use common JavaScript code.
+        comment_number=Int(title=_('The comment ID.'), required=True),
+        visible=Bool(title=_('Show this comment?'), required=True))
+    @call_with(user=REQUEST_USER)
+    @export_write_operation()
+    @operation_for_version('devel')
+    def setCommentVisibility(user, comment_number, visible):
+        """Set the visible attribute on a code review comment.
+
+        This is restricted to Launchpad admins, registry experts, and
+        comment authors, and will return a HTTP Error 401: Unauthorized
+        error for non-admin callers.
+
+        :raises WrongBranchMergeProposal: if the comment with this ID is not
+            on this merge proposal.
+        """
 
     @call_with(user=REQUEST_USER)
     # Really IBugTask.

=== modified file 'lib/lp/code/interfaces/codereviewcomment.py'
--- lib/lp/code/interfaces/codereviewcomment.py	2013-01-07 02:40:55 +0000
+++ lib/lp/code/interfaces/codereviewcomment.py	2018-04-25 12:03:26 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """CodeReviewComment interfaces."""
@@ -16,6 +16,7 @@
 from lazr.restful.fields import Reference
 from zope.interface import Interface
 from zope.schema import (
+    Bool,
     Choice,
     Datetime,
     Int,
@@ -87,6 +88,16 @@
             title=_('The message as quoted in email.'),
             readonly=True))
 
+    visible = Bool(title=_('Whether this comment is visible.'))
+
+    def userCanSetCommentVisibility(user):
+        """Can `user` set the visibility of this comment?
+
+        Admins and registry experts can set the visibility of any code
+        review comment.  Comment authors can set the visibility of their own
+        comments.
+        """
+
 
 class ICodeReviewCommentDeletion(Interface):
     """This interface provides deletion of CodeReviewComments.

=== modified file 'lib/lp/code/model/branchmergeproposal.py'
--- lib/lp/code/model/branchmergeproposal.py	2017-11-24 17:22:34 +0000
+++ lib/lp/code/model/branchmergeproposal.py	2018-04-25 12:03:26 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Database class for branch merge proposals."""
@@ -43,6 +43,7 @@
 from zope.error.interfaces import IErrorReportingUtility
 from zope.event import notify
 from zope.interface import implementer
+from zope.security.interfaces import Unauthorized
 
 from lp.app.enums import PRIVATE_INFORMATION_TYPES
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
@@ -97,6 +98,7 @@
     validate_public_person,
     )
 from lp.registry.interfaces.product import IProduct
+from lp.registry.interfaces.role import IPersonRoles
 from lp.registry.model.person import Person
 from lp.services.config import config
 from lp.services.database.bulk import (
@@ -560,14 +562,30 @@
         return CodeReviewComment.selectBy(branch_merge_proposal=self.id)
 
     def getComment(self, id):
-        """See `IBranchMergeProposal`.
-
-        This function can raise WrongBranchMergeProposal."""
+        """See `IBranchMergeProposal`."""
         comment = CodeReviewComment.get(id)
         if comment.branch_merge_proposal != self:
             raise WrongBranchMergeProposal
         return comment
 
+    def userCanSetCommentVisibility(self, user):
+        """See `IBranchMergeProposal`."""
+        if user is None:
+            return False
+        roles = IPersonRoles(user)
+        return roles.in_admin or roles.in_registry_experts
+
+    def setCommentVisibility(self, user, comment_number, visible):
+        """See `IBranchMergeProposal`."""
+        comment = CodeReviewComment.get(comment_number)
+        if comment.branch_merge_proposal != self:
+            raise WrongBranchMergeProposal
+        if not comment.userCanSetCommentVisibility(user):
+            raise Unauthorized(
+                "User %s cannot hide or show code review comments." %
+                (user.name if user is not None else "<anonymous>"))
+        comment.message.setVisible(visible)
+
     def getVoteReference(self, id):
         """See `IBranchMergeProposal`.
 

=== modified file 'lib/lp/code/model/codereviewcomment.py'
--- lib/lp/code/model/codereviewcomment.py	2015-10-19 10:56:16 +0000
+++ lib/lp/code/model/codereviewcomment.py	2018-04-25 12:03:26 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """The database implementation class for CodeReviewComment."""
@@ -124,3 +124,13 @@
         if self.message.raw is None:
             return None
         return signed_message_from_string(self.message.raw.read())
+
+    @property
+    def visible(self):
+        return self.message.visible
+
+    def userCanSetCommentVisibility(self, user):
+        """See `ICodeReviewComment`."""
+        return (
+            self.branch_merge_proposal.userCanSetCommentVisibility(user) or
+            (user is not None and user.inTeam(self.author)))

=== modified file 'lib/lp/code/model/tests/test_branchmergeproposal.py'
--- lib/lp/code/model/tests/test_branchmergeproposal.py	2018-01-02 16:10:26 +0000
+++ lib/lp/code/model/tests/test_branchmergeproposal.py	2018-04-25 12:03:26 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for BranchMergeProposals."""
@@ -32,6 +32,7 @@
     )
 import transaction
 from zope.component import getUtility
+from zope.security.interfaces import Unauthorized
 from zope.security.proxy import removeSecurityProxy
 
 from lp.app.enums import InformationType
@@ -607,6 +608,57 @@
                           self.merge_proposal2.getComment, self.comment.id)
 
 
+class TestMergeProposalSetCommentVisibility(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_anonymous(self):
+        comment = self.factory.makeCodeReviewComment()
+        self.assertRaisesWithContent(
+            Unauthorized,
+            "User <anonymous> cannot hide or show code review comments.",
+            comment.branch_merge_proposal.setCommentVisibility,
+            user=None, comment_number=comment.id, visible=False)
+
+    def test_random_user(self):
+        comment = self.factory.makeCodeReviewComment()
+        person = self.factory.makePerson()
+        self.assertRaisesWithContent(
+            Unauthorized,
+            "User %s cannot hide or show code review comments." % person.name,
+            comment.branch_merge_proposal.setCommentVisibility,
+            user=person, comment_number=comment.id, visible=False)
+
+    def test_comment_author(self):
+        comment = self.factory.makeCodeReviewComment()
+        another_comment = self.factory.makeCodeReviewComment(
+            merge_proposal=comment.branch_merge_proposal)
+        comment.branch_merge_proposal.setCommentVisibility(
+            user=comment.author, comment_number=comment.id, visible=False)
+        self.assertFalse(comment.visible)
+        self.assertTrue(another_comment.visible)
+
+    def test_registry_expert(self):
+        comment = self.factory.makeCodeReviewComment()
+        another_comment = self.factory.makeCodeReviewComment(
+            merge_proposal=comment.branch_merge_proposal)
+        comment.branch_merge_proposal.setCommentVisibility(
+            user=self.factory.makeRegistryExpert(), comment_number=comment.id,
+            visible=False)
+        self.assertFalse(comment.visible)
+        self.assertTrue(another_comment.visible)
+
+    def test_admin(self):
+        comment = self.factory.makeCodeReviewComment()
+        another_comment = self.factory.makeCodeReviewComment(
+            merge_proposal=comment.branch_merge_proposal)
+        comment.branch_merge_proposal.setCommentVisibility(
+            user=self.factory.makeAdministrator(), comment_number=comment.id,
+            visible=False)
+        self.assertFalse(comment.visible)
+        self.assertTrue(another_comment.visible)
+
+
 class TestMergeProposalGetVoteReference(TestCaseWithFactory):
     """Tester for `BranchMergeProposal.getComment`."""
 

=== modified file 'lib/lp/code/templates/branchmergeproposal-index.pt'
--- lib/lp/code/templates/branchmergeproposal-index.pt	2016-05-19 01:04:56 +0000
+++ lib/lp/code/templates/branchmergeproposal-index.pt	2018-04-25 12:03:26 +0000
@@ -218,18 +218,25 @@
 
     Y.on('load', function() {
         var logged_in = LP.links['me'] !== undefined;
-        var ic = Y.lp.code.branchmergeproposal.inlinecomments;
-        var diffnav = new ic.DiffNav({srcNode: Y.one('#review-diff')});
+        var comment = null;
         if (logged_in) {
-            var comment = new Y.lp.app.comment.CodeReviewComment();
+            comment = new Y.lp.app.comment.CodeReviewComment();
             comment.render();
-            comment.on('CodeReviewComment.appended', function () {
-                diffnav.update_on_new_comment();
-            });
-            Y.lp.code.branchmergeproposal.status.connect_status(conf);
-        }
-        Y.lp.code.branchmergeproposal.reviewcomment.connect_links();
-        diffnav.render();
+        }
+
+        var review_diff_node = Y.one('#review-diff');
+        if (review_diff_node !== null) {
+            var ic = Y.lp.code.branchmergeproposal.inlinecomments;
+            var diffnav = new ic.DiffNav({srcNode: review_diff_node});
+            if (comment !== null) {
+                comment.on('CodeReviewComment.appended', function () {
+                    diffnav.update_on_new_comment();
+                });
+                Y.lp.code.branchmergeproposal.status.connect_status(conf);
+            }
+            Y.lp.code.branchmergeproposal.reviewcomment.connect_links();
+            diffnav.render();
+        }
 
         if (Y.Lang.isValue(LP.cache.merge_proposal_event_key)) {
             var upt = Y.lp.code.branchmergeproposal.updater;
@@ -247,6 +254,15 @@
                 diffnav.render();
             });
         }
+
+        LP.cache.comment_context = LP.cache.context;
+        var cl = new Y.lp.app.comment.CommentList({
+            comment_list_container: Y.one('#conversation')
+        });
+        if (comment !== null) {
+            comment.on('CodeReviewComment.appended', cl.bind_new_comment);
+        }
+        cl.render();
     }, window);
 
     Y.on('domready', function() {

=== modified file 'lib/lp/registry/browser/distroseriesdifference.py'
--- lib/lp/registry/browser/distroseriesdifference.py	2015-07-09 12:18:51 +0000
+++ lib/lp/registry/browser/distroseriesdifference.py	2018-04-25 12:03:26 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010-2012 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Browser views for DistroSeriesDifferences."""
@@ -255,6 +255,8 @@
 
     download_url = None
 
+    visible = True
+
     def __init__(self, comment):
         """Setup the attributes required by `IComment`."""
         super(DistroSeriesDifferenceDisplayComment, self).__init__(None)

=== modified file 'lib/lp/services/comments/browser/messagecomment.py'
--- lib/lp/services/comments/browser/messagecomment.py	2012-01-27 18:56:36 +0000
+++ lib/lp/services/comments/browser/messagecomment.py	2018-04-25 12:03:26 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -22,6 +22,15 @@
         self.comment_limit = comment_limit
 
     @property
+    def extra_css_class(self):
+        if not self.visible:
+            # If a comment that isn't visible is being rendered, it's being
+            # rendered for an admin or registry_expert.
+            return 'adminHiddenComment'
+        else:
+            return ''
+
+    @property
     def display_attachments(self):
         return []
 
@@ -64,3 +73,5 @@
         # less than 3 (which can happen in a testcase) and it makes
         # counting the strings harder.
         return "%s..." % self.body_text[:self.comment_limit]
+
+    show_spam_controls = False

=== modified file 'lib/lp/services/comments/interfaces/conversation.py'
--- lib/lp/services/comments/interfaces/conversation.py	2012-02-01 15:26:32 +0000
+++ lib/lp/services/comments/interfaces/conversation.py	2018-04-25 12:03:26 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Interfaces to do with conversations on Launchpad entities."""
@@ -73,6 +73,11 @@
         description=_("Should attachments be displayed for this comment."),
         readonly=True)
 
+    visible = Bool(title=_("Whether this comment is visible."))
+
+    show_spam_controls = Bool(
+        title=_("Whether to show spam controls for this comment."))
+
 
 class IConversation(Interface):
     """A conversation has a number of comments."""

=== modified file 'lib/lp/services/comments/templates/comment.pt'
--- lib/lp/services/comments/templates/comment.pt	2014-05-29 07:27:25 +0000
+++ lib/lp/services/comments/templates/comment.pt	2018-04-25 12:03:26 +0000
@@ -25,6 +25,12 @@
     <div class="boardCommentFooter"
          tal:define="link context/menu:context/reply | nothing"
          tal:condition="link/enabled | nothing">
+        <a tal:condition="context/show_spam_controls"
+           tal:attributes="id string:mark-spam-${context/index};"
+           class="js-action mark-spam" href="#">
+          <tal:not-spam condition="not: context/visible">Unhide</tal:not-spam>
+          <tal:spam condition="context/visible">Hide</tal:spam>
+        </a>
         <a itemprop="replyToUrl"
              tal:attributes="href link/fmt:url"
              tal:content="link/escapedtext">

_______________________________________________
Mailing list: https://launchpad.net/~launchpad-reviewers
Post to     : launchpad-reviewers@lists.launchpad.net
Unsubscribe : https://launchpad.net/~launchpad-reviewers
More help   : https://help.launchpad.net/ListHelp

Reply via email to