Austin Huang
4 years ago
No known key found for this signature in database
GPG Key ID: 84C23AA04587A91F
34 changed files with 149 additions and 496 deletions
-
2app/src/main/java/awais/instagrabber/adapters/FeedAdapterV2.java
-
2app/src/main/java/awais/instagrabber/adapters/SliderItemsAdapter.java
-
4app/src/main/java/awais/instagrabber/adapters/viewholder/FeedGridItemViewHolder.java
-
2app/src/main/java/awais/instagrabber/adapters/viewholder/SearchItemViewHolder.java
-
2app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemMediaShareViewHolder.java
-
2app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemMediaViewHolder.java
-
4app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemRavenMediaViewHolder.java
-
4app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemReelShareViewHolder.java
-
2app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemStoryShareViewHolder.java
-
2app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java
-
2app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedItemViewHolder.java
-
2app/src/main/java/awais/instagrabber/asyncs/DiscoverPostFetchService.java
-
2app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java
-
2app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java
-
20app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java
-
2app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java
-
35app/src/main/java/awais/instagrabber/models/enums/FollowingType.kt
-
40app/src/main/java/awais/instagrabber/models/enums/MediaItemType.kt
-
57app/src/main/java/awais/instagrabber/repositories/responses/ChildCommentsFetchResponse.kt
-
57app/src/main/java/awais/instagrabber/repositories/responses/CommentsFetchResponse.kt
-
75app/src/main/java/awais/instagrabber/repositories/responses/Hashtag.kt
-
68app/src/main/java/awais/instagrabber/repositories/responses/LocationFeedResponse.kt
-
25app/src/main/java/awais/instagrabber/repositories/responses/LoginRequiredResponse.java
-
6app/src/main/java/awais/instagrabber/repositories/responses/Media.kt
-
38app/src/main/java/awais/instagrabber/repositories/responses/UserSearchResponse.kt
-
77app/src/main/java/awais/instagrabber/repositories/responses/discover/TopicCluster.kt
-
71app/src/main/java/awais/instagrabber/repositories/responses/discover/TopicalExploreFeedResponse.kt
-
4app/src/main/java/awais/instagrabber/utils/DMUtils.java
-
4app/src/main/java/awais/instagrabber/utils/DirectItemFactory.kt
-
10app/src/main/java/awais/instagrabber/utils/DownloadUtils.kt
-
2app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java
-
6app/src/main/java/awais/instagrabber/viewmodels/PostViewV2ViewModel.kt
-
4app/src/main/java/awais/instagrabber/webservices/FeedService.java
-
2app/src/main/java/awais/instagrabber/webservices/LocationService.java
@ -1,35 +1,24 @@ |
|||||
package awais.instagrabber.models.enums; |
|
||||
|
package awais.instagrabber.models.enums |
||||
|
|
||||
import com.google.gson.annotations.SerializedName; |
|
||||
|
import java.io.Serializable |
||||
|
import java.util.* |
||||
|
|
||||
import java.io.Serializable; |
|
||||
import java.util.HashMap; |
|
||||
import java.util.Map; |
|
||||
|
|
||||
public enum FollowingType implements Serializable { |
|
||||
@SerializedName("1") |
|
||||
|
enum class FollowingType(val id: Int) : Serializable { |
||||
FOLLOWING(1), |
FOLLOWING(1), |
||||
@SerializedName("0") |
|
||||
NOT_FOLLOWING(0); |
NOT_FOLLOWING(0); |
||||
|
|
||||
private final int id; |
|
||||
private static final Map<Integer, FollowingType> map = new HashMap<>(); |
|
||||
|
companion object { |
||||
|
private val map: MutableMap<Int, FollowingType> = mutableMapOf() |
||||
|
|
||||
static { |
|
||||
for (FollowingType type : FollowingType.values()) { |
|
||||
map.put(type.id, type); |
|
||||
} |
|
||||
|
@JvmStatic |
||||
|
fun valueOf(id: Int): FollowingType? { |
||||
|
return map[id] |
||||
} |
} |
||||
|
|
||||
FollowingType(final int id) { |
|
||||
this.id = id; |
|
||||
|
init { |
||||
|
for (type in values()) { |
||||
|
map[type.id] = type |
||||
} |
} |
||||
|
|
||||
public int getId() { |
|
||||
return id; |
|
||||
} |
} |
||||
|
|
||||
public static FollowingType valueOf(final int id) { |
|
||||
return map.get(id); |
|
||||
} |
} |
||||
} |
} |
@ -1,42 +1,26 @@ |
|||||
package awais.instagrabber.models.enums; |
|
||||
|
package awais.instagrabber.models.enums |
||||
|
|
||||
import com.google.gson.annotations.SerializedName; |
|
||||
|
import java.io.Serializable |
||||
|
|
||||
import java.io.Serializable; |
|
||||
import java.util.HashMap; |
|
||||
import java.util.Map; |
|
||||
|
|
||||
public enum MediaItemType implements Serializable { |
|
||||
@SerializedName("1") |
|
||||
|
enum class MediaItemType(val id: Int) : Serializable { |
||||
MEDIA_TYPE_IMAGE(1), |
MEDIA_TYPE_IMAGE(1), |
||||
@SerializedName("2") |
|
||||
MEDIA_TYPE_VIDEO(2), |
MEDIA_TYPE_VIDEO(2), |
||||
@SerializedName("8") |
|
||||
MEDIA_TYPE_SLIDER(8), |
MEDIA_TYPE_SLIDER(8), |
||||
@SerializedName("11") |
|
||||
MEDIA_TYPE_VOICE(11), |
MEDIA_TYPE_VOICE(11), |
||||
// 5 is arbitrary |
|
||||
@SerializedName("5") |
|
||||
MEDIA_TYPE_LIVE(5); |
|
||||
|
MEDIA_TYPE_LIVE(5); // arbitrary |
||||
|
|
||||
private final int id; |
|
||||
private static final Map<Integer, MediaItemType> map = new HashMap<>(); |
|
||||
|
companion object { |
||||
|
private val map: MutableMap<Int, MediaItemType> = mutableMapOf() |
||||
|
|
||||
static { |
|
||||
for (MediaItemType type : MediaItemType.values()) { |
|
||||
map.put(type.id, type); |
|
||||
} |
|
||||
|
@JvmStatic |
||||
|
fun valueOf(id: Int): MediaItemType? { |
||||
|
return map[id] |
||||
} |
} |
||||
|
|
||||
MediaItemType(final int id) { |
|
||||
this.id = id; |
|
||||
|
init { |
||||
|
for (type in values()) { |
||||
|
map[type.id] = type |
||||
} |
} |
||||
|
|
||||
public int getId() { |
|
||||
return id; |
|
||||
} |
} |
||||
|
|
||||
public static MediaItemType valueOf(final int id) { |
|
||||
return map.get(id); |
|
||||
} |
} |
||||
} |
} |
@ -1,51 +1,10 @@ |
|||||
package awais.instagrabber.repositories.responses; |
|
||||
|
package awais.instagrabber.repositories.responses |
||||
|
|
||||
import androidx.annotation.NonNull; |
|
||||
|
import awais.instagrabber.models.Comment |
||||
|
|
||||
import java.util.List; |
|
||||
|
|
||||
import awais.instagrabber.models.Comment; |
|
||||
|
|
||||
public class ChildCommentsFetchResponse { |
|
||||
private final int childCommentCount; |
|
||||
private final String nextMaxChildCursor; |
|
||||
private final List<Comment> childComments; |
|
||||
private final boolean hasMoreTailChildComments; |
|
||||
|
|
||||
public ChildCommentsFetchResponse(final int childCommentCount, |
|
||||
final String nextMaxChildCursor, |
|
||||
final List<Comment> childComments, |
|
||||
final boolean hasMoreTailChildComments) { |
|
||||
this.childCommentCount = childCommentCount; |
|
||||
this.nextMaxChildCursor = nextMaxChildCursor; |
|
||||
this.childComments = childComments; |
|
||||
this.hasMoreTailChildComments = hasMoreTailChildComments; |
|
||||
} |
|
||||
|
|
||||
public int getChildCommentCount() { |
|
||||
return childCommentCount; |
|
||||
} |
|
||||
|
|
||||
public String getNextMaxChildCursor() { |
|
||||
return nextMaxChildCursor; |
|
||||
} |
|
||||
|
|
||||
public boolean getHasMoreTailChildComments() { |
|
||||
return hasMoreTailChildComments; |
|
||||
} |
|
||||
|
|
||||
public List<Comment> getChildComments() { |
|
||||
return childComments; |
|
||||
} |
|
||||
|
|
||||
@NonNull |
|
||||
@Override |
|
||||
public String toString() { |
|
||||
return "ChildCommentsFetchResponse{" + |
|
||||
"childCommentCount=" + childCommentCount + |
|
||||
", nextMaxChildCursor='" + nextMaxChildCursor + '\'' + |
|
||||
", childComments=" + childComments + |
|
||||
", hasMoreTailChildComments=" + hasMoreTailChildComments + |
|
||||
'}'; |
|
||||
} |
|
||||
} |
|
||||
|
data class ChildCommentsFetchResponse( |
||||
|
val childCommentCount: Int, |
||||
|
val nextMaxChildCursor: String?, |
||||
|
val childComments: List<Comment>?, |
||||
|
val hasMoreTailChildComments: Boolean? |
||||
|
) |
@ -1,51 +1,10 @@ |
|||||
package awais.instagrabber.repositories.responses; |
|
||||
|
package awais.instagrabber.repositories.responses |
||||
|
|
||||
import androidx.annotation.NonNull; |
|
||||
|
import awais.instagrabber.models.Comment |
||||
|
|
||||
import java.util.List; |
|
||||
|
|
||||
import awais.instagrabber.models.Comment; |
|
||||
|
|
||||
public class CommentsFetchResponse { |
|
||||
private final int commentCount; |
|
||||
private final String nextMinId; |
|
||||
private final List<Comment> comments; |
|
||||
private final boolean hasMoreComments; |
|
||||
|
|
||||
public CommentsFetchResponse(final int commentCount, |
|
||||
final String nextMinId, |
|
||||
final List<Comment> comments, |
|
||||
final boolean hasMoreComments) { |
|
||||
this.commentCount = commentCount; |
|
||||
this.nextMinId = nextMinId; |
|
||||
this.comments = comments; |
|
||||
this.hasMoreComments = hasMoreComments; |
|
||||
} |
|
||||
|
|
||||
public int getCommentCount() { |
|
||||
return commentCount; |
|
||||
} |
|
||||
|
|
||||
public String getNextMinId() { |
|
||||
return nextMinId; |
|
||||
} |
|
||||
|
|
||||
public List<Comment> getComments() { |
|
||||
return comments; |
|
||||
} |
|
||||
|
|
||||
public boolean getHasMoreComments() { |
|
||||
return hasMoreComments; |
|
||||
} |
|
||||
|
|
||||
@NonNull |
|
||||
@Override |
|
||||
public String toString() { |
|
||||
return "CommentsFetchResponse{" + |
|
||||
"commentCount=" + commentCount + |
|
||||
", nextMinId='" + nextMinId + '\'' + |
|
||||
", comments=" + comments + |
|
||||
", hasMoreComments=" + hasMoreComments + |
|
||||
'}'; |
|
||||
} |
|
||||
} |
|
||||
|
data class CommentsFetchResponse( |
||||
|
val commentCount: Int, |
||||
|
val nextMinId: String?, |
||||
|
val comments: List<Comment>?, |
||||
|
val hasMoreComments: Boolean |
||||
|
) |
@ -1,63 +1,12 @@ |
|||||
package awais.instagrabber.repositories.responses; |
|
||||
|
|
||||
import java.io.Serializable; |
|
||||
import java.util.Objects; |
|
||||
|
|
||||
import awais.instagrabber.models.enums.FollowingType; |
|
||||
|
|
||||
public final class Hashtag implements Serializable { |
|
||||
private final FollowingType following; // 0 false 1 true; not on search results |
|
||||
private final long mediaCount; |
|
||||
private final String id; |
|
||||
private final String name; |
|
||||
private final String searchResultSubtitle; // shows how many posts there are on search results |
|
||||
|
|
||||
public Hashtag(final String id, |
|
||||
final String name, |
|
||||
final long mediaCount, |
|
||||
final FollowingType following, |
|
||||
final String searchResultSubtitle) { |
|
||||
this.id = id; |
|
||||
this.name = name; |
|
||||
this.mediaCount = mediaCount; |
|
||||
this.following = following; |
|
||||
this.searchResultSubtitle = searchResultSubtitle; |
|
||||
} |
|
||||
|
|
||||
public String getId() { |
|
||||
return id; |
|
||||
} |
|
||||
|
|
||||
public String getName() { |
|
||||
return name; |
|
||||
} |
|
||||
|
|
||||
public Long getMediaCount() { |
|
||||
return mediaCount; |
|
||||
} |
|
||||
|
|
||||
public FollowingType getFollowing() { |
|
||||
return following; |
|
||||
} |
|
||||
|
|
||||
public String getSubtitle() { |
|
||||
return searchResultSubtitle; |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public boolean equals(final Object o) { |
|
||||
if (this == o) return true; |
|
||||
if (o == null || getClass() != o.getClass()) return false; |
|
||||
final Hashtag hashtag = (Hashtag) o; |
|
||||
return mediaCount == hashtag.mediaCount && |
|
||||
following == hashtag.following && |
|
||||
Objects.equals(id, hashtag.id) && |
|
||||
Objects.equals(name, hashtag.name) && |
|
||||
Objects.equals(searchResultSubtitle, hashtag.searchResultSubtitle); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public int hashCode() { |
|
||||
return Objects.hash(following, mediaCount, id, name, searchResultSubtitle); |
|
||||
} |
|
||||
} |
|
||||
|
package awais.instagrabber.repositories.responses |
||||
|
|
||||
|
import awais.instagrabber.models.enums.FollowingType |
||||
|
import java.io.Serializable |
||||
|
|
||||
|
data class Hashtag( |
||||
|
val id: String, |
||||
|
val name: String, |
||||
|
val mediaCount: Long, |
||||
|
val following: FollowingType?, // 0 false 1 true; not on search results |
||||
|
val searchResultSubtitle: String? // shows how many posts there are on search results |
||||
|
) : Serializable |
@ -1,57 +1,11 @@ |
|||||
package awais.instagrabber.repositories.responses; |
|
||||
|
|
||||
import java.util.List; |
|
||||
|
|
||||
public class LocationFeedResponse { |
|
||||
private final int numResults; |
|
||||
private final String nextMaxId; |
|
||||
private final boolean moreAvailable; |
|
||||
private final long mediaCount; |
|
||||
private final String status; |
|
||||
private final List<Media> items; |
|
||||
private final Location location; |
|
||||
|
|
||||
public LocationFeedResponse(final int numResults, |
|
||||
final String nextMaxId, |
|
||||
final boolean moreAvailable, |
|
||||
final long mediaCount, |
|
||||
final String status, |
|
||||
final List<Media> items, |
|
||||
final Location location) { |
|
||||
this.numResults = numResults; |
|
||||
this.nextMaxId = nextMaxId; |
|
||||
this.moreAvailable = moreAvailable; |
|
||||
this.mediaCount = mediaCount; |
|
||||
this.status = status; |
|
||||
this.items = items; |
|
||||
this.location = location; |
|
||||
} |
|
||||
|
|
||||
public int getNumResults() { |
|
||||
return numResults; |
|
||||
} |
|
||||
|
|
||||
public String getNextMaxId() { |
|
||||
return nextMaxId; |
|
||||
} |
|
||||
|
|
||||
public boolean isMoreAvailable() { |
|
||||
return moreAvailable; |
|
||||
} |
|
||||
|
|
||||
public String getStatus() { |
|
||||
return status; |
|
||||
} |
|
||||
|
|
||||
public List<Media> getItems() { |
|
||||
return items; |
|
||||
} |
|
||||
|
|
||||
public long getMediaCount() { |
|
||||
return mediaCount; |
|
||||
} |
|
||||
|
|
||||
public Location getLocation() { |
|
||||
return location; |
|
||||
} |
|
||||
} |
|
||||
|
package awais.instagrabber.repositories.responses |
||||
|
|
||||
|
data class LocationFeedResponse( |
||||
|
val numResults: Int, |
||||
|
val nextMaxId: String?, |
||||
|
val moreAvailable: Boolean?, |
||||
|
val mediaCount: Long?, |
||||
|
val status: String, |
||||
|
val items: List<Media>?, |
||||
|
val location: Location |
||||
|
) |
@ -1,25 +0,0 @@ |
|||||
package awais.instagrabber.repositories.responses; |
|
||||
|
|
||||
public class LoginRequiredResponse { |
|
||||
private String message = "login_required"; |
|
||||
private int logoutReason; |
|
||||
private String status = "fail"; |
|
||||
|
|
||||
public LoginRequiredResponse(final String message, final int logoutReason, final String status) { |
|
||||
this.message = message; |
|
||||
this.logoutReason = logoutReason; |
|
||||
this.status = status; |
|
||||
} |
|
||||
|
|
||||
public String getMessage() { |
|
||||
return message; |
|
||||
} |
|
||||
|
|
||||
public int getLogoutReason() { |
|
||||
return logoutReason; |
|
||||
} |
|
||||
|
|
||||
public String getStatus() { |
|
||||
return status; |
|
||||
} |
|
||||
} |
|
@ -1,33 +1,7 @@ |
|||||
package awais.instagrabber.repositories.responses; |
|
||||
|
package awais.instagrabber.repositories.responses |
||||
|
|
||||
import java.util.List; |
|
||||
|
|
||||
public class UserSearchResponse { |
|
||||
private final int numResults; |
|
||||
private final List<User> users; |
|
||||
private final boolean hasMore; |
|
||||
private final String status; |
|
||||
|
|
||||
public UserSearchResponse(final int numResults, final List<User> users, final boolean hasMore, final String status) { |
|
||||
this.numResults = numResults; |
|
||||
this.users = users; |
|
||||
this.hasMore = hasMore; |
|
||||
this.status = status; |
|
||||
} |
|
||||
|
|
||||
public int getNumResults() { |
|
||||
return numResults; |
|
||||
} |
|
||||
|
|
||||
public List<User> getUsers() { |
|
||||
return users; |
|
||||
} |
|
||||
|
|
||||
public boolean hasMore() { |
|
||||
return hasMore; |
|
||||
} |
|
||||
|
|
||||
public String getStatus() { |
|
||||
return status; |
|
||||
} |
|
||||
} |
|
||||
|
data class UserSearchResponse( |
||||
|
val numResults: Int, |
||||
|
val users: List<User>?, |
||||
|
val status: String |
||||
|
) |
@ -1,63 +1,14 @@ |
|||||
package awais.instagrabber.repositories.responses.discover; |
|
||||
|
|
||||
import java.io.Serializable; |
|
||||
|
|
||||
import awais.instagrabber.repositories.responses.Media; |
|
||||
|
|
||||
public class TopicCluster implements Serializable { |
|
||||
private final String id; |
|
||||
private final String title; |
|
||||
private final String type; |
|
||||
private final boolean canMute; |
|
||||
private final boolean isMuted; |
|
||||
private final int rankedPosition; |
|
||||
private Media coverMedia; |
|
||||
|
|
||||
public TopicCluster(final String id, |
|
||||
final String title, |
|
||||
final String type, |
|
||||
final boolean canMute, |
|
||||
final boolean isMuted, |
|
||||
final int rankedPosition, |
|
||||
final Media coverMedia) { |
|
||||
this.id = id; |
|
||||
this.title = title; |
|
||||
this.type = type; |
|
||||
this.canMute = canMute; |
|
||||
this.isMuted = isMuted; |
|
||||
this.rankedPosition = rankedPosition; |
|
||||
this.coverMedia = coverMedia; |
|
||||
} |
|
||||
|
|
||||
public String getId() { |
|
||||
return id; |
|
||||
} |
|
||||
|
|
||||
public String getTitle() { |
|
||||
return title; |
|
||||
} |
|
||||
|
|
||||
public String getType() { |
|
||||
return type; |
|
||||
} |
|
||||
|
|
||||
public boolean isCanMute() { |
|
||||
return canMute; |
|
||||
} |
|
||||
|
|
||||
public boolean isMuted() { |
|
||||
return isMuted; |
|
||||
} |
|
||||
|
|
||||
public int getRankedPosition() { |
|
||||
return rankedPosition; |
|
||||
} |
|
||||
|
|
||||
public Media getCoverMedia() { |
|
||||
return coverMedia; |
|
||||
} |
|
||||
|
|
||||
public void setCoverMedia(final Media coverMedia) { |
|
||||
this.coverMedia = coverMedia; |
|
||||
} |
|
||||
} |
|
||||
|
package awais.instagrabber.repositories.responses.discover |
||||
|
|
||||
|
import awais.instagrabber.repositories.responses.Media |
||||
|
import java.io.Serializable |
||||
|
|
||||
|
data class TopicCluster( |
||||
|
val id: String, |
||||
|
val title: String, |
||||
|
val type: String?, |
||||
|
val canMute: Boolean?, |
||||
|
val isMuted: Boolean?, |
||||
|
val rankedPosition: Int, |
||||
|
var coverMedia: Media? |
||||
|
) : Serializable |
@ -1,58 +1,13 @@ |
|||||
package awais.instagrabber.repositories.responses.discover; |
|
||||
|
|
||||
import java.util.List; |
|
||||
import awais.instagrabber.repositories.responses.WrappedMedia; |
|
||||
|
|
||||
public class TopicalExploreFeedResponse { |
|
||||
private final boolean moreAvailable; |
|
||||
private final String nextMaxId; |
|
||||
private final String maxId; |
|
||||
private final String status; |
|
||||
private final int numResults; |
|
||||
private final List<TopicCluster> clusters; |
|
||||
private final List<WrappedMedia> items; |
|
||||
|
|
||||
public TopicalExploreFeedResponse(final boolean moreAvailable, |
|
||||
final String nextMaxId, |
|
||||
final String maxId, |
|
||||
final String status, |
|
||||
final int numResults, |
|
||||
final List<TopicCluster> clusters, |
|
||||
final List<WrappedMedia> items) { |
|
||||
this.moreAvailable = moreAvailable; |
|
||||
this.nextMaxId = nextMaxId; |
|
||||
this.maxId = maxId; |
|
||||
this.status = status; |
|
||||
this.numResults = numResults; |
|
||||
this.clusters = clusters; |
|
||||
this.items = items; |
|
||||
} |
|
||||
|
|
||||
public boolean isMoreAvailable() { |
|
||||
return moreAvailable; |
|
||||
} |
|
||||
|
|
||||
public String getNextMaxId() { |
|
||||
return nextMaxId; |
|
||||
} |
|
||||
|
|
||||
public String getMaxId() { |
|
||||
return maxId; |
|
||||
} |
|
||||
|
|
||||
public String getStatus() { |
|
||||
return status; |
|
||||
} |
|
||||
|
|
||||
public int getNumResults() { |
|
||||
return numResults; |
|
||||
} |
|
||||
|
|
||||
public List<TopicCluster> getClusters() { |
|
||||
return clusters; |
|
||||
} |
|
||||
|
|
||||
public List<WrappedMedia> getItems() { |
|
||||
return items; |
|
||||
} |
|
||||
} |
|
||||
|
package awais.instagrabber.repositories.responses.discover |
||||
|
|
||||
|
import awais.instagrabber.repositories.responses.WrappedMedia |
||||
|
|
||||
|
data class TopicalExploreFeedResponse( |
||||
|
val moreAvailable: Boolean, |
||||
|
val nextMaxId: String?, |
||||
|
val maxId: String?, |
||||
|
val status: String, |
||||
|
val numResults: Int, |
||||
|
val clusters: List<TopicCluster>?, |
||||
|
val items: List<WrappedMedia>? |
||||
|
) |
Write
Preview
Loading…
Cancel
Save
Reference in new issue