diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b115274..f5b21fa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Change Log +## [Unreleased] + +### Added +- Show more issue events. + +### Fixed +- Fix crash that occurred when opening a pull request with reviews. +- Some minor improvements. + ## [ForkHub v1.2.5] - 2016-12-16 ### Added @@ -149,6 +158,7 @@ - Last official GitHub release +[Unreleased]: https://github.com/jonan/ForkHub/compare/ForkHub-v1.2.5...master [ForkHub v1.2.5]: https://github.com/jonan/ForkHub/compare/ForkHub-v1.2.4...ForkHub-v1.2.5 [ForkHub v1.2.4]: https://github.com/jonan/ForkHub/compare/ForkHub-v1.2.3...ForkHub-v1.2.4 [ForkHub v1.2.3]: https://github.com/jonan/ForkHub/compare/ForkHub-v1.2.2...ForkHub-v1.2.3 diff --git a/app/src/main/java/com/github/mobile/api/RequestConfiguration.java b/app/src/main/java/com/github/mobile/api/RequestConfiguration.java index 3011796a..590dd2bb 100644 --- a/app/src/main/java/com/github/mobile/api/RequestConfiguration.java +++ b/app/src/main/java/com/github/mobile/api/RequestConfiguration.java @@ -30,7 +30,7 @@ public class RequestConfiguration implements okhttp3.Interceptor { private static final String HEADER_USER_AGENT = "ForkHub/2.0"; - private static final String HEADER_ACCEPT = "application/vnd.github.v3.html+json"; + private static final String HEADER_ACCEPT = "application/vnd.github.v3.full+json"; private final Provider accountProvider; diff --git a/app/src/main/java/com/github/mobile/api/model/CommitAuthor.java b/app/src/main/java/com/github/mobile/api/model/CommitAuthor.java new file mode 100644 index 00000000..d752e7c4 --- /dev/null +++ b/app/src/main/java/com/github/mobile/api/model/CommitAuthor.java @@ -0,0 +1,26 @@ +/* + * Copyright 2016 Jon Ander Peñalba + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.mobile.api.model; + +import java.util.Date; + +public class CommitAuthor { + public String name; + + public String email; + + public Date date; +} diff --git a/app/src/main/java/com/github/mobile/api/model/Issue.java b/app/src/main/java/com/github/mobile/api/model/Issue.java index 3fcbbb8c..c6126ccd 100644 --- a/app/src/main/java/com/github/mobile/api/model/Issue.java +++ b/app/src/main/java/com/github/mobile/api/model/Issue.java @@ -21,6 +21,8 @@ public class Issue { public long id; + public Repository repository; + public String url; public String html_url; @@ -39,6 +41,8 @@ public class Issue { public User assignee; + public List assignees; + public Milestone milestone; public boolean locked; diff --git a/app/src/main/java/com/github/mobile/api/model/LineComment.java b/app/src/main/java/com/github/mobile/api/model/LineComment.java new file mode 100644 index 00000000..0626e30a --- /dev/null +++ b/app/src/main/java/com/github/mobile/api/model/LineComment.java @@ -0,0 +1,30 @@ +/* + * Copyright 2016 Jon Ander Peñalba + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.mobile.api.model; + +import java.util.Date; + +public class LineComment { + public long id; + + public User user; + + public String body_html; + + public Date created_at; + + public Date updated_at; +} diff --git a/app/src/main/java/com/github/mobile/api/model/ReferenceSource.java b/app/src/main/java/com/github/mobile/api/model/ReferenceSource.java new file mode 100644 index 00000000..3c0f6bfb --- /dev/null +++ b/app/src/main/java/com/github/mobile/api/model/ReferenceSource.java @@ -0,0 +1,26 @@ +/* + * Copyright 2016 Jon Ander Peñalba + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.mobile.api.model; + +import java.util.Date; + +public class ReferenceSource { + public String type; + + public Issue issue; + + public Date date; +} diff --git a/app/src/main/java/com/github/mobile/api/model/Repository.java b/app/src/main/java/com/github/mobile/api/model/Repository.java index f3c5fb24..ba638b5c 100644 --- a/app/src/main/java/com/github/mobile/api/model/Repository.java +++ b/app/src/main/java/com/github/mobile/api/model/Repository.java @@ -17,6 +17,8 @@ import com.squareup.moshi.Json; +import java.util.Date; + public class Repository { public long id; @@ -34,11 +36,11 @@ public class Repository { @Json(name = "fork") public boolean is_fork; - public String created_at; + public Date created_at; - public String updated_at; + public Date updated_at; - public String pushed_at; + public Date pushed_at; public String homepage; diff --git a/app/src/main/java/com/github/mobile/api/model/TimelineEvent.java b/app/src/main/java/com/github/mobile/api/model/TimelineEvent.java index 6eb7ff3e..566b5425 100644 --- a/app/src/main/java/com/github/mobile/api/model/TimelineEvent.java +++ b/app/src/main/java/com/github/mobile/api/model/TimelineEvent.java @@ -15,7 +15,10 @@ */ package com.github.mobile.api.model; +import org.eclipse.egit.github.core.Comment; + import java.util.Date; +import java.util.List; public class TimelineEvent { public static final String EVENT_ASSIGNED = "assigned"; @@ -36,6 +39,8 @@ public class TimelineEvent { public static final String EVENT_RENAMED = "renamed"; public static final String EVENT_REOPENED = "reopened"; public static final String EVENT_REVIEWED = "reviewed"; + public static final String EVENT_REVIEW_REQUESTED = "review_requested"; + public static final String EVENT_REVIEW_REQUEST_REMOVED = "review_request_removed"; public static final String EVENT_SUBSCRIBED = "subscribed"; public static final String EVENT_UNASSIGNED = "unassigned"; public static final String EVENT_UNLABELED = "unlabeled"; @@ -46,14 +51,34 @@ public class TimelineEvent { public User actor; + public CommitAuthor author; + + public CommitAuthor committer; + + public List comments; + + public ReferenceSource source; + + public User review_requester; + + public User requested_reviewer; + public String commit_id; + public String sha; + + public String message; + public String event; public Date created_at; public Date updated_at; + public String body; + + public String body_html; + public Label label; public User assignee; @@ -61,4 +86,15 @@ public class TimelineEvent { public Milestone milestone; public Rename rename; + + public Comment getOldCommentModel() { + Comment comment = new Comment(); + comment.setCreatedAt(created_at); + comment.setUpdatedAt(updated_at); + comment.setBody(body); + comment.setBodyHtml(body_html); + comment.setId(id); + comment.setUser(actor.getOldUserModel()); + return comment; + } } diff --git a/app/src/main/java/com/github/mobile/api/model/User.java b/app/src/main/java/com/github/mobile/api/model/User.java index c47e5c31..dc68e513 100644 --- a/app/src/main/java/com/github/mobile/api/model/User.java +++ b/app/src/main/java/com/github/mobile/api/model/User.java @@ -17,6 +17,8 @@ import com.squareup.moshi.Json; +import java.util.Date; + public class User { public static final String TYPE_USER = "User"; public static final String TYPE_ORGANIZATION = "Organization"; @@ -55,9 +57,9 @@ public class User { public int following; - public String created_at; + public Date created_at; - public String updated_at; + public Date updated_at; public int total_private_repos; @@ -70,4 +72,30 @@ public class User { public int collaborators; public Plan plan; + + public org.eclipse.egit.github.core.User getOldUserModel() { + org.eclipse.egit.github.core.User user = new org.eclipse.egit.github.core.User(); + user.setId((int) id); + user.setLogin(login); + user.setAvatarUrl(avatar_url); + user.setType(type); + user.setName(name); + user.setCompany(company); + user.setBlog(blog); + user.setLocation(location); + user.setEmail(email); + user.setHireable(is_hireable); + user.setBio(bio); + user.setPublicRepos(public_repos); + user.setPublicGists(public_gists); + user.setFollowers(followers); + user.setFollowing(following); + user.setCreatedAt(created_at); + user.setTotalPrivateRepos(total_private_repos); + user.setOwnedPrivateRepos(owned_private_repos); + user.setPrivateGists(private_gists); + user.setDiskUsage(disk_usage); + user.setCollaborators(collaborators); + return user; + } } diff --git a/app/src/main/java/com/github/mobile/core/issue/FullIssue.java b/app/src/main/java/com/github/mobile/core/issue/FullIssue.java index d41af048..833a61fc 100644 --- a/app/src/main/java/com/github/mobile/core/issue/FullIssue.java +++ b/app/src/main/java/com/github/mobile/core/issue/FullIssue.java @@ -18,19 +18,14 @@ import com.github.mobile.api.model.TimelineEvent; import com.github.mobile.api.model.ReactionSummary; -import java.io.Serializable; -import java.util.ArrayList; import java.util.Collection; -import org.eclipse.egit.github.core.Comment; import org.eclipse.egit.github.core.Issue; /** * Issue model with comments */ -public class FullIssue extends ArrayList implements Serializable { - - private static final long serialVersionUID = 4586476132467323827L; +public class FullIssue { private final Issue issue; @@ -43,23 +38,13 @@ public class FullIssue extends ArrayList implements Serializable { * * @param issue * @param reactions - * @param comments * @param events */ public FullIssue(final Issue issue, final ReactionSummary reactions, - final Collection comments, final Collection events) { - super(comments); - - this.events = events; - this.reactions = reactions; + final Collection events) { this.issue = issue; - } - - /** - * Create empty wrapper - */ - public FullIssue() { - this.issue = null; + this.reactions = reactions; + this.events = events; } /** diff --git a/app/src/main/java/com/github/mobile/core/issue/RefreshIssueTask.java b/app/src/main/java/com/github/mobile/core/issue/RefreshIssueTask.java index 3d92ce42..aa8dc277 100644 --- a/app/src/main/java/com/github/mobile/core/issue/RefreshIssueTask.java +++ b/app/src/main/java/com/github/mobile/core/issue/RefreshIssueTask.java @@ -21,6 +21,7 @@ import com.github.mobile.accounts.AuthenticatedUserTask; import com.github.mobile.api.model.TimelineEvent; +import com.github.mobile.api.service.IssueService; import com.github.mobile.api.service.PaginationService; import com.github.mobile.api.model.ReactionSummary; import com.github.mobile.util.HtmlUtils; @@ -28,17 +29,10 @@ import com.google.inject.Inject; import java.io.IOException; -import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; -import java.util.List; -import org.eclipse.egit.github.core.Comment; -import org.eclipse.egit.github.core.CommitComment; import org.eclipse.egit.github.core.IRepositoryIdProvider; import org.eclipse.egit.github.core.Issue; -import org.eclipse.egit.github.core.service.IssueService; -import org.eclipse.egit.github.core.service.PullRequestService; /** * Task to load and store an {@link Issue} @@ -48,17 +42,11 @@ public class RefreshIssueTask extends AuthenticatedUserTask { private static final String TAG = "RefreshIssueTask"; @Inject - private IssueService issueService; - - @Inject - private PullRequestService pullService; + private IssueService service; @Inject private IssueStore store; - @Inject - private com.github.mobile.api.service.IssueService newIssueService; - private final IRepositoryIdProvider repositoryId; private final int issueNumber; @@ -91,42 +79,32 @@ public RefreshIssueTask(Context context, public FullIssue run(Account account) throws Exception { Issue issue = store.refreshIssue(repositoryId, issueNumber); bodyImageGetter.encode(issue.getId(), issue.getBodyHtml()); - List comments; - if (issue.getComments() > 0) - comments = issueService.getComments(repositoryId, issueNumber); - else - comments = Collections.emptyList(); - - List reviews; - if (IssueUtils.isPullRequest(issue)) - reviews = pullService.getComments(repositoryId, issueNumber); - else - reviews = Collections.emptyList(); - - for (Comment comment : comments) { - String formatted = HtmlUtils.format(comment.getBodyHtml()) - .toString(); - comment.setBodyHtml(formatted); - commentImageGetter.encode(comment.getId(), formatted); - } final String[] repo = repositoryId.generateId().split("/"); PaginationService paginationService = new PaginationService(1, PaginationService.ITEMS_PER_PAGE_MAX) { @Override public Collection getSinglePage(int page, int itemsPerPage) throws IOException { - return newIssueService.getTimeline(repo[0], repo[1], issueNumber, page, itemsPerPage).execute().body(); + return service.getTimeline(repo[0], repo[1], issueNumber, page, itemsPerPage).execute().body(); } }; Collection timelineEvents = paginationService.getAll(); + for (TimelineEvent comment : timelineEvents) { + if (TimelineEvent.EVENT_COMMENTED.equals(comment.event)) { + String formatted = HtmlUtils.format(comment.body_html).toString(); + comment.body_html = formatted; + commentImageGetter.encode(comment.id, formatted); + } + } + ReactionSummary reactions = new ReactionSummary(); try { - reactions = newIssueService.getIssue(repo[0], repo[1], issueNumber).execute().body().reactions; + reactions = service.getIssue(repo[0], repo[1], issueNumber).execute().body().reactions; } catch (Exception e) { // Reactions are in a preview state, API can change, so make sure we don't crash if it does. } - return new FullIssue(issue, reactions, sortAllComments(comments, reviews), timelineEvents); + return new FullIssue(issue, reactions, timelineEvents); } @Override @@ -135,31 +113,4 @@ protected void onException(Exception e) throws RuntimeException { Log.d(TAG, "Exception loading issue", e); } - - private List sortAllComments(List comments, List reviews) { - List allComments = new ArrayList<>(comments.size() + reviews.size()); - - int numReviews = reviews.size(); - - int start = 0; - for (Comment comment : comments) { - for (int i = start; i < numReviews; i++) { - CommitComment review = reviews.get(i); - if (comment.getCreatedAt().after(review.getCreatedAt())) { - allComments.add(review); - start++; - } else { - i = numReviews; - } - } - allComments.add(comment); - } - - // Add the remaining reviews - for (int i = start; i < numReviews; i++) { - allComments.add(reviews.get(i)); - } - - return allComments; - } } diff --git a/app/src/main/java/com/github/mobile/ui/comment/CommentListAdapter.java b/app/src/main/java/com/github/mobile/ui/comment/CommentListAdapter.java index 8b77ccd1..26ef3653 100644 --- a/app/src/main/java/com/github/mobile/ui/comment/CommentListAdapter.java +++ b/app/src/main/java/com/github/mobile/ui/comment/CommentListAdapter.java @@ -15,328 +15,72 @@ */ package com.github.mobile.ui.comment; - -import android.app.Activity; -import android.content.Context; -import android.content.res.Resources; -import android.text.Html; import android.text.method.LinkMovementMethod; +import android.view.LayoutInflater; import android.view.View; -import com.github.kevinsawicki.wishlist.MultiTypeAdapter; +import com.github.kevinsawicki.wishlist.SingleTypeAdapter; import com.github.mobile.R; -import com.github.mobile.api.model.TimelineEvent; -import com.github.mobile.api.model.User; -import com.github.mobile.ui.issue.IssueFragment; -import com.github.mobile.ui.user.UserViewActivity; import com.github.mobile.util.AvatarLoader; import com.github.mobile.util.HttpImageGetter; import com.github.mobile.util.TimeUtils; -import com.github.mobile.util.TypefaceUtils; - -import java.util.Collection; import org.eclipse.egit.github.core.Comment; -import org.eclipse.egit.github.core.CommitComment; /** * Adapter for a list of {@link Comment} objects */ -public class CommentListAdapter extends MultiTypeAdapter { - - private final Context context; - - private final Resources resources; +public class CommentListAdapter extends SingleTypeAdapter { private final AvatarLoader avatars; private final HttpImageGetter imageGetter; - private final IssueFragment issueFragment; - - private final String user; - - private final boolean isCollaborator; - - - /** - * Create list adapter - * - * @param activity - * @param avatars - * @param imageGetter - */ - public CommentListAdapter(Activity activity, AvatarLoader avatars, HttpImageGetter imageGetter) { - this(activity, avatars, imageGetter, null, false, ""); - } - /** * Create list adapter * - * @param activity + * @param inflater * @param avatars * @param imageGetter - * @param issueFragment */ - public CommentListAdapter(Activity activity, AvatarLoader avatars, - HttpImageGetter imageGetter, IssueFragment issueFragment, - boolean isCollaborator, String loggedUser) { - super(activity.getLayoutInflater()); + public CommentListAdapter(LayoutInflater inflater, AvatarLoader avatars, + HttpImageGetter imageGetter) { + super(inflater, R.layout.comment_item); - this.context = activity; - this.resources = activity.getResources(); this.avatars = avatars; this.imageGetter = imageGetter; - this.issueFragment = issueFragment; - this.isCollaborator = isCollaborator; - this.user = loggedUser; } @Override - protected void update(int position, Object obj, int type) { - if (type == 0) { - updateComment((Comment) obj); - } else { - if (obj instanceof CommitComment) { - updateReview((CommitComment) obj); - } else { - updateEvent((TimelineEvent) obj); - } - } - } - - protected void updateEvent(final TimelineEvent event) { - String eventString = event.event; - - User actor; - if (eventString.equals(TimelineEvent.EVENT_ASSIGNED) || eventString.equals(TimelineEvent.EVENT_UNASSIGNED)) { - actor = event.assignee; - } else { - actor = event.actor; - } - - String message = String.format("%s ", actor == null ? "ghost" : actor.login); - avatars.bind(imageView(2), actor); - - switch (eventString) { - case TimelineEvent.EVENT_ASSIGNED: - int assignedTextResource = R.string.issue_event_label_assigned; - if (event.actor.id == event.assignee.id) { - assignedTextResource = R.string.issue_event_label_self_assigned; - } - message += String.format(resources.getString(assignedTextResource), "" + event.actor.login + ""); - setText(0, TypefaceUtils.ICON_PERSON); - textView(0).setTextColor(resources.getColor(R.color.issue_event_normal)); - break; - case TimelineEvent.EVENT_UNASSIGNED: - int unassignedTextResource = R.string.issue_event_label_unassigned; - if (event.actor.id == event.assignee.id) { - unassignedTextResource = R.string.issue_event_label_self_unassigned; - } - message += String.format(resources.getString(unassignedTextResource), "" + event.actor.login + ""); - setText(0, TypefaceUtils.ICON_PERSON); - textView(0).setTextColor(resources.getColor(R.color.issue_event_normal)); - break; - case TimelineEvent.EVENT_LABELED: - message += String.format(resources.getString(R.string.issue_event_label_added), "" + event.label.name + ""); - setText(0, TypefaceUtils.ICON_TAG); - textView(0).setTextColor(resources.getColor(R.color.issue_event_normal)); - break; - case TimelineEvent.EVENT_UNLABELED: - message += String.format(resources.getString(R.string.issue_event_label_removed), "" + event.label.name + ""); - setText(0, TypefaceUtils.ICON_TAG); - textView(0).setTextColor(resources.getColor(R.color.issue_event_normal)); - break; - case TimelineEvent.EVENT_REFERENCED: - message += String.format(resources.getString(R.string.issue_event_referenced), "" + event.commit_id.substring(0,7) + ""); - setText(0, TypefaceUtils.ICON_BOOKMARK); - textView(0).setTextColor(resources.getColor(R.color.issue_event_normal)); - break; - case TimelineEvent.EVENT_MILESTONED: - message += String.format(resources.getString(R.string.issue_event_milestone_added), "" + event.milestone.title + ""); - setText(0, TypefaceUtils.ICON_MILESTONE); - textView(0).setTextColor(resources.getColor(R.color.issue_event_normal)); - break; - case TimelineEvent.EVENT_DEMILESTONED: - message += String.format(resources.getString(R.string.issue_event_milestone_removed), "" + event.milestone.title + ""); - setText(0, TypefaceUtils.ICON_MILESTONE); - textView(0).setTextColor(resources.getColor(R.color.issue_event_normal)); - break; - case TimelineEvent.EVENT_CLOSED: - if (event.commit_id == null) { - message += resources.getString(R.string.issue_event_closed); - } else { - message += String.format(resources.getString(R.string.issue_event_closed_from_commit), "" + event.commit_id.substring(0,7) + ""); - } - setText(0, TypefaceUtils.ICON_CIRCLE_SLASH); - textView(0).setTextColor(resources.getColor(R.color.issue_event_red)); - break; - case TimelineEvent.EVENT_REOPENED: - message += resources.getString(R.string.issue_event_reopened); - setText(0, TypefaceUtils.ICON_PRIMITIVE_DOT); - textView(0).setTextColor(resources.getColor(R.color.issue_event_green)); - break; - case TimelineEvent.EVENT_RENAMED: - message += String.format(resources.getString(R.string.issue_event_rename), - "" + event.rename.from + "", - "" + event.rename.to + ""); - setText(0, TypefaceUtils.ICON_PENCIL); - textView(0).setTextColor(resources.getColor(R.color.issue_event_normal)); - break; - case TimelineEvent.EVENT_MERGED: - message += String.format(resources.getString(R.string.issue_event_merged), "" + event.commit_id.substring(0,7) + ""); - setText(0, TypefaceUtils.ICON_GIT_MERGE); - textView(0).setTextColor(resources.getColor(R.color.issue_event_purple)); - break; - case TimelineEvent.EVENT_LOCKED: - message += resources.getString(R.string.issue_event_lock); - setText(0, TypefaceUtils.ICON_LOCK); - textView(0).setTextColor(resources.getColor(R.color.issue_event_dark)); - break; - case TimelineEvent.EVENT_UNLOCKED: - message += resources.getString(R.string.issue_event_unlock); - setText(0, TypefaceUtils.ICON_KEY); - textView(0).setTextColor(resources.getColor(R.color.issue_event_dark)); - break; - case TimelineEvent.EVENT_HEAD_REF_DELETED: - message += resources.getString(R.string.issue_event_head_ref_deleted); - setText(0, TypefaceUtils.ICON_GIT_BRANCH); - textView(0).setTextColor(resources.getColor(R.color.issue_event_light)); - break; - case TimelineEvent.EVENT_HEAD_REF_RESTORED: - message += resources.getString(R.string.issue_event_head_ref_restored); - setText(0, TypefaceUtils.ICON_GIT_BRANCH); - textView(0).setTextColor(resources.getColor(R.color.issue_event_light)); - break; - } - - message += " " + TimeUtils.getRelativeTime(event.created_at); - setText(1, Html.fromHtml(message)); - } - - protected void updateReview(final CommitComment review) { - String message = String.format("%s ", review.getUser() == null ? "ghost" : review.getUser().getLogin()); - avatars.bind(imageView(2), review.getUser()); - message += resources.getString(R.string.issue_event_comment_diff); - setText(0, TypefaceUtils.ICON_CODE); - textView(0).setTextColor(resources.getColor(R.color.issue_event_light)); - - message += " " + TimeUtils.getRelativeTime(review.getCreatedAt()); - setText(1, Html.fromHtml(message)); - } - - protected void updateComment(final Comment comment) { + protected void update(int position, Comment comment) { imageGetter.bind(textView(0), comment.getBodyHtml(), comment.getId()); avatars.bind(imageView(4), comment.getUser()); - imageView(4).setOnClickListener(new View.OnClickListener() { - - @Override - public void onClick(View v) { - context.startActivity(UserViewActivity.createIntent(comment.getUser())); - } - }); setText(1, comment.getUser() == null ? "ghost" : comment.getUser().getLogin()); setText(2, TimeUtils.getRelativeTime(comment.getCreatedAt())); setGone(3, !comment.getUpdatedAt().after(comment.getCreatedAt())); - - boolean canEdit = isCollaborator || - (comment.getUser() != null && comment.getUser().getLogin().equals(user)); - - if (issueFragment != null && canEdit) { - // Edit button - setGone(5, false); - view(5).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - issueFragment.editComment(comment); - } - }); - // Delete button - setGone(6, false); - view(6).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - issueFragment.deleteComment(comment); - } - }); - } else { - setGone(5, true); - setGone(6, true); - } - } - - public MultiTypeAdapter setItems(Collection items) { - if (items == null || items.isEmpty()) - return this; - return setItems(items.toArray()); - } - - public MultiTypeAdapter setItems(final Object[] items) { - if (items == null || items.length == 0) - return this; - - this.clear(); - - for (Object item : items) { - if (item instanceof CommitComment) - this.addItem(1, item); - else if (item instanceof Comment) - this.addItem(0, item); - else - this.addItem(1, item); - } - - notifyDataSetChanged(); - return this; - } - - @Override - protected View initialize(int type, View view) { - view = super.initialize(type, view); - - if (type == 0) { - textView(view, 0).setMovementMethod(LinkMovementMethod.getInstance()); - TypefaceUtils.setOcticons(textView(view, 5), textView(view, 6)); - setText(view, 5, TypefaceUtils.ICON_PENCIL); - setText(view, 6, TypefaceUtils.ICON_X); - } else { - TypefaceUtils.setOcticons(textView(view, 0)); - } - - return view; } @Override - public int getViewTypeCount() { - return 2; + public long getItemId(final int position) { + return getItem(position).getId(); } @Override - protected int getChildLayoutId(int type) { - if (type == 0) - return R.layout.comment_item; - else - return R.layout.comment_event_item; - } + protected View initialize(View view) { + view = super.initialize(view); - @Override - public boolean areAllItemsEnabled() { - return false; - } + textView(view, 0).setMovementMethod(LinkMovementMethod.getInstance()); + setGone(5, true); + setGone(6, true); - @Override - public boolean isEnabled(int position) { - return false; + return view; } @Override - protected int[] getChildViewIds(int type) { - if(type == 0) - return new int[] { R.id.tv_comment_body, R.id.tv_comment_author, - R.id.tv_comment_date, R.id.tv_comment_edited, R.id.iv_avatar, - R.id.iv_comment_edit, R.id.iv_comment_delete }; - else - return new int[]{R.id.tv_event_icon, R.id.tv_event, R.id.iv_avatar}; + protected int[] getChildViewIds() { + return new int[] { R.id.tv_comment_body, R.id.tv_comment_author, + R.id.tv_comment_date, R.id.tv_comment_edited, R.id.iv_avatar, + R.id.iv_comment_edit, R.id.iv_comment_delete }; } } diff --git a/app/src/main/java/com/github/mobile/ui/gist/GistFragment.java b/app/src/main/java/com/github/mobile/ui/gist/GistFragment.java index 354eed86..56b1c15b 100644 --- a/app/src/main/java/com/github/mobile/ui/gist/GistFragment.java +++ b/app/src/main/java/com/github/mobile/ui/gist/GistFragment.java @@ -149,7 +149,8 @@ public void onViewCreated(View view, Bundle savedInstanceState) { progress = finder.find(R.id.pb_loading); Activity activity = getActivity(); - adapter = new HeaderFooterListAdapter(list, new CommentListAdapter(activity, avatars, imageGetter)); + adapter = new HeaderFooterListAdapter(list, + new CommentListAdapter(activity.getLayoutInflater(), avatars, imageGetter)); list.setAdapter(adapter); } diff --git a/app/src/main/java/com/github/mobile/ui/issue/EventListAdapter.java b/app/src/main/java/com/github/mobile/ui/issue/EventListAdapter.java new file mode 100644 index 00000000..81617740 --- /dev/null +++ b/app/src/main/java/com/github/mobile/ui/issue/EventListAdapter.java @@ -0,0 +1,350 @@ +/* + * Copyright 2012 GitHub Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.mobile.ui.issue; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Resources; +import android.text.Html; +import android.text.method.LinkMovementMethod; +import android.view.View; + +import com.github.kevinsawicki.wishlist.MultiTypeAdapter; +import com.github.mobile.R; +import com.github.mobile.api.model.Issue; +import com.github.mobile.api.model.TimelineEvent; +import com.github.mobile.api.model.User; +import com.github.mobile.ui.user.UserViewActivity; +import com.github.mobile.util.AvatarLoader; +import com.github.mobile.util.HttpImageGetter; +import com.github.mobile.util.TimeUtils; +import com.github.mobile.util.TypefaceUtils; + +import java.util.Collection; + +/** + * Adapter for a list of {@link TimelineEvent} objects + */ +public class EventListAdapter extends MultiTypeAdapter { + private static final int VIEW_COMMENT = 0; + private static final int VIEW_EVENT = 1; + private static final int VIEW_TOTAL = 2; + + + private final Context context; + + private final Resources resources; + + private final AvatarLoader avatars; + + private final HttpImageGetter imageGetter; + + private final IssueFragment issueFragment; + + private final String user; + + private final boolean isCollaborator; + + + /** + * Create list adapter + * + * @param activity + * @param avatars + * @param imageGetter + * @param issueFragment + */ + public EventListAdapter(Activity activity, AvatarLoader avatars, + HttpImageGetter imageGetter, IssueFragment issueFragment, + boolean isCollaborator, String loggedUser) { + super(activity.getLayoutInflater()); + + this.context = activity; + this.resources = activity.getResources(); + this.avatars = avatars; + this.imageGetter = imageGetter; + this.issueFragment = issueFragment; + this.isCollaborator = isCollaborator; + this.user = loggedUser; + } + + @Override + protected void update(int position, Object obj, int type) { + switch (type) { + case VIEW_COMMENT: + updateComment((TimelineEvent) obj); + break; + case VIEW_EVENT: + updateEvent((TimelineEvent) obj); + break; + } + } + + private void updateEvent(final TimelineEvent event) { + String eventString = event.event; + + User actor; + if (eventString.equals(TimelineEvent.EVENT_ASSIGNED) || eventString.equals(TimelineEvent.EVENT_UNASSIGNED)) { + actor = event.assignee; + } else { + actor = event.actor; + } + + String message = String.format("%s ", actor == null ? "ghost" : actor.login); + if (actor != null) { + setGone(2, false); + avatars.bind(imageView(2), actor); + } else { + setGone(2, true); + } + + switch (eventString) { + case TimelineEvent.EVENT_ASSIGNED: + int assignedTextResource = R.string.issue_event_label_assigned; + if (event.actor.id == event.assignee.id) { + assignedTextResource = R.string.issue_event_label_self_assigned; + } + message += String.format(resources.getString(assignedTextResource), "" + event.actor.login + ""); + setText(0, TypefaceUtils.ICON_PERSON); + textView(0).setTextColor(resources.getColor(R.color.issue_event_normal)); + break; + case TimelineEvent.EVENT_UNASSIGNED: + int unassignedTextResource = R.string.issue_event_label_unassigned; + if (event.actor.id == event.assignee.id) { + unassignedTextResource = R.string.issue_event_label_self_unassigned; + } + message += String.format(resources.getString(unassignedTextResource), "" + event.actor.login + ""); + setText(0, TypefaceUtils.ICON_PERSON); + textView(0).setTextColor(resources.getColor(R.color.issue_event_normal)); + break; + case TimelineEvent.EVENT_LABELED: + message += String.format(resources.getString(R.string.issue_event_label_added), "" + event.label.name + ""); + setText(0, TypefaceUtils.ICON_TAG); + textView(0).setTextColor(resources.getColor(R.color.issue_event_normal)); + break; + case TimelineEvent.EVENT_UNLABELED: + message += String.format(resources.getString(R.string.issue_event_label_removed), "" + event.label.name + ""); + setText(0, TypefaceUtils.ICON_TAG); + textView(0).setTextColor(resources.getColor(R.color.issue_event_normal)); + break; + case TimelineEvent.EVENT_REFERENCED: + message += String.format(resources.getString(R.string.issue_event_referenced), "" + event.commit_id.substring(0,7) + ""); + setText(0, TypefaceUtils.ICON_BOOKMARK); + textView(0).setTextColor(resources.getColor(R.color.issue_event_normal)); + break; + case TimelineEvent.EVENT_CROSS_REFERENCED: + Issue issue = event.source.issue; + String crossRef = issue.repository.full_name + "#" + issue.number; + message += String.format(resources.getString(R.string.issue_event_cross_referenced), "" + crossRef + ""); + setText(0, TypefaceUtils.ICON_BOOKMARK); + textView(0).setTextColor(resources.getColor(R.color.issue_event_normal)); + break; + case TimelineEvent.EVENT_REVIEW_REQUESTED: + message += String.format(resources.getString(R.string.issue_event_review_requested), "" + event.requested_reviewer.login + ""); + setText(0, TypefaceUtils.ICON_EYE); + textView(0).setTextColor(resources.getColor(R.color.issue_event_normal)); + break; + case TimelineEvent.EVENT_REVIEW_REQUEST_REMOVED: + message += String.format(resources.getString(R.string.issue_event_review_request_removed), "" + event.requested_reviewer.login + ""); + setText(0, TypefaceUtils.ICON_X); + textView(0).setTextColor(resources.getColor(R.color.issue_event_normal)); + break; + case TimelineEvent.EVENT_MILESTONED: + message += String.format(resources.getString(R.string.issue_event_milestone_added), "" + event.milestone.title + ""); + setText(0, TypefaceUtils.ICON_MILESTONE); + textView(0).setTextColor(resources.getColor(R.color.issue_event_normal)); + break; + case TimelineEvent.EVENT_DEMILESTONED: + message += String.format(resources.getString(R.string.issue_event_milestone_removed), "" + event.milestone.title + ""); + setText(0, TypefaceUtils.ICON_MILESTONE); + textView(0).setTextColor(resources.getColor(R.color.issue_event_normal)); + break; + case TimelineEvent.EVENT_CLOSED: + if (event.commit_id == null) { + message += resources.getString(R.string.issue_event_closed); + } else { + message += String.format(resources.getString(R.string.issue_event_closed_from_commit), "" + event.commit_id.substring(0,7) + ""); + } + setText(0, TypefaceUtils.ICON_CIRCLE_SLASH); + textView(0).setTextColor(resources.getColor(R.color.issue_event_red)); + break; + case TimelineEvent.EVENT_REOPENED: + message += resources.getString(R.string.issue_event_reopened); + setText(0, TypefaceUtils.ICON_PRIMITIVE_DOT); + textView(0).setTextColor(resources.getColor(R.color.issue_event_green)); + break; + case TimelineEvent.EVENT_RENAMED: + message += String.format(resources.getString(R.string.issue_event_rename), + "" + event.rename.from + "", + "" + event.rename.to + ""); + setText(0, TypefaceUtils.ICON_PENCIL); + textView(0).setTextColor(resources.getColor(R.color.issue_event_normal)); + break; + case TimelineEvent.EVENT_MERGED: + message += String.format(resources.getString(R.string.issue_event_merged), "" + event.commit_id.substring(0,7) + ""); + setText(0, TypefaceUtils.ICON_GIT_MERGE); + textView(0).setTextColor(resources.getColor(R.color.issue_event_purple)); + break; + case TimelineEvent.EVENT_COMMITTED: + setGone(2, true); + message = String.format("%s ", event.author.name) + event.message; + setText(0, TypefaceUtils.ICON_GIT_COMMIT); + textView(0).setTextColor(resources.getColor(R.color.issue_event_normal)); + break; + case TimelineEvent.EVENT_LINE_COMMENTED: + message += resources.getString(R.string.issue_event_comment_diff); + setText(0, TypefaceUtils.ICON_CODE); + textView(0).setTextColor(resources.getColor(R.color.issue_event_light)); + break; + case TimelineEvent.EVENT_LOCKED: + message += resources.getString(R.string.issue_event_lock); + setText(0, TypefaceUtils.ICON_LOCK); + textView(0).setTextColor(resources.getColor(R.color.issue_event_dark)); + break; + case TimelineEvent.EVENT_UNLOCKED: + message += resources.getString(R.string.issue_event_unlock); + setText(0, TypefaceUtils.ICON_KEY); + textView(0).setTextColor(resources.getColor(R.color.issue_event_dark)); + break; + case TimelineEvent.EVENT_HEAD_REF_DELETED: + message += resources.getString(R.string.issue_event_head_ref_deleted); + setText(0, TypefaceUtils.ICON_GIT_BRANCH); + textView(0).setTextColor(resources.getColor(R.color.issue_event_light)); + break; + case TimelineEvent.EVENT_HEAD_REF_RESTORED: + message += resources.getString(R.string.issue_event_head_ref_restored); + setText(0, TypefaceUtils.ICON_GIT_BRANCH); + textView(0).setTextColor(resources.getColor(R.color.issue_event_light)); + break; + } + + if (event.created_at != null) { + message += " " + TimeUtils.getRelativeTime(event.created_at); + } + setText(1, Html.fromHtml(message)); + } + + private void updateComment(final TimelineEvent comment) { + imageGetter.bind(textView(0), comment.body_html, comment.id); + avatars.bind(imageView(4), comment.actor); + imageView(4).setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + context.startActivity(UserViewActivity.createIntent(comment.actor)); + } + }); + + setText(1, comment.actor == null ? "ghost" : comment.actor.login); + setText(2, TimeUtils.getRelativeTime(comment.created_at)); + setGone(3, !comment.updated_at.after(comment.created_at)); + + boolean canEdit = isCollaborator || + (comment.actor != null && comment.actor.login.equals(user)); + + if (canEdit) { + // Edit button + setGone(5, false); + view(5).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + issueFragment.editComment(comment.getOldCommentModel()); + } + }); + // Delete button + setGone(6, false); + view(6).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + issueFragment.deleteComment(comment.getOldCommentModel()); + } + }); + } else { + setGone(5, true); + setGone(6, true); + } + } + + public MultiTypeAdapter setItems(Collection items) { + if (items == null || items.isEmpty()) + return this; + + this.clear(); + + for (TimelineEvent item : items) { + if (TimelineEvent.EVENT_COMMENTED.equals(item.event)) { + this.addItem(VIEW_COMMENT, item); + } else { + this.addItem(VIEW_EVENT, item); + } + } + + notifyDataSetChanged(); + return this; + } + + @Override + protected View initialize(int type, View view) { + view = super.initialize(type, view); + + switch (type) { + case VIEW_COMMENT: + textView(view, 0).setMovementMethod(LinkMovementMethod.getInstance()); + TypefaceUtils.setOcticons(textView(view, 5), textView(view, 6)); + setText(view, 5, TypefaceUtils.ICON_PENCIL); + setText(view, 6, TypefaceUtils.ICON_X); + break; + case VIEW_EVENT: + TypefaceUtils.setOcticons(textView(view, 0)); + break; + } + + return view; + } + + @Override + public int getViewTypeCount() { + return VIEW_TOTAL; + } + + @Override + protected int getChildLayoutId(int type) { + if (type == VIEW_COMMENT) + return R.layout.comment_item; + else + return R.layout.comment_event_item; + } + + @Override + public boolean areAllItemsEnabled() { + return false; + } + + @Override + public boolean isEnabled(int position) { + return false; + } + + @Override + protected int[] getChildViewIds(int type) { + if(type == VIEW_COMMENT) + return new int[] { R.id.tv_comment_body, R.id.tv_comment_author, + R.id.tv_comment_date, R.id.tv_comment_edited, R.id.iv_avatar, + R.id.iv_comment_edit, R.id.iv_comment_delete }; + else + return new int[]{R.id.tv_event_icon, R.id.tv_event, R.id.iv_avatar}; + } +} diff --git a/app/src/main/java/com/github/mobile/ui/issue/IssueFragment.java b/app/src/main/java/com/github/mobile/ui/issue/IssueFragment.java index 5ddaa682..58397874 100644 --- a/app/src/main/java/com/github/mobile/ui/issue/IssueFragment.java +++ b/app/src/main/java/com/github/mobile/ui/issue/IssueFragment.java @@ -58,6 +58,7 @@ import com.github.kevinsawicki.wishlist.ViewUtils; import com.github.mobile.R; import com.github.mobile.accounts.AccountUtils; +import com.github.mobile.api.model.LineComment; import com.github.mobile.api.model.TimelineEvent; import com.github.mobile.api.model.ReactionSummary; import com.github.mobile.core.issue.DeleteCommentTask; @@ -76,7 +77,6 @@ import com.github.mobile.ui.ReactionsView; import com.github.mobile.ui.StyledText; import com.github.mobile.ui.UriLauncherActivity; -import com.github.mobile.ui.comment.CommentListAdapter; import com.github.mobile.ui.commit.CommitCompareViewActivity; import com.github.mobile.ui.user.UserViewActivity; import com.github.mobile.util.AvatarLoader; @@ -88,6 +88,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Date; import java.util.List; @@ -108,7 +109,7 @@ public class IssueFragment extends DialogFragment { private int issueNumber; - private List items; + private List items; private RepositoryId repositoryId; @@ -138,7 +139,7 @@ public class IssueFragment extends DialogFragment { private View footerView; - private HeaderFooterListAdapter adapter; + private HeaderFooterListAdapter adapter; private EditMilestoneTask milestoneTask; @@ -354,8 +355,8 @@ public void onClick(View v) { Activity activity = getActivity(); loggedUser = AccountUtils.getLogin(activity); - adapter = new HeaderFooterListAdapter(list, - new CommentListAdapter(activity, avatars, commentImageGetter, this, isCollaborator, loggedUser)); + adapter = new HeaderFooterListAdapter(list, + new EventListAdapter(activity, avatars, commentImageGetter, this, isCollaborator, loggedUser)); list.setAdapter(adapter); } @@ -496,41 +497,22 @@ protected void onSuccess(FullIssue fullIssue) throws Exception { issue = fullIssue.getIssue(); reactions = fullIssue.getReactions(); - List events = (List) fullIssue.getEvents(); - int numEvents = events.size(); - - List allItems = new ArrayList<>(); - - int start = 0; - for (Comment comment : fullIssue) { - for (int e = start; e < numEvents; e++) { - TimelineEvent event = events.get(e); - if (shouldAddEvent(event, allItems)) { - if (comment.getCreatedAt().after(event.created_at)) { - allItems.add(event); - start = e + 1; - } else { - e = numEvents; - } - } - } - allItems.add(comment); - } + Collection allItems = fullIssue.getEvents(); - // Adding the last events or if there are no comments - for (int e = start; e < numEvents; e++) { - TimelineEvent event = events.get(e); - if (shouldAddEvent(event, allItems)) - allItems.add(event); + List neededItems = new ArrayList<>(allItems.size()); + for (TimelineEvent event : allItems) { + if (shouldAddEvent(event, neededItems)) { + neededItems.add(event); + } } + items = neededItems; - items = allItems; - updateList(fullIssue.getIssue(), allItems); + updateList(issue, items); } }.execute(); } - private void updateList(Issue issue, List items) { + private void updateList(Issue issue, List items) { adapter.getWrappedAdapter().setItems(items); adapter.removeHeader(loadingView); @@ -616,15 +598,6 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { updateHeader(editedIssue); break; case COMMENT_CREATE: - Comment comment = (Comment) data - .getSerializableExtra(EXTRA_COMMENT); - if (items != null) { - items.add(comment); - issue.setComments(issue.getComments() + 1); - updateList(issue, items); - } else - refreshIssue(); - break; case COMMENT_EDIT: // TODO: update the commit without reloading the full issue refreshIssue(); @@ -728,17 +701,13 @@ public boolean onOptionsItemSelected(MenuItem item) { } } - static private boolean shouldAddEvent(TimelineEvent event, List allItems) { + static private boolean shouldAddEvent(TimelineEvent event, List allItems) { // Exclude some events List excludedEvents = Arrays.asList( TimelineEvent.EVENT_MENTIONED, TimelineEvent.EVENT_SUBSCRIBED, TimelineEvent.EVENT_UNSUBSCRIBED, - TimelineEvent.EVENT_LINE_COMMENTED, - TimelineEvent.EVENT_REVIEWED, - TimelineEvent.EVENT_COMMITTED, - TimelineEvent.EVENT_CROSS_REFERENCED, - TimelineEvent.EVENT_COMMENTED); + TimelineEvent.EVENT_REVIEWED); if (event == null || excludedEvents.contains(event.event)) return false; @@ -747,25 +716,35 @@ static private boolean shouldAddEvent(TimelineEvent event, List allItems if (TimelineEvent.EVENT_REFERENCED.equals(event.event) && event.commit_id == null) return false; + // Don't show empty line comments + if (TimelineEvent.EVENT_LINE_COMMENTED.equals(event.event)) { + if (event.comments == null || event.comments.isEmpty()) { + return false; + } + + // Populate some data for better visualization + LineComment comment = event.comments.get(0); + event.actor = comment.user; + event.created_at = comment.created_at; + } + int currentSize = allItems.size(); if (currentSize == 0) return true; - Object previousItem = allItems.get(currentSize - 1); - if (!(previousItem instanceof TimelineEvent)) - return true; + TimelineEvent previousItem = allItems.get(currentSize - 1); // Remove referenced event before a merge if (TimelineEvent.EVENT_MERGED.equals(event.event) && - TimelineEvent.EVENT_REFERENCED.equals(((TimelineEvent) previousItem).event) && - event.commit_id.equals(((TimelineEvent) previousItem).commit_id)) { + TimelineEvent.EVENT_REFERENCED.equals((previousItem).event) && + event.commit_id.equals((previousItem).commit_id)) { allItems.remove(currentSize - 1); return true; } // Don't show the close event after the merged event if (TimelineEvent.EVENT_CLOSED.equals(event.event) && - TimelineEvent.EVENT_MERGED.equals(((TimelineEvent) previousItem).event)) + TimelineEvent.EVENT_MERGED.equals((previousItem).event)) return false; return true; diff --git a/app/src/main/java/com/github/mobile/ui/user/UserViewActivity.java b/app/src/main/java/com/github/mobile/ui/user/UserViewActivity.java index fca965cf..d084264c 100644 --- a/app/src/main/java/com/github/mobile/ui/user/UserViewActivity.java +++ b/app/src/main/java/com/github/mobile/ui/user/UserViewActivity.java @@ -73,6 +73,17 @@ public class UserViewActivity extends TabPagerActivity public static Intent createIntent(User user) { return new Builder("user.VIEW").user(user).toIntent(); } + + /** + * Create intent for this activity + * + * @param user + * @return intent + */ + public static Intent createIntent(com.github.mobile.api.model.User user) { + return new Builder("user.VIEW").user(user.getOldUserModel()).toIntent(); + } + /** * Create intent for this activity and open the given tab * diff --git a/app/src/main/res/layout/comment_event.xml b/app/src/main/res/layout/comment_event.xml index 0e0211b4..11defc7c 100644 --- a/app/src/main/res/layout/comment_event.xml +++ b/app/src/main/res/layout/comment_event.xml @@ -21,5 +21,7 @@ + android:layout_marginLeft="16dp" + android:ellipsize="end" + android:maxLines="5" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6857ac15..d3104d86 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -111,6 +111,9 @@ removed this from the %1$s milestone merged commit %1$s referenced this issue from commit %1$s + referenced this issue in %1$s + requested review from %1$s + removed their request for review from %1$s closed this closed this from commit %1$s reopened this