Commits

Andreas Knecht  committed 25e40f2

Fixed couple of paging bugs. Implemented paging for comments for issue.

  • Participants
  • Parent commits 9dcad2e

Comments (0)

Files changed (11)

File src/main/dashcode/JIRA-web-client.dcproj/andreask.wdgtuser

 		<string>-load</string>
 		<key>mobile/main.js:245</key>
 		<string>-viewIssue</string>
+		<key>mobile/main.js:251</key>
+		<string>-viewIssue</string>
 		<key>mobile/main.js:267</key>
 		<string>-loadIssueComments</string>
-		<key>mobile/main.js:275</key>
-		<string>-loadIssueComments</string>
 		<key>mobile/main.js:28</key>
 		<string>-load</string>
-		<key>mobile/main.js:282</key>
+		<key>mobile/main.js:301</key>
 		<string>-loadIssueComments</string>
-		<key>mobile/main.js:302</key>
+		<key>mobile/main.js:304</key>
 		<string>-loadIssueComments</string>
-		<key>mobile/main.js:303</key>
+		<key>mobile/main.js:308</key>
 		<string>-loadIssueComments</string>
-		<key>mobile/main.js:306</key>
+		<key>mobile/main.js:309</key>
 		<string>-loadIssueComments</string>
-		<key>mobile/main.js:313</key>
-		<string>-loadIssueComments</string>
-		<key>mobile/main.js:320</key>
-		<string>-loadIssueComments</string>
-		<key>mobile/main.js:321</key>
-		<string>-loadIssueComments</string>
-		<key>mobile/main.js:335</key>
+		<key>mobile/main.js:360</key>
 		<string>-runJQL</string>
-		<key>mobile/main.js:352</key>
+		<key>mobile/main.js:377</key>
 		<string>-loadJQLResults</string>
-		<key>mobile/main.js:353</key>
+		<key>mobile/main.js:378</key>
 		<string>-loadJQLResults</string>
-		<key>mobile/main.js:355</key>
+		<key>mobile/main.js:380</key>
 		<string>-loadJQLResults</string>
-		<key>mobile/main.js:400</key>
+		<key>mobile/main.js:425</key>
 		<string>-loadJQLResults</string>
-		<key>mobile/main.js:402</key>
+		<key>mobile/main.js:427</key>
 		<string>-loadJQLResults</string>
-		<key>mobile/main.js:405</key>
+		<key>mobile/main.js:430</key>
 		<string>-loadJQLResults</string>
-		<key>mobile/main.js:410</key>
+		<key>mobile/main.js:435</key>
 		<string>-</string>
-		<key>mobile/main.js:412</key>
+		<key>mobile/main.js:437</key>
 		<string>-</string>
 		<key>mobile/main.js:68</key>
 		<string>-itemClicked</string>
 				<integer>0</integer>
 				<integer>0</integer>
 				<integer>1</integer>
-				<integer>1</integer>
+				<integer>2</integer>
+				<integer>0</integer>
 			</array>
 			<array>
 				<integer>0</integer>
 				<integer>0</integer>
 				<integer>1</integer>
+				<integer>2</integer>
+				<integer>0</integer>
+				<integer>0</integer>
+			</array>
+			<array>
+				<integer>0</integer>
+				<integer>0</integer>
+				<integer>1</integer>
+				<integer>2</integer>
 				<integer>1</integer>
 				<integer>0</integer>
 			</array>
 				<integer>0</integer>
 				<integer>0</integer>
 				<integer>1</integer>
-				<integer>1</integer>
-				<integer>0</integer>
-				<integer>0</integer>
-			</array>
-			<array>
-				<integer>0</integer>
-				<integer>0</integer>
-				<integer>1</integer>
-				<integer>2</integer>
-			</array>
-			<array>
-				<integer>0</integer>
-				<integer>0</integer>
-				<integer>1</integer>
-				<integer>2</integer>
-				<integer>0</integer>
-			</array>
-			<array>
-				<integer>0</integer>
-				<integer>0</integer>
-				<integer>1</integer>
-				<integer>2</integer>
-				<integer>0</integer>
-				<integer>0</integer>
-			</array>
-			<array>
-				<integer>0</integer>
-				<integer>0</integer>
-				<integer>1</integer>
 				<integer>6</integer>
 			</array>
 			<array>
 				<integer>0</integer>
 				<integer>1</integer>
 				<integer>6</integer>
-				<integer>1</integer>
-			</array>
-			<array>
-				<integer>0</integer>
-				<integer>0</integer>
-				<integer>1</integer>
-				<integer>6</integer>
-				<integer>3</integer>
-			</array>
-			<array>
-				<integer>0</integer>
-				<integer>0</integer>
-				<integer>1</integer>
-				<integer>6</integer>
-				<integer>2</integer>
-			</array>
-			<array>
-				<integer>0</integer>
-				<integer>0</integer>
-				<integer>1</integer>
-				<integer>6</integer>
 				<integer>0</integer>
 			</array>
 			<array>
 				<integer>0</integer>
 				<integer>0</integer>
 				<integer>1</integer>
+				<integer>6</integer>
+				<integer>1</integer>
+			</array>
+			<array>
+				<integer>0</integer>
+				<integer>0</integer>
+				<integer>1</integer>
+				<integer>6</integer>
+				<integer>2</integer>
+			</array>
+			<array>
+				<integer>0</integer>
+				<integer>0</integer>
+				<integer>1</integer>
+				<integer>6</integer>
+				<integer>2</integer>
+				<integer>2</integer>
+			</array>
+			<array>
+				<integer>0</integer>
+				<integer>0</integer>
+				<integer>1</integer>
+				<integer>6</integer>
+				<integer>2</integer>
+				<integer>2</integer>
+				<integer>0</integer>
+			</array>
+			<array>
+				<integer>0</integer>
+				<integer>0</integer>
+				<integer>1</integer>
+				<integer>6</integer>
+				<integer>3</integer>
+			</array>
+			<array>
+				<integer>0</integer>
+				<integer>0</integer>
+				<integer>1</integer>
+				<integer>1</integer>
+			</array>
+			<array>
+				<integer>0</integer>
+				<integer>0</integer>
+				<integer>1</integer>
+				<integer>1</integer>
+				<integer>0</integer>
+			</array>
+			<array>
+				<integer>0</integer>
+				<integer>0</integer>
+				<integer>1</integer>
+				<integer>1</integer>
+				<integer>0</integer>
+				<integer>0</integer>
+			</array>
+			<array>
+				<integer>0</integer>
+				<integer>0</integer>
+				<integer>1</integer>
+				<integer>2</integer>
+			</array>
+			<array>
+				<integer>0</integer>
+				<integer>0</integer>
+				<integer>1</integer>
 				<integer>3</integer>
 			</array>
 			<array>
 				<integer>0</integer>
 				<integer>0</integer>
 				<integer>1</integer>
-				<integer>6</integer>
-				<integer>2</integer>
-				<integer>2</integer>
-			</array>
-			<array>
-				<integer>0</integer>
-				<integer>0</integer>
-				<integer>1</integer>
-				<integer>6</integer>
-				<integer>2</integer>
-				<integer>2</integer>
-				<integer>0</integer>
-			</array>
-			<array>
-				<integer>0</integer>
-				<integer>0</integer>
-				<integer>1</integer>
 				<integer>2</integer>
 				<integer>1</integer>
 			</array>
-			<array>
-				<integer>0</integer>
-				<integer>0</integer>
-				<integer>1</integer>
-				<integer>2</integer>
-				<integer>1</integer>
-				<integer>0</integer>
-			</array>
-			<array>
-				<integer>0</integer>
-				<integer>0</integer>
-				<integer>1</integer>
-				<integer>0</integer>
-			</array>
-			<array>
-				<integer>0</integer>
-				<integer>0</integer>
-				<integer>1</integer>
-				<integer>0</integer>
-				<integer>0</integer>
-			</array>
 		</array>
 		<key>Objects view last selections</key>
 		<array>
 				<integer>0</integer>
 				<integer>0</integer>
 				<integer>1</integer>
+				<integer>6</integer>
+				<integer>2</integer>
+				<integer>2</integer>
 				<integer>0</integer>
 			</array>
 		</array>
 			<string>view2</string>
 			<string>listLevel</string>
 			<string>view1</string>
+			<string>view_issue</string>
 			<string>filtersView</string>
-			<string>menu</string>
 		</array>
 	</dict>
 	<key>NavigatorSplitViewGeometry</key>

File src/main/dashcode/JIRA-web-client.dcproj/project/index.html

                 <div id="issue_comments" apple-part="com.apple.Dashcode.part.box">
                     <div id="text4" apple-part="com.apple.Dashcode.part.webtext" class="apple-text apple-no-children" apple-default-image-visibility="hidden" apple-text-overflow="ellipsis" apple-style="part-height-dependent: true;part-width-dependent: true;"></div>
                     <div id="issue_comments_container"></div>
+                    <div id="comment_footer">
+                        <div id="older_comments_button" apple-part="com.apple.Dashcode.part.pushbutton" class="apple-no-children" apple-text-overflow="ellipsis" apple-style="image-theme: 0; image-shape: 3; image-button-type: 0; image-opacity: 1.00; image-top-color: 0.290196091,0.423529416,0.607843161,1; image-bottom-color: 1,1,1,1; image-radius: 5,5,5,5; image-border: 1; image-border-color: 0,0,0,0.2; image-border-width: 1; shine-on: 1; shine-strength: 0.17; shine-alpha-level: 0.04; shine-height: 0.50; glass-arc-height: 0.00; embossed-on: 1; embossed-depth: 1; embossed-shadow: 0.68; embossed-highlight: 0.3; shadow-include-shadow: 0;"></div>
+                    </div>
                     <div id="add_comment_form" apple-part="com.apple.Dashcode.part.box">
                         <div id="topRectangleShape1" apple-part="com.apple.Dashcode.part.toprectanglewebshape" apple-style="image-theme: 1; image-shape: 9; image-button-type: 3; image-opacity: 1.00; image-top-color: 0.793478261,0.793478261,0.793478261,1; image-bottom-color: 1,1,1,1; image-radius: 0,0,-1,-1; image-border: 0; image-border-color: 0.5294,0.5294,0.5294,1; image-border-width: 1; shine-strength: 0.90; shine-alpha-level: 0.67; shine-height: 0.50; glass-arc-height: 0.06; shadow-include-shadow: 0;" apple-group="topRectangleShape1">
                             <div id="text5" apple-part="com.apple.Dashcode.part.webtext" class="apple-text apple-no-children" apple-default-image-visibility="hidden" apple-text-overflow="ellipsis" apple-style="part-height-dependent: true;part-width-dependent: true;"></div>

File src/main/dashcode/JIRA-web-client.dcproj/project/mobile/Parts/setup.js

     "label1": { "text": "Item", "view": "DC.Text" },
     "login_button": { "initialHeight": 30, "initialWidth": 59, "leftImageWidth": 5, "onclick": "doLogin", "rightImageWidth": 5, "text": "Login", "view": "DC.PushButton" },
     "login_submit": { "initialHeight": 30, "initialWidth": 73, "leftImageWidth": 15, "onclick": "doLoginSubmit", "rightImageWidth": 15, "text": "Login", "view": "DC.PushButton" },
+    "older_comments_button": { "initialHeight": 20, "initialWidth": 181, "leftImageWidth": 5, "rightImageWidth": 5, "text": "Show older comments (123)", "view": "DC.PushButton" },
     "priority": { "view": "DC.ImageLayout" },
     "rowTitle": { "text": "Item", "view": "DC.Text" },
     "stackLayout": { "subviewsTransitions": [{ "direction": "right-left", "duration": "", "timing": "ease-in-out", "type": "push" }, { "direction": "right-left", "duration": "", "timing": "ease-in-out", "type": "push" }, { "direction": "right-left", "duration": "", "timing": "ease-in-out", "type": "push" }, { "direction": "right-left", "duration": "", "timing": "ease-in-out", "type": "push" }, { "direction": "right-left", "duration": "", "timing": "ease-in-out", "type": "push" }, { "direction": "right-left", "duration": "", "timing": "ease-in-out", "type": "push" }, { "direction": "right-left", "duration": "", "timing": "ease-in-out", "type": "push" }], "view": "DC.StackLayout" },
     "text1": { "text": "Password", "view": "DC.Text" },
     "text2": { "text": "Enter an issue key", "view": "DC.Text" },
     "text3": { "text": "Enter a JQL query", "view": "DC.Text" },
-    "text4": { "text": "Comments", "view": "DC.Text" },
+    "text4": { "text": "Recent Comments", "view": "DC.Text" },
     "text5": { "text": "Add comment", "view": "DC.Text" },
     "unauthMenu": { "allowsEmptySelection": true, "dataArray": [["Browse Issue", "browse_issue"], ["Find Issues", "find_issues"]], "labelElementId": "label1", "listStyle": "List.EDGE_TO_EDGE", "sampleRows": 3, "selectionEnabled": true, "view": "DC.List" }
 };
 
 
 
+
+
+

File src/main/dashcode/JIRA-web-client.dcproj/project/mobile/main.css

     font-weight: bold;
     font-size: 12px;
     color: rgb(255, 255, 255);
-    text-shadow: rgba(0, 0, 0, 0.0195312) 0px -1px 0px;
+    text-shadow: rgba(0, 0, 0, 0.00390625) 0px -1px 0px;
     text-align: center;
     text-overflow: ellipsis;
     white-space: nowrap;
     font-weight: bold;
     font-size: 12px;
     color: rgb(255, 255, 255);
-    text-shadow: rgba(0, 0, 0, 0.179688) 0px -1px 0px;
+    text-shadow: rgba(0, 0, 0, 0.164062) 0px -1px 0px;
     text-align: center;
     text-overflow: ellipsis;
     white-space: nowrap;
     font-weight: bold;
     font-size: 12px;
     color: rgb(255, 255, 255);
-    text-shadow: rgba(0, 0, 0, 0.191406) 0px -1px 0px;
+    text-shadow: rgba(0, 0, 0, 0.175781) 0px -1px 0px;
     text-align: center;
     text-overflow: ellipsis;
     white-space: nowrap;
     font-size: 12px;
     color: rgb(255, 255, 255);
     text-align: center;
-    text-shadow: rgba(0, 0, 0, 0.523438) 0px -1px 0px;
+    text-shadow: rgba(0, 0, 0, 0.507812) 0px -1px 0px;
     text-overflow: ellipsis;
     white-space: nowrap;
     height: 30px;
     margin-left: auto;
     margin-right: auto;
 }
+
+#comment_footer {
+    font-size: 12px;
+    margin-left: 0px;
+}
+
+#older_comments_button {
+    font-family: Helvetica;
+    font-weight: bold;
+    font-size: 12px;
+    color: rgb(255, 255, 255);
+    text-align: center;
+    text-shadow: rgba(0, 0, 0, 0.585938) 0px -1px 0px;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    -webkit-dashboard-region: dashboard-region(control rectangle);
+    cursor: default;
+    -webkit-user-select: none;
+    position: relative;
+    -webkit-margin-top-collapse: separate;
+    -webkit-margin-bottom-collapse: separate;
+    height: 20px;
+    margin-bottom: 2px;
+    margin-top: 5px;
+    width: 181px;
+    margin-right: 5px;
+    margin-left: auto;
+}

File src/main/dashcode/JIRA-web-client.dcproj/project/mobile/main.js

                     }
                 });
             }
+
             jQuery("#issue_comments_container").empty();
-
             browser.goForward("view_issue", issueKey);
 
-            loadIssueComments(issueKey);
+            if(resp.comments.size > 0) {
+                loadIssueComments(issueKey, resp.addCommentPermission);
+            } else {
+                jQuery("#issue_comments_container").hide();
+            }
         },
         error: function(resp) {
             if(resp.status === 404) {
     });
 }
 
-function loadIssueComments(issueKey) {
+function loadIssueComments(issueKey, addPermission) {
     var commentContainer = jQuery("#issue_comments_container");
-    var addComment = function(comment) {
+    jQuery("#add_comment_form").hide();
+    var numComments = 0;
+
+    var createCommentRow = function(comment) {
         var commentRow = jQuery("#issue_commentRowTemplate").clone();
         commentRow.find("#issue_commentheader").text(comment.author.fullName + " - " + comment.created);
         commentRow.find("#issue_commentbody").html(comment.body);
                 jQuery(this).css("width", "99%").css("height","auto");
             }
         });
-        commentContainer.append(commentRow);
+        return commentRow;
     };
 
-    jQuery("#add_comment_form").hide();
-    jQuery.ajax({
-        url: contextPath + "/rest/iphone/1.0/issue/" + issueKey + "/comment",
-        dataType: "json",
-        success: function(resp) {
-            if(resp.comments && resp.comments.length > 0) {
-                commentContainer.show();
-                jQuery(resp.comments).each(function() {
-                    addComment(this);
-                });
-            } else {
-                commentContainer.hide();
+    var addComments = function(comments, total) {
+        for(var i = 0; i < comments.length; i++) {
+            commentContainer.append(createCommentRow(comments[i]));
+            numComments++;
+        }
+
+        if(total && total > numComments) {
+            jQuery("#comment_footer").show();
+            document.getElementById("older_comments_button").object.setText("Show older comments (" + (total - numComments) + ")");
+            jQuery("#older_comments_button").click(function(e) {
+                fetchComments("[" + numComments + ":" + (numComments + 19) + "]");
+                e.preventDefault();
+            });
+        }
+        else if(total) {
+            jQuery("#comment_footer").hide();
+        }
+    };
+
+    var fetchComments = function(range) {
+        jQuery.ajax({
+            url: contextPath + "/rest/iphone/1.0/issue/" + issueKey + "?expand=comments" + range,
+            dataType: "json",
+            success: function(resp) {
+                addComments(resp.comments.entries, resp.comments.size);
+            },
+            error: function(resp) {
+                alert("Error retrieving comments for '" + issueKey + "'");
             }
 
-            if(resp.addPermission) {
-                jQuery("#comment_textarea").val("");
-                jQuery("#add_comment_form").show();
-                jQuery("#submit_comment").click(function() {
-                    var commentBody = jQuery("#comment_textarea").val();
+        });
+    };
 
-                    jQuery.ajax({
-                        type: "post",
-                        url: contextPath + "/rest/iphone/1.0/issue/" + issueKey + "/comment",
-                        dataType: "json",
-                        contentType: "application/json",
-                        data: JSON.stringify({"body": commentBody}),
-                        success:function(resp) {
-                            commentContainer.show();
-                            addComment(resp);
-                            jQuery("#comment_textarea").val("");
-                        },
-                        error: function(resp) {
-                            if (resp.status === 401) {
-                                alert("You don't have permission to add a comment to this issue!");
-                            } else if(resp.responseText && resp.responseText.length > 0) {
-                                var errorCollection = JSON.parse(resp.responseText);
-                                var alertText = "Error adding comment:\n";
-                                jQuery(errorCollection.errors).each(function() {
-                                    alertText += this.error + "\n";
-                                });
-                                alert(alertText);
-                            } else {
-                                alert("Unknown error communicating with server!");
-                            }
-                        }
-                    });
-                });
-            }
-        },
-        error: function(resp) {
-            alert("Error retrieving comments for '" + issueKey + "'");
-        }
+    if(addPermission) {
+        jQuery("#comment_textarea").val("");
+        jQuery("#add_comment_form").show();
+        jQuery("#submit_comment").click(function() {
+            var commentBody = jQuery("#comment_textarea").val();
+            jQuery.ajax({
+                type: "post",
+                url: contextPath + "/rest/iphone/1.0/issue/" + issueKey + "/comment",
+                dataType: "json",
+                contentType: "application/json",
+                data: JSON.stringify({"body": commentBody}),
+                success:function(resp) {
+                    commentContainer.show();
+                    var commentRow = createCommentRow(resp);
+                    numComments++;
+                    commentContainer.prepend(commentRow);
+                    jQuery("#comment_textarea").val("");
+                },
+                error: function(resp) {
+                    if (resp.status === 401) {
+                        alert("You don't have permission to add a comment to this issue!");
+                    } else if(resp.responseText && resp.responseText.length > 0) {
+                        var errorCollection = JSON.parse(resp.responseText);
+                        var alertText = "Error adding comment:\n";
+                        jQuery(errorCollection.errors).each(function() {
+                            alertText += this.error + "\n";
+                        });
+                        alert(alertText);
+                    } else {
+                        alert("Unknown error communicating with server!");
+                    }
+                }
+            });
+        });
+    }
 
-    });
+    jQuery("#comment_footer").hide();
+    fetchComments(getRange());
 }
 
 function runJQL(event) {

File src/main/java/com/atlassian/jira/plugin/iphone/rest/IssuesResource.java

 package com.atlassian.jira.plugin.iphone.rest;
 
 import com.atlassian.jira.bc.issue.comment.CommentService;
-import com.atlassian.jira.bc.issue.search.SearchService;
 import com.atlassian.jira.issue.Issue;
 import com.atlassian.jira.issue.IssueManager;
 import com.atlassian.jira.issue.MutableIssue;
 import com.atlassian.jira.issue.comments.Comment;
 import com.atlassian.jira.plugin.iphone.components.FieldRenderer;
 import com.atlassian.jira.plugin.iphone.components.JaxbUserFactory;
-import com.atlassian.jira.plugin.iphone.rest.model.Comments;
+import com.atlassian.jira.plugin.iphone.rest.model.CommentWrapperCallback;
+import com.atlassian.jira.plugin.iphone.rest.model.CommentsWrapper;
 import com.atlassian.jira.plugin.iphone.rest.model.JaxbComment;
 import com.atlassian.jira.plugin.iphone.rest.model.JaxbIssue;
 import static com.atlassian.jira.rest.v1.model.errors.ErrorCollection.Builder.newBuilder;
 import javax.ws.rs.core.Response;
 import static javax.ws.rs.core.Response.Status.NOT_FOUND;
 import java.net.URI;
-import java.util.List;
 
 
 /**
 public class IssuesResource
 {
     private final JiraAuthenticationContext authenticationContext;
-    private final SearchService searchService;
     private final JaxbUserFactory jaxbUserFactory;
     private final FieldRenderer fieldRenderer;
     private final CommentService commentService;
     private final PermissionManager permissionManager;
     private final IssueManager issueManager;
 
-    public IssuesResource(final JiraAuthenticationContext authenticationContext, final SearchService searchService,
+    public IssuesResource(final JiraAuthenticationContext authenticationContext,
             final JaxbUserFactory jaxbUserFactory, final FieldRenderer fieldRenderer, final CommentService commentService,
             final OutlookDateManager outlookDateManager, final PermissionManager permissionManager, final IssueManager issueManager)
     {
         this.authenticationContext = authenticationContext;
-        this.searchService = searchService;
         this.jaxbUserFactory = jaxbUserFactory;
         this.fieldRenderer = fieldRenderer;
         this.commentService = commentService;
         {
             return Response.status(NOT_FOUND).cacheControl(NO_CACHE).build();
         }
-        return Response.ok(new JaxbIssue(issue, jaxbUserFactory, fieldRenderer)).cacheControl(NO_CACHE).build();
-    }
+        final CommentWrapperCallback callback = new CommentWrapperCallback(issue, authenticationContext.getUser(),
+                commentService, authenticationContext, jaxbUserFactory, fieldRenderer);
+        final CommentsWrapper comments = new CommentsWrapper(callback, callback.getSize());
+        final boolean addCommentPermission = permissionManager.hasPermission(Permissions.COMMENT_ISSUE, issue, authenticationContext.getUser());
 
-    @GET
-    @Path ("{issuekey}/comment")
-    public Response getIssuesComments(@PathParam ("issuekey") final String issueKey)
-    {
-        final Issue issue = getIssueByKey(issueKey, authenticationContext.getUser());
-        final ErrorCollection errors = new SimpleErrorCollection();
-        @SuppressWarnings ("unchecked")
-        final List<Comment> comments = commentService.getCommentsForUser(authenticationContext.getUser(), issue, errors);
-        final OutlookDate outlookDate = outlookDateManager.getOutlookDate(authenticationContext.getLocale());
-        return Response.ok(new Comments(comments, permissionManager.hasPermission(Permissions.COMMENT_ISSUE, issue, authenticationContext.getUser()),
-                jaxbUserFactory, outlookDate, fieldRenderer, issue)).cacheControl(NO_CACHE).build();
+        return Response.ok(new JaxbIssue(issue, comments, addCommentPermission,
+                jaxbUserFactory, fieldRenderer)).cacheControl(NO_CACHE).build();
     }
 
     @GET

File src/main/java/com/atlassian/jira/plugin/iphone/rest/model/CommentWrapperCallback.java

+package com.atlassian.jira.plugin.iphone.rest.model;
+
+import com.atlassian.jira.bc.issue.comment.CommentService;
+import com.atlassian.jira.issue.Issue;
+import com.atlassian.jira.issue.IssueFieldConstants;
+import com.atlassian.jira.issue.comments.Comment;
+import com.atlassian.jira.issue.comments.CommentComparator;
+import com.atlassian.jira.plugin.iphone.components.FieldRenderer;
+import com.atlassian.jira.plugin.iphone.components.JaxbUserFactory;
+import com.atlassian.jira.security.JiraAuthenticationContext;
+import com.atlassian.jira.util.ErrorCollection;
+import com.atlassian.jira.util.Function;
+import com.atlassian.jira.util.SimpleErrorCollection;
+import com.atlassian.jira.util.collect.CollectionUtil;
+import com.atlassian.jira.web.util.OutlookDate;
+import com.atlassian.plugins.rest.common.expand.entity.ListWrapperCallback;
+import com.atlassian.plugins.rest.common.expand.parameter.Indexes;
+import com.opensymphony.user.User;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * TODO: Document this class / interface here
+ *
+ * @since v4.1
+ */
+public class CommentWrapperCallback implements ListWrapperCallback<JaxbComment>
+{
+    private final User user;
+    private final Issue issue;
+    private final CommentService commentService;
+    private final JiraAuthenticationContext authenticationContext;
+    private final JaxbUserFactory jaxbUserFactory;
+    private final FieldRenderer fieldRenderer;
+
+    public CommentWrapperCallback(final Issue issue, final User user, final CommentService commentService,
+            final JiraAuthenticationContext authenticationContext, final JaxbUserFactory jaxbUserFactory,
+            final FieldRenderer fieldRenderer)
+    {
+        this.issue = issue;
+        this.user = user;
+        this.commentService = commentService;
+        this.authenticationContext = authenticationContext;
+        this.jaxbUserFactory = jaxbUserFactory;
+        this.fieldRenderer = fieldRenderer;
+    }
+
+    public List<JaxbComment> getItems(final Indexes indexes)
+    {
+        final ErrorCollection errors = new SimpleErrorCollection();
+        @SuppressWarnings ("unchecked")
+        final List<Comment> comments = commentService.getCommentsForUser(user, issue, errors);
+        if (comments != null)
+        {
+            final OutlookDate outlookDate = authenticationContext.getOutlookDate();
+            final int searchCount = comments.size();
+            int startIndex = Math.max(0, indexes.getMinIndex(searchCount));
+            int endIndex = Math.max(0, indexes.getMaxIndex(searchCount)) + 1;
+
+            final Comparator<Comment> reverseComparator = Collections.reverseOrder(CommentComparator.COMPARATOR);
+            Collections.sort(comments, reverseComparator);
+
+            final List<Comment> res = comments.subList(startIndex, endIndex);
+            return CollectionUtil.transform(res, new Function<Comment, JaxbComment>()
+            {
+                public JaxbComment get(final Comment comment)
+                {
+                    return new JaxbComment(comment.getId(),
+                            jaxbUserFactory.createJaxbUser(comment.getAuthor()),
+                            fieldRenderer.getFieldHtml(IssueFieldConstants.COMMENT, comment.getBody(), issue),
+                            outlookDate.formatDMYHMS(comment.getCreated()));
+                }
+            });
+        }
+        return Collections.emptyList();
+    }
+
+    public int getSize()
+    {
+        final ErrorCollection errors = new SimpleErrorCollection();
+        @SuppressWarnings ("unchecked")
+        final List<Comment> comments = commentService.getCommentsForUser(user, issue, errors);
+        if (comments != null)
+        {
+            return comments.size();
+        }
+        return 0;
+    }
+}

File src/main/java/com/atlassian/jira/plugin/iphone/rest/model/Comments.java

-package com.atlassian.jira.plugin.iphone.rest.model;
-
-import com.atlassian.jira.issue.Issue;
-import com.atlassian.jira.issue.IssueFieldConstants;
-import com.atlassian.jira.issue.comments.Comment;
-import com.atlassian.jira.plugin.iphone.components.FieldRenderer;
-import com.atlassian.jira.plugin.iphone.components.JaxbUserFactory;
-import com.atlassian.jira.util.Function;
-import com.atlassian.jira.util.collect.CollectionUtil;
-import com.atlassian.jira.web.util.OutlookDate;
-
-import javax.xml.bind.annotation.XmlElement;
-import javax.xml.bind.annotation.XmlRootElement;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-
-/**
- * TODO: Document this class / interface here
- *
- * @since v4.1
- */
-@XmlRootElement
-public class Comments
-{
-    @XmlElement
-    private final List<JaxbComment> comments = new ArrayList<JaxbComment>();
-
-    @XmlElement
-    private boolean addPermission;
-
-    private Comments() {}
-
-    public Comments(final Collection<Comment> comments, final boolean addPermission, final JaxbUserFactory jaxbUserFactory,
-            final OutlookDate outlookDate, final FieldRenderer fieldRenderer, final Issue issue)
-    {
-        this.addPermission = addPermission;
-        this.comments.addAll(CollectionUtil.transform(comments, new Function<Comment, JaxbComment>()
-        {
-            public JaxbComment get(final Comment comment)
-            {
-                return new JaxbComment(comment.getId(),
-                        jaxbUserFactory.createJaxbUser(comment.getAuthor()),
-                        fieldRenderer.getFieldHtml(IssueFieldConstants.COMMENT, comment.getBody(), issue),
-                        outlookDate.formatDMYHMS(comment.getCreated()));
-            }
-        }));
-    }
-
-    public List<JaxbComment> getComments()
-    {
-        return comments;
-    }
-
-    public boolean isAddPermission()
-    {
-        return addPermission;
-    }
-}

File src/main/java/com/atlassian/jira/plugin/iphone/rest/model/CommentsWrapper.java

+package com.atlassian.jira.plugin.iphone.rest.model;
+
+import com.atlassian.plugins.rest.common.expand.entity.ListWrapper;
+import com.atlassian.plugins.rest.common.expand.entity.ListWrapperCallback;
+import com.atlassian.plugins.rest.common.expand.Expandable;
+
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlTransient;
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * TODO: Document this class / interface here
+ *
+ * @since v4.1
+ */
+public class CommentsWrapper implements ListWrapper<JaxbComment>
+{
+    @XmlAttribute
+    private String expand;
+
+    @XmlAttribute
+    private int size;
+
+    @XmlElement
+    @Expandable
+    private List<JaxbComment> entries = new ArrayList<JaxbComment>();
+
+    @XmlTransient
+    private final ListWrapperCallback<JaxbComment> callback;
+
+    private CommentsWrapper()
+    {
+        this.size = 0;
+        this.callback = null;
+    }
+
+    public CommentsWrapper(final ListWrapperCallback<JaxbComment> callback, final int size)
+    {
+        this.callback = callback;
+        this.size = size;
+    }
+
+    public ListWrapperCallback<JaxbComment> getCallback()
+    {
+        return callback;
+    }
+
+    public String getExpand()
+    {
+        return expand;
+    }
+
+    public List<JaxbComment> getEntries()
+    {
+        return entries;
+    }
+
+    public int getSize()
+    {
+        return size;
+    }
+}

File src/main/java/com/atlassian/jira/plugin/iphone/rest/model/Issues.java

 
     @XmlElement
     @Expandable
-    private FilterResultWrapper result;
+    private final FilterResultWrapper result;
 
-    private Issues() {}
+    private Issues()
+    {
+        result = null;
+    }
 
     public Issues(final FilterResultWrapper result)
     {

File src/main/java/com/atlassian/jira/plugin/iphone/rest/model/JaxbIssue.java

 import com.atlassian.jira.issue.priority.Priority;
 import com.atlassian.jira.plugin.iphone.components.JaxbUserFactory;
 import com.atlassian.jira.plugin.iphone.components.FieldRenderer;
+import com.atlassian.plugins.rest.common.expand.Expandable;
 
 import javax.xml.bind.annotation.XmlElement;
 import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlAttribute;
 
 /**
  * Represents an issue to be served via REST
     @XmlElement
     private JaxbUser reporter;
 
+    @XmlAttribute
+    private String expand;
 
-    private JaxbIssue() { }
+    @XmlElement
+    @Expandable
+    private CommentsWrapper comments;
+
+    @XmlElement
+    private boolean addCommentPermission;
+
+    private JaxbIssue() {}
 
     public JaxbIssue(final Long id, final String key, final String summary, final IssueType issueType, final Priority priority)
     {
     }
 
     //TODO: Introduce builder!
-    public JaxbIssue(final Issue issue, final JaxbUserFactory jaxbUserFactory, final FieldRenderer fieldRenderer)
+    public JaxbIssue(final Issue issue, final CommentsWrapper comments, final boolean addCommentPermission,  
+            final JaxbUserFactory jaxbUserFactory, final FieldRenderer fieldRenderer)
     {
         this(issue.getId(), issue.getKey(), issue.getSummary(), issue.getIssueTypeObject(), issue.getPriorityObject());
 
         this.assignee = issue.getAssigneeId() == null ? null : jaxbUserFactory.createJaxbUser(issue.getAssigneeId());
         this.reporter = jaxbUserFactory.createJaxbUser(issue.getReporterId());
         this.description = fieldRenderer.getFieldHtml(IssueFieldConstants.DESCRIPTION, issue.getDescription(), issue);
+
+        this.comments = comments;
+        this.addCommentPermission = addCommentPermission;
     }
 
     public String getDescription()
     {
         return status;
     }
+
+    public boolean isAddCommentPermission()
+    {
+        return addCommentPermission;
+    }
+
+    public CommentsWrapper getComments()
+    {
+        return comments;
+    }
+
+    public String getExpand()
+    {
+        return expand;
+    }
 }